我需要编写一个脚本来搜索CSV文件,并在其上执行某些搜索功能;
现在,我在编写程序时没有任何问题,但是当我现在转向面向对象编程时,我想使用对象的类和实例。
然而,在OOP中的思考对我来说并不是很自然,所以我不完全确定要走哪条路。我不是在寻找具体的代码,而是在寻找如何设计脚本的建议。
我目前的想法是这样的;
它如何在index.php中起作用:
我用这种方法看到的问题是这个;
我应该这样做吗?
我的主要问题是看起来我可能需要多个搜索对象并在我的循环类中迭代这个。
非常感谢任何帮助。我对OOP很新,虽然我了解各个部分,但我还没有看到更大的图景。我可能会过度复杂化我正在尝试做的事情,或者可能有一种我看不到的更简单的方式。
答案 0 :(得分:11)
PHP已经提供了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
}
由于SplFileObject流式传输CSV数据,因此内存消耗非常低,您可以有效地处理大型CSV文件,但由于它是文件i / o,因此不是最快的。但是,SplFileObject实现了Iterator接口,因此您可以将该$ file实例包装到其他迭代器中以修改迭代。例如,要限制文件i / o,可以将其包装到CachingIterator中:
$cachedFile = new CachingIterator($file, CachingIterator::FULL_CACHE);
要填充缓存,请迭代$ cachedFile。这将填充缓存
foreach ($cachedFile as $row) {
要迭代缓存,然后执行
foreach ($cachedFile->getCache() as $row) {
权衡显然是增加了记忆力。
现在,要进行查询,可以将CachingIterator或SplFileObject包装到FilterIterator中,这会在迭代csv数据时限制输出
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);
}
}
FilterIterator将省略内部迭代器中的所有条目,这些条目不满足FilterIterator的accept方法中的测试。在上面,我们根据禁止条目数组检查csv文件中的当前行,如果匹配,则数据不包含在迭代中。你这样使用它:
$filteredCachedFile = new BannedEntriesFilter(
new ArrayIterator($cachedFile->getCache())
)
由于缓存的结果总是一个Array,我们需要先将该Array包装到ArrayIterator中,然后才能将它包装到FilterIterator中。请注意,要使用缓存,您还需要至少迭代一次CachingIterator。我们假设您已经完成了上述操作。下一步是配置禁止的条目
$filteredCachedFile->setBannedEntries(
array(
// banned entries for column 0
array('foo', 'bar'),
// banned entries for column 1
array( …
)
);
我想这很简单。您有一个多维数组,其中包含禁止条目的CSV数据中每列的一个条目。然后,您只需遍历实例,它将只为您提供没有禁止条目的行
foreach ($filteredCachedFile as $row) {
// do something with filtered rows
}
或者,如果您只想将结果放入数组中:
$results = iterator_to_array($filteredCachedFile);
您可以堆叠多个FilterIterators以进一步限制结果。如果您不想为每个过滤编写一个类,请查看CallbackFilterIterator,它允许在运行时传递接受逻辑:
$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
}
}
);
答案 1 :(得分:2)
我将说明设计符合您所述需求的OOP代码的合理方法。虽然我坚信下面提出的想法是合理的,但请注意:
高度设计的解决方案将首先尝试定义数据接口。也就是说,考虑一下可以表示允许您执行所有查询操作的数据。这是一个可行的方法:
这个定义足以通过循环遍历行并对特定列的值执行某种类型的测试来实现您提到的所有三种类型的查询。
下一步是定义一个在代码中描述上述内容的接口。一个不是特别好但仍然适当的方法是:
interface IDataSet {
public function getRowCount();
public function getValueAt($row, $column);
}
既然这部分已经完成,你可以去定义一个实现这个接口的具体类,并且可以在你的情况下使用:
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;
}
}
下一步是编写一些代码,将输入数据转换为某种IDataSet
:
function CSVToDataSet($file) {
return new InMemoryDataSet(array_map('str_getcsv', file($file)));
}
现在,您可以从CSV文件中轻松创建IDataSet
,并且您知道可以对其执行查询,因为IDataSet
是为此目的明确设计的。你快到了。
唯一缺少的是创建一个可以在IDataSet
上执行查询的可重用类。这是其中之一:
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; });
}
}
此代码将返回一个数组,其中键是CSV数据中的值,值是具有每个值出现的行的从零开始的索引的数组。由于只返回重复值,因此每个数组至少包含两个元素。
所以此时你准备好了:
$dataSet = CSVToDataSet("data.csv");
$query = new DataQuery($dataSet);
$dupes = $query->getRowsWithDuplicates(0);
清晰,可维护的代码,支持将来修改,无需在整个应用程序中进行编辑。
如果要添加更多查询操作,请将它们添加到DataQuery
,您可以立即在所有具体类型的数据集上使用它们。数据集和任何其他外部代码不需要任何修改。
如果要更改数据的内部表示,请相应地修改InMemoryDataSet
或创建另一个实现IDataSet
的类,然后使用CSVToDataSet
中的那个。查询类和任何其他外部代码不需要任何修改。
如果您需要更改数据集的定义(可能允许更有效地执行更多类型的查询),那么您必须修改IDataSet
,这也会带来所有具体的数据集类到图片中,也可能是DataQuery
。虽然这不是世界末日,但这正是你想要避免的事情。
这正是我建议从这个开始的原因:如果你为数据集提出了一个很好的定义,那么其他一切都将落实到位。
答案 2 :(得分:2)
你实际上选择了一个学习OOP的坏榜样。因为,您正在寻找“导入”和“搜索”文件的功能,最好以程序方式实现,而不是以面向对象的方式实现。请记住,世界上并非一切都是“对象”。除了对象之外,我们还有“过程”,“操作”等。您仍然可以使用类来实现此功能,这是推荐的方式。但是,只是将一个功能放在一个类中并不会自动将它变成真正的OOP。
我想说的是,你可能在努力理解OOP方面的这个功能的原因之一是,它实际上并不是面向对象的。 如果您熟悉Java Math类(PHP可能有类似的东西),它有一堆方法/函数,如abs,log等。这虽然是一个类,但实际上并不是面向对象的类感。这只是一堆功能。
面向对象意义上的一个真正的课程是什么?这是一个很大的主题,但至少有一个通用标准是它同时具有状态(属性/字段)和行为(方法),这种方式在行为和状态之间存在内在联系。如果是这样,例如,对方法的调用访问状态(因为它们如此捆绑在一起)。 这是一个简单的OOP类:
Class person {
// State
name;
age;
income;
// Behavior
getName();
setName()
.
.
.
getMonthlyIncome() {
return income / 12;
}
}
这是一个班级,尽管它在现实中的出现(作为一个班级)是程序性的:
class Math {
multiply(double x, double y) {
return x * y;
}
divide(double x, double y) {
return x / y;
}
exponentiate(double x, double y) {
return x^y;
}