Skip to content

Commit cccc176

Browse files
authored
Merge 87fc921 into 0defe17
2 parents 0defe17 + 87fc921 commit cccc176

File tree

7 files changed

+330
-6
lines changed

7 files changed

+330
-6
lines changed

lib/Doctrine/ODM/MongoDB/Aggregation/Builder.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Doctrine\ODM\MongoDB\Aggregation;
66

77
use Doctrine\ODM\MongoDB\DocumentManager;
8+
use Doctrine\ODM\MongoDB\Iterator\UnrewindableIterator;
89
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
910
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
1011
use Doctrine\ODM\MongoDB\Iterator\Iterator;
@@ -53,6 +54,9 @@ class Builder
5354

5455
/** @var Stage[] */
5556
private $stages = [];
57+
58+
/** @var bool */
59+
private $rewindable = true;
5660

5761
/**
5862
* Create a new aggregation builder.
@@ -430,6 +434,16 @@ public function replaceRoot($expression = null) : Stage\ReplaceRoot
430434
return $stage;
431435
}
432436

437+
/**
438+
* Controls if resulting iterator should be wrapped with CachingIterator.
439+
*/
440+
public function rewindable(bool $rewindable = true) : self
441+
{
442+
$this->rewindable = $rewindable;
443+
444+
return $this;
445+
}
446+
433447
/**
434448
* Randomly selects the specified number of documents from its input.
435449
*
@@ -550,6 +564,8 @@ private function prepareIterator(Cursor $cursor) : Iterator
550564
$cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $class);
551565
}
552566

553-
return new CachingIterator($cursor);
567+
$cursor = $this->rewindable ? new CachingIterator($cursor) : new UnrewindableIterator($cursor);
568+
569+
return $cursor;
554570
}
555571
}

lib/Doctrine/ODM/MongoDB/Aggregation/Stage.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,16 @@ public function replaceRoot($expression = null) : Stage\ReplaceRoot
264264
return $this->builder->replaceRoot($expression);
265265
}
266266

267+
/**
268+
* Controls if resulting iterator should be wrapped with CachingIterator.
269+
*/
270+
public function rewindable(bool $rewindable = true) : self
271+
{
272+
$this->builder->rewindable($rewindable);
273+
274+
return $this;
275+
}
276+
267277
/**
268278
* Randomly selects the specified number of documents from its input.
269279
*
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\ODM\MongoDB\Iterator;
6+
7+
use Generator;
8+
use RuntimeException;
9+
use Traversable;
10+
11+
/**
12+
* Iterator for wrapping a Traversable and caching its results.
13+
*
14+
* By caching results, this iterators allows a Traversable to be counted and
15+
* rewound multiple times, even if the wrapped object does not natively support
16+
* those operations (e.g. MongoDB\Driver\Cursor).
17+
*
18+
* @internal
19+
*/
20+
final class UnrewindableIterator implements Iterator
21+
{
22+
/** @var Generator|null */
23+
private $iterator;
24+
25+
/** @var bool */
26+
private $iteratorAdvanced = false;
27+
28+
/**
29+
* Initialize the iterator. This effectively rewinds the Traversable and
30+
* the wrapping Generator, which will execute up to its first yield statement.
31+
* Additionally, this mimics behavior of the SPL iterators and allows users
32+
* to omit an explicit call to rewind() before using the other methods.
33+
*/
34+
public function __construct(Traversable $iterator)
35+
{
36+
$this->iterator = $this->wrapTraversable($iterator);
37+
$this->storeCurrentItem();
38+
}
39+
40+
public function toArray() : array
41+
{
42+
$this->preventRewinding(__METHOD__);
43+
44+
$toArray = function () {
45+
yield $this->key() => $this->current();
46+
yield from $this->getIterator();
47+
};
48+
49+
return iterator_to_array($toArray());
50+
}
51+
52+
/**
53+
* @see http://php.net/iterator.current
54+
*
55+
* @return mixed
56+
*/
57+
public function current()
58+
{
59+
return $this->getIterator()->current();
60+
}
61+
62+
/**
63+
* @see http://php.net/iterator.mixed
64+
*
65+
* @return mixed
66+
*/
67+
public function key()
68+
{
69+
return $this->getIterator()->key();
70+
}
71+
72+
/**
73+
* @see http://php.net/iterator.next
74+
*/
75+
public function next() : void
76+
{
77+
if ($iterator = $this->getIterator()) {
78+
$iterator->next();
79+
}
80+
}
81+
82+
/**
83+
* @see http://php.net/iterator.rewind
84+
*/
85+
public function rewind() : void
86+
{
87+
$this->preventRewinding(__METHOD__);
88+
}
89+
90+
/**
91+
* @see http://php.net/iterator.valid
92+
*/
93+
public function valid() : bool
94+
{
95+
return $this->key() !== null;
96+
}
97+
98+
private function preventRewinding(string $method): void
99+
{
100+
if ($this->iteratorAdvanced) {
101+
throw new \LogicException(sprintf(
102+
'Cannot call %s for iterator that already yielded results',
103+
$method
104+
));
105+
}
106+
}
107+
108+
private function getIterator() : Generator
109+
{
110+
if ($this->iterator === null) {
111+
throw new RuntimeException('Iterator has already been destroyed');
112+
}
113+
114+
return $this->iterator;
115+
}
116+
117+
118+
/**
119+
* Stores the current item in the cache.
120+
*/
121+
private function storeCurrentItem() : void
122+
{
123+
$key = $this->iterator->key();
124+
125+
if ($key === null) {
126+
return;
127+
}
128+
}
129+
130+
private function wrapTraversable(Traversable $traversable) : Generator
131+
{
132+
foreach ($traversable as $key => $value) {
133+
yield $key => $value;
134+
$this->iteratorAdvanced = true;
135+
}
136+
137+
$this->iterator = null;
138+
}
139+
}

lib/Doctrine/ODM/MongoDB/Query/Builder.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ class Builder
8080
*/
8181
private $readOnly = false;
8282

83+
/**
84+
* @var bool
85+
*/
86+
private $rewindable = true;
87+
8388
/**
8489
* The Collection instance.
8590
*
@@ -713,7 +718,8 @@ public function getQuery(array $options = []) : Query
713718
$this->hydrate,
714719
$this->refresh,
715720
$this->primers,
716-
$this->readOnly
721+
$this->readOnly,
722+
$this->rewindable
717723
);
718724
}
719725

@@ -1379,6 +1385,13 @@ public function setReadPreference(ReadPreference $readPreference) : self
13791385
return $this;
13801386
}
13811387

1388+
public function setRewindable(bool $rewindable = true): self
1389+
{
1390+
$this->rewindable = $rewindable;
1391+
1392+
return $this;
1393+
}
1394+
13821395
/**
13831396
* Set the expression's query criteria.
13841397
*

lib/Doctrine/ODM/MongoDB/Query/Query.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use BadMethodCallException;
88
use Doctrine\ODM\MongoDB\DocumentManager;
9+
use Doctrine\ODM\MongoDB\Iterator\UnrewindableIterator;
910
use Doctrine\ODM\MongoDB\Iterator\CachingIterator;
1011
use Doctrine\ODM\MongoDB\Iterator\HydratingIterator;
1112
use Doctrine\ODM\MongoDB\Iterator\Iterator;
@@ -84,6 +85,11 @@ final class Query implements IteratorAggregate
8485
*/
8586
private $primers = [];
8687

88+
/**
89+
* @var bool
90+
*/
91+
private $rewindable = true;
92+
8793
/**
8894
* Hints for UnitOfWork behavior.
8995
*
@@ -115,7 +121,7 @@ final class Query implements IteratorAggregate
115121
*/
116122
private $options;
117123

118-
public function __construct(DocumentManager $dm, ClassMetadata $class, Collection $collection, array $query = [], array $options = [], bool $hydrate = true, bool $refresh = false, array $primers = [], bool $readOnly = false)
124+
public function __construct(DocumentManager $dm, ClassMetadata $class, Collection $collection, array $query = [], array $options = [], bool $hydrate = true, bool $refresh = false, array $primers = [], bool $readOnly = false, bool $rewindable = true)
119125
{
120126
$primers = array_filter($primers);
121127

@@ -144,6 +150,7 @@ public function __construct(DocumentManager $dm, ClassMetadata $class, Collectio
144150

145151
$this->setReadOnly($readOnly);
146152
$this->setRefresh($refresh);
153+
$this->setRewindable($rewindable);
147154

148155
if (! isset($query['readPreference'])) {
149156
return;
@@ -315,6 +322,14 @@ public function setRefresh(bool $refresh) : void
315322
$this->unitOfWorkHints[self::HINT_REFRESH] = $refresh;
316323
}
317324

325+
/**
326+
* Set to enable wrapping of resulting Iterator with CachingIterator
327+
*/
328+
public function setRewindable(bool $rewindable = true) : void
329+
{
330+
$this->rewindable = $rewindable;
331+
}
332+
318333
/**
319334
* Execute the query and return its results as an array.
320335
*
@@ -344,16 +359,16 @@ static function ($value) {
344359
*
345360
* Note: while this method could strictly take a MongoDB\Driver\Cursor, we
346361
* accept Traversable for testing purposes since Cursor cannot be mocked.
347-
* HydratingIterator and CachingIterator both expect a Traversable so this
348-
* should not have any adverse effects.
362+
* HydratingIterator, CachingIterator, and BaseIterator expect a Traversable
363+
* so this should not have any adverse effects.
349364
*/
350365
private function makeIterator(Traversable $cursor) : Iterator
351366
{
352367
if ($this->hydrate) {
353368
$cursor = new HydratingIterator($cursor, $this->dm->getUnitOfWork(), $this->class, $this->unitOfWorkHints);
354369
}
355370

356-
$cursor = new CachingIterator($cursor);
371+
$cursor = $this->rewindable ? new CachingIterator($cursor) : new UnrewindableIterator($cursor);
357372

358373
if (! empty($this->primers)) {
359374
$referencePrimer = new ReferencePrimer($this->dm, $this->dm->getUnitOfWork());

tests/Doctrine/ODM/MongoDB/Tests/Functional/Iterator/HydratingIteratorTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Doctrine\ODM\MongoDB\Tests\BaseTest;
99
use Documents\User;
1010
use MongoDB\BSON\ObjectId;
11+
use Throwable;
1112
use function is_array;
1213

1314
final class HydratingIteratorTest extends BaseTest

0 commit comments

Comments
 (0)