I need to write a script that will search through a CSV file, and perform certain search functions on it;
Now, I have no problem at all coding this procedurally, but as I am now moving on to Object Orientated Programming, I would like to use classes and instances of objects instead.
However, thinking in OOP doesn't come naturally to me yet, so I'm not entirely sure which way to go. I'm not looking for specific code, but rather suggestions on how I could design the script.
My current thinking is this;
How it would function in index.php:
The problem I see with this approach is this;
Should I instead go like this?
My main issue with this is that it appears that I may need multiple search objects and iterate through this within my loop class.
Any help would be much appreciated. I'm very new to OOP, and while I understand the individual parts, I'm not yet able to see the bigger picture. I may be overcomplicating what it is I'm trying to do, or there may be a much simpler way that I can't see yet.
How to handle CSV file with PHP. The function that parses the CSV file is fgetcsv, with the following syntax: fgetcsv("filename. csv", 1000, ",");
To search a CSV file, the best option is to use csvReadArray to import the whole CSV file into a 2-dimensional array. Then you can use the Javascript “findIndex” command to search the array. The findIndex() method returns the index of the first element in an array that pass a test (provided as a function).
Relative file paths take into account the current folder. For example, to instead write to a folder within the current folder you are able to specify f'mycsvfolder/{ColumnName}. csv' and the file will be written to the specified folder in the current directory. It's with this method that writing f'{ColumnName}.
PHP already offers a way to read a CSV file in an OO manner with SplFileObject:
$file = new SplFileObject("data.csv");
// tell object that it is reading a CSV file
$file->setFlags(SplFileObject::READ_CSV);
$file->setCsvControl(',', '"', '\\');
// iterate over the data
foreach ($file as $row) {
list ($fruit, $quantity) = $row;
// Do something with values
}
Since SplFileObject streams over the CSV data, the memory consumption is quite low and you can efficiently handle large CSV files, but since it is file i/o, it is not the fastest. However, an SplFileObject implements the Iterator interface, so you can wrap that $file instance into other iterators to modify the iteration. For instance, to limit file i/o, you could wrap it into a CachingIterator:
$cachedFile = new CachingIterator($file, CachingIterator::FULL_CACHE);
To fill the cache, you iterate over the $cachedFile. This will fill the cache
foreach ($cachedFile as $row) {
To iterate over the cache then, you do
foreach ($cachedFile->getCache() as $row) {
The tradeoff is increased memory obviously.
Now, to do your queries, you could wrap that CachingIterator or the SplFileObject into a FilterIterator which would limit the output when iterating over the csv data
class BannedEntriesFilter extends FilterIterator
{
private $bannedEntries = array();
public function setBannedEntries(array $bannedEntries)
{
$this->bannedEntries = $bannedEntries;
}
public function accept()
{
foreach ($this->current() as $key => $val) {
return !$this->isBannedEntryInColumn($val, $key);
}
}
public function $isBannedEntryInColumn($entry, $column)
{
return isset($this->bannedEntries[$column])
&& in_array($this->bannedEntries[$column], $entry);
}
}
A FilterIterator will omit all entries from the inner Iterator which does not satisfy the test in the FilterIterator's accept method. Above, we check the current row from the csv file against an array of banned entries and if it matches, the data is not included in the iteration. You use it like this:
$filteredCachedFile = new BannedEntriesFilter(
new ArrayIterator($cachedFile->getCache())
)
Since the cached results are always an Array, we need to wrap that Array into an ArrayIterator before we can wrap it into our FilterIterator. Note that to use the cache, you also need to iterate the CachingIterator at least once. We just assume you already did that above. The next step is to configure the banned entries
$filteredCachedFile->setBannedEntries(
array(
// banned entries for column 0
array('foo', 'bar'),
// banned entries for column 1
array( …
)
);
I guess that's rather straightforward. You have a multidimensional array with one entry for each column in the CSV data holding the banned entries. You then simply iterate over the instance and it will give you only the rows not having banned entries
foreach ($filteredCachedFile as $row) {
// do something with filtered rows
}
or, if you just want to get the results into an array:
$results = iterator_to_array($filteredCachedFile);
You can stack multiple FilterIterators to further limit the results. If you dont feel like writing a class for each filtering, have a look at the CallbackFilterIterator, which allows passing of the accept logic at runtime:
$filteredCachedFile = new CallbackFilterIterator(
new ArrayIterator($cachedFile->getCache()),
function(array $row) {
static $bannedEntries = array(
array('foo', 'bar'),
…
);
foreach ($row as $key => $val) {
// logic from above returning boolean if match is found
}
}
);
I 'm going to illustrate a reasonable approach to designing OOP code that serves your stated needs. While I firmly believe that the ideas presented below are sound, please be aware that:
A highly engineered solution would start by trying to define the interface to the data. That is, think about what would be a representation of the data that allows you to perform all your query operations. Here's one that would work:
This definition is enough to implement all three types of queries you mention by looping over the rows and performing some type of test on the values of a particular column.
The next move is to define an interface that describes the above in code. A not particularly nice but still adequate approach would be:
interface IDataSet {
public function getRowCount();
public function getValueAt($row, $column);
}
Now that this part is done, you can go and define a concrete class that implements this interface and can be used in your situation:
class InMemoryDataSet implements IDataSet {
private $_data = array();
public function __construct(array $data) {
$this->_data = $data;
}
public function getRowCount() {
return count($this->_data);
}
public function getValueAt($row, $column) {
if ($row >= $this->getRowCount()) {
throw new OutOfRangeException();
}
return isset($this->_data[$row][$column])
? $this->_data[$row][$column]
: null;
}
}
The next step is to go and write some code that converts your input data to some kind of IDataSet
:
function CSVToDataSet($file) {
return new InMemoryDataSet(array_map('str_getcsv', file($file)));
}
Now you can trivially create an IDataSet
from a CSV file, and you know that you can perform your queries on it because IDataSet
was explicitly designed for that purpose. You 're almost there.
The only thing missing is creating a reusable class that can perform your queries on an IDataSet
. Here is one of them:
class DataQuery {
private $_dataSet;
public function __construct(IDataSet $dataSet) {
$this->_dataSet = $dataSet;
}
public static function getRowsWithDuplicates($columnIndex) {
$values = array();
for ($i = 0; $i < $this->_dataSet->getRowCount(); ++$i) {
$values[$this->_dataSet->->getValueAt($i, $columnIndex)][] = $i;
}
return array_filter($values, function($row) { return count($row) > 1; });
}
}
This code will return an array where the keys are values in your CSV data and the values are arrays with the zero-based indexes of the rows where each value appears. Since only duplicate values are returned, each array will have at least two elements.
So at this point you are ready to go:
$dataSet = CSVToDataSet("data.csv");
$query = new DataQuery($dataSet);
$dupes = $query->getRowsWithDuplicates(0);
Clean, maintainable code that supports being modified in the future without requiring edits all over your application.
If you want to add more query operations, add them to DataQuery
and you can instantly use them on all concrete types of data sets. The data set and any other external code will not need any modifications.
If you want to change the internal representation of the data, modify InMemoryDataSet
accordingly or create another class that implements IDataSet
and use that one instead from CSVToDataSet
. The query class and any other external code will not need any modifications.
If you need to change the definition of the data set (perhaps to allow more types of queries to be performed efficiently) then you have to modify IDataSet
, which also brings all the concrete data set classes into the picture and probably DataQuery
as well. While this won't be the end of the world, it's exactly the kind of thing you would want to avoid.
And this is precisely the reason why I suggested to start from this: If you come up with a good definition for the data set, everything else will just fall into place.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With