Laravel 13 迭代器模式深度解析

迭代器模式是一种行为型设计模式,它提供一种方法顺序访问聚合对象中的元素,而不暴露其底层表示。本文将深入探讨 Laravel 13 中迭代器模式的高级用法。

迭代器模式基础

什么是迭代器模式

迭代器模式允许在不暴露集合内部结构的情况下遍历集合中的元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

namespace App\Contracts;

interface IteratorInterface
{
public function current(): mixed;

public function key(): mixed;

public function next(): void;

public function rewind(): void;

public function valid(): bool;
}
1
2
3
4
5
6
7
8
<?php

namespace App\Contracts;

interface AggregateInterface
{
public function createIterator(): IteratorInterface;
}

自定义迭代器

数组迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<?php

namespace App\Iterators;

use App\Contracts\IteratorInterface;

class ArrayIterator implements IteratorInterface
{
protected array $items;
protected int $position = 0;

public function __construct(array $items)
{
$this->items = array_values($items);
}

public function current(): mixed
{
return $this->items[$this->position] ?? null;
}

public function key(): mixed
{
return $this->position;
}

public function next(): void
{
$this->position++;
}

public function rewind(): void
{
$this->position = 0;
}

public function valid(): bool
{
return isset($this->items[$this->position]);
}
}

过滤迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
<?php

namespace App\Iterators;

use Iterator;

class FilterIterator implements Iterator
{
protected Iterator $iterator;
protected $callback;
protected mixed $current;
protected int $key = 0;

public function __construct(Iterator $iterator, callable $callback)
{
$this->iterator = $iterator;
$this->callback = $callback;
}

public function current(): mixed
{
return $this->current;
}

public function key(): mixed
{
return $this->key;
}

public function next(): void
{
$this->iterator->next();
$this->fetchNextValid();
}

public function rewind(): void
{
$this->iterator->rewind();
$this->key = 0;
$this->fetchNextValid();
}

public function valid(): bool
{
return $this->current !== null;
}

protected function fetchNextValid(): void
{
$this->current = null;

while ($this->iterator->valid()) {
$item = $this->iterator->current();

if (($this->callback)($item)) {
$this->current = $item;
$this->key++;
break;
}

$this->iterator->next();
}
}
}

映射迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<?php

namespace App\Iterators;

use Iterator;

class MapIterator implements Iterator
{
protected Iterator $iterator;
protected $callback;

public function __construct(Iterator $iterator, callable $callback)
{
$this->iterator = $iterator;
$this->callback = $callback;
}

public function current(): mixed
{
return ($this->callback)($this->iterator->current());
}

public function key(): mixed
{
return $this->iterator->key();
}

public function next(): void
{
$this->iterator->next();
}

public function rewind(): void
{
$this->iterator->rewind();
}

public function valid(): bool
{
return $this->iterator->valid();
}
}

数据库结果集迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?php

namespace App\Iterators\Database;

use Iterator;
use Illuminate\Support\Facades\DB;

class LazyQueryIterator implements Iterator
{
protected string $query;
protected array $bindings;
protected int $chunkSize;
protected int $position = 0;
protected int $chunkPosition = 0;
protected array $currentChunk = [];
protected int $totalOffset = 0;

public function __construct(string $query, array $bindings = [], int $chunkSize = 100)
{
$this->query = $query;
$this->bindings = $bindings;
$this->chunkSize = $chunkSize;
}

public function current(): mixed
{
return $this->currentChunk[$this->chunkPosition] ?? null;
}

public function key(): mixed
{
return $this->position;
}

public function next(): void
{
$this->position++;
$this->chunkPosition++;

if ($this->chunkPosition >= count($this->currentChunk)) {
$this->loadNextChunk();
}
}

public function rewind(): void
{
$this->position = 0;
$this->chunkPosition = 0;
$this->totalOffset = 0;
$this->loadNextChunk();
}

public function valid(): bool
{
return isset($this->currentChunk[$this->chunkPosition]);
}

protected function loadNextChunk(): void
{
$this->currentChunk = DB::select(
$this->query . " LIMIT {$this->chunkSize} OFFSET {$this->totalOffset}",
$this->bindings
);

$this->totalOffset += $this->chunkSize;
$this->chunkPosition = 0;
}
}

树形结构迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

namespace App\Iterators\Tree;

use Iterator;
use Countable;

class DepthFirstIterator implements Iterator, Countable
{
protected array $stack = [];
protected int $position = 0;
protected $getChildren;

public function __construct($root, callable $getChildren)
{
$this->getChildren = $getChildren;
$this->buildStack($root);
}

protected function buildStack($node): void
{
$this->stack[] = $node;

$children = ($this->getChildren)($node);

if (is_iterable($children)) {
foreach ($children as $child) {
$this->buildStack($child);
}
}
}

public function current(): mixed
{
return $this->stack[$this->position] ?? null;
}

public function key(): mixed
{
return $this->position;
}

public function next(): void
{
$this->position++;
}

public function rewind(): void
{
$this->position = 0;
}

public function valid(): bool
{
return isset($this->stack[$this->position]);
}

public function count(): int
{
return count($this->stack);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

namespace App\Iterators\Tree;

use Iterator;

class BreadthFirstIterator implements Iterator
{
protected array $queue = [];
protected int $position = 0;
protected $getChildren;

public function __construct($root, callable $getChildren)
{
$this->getChildren = $getChildren;
$this->queue = [$root];
}

public function current(): mixed
{
return $this->queue[$this->position] ?? null;
}

public function key(): mixed
{
return $this->position;
}

public function next(): void
{
$this->position++;

if (!isset($this->queue[$this->position])) {
$this->loadNextLevel();
}
}

public function rewind(): void
{
$this->position = 0;
}

public function valid(): bool
{
return isset($this->queue[$this->position]);
}

protected function loadNextLevel(): void
{
$currentLevel = array_slice($this->queue, $this->position);

foreach ($currentLevel as $node) {
$children = ($this->getChildren)($node);

if (is_iterable($children)) {
foreach ($children as $child) {
$this->queue[] = $child;
}
}
}
}
}

分页迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
<?php

namespace App\Iterators\Pagination;

use Iterator;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;

class PaginatorIterator implements Iterator
{
protected $query;
protected int $perPage;
protected int $currentPage = 1;
protected array $currentItems = [];
protected int $itemPosition = 0;
protected int $totalPosition = 0;
protected ?int $totalItems = null;

public function __construct($query, int $perPage = 15)
{
$this->query = $query;
$this->perPage = $perPage;
}

public function current(): mixed
{
return $this->currentItems[$this->itemPosition] ?? null;
}

public function key(): mixed
{
return $this->totalPosition;
}

public function next(): void
{
$this->itemPosition++;
$this->totalPosition++;

if ($this->itemPosition >= count($this->currentItems)) {
$this->loadNextPage();
}
}

public function rewind(): void
{
$this->currentPage = 1;
$this->itemPosition = 0;
$this->totalPosition = 0;
$this->loadCurrentPage();
}

public function valid(): bool
{
return isset($this->currentItems[$this->itemPosition]);
}

protected function loadCurrentPage(): void
{
$paginator = $this->query->paginate($this->perPage, ['*'], 'page', $this->currentPage);
$this->currentItems = $paginator->items();

if ($this->totalItems === null) {
$this->totalItems = $paginator->total();
}
}

protected function loadNextPage(): void
{
$this->currentPage++;
$this->itemPosition = 0;
$this->loadCurrentPage();
}

public function getTotalItems(): ?int
{
return $this->totalItems;
}
}

文件系统迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
<?php

namespace App\Iterators\Filesystem;

use Iterator;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;

class DirectoryIterator implements Iterator
{
protected RecursiveIteratorIterator $iterator;
protected array $filters = [];

public function __construct(string $path, int $mode = RecursiveIteratorIterator::LEAVES_ONLY)
{
$directoryIterator = new RecursiveDirectoryIterator(
$path,
RecursiveDirectoryIterator::SKIP_DOTS
);

$this->iterator = new RecursiveIteratorIterator($directoryIterator, $mode);
}

public function filter(callable $callback): self
{
$this->filters[] = $callback;
return $this;
}

public function byExtension(string $extension): self
{
return $this->filter(fn($file) => $file->getExtension() === $extension);
}

public function byPattern(string $pattern): self
{
return $this->filter(fn($file) => fnmatch($pattern, $file->getFilename()));
}

public function current(): mixed
{
return $this->iterator->current();
}

public function key(): mixed
{
return $this->iterator->key();
}

public function next(): void
{
$this->iterator->next();
$this->skipFiltered();
}

public function rewind(): void
{
$this->iterator->rewind();
$this->skipFiltered();
}

public function valid(): bool
{
return $this->iterator->valid();
}

protected function skipFiltered(): void
{
while ($this->iterator->valid() && !$this->passesFilters($this->iterator->current())) {
$this->iterator->next();
}
}

protected function passesFilters($file): bool
{
foreach ($this->filters as $filter) {
if (!$filter($file)) {
return false;
}
}

return true;
}
}

缓存迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php

namespace App\Iterators\Cache;

use Iterator;
use Illuminate\Support\Facades\Cache;

class CachedIterator implements Iterator
{
protected Iterator $iterator;
protected string $cacheKey;
protected int $ttl;
protected array $cachedItems = [];
protected bool $loaded = false;
protected int $position = 0;

public function __construct(Iterator $iterator, string $cacheKey, int $ttl = 3600)
{
$this->iterator = $iterator;
$this->cacheKey = $cacheKey;
$this->ttl = $ttl;
}

public function current(): mixed
{
$this->ensureLoaded();
return $this->cachedItems[$this->position] ?? null;
}

public function key(): mixed
{
return $this->position;
}

public function next(): void
{
$this->ensureLoaded();
$this->position++;
}

public function rewind(): void
{
$this->ensureLoaded();
$this->position = 0;
}

public function valid(): bool
{
$this->ensureLoaded();
return isset($this->cachedItems[$this->position]);
}

protected function ensureLoaded(): void
{
if ($this->loaded) {
return;
}

$cached = Cache::get($this->cacheKey);

if ($cached !== null) {
$this->cachedItems = $cached;
} else {
$this->cachedItems = iterator_to_array($this->iterator);
Cache::put($this->cacheKey, $this->cachedItems, $this->ttl);
}

$this->loaded = true;
}

public function toArray(): array
{
$this->ensureLoaded();
return $this->cachedItems;
}
}

批处理迭代器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?php

namespace App\Iterators\Batch;

use Iterator;

class BatchIterator implements Iterator
{
protected Iterator $iterator;
protected int $batchSize;
protected array $currentBatch = [];
protected int $batchNumber = 0;

public function __construct(Iterator $iterator, int $batchSize = 100)
{
$this->iterator = $iterator;
$this->batchSize = $batchSize;
}

public function current(): mixed
{
return $this->currentBatch;
}

public function key(): mixed
{
return $this->batchNumber;
}

public function next(): void
{
$this->loadNextBatch();
}

public function rewind(): void
{
$this->iterator->rewind();
$this->batchNumber = 0;
$this->loadNextBatch();
}

public function valid(): bool
{
return !empty($this->currentBatch);
}

protected function loadNextBatch(): void
{
$this->currentBatch = [];
$count = 0;

while ($this->iterator->valid() && $count < $this->batchSize) {
$this->currentBatch[] = $this->iterator->current();
$this->iterator->next();
$count++;
}

if (!empty($this->currentBatch)) {
$this->batchNumber++;
}
}
}

测试迭代器模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<?php

namespace Tests\Unit\Iterators;

use Tests\TestCase;
use App\Iterators\ArrayIterator;
use App\Iterators\FilterIterator;
use App\Iterators\MapIterator;
use App\Iterators\Batch\BatchIterator;

class IteratorTest extends TestCase
{
public function test_array_iterator(): void
{
$items = ['a', 'b', 'c'];
$iterator = new ArrayIterator($items);

$result = [];
foreach ($iterator as $item) {
$result[] = $item;
}

$this->assertEquals($items, $result);
}

public function test_filter_iterator(): void
{
$items = [1, 2, 3, 4, 5, 6];
$inner = new ArrayIterator($items);
$iterator = new FilterIterator($inner, fn($n) => $n % 2 === 0);

$result = [];
foreach ($iterator as $item) {
$result[] = $item;
}

$this->assertEquals([2, 4, 6], $result);
}

public function test_map_iterator(): void
{
$items = [1, 2, 3];
$inner = new ArrayIterator($items);
$iterator = new MapIterator($inner, fn($n) => $n * 2);

$result = [];
foreach ($iterator as $item) {
$result[] = $item;
}

$this->assertEquals([2, 4, 6], $result);
}

public function test_batch_iterator(): void
{
$items = range(1, 10);
$inner = new ArrayIterator($items);
$iterator = new BatchIterator($inner, 3);

$batches = [];
foreach ($iterator as $batch) {
$batches[] = $batch;
}

$this->assertCount(4, $batches);
$this->assertEquals([1, 2, 3], $batches[0]);
$this->assertEquals([10], $batches[3]);
}
}

最佳实践

1. 实现 Countable 接口

1
2
3
4
5
6
7
8
9
10
11
<?php

class CountableIterator implements Iterator, Countable
{
protected array $items;

public function count(): int
{
return count($this->items);
}
}

2. 实现 SeekableIterator 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php

class SeekableIterator implements \SeekableIterator
{
protected array $items;
protected int $position = 0;

public function seek(int $position): void
{
if (!isset($this->items[$position])) {
throw new \OutOfBoundsException("Position {$position} does not exist");
}

$this->position = $position;
}
}

3. 使用 Generator 简化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php

class GeneratorIterator
{
public static function fromArray(array $items): \Generator
{
foreach ($items as $key => $value) {
yield $key => $value;
}
}

public static function filter(\Generator $generator, callable $callback): \Generator
{
foreach ($generator as $key => $value) {
if ($callback($value, $key)) {
yield $key => $value;
}
}
}

public static function map(\Generator $generator, callable $callback): \Generator
{
foreach ($generator as $key => $value) {
yield $key => $callback($value, $key);
}
}
}

总结

Laravel 13 的迭代器模式提供了一种统一的方式来遍历各种数据结构。通过合理使用迭代器模式,可以创建灵活、可组合的数据遍历逻辑,同时保持代码的清晰和可维护性。