Skip to content

FilterIterator / LimitIterator / CachingIterator

概述

PHP SPL 提供了多个装饰器迭代器,用于对底层迭代器进行过滤、限制和缓存操作。FilterIterator 用于过滤元素,LimitIterator 用于限制迭代数量,CachingIterator 用于缓存已遍历的元素以便回看。这些装饰器可以组合使用,构建复杂的数据处理管道。

核心概念

装饰器迭代器包装一个底层迭代器,在不修改原数据的情况下提供额外的遍历功能。

基础概念

FilterIterator

FilterIterator 是一个抽象类,子类需要实现 accept() 方法来决定是否接受当前元素。

LimitIterator

LimitIterator 包装一个迭代器,限制返回的元素数量,类似于 SQL 的 LIMIT offset, count

CachingIterator

CachingIterator 缓存已遍历的元素,支持查看上一个/所有已遍历的元素。

语法与代码

FilterIterator 基本用法

php
<?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
<?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
<?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: 15

LimitIterator 实现分页

php
<?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 10

CachingIterator 基本用法

php
<?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
<?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
<?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_KEYtoString 包含键
CachingIterator::TOSTRING_USE_CURRENTtoString 包含当前值
CachingIterator::FULL_CACHE缓存所有元素

实战示例

实战:日志分析器

php
<?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 不会报错,只是返回空结果。

最佳实践

  1. 优先使用内置 CallbackFilterIterator:PHP 5.4+ 已提供,无需自定义。
  2. 合理设置缓存模式FULL_CACHE 会缓存所有元素,大数据集时注意内存。
  3. 组合装饰器:将过滤、限制、排序等装饰器组合使用。

更多 SPL 迭代器

CallbackFilterIterator

PHP 5.4+ 提供了 CallbackFilterIterator,无需创建子类即可实现过滤:

php
<?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
<?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
<?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
<?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 h

RegexIterator

RegexIterator 使用正则表达式过滤迭代器的键或值:

php
<?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
<?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),而 InnerIteratorCachingIterator 的抽象基类。直接使用 CachingIterator 即可。

LimitIterator 的 offset 可以大于迭代器长度吗?

可以,只是不会产生任何输出。不会抛出异常。

参考链接