FilterIterator / LimitIterator / CachingIterator
概述
PHP SPL 提供了多个装饰器迭代器,用于对底层迭代器进行过滤、限制和缓存操作。FilterIterator 用于过滤元素,LimitIterator 用于限制迭代数量,CachingIterator 用于缓存已遍历的元素以便回看。这些装饰器可以组合使用,构建复杂的数据处理管道。
核心概念
装饰器迭代器包装一个底层迭代器,在不修改原数据的情况下提供额外的遍历功能。
基础概念
FilterIterator
FilterIterator 是一个抽象类,子类需要实现 accept() 方法来决定是否接受当前元素。
LimitIterator
LimitIterator 包装一个迭代器,限制返回的元素数量,类似于 SQL 的 LIMIT offset, count。
CachingIterator
CachingIterator 缓存已遍历的元素,支持查看上一个/所有已遍历的元素。
语法与代码
FilterIterator 基本用法
<?php
declare(strict_types=1);
class EvenFilterIterator extends \FilterIterator
{
public function accept(): bool
{
return $this->current() % 2 === 0;
}
}
$numbers = new \ArrayIterator([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
$evenNumbers = new EvenFilterIterator($numbers);
foreach ($evenNumbers as $number) {
echo "{$number} ";
}
// 2 4 6 8 10通用过滤迭代器
<?php
declare(strict_types=1);
class CallbackFilterIterator extends \FilterIterator
{
private \Closure $callback;
public function __construct(\Iterator $iterator, callable $callback)
{
parent::__construct($iterator);
$this->callback = \Closure::fromCallable($callback);
}
public function accept(): bool
{
return ($this->callback)($this->current(), $this->key());
}
}
$data = new \ArrayIterator([
['name' => 'Alice', 'age' => 30],
['name' => 'Bob', 'age' => 25],
['name' => 'Charlie', 'age' => 35],
]);
$adults = new CallbackFilterIterator($data, function (array $person): bool {
return $person['age'] >= 30;
});
foreach ($adults as $person) {
echo "{$person['name']} ({$person['age']})\n";
}
// Alice (30)
// Charlie (35)PHP 5.4+
PHP 5.4+ 提供了内置的 CallbackFilterIterator,可以直接使用,无需自定义。
LimitIterator 基本用法
<?php
declare(strict_types=1);
$allItems = new \ArrayIterator(range(1, 100));
// 从偏移量 5 开始,取 10 个元素
$limited = new \LimitIterator($allItems, 5, 10);
foreach ($limited as $key => $value) {
echo "{$key}: {$value}\n";
}
// 5: 6
// 6: 7
// ...
// 14: 15LimitIterator 实现分页
<?php
declare(strict_types=1);
class Paginator
{
private \ArrayIterator $items;
private int $perPage;
public function __construct(array $items, int $perPage = 10)
{
$this->items = new \ArrayIterator($items);
$this->perPage = $perPage;
}
public function getPage(int $page): \LimitIterator
{
$offset = ($page - 1) * $this->perPage;
return new \LimitIterator($this->items, $offset, $this->perPage);
}
public function totalPages(): int
{
return (int) ceil($this->items->count() / $this->perPage);
}
}
$items = range(1, 53);
$paginator = new Paginator($items, 10);
echo "总页数: " . $paginator->totalPages() . "\n";
$page1 = $paginator->getPage(1);
echo "第 1 页: ";
foreach ($page1 as $item) {
echo "{$item} ";
}
// 第 1 页: 1 2 3 4 5 6 7 8 9 10CachingIterator 基本用法
<?php
declare(strict_types=1);
$items = new \ArrayIterator(['a', 'b', 'c', 'd', 'e']);
// FULL_CACHE 模式缓存所有元素
$cache = new \CachingIterator($items, \CachingIterator::FULL_CACHE);
foreach ($cache as $item) {
echo "当前: {$item}\n";
if ($cache->hasNext()) {
echo " 下一个: {$cache->getInnerIterator()->current()}\n";
}
}
echo "\n所有缓存元素: ";
print_r(iterator_to_array($cache->getCache()));CachingIterator 查看上一个元素
<?php
declare(strict_types=1);
class PairProcessor
{
public function process(iterable $items): \Generator
{
$cache = new \CachingIterator(
new \ArrayIterator(is_array($items) ? $items : iterator_to_array($items))
);
$previous = null;
foreach ($cache as $current) {
if ($previous !== null) {
yield "({$previous}, {$current})";
}
$previous = $current;
}
}
}
$processor = new PairProcessor();
foreach ($processor->process([1, 2, 3, 4, 5]) as $pair) {
echo "{$pair} ";
}
// (1, 2) (2, 3) (3, 4) (4, 5)组合使用装饰器
<?php
declare(strict_types=1);
$allData = new \ArrayIterator(range(1, 50));
// 先过滤偶数,再限制结果
$evenFilter = new EvenFilterIterator($allData);
$limited = new \LimitIterator($evenFilter, 2, 5);
foreach ($limited as $value) {
echo "{$value} ";
}
// 输出: 6 8 10 12 14详细说明
FilterIterator 的 accept() 方法
accept() 在每次迭代时被调用,返回 true 表示接受当前元素,false 表示跳过。
LimitIterator 参数
LimitIterator(Iterator $iterator, int $offset = 0, int $count = -1)
offset:从第几个元素开始(0-based)count:返回多少个元素(-1 表示无限制)
CachingIterator 模式
| 标志 | 说明 |
|---|---|
CachingIterator::CALL_TOSTRING | 自动调用 __toString() |
CachingIterator::TOSTRING_USE_KEY | toString 包含键 |
CachingIterator::TOSTRING_USE_CURRENT | toString 包含当前值 |
CachingIterator::FULL_CACHE | 缓存所有元素 |
实战示例
实战:日志分析器
<?php
declare(strict_types=1);
class ErrorLogFilter extends \FilterIterator
{
public function accept(): bool
{
$line = $this->current();
return str_contains($line, 'ERROR') || str_contains($line, 'CRITICAL');
}
}
class LogAnalyzer
{
public function getErrors(string $filePath, int $limit = 50): \LimitIterator
{
$lines = file($filePath, FILE_IGNORE_NEW_LINES) ?: [];
$iterator = new \ArrayIterator($lines);
$filtered = new ErrorLogFilter($iterator);
return new \LimitIterator($filtered, 0, $limit);
}
}
// 使用
$analyzer = new LogAnalyzer();
// foreach ($analyzer->getErrors('app.log') as $error) {
// echo $error . "\n";
// }注意事项
FilterIterator 的性能
FilterIterator::accept() 在每次 next() 时被调用。如果 accept() 逻辑很重,可能会影响性能。
LimitIterator 的边界
当 offset 超出底层迭代器的元素数量时,LimitIterator 不会报错,只是返回空结果。
最佳实践
- 优先使用内置 CallbackFilterIterator:PHP 5.4+ 已提供,无需自定义。
- 合理设置缓存模式:
FULL_CACHE会缓存所有元素,大数据集时注意内存。 - 组合装饰器:将过滤、限制、排序等装饰器组合使用。
更多 SPL 迭代器
CallbackFilterIterator
PHP 5.4+ 提供了 CallbackFilterIterator,无需创建子类即可实现过滤:
<?php
declare(strict_types=1);
$files = new \DirectoryIterator('/path/to/project');
// 只保留 PHP 文件
$phpFiles = new \CallbackFilterIterator(
$files,
fn(\SplFileInfo $file): bool => $file->isFile()
&& $file->getExtension() === 'php'
&& !str_starts_with($file->getFilename(), '.')
);
foreach ($phpFiles as $file) {
echo $file->getFilename() . " (" . $file->getSize() . " bytes)\n";
}InfiniteIterator
InfiniteIterator 在遍历结束后自动重新开始,适合无限循环场景:
<?php
declare(strict_types=1);
// 轮询负载均衡器
$servers = ['server1', 'server2', 'server3', 'server4'];
$roundRobin = new \InfiniteIterator(new \ArrayIterator($servers));
// 模拟分配 10 个请求
$requestCount = 10;
$assignments = [];
for ($i = 0; $i < $requestCount; $i++) {
$roundRobin->rewind();
for ($j = 0; $j <= $i; $j++) {
$roundRobin->next();
}
$assignments[] = $roundRobin->current();
}
print_r($assignments);
// ['server1', 'server2', 'server3', 'server4', 'server1', ...]注意
InfiniteIterator 配合 LimitIterator 使用可以实现有限次数的轮询。
NoRewindIterator
NoRewindIterator 包装另一个迭代器,阻止 rewind() 操作:
<?php
declare(strict_types=1);
$iterator = new \NoRewindIterator(new \ArrayIterator([1, 2, 3]));
foreach ($iterator as $value) {
echo $value . "\n";
}
// 第二次遍历时不会有输出,因为 rewind() 被阻止了
foreach ($iterator as $value) {
echo $value . "\n";
}AppendIterator
AppendIterator 将多个迭代器顺序连接:
<?php
declare(strict_types=1);
$iterator = new \AppendIterator();
$iterator->append(new \ArrayIterator(['a', 'b', 'c']));
$iterator->append(new \ArrayIterator(['d', 'e']));
$iterator->append(new \ArrayIterator(['f', 'g', 'h']));
foreach ($iterator as $value) {
echo $value . " ";
}
// a b c d e f g hRegexIterator
RegexIterator 使用正则表达式过滤迭代器的键或值:
<?php
declare(strict_types=1);
$files = new \DirectoryIterator('/path/to/src');
// 只匹配以 "Test" 结尾的 PHP 文件
$testFiles = new \RegexIterator(
$files,
'/Test\.php$/',
\RegexIterator::MATCH,
\RegexIterator::USE_KEY
);
foreach ($testFiles as $file) {
echo $file->getFilename() . "\n";
}实战:构建查询结果分页器
<?php
declare(strict_types=1);
class Paginator implements \IteratorAggregate
{
private \LimitIterator $limitIterator;
public function __construct(
private readonly \Iterator $source,
private readonly int $currentPage = 1,
private readonly int $perPage = 20,
) {
$offset = ($this->currentPage - 1) * $this->perPage;
$this->limitIterator = new \LimitIterator(
$this->source,
$offset,
$this->perPage
);
}
public function getIterator(): \Traversable
{
return $this->limitIterator;
}
public function getTotalItems(): int
{
$count = 0;
foreach (clone $this->source as $_) {
$count++;
}
return $count;
}
public function getTotalPages(): int
{
return (int) ceil($this->getTotalItems() / $this->perPage);
}
public function hasPreviousPage(): bool
{
return $this->currentPage > 1;
}
public function hasNextPage(): bool
{
return $this->currentPage < $this->getTotalPages();
}
}
$allItems = new \ArrayIterator(range(1, 100));
$paginator = new Paginator($allItems, currentPage: 3, perPage: 10);
echo "第 {$paginator->currentPage} 页 / 共 {$paginator->getTotalPages()} 页\n";
echo "数据: " . implode(', ', iterator_to_array($paginator)) . "\n";常见误区与 FAQ
CachingIterator 和 InnerIterator 的区别?
CachingIterator 缓存上一个元素(用于 look-ahead),而 InnerIterator 是 CachingIterator 的抽象基类。直接使用 CachingIterator 即可。
LimitIterator 的 offset 可以大于迭代器长度吗?
可以,只是不会产生任何输出。不会抛出异常。