1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
4
|
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
5
|
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
6
|
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
7
|
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
8
|
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
9
|
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
10
|
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
11
|
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
12
|
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
13
|
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
14
|
|
|
*/ |
15
|
|
|
|
16
|
|
|
namespace Alcaeus\MongoDbAdapter; |
17
|
|
|
|
18
|
|
|
use Alcaeus\MongoDbAdapter\Helper\ReadPreference; |
19
|
|
|
use MongoDB\Collection; |
20
|
|
|
use MongoDB\Driver\Cursor; |
21
|
|
|
|
22
|
|
|
/** |
23
|
|
|
* @internal |
24
|
|
|
*/ |
25
|
|
|
abstract class AbstractCursor |
26
|
|
|
{ |
27
|
|
|
use ReadPreference; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* @var int|null |
31
|
|
|
*/ |
32
|
|
|
protected $batchSize = null; |
33
|
|
|
|
34
|
|
|
/** |
35
|
|
|
* @var Collection |
36
|
|
|
*/ |
37
|
|
|
protected $collection; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var \MongoClient |
41
|
|
|
*/ |
42
|
|
|
protected $connection; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var Cursor |
46
|
|
|
*/ |
47
|
|
|
protected $cursor; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* @var \MongoDB\Database |
51
|
|
|
*/ |
52
|
|
|
protected $db; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var \Iterator |
56
|
|
|
*/ |
57
|
|
|
protected $iterator; |
58
|
|
|
|
59
|
|
|
/** |
60
|
|
|
* @var string |
61
|
|
|
*/ |
62
|
|
|
protected $ns; |
63
|
|
|
|
64
|
|
|
/** |
65
|
|
|
* @var bool |
66
|
|
|
*/ |
67
|
|
|
protected $startedIterating = false; |
68
|
|
|
|
69
|
|
|
/** |
70
|
|
|
* @var bool |
71
|
|
|
*/ |
72
|
|
|
protected $cursorNeedsAdvancing = true; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* @var mixed |
76
|
|
|
*/ |
77
|
|
|
private $current = null; |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* @var mixed |
81
|
|
|
*/ |
82
|
|
|
private $key = null; |
83
|
|
|
|
84
|
|
|
/** |
85
|
|
|
* @var mixed |
86
|
|
|
*/ |
87
|
|
|
private $valid = false; |
88
|
|
|
|
89
|
|
|
/** |
90
|
|
|
* @var int |
91
|
|
|
*/ |
92
|
|
|
protected $position = 0; |
93
|
|
|
|
94
|
|
|
/** |
95
|
|
|
* @var array |
96
|
|
|
*/ |
97
|
|
|
protected $optionNames = [ |
98
|
|
|
'batchSize', |
99
|
|
|
'readPreference', |
100
|
|
|
]; |
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* @return Cursor |
104
|
|
|
*/ |
105
|
|
|
abstract protected function ensureCursor(); |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* @return array |
109
|
|
|
*/ |
110
|
|
|
abstract protected function getCursorInfo(); |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Create a new cursor |
114
|
|
|
* @link http://www.php.net/manual/en/mongocursor.construct.php |
115
|
|
|
* @param \MongoClient $connection Database connection. |
116
|
|
|
* @param string $ns Full name of database and collection. |
117
|
|
|
*/ |
118
|
92 |
|
public function __construct(\MongoClient $connection, $ns) |
119
|
|
|
{ |
120
|
92 |
|
$this->connection = $connection; |
121
|
92 |
|
$this->ns = $ns; |
122
|
|
|
|
123
|
92 |
|
$nsParts = explode('.', $ns); |
124
|
92 |
|
$dbName = array_shift($nsParts); |
125
|
92 |
|
$collectionName = implode('.', $nsParts); |
126
|
|
|
|
127
|
92 |
|
$this->db = $connection->selectDB($dbName)->getDb(); |
128
|
|
|
|
129
|
92 |
|
if ($collectionName) { |
130
|
56 |
|
$this->collection = $connection->selectCollection($dbName, $collectionName)->getCollection(); |
131
|
56 |
|
} |
132
|
92 |
|
} |
133
|
|
|
|
134
|
|
|
/** |
135
|
|
|
* Returns the current element |
136
|
|
|
* @link http://www.php.net/manual/en/mongocursor.current.php |
137
|
|
|
* @return array |
138
|
|
|
*/ |
139
|
44 |
|
public function current() |
140
|
|
|
{ |
141
|
44 |
|
return $this->current; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
/** |
145
|
|
|
* Returns the current result's _id |
146
|
|
|
* @link http://www.php.net/manual/en/mongocursor.key.php |
147
|
|
|
* @return string The current result's _id as a string. |
148
|
|
|
*/ |
149
|
28 |
|
public function key() |
150
|
|
|
{ |
151
|
28 |
|
return $this->key; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* Advances the cursor to the next result, and returns that result |
156
|
|
|
* @link http://www.php.net/manual/en/mongocursor.next.php |
157
|
|
|
* @throws \MongoConnectionException |
158
|
|
|
* @throws \MongoCursorTimeoutException |
159
|
|
|
* @return array Returns the next object |
160
|
|
|
*/ |
161
|
46 |
|
public function next() |
162
|
|
|
{ |
163
|
46 |
View Code Duplication |
if (! $this->startedIterating) { |
|
|
|
|
164
|
1 |
|
$this->ensureIterator(); |
165
|
1 |
|
$this->startedIterating = true; |
166
|
1 |
|
} else { |
167
|
46 |
|
if ($this->cursorNeedsAdvancing) { |
168
|
45 |
|
$this->ensureIterator()->next(); |
169
|
45 |
|
} |
170
|
|
|
|
171
|
46 |
|
$this->cursorNeedsAdvancing = true; |
172
|
46 |
|
$this->position++; |
173
|
|
|
} |
174
|
|
|
|
175
|
46 |
|
return $this->storeIteratorState(); |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* Returns the cursor to the beginning of the result set |
180
|
|
|
* @throws \MongoConnectionException |
181
|
|
|
* @throws \MongoCursorTimeoutException |
182
|
|
|
* @return void |
183
|
|
|
*/ |
184
|
77 |
|
public function rewind() |
185
|
|
|
{ |
186
|
|
|
// We can recreate the cursor to allow it to be rewound |
187
|
77 |
|
$this->reset(); |
188
|
77 |
|
$this->startedIterating = true; |
189
|
77 |
|
$this->position = 0; |
190
|
77 |
|
$this->ensureIterator()->rewind(); |
191
|
75 |
|
$this->storeIteratorState(); |
192
|
75 |
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Checks if the cursor is reading a valid result. |
196
|
|
|
* @link http://www.php.net/manual/en/mongocursor.valid.php |
197
|
|
|
* @return boolean If the current result is not null. |
198
|
|
|
*/ |
199
|
75 |
|
public function valid() |
200
|
|
|
{ |
201
|
75 |
|
return $this->valid; |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Limits the number of elements returned in one batch. |
206
|
|
|
* |
207
|
|
|
* @link http://docs.php.net/manual/en/mongocursor.batchsize.php |
208
|
|
|
* @param int|null $batchSize The number of results to return per batch |
209
|
|
|
* @return $this Returns this cursor. |
210
|
|
|
*/ |
211
|
2 |
|
public function batchSize($batchSize) |
212
|
|
|
{ |
213
|
2 |
|
$this->batchSize = $batchSize; |
214
|
|
|
|
215
|
2 |
|
return $this; |
216
|
|
|
} |
217
|
|
|
|
218
|
|
|
/** |
219
|
|
|
* Checks if there are documents that have not been sent yet from the database for this cursor |
220
|
|
|
* @link http://www.php.net/manual/en/mongocursor.dead.php |
221
|
|
|
* @return boolean Returns if there are more results that have not been sent to the client, yet. |
222
|
|
|
*/ |
223
|
|
|
public function dead() |
224
|
|
|
{ |
225
|
|
|
return $this->ensureCursor()->isDead(); |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* @return array |
230
|
|
|
*/ |
231
|
5 |
|
public function info() |
232
|
|
|
{ |
233
|
5 |
|
return $this->getCursorInfo() + $this->getIterationInfo(); |
234
|
|
|
} |
235
|
|
|
|
236
|
|
|
/** |
237
|
|
|
* @link http://www.php.net/manual/en/mongocursor.setreadpreference.php |
238
|
|
|
* @param string $readPreference |
239
|
|
|
* @param array $tags |
240
|
|
|
* @return $this Returns this cursor. |
241
|
|
|
*/ |
242
|
92 |
|
public function setReadPreference($readPreference, $tags = null) |
243
|
|
|
{ |
244
|
92 |
|
$this->setReadPreferenceFromParameters($readPreference, $tags); |
245
|
|
|
|
246
|
92 |
|
return $this; |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
/** |
250
|
|
|
* Sets a client-side timeout for this query |
251
|
|
|
* @link http://www.php.net/manual/en/mongocursor.timeout.php |
252
|
|
|
* @param int $ms The number of milliseconds for the cursor to wait for a response. By default, the cursor will wait forever. |
253
|
|
|
* @return $this Returns this cursor |
254
|
|
|
*/ |
255
|
|
|
public function timeout($ms) |
|
|
|
|
256
|
|
|
{ |
257
|
|
|
trigger_error('The ' . __METHOD__ . ' method is not implemented in mongo-php-adapter', E_USER_WARNING); |
258
|
|
|
return $this; |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
/** |
262
|
|
|
* Applies all options set on the cursor, overwriting any options that have already been set |
263
|
|
|
* |
264
|
|
|
* @param array $optionNames Array of option names to be applied (will be read from properties) |
265
|
|
|
* @return array |
266
|
|
|
*/ |
267
|
86 |
|
protected function getOptions($optionNames = null) |
268
|
|
|
{ |
269
|
86 |
|
$options = []; |
270
|
|
|
|
271
|
86 |
|
if ($optionNames === null) { |
272
|
79 |
|
$optionNames = $this->optionNames; |
273
|
79 |
|
} |
274
|
|
|
|
275
|
86 |
|
foreach ($optionNames as $option) { |
276
|
86 |
|
$converter = 'convert' . ucfirst($option); |
277
|
86 |
|
$value = method_exists($this, $converter) ? $this->$converter() : $this->$option; |
278
|
|
|
|
279
|
86 |
|
if ($value === null) { |
280
|
86 |
|
continue; |
281
|
|
|
} |
282
|
|
|
|
283
|
81 |
|
$options[$option] = $value; |
284
|
86 |
|
} |
285
|
|
|
|
286
|
86 |
|
return $options; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* @return \Iterator |
291
|
|
|
*/ |
292
|
79 |
|
protected function ensureIterator() |
293
|
|
|
{ |
294
|
79 |
|
if ($this->iterator === null) { |
295
|
|
|
// MongoDB\Driver\Cursor needs to be wrapped into a \Generator so that a valid \Iterator with working implementations of |
296
|
|
|
// next, current, valid, key and rewind is returned. These methods don't work if we wrap the Cursor inside an \IteratorIterator |
297
|
79 |
|
$this->iterator = $this->wrapTraversable($this->ensureCursor()); |
298
|
77 |
|
} |
299
|
|
|
|
300
|
77 |
|
return $this->iterator; |
301
|
|
|
} |
302
|
|
|
|
303
|
|
|
/** |
304
|
|
|
* @param \Traversable $traversable |
305
|
|
|
* @return \Generator |
306
|
|
|
*/ |
307
|
36 |
|
protected function wrapTraversable(\Traversable $traversable) |
308
|
|
|
{ |
309
|
36 |
|
foreach ($traversable as $key => $value) { |
310
|
22 |
|
yield $key => $value; |
311
|
36 |
|
} |
312
|
36 |
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* @throws \MongoCursorException |
316
|
|
|
*/ |
317
|
25 |
|
protected function errorIfOpened() |
318
|
|
|
{ |
319
|
25 |
|
if ($this->cursor === null) { |
320
|
25 |
|
return; |
321
|
|
|
} |
322
|
|
|
|
323
|
|
|
throw new \MongoCursorException('cannot modify cursor after beginning iteration.'); |
324
|
|
|
} |
325
|
|
|
|
326
|
|
|
/** |
327
|
|
|
* @return array |
328
|
|
|
*/ |
329
|
5 |
|
protected function getIterationInfo() |
330
|
|
|
{ |
331
|
|
|
$iterationInfo = [ |
332
|
5 |
|
'started_iterating' => $this->cursor !== null, |
333
|
5 |
|
]; |
334
|
|
|
|
335
|
5 |
|
if ($this->cursor !== null) { |
336
|
5 |
|
switch ($this->cursor->getServer()->getType()) { |
337
|
5 |
|
case \MongoDB\Driver\Server::TYPE_RS_ARBITER: |
338
|
|
|
$typeString = 'ARBITER'; |
339
|
|
|
break; |
340
|
5 |
|
case \MongoDB\Driver\Server::TYPE_MONGOS: |
341
|
|
|
$typeString = 'MONGOS'; |
342
|
|
|
break; |
343
|
5 |
|
case \MongoDB\Driver\Server::TYPE_RS_PRIMARY: |
344
|
|
|
$typeString = 'PRIMARY'; |
345
|
|
|
break; |
346
|
5 |
|
case \MongoDB\Driver\Server::TYPE_RS_SECONDARY: |
347
|
|
|
$typeString = 'SECONDARY'; |
348
|
|
|
break; |
349
|
5 |
|
default: |
350
|
5 |
|
$typeString = 'STANDALONE'; |
351
|
5 |
|
} |
352
|
|
|
|
353
|
5 |
|
$cursorId = (string) $this->cursor->getId(); |
354
|
|
|
$iterationInfo += [ |
355
|
5 |
|
'id' => (int) $cursorId, |
356
|
5 |
|
'at' => $this->position, |
357
|
5 |
|
'numReturned' => $this->position, // This can't be obtained from the new cursor |
358
|
5 |
|
'server' => sprintf('%s:%d;-;.;%d', $this->cursor->getServer()->getHost(), $this->cursor->getServer()->getPort(), getmypid()), |
359
|
5 |
|
'host' => $this->cursor->getServer()->getHost(), |
360
|
5 |
|
'port' => $this->cursor->getServer()->getPort(), |
361
|
5 |
|
'connection_type_desc' => $typeString, |
362
|
|
|
]; |
363
|
5 |
|
} |
364
|
|
|
|
365
|
5 |
|
return $iterationInfo; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* @throws \Exception |
370
|
|
|
*/ |
371
|
|
|
protected function notImplemented() |
372
|
|
|
{ |
373
|
|
|
throw new \Exception('Not implemented'); |
374
|
|
|
} |
375
|
|
|
|
376
|
|
|
/** |
377
|
|
|
* Clears the cursor |
378
|
|
|
* |
379
|
|
|
* This is generic but implemented as protected since it's only exposed in MongoCursor |
380
|
|
|
*/ |
381
|
78 |
|
protected function reset() |
382
|
|
|
{ |
383
|
78 |
|
$this->startedIterating = false; |
384
|
78 |
|
$this->cursorNeedsAdvancing = true; |
385
|
78 |
|
$this->cursor = null; |
386
|
78 |
|
$this->iterator = null; |
387
|
78 |
|
$this->storeIteratorState(); |
388
|
78 |
|
} |
389
|
|
|
|
390
|
|
|
/** |
391
|
|
|
* @return array |
392
|
|
|
*/ |
393
|
3 |
|
public function __sleep() |
394
|
|
|
{ |
395
|
3 |
|
return ['batchSize', 'connection', 'iterator', 'ns', 'optionNames', 'position', 'startedIterating']; |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
/** |
399
|
|
|
* Stores the current cursor element. |
400
|
|
|
* |
401
|
|
|
* This is necessary because hasNext() might advance the iterator but we still |
402
|
|
|
* need to be able to return the current object. |
403
|
|
|
*/ |
404
|
79 |
|
protected function storeIteratorState() |
405
|
|
|
{ |
406
|
79 |
|
if (! $this->startedIterating) { |
407
|
78 |
|
$this->current = null; |
408
|
78 |
|
$this->key = null; |
409
|
78 |
|
$this->valid = false; |
410
|
78 |
|
return null; |
411
|
|
|
} |
412
|
|
|
|
413
|
77 |
|
$this->current = $this->ensureIterator()->current(); |
414
|
77 |
|
$this->key = $this->ensureIterator()->key(); |
415
|
77 |
|
$this->valid = $this->ensureIterator()->valid(); |
416
|
|
|
|
417
|
77 |
|
if ($this->current !== null) { |
418
|
46 |
|
$this->current = TypeConverter::toLegacy($this->current); |
419
|
46 |
|
} |
420
|
|
|
|
421
|
77 |
|
return $this->current; |
422
|
|
|
} |
423
|
|
|
} |
424
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.