1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* This file is part of the PHPMongo package. |
5
|
|
|
* |
6
|
|
|
* (c) Dmytro Sokil <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Sokil\Mongo; |
13
|
|
|
|
14
|
|
|
use Sokil\Mongo\Exception\CursorException; |
15
|
|
|
use Sokil\Mongo\Exception\FeatureNotSupportedException; |
16
|
|
|
|
17
|
|
|
class Cursor implements \Iterator, \Countable |
18
|
|
|
{ |
19
|
|
|
/** |
20
|
|
|
* |
21
|
|
|
* @var \Sokil\Mongo\Client |
22
|
|
|
*/ |
23
|
|
|
private $client; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* |
27
|
|
|
* @var \Sokil\Mongo\Collection |
28
|
|
|
*/ |
29
|
|
|
private $collection; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* |
33
|
|
|
* @var array |
34
|
|
|
*/ |
35
|
|
|
private $fields = array(); |
36
|
|
|
|
37
|
|
|
/** |
38
|
|
|
* |
39
|
|
|
* @var \MongoCursor |
40
|
|
|
*/ |
41
|
|
|
private $cursor; |
42
|
|
|
/** |
43
|
|
|
* |
44
|
|
|
* @var \Sokil\Mongo\Expression |
45
|
|
|
*/ |
46
|
|
|
private $expression; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Offset |
50
|
|
|
* @var int |
51
|
|
|
*/ |
52
|
|
|
private $skip = 0; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Limit |
56
|
|
|
* @var int |
57
|
|
|
*/ |
58
|
|
|
private $limit = 0; |
59
|
|
|
|
60
|
|
|
/** |
61
|
|
|
* Definition of sort |
62
|
|
|
* @var array |
63
|
|
|
*/ |
64
|
|
|
private $sort = array(); |
65
|
|
|
|
66
|
|
|
/** |
67
|
|
|
* Definition of read preference |
68
|
|
|
* @var array |
69
|
|
|
*/ |
70
|
|
|
private $readPreference = array(); |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* Return result as array or as Document instance |
74
|
|
|
* @var boolean |
75
|
|
|
*/ |
76
|
|
|
private $isResultAsArray = false; |
77
|
|
|
|
78
|
|
|
/** |
79
|
|
|
* Cursor options |
80
|
|
|
* @var array |
81
|
|
|
*/ |
82
|
|
|
private $options = array( |
83
|
|
|
'expressionClass' => '\Sokil\Mongo\Expression', |
84
|
|
|
/** |
85
|
|
|
* @link http://docs.mongodb.org/manual/reference/method/cursor.batchSize/ |
86
|
|
|
* @var int number of documents to return in each batch of the response from the MongoDB instance |
87
|
|
|
*/ |
88
|
|
|
'batchSize' => null, |
89
|
|
|
// client timeout |
90
|
|
|
'clientTimeout' => null, |
91
|
|
|
// Specifies a cumulative time limit in milliseconds to be allowed by the server for processing operations on the cursor. |
92
|
|
|
'serverTimeout' => null, |
93
|
|
|
); |
94
|
|
|
|
95
|
|
|
/** |
96
|
|
|
* Use document pool to create Document object from array |
97
|
|
|
* @var bool |
98
|
|
|
*/ |
99
|
|
|
private $isDocumentPoolUsed = true; |
100
|
|
|
|
101
|
|
|
/** |
102
|
|
|
* Index hinting |
103
|
|
|
* @param \Sokil\Mongo\Collection $collection |
104
|
|
|
* @param array $options |
105
|
|
|
*/ |
106
|
|
|
private $hint; |
107
|
|
|
|
108
|
|
|
public function __construct(Collection $collection, array $options = null) |
109
|
|
|
{ |
110
|
|
|
$this->collection = $collection; |
111
|
|
|
|
112
|
|
|
$this->client = $this->collection->getDatabase()->getClient(); |
113
|
|
|
|
114
|
|
|
if ($options) { |
115
|
|
|
$this->options = $options + $this->options; |
116
|
|
|
} |
117
|
|
|
|
118
|
|
|
// expression |
119
|
|
|
$this->expression = $this->expression(); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
public function __call($name, $arguments) |
123
|
|
|
{ |
124
|
|
|
call_user_func_array(array($this->expression, $name), $arguments); |
125
|
|
|
return $this; |
126
|
|
|
} |
127
|
|
|
|
128
|
|
|
/** |
129
|
|
|
* Get option |
130
|
|
|
* |
131
|
|
|
* @param string|int $name |
132
|
|
|
* @return mixed |
133
|
|
|
*/ |
134
|
|
|
public function getOption($name, $default = null) |
135
|
|
|
{ |
136
|
|
|
return isset($this->options[$name]) ? $this->options[$name] : $default; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Get result as array |
141
|
|
|
* @return $this |
142
|
|
|
*/ |
143
|
|
|
public function asArray() |
144
|
|
|
{ |
145
|
|
|
$this->isResultAsArray = true; |
146
|
|
|
return $this; |
147
|
|
|
} |
148
|
|
|
|
149
|
|
|
/** |
150
|
|
|
* Get result as object |
151
|
|
|
* @return $this |
152
|
|
|
*/ |
153
|
|
|
public function asObject() |
154
|
|
|
{ |
155
|
|
|
$this->isResultAsArray = false; |
156
|
|
|
return $this; |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
/** |
160
|
|
|
* Check if result returned as array |
161
|
|
|
* @return bool |
162
|
|
|
*/ |
163
|
|
|
public function isResultAsArray() |
164
|
|
|
{ |
165
|
|
|
return $this->isResultAsArray; |
166
|
|
|
} |
167
|
|
|
|
168
|
|
|
/** |
169
|
|
|
* Return only specified fields |
170
|
|
|
* |
171
|
|
|
* @param array $fields |
172
|
|
|
* @return \Sokil\Mongo\Cursor |
173
|
|
|
*/ |
174
|
|
|
public function fields(array $fields) |
175
|
|
|
{ |
176
|
|
|
$this->fields = array_fill_keys($fields, 1); |
177
|
|
|
|
178
|
|
|
$this->skipDocumentPool(); |
179
|
|
|
|
180
|
|
|
return $this; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* Return all fields except specified |
185
|
|
|
* |
186
|
|
|
* @param array $fields |
187
|
|
|
* @return \Sokil\Mongo\Cursor |
188
|
|
|
*/ |
189
|
|
|
public function skipFields(array $fields) |
190
|
|
|
{ |
191
|
|
|
$this->fields = array_fill_keys($fields, 0); |
192
|
|
|
|
193
|
|
|
$this->skipDocumentPool(); |
194
|
|
|
|
195
|
|
|
return $this; |
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
/** |
199
|
|
|
* Append field to accept list |
200
|
|
|
* |
201
|
|
|
* @param string $field field name |
202
|
|
|
* @return \Sokil\Mongo\Cursor |
203
|
|
|
*/ |
204
|
|
|
public function field($field) |
205
|
|
|
{ |
206
|
|
|
$this->fields[$field] = 1; |
207
|
|
|
|
208
|
|
|
$this->skipDocumentPool(); |
209
|
|
|
|
210
|
|
|
return $this; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* Append field to skip list |
215
|
|
|
* |
216
|
|
|
* @param string $field field name |
217
|
|
|
* @return Cursor |
218
|
|
|
*/ |
219
|
|
|
public function skipField($field) |
220
|
|
|
{ |
221
|
|
|
$this->fields[$field] = 0; |
222
|
|
|
|
223
|
|
|
$this->skipDocumentPool(); |
224
|
|
|
|
225
|
|
|
return $this; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/** |
229
|
|
|
* Paginate list of sub-documents |
230
|
|
|
* |
231
|
|
|
* @param string $field |
232
|
|
|
* @param integer $limit |
233
|
|
|
* @param integer $skip |
234
|
|
|
* @return \Sokil\Mongo\Cursor |
235
|
|
|
* @throws Exception |
236
|
|
|
*/ |
237
|
|
|
public function slice($field, $limit, $skip = null) |
238
|
|
|
{ |
239
|
|
|
$limit = (int) $limit; |
240
|
|
|
$skip = (int) $skip; |
241
|
|
|
|
242
|
|
|
if($skip) { |
243
|
|
|
$this->fields[$field] = array('$slice' => array($skip, $limit)); |
244
|
|
|
} |
245
|
|
|
else { |
246
|
|
|
$this->fields[$field] = array('$slice' => $limit); |
247
|
|
|
} |
248
|
|
|
|
249
|
|
|
$this->skipDocumentPool(); |
250
|
|
|
|
251
|
|
|
return $this; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/** |
255
|
|
|
* Merge expression |
256
|
|
|
* @param \Sokil\Mongo\Expression $expression |
257
|
|
|
* @return \Sokil\Mongo\Cursor |
258
|
|
|
*/ |
259
|
|
|
public function query(Expression $expression) |
260
|
|
|
{ |
261
|
|
|
$this->expression->merge($expression); |
262
|
|
|
return $this; |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* Helper to create new expression |
267
|
|
|
* |
268
|
|
|
* @return \Sokil\Mongo\Expression |
269
|
|
|
*/ |
270
|
|
|
public function expression() |
271
|
|
|
{ |
272
|
|
|
return new $this->options['expressionClass']; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Filter by list of \MongoId |
277
|
|
|
* |
278
|
|
|
* @param array $idList list of ids |
279
|
|
|
* @return \Sokil\Mongo\Cursor |
280
|
|
|
*/ |
281
|
|
|
public function byIdList(array $idList) |
282
|
|
|
{ |
283
|
|
|
$this->expression->whereIn('_id', self::mixedToMongoIdList($idList)); |
284
|
|
|
return $this; |
285
|
|
|
} |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Filter by id |
289
|
|
|
* |
290
|
|
|
* @param string|\MongoId $id id of document |
291
|
|
|
* @return \Sokil\Mongo\Cursor |
292
|
|
|
*/ |
293
|
|
|
public function byId($id) |
294
|
|
|
{ |
295
|
|
|
if($id instanceof \MongoId) { |
296
|
|
|
$this->expression->where('_id', $id); |
297
|
|
|
} else { |
298
|
|
|
try { |
299
|
|
|
$this->expression->where('_id', new \MongoId($id)); |
300
|
|
|
} catch (\MongoException $e) { |
301
|
|
|
$this->expression->where('_id', $id); |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
return $this; |
306
|
|
|
} |
307
|
|
|
|
308
|
|
|
/** |
309
|
|
|
* Skip defined number of documents |
310
|
|
|
* |
311
|
|
|
* @param int $skip number of documents to skip |
312
|
|
|
* @return \Sokil\Mongo\Cursor |
313
|
|
|
*/ |
314
|
|
|
public function skip($skip) |
315
|
|
|
{ |
316
|
|
|
$this->skip = (int) $skip; |
317
|
|
|
|
318
|
|
|
return $this; |
319
|
|
|
} |
320
|
|
|
|
321
|
|
|
/** |
322
|
|
|
* Limit result set to specified number of elements |
323
|
|
|
* |
324
|
|
|
* @param int $limit number of elements in result set |
325
|
|
|
* @param int|null $offset number of elements to skip |
326
|
|
|
* @return \Sokil\Mongo\Cursor |
327
|
|
|
*/ |
328
|
|
|
public function limit($limit, $offset = null) |
329
|
|
|
{ |
330
|
|
|
$this->limit = (int) $limit; |
331
|
|
|
|
332
|
|
|
if(null !== $offset) { |
333
|
|
|
$this->skip($offset); |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
return $this; |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Specifies the number of documents to return in each batch of the response from the MongoDB instance. |
341
|
|
|
* |
342
|
|
|
* @param int $size number of documents |
343
|
|
|
* @link http://docs.mongodb.org/manual/reference/method/cursor.batchSize/ |
344
|
|
|
* @return \Sokil\Mongo\Cursor |
345
|
|
|
*/ |
346
|
|
|
public function setBatchSize($size) |
347
|
|
|
{ |
348
|
|
|
$this->options['batchSize'] = (int) $size; |
349
|
|
|
|
350
|
|
|
return $this; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
/** |
354
|
|
|
* Instructs the driver to stop waiting for a response and throw a |
355
|
|
|
* MongoCursorTimeoutException after a set time, |
356
|
|
|
* A timeout can be set at any time and will affect subsequent queries on |
357
|
|
|
* the cursor, including fetching more results from the database. |
358
|
|
|
* @param type $ms |
359
|
|
|
* @return \Sokil\Mongo\Cursor |
360
|
|
|
*/ |
361
|
|
|
public function setClientTimeout($ms) |
362
|
|
|
{ |
363
|
|
|
$this->options['clientTimeout'] = (int) $ms; |
364
|
|
|
|
365
|
|
|
return $this; |
366
|
|
|
} |
367
|
|
|
|
368
|
|
|
/** |
369
|
|
|
* Server-side timeout for a query, |
370
|
|
|
* Specifies a cumulative time limit in milliseconds to be allowed |
371
|
|
|
* by the server for processing operations on the cursor. |
372
|
|
|
* @param type $ms |
373
|
|
|
* @return \Sokil\Mongo\Cursor |
374
|
|
|
*/ |
375
|
|
|
public function setServerTimeout($ms) |
376
|
|
|
{ |
377
|
|
|
$this->options['serverTimeout'] = (int) $ms; |
378
|
|
|
|
379
|
|
|
return $this; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* Sort result by specified keys and directions |
384
|
|
|
* |
385
|
|
|
* An array of fields by which to sort. Each element in the array has as key the field name, and as value either |
386
|
|
|
* 1 for ascending sort, or -1 for descending sort. Each result is first sorted on the first field in the array, |
387
|
|
|
* then (if it exists) on the second field in the array, etc. This means that the order of the fields in the |
388
|
|
|
* fields array is important. See also the examples section. |
389
|
|
|
* |
390
|
|
|
* @param array $sort |
391
|
|
|
* @return \Sokil\Mongo\Cursor |
392
|
|
|
*/ |
393
|
|
|
public function sort(array $sort) |
394
|
|
|
{ |
395
|
|
|
$this->sort = $sort; |
396
|
|
|
return $this; |
397
|
|
|
} |
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* |
401
|
|
|
* @return \MongoCursor |
402
|
|
|
*/ |
403
|
|
|
private function getCursor() |
404
|
|
|
{ |
405
|
|
|
if($this->cursor) { |
406
|
|
|
return $this->cursor; |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
$this->cursor = $this->collection |
410
|
|
|
->getMongoCollection() |
411
|
|
|
->find($this->expression->toArray(), $this->fields); |
412
|
|
|
|
413
|
|
|
if($this->skip) { |
414
|
|
|
$this->cursor->skip($this->skip); |
415
|
|
|
} |
416
|
|
|
|
417
|
|
|
if($this->limit) { |
418
|
|
|
$this->cursor->limit($this->limit); |
419
|
|
|
} |
420
|
|
|
|
421
|
|
|
if($this->options['batchSize']) { |
422
|
|
|
$this->cursor->batchSize($this->options['batchSize']); |
423
|
|
|
} |
424
|
|
|
|
425
|
|
|
if($this->options['clientTimeout']) { |
426
|
|
|
$this->cursor->timeout($this->options['clientTimeout']); |
427
|
|
|
} |
428
|
|
|
|
429
|
|
|
if($this->options['serverTimeout']) { |
430
|
|
|
$this->cursor->maxTimeMS($this->options['clientTimeout']); |
431
|
|
|
} |
432
|
|
|
|
433
|
|
|
if (!empty($this->sort)) { |
434
|
|
|
$this->cursor->sort($this->sort); |
435
|
|
|
} |
436
|
|
|
|
437
|
|
|
if($this->hint) { |
438
|
|
|
$this->cursor->hint($this->hint); |
439
|
|
|
} |
440
|
|
|
|
441
|
|
|
// log request |
442
|
|
|
if($this->client->hasLogger()) { |
443
|
|
|
$this->client->getLogger()->debug(get_called_class() . ': ' . json_encode(array( |
444
|
|
|
'collection' => $this->collection->getName(), |
445
|
|
|
'query' => $this->expression->toArray(), |
446
|
|
|
'project' => $this->fields, |
447
|
|
|
'sort' => $this->sort, |
448
|
|
|
))); |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
$this->cursor->rewind(); |
452
|
|
|
|
453
|
|
|
// define read preferences |
454
|
|
|
if (!empty($this->readPreference)) { |
455
|
|
|
$this->cursor->setReadPreference( |
456
|
|
|
$this->readPreference['type'], |
457
|
|
|
$this->readPreference['tagsets'] |
458
|
|
|
); |
459
|
|
|
} |
460
|
|
|
|
461
|
|
|
return $this->cursor; |
462
|
|
|
} |
463
|
|
|
|
464
|
|
|
/** |
465
|
|
|
* Count documents in result without applying limit and offset |
466
|
|
|
* @return int count |
467
|
|
|
*/ |
468
|
|
|
public function count() |
469
|
|
|
{ |
470
|
|
|
return (int) $this->collection |
471
|
|
|
->getMongoCollection() |
472
|
|
|
->count($this->expression->toArray()); |
473
|
|
|
} |
474
|
|
|
|
475
|
|
|
public function explain() |
476
|
|
|
{ |
477
|
|
|
if (Client::isEmulationMode()) { |
478
|
|
|
throw new FeatureNotSupportedException('Feature not implemented in emulation mode'); |
479
|
|
|
} |
480
|
|
|
|
481
|
|
|
return $this->getCursor()->explain(); |
482
|
|
|
} |
483
|
|
|
|
484
|
|
|
/** |
485
|
|
|
* Count documents in result with applying limit and offset |
486
|
|
|
* @return int count |
487
|
|
|
*/ |
488
|
|
|
public function limitedCount() |
489
|
|
|
{ |
490
|
|
|
return (int) $this->collection |
491
|
|
|
->getMongoCollection() |
492
|
|
|
->count($this->expression->toArray(), $this->limit, $this->skip); |
493
|
|
|
} |
494
|
|
|
|
495
|
|
|
|
496
|
|
|
/** |
497
|
|
|
* Gte list of \MongoId of current search query |
498
|
|
|
* @return array |
499
|
|
|
*/ |
500
|
|
|
public function getIdList() |
501
|
|
|
{ |
502
|
|
|
return self::mixedToMongoIdList($this->findAll()); |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
/** |
506
|
|
|
* Find one document which correspond to expression |
507
|
|
|
* |
508
|
|
|
* @return \Sokil\Mongo\Document|array|null |
509
|
|
|
*/ |
510
|
|
|
public function findOne() |
511
|
|
|
{ |
512
|
|
|
try { |
513
|
|
|
$mongoDocument = $this->collection |
514
|
|
|
->getMongoCollection() |
515
|
|
|
->findOne( |
516
|
|
|
$this->expression->toArray(), |
517
|
|
|
$this->fields |
518
|
|
|
); |
519
|
|
|
} catch (\Exception $e) { |
520
|
|
|
throw new CursorException( |
521
|
|
|
$e->getMessage(), |
522
|
|
|
$e->getCode(), |
523
|
|
|
$e |
524
|
|
|
); |
525
|
|
|
} |
526
|
|
|
|
527
|
|
|
if (empty($mongoDocument)) { |
528
|
|
|
return null; |
529
|
|
|
} |
530
|
|
|
|
531
|
|
|
if (true === $this->isResultAsArray) { |
532
|
|
|
return $mongoDocument; |
533
|
|
|
} |
534
|
|
|
|
535
|
|
|
return $this->collection->hydrate( |
536
|
|
|
$mongoDocument, |
537
|
|
|
$this->isDocumentPoolUsed() |
538
|
|
|
); |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
/** |
542
|
|
|
* |
543
|
|
|
* @return array result of searching |
544
|
|
|
*/ |
545
|
|
|
public function findAll() |
546
|
|
|
{ |
547
|
|
|
return iterator_to_array($this); |
548
|
|
|
} |
549
|
|
|
|
550
|
|
|
/** |
551
|
|
|
* Get random document |
552
|
|
|
* @return |
553
|
|
|
*/ |
554
|
|
|
public function findRandom() |
555
|
|
|
{ |
556
|
|
|
$count = $this->count(); |
557
|
|
|
|
558
|
|
|
if(!$count) { |
559
|
|
|
return null; |
560
|
|
|
} |
561
|
|
|
|
562
|
|
|
if(1 === $count) { |
563
|
|
|
return $this->findOne(); |
564
|
|
|
} |
565
|
|
|
|
566
|
|
|
return $this |
567
|
|
|
->skip(mt_rand(0, $count - 1)) |
568
|
|
|
->limit(1) |
569
|
|
|
->current(); |
570
|
|
|
} |
571
|
|
|
|
572
|
|
|
/** |
573
|
|
|
* Get query builder's expression |
574
|
|
|
* |
575
|
|
|
* @return Expression |
576
|
|
|
*/ |
577
|
|
|
public function getExpression() |
578
|
|
|
{ |
579
|
|
|
return $this->expression; |
580
|
|
|
} |
581
|
|
|
|
582
|
|
|
/** |
583
|
|
|
* Get MongoDB query array |
584
|
|
|
* |
585
|
|
|
* @return array |
586
|
|
|
*/ |
587
|
|
|
public function getMongoQuery() |
588
|
|
|
{ |
589
|
|
|
return $this->expression->toArray(); |
590
|
|
|
} |
591
|
|
|
|
592
|
|
|
/** |
593
|
|
|
* Return the values from a single field in the result set of documents |
594
|
|
|
* |
595
|
|
|
* @param string $fieldName |
596
|
|
|
* @return array |
597
|
|
|
*/ |
598
|
|
|
public function pluck($fieldName) |
599
|
|
|
{ |
600
|
|
|
// if field with embedded document or native php function not exists |
601
|
|
|
if (false !== strpos($fieldName, '.') || !function_exists('array_column')) { |
602
|
|
|
return $this->pluckDotNotated($fieldName); |
603
|
|
|
} |
604
|
|
|
|
605
|
|
|
// use native php function if field without embedded document |
606
|
|
View Code Duplication |
if ($this->isResultAsArray) { |
|
|
|
|
607
|
|
|
$result = $this->findAll(); |
608
|
|
|
} else { |
609
|
|
|
$cursor = clone $this; |
610
|
|
|
$result = $cursor->asArray()->findAll(); |
611
|
|
|
unset($cursor); |
612
|
|
|
} |
613
|
|
|
|
614
|
|
|
return array_column($result, $fieldName, '_id'); |
615
|
|
|
|
616
|
|
|
} |
617
|
|
|
|
618
|
|
|
/** |
619
|
|
|
* Pluck by dot-notated field name |
620
|
|
|
* |
621
|
|
|
* @param string $fieldName field name |
622
|
|
|
* @return array |
623
|
|
|
*/ |
624
|
|
|
private function pluckDotNotated($fieldName) |
625
|
|
|
{ |
626
|
|
View Code Duplication |
if ($this->isResultAsArray) { |
|
|
|
|
627
|
|
|
$cursor = clone $this; |
628
|
|
|
$result = $cursor->asObject()->findAll(); |
629
|
|
|
unset($cursor); |
630
|
|
|
} else { |
631
|
|
|
$result = $this->findAll(); |
632
|
|
|
} |
633
|
|
|
|
634
|
|
|
$list = array(); |
635
|
|
|
foreach($result as $key => $document) { |
636
|
|
|
$list[$key] = $document->get($fieldName); |
637
|
|
|
} |
638
|
|
|
|
639
|
|
|
return $list; |
640
|
|
|
} |
641
|
|
|
|
642
|
|
|
/** |
643
|
|
|
* Get document instance and remove it from collection |
644
|
|
|
* |
645
|
|
|
* @return \Sokil\Mongo\Document |
646
|
|
|
*/ |
647
|
|
|
public function findAndRemove() |
648
|
|
|
{ |
649
|
|
|
$mongoDocument = $this->collection->getMongoCollection()->findAndModify( |
650
|
|
|
$this->expression->toArray(), |
651
|
|
|
null, |
652
|
|
|
$this->fields, |
653
|
|
|
array( |
654
|
|
|
'remove' => true, |
655
|
|
|
'sort' => $this->sort, |
656
|
|
|
) |
657
|
|
|
); |
658
|
|
|
|
659
|
|
|
if (empty($mongoDocument)) { |
660
|
|
|
return null; |
661
|
|
|
} |
662
|
|
|
|
663
|
|
|
return $this->collection->hydrate( |
664
|
|
|
$mongoDocument, |
665
|
|
|
$this->isDocumentPoolUsed() |
666
|
|
|
); |
667
|
|
|
} |
668
|
|
|
|
669
|
|
|
/** |
670
|
|
|
* Find first document and update it |
671
|
|
|
* |
672
|
|
|
* @param Operator $operator operations with document to update |
673
|
|
|
* @param bool $upsert if document not found - create |
674
|
|
|
* @param bool $returnUpdated if true - return updated document |
675
|
|
|
* |
676
|
|
|
* @return null|Document |
677
|
|
|
*/ |
678
|
|
|
public function findAndUpdate(Operator $operator, $upsert = false, $returnUpdated = true) |
679
|
|
|
{ |
680
|
|
|
$mongoDocument = $this->collection |
681
|
|
|
->getMongoCollection() |
682
|
|
|
->findAndModify( |
683
|
|
|
$this->expression->toArray(), |
684
|
|
|
$operator ? $operator->toArray() : null, |
685
|
|
|
$this->fields, |
686
|
|
|
array( |
687
|
|
|
'new' => $returnUpdated, |
688
|
|
|
'sort' => $this->sort, |
689
|
|
|
'upsert' => $upsert, |
690
|
|
|
) |
691
|
|
|
); |
692
|
|
|
|
693
|
|
|
if (empty($mongoDocument)) { |
694
|
|
|
return null; |
695
|
|
|
} |
696
|
|
|
|
697
|
|
|
return $this->collection->hydrate($mongoDocument, $this->isDocumentPoolUsed()); |
698
|
|
|
} |
699
|
|
|
|
700
|
|
View Code Duplication |
public function map($handler) |
|
|
|
|
701
|
|
|
{ |
702
|
|
|
$result = array(); |
703
|
|
|
|
704
|
|
|
foreach($this as $id => $document) { |
705
|
|
|
$result[$id] = $handler($document); |
706
|
|
|
} |
707
|
|
|
|
708
|
|
|
return $result; |
709
|
|
|
} |
710
|
|
|
|
711
|
|
View Code Duplication |
public function filter($handler) |
|
|
|
|
712
|
|
|
{ |
713
|
|
|
$result = array(); |
714
|
|
|
|
715
|
|
|
foreach($this as $id => $document) { |
716
|
|
|
if(!$handler($document)) { |
717
|
|
|
continue; |
718
|
|
|
} |
719
|
|
|
|
720
|
|
|
$result[$id] = $document; |
721
|
|
|
} |
722
|
|
|
|
723
|
|
|
return $result; |
724
|
|
|
} |
725
|
|
|
|
726
|
|
|
/** |
727
|
|
|
* Get result set of documents. |
728
|
|
|
* |
729
|
|
|
* @return \Sokil\Mongo\ResultSet |
730
|
|
|
*/ |
731
|
|
|
public function getResultSet() |
732
|
|
|
{ |
733
|
|
|
return new ResultSet($this->findAll()); |
734
|
|
|
} |
735
|
|
|
|
736
|
|
|
/** |
737
|
|
|
* Get paginator |
738
|
|
|
* |
739
|
|
|
* @param int $page page number |
740
|
|
|
* @param int $itemsOnPage number of items on page |
741
|
|
|
* @return \Sokil\Mongo\Paginator |
742
|
|
|
*/ |
743
|
|
|
public function paginate($page, $itemsOnPage = 30) |
744
|
|
|
{ |
745
|
|
|
$paginator = new Paginator($this); |
746
|
|
|
|
747
|
|
|
return $paginator |
748
|
|
|
->setCurrentPage($page) |
749
|
|
|
->setItemsOnPage($itemsOnPage); |
750
|
|
|
|
751
|
|
|
} |
752
|
|
|
|
753
|
|
|
public function current() |
754
|
|
|
{ |
755
|
|
|
$mongoDocument = $this->getCursor()->current(); |
756
|
|
|
if (empty($mongoDocument)) { |
757
|
|
|
return null; |
758
|
|
|
} |
759
|
|
|
|
760
|
|
|
if ($this->isResultAsArray) { |
761
|
|
|
return $mongoDocument; |
762
|
|
|
} |
763
|
|
|
|
764
|
|
|
return $this->collection->hydrate( |
765
|
|
|
$mongoDocument, |
766
|
|
|
$this->isDocumentPoolUsed() |
767
|
|
|
); |
768
|
|
|
} |
769
|
|
|
|
770
|
|
|
public function key() |
771
|
|
|
{ |
772
|
|
|
return $this->getCursor()->key(); |
773
|
|
|
} |
774
|
|
|
|
775
|
|
|
public function next() |
776
|
|
|
{ |
777
|
|
|
$this->getCursor()->next(); |
778
|
|
|
return $this; |
779
|
|
|
} |
780
|
|
|
|
781
|
|
|
public function rewind() |
782
|
|
|
{ |
783
|
|
|
$this->getCursor()->rewind(); |
784
|
|
|
return $this; |
785
|
|
|
} |
786
|
|
|
|
787
|
|
|
public function valid() |
788
|
|
|
{ |
789
|
|
|
return $this->getCursor()->valid(); |
790
|
|
|
} |
791
|
|
|
|
792
|
|
|
public function readPrimaryOnly() |
793
|
|
|
{ |
794
|
|
|
$this->readPreference = array( |
795
|
|
|
'type' => \MongoClient::RP_PRIMARY, |
796
|
|
|
'tagsets' => array(), |
797
|
|
|
); |
798
|
|
|
|
799
|
|
|
return $this; |
800
|
|
|
} |
801
|
|
|
|
802
|
|
|
public function readPrimaryPreferred(array $tags = null) |
803
|
|
|
{ |
804
|
|
|
$this->readPreference = array( |
805
|
|
|
'type' => \MongoClient::RP_PRIMARY_PREFERRED, |
806
|
|
|
'tagsets' => $tags, |
807
|
|
|
); |
808
|
|
|
|
809
|
|
|
return $this; |
810
|
|
|
} |
811
|
|
|
|
812
|
|
|
public function readSecondaryOnly(array $tags = null) |
813
|
|
|
{ |
814
|
|
|
$this->readPreference = array( |
815
|
|
|
'type' => \MongoClient::RP_SECONDARY, |
816
|
|
|
'tagsets' => $tags, |
817
|
|
|
); |
818
|
|
|
|
819
|
|
|
return $this; |
820
|
|
|
} |
821
|
|
|
|
822
|
|
|
public function readSecondaryPreferred(array $tags = null) |
823
|
|
|
{ |
824
|
|
|
$this->readPreference = array( |
825
|
|
|
'type' => \MongoClient::RP_SECONDARY_PREFERRED, |
826
|
|
|
'tagsets' => $tags, |
827
|
|
|
); |
828
|
|
|
|
829
|
|
|
return $this; |
830
|
|
|
} |
831
|
|
|
|
832
|
|
|
public function readNearest(array $tags = null) |
833
|
|
|
{ |
834
|
|
|
$this->readPreference = array( |
835
|
|
|
'type' => \MongoClient::RP_NEAREST, |
836
|
|
|
'tagsets' => $tags, |
837
|
|
|
); |
838
|
|
|
|
839
|
|
|
return $this; |
840
|
|
|
} |
841
|
|
|
|
842
|
|
|
/** |
843
|
|
|
* @return array |
844
|
|
|
*/ |
845
|
|
|
public function getReadPreference() |
846
|
|
|
{ |
847
|
|
|
if($this->cursor) { |
848
|
|
|
return $this->cursor->getReadPreference(); |
849
|
|
|
} |
850
|
|
|
|
851
|
|
|
return $this->readPreference; |
852
|
|
|
} |
853
|
|
|
|
854
|
|
|
public function isDocumentPoolUsed() |
855
|
|
|
{ |
856
|
|
|
return $this->isDocumentPoolUsed; |
857
|
|
|
} |
858
|
|
|
|
859
|
|
|
public function useDocumentPool() |
860
|
|
|
{ |
861
|
|
|
$this->isDocumentPoolUsed = true; |
862
|
|
|
return $this; |
863
|
|
|
} |
864
|
|
|
|
865
|
|
|
public function skipDocumentPool() |
866
|
|
|
{ |
867
|
|
|
$this->isDocumentPoolUsed = false; |
868
|
|
|
return $this; |
869
|
|
|
} |
870
|
|
|
|
871
|
|
|
/** |
872
|
|
|
* Specify index to use |
873
|
|
|
* |
874
|
|
|
* @link http://docs.mongodb.org/manual/reference/operator/meta/hint/ |
875
|
|
|
* @param array|string $specification Specify the index either by the index name or by document |
876
|
|
|
* @return \Sokil\Mongo\Cursor |
877
|
|
|
*/ |
878
|
|
|
public function hint($specification) |
879
|
|
|
{ |
880
|
|
|
$this->hint = $specification; |
881
|
|
|
return $this; |
882
|
|
|
} |
883
|
|
|
|
884
|
|
|
/** |
885
|
|
|
* Copy selected documents to another collection |
886
|
|
|
* |
887
|
|
|
* @param type $targetCollectionName |
888
|
|
|
* @param type $targetDatabaseName Target database name. If not specified - use current |
889
|
|
|
*/ |
890
|
|
|
public function copyToCollection($targetCollectionName, $targetDatabaseName = null) |
891
|
|
|
{ |
892
|
|
|
// target database |
893
|
|
|
if(!$targetDatabaseName) { |
894
|
|
|
$database = $this->collection->getDatabase(); |
895
|
|
|
} else { |
896
|
|
|
$database = $this->client->getDatabase($targetDatabaseName); |
897
|
|
|
} |
898
|
|
|
|
899
|
|
|
// target collection |
900
|
|
|
$targetMongoCollection = $database |
901
|
|
|
->getCollection($targetCollectionName) |
902
|
|
|
->getMongoCollection(); |
903
|
|
|
|
904
|
|
|
// cursor |
905
|
|
|
$cursor = $this->getCursor(); |
906
|
|
|
|
907
|
|
|
$batchLimit = 100; |
908
|
|
|
$inProgress = true; |
909
|
|
|
|
910
|
|
|
// copy data |
911
|
|
|
while($inProgress) { |
912
|
|
|
// get next pack of documents |
913
|
|
|
$documentList = array(); |
914
|
|
|
for($i = 0; $i < $batchLimit; $i++) { |
915
|
|
|
if(!$cursor->valid()) { |
916
|
|
|
$inProgress = false; |
917
|
|
|
|
918
|
|
|
if($documentList) { |
|
|
|
|
919
|
|
|
// still need batch insert |
920
|
|
|
break; |
921
|
|
|
} else { |
922
|
|
|
// no documents to insert - just exit |
923
|
|
|
break(2); |
924
|
|
|
} |
925
|
|
|
} |
926
|
|
|
|
927
|
|
|
$documentList[] = $cursor->current(); |
928
|
|
|
$cursor->next(); |
929
|
|
|
} |
930
|
|
|
|
931
|
|
|
// insert |
932
|
|
|
$result = $targetMongoCollection->batchInsert($documentList); |
933
|
|
|
|
934
|
|
|
// check result |
935
|
|
View Code Duplication |
if(is_array($result)) { |
|
|
|
|
936
|
|
|
if($result['ok'] != 1) { |
937
|
|
|
throw new Exception('Batch insert error: ' . $result['err']); |
938
|
|
|
} |
939
|
|
|
} elseif(!$result) { |
940
|
|
|
throw new Exception('Batch insert error'); |
941
|
|
|
} |
942
|
|
|
} |
943
|
|
|
|
944
|
|
|
return $this; |
945
|
|
|
} |
946
|
|
|
|
947
|
|
|
/** |
948
|
|
|
* Move selected documents to another collection. |
949
|
|
|
* Dociuments will be removed from source collection only after |
950
|
|
|
* copying them to target collection. |
951
|
|
|
* |
952
|
|
|
* @param type $targetCollectionName |
953
|
|
|
* @param type $targetDatabaseName Target database name. If not specified - use current |
954
|
|
|
*/ |
955
|
|
|
public function moveToCollection($targetCollectionName, $targetDatabaseName = null) |
956
|
|
|
{ |
957
|
|
|
// copy to target |
958
|
|
|
$this->copyToCollection($targetCollectionName, $targetDatabaseName); |
959
|
|
|
|
960
|
|
|
// remove from source |
961
|
|
|
$this->collection->deleteDocuments($this->expression); |
|
|
|
|
962
|
|
|
} |
963
|
|
|
|
964
|
|
|
/** |
965
|
|
|
* Used to get hash that uniquely identifies current query |
966
|
|
|
*/ |
967
|
|
|
public function getHash() |
968
|
|
|
{ |
969
|
|
|
$hash = array(); |
970
|
|
|
|
971
|
|
|
// expression |
972
|
|
|
$hash[] = json_encode($this->expression->toArray()); |
973
|
|
|
|
974
|
|
|
// sorts |
975
|
|
View Code Duplication |
if (!empty($this->sort)) { |
|
|
|
|
976
|
|
|
$sort = $this->sort; |
977
|
|
|
ksort($sort); |
978
|
|
|
$hash[] = implode('', array_merge(array_keys($sort), array_values($sort))); |
979
|
|
|
} |
980
|
|
|
|
981
|
|
|
// fields |
982
|
|
View Code Duplication |
if (!empty($this->fields)) { |
|
|
|
|
983
|
|
|
$fields = $this->fields; |
984
|
|
|
ksort($fields); |
985
|
|
|
$hash[] = implode('', array_merge(array_keys($fields), array_values($fields))); |
986
|
|
|
} |
987
|
|
|
|
988
|
|
|
// skip and limit |
989
|
|
|
$hash[] = $this->skip; |
990
|
|
|
$hash[] = $this->limit; |
991
|
|
|
|
992
|
|
|
// get hash |
993
|
|
|
return md5(implode(':', $hash)); |
994
|
|
|
} |
995
|
|
|
|
996
|
|
|
/** |
997
|
|
|
* Get list of MongoId objects from array of strings, MongoId's and Document's |
998
|
|
|
* |
999
|
|
|
* @param array $list |
1000
|
|
|
* @return array list of \MongoId |
1001
|
|
|
*/ |
1002
|
|
|
public static function mixedToMongoIdList(array $list) |
1003
|
|
|
{ |
1004
|
|
|
return array_map(function($element) { |
1005
|
|
|
// MongoId |
1006
|
|
|
if($element instanceof \MongoId) { |
1007
|
|
|
return $element; |
1008
|
|
|
} |
1009
|
|
|
|
1010
|
|
|
// \Sokil\Mongo\Document |
1011
|
|
|
if($element instanceof Document) { |
1012
|
|
|
return $element->getId(); |
1013
|
|
|
} |
1014
|
|
|
|
1015
|
|
|
// array with id key |
1016
|
|
|
if(is_array($element)) { |
1017
|
|
|
if(!isset($element['_id'])) { |
1018
|
|
|
throw new \InvalidArgumentException('Array must have _id key'); |
1019
|
|
|
} |
1020
|
|
|
return $element['_id']; |
1021
|
|
|
} |
1022
|
|
|
|
1023
|
|
|
// string |
1024
|
|
|
if(is_string($element)) { |
1025
|
|
|
try { |
1026
|
|
|
return new \MongoId($element); |
1027
|
|
|
} catch (\MongoException $e) { |
1028
|
|
|
return $element; |
1029
|
|
|
} |
1030
|
|
|
} |
1031
|
|
|
|
1032
|
|
|
// int |
1033
|
|
|
if(is_int($element)) { |
1034
|
|
|
return $element; |
1035
|
|
|
} |
1036
|
|
|
|
1037
|
|
|
throw new \InvalidArgumentException('Must be \MongoId, \Sokil\Mongo\Document, array with _id key, string or integer'); |
1038
|
|
|
|
1039
|
|
|
}, array_values($list)); |
1040
|
|
|
} |
1041
|
|
|
} |
1042
|
|
|
|
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.