Skip to content

Commit d4144e4

Browse files
committed
PHPLIB-501: Use IteratorIterator to prevent memory leak with generators
1 parent 32cfb9b commit d4144e4

File tree

1 file changed

+31
-19
lines changed

1 file changed

+31
-19
lines changed

src/Model/CachingIterator.php

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Countable;
2121
use Generator;
2222
use Iterator;
23+
use IteratorIterator;
2324
use Traversable;
2425
use function count;
2526
use function current;
@@ -41,7 +42,7 @@ class CachingIterator implements Countable, Iterator
4142
/** @var array */
4243
private $items = [];
4344

44-
/** @var Generator */
45+
/** @var IteratorIterator */
4546
private $iterator;
4647

4748
/** @var boolean */
@@ -50,6 +51,9 @@ class CachingIterator implements Countable, Iterator
5051
/** @var boolean */
5152
private $iteratorExhausted = false;
5253

54+
/** @var boolean */
55+
private $iteratorPrepared = false;
56+
5357
/**
5458
* Initialize the iterator and stores the first item in the cache. This
5559
* effectively rewinds the Traversable and the wrapping Generator, which
@@ -61,8 +65,7 @@ class CachingIterator implements Countable, Iterator
6165
*/
6266
public function __construct(Traversable $traversable)
6367
{
64-
$this->iterator = $this->wrapTraversable($traversable);
65-
$this->storeCurrentItem();
68+
$this->iterator = new IteratorIterator($traversable);
6669
}
6770

6871
/**
@@ -82,6 +85,8 @@ public function count()
8285
*/
8386
public function current()
8487
{
88+
$this->prepareIterator();
89+
8590
return current($this->items);
8691
}
8792

@@ -91,6 +96,8 @@ public function current()
9196
*/
9297
public function key()
9398
{
99+
$this->prepareIterator();
100+
94101
return key($this->items);
95102
}
96103

@@ -100,9 +107,15 @@ public function key()
100107
*/
101108
public function next()
102109
{
110+
$this->prepareIterator();
111+
103112
if (! $this->iteratorExhausted) {
113+
$this->iteratorAdvanced = true;
104114
$this->iterator->next();
115+
105116
$this->storeCurrentItem();
117+
118+
$this->iteratorExhausted = !$this->iterator->valid();
106119
}
107120

108121
next($this->items);
@@ -114,6 +127,8 @@ public function next()
114127
*/
115128
public function rewind()
116129
{
130+
$this->prepareIterator();
131+
117132
/* If the iterator has advanced, exhaust it now so that future iteration
118133
* can rely on the cache.
119134
*/
@@ -138,11 +153,24 @@ public function valid()
138153
*/
139154
private function exhaustIterator()
140155
{
156+
$this->prepareIterator();
157+
141158
while (! $this->iteratorExhausted) {
142159
$this->next();
143160
}
144161
}
145162

163+
private function prepareIterator()
164+
{
165+
if ($this->iteratorPrepared) {
166+
return;
167+
}
168+
169+
$this->iterator->rewind();
170+
$this->iteratorPrepared = true;
171+
$this->storeCurrentItem();
172+
}
173+
146174
/**
147175
* Stores the current item in the cache.
148176
*/
@@ -156,20 +184,4 @@ private function storeCurrentItem()
156184

157185
$this->items[$key] = $this->iterator->current();
158186
}
159-
160-
/**
161-
* Wraps the Traversable with a Generator.
162-
*
163-
* @param Traversable $traversable
164-
* @return Generator
165-
*/
166-
private function wrapTraversable(Traversable $traversable)
167-
{
168-
foreach ($traversable as $key => $value) {
169-
yield $key => $value;
170-
$this->iteratorAdvanced = true;
171-
}
172-
173-
$this->iteratorExhausted = true;
174-
}
175187
}

0 commit comments

Comments
 (0)