GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 290193...d482c6 )
by De
04:24
created

Collection::__get()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
c 3
b 0
f 0
dl 0
loc 4
rs 10
cc 1
eloc 2
nc 1
nop 1
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\Document\InvalidDocumentException;
15
use Sokil\Mongo\Collection\Definition;
16
use Sokil\Mongo\Enum\Language;
17
18
/**
19
 * Instance of this class is a representation of mongo collection.
20
 * It aggregates \MongoCollection instance.
21
 *
22
 * @link https://github.com/sokil/php-mongo#selecting-database-and-collection Selecting collection
23
 * @link https://github.com/sokil/php-mongo#querying-documents Querying documents
24
 * @link https://github.com/sokil/php-mongo#update-few-documents Update few documents
25
 * @link https://github.com/sokil/php-mongo#deleting-collections-and-documents Deleting collection
26
 *
27
 * @author Dmytro Sokil <[email protected]>
28
 */
29
class Collection implements \Countable
30
{
31
    /**
32
     * @var string expression class. This class may be overloaded to define
33
     *  own query methods (whereUserAgeGreatedThan(), etc.)
34
     * @deprecated since 1.13 Use 'expressionClass' declaration in mapping
35
     */
36
    protected $_queryExpressionClass;
37
38
    /**
39
     * @deprecated since 1.13 Use 'documentClass' declaration in mapping
40
     * @var string Default class for document
41
     */
42
    private $documentClass;
43
44
    /**
45
     * List of arrays, where each item array is an index definition.
46
     * Every index definition must contain key 'keys' with list of fields and orders,
47
     * and optional options from @link http://php.net/manual/en/mongocollection.createindex.php:
48
     *
49
     * Example:
50
     * array(
51
     *     array(
52
     *         'keys' => array('field1' => 1, 'field2' => -1),
53
     *         'unique' => true
54
     *     ),
55
     *     ...
56
     * )
57
     * @var array list of indexes
58
     * @deprecated since 1.13 Use 'index' declaration in mapping
59
     */
60
    protected $_index;
61
62
    /**
63
     *
64
     * @var \Sokil\Mongo\Database
65
     */
66
    protected $_database;
67
68
    /**
69
     *
70
     * @var \MongoCollection
71
     */
72
    protected $_mongoCollection;
73
74
    /**
75
     * Implementation of identity map pattern
76
     *
77
     * @var array list of cached documents
78
     */
79
    private $documentPool = array();
80
81
    /**
82
     *
83
     * @var bool cache or not documents
84
     */
85
    private $isDocumentPoolEnabled = true;
86
87
    /**
88
     * @deprecated since 1.13 Use 'versioning' declaration in mapping
89
     * @var bool default value of versioning
90
     */
91
    protected $versioning;
92
93
    /**
94
     * @var \Sokil\Mongo\Collection\Definition collection options
95
     */
96
    private $definition;
97
98
    public function __construct(Database $database, $collection, Definition $definition = null)
99
    {
100
        // define db
101
        $this->_database = $database;
102
103
        $this->initCollection($collection);
104
105
        // init definition
106
        $this->definition = $definition ? $definition : new Definition();
107
108
        if($this->documentClass) {
0 ignored issues
show
Deprecated Code introduced by
The property Sokil\Mongo\Collection::$documentClass has been deprecated with message: since 1.13 Use 'documentClass' declaration in mapping

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
109
            $this->definition->setOption('documentClass', $this->documentClass);
0 ignored issues
show
Deprecated Code introduced by
The property Sokil\Mongo\Collection::$documentClass has been deprecated with message: since 1.13 Use 'documentClass' declaration in mapping

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
110
        }
111
        if($this->versioning !== null) {
0 ignored issues
show
Deprecated Code introduced by
The property Sokil\Mongo\Collection::$versioning has been deprecated with message: since 1.13 Use 'versioning' declaration in mapping

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
112
            $this->definition->setOption('versioning', $this->versioning);
0 ignored issues
show
Deprecated Code introduced by
The property Sokil\Mongo\Collection::$versioning has been deprecated with message: since 1.13 Use 'versioning' declaration in mapping

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
113
        }
114
        if($this->_index) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_index of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Deprecated Code introduced by
The property Sokil\Mongo\Collection::$_index has been deprecated with message: since 1.13 Use 'index' declaration in mapping

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
115
            $this->definition->setOption('index', $this->_index);
0 ignored issues
show
Deprecated Code introduced by
The property Sokil\Mongo\Collection::$_index has been deprecated with message: since 1.13 Use 'index' declaration in mapping

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
116
        }
117
        if($this->_queryExpressionClass) {
0 ignored issues
show
Deprecated Code introduced by
The property Sokil\Mongo\Collection::$_queryExpressionClass has been deprecated with message: since 1.13 Use 'expressionClass' declaration in mapping

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
118
            $this->definition->setOption('expressionClass', $this->_queryExpressionClass);
0 ignored issues
show
Deprecated Code introduced by
The property Sokil\Mongo\Collection::$_queryExpressionClass has been deprecated with message: since 1.13 Use 'expressionClass' declaration in mapping

This property has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the property will be removed from the class and what other property to use instead.

Loading history...
119
        }
120
    }
121
122
    protected function initCollection($collection)
123
    {
124
        // init mongo collection
125
        if($collection instanceof \MongoCollection) {
126
            $this->_mongoCollection = $collection;
127
        } else {
128
            $this->_mongoCollection = $this->_database->getMongoDB()->selectCollection($collection);
129
        }
130
    }
131
132
    /**
133
     * Start versioning documents on modify
134
     *
135
     * @deprecated since 1.13 Use 'versioning' declaration in mapping
136
     * @return \Sokil\Mongo\Collection
137
     */
138
    public function enableVersioning()
139
    {
140
        $this->definition->setOption('versioning', true);
141
        return $this;
142
    }
143
144
    /**
145
     * Check if versioning enabled
146
     *
147
     * @deprecated since 1.13 Use 'versioning' declaration in mapping
148
     * @return bool
149
     */
150
    public function isVersioningEnabled()
151
    {
152
        return $this->definition->getOption('versioning');
153
    }
154
155
    /**
156
     * Get option
157
     *
158
     * @param string|int $name
159
     * @return mixed
160
     */
161
    public function getOption($name)
162
    {
163
        return $this->definition->getOption($name);
164
    }
165
166
    public function getOptions()
167
    {
168
        return $this->definition->getOptions();
169
    }
170
171
    public function __get($name)
172
    {
173
        return $this->getDocument($name);
174
    }
175
176
    /**
177
     * Get name of collection
178
     * 
179
     * @return string name of collection
180
     */
181
    public function getName()
182
    {
183
        return $this->_mongoCollection->getName();
184
    }
185
186
    /**
187
     * Get native collection instance of mongo driver
188
     * 
189
     * @return \MongoCollection
190
     */
191
    public function getMongoCollection()
192
    {
193
        return $this->_mongoCollection;
194
    }
195
196
    /**
197
     *
198
     * @return \Sokil\Mongo\Database
199
     */
200
    public function getDatabase()
201
    {
202
        return $this->_database;
203
    }
204
205
    /**
206
     * Delete collection
207
     * 
208
     * @return \Sokil\Mongo\Collection
209
     * @throws \Sokil\Mongo\Exception
210
     */
211
    public function delete() {
212
        $status = $this->_mongoCollection->drop();
213
        if($status['ok'] != 1) {
214
            // check if collection exists
215
            if('ns not found' !== $status['errmsg']) {
216
                // collection exist
217
                throw new Exception('Error deleting collection ' . $this->getName() . ': ' . $status['errmsg']);
218
            }
219
        }
220
221
        return $this;
222
    }
223
224
    /**
225
     * Override to define class name of document by document data
226
     *
227
     * @param array $documentData
228
     * @return string Document class data
229
     */
230
    public function getDocumentClassName(array $documentData = null)
231
    {
232
        $documentClass = $this->definition->getOption('documentClass');
233
234
        if(is_callable($documentClass)) {
235
            return call_user_func($documentClass, $documentData);
236
        }
237
238
        if(class_exists($documentClass)) {
239
            return $documentClass;
240
        }
241
242
        throw new Exception('Property "documentClass" must be callable or valid name of class');
243
    }
244
245
    /**
246
     * Factory method to get not stored Document instance from array
247
     * @param array $data
248
     * @return \Sokil\Mongo\Document
249
     */
250
    public function createDocument(array $data = null)
251
    {
252
        $className = $this->getDocumentClassName($data);
253
254
        /* @var $document \Sokil\Mongo\Document */
255
        $document = new $className(
256
            $this,
257
            $data,
258
            array('stored' => false) + $this->definition->getOptions()
259
        );
260
261
        // store document to identity map
262
        if($this->isDocumentPoolEnabled()) {
263
            $collection = $this;
264
            $document->onAfterInsert(function(\Sokil\Mongo\Event $event) use($collection) {
0 ignored issues
show
Bug introduced by
The call to onAfterInsert() misses a required argument $priority.

This check looks for function calls that miss required arguments.

Loading history...
265
                $collection->addDocumentToDocumentPool($event->getTarget());
266
            });
267
        }
268
269
        return $document;
270
    }
271
272
    /**
273
     * Factory method to get document object from array of stored document
274
     *
275
     * @param array $data
276
     * @return \Sokil\Mongo\Document
277
     */
278
    public function hydrate($data, $useDocumentPool = true)
279
    {
280
        if(!is_array($data) || !isset($data['_id'])) {
281
            throw new Exception('Document must be stored and has _id key');
282
        }
283
284
        // if document already in pool - return it
285
        if($useDocumentPool && $this->isDocumentPoolEnabled() && $this->isDocumentInDocumentPool($data['_id'])) {
286
            return $this
287
                ->getDocumentFromDocumentPool($data['_id'])
288
                ->mergeUnmodified($data);
289
        }
290
291
        // init document instance
292
        $className = $this->getDocumentClassName($data);
293
        $document = new $className(
294
            $this,
295
            $data,
296
            array('stored' => true) + $this->definition->getOptions()
297
        );
298
299
        // store document in cache
300
        if($useDocumentPool && $this->isDocumentPoolEnabled()) {
301
            $this->addDocumentToDocumentPool($document);
302
        }
303
304
        return $document;
305
    }
306
307
    /**
308
     * Total count of documents in collection
309
     * 
310
     * @return int 
311
     */
312
    public function count()
313
    {
314
        return $this->find()->count();
315
    }
316
317
    /**
318
     * Retrieve a list of distinct values for the given key across a collection.
319
     *
320
     * @param string $selector field selector
321
     * @param array|callable|\Sokil\Mongo\Expression $expression expression to search documents
322
     * @return array distinct values
323
     */
324
    public function getDistinct($selector, $expression = null)
325
    {
326
        if($expression) {
327
            return $this->_mongoCollection->distinct(
328
                $selector,
329
                Expression::convertToArray($expression)
330
            );
331
        }
332
333
        return $this->_mongoCollection->distinct($selector);
334
    }
335
336
    /**
337
     * Create new Expression instance to use in query builder or update operations
338
     * 
339
     * @return \Sokil\Mongo\Expression
340
     */
341
    public function expression()
342
    {
343
        $className = $this->definition->getExpressionClass();
344
        return new $className;
345
    }
346
347
    /**
348
     * Create Operator instance to use in update operations
349
     * 
350
     * @return \Sokil\Mongo\Operator
351
     */
352
    public function operator()
353
    {
354
        return new Operator();
355
    }
356
357
    /**
358
     * Create document query builder
359
     *
360
     * @param $callable callable|null Function to configure query builder&
361
     * @return \Sokil\Mongo\Cursor|\Sokil\Mongo\Expression
362
     */
363
    public function find($callable = null)
364
    {
365
        /** @var \Sokil\Mongo\Cursor $cursor */
366
        $cursor = new Cursor($this, array(
367
            'expressionClass'   => $this->definition->getExpressionClass(),
368
            'batchSize'         => $this->definition->getOption('batchSize'),
369
            'clientTimeout'     => $this->definition->getOption('cursorClientTimeout'),
370
            'serverTimeout'     => $this->definition->getOption('cursorServerTimeout'),
371
        ));
372
373
        if(is_callable($callable)) {
374
            $callable($cursor->getExpression());
375
        }
376
377
        return $cursor;
378
    }
379
380
    /**
381
     * Create document query builder
382
     *
383
     * @return \Sokil\Mongo\Cursor
384
     */
385
    public function findAsArray($callable = null)
386
    {
387
        return $this
388
            ->find($callable)
389
            ->asArray();
390
    }
391
392
    /**
393
     * Stop storing found documents to pool
394
     *
395
     * @return \Sokil\Mongo\Collection
396
     */
397
    public function disableDocumentPool()
398
    {
399
        $this->isDocumentPoolEnabled = false;
400
        return $this;
401
    }
402
403
    /**
404
     * Start storing found documents to pool
405
     *
406
     * @return \Sokil\Mongo\Collection
407
     */
408
    public function enableDocumentPool()
409
    {
410
        $this->isDocumentPoolEnabled = true;
411
        return $this;
412
    }
413
414
    /**
415
     * Check if document pool enabled and requested documents store to it
416
     *
417
     * @return bool
418
     */
419
    public function isDocumentPoolEnabled()
420
    {
421
        return $this->isDocumentPoolEnabled;
422
    }
423
424
    public function clearDocumentPool()
425
    {
426
        $this->documentPool = array();
427
        return $this;
428
    }
429
430
    /**
431
     * Check if documents are in pool
432
     *
433
     * @return bool
434
     */
435
    public function isDocumentPoolEmpty()
436
    {
437
        return !$this->documentPool;
438
    }
439
440
    /**
441
     * Store document to pool
442
     *
443
     * @param array $document
444
     * @return \Sokil\Mongo\Collection
445
     */
446
    public function addDocumentToDocumentPool(Document $document)
447
    {
448
        $documentId = (string) $document->getId();
449
450
        if(!isset($this->documentPool[$documentId])) {
451
            $this->documentPool[$documentId] = $document;
452
        } else {
453
            // merging because document after
454
            // load and before getting in second place may be changed
455
            // and this changes must be preserved:
456
            //
457
            // 1. Document loads and modifies in current session
458
            // 2. Document loads modified in another session
459
            // 3. Document loads once again in current session. Changes from stage 2 merges as unmodified
460
461
            $this->documentPool[$documentId]->mergeUnmodified($document->toArray());
462
        }
463
464
        return $this;
465
    }
466
467
    /**
468
     * Store documents to identity map
469
     *
470
     * @param array $documents list of Document instances
471
     * @return \Sokil\Mongo\Collection
472
     */
473
    public function addDocumentsToDocumentPool(array $documents)
474
    {
475
        foreach($documents as $document) {
476
            $this->addDocumentToDocumentPool($document);
477
        }
478
479
        return $this;
480
    }
481
482
    /**
483
     * Remove document instance from identity map
484
     *
485
     * @param \Sokil\Mongo\Document $document
486
     * @return \Sokil\Mongo\Collection
487
     */
488
    public function removeDocumentFromDocumentPool(Document $document)
489
    {
490
        unset($this->documentPool[(string) $document]);
491
        return $this;
492
    }
493
494
    /**
495
     * Get document from identity map by it's id
496
     *
497
     * @param string|int|\MongoId $id
498
     * @return \Sokil\Mongo\Document
499
     */
500
    public function getDocumentFromDocumentPool($id)
501
    {
502
        return $this->documentPool[(string) $id];
503
    }
504
505
    /**
506
     * Get documents from pool if they stored
507
     *
508
     * @param array $ids
509
     */
510
    public function getDocumentsFromDocumentPool(array $ids = null)
511
    {
512
        if(!$ids) {
513
            return $this->documentPool;
514
        }
515
516
        return array_intersect_key(
517
            $this->documentPool,
518
            array_flip(array_map('strval', $ids))
519
        );
520
    }
521
522
    /**
523
     * Get number of documents in document pool
524
     *
525
     * @return int
526
     */
527
    public function documentPoolCount()
528
    {
529
        return count($this->documentPool);
530
    }
531
532
    /**
533
     * Check if document exists in identity map
534
     *
535
     * @param \Sokil\Mongo\Document|\MongoId|int|string $document Document instance or it's id
536
     * @return boolean
537
     */
538
    public function isDocumentInDocumentPool($document)
539
    {
540
        if($document instanceof Document) {
541
            $document = $document->getId();
542
        }
543
544
        return isset($this->documentPool[(string) $document]);
545
    }
546
547
    /**
548
     * Get document by id
549
     * If callable specified, document always loaded directly omitting document pool.
550
     * Method may return document as array if cursor configured through Cursor::asArray()
551
     *
552
     * @param string|\MongoId $id
553
     * @param callable $callable cursor callable used to configure cursor
554
     * @return \Sokil\Mongo\Document|array|null
555
     */
556
    public function getDocument($id, $callable = null)
557
    {
558
        if(!$this->isDocumentPoolEnabled) {
559
            return $this->getDocumentDirectly($id, $callable);
560
        }
561
562
        if(!$callable && $this->isDocumentInDocumentPool($id)) {
563
            return $this->getDocumentFromDocumentPool($id);
564
        }
565
566
        $document = $this->getDocumentDirectly($id, $callable);
567
568
        // if callable configure cursor to return document as array,
569
        // than it can't be stored to document pool
570
        if($document instanceof Document) {
571
            $this->addDocumentToDocumentPool($document);
572
        }
573
574
        return $document;
575
    }
576
577
578
    /**
579
     * Get document by id directly omitting cache
580
     * Method may return document as array if cursor configured through Cursor::asArray()
581
     * 
582
     * @param string|\MongoId $id
583
     * @param callable $callable cursor callable used to configure cursor
584
     * @return \Sokil\Mongo\Document|array|null
585
     */
586
    public function getDocumentDirectly($id, $callable = null)
587
    {
588
        $cursor = $this->find();
589
590
        if(is_callable($callable)) {
591
            call_user_func($callable, $cursor);
592
        }
593
594
        return $cursor
595
            ->byId($id)
596
            ->skipDocumentPool()
597
            ->findOne();
598
    }
599
600
    /**
601
     * Check if document belongs to collection
602
     *
603
     * @param \Sokil\Mongo\Document $document
604
     * @return type
605
     */
606
    public function hasDocument(Document $document)
607
    {
608
        // check connection
609
        if($document->getCollection()->getDatabase()->getClient()->getDsn() !== $this->getDatabase()->getClient()->getDsn()) {
610
            return false;
611
        }
612
613
        // check database
614
        if ($document->getCollection()->getDatabase()->getName() !== $this->getDatabase()->getName()) {
615
            return false;
616
        }
617
618
        // check collection
619
        return $document->getCollection()->getName() == $this->getName();
620
    }
621
622
    /**
623
     * Get documents by list of id
624
     *
625
     * @param array $idList list of ids
626
     * @param callable $callable cursor callable used to configure cursor
627
     * @return array|null
628
     */
629
    public function getDocuments(array $idList, $callable = null)
630
    {
631
        $idListToFindDirectly = $idList;
632
633
        // try to egt document from pool if enabled
634
        $documentsInDocumentPool = array();
635
        if ($this->isDocumentPoolEnabled && !$callable) {
636
            $documentsInDocumentPool = $this->getDocumentsFromDocumentPool($idList);
637
            if (count($documentsInDocumentPool) === count($idList)) {
638
                return $documentsInDocumentPool;
639
            }
640
641
            // skip ids already found in pool
642
            $idListToFindDirectly = array_diff_key(
643
                array_map('strval', $idList),
644
                array_keys($documentsInDocumentPool)
645
            );
646
        }
647
648
        // get documents directly
649
        $cursor = $this->find();
650
651
        if (is_callable($callable)) {
652
            call_user_func($callable, $cursor);
653
        }
654
655
        $documentsGettingDirectly = $cursor->byIdList($idListToFindDirectly)->findAll();
656
        if (!$documentsGettingDirectly) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $documentsGettingDirectly of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
657
            return $documentsInDocumentPool ? $documentsInDocumentPool : array();
658
        }
659
660
        if ($this->isDocumentPoolEnabled) {
661
            $this->addDocumentsToDocumentPool($documentsGettingDirectly);
662
        }
663
664
        return $documentsGettingDirectly + $documentsInDocumentPool;
665
    }
666
667
    /**
668
     * Creates batch insert operation handler
669
     * @param int|string $writeConcern Write concern. Default is 1 (Acknowledged)
670
     * @param int $timeout Timeout for write concern. Default is 10000 milliseconds
671
     * @param bool $ordered Determins if MongoDB must apply this batch in order (sequentally,
672
     *   one item at a time) or can rearrange it. Defaults to TRUE
673
     * @return BatchInsert
674
     */
675
    public function createBatchInsert($writeConcern = null, $timeout = null, $ordered = null)
676
    {
677
        return new BatchInsert(
678
            $this,
679
            $writeConcern,
680
            $timeout,
681
            $ordered
682
        );
683
    }
684
685
    /**
686
     * Creates batch update operation handler
687
     * @param int|string $writeConcern Write concern. Default is 1 (Acknowledged)
688
     * @param int $timeout Timeout for write concern. Default is 10000 milliseconds
689
     * @param bool $ordered Determins if MongoDB must apply this batch in order (sequentally,
690
     *   one item at a time) or can rearrange it. Defaults to TRUE
691
     * @return BatchUpdate
692
     */
693
    public function createBatchUpdate($writeConcern = null, $timeout = null, $ordered = null)
694
    {
695
        return new BatchUpdate(
696
            $this,
697
            $writeConcern,
698
            $timeout,
699
            $ordered
700
        );
701
    }
702
703
    /**
704
     * Creates batch delete operation handler
705
     * @param int|string $writeConcern Write concern. Default is 1 (Acknowledged)
706
     * @param int $timeout Timeout for write concern. Default is 10000 milliseconds
707
     * @param bool $ordered Determins if MongoDB must apply this batch in order (sequentally,
708
     *   one item at a time) or can rearrange it. Defaults to TRUE
709
     * @return BatchDelete
710
     */
711
    public function createBatchDelete($writeConcern = null, $timeout = null, $ordered = null)
712
    {
713
        return new BatchDelete(
714
            $this,
715
            $writeConcern,
716
            $timeout,
717
            $ordered
718
        );
719
    }
720
721
    /**
722
     * @deprecated since 1.13. Use Document::delete()
723
     * @param \Sokil\Mongo\Document $document
724
     * @return \Sokil\Mongo\Collection
725
     */
726
    public function deleteDocument(Document $document)
727
    {
728
        $document->delete();
729
        return $this;
730
    }
731
732
    /**
733
     * Delete documents by expression
734
     * 
735
     * @param callable|array|\Sokil\Mongo\Expression $expression
736
     * @return \Sokil\Mongo\Collection
737
     * @throws Exception
738
     */
739
    public function batchDelete($expression = array())
740
    {
741
        // remove
742
        $result = $this->_mongoCollection->remove(
743
            Expression::convertToArray($expression)
744
        );
745
746
        // check result
747
        if(true !== $result && $result['ok'] != 1) {
748
            throw new Exception('Error removing documents from collection: ' . $result['err']);
749
        }
750
751
        return $this;
752
    }
753
754
    /**
755
     * @deprecated since 1.13. Use Collection::batchDelete();
756
     */
757
    public function deleteDocuments($expression = array())
758
    {
759
        return $this->batchDelete($expression);
760
    }
761
762
    /**
763
     * Insert multiple documents defined as arrays
764
     *
765
     * Prior to version 1.5.0 of the driver it was possible to use MongoCollection::batchInsert(),
766
     * however, as of 1.5.0 that method is now discouraged.
767
     *
768
     * You can use Collection::createBatchInsert()
769
     *
770
     * @param array $rows list of documents to insert, defined as arrays
771
     * @return \Sokil\Mongo\Collection
772
     * @throws \Sokil\Mongo\Document\InvalidDocumentException
773
     * @throws \Sokil\Mongo\Exception
774
     */
775
    public function batchInsert($rows, $validate = true)
776
    {
777
        if($validate) {
778
            $document = $this->createDocument();
779
            foreach($rows as $row) {
780
                $document->merge($row);
781
782
                if(!$document->isValid()) {
783
                    throw new InvalidDocumentException('Document is invalid on batch insert');
784
                }
785
786
                $document->reset();
787
            }
788
        }
789
790
        $result = $this->_mongoCollection->batchInsert($rows);
791
792
        // If the w parameter is set to acknowledge the write,
793
        // returns an associative array with the status of the inserts ("ok")
794
        // and any error that may have occurred ("err").
795 View Code Duplication
        if(is_array($result)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
796
            if($result['ok'] != 1) {
797
                throw new Exception('Batch insert error: ' . $result['err']);
798
            }
799
800
            return $this;
801
        }
802
803
        // Otherwise, returns TRUE if the batch insert was successfully sent,
804
        // FALSE otherwise.
805
        if(!$result) {
806
            throw new Exception('Batch insert error');
807
        }
808
809
        return $this;
810
    }
811
812
    /**
813
     * @deprecated since 1.13 Use Collection::batchInsert()
814
     */
815
    public function insertMultiple($rows, $validate = true)
816
    {
817
        return $this->batchInsert($rows, $validate);
818
    }
819
820
    /**
821
     * Direct insert of array to MongoDB without creating document object and validation
822
     *
823
     * @param array $document
824
     * @return \Sokil\Mongo\Collection
825
     * @throws Exception
826
     */
827
    public function insert(array $document)
828
    {
829
        $result = $this->_mongoCollection->insert($document);
830
831
        // if write concern acknowledged
832 View Code Duplication
        if(is_array($result)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
833
            if($result['ok'] != 1) {
834
                throw new Exception('Insert error: ' . $result['err'] . ': ' . $result['errmsg']);
835
            }
836
837
            return $this;
838
        }
839
840
        // if write concern unacknowledged
841
        if(!$result) {
842
            throw new Exception('Insert error');
843
        }
844
845
        return $this;
846
    }
847
848
    /**
849
     * Update multiple documents
850
     *
851
     * @param \Sokil\Mongo\Expression|array|callable $expression expression to define
852
     *  which documents will change.
853
     * @param \Sokil\Mongo\Operator|array|callable $updateData new data or operators to update
854
     * @param array $options update options, see http://php.net/manual/ru/mongocollection.update.php
855
     * @return \Sokil\Mongo\Collection
856
     * @throws \Sokil\Mongo\Exception
857
     */
858
    public function update($expression, $updateData, array $options = array())
859
    {
860
        // execute update operator
861
        $result = $this->_mongoCollection->update(
862
            Expression::convertToArray($expression),
863
            Operator::convertToArray($updateData),
864
            $options
865
        );
866
867
        // if write concern acknowledged
868 View Code Duplication
        if(is_array($result)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
869
            if($result['ok'] != 1) {
870
                throw new Exception(sprintf('Update error: %s: %s', $result['err'], $result['errmsg']));
871
            }
872
            return $this;
873
        }
874
875
        // if write concern unacknowledged
876
        if(!$result) {
877
            throw new Exception('Update error');
878
        }
879
880
        return $this;
881
    }
882
883
    /**
884
     * Update multiple documents
885
     *
886
     * @param \Sokil\Mongo\Expression|array|callable $expression expression to define
887
     *  which documents will change.
888
     * @param \Sokil\Mongo\Operator|array|callable $updateData new data or operators
889
     *  to update
890
     * @return \Sokil\Mongo\Collection
891
     * @throws \Sokil\Mongo\Exception
892
     */
893
    public function batchUpdate($expression, $updateData)
894
    {
895
        return $this->update($expression, $updateData, array(
896
            'multiple'  => true,
897
        ));
898
    }
899
900
    /**
901
     * @deprecated since 1.13 Use Collection::batchUpdate()
902
     */
903
    public function updateMultiple($expression, $updateData)
904
    {
905
        return $this->batchUpdate($expression, $updateData);
906
    }
907
908
    /**
909
     * Update all documents
910
     *
911
     * @deprecated since 1.13. Use Collection::batchUpdate([])
912
     * @param \Sokil\Mongo\Operator|array|callable $updateData new data or operators
913
     * @return \Sokil\Mongo\Collection
914
     * @throws \Sokil\Mongo\Exception
915
     */
916
    public function updateAll($updateData)
917
    {
918
        return $this->update(array(), $updateData, array(
919
            'multiple'  => true,
920
        ));
921
    }
922
923
    /**
924
     * Create aggregator pipeline instance
925
     *
926
     * @return \Sokil\Mongo\Pipeline
927
     * @deprecated since 1.10.10, use Collection::createAggregator() or callable in Collection::aggregate()
928
     */
929
    public function createPipeline()
930
    {
931
        return $this->createAggregator();
932
    }
933
934
    /**
935
     * Start aggregation
936
     *
937
     * @link http://docs.mongodb.org/manual/reference/operator/aggregation/
938
     * @return \Sokil\Mongo\Pipeline
939
     */
940
    public function createAggregator()
941
    {
942
        return new Pipeline($this);
943
    }
944
945
    /**
946
     * Aggregate using pipeline
947
     *
948
     * @param callable|array|\Sokil\Mongo\Pipeline $pipeline list of pipeline stages
949
     * @link http://docs.mongodb.org/manual/reference/operator/aggregation/
950
     * @return array result of aggregation
951
     * @throws \Sokil\Mongo\Exception
952
     */
953
    public function aggregate($pipeline)
954
    {
955
        // configure through callable
956
        if (is_callable($pipeline)) {
957
            $pipelineConfiguratorCallable = $pipeline;
958
            $pipeline = $this->createAggregator();
959
            call_user_func($pipelineConfiguratorCallable, $pipeline);
960
        }
961
962
        // get aggregation array
963 View Code Duplication
        if ($pipeline instanceof Pipeline) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
964
            $pipeline = $pipeline->toArray();
965
        } elseif (!is_array($pipeline)) {
966
            throw new Exception('Wrong pipeline specified');
967
        }
968
969
        // log
970
        $client = $this->_database->getClient();
971
        if($client->hasLogger()) {
972
            $client->getLogger()->debug(
973
                get_called_class() . ':<br><b>Pipeline</b>:<br>' .
974
                json_encode($pipeline));
975
        }
976
977
        // aggregate
978
        $status = $this->_database->executeCommand(array(
979
            'aggregate' => $this->getName(),
980
            'pipeline'  => $pipeline
981
        ));
982
983
        if($status['ok'] != 1) {
984
            throw new Exception('Aggregate error: ' . $status['errmsg']);
985
        }
986
987
        return $status['result'];
988
    }
989
990
    public function explainAggregate($pipeline)
991
    {
992
        if(version_compare($this->getDatabase()->getClient()->getDbVersion(), '2.6.0', '<')) {
993
            throw new Exception('Explain of aggregation implemented only from 2.6.0');
994
        }
995
996 View Code Duplication
        if($pipeline instanceof Pipeline) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

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.

Loading history...
997
            $pipeline = $pipeline->toArray();
998
        }
999
        elseif(!is_array($pipeline)) {
1000
            throw new Exception('Wrong pipeline specified');
1001
        }
1002
1003
        // aggregate
1004
        return $this->_database->executeCommand(array(
1005
            'aggregate' => $this->getName(),
1006
            'pipeline'  => $pipeline,
1007
            'explain'   => true
1008
        ));
1009
    }
1010
1011
    /**
1012
     * Validates a collection. The method scans a collection’s data structures
1013
     * for correctness and returns a single document that describes the
1014
     * relationship between the logical collection and the physical
1015
     * representation of the data.
1016
     *
1017
     * @link http://docs.mongodb.org/manual/reference/method/db.collection.validate/
1018
     * @param bool $full Specify true to enable a full validation and to return
1019
     *      full statistics. MongoDB disables full validation by default because it
1020
     *      is a potentially resource-intensive operation.
1021
     * @return array
1022
     * @throws Exception
1023
     */
1024
    public function validate($full = false)
1025
    {
1026
        $response = $this->_mongoCollection->validate($full);
1027
        if(!$response || $response['ok'] != 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $response of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1028
            throw new Exception($response['errmsg']);
1029
        }
1030
1031
        return $response;
1032
    }
1033
1034
    /**
1035
     * Create index
1036
     *
1037
     * @param array $key
1038
     * @param array $options see @link http://php.net/manual/en/mongocollection.ensureindex.php
1039
     * @return \Sokil\Mongo\Collection
1040
     */
1041
    public function ensureIndex(array $key, array $options = array())
1042
    {
1043
        $this->_mongoCollection->ensureIndex($key, $options);
1044
        return $this;
1045
    }
1046
1047
    /**
1048
     * Create unique index
1049
     *
1050
     * @param array $key
1051
     * @param boolean $dropDups
1052
     * @return \Sokil\Mongo\Collection
1053
     */
1054
    public function ensureUniqueIndex(array $key, $dropDups = false)
1055
    {
1056
        $this->_mongoCollection->ensureIndex($key, array(
1057
            'unique'    => true,
1058
            'dropDups'  => (bool) $dropDups,
1059
        ));
1060
1061
        return $this;
1062
    }
1063
1064
    /**
1065
     * Create sparse index.
1066
     *
1067
     * Sparse indexes only contain entries for documents that have the indexed
1068
     * field, even if the index field contains a null value. The index skips
1069
     * over any document that is missing the indexed field.
1070
     *
1071
     * @link http://docs.mongodb.org/manual/core/index-sparse/
1072
     *
1073
     * @param string|array $key An array specifying the index's fields as its
1074
     *  keys. For each field, the value is either the index direction or index
1075
     *  type. If specifying direction, specify 1 for ascending or -1
1076
     *  for descending.
1077
     *
1078
     * @return \Sokil\Mongo\Collection
1079
     */
1080
    public function ensureSparseIndex(array $key)
1081
    {
1082
        $this->_mongoCollection->ensureIndex($key, array(
1083
            'sparse'    => true,
1084
        ));
1085
1086
        return $this;
1087
    }
1088
1089
    /**
1090
     * Create TTL index
1091
     *
1092
     * @link http://docs.mongodb.org/manual/tutorial/expire-data/
1093
     *
1094
     * If seconds not specified then document expired at specified time, as
1095
     * described at @link http://docs.mongodb.org/manual/tutorial/expire-data/#expire-documents-at-a-certain-clock-time
1096
     *
1097
     * @param string|array $key key must be date to use TTL
1098
     * @param int $seconds
1099
     * @return \Sokil\Mongo\Collection
1100
     */
1101
    public function ensureTTLIndex(array $key, $seconds = 0)
1102
    {
1103
        $this->_mongoCollection->ensureIndex($key, array(
1104
            'expireAfterSeconds' => $seconds,
1105
        ));
1106
1107
        return $this;
1108
    }
1109
1110
    /**
1111
     * Create geo index 2dsphere
1112
     *
1113
     * @link http://docs.mongodb.org/manual/tutorial/build-a-2dsphere-index/
1114
     *
1115
     * @param string $field
1116
     * @return \Sokil\Mongo\Collection
1117
     */
1118 View Code Duplication
    public function ensure2dSphereIndex($field)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
1119
    {
1120
        if (is_array($field)) {
1121
            $keys = array_fill_keys($field, '2dsphere');
1122
        } else {
1123
            $keys = array(
1124
                $field => '2dsphere',
1125
            );
1126
        }
1127
1128
        $this->_mongoCollection->ensureIndex($keys);
1129
1130
        return $this;
1131
    }
1132
1133
    /**
1134
     * Create geo index 2dsphere
1135
     *
1136
     * @link http://docs.mongodb.org/manual/tutorial/build-a-2d-index/
1137
     *
1138
     * @param string $field
1139
     * @return \Sokil\Mongo\Collection
1140
     */
1141 View Code Duplication
    public function ensure2dIndex($field)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

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.

Loading history...
1142
    {
1143
        if (is_array($field)) {
1144
            $keys = array_fill_keys($field, '2d');
1145
        } else {
1146
            $keys = array(
1147
                $field => '2d',
1148
            );
1149
        }
1150
1151
        $this->_mongoCollection->ensureIndex($keys);
1152
1153
        return $this;
1154
    }
1155
1156
    /**
1157
     * Create fulltext index
1158
     *
1159
     * @link https://docs.mongodb.org/manual/core/index-text/
1160
     * @link https://docs.mongodb.org/manual/tutorial/specify-language-for-text-index/
1161
     *
1162
     * If a collection contains documents or embedded documents that are in different languages,
1163
     * include a field named language in the documents or embedded documents and specify as its value the language
1164
     * for that document or embedded document.
1165
     *
1166
     * The specified language in the document overrides the default language for the text index.
1167
     * The specified language in an embedded document override the language specified in an enclosing document or
1168
     * the default language for the index.
1169
     *
1170
     * @param array|string $field definition of fields where full text index ensured.
1171
     *  May be string to ensure index on one field, array of fields to create full text index on few fields, and
1172
     *   widdcard '$**'  to create index on all fields of collection. Default value is '$**'
1173
     * @param $weights For a text index, the weight of an indexed field denotes the significance of the field
1174
     *   relative to the other indexed fields in terms of the text search score.
1175
     * @param $defaultLanguage The default language associated with the indexed data determines the rules to parse
1176
     *  word roots (i.e. stemming) and ignore stop words. The default language for the indexed data is english.
1177
     * @param $languageOverride To use a field with a name other than language, include the
1178
     *   language_override option when creating the index.
1179
     *
1180
     * @return Collection
1181
     */
1182
    public function ensureFulltextIndex(
1183
        $field = '$**',
1184
        array $weights = null,
1185
        $defaultLanguage = Language::ENGLISH,
1186
        $languageOverride = null
1187
    ) {
1188
        // keys
1189
        if (is_array($field)) {
1190
            $keys = array_fill_keys($field, 'text');
1191
        } else {
1192
            $keys = array(
1193
                $field => 'text',
1194
            );
1195
        }
1196
1197
        // options
1198
        $options = array(
1199
            'default_language' => $defaultLanguage,
1200
        );
1201
1202
        if (is_array($weights) && $weights) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $weights of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1203
            $options['weights'] = $weights;
1204
        }
1205
1206
        if ($languageOverride) {
1207
            $options['language_override'] = $languageOverride;
1208
        }
1209
1210
        // create index
1211
        $this->_mongoCollection->ensureIndex($keys, $options);
1212
1213
        return $this;
1214
    }
1215
1216
1217
1218
    /**
1219
     * Create indexes based on self::$_index metadata
1220
     *
1221
     * @return \Sokil\Mongo\Collection
1222
     * @throws \Exception
1223
     */
1224
    public function initIndexes()
1225
    {
1226
        // read index definition from collection options
1227
        // if not specified - use defined in property
1228
        $indexDefinition = $this->definition->getOption('index');
1229
1230
        // ensure indexes
1231
        foreach($indexDefinition as $options) {
1232
1233
            if(empty($options['keys'])) {
1234
                throw new Exception('Keys not specified');
1235
            }
1236
1237
            $keys = $options['keys'];
1238
            unset($options['keys']);
1239
1240
            $this->_mongoCollection->ensureIndex($keys, $options);
1241
        }
1242
1243
        return $this;
1244
    }
1245
1246
    /**
1247
     * Get index info
1248
     * @return array
1249
     */
1250
    public function getIndexes()
1251
    {
1252
        return $this->_mongoCollection->getIndexInfo();
1253
    }
1254
1255
    public function readPrimaryOnly()
1256
    {
1257
        $this->_mongoCollection->setReadPreference(\MongoClient::RP_PRIMARY);
1258
        return $this;
1259
    }
1260
1261
    public function readPrimaryPreferred(array $tags = null)
1262
    {
1263
        $this->_mongoCollection->setReadPreference(\MongoClient::RP_PRIMARY_PREFERRED, $tags);
1264
        return $this;
1265
    }
1266
1267
    public function readSecondaryOnly(array $tags = null)
1268
    {
1269
        $this->_mongoCollection->setReadPreference(\MongoClient::RP_SECONDARY, $tags);
1270
        return $this;
1271
    }
1272
1273
    public function readSecondaryPreferred(array $tags = null)
1274
    {
1275
        $this->_mongoCollection->setReadPreference(\MongoClient::RP_SECONDARY_PREFERRED, $tags);
1276
        return $this;
1277
    }
1278
1279
    public function readNearest(array $tags = null)
1280
    {
1281
        $this->_mongoCollection->setReadPreference(\MongoClient::RP_NEAREST, $tags);
1282
        return $this;
1283
    }
1284
1285
    public function getReadPreference()
1286
    {
1287
        return $this->_mongoCollection->getReadPreference();
1288
    }
1289
1290
    /**
1291
     * Define write concern for all requests to current collection
1292
     *
1293
     * @param string|integer $w write concern
1294
     * @param int $timeout timeout in milliseconds
1295
     * @throws \Sokil\Mongo\Exception
1296
     * @return \Sokil\Mongo\Collection
1297
     */
1298
    public function setWriteConcern($w, $timeout = 10000)
1299
    {
1300
        if(!$this->_mongoCollection->setWriteConcern($w, (int) $timeout)) {
1301
            throw new Exception('Error setting write concern');
1302
        }
1303
1304
        return $this;
1305
    }
1306
1307
    /**
1308
     * Define unacknowledged write concern for all requests to current collection
1309
     *
1310
     * @param int $timeout timeout in milliseconds
1311
     * @throws \Sokil\Mongo\Exception
1312
     * @return \Sokil\Mongo\Collection
1313
     */
1314
    public function setUnacknowledgedWriteConcern($timeout = 10000)
1315
    {
1316
        $this->setWriteConcern(0, (int) $timeout);
1317
        return $this;
1318
    }
1319
1320
    /**
1321
     * Define majority write concern for all requests to current collection
1322
     *
1323
     * @param int $timeout timeout in milliseconds
1324
     * @throws \Sokil\Mongo\Exception
1325
     * @return \Sokil\Mongo\Collection
1326
     */
1327
    public function setMajorityWriteConcern($timeout = 10000)
1328
    {
1329
        $this->setWriteConcern('majority', (int) $timeout);
1330
        return $this;
1331
    }
1332
1333
    /**
1334
     * Get currently active write concern on all requests to collection
1335
     *
1336
     * @return int|string write concern
1337
     */
1338
    public function getWriteConcern()
1339
    {
1340
        return $this->_mongoCollection->getWriteConcern();
1341
    }
1342
1343
    /**
1344
     * Get collection stat
1345
     *
1346
     * @return array collection stat
1347
     */
1348
    public function stats()
1349
    {
1350
        return $this->getDatabase()->executeCommand(array(
1351
            'collstats' => $this->getName(),
1352
        ));
1353
    }
1354
}
1355