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 ( 2d4913...1eb6a1 )
by De
02:14
created

Document::relations()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 5
rs 9.4285
cc 1
eloc 2
nc 1
nop 0
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\InvalidOperationException;
15
use Sokil\Mongo\Document\RelationManager;
16
use Sokil\Mongo\Document\RevisionManager;
17
use Sokil\Mongo\Document\InvalidDocumentException;
18
use Sokil\Mongo\Collection\Definition;
19
use Sokil\Mongo\Document\OptimisticLockFailureException;
20
use Sokil\Mongo\Exception\WriteException;
21
use Symfony\Component\EventDispatcher\EventDispatcher;
22
use GeoJson\Geometry\Geometry;
23
24
/**
25
 * Instance of this class is a representation of one document from collection.
26
 *
27
 * @link https://github.com/sokil/php-mongo#document-schema Document schema
28
 * @link https://github.com/sokil/php-mongo#create-new-document Create new document
29
 * @link https://github.com/sokil/php-mongo#get-and-set-data-in-document get and set data
30
 * @link https://github.com/sokil/php-mongo#storing-document Saving document
31
 * @link https://github.com/sokil/php-mongo#document-validation Validation
32
 * @link https://github.com/sokil/php-mongo#deleting-collections-and-documents Deleting documents
33
 * @link https://github.com/sokil/php-mongo#events Event handlers
34
 * @link https://github.com/sokil/php-mongo#behaviors Behaviors
35
 * @link https://github.com/sokil/php-mongo#relations Relations
36
 *
37
 * @method \Sokil\Mongo\Document onAfterConstruct(callable $handler, int $priority = 0)
38
 * @method \Sokil\Mongo\Document onBeforeValidate(callable $handler, int $priority = 0)
39
 * @method \Sokil\Mongo\Document onAfterValidate(callable $handler, int $priority = 0)
40
 * @method \Sokil\Mongo\Document onValidateError(callable $handler, int $priority = 0)
41
 * @method \Sokil\Mongo\Document onBeforeInsert(callable $handler, int $priority = 0)
42
 * @method \Sokil\Mongo\Document onAfterInsert(callable $handler, int $priority = 0)
43
 * @method \Sokil\Mongo\Document onBeforeUpdate(callable $handler, int $priority = 0)
44
 * @method \Sokil\Mongo\Document onAfterUpdate(callable $handler, int $priority = 0)
45
 * @method \Sokil\Mongo\Document onBeforeSave(callable $handler, int $priority = 0)
46
 * @method \Sokil\Mongo\Document onAfterSave(callable $handler, int $priority = 0)
47
 * @method \Sokil\Mongo\Document onBeforeDelete(callable $handler, int $priority = 0)
48
 * @method \Sokil\Mongo\Document onAfterDelete(callable $handler, int $priority = 0)
49
 *
50
 * @author Dmytro Sokil <[email protected]>
51
 */
52
class Document extends Structure
53
{
54
    /**
55
     *
56
     * @var \Sokil\Mongo\Document\RelationManager
57
     */
58
    private $relationManager;
59
60
    const RELATION_HAS_ONE = 'HAS_ONE';
61
    const RELATION_BELONGS = 'BELONGS';
62
    const RELATION_HAS_MANY = 'HAS_MANY';
63
    const RELATION_MANY_MANY = 'MANY_MANY';
64
65
    /**
66
     *
67
     * @var \Sokil\Mongo\Document\RevisionManager
68
     */
69
    private $revisionManager;
70
71
    /**
72
     *
73
     * @var \Sokil\Mongo\Collection
74
     */
75
    private $collection;
76
77
    /**
78
     * @var \Symfony\Component\EventDispatcher\EventDispatcher Event Dispatcher instance
79
     */
80
    private $eventDispatcher;
81
82
    /**
83
     * @var \Sokil\Mongo\Operator Modification operator instance
84
     */
85
    private $operator;
86
87
    /**
88
     *
89
     * @var array list of defined behaviors
90
     */
91
    private $behaviors = array();
92
93
    /**
94
     *
95
     * @var array document options
96
     */
97
    private $options;
98
99
    /**
100
     * @param \Sokil\Mongo\Collection $collection instance of Mongo collection
101
     * @param array $data mongo document
102
     * @param array $options options of object initialization
103
     */
104
    public function __construct(Collection $collection, array $data = null, array $options = array())
105
    {
106
        // link to collection
107
        $this->collection = $collection;
108
109
        // configure document with options
110
        $this->options = $options;
111
112
        // init document
113
        $this->initDelegates();
114
115
        // initialize with data
116
        parent::__construct($data, $this->getOption('stored'));
117
118
        // use versioning
119
        if ($this->getOption('versioning')) {
120
            $this->getRevisionManager()->listen();
121
        }
122
123
        // execure after construct event handlers
124
        $this->eventDispatcher->dispatch('afterConstruct');
125
    }
126
127
    public function getOptions()
128
    {
129
        return $this->options;
130
    }
131
132
    public function getOption($name, $default = null)
133
    {
134
        return isset($this->options[$name]) ? $this->options[$name] : $default;
135
    }
136
137
    public function hasOption($name)
138
    {
139
        return isset($this->options[$name]);
140
    }
141
142
    /**
143
     * Get instance of collection
144
     * @return \Sokil\Mongo\Collection
145
     */
146
    public function getCollection()
147
    {
148
        return $this->collection;
149
    }
150
151
    /**
152
     * Reset all data passed to object in run-time, like events, behaviors,
153
     * data modifications, etc. to the state just after open or save document
154
     *
155
     * @return \Sokil\Mongo\Document
156
     */
157
    public function reset()
158
    {
159
        // reset structure
160
        parent::reset();
161
162
        // reset errors
163
        $this->clearErrors();
164
165
        // reset behaviors
166
        $this->clearBehaviors();
167
168
        // init delegates
169
        $this->initDelegates();
170
171
        return $this;
172
    }
173
174
    /**
175
     * Reload data from db and reset all unsaved data
176
     */
177
    public function refresh()
178
    {
179
        $data = $this->collection
180
            ->getMongoCollection()
181
            ->findOne(array(
182
                '_id' => $this->getId()
183
            ));
184
185
        $this->replace($data);
186
187
        $this->operator->reset();
188
189
        return $this;
190
    }
191
192
    /**
193
     * Initialise relative classes
194
     */
195
    private function initDelegates()
196
    {
197
        // start event dispatching
198
        $this->eventDispatcher = new EventDispatcher;
199
        
200
        // create operator
201
        $this->operator = $this->getCollection()->operator();
202
203
        // attach behaviors
204
        $this->attachBehaviors($this->behaviors());
205
        if ($this->hasOption('behaviors')) {
206
            $this->attachBehaviors($this->getOption('behaviors'));
207
        }
208
    }
209
210
    public function __toString()
211
    {
212
        return (string) $this->getId();
213
    }
214
215
    public function __call($name, $arguments)
216
    {
217
        // behaviors
218
        foreach ($this->behaviors as $behavior) {
219
            if (!method_exists($behavior, $name)) {
220
                continue;
221
            }
222
223
            return call_user_func_array(array($behavior, $name), $arguments);
224
        }
225
226
        // adding event
227
        if ('on' === substr($name, 0, 2)) {
228
            // prepend event name to function args
229
            $addListenerArguments = $arguments;
230
            array_unshift($addListenerArguments, lcfirst(substr($name, 2)));
231
            // add listener
232
            call_user_func_array(
233
                array($this->eventDispatcher, 'addListener'),
234
                $addListenerArguments
235
            );
236
            
237
            return $this;
238
        }
239
240
        // getter
241
        if ('get' === strtolower(substr($name, 0, 3))) {
242
            return $this->get(lcfirst(substr($name, 3)));
243
        }
244
245
        // setter
246
        if ('set' === strtolower(substr($name, 0, 3)) && isset($arguments[0])) {
247
            return $this->set(lcfirst(substr($name, 3)), $arguments[0]);
248
        }
249
250
        throw new Exception('Document has no method "' . $name . '"');
251
    }
252
253
    public function __get($name)
254
    {
255
        if ($this->getRelationManager()->isRelationExists($name)) {
256
            // resolve relation
257
            return $this->getRelationManager()->getRelated($name);
258
        } else {
259
            // get document parameter
260
            return parent::__get($name);
261
        }
262
    }
263
264
    /**
265
     * Set geo data as GeoJson object
266
     *
267
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
268
     * to use Point, LineString and Polygon.
269
     *
270
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
271
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
272
     *
273
     * @link http://geojson.org/
274
     * @param string $field
275
     * @param \GeoJson\Geometry\Geometry $geometry
276
     * @return \Sokil\Mongo\Document
277
     */
278
    public function setGeometry($field, Geometry $geometry)
279
    {
280
        return $this->set($field, $geometry);
281
    }
282
283
    /**
284
     * Set point as longitude and latitude
285
     *
286
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
287
     * to use Point, LineString and Polygon.
288
     *
289
     * @link http://docs.mongodb.org/manual/core/2dsphere/#point
290
     * @param string $field
291
     * @param float $longitude
292
     * @param float $latitude
293
     * @return \Sokil\Mongo\Document
294
     */
295
    public function setPoint($field, $longitude, $latitude)
296
    {
297
        return $this->setGeometry(
298
            $field,
299
            new \GeoJson\Geometry\Point(array(
300
                $longitude,
301
                $latitude
302
            ))
303
        );
304
    }
305
306
    /**
307
     * Set point as longitude and latitude in legacy format
308
     *
309
     * May be used 2d index
310
     *
311
     * @link http://docs.mongodb.org/manual/core/2d/#geospatial-indexes-store-grid-coordinates
312
     * @param string $field
313
     * @param float $longitude
314
     * @param float $latitude
315
     * @return \Sokil\Mongo\Document
316
     */
317
    public function setLegacyPoint($field, $longitude, $latitude)
318
    {
319
        return $this->set(
320
            $field,
321
            array($longitude, $latitude)
322
        );
323
    }
324
325
    /**
326
     * Set line string as array of points
327
     *
328
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
329
     * to use Point, LineString and Polygon.
330
     *
331
     * @link http://docs.mongodb.org/manual/core/2dsphere/#linestring
332
     * @param string $field
333
     * @param array $pointArray array of points
334
     * @return \Sokil\Mongo\Document
335
     */
336
    public function setLineString($field, array $pointArray)
337
    {
338
        return $this->setGeometry(
339
            $field,
340
            new \GeoJson\Geometry\LineString($pointArray)
341
        );
342
    }
343
344
    /**
345
     * Set polygon as array of line rings.
346
     *
347
     * Line ring is closed line string (first and last point same).
348
     * Line string is array of points.
349
     *
350
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
351
     * to use Point, LineString and Polygon.
352
     *
353
     * @link http://docs.mongodb.org/manual/core/2dsphere/#polygon
354
     * @param string $field
355
     * @param array $lineRingsArray array of line rings
356
     * @return \Sokil\Mongo\Document
357
     */
358
    public function setPolygon($field, array $lineRingsArray)
359
    {
360
        return $this->setGeometry(
361
            $field,
362
            new \GeoJson\Geometry\Polygon($lineRingsArray)
363
        );
364
    }
365
366
    /**
367
     * Set multi point as array of points
368
     *
369
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
370
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
371
     *
372
     * @link http://docs.mongodb.org/manual/core/2dsphere/#multipoint
373
     * @param string $field
374
     * @param array $pointArray array of point arrays
375
     * @return \Sokil\Mongo\Document
376
     */
377
    public function setMultiPoint($field, $pointArray)
378
    {
379
        return $this->setGeometry(
380
            $field,
381
            new \GeoJson\Geometry\MultiPoint($pointArray)
382
        );
383
    }
384
385
    /**
386
     * Set multi line string as array of line strings
387
     *
388
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
389
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
390
     *
391
     * http://docs.mongodb.org/manual/core/2dsphere/#multilinestring
392
     * @param string $field
393
     * @param array $lineStringArray array of line strings
394
     * @return \Sokil\Mongo\Document
395
     */
396
    public function setMultiLineString($field, $lineStringArray)
397
    {
398
        return $this->setGeometry(
399
            $field,
400
            new \GeoJson\Geometry\MultiLineString($lineStringArray)
401
        );
402
    }
403
404
    /**
405
     * Set multy polygon as array of polygons.
406
     *
407
     * Polygon is array of line rings.
408
     * Line ring is closed line string (first and last point same).
409
     * Line string is array of points.
410
     *
411
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
412
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
413
     *
414
     * @link http://docs.mongodb.org/manual/core/2dsphere/#multipolygon
415
     * @param string $field
416
     * @param array $polygonsArray array of polygons
417
     * @return \Sokil\Mongo\Document
418
     */
419
    public function setMultyPolygon($field, array $polygonsArray)
420
    {
421
        return $this->setGeometry(
422
            $field,
423
            new \GeoJson\Geometry\MultiPolygon($polygonsArray)
424
        );
425
    }
426
427
    /**
428
     * Set collection of different geometries
429
     *
430
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
431
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
432
     *
433
     * @link http://docs.mongodb.org/manual/core/2dsphere/#geometrycollection
434
     * @param string $field
435
     * @param array $geometryCollection
436
     * @return \Sokil\Mongo\Document
437
     */
438
    public function setGeometryCollection($field, array $geometryCollection)
439
    {
440
        return $this->setGeometry(
441
            $field,
442
            new \GeoJson\Geometry\GeometryCollection($geometryCollection)
443
        );
444
    }
445
446
    /**
447
     * Override in child class to define relations
448
     * @return array relation description
449
     */
450
    protected function relations()
451
    {
452
        // [relationName => [relationType, targetCollection, reference], ...]
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
453
        return array();
454
    }
455
456
    /**
457
     * Relation definition through mapping is more prior to defined in class
458
     * @return array definition of relations
459
     */
460
    public function getRelationDefinition()
461
    {
462
        $relations = $this->getOption('relations');
463
        if (!is_array($relations)) {
464
            return $this->relations();
465
        }
466
467
        return $relations + $this->relations();
468
    }
469
470
    /**
471
     *
472
     * @return \Sokil\Mongo\Document\RelationManager
473
     */
474
    private function getRelationManager()
475
    {
476
        if ($this->relationManager) {
477
            return $this->relationManager;
478
        }
479
480
        $this->relationManager = new RelationManager($this);
481
482
        return $this->relationManager;
483
    }
484
485
    /**
486
     * Get related documents
487
     * @param string $relationName
488
     * @return array|\Sokil\Mongo\Document related document or array of documents
489
     */
490
    public function getRelated($relationName)
491
    {
492
        return $this->getRelationManager()->getRelated($relationName);
493
    }
494
495
    public function addRelation($relationName, Document $document)
496
    {
497
        $this->getRelationManager()->addRelation($relationName, $document);
498
499
        return $this;
500
    }
501
502
    public function removeRelation($relationName, Document $document = null)
503
    {
504
        $this->getRelationManager()->removeRelation($relationName, $document);
505
506
        return $this;
507
    }
508
509
    /**
510
     * Manually trigger defined events
511
     * @param string $eventName event name
512
     * @return \Sokil\Mongo\Event
513
     */
514
    public function triggerEvent($eventName, Event $event = null)
515
    {
516
        if (!$event) {
517
            $event = new Event;
518
        }
519
520
        $event->setTarget($this);
521
522
        return $this->eventDispatcher->dispatch($eventName, $event);
523
    }
524
525
    /**
526
     * Attach event handler
527
     * @param string $event event name
528
     * @param callable|array|string $handler event handler
529
     * @return \Sokil\Mongo\Document
530
     */
531
    public function attachEvent($event, $handler, $priority = 0)
532
    {
533
        $this->eventDispatcher->addListener($event, $handler, $priority);
534
        return $this;
535
    }
536
537
    /**
538
     * Check if event attached
539
     *
540
     * @param string $event event name
541
     * @return bool
542
     */
543
    public function hasEvent($event)
544
    {
545
        return $this->eventDispatcher->hasListeners($event);
546
    }
547
548
    public function getId()
549
    {
550
        return $this->get('_id');
551
    }
552
553
    /**
554
     * Get normalized id of document.
555
     * If id is valid ObjectId (string consisting of exactly 24 hexadecimal characters), convert it to \MongoId.
556
     *
557
     * @param \MongoId|string|int $id document identifier
558
     *
559
     * @return \MongoId|string|int
560
     */
561
    private function normalizeDocumentId($id)
562
    {
563
        if (\MongoId::isValid($id) && !$id instanceof \MongoId) {
564
            return new \MongoId($id);
565
        } else {
566
            return $id;
567
        }
568
    }
569
570
    /**
571
     * Used to define id of stored document.
572
     * This id must be already present in db
573
     *
574
     * @param \MongoId|string|int $id document identifier
575
     *
576
     * @return Document
577
     */
578
    public function defineId($id)
579
    {
580
        $this->mergeUnmodified(array('_id' => $this->normalizeDocumentId($id)));
581
        return $this;
582
    }
583
584
    /**
585
     * Used to define id of not stored document or chane id of stored document.
586
     *
587
     * @param \MongoId|string $id id of document
588
     *
589
     * @return Document
590
     */
591
    public function setId($id)
592
    {
593
        return $this->set('_id', $this->normalizeDocumentId($id));
594
    }
595
596
    /**
597
     * Check if document is stored
598
     *
599
     * @return bool
600
     */
601
    public function isStored()
602
    {
603
        return $this->get('_id') && !$this->isModified('_id');
604
    }
605
606
    /**
607
     * Validate document
608
     *
609
     * @throws \Sokil\Mongo\Document\InvalidDocumentException
610
     * @return \Sokil\Mongo\Document
611
     */
612
    public function validate()
613
    {
614
        if ($this->triggerEvent('beforeValidate')->isCancelled()) {
615
            return $this;
616
        }
617
618
        if (!$this->isValid()) {
619
            $exception = new InvalidDocumentException('Document not valid');
620
            $exception->setDocument($this);
621
622
            $this->triggerEvent('validateError');
623
624
            throw $exception;
625
        }
626
627
        $this->triggerEvent('afterValidate');
628
629
        return $this;
630
    }
631
632
    public function behaviors()
633
    {
634
        return array();
635
    }
636
637
    public function attachBehaviors(array $behaviors)
638
    {
639
        foreach ($behaviors as $name => $behavior) {
640
            $this->attachBehavior($name, $behavior);
641
        }
642
643
        return $this;
644
    }
645
646
    /**
647
     *
648
     * @param string $name unique name of attached behavior
649
     * @param string|array|\Sokil\Mongo\Behavior $behavior Behavior instance or behavior definition
650
     * @return \Sokil\Mongo\Document
651
     * @throws Exception
652
     */
653
    public function attachBehavior($name, $behavior)
654
    {
655
        if (is_string($behavior)) {
656
            // behavior defined as string
657
            $className = $behavior;
658
            $behavior = new $className();
659
        } elseif (is_array($behavior)) {
660
            // behavior defined as array
661
            if (empty($behavior['class'])) {
662
                throw new Exception('Behavior class not specified');
663
            }
664
            $className = $behavior['class'];
665
            unset($behavior['class']);
666
            $behavior = new $className($behavior);
667
        } elseif (!($behavior instanceof Behavior)) {
668
            // behavior bust be Behavior instance, but something else found
669
            throw new Exception('Wrong behavior specified with name ' . $name);
670
        }
671
672
        $behavior->setOwner($this);
673
674
        $this->behaviors[$name] = $behavior;
675
676
        return $this;
677
    }
678
679
    public function clearBehaviors()
680
    {
681
        $this->behaviors = array();
682
        return $this;
683
    }
684
685
    public function getOperator()
686
    {
687
        return $this->operator;
688
    }
689
690
    public function isModificationOperatorDefined()
691
    {
692
        return $this->operator->isDefined();
693
    }
694
695
    /**
696
     * Update value in local cache and in DB
697
     *
698
     * @param string $fieldName point-delimited field name
699
     * @param mixed $value value to store
700
     * @return \Sokil\Mongo\Document
701
     */
702
    public function set($fieldName, $value)
703
    {
704
        parent::set($fieldName, $value);
705
706
        // if document saved - save through update
707
        if ($this->getId()) {
708
            $this->operator->set($fieldName, $value);
709
        }
710
711
        return $this;
712
    }
713
714
    /**
715
     * Remove field
716
     *
717
     * @param string $fieldName field name
718
     * @return \Sokil\Mongo\Document
719
     */
720
    public function unsetField($fieldName)
721
    {
722
        if (!$this->has($fieldName)) {
723
            return $this;
724
        }
725
726
        parent::unsetField($fieldName);
727
728
        if ($this->getId()) {
729
            $this->operator->unsetField($fieldName);
730
        }
731
732
        return $this;
733
    }
734
735
    public function __unset($fieldName)
736
    {
737
        $this->unsetField($fieldName);
738
    }
739
740
    /**
741
     * Get reference to document
742
     *
743
     * @throws Exception
744
     * @return array
745
     */
746
    public function createReference()
747
    {
748
        $documentId = $this->getId();
749
        if (null === $documentId) {
750
            throw new Exception('Document must be stored to get DBRef');
751
        }
752
753
        return $this
754
            ->getCollection()
755
            ->getMongoCollection()
756
            ->createDBRef($documentId);
757
    }
758
759
    /**
760
     * Store DBRef to specified field
761
     *
762
     * @param $name
763
     * @param Document $document
764
     * @return Document
765
     */
766
    public function setReference($name, Document $document)
767
    {
768
        return $this->set(
769
            $name,
770
            $document->createReference()
771
        );
772
    }
773
774
    /**
775
     * Get document by reference
776
     *
777
     * @param string    $name   name of field where reference stored
778
     * @return null|Document
779
     */
780
    public function getReferencedDocument($name)
781
    {
782
        $reference = $this->get($name);
783
        if (null === $reference) {
784
            return null;
785
        }
786
787
        return $this->collection
788
            ->getDatabase()
789
            ->getDocumentByReference($reference);
790
    }
791
792
    /**
793
     * Push reference to list
794
     *
795
     * @param string $name
796
     * @param Document $document
797
     * @return Document
798
     */
799
    public function pushReference($name, Document $document)
800
    {
801
        return $this->push(
802
            $name,
803
            $document->createReference()
804
        );
805
    }
806
807
    /**
808
     * Get document by reference
809
     *
810
     * @param string    $name   name of field where reference stored
811
     * @return null|Document
812
     *
813
     * @throws Exception
814
     */
815
    public function getReferencedDocumentList($name)
816
    {
817
        $referenceList = $this->get($name);
818
        if (null === $referenceList) {
819
            return null;
820
        }
821
822
        if (!isset($referenceList[0])) {
823
            throw new Exception('List of references not found');
824
        }
825
826
        // build list of referenced collections and ids
827
        $documentIdList = array();
828
        foreach ($referenceList as $reference) {
829
            if (empty($reference['$ref']) || empty($reference['$id'])) {
830
                throw new Exception(sprintf(
831
                    'Invalid reference in list for document %s in field %s',
832
                    $this->getId(),
833
                    $name
834
                ));
835
            }
836
837
            $documentIdList[$reference['$ref']][] = $reference['$id'];
838
        }
839
840
        // get list
841
        $documentList = array();
842
        $database = $this->collection->getDatabase();
843
        foreach ($documentIdList as $collectionName => $documentIdList) {
844
            $documentList += $database->getCollection($collectionName)->find()->byIdList($documentIdList)->findAll();
845
        }
846
847
        return $documentList;
848
    }
849
850
    /**
851
     * @param array $data
852
     * @return Document
853
     */
854
    public function merge(array $data)
855
    {
856
        if ($this->isStored()) {
857
            foreach ($data as $fieldName => $value) {
858
                $this->set($fieldName, $value);
859
            }
860
        } else {
861
            parent::merge($data);
862
        }
863
864
        return $this;
865
    }
866
867
    /**
868
     * If field not exist - set value.
869
     * If field exists and is not array - convert to array and append
870
     * If field is array - append
871
     *
872
     * @param string $selector
873
     * @param mixed $value
874
     * @return Document
875
     */
876
    public function append($selector, $value)
877
    {
878
        parent::append($selector, $value);
879
880
        // if document saved - save through update
881
        if ($this->getId()) {
882
            $this->operator->set($selector, $this->get($selector));
883
        }
884
885
        return $this;
886
    }
887
888
    /**
889
     * Push argument as single element to field value
890
     *
891
     * @param string $fieldName
892
     * @param mixed $value
893
     * @return \Sokil\Mongo\Document
894
      */
895
    public function push($fieldName, $value)
896
    {
897
        $oldValue = $this->get($fieldName);
898
899
        // check if old value is list or sub document
900
        // on sub document throw exception
901
        if (is_array($oldValue)) {
902
            $isSubDocument = (array_keys($oldValue) !== range(0, count($oldValue) - 1));
903
            if ($isSubDocument) {
904
                throw new InvalidOperationException(sprintf('The field "%s" must be an array but is of type Object', $fieldName));
905
            }
906
        }
907
908
        // prepare new value
909
        $value = Structure::prepareToStore($value);
910
911
        // field not exists
912
        if (!$oldValue) {
913
            if ($this->getId()) {
914
                $this->operator->push($fieldName, $value);
915
            }
916
            $value = array($value);
917
        } // field already exist and has single value
918 View Code Duplication
        elseif (!is_array($oldValue)) {
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...
919
            $value = array_merge((array) $oldValue, array($value));
920
            if ($this->getId()) {
921
                $this->operator->set($fieldName, $value);
922
            }
923
        } // field exists and is array
924
        else {
925
            if ($this->getId()) {
926
                $setValue = $this->operator->get('$set', $fieldName);
927
                if ($setValue) {
928
                    $setValue[] = $value;
929
                    $this->operator->set($fieldName, $setValue);
930
                } else {
931
                    $this->operator->push($fieldName, $value);
932
                }
933
            }
934
            $value = array_merge($oldValue, array($value));
935
        }
936
937
        // update local data
938
        parent::set($fieldName, $value);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of push()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
939
940
        return $this;
941
    }
942
943
    /**
944
     * Push each element of argument's array as single element to field value
945
     *
946
     * @param string $fieldName
947
     * @param array $values
948
     * @return \Sokil\Mongo\Document
949
     */
950
    public function pushEach($fieldName, array $values)
951
    {
952
        $oldValue = $this->get($fieldName);
953
954
        if ($this->getId()) {
955
            if (!$oldValue) {
956
                $this->operator->pushEach($fieldName, $values);
957 View Code Duplication
            } elseif (!is_array($oldValue)) {
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...
958
                $values = array_merge((array) $oldValue, $values);
959
                $this->operator->set($fieldName, $values);
960
            } else {
961
                $this->operator->pushEach($fieldName, $values);
962
                $values = array_merge($oldValue, $values);
963
            }
964
        } else {
965
            if ($oldValue) {
966
                $values = array_merge((array) $oldValue, $values);
967
            }
968
        }
969
970
        // update local data
971
        parent::set($fieldName, $values);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of pushEach()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
972
973
        return $this;
974
    }
975
976
    public function addToSet($fieldName, $value)
977
    {
978
        $set = $this->get($fieldName);
979
980
        // prepare new value
981
        $value = Structure::prepareToStore($value);
982
983
        // add to set
984
        if (empty($set)) {
985
            $updatedSet = array($value);
986
            if ($this->getId()) {
987
                $this->operator->addToSet($fieldName, $value);
988
            }
989
        } elseif (!is_array($set)) {
990
            if ($set === $value) {
991
                return $this;
992
            }
993
            $updatedSet = array($set, $value);
994
            if ($this->getId()) {
995
                $this->operator->set($fieldName, $updatedSet);
996
            }
997
        } elseif (array_keys($set) !== range(0, count($set) - 1)) {
998
            // check if old value is list or sub document
999
            // on sub document throw exception
1000
            throw new InvalidOperationException(sprintf('The field "%s" must be an array but is of type Object', $fieldName));
1001
        } else {
1002
            // check if already in set
1003
            if (in_array($value, $set, true)) {
1004
                return $this;
1005
            }
1006
            $updatedSet = array_merge($set, array($value));
1007
            if ($this->getId()) {
1008
                $setValue = $this->operator->get('$set', $fieldName);
1009
                if ($setValue) {
1010
                    $this->operator->set($fieldName, $updatedSet);
1011
                } else {
1012
                    $this->operator->addToSet($fieldName, $value);
1013
                }
1014
            }
1015
        }
1016
1017
        parent::set($fieldName, $updatedSet);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of addToSet()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1018
1019
        return $this;
1020
    }
1021
1022
    public function addToSetEach($fieldName, array $values)
0 ignored issues
show
Unused Code introduced by
The parameter $fieldName is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $values is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1023
    {
1024
        throw new \RuntimeException('Not implemented');
1025
    }
1026
1027
    /**
1028
     * Removes from an existing array all instances of a value or
1029
     * values that match a specified query
1030
     *
1031
     * @param integer|string|array|\Sokil\Mongo\Expression|callable $expression
1032
     * @param mixed|\Sokil\Mongo\Expression|callable $value
1033
     * @return \Sokil\Mongo\Document
1034
     */
1035
    public function pull($expression, $value = null)
1036
    {
1037
        $this->operator->pull($expression, $value);
1038
        return $this;
1039
    }
1040
1041
    public function increment($fieldName, $value = 1)
1042
    {
1043
        parent::set($fieldName, (int) $this->get($fieldName) + $value);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of increment()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1044
1045
        if ($this->getId()) {
1046
            $this->operator->increment($fieldName, $value);
1047
        }
1048
1049
1050
        return $this;
1051
    }
1052
1053
    public function decrement($fieldName, $value = 1)
1054
    {
1055
        return $this->increment($fieldName, -1 * $value);
1056
    }
1057
1058 View Code Duplication
    public function bitwiceAnd($field, $value)
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...
1059
    {
1060
        parent::set($field, (int) $this->get($field) & $value);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of bitwiceAnd()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1061
1062
        if ($this->getId()) {
1063
            $this->operator->bitwiceAnd($field, $value);
1064
        }
1065
1066
        return $this;
1067
    }
1068
1069 View Code Duplication
    public function bitwiceOr($field, $value)
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...
1070
    {
1071
        parent::set($field, (int) $this->get($field) | $value);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of bitwiceOr()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1072
1073
        if ($this->getId()) {
1074
            $this->operator->bitwiceOr($field, $value);
1075
        }
1076
1077
        return $this;
1078
    }
1079
1080
    public function bitwiceXor($field, $value)
1081
    {
1082
        $oldFieldValue = (int) $this->get($field);
1083
        $newValue = $oldFieldValue ^ $value;
1084
1085
        parent::set($field, $newValue);
0 ignored issues
show
Comprehensibility Bug introduced by
It seems like you call parent on a different method (set() instead of bitwiceXor()). Are you sure this is correct? If so, you might want to change this to $this->set().

This check looks for a call to a parent method whose name is different than the method from which it is called.

Consider the following code:

class Daddy
{
    protected function getFirstName()
    {
        return "Eidur";
    }

    protected function getSurName()
    {
        return "Gudjohnsen";
    }
}

class Son
{
    public function getFirstName()
    {
        return parent::getSurname();
    }
}

The getFirstName() method in the Son calls the wrong method in the parent class.

Loading history...
1086
1087
        if ($this->getId()) {
1088
            if (version_compare($this->getCollection()->getDatabase()->getClient()->getDbVersion(), '2.6', '>=')) {
1089
                $this->operator->bitwiceXor($field, $value);
1090
            } else {
1091
                $this->operator->set($field, $newValue);
1092
            }
1093
        }
1094
1095
        return $this;
1096
    }
1097
1098
    /**
1099
     * Internal method to insert document
1100
     */
1101
    private function internalInsert()
1102
    {
1103
        if ($this->triggerEvent('beforeInsert')->isCancelled()) {
1104
            return;
1105
        }
1106
1107
        $document = $this->toArray();
1108
1109
        // try write document
1110
        try {
1111
            $this
1112
                ->collection
1113
                ->getMongoCollection()
1114
                ->insert($document);
1115
        } catch (\Exception $e) {
1116
            throw new WriteException(
1117
                $e->getMessage(),
1118
                $e->getCode(),
1119
                $e
1120
            );
1121
        }
1122
1123
        // set id
1124
        $this->defineId($document['_id']);
1125
1126
        // after insert event
1127
        $this->triggerEvent('afterInsert');
1128
    }
1129
1130
    /**
1131
     * Internal method to update document
1132
     *
1133
     * @throws WriteException
1134
     * @throws OptimisticLockFailureException
1135
     */
1136
    private function internalUpdate()
1137
    {
1138
        if ($this->triggerEvent('beforeUpdate')->isCancelled()) {
1139
            return;
1140
        }
1141
1142
        // locking
1143
        $query = array('_id' => $this->getId());
1144
        if ($this->getOption('lock') === Definition::LOCK_OPTIMISTIC) {
1145
            $query['__version__'] = $this->get('__version__');
1146
            $this->getOperator()->increment('__version__');
1147
        }
1148
1149
        // update
1150
        try {
1151
            $status = $this
1152
                ->collection
1153
                ->getMongoCollection()
1154
                ->update(
1155
                    $query,
1156
                    $this->getOperator()->toArray()
1157
                );
1158
        } catch (\Exception $e) {
1159
            throw new WriteException(
1160
                $e->getMessage(),
1161
                $e->getCode(),
1162
                $e
1163
            );
1164
        }
1165
1166
        // check update status
1167
        if ($status['ok'] != 1) {
1168
            throw new WriteException(
1169
                sprintf(
1170
                    'Update error: %s: %s',
1171
                    $status['err'],
1172
                    $status['errmsg']
1173
                )
1174
            );
1175
        }
1176
1177
        // check if document modified due to specified lock
1178
        if ($this->getOption('lock') === Definition::LOCK_OPTIMISTIC) {
1179
            if ($status['n'] === 0) {
1180
                throw new OptimisticLockFailureException;
1181
            }
1182
        }
1183
1184
        if ($this->getOperator()->isReloadRequired()) {
1185
            $this->refresh();
1186
        } else {
1187
            $this->getOperator()->reset();
1188
        }
1189
1190
        $this->triggerEvent('afterUpdate');
1191
    }
1192
1193
    /**
1194
     * Save document
1195
     *
1196
     * @param bool $validate
1197
     * @return Document
1198
     * @throws WriteException
1199
     */
1200
    public function save($validate = true)
1201
    {
1202
        // if document already in db and not modified - skip this method
1203
        if (!$this->isSaveRequired()) {
1204
            return $this;
1205
        }
1206
1207
        if ($validate) {
1208
            $this->validate();
1209
        }
1210
1211
        // handle beforeSave event
1212
        if ($this->triggerEvent('beforeSave')->isCancelled()) {
1213
            return $this;
1214
        }
1215
1216
        // write document
1217
        if ($this->isStored()) {
1218
            $this->internalUpdate();
1219
        } else {
1220
            $this->internalInsert();
1221
        }
1222
1223
        // handle afterSave event
1224
        $this->triggerEvent('afterSave');
1225
1226
        // set document unmodified
1227
        $this->apply();
1228
        
1229
        return $this;
1230
    }
1231
1232
    /**
1233
     * Check if document require save
1234
     *
1235
     * @return bool
1236
     */
1237
    public function isSaveRequired()
1238
    {
1239
        return !$this->isStored() || $this->isModified() || $this->isModificationOperatorDefined();
1240
    }
1241
1242
    /**
1243
     * Delete document
1244
     *
1245
     * @return Document
1246
     * @throws Exception
1247
     */
1248
    public function delete()
1249
    {
1250
        if ($this->triggerEvent('beforeDelete')->isCancelled()) {
1251
            return $this;
1252
        }
1253
1254
        $status = $this->collection->getMongoCollection()->remove(array(
1255
            '_id'   => $this->getId(),
1256
        ));
1257
1258
        if (true !== $status && $status['ok'] != 1) {
1259
            throw new \Sokil\Mongo\Exception(sprintf('Delete document error: %s', $status['err']));
1260
        }
1261
1262
        $this->triggerEvent('afterDelete');
1263
1264
        // drop from document's pool
1265
        $this->getCollection()->removeDocumentFromDocumentPool($this);
1266
1267
        return $this;
1268
    }
1269
1270
    /**
1271
     * Get revisions manager
1272
     *
1273
     * @return \Sokil\Mongo\Document\RevisionManager
1274
     */
1275
    public function getRevisionManager()
1276
    {
1277
        if (!$this->revisionManager) {
1278
            $this->revisionManager = new RevisionManager($this);
1279
        }
1280
1281
        return $this->revisionManager;
1282
    }
1283
1284
    /**
1285
     * @deprecated since 1.13.0 use self::getRevisionManager()->getRevisions()
1286
     */
1287
    public function getRevisions($limit = null, $offset = null)
1288
    {
1289
        return $this->getRevisionManager()->getRevisions($limit, $offset);
1290
    }
1291
1292
    /**
1293
     * @deprecated since 1.13.0 use self::getRevisionManager()->getRevision()
1294
     */
1295
    public function getRevision($id)
1296
    {
1297
        return $this->getRevisionManager()->getRevision($id);
1298
    }
1299
1300
    /**
1301
     * @deprecated since 1.13.0 use self::getRevisionManager()->getRevisionsCount()
1302
     */
1303
    public function getRevisionsCount()
1304
    {
1305
        return $this->getRevisionManager()->getRevisionsCount();
1306
    }
1307
1308
    /**
1309
     * @deprecated since 1.13.0 use self::getRevisionManager()->clearRevisions()
1310
     */
1311
    public function clearRevisions()
1312
    {
1313
        $this->getRevisionManager()->clearRevisions();
1314
        return $this;
1315
    }
1316
}
1317