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 ( d42e2b...ab132c )
by De
03:07
created

Document::save()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 31
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 31
rs 8.439
c 0
b 0
f 0
cc 5
eloc 14
nc 7
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\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
        // lisk 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
        // execute before construct callable
116
        $this->beforeConstruct();
117
118
        // initialize with data
119
        parent::__construct($data, $this->getOption('stored'));
120
121
        // use versioning
122
        if($this->getOption('versioning')) {
123
            $this->getRevisionManager()->listen();
124
        }
125
126
        // execure after construct event handlers
127
        $this->eventDispatcher->dispatch('afterConstruct');
128
    }
129
130
    public function getOptions()
131
    {
132
        return $this->options;
133
    }
134
135
    public function getOption($name, $default = null)
136
    {
137
        return isset($this->options[$name]) ? $this->options[$name] : $default;
138
    }
139
140
    public function hasOption($name)
141
    {
142
        return isset($this->options[$name]);
143
    }
144
145
    /**
146
     * Event handler, called before running constructor.
147
     * May be overridden in child classes
148
     */
149
    public function beforeConstruct()
150
    {
151
152
    }
153
154
    /**
155
     * Get instance of collection
156
     * @return \Sokil\Mongo\Collection
157
     */
158
    public function getCollection()
159
    {
160
        return $this->collection;
161
    }
162
163
    /**
164
     * Reset all data passed to object in run-time, like events, behaviors,
165
     * data modifications, etc. to the state just after open or save document
166
     *
167
     * @return \Sokil\Mongo\Document
168
     */
169
    public function reset()
170
    {
171
        // reset structure
172
        parent::reset();
173
174
        // reset errors
175
        $this->clearErrors();
176
177
        // reset behaviors
178
        $this->clearBehaviors();
179
180
        // init delegates
181
        $this->initDelegates();
182
183
        return $this;
184
    }
185
186
    /**
187
     * Reload data from db and reset all unsaved data
188
     */
189
    public function refresh()
190
    {
191
        $data = $this->collection
192
            ->getMongoCollection()
193
            ->findOne(array(
194
                '_id' => $this->getId()
195
            ));
196
197
        $this->replace($data);
198
199
        $this->operator->reset();
200
201
        return $this;
202
    }
203
204
    /**
205
     * Initialise relative classes
206
     */
207
    private function initDelegates()
208
    {
209
        // start event dispatching
210
        $this->eventDispatcher = new EventDispatcher;
211
        
212
        // create operator
213
        $this->operator = $this->getCollection()->operator();
214
215
        // attach behaviors
216
        $this->attachBehaviors($this->behaviors());
217
        if($this->hasOption('behaviors')) {
218
            $this->attachBehaviors($this->getOption('behaviors'));
219
        }
220
    }
221
222
    public function __toString()
223
    {
224
        return (string) $this->getId();
225
    }
226
227
    public function __call($name, $arguments)
228
    {
229
        // behaviors
230
        foreach ($this->behaviors as $behavior) {
231
            if (!method_exists($behavior, $name)) {
232
                continue;
233
            }
234
235
            return call_user_func_array(array($behavior, $name), $arguments);
236
        }
237
238
        // adding event
239
        if('on' === substr($name, 0, 2)) {
240
            // prepend event name to function args
241
            $addListenerArguments = $arguments;
242
            array_unshift($addListenerArguments, lcfirst(substr($name, 2)));
243
            // add listener
244
            call_user_func_array(
245
                array($this->eventDispatcher, 'addListener'),
246
                $addListenerArguments
247
            );
248
            
249
            return $this;
250
        }
251
252
        // getter
253
        if ('get' === strtolower(substr($name, 0, 3))) {
254
            return $this->get(lcfirst(substr($name, 3)));
255
        }
256
257
        // setter
258
        if ('set' === strtolower(substr($name, 0, 3)) && isset($arguments[0])) {
259
            return $this->set(lcfirst(substr($name, 3)), $arguments[0]);
260
        }
261
262
        throw new Exception('Document has no method "' . $name . '"');
263
    }
264
265
    public function __get($name)
266
    {
267
        if ($this->getRelationManager()->isRelationExists($name)) {
268
            // resolve relation
269
            return $this->getRelationManager()->getRelated($name);
270
        } else {
271
            // get document parameter
272
            return parent::__get($name);
273
        }
274
    }
275
276
    /**
277
     * Set geo data as GeoJson object
278
     *
279
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
280
     * to use Point, LineString and Polygon.
281
     *
282
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
283
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
284
     *
285
     * @link http://geojson.org/
286
     * @param string $field
287
     * @param \GeoJson\Geometry\Geometry $geometry
288
     * @return \Sokil\Mongo\Document
289
     */
290
    public function setGeometry($field, Geometry $geometry)
291
    {
292
        return $this->set($field, $geometry);
293
    }
294
295
    /**
296
     * Set point as longitude and latitude
297
     *
298
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
299
     * to use Point, LineString and Polygon.
300
     *
301
     * @link http://docs.mongodb.org/manual/core/2dsphere/#point
302
     * @param string $field
303
     * @param float $longitude
304
     * @param float $latitude
305
     * @return \Sokil\Mongo\Document
306
     */
307
    public function setPoint($field, $longitude, $latitude)
308
    {
309
        return $this->setGeometry(
310
            $field,
311
            new \GeoJson\Geometry\Point(array(
312
                $longitude,
313
                $latitude
314
            ))
315
        );
316
    }
317
318
    /**
319
     * Set point as longitude and latitude in legacy format
320
     *
321
     * May be used 2d index
322
     *
323
     * @link http://docs.mongodb.org/manual/core/2d/#geospatial-indexes-store-grid-coordinates
324
     * @param string $field
325
     * @param float $longitude
326
     * @param float $latitude
327
     * @return \Sokil\Mongo\Document
328
     */
329
    public function setLegacyPoint($field, $longitude, $latitude)
330
    {
331
        return $this->set(
332
            $field,
333
            array($longitude, $latitude)
334
        );
335
    }
336
337
    /**
338
     * Set line string as array of points
339
     *
340
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
341
     * to use Point, LineString and Polygon.
342
     *
343
     * @link http://docs.mongodb.org/manual/core/2dsphere/#linestring
344
     * @param string $field
345
     * @param array $pointArray array of points
346
     * @return \Sokil\Mongo\Document
347
     */
348
    public function setLineString($field, array $pointArray)
349
    {
350
        return $this->setGeometry(
351
            $field,
352
            new \GeoJson\Geometry\LineString($pointArray)
353
        );
354
    }
355
356
    /**
357
     * Set polygon as array of line rings.
358
     *
359
     * Line ring is closed line string (first and last point same).
360
     * Line string is array of points.
361
     *
362
     * Requires MongoDB version 2.4 or above with 2dsparse index version 1
363
     * to use Point, LineString and Polygon.
364
     *
365
     * @link http://docs.mongodb.org/manual/core/2dsphere/#polygon
366
     * @param string $field
367
     * @param array $lineRingsArray array of line rings
368
     * @return \Sokil\Mongo\Document
369
     */
370
    public function setPolygon($field, array $lineRingsArray)
371
    {
372
        return $this->setGeometry(
373
            $field,
374
            new \GeoJson\Geometry\Polygon($lineRingsArray)
375
        );
376
    }
377
378
    /**
379
     * Set multi point as array of points
380
     *
381
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
382
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
383
     *
384
     * @link http://docs.mongodb.org/manual/core/2dsphere/#multipoint
385
     * @param string $field
386
     * @param array $pointArray array of point arrays
387
     * @return \Sokil\Mongo\Document
388
     */
389
    public function setMultiPoint($field, $pointArray)
390
    {
391
        return $this->setGeometry(
392
            $field,
393
            new \GeoJson\Geometry\MultiPoint($pointArray)
394
        );
395
    }
396
397
    /**
398
     * Set multi line string as array of line strings
399
     *
400
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
401
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
402
     *
403
     * http://docs.mongodb.org/manual/core/2dsphere/#multilinestring
404
     * @param string $field
405
     * @param array $lineStringArray array of line strings
406
     * @return \Sokil\Mongo\Document
407
     */
408
    public function setMultiLineString($field, $lineStringArray)
409
    {
410
        return $this->setGeometry(
411
            $field,
412
            new \GeoJson\Geometry\MultiLineString($lineStringArray)
413
        );
414
    }
415
416
    /**
417
     * Set multy polygon as array of polygons.
418
     *
419
     * Polygon is array of line rings.
420
     * Line ring is closed line string (first and last point same).
421
     * Line string is array of points.
422
     *
423
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
424
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
425
     *
426
     * @link http://docs.mongodb.org/manual/core/2dsphere/#multipolygon
427
     * @param string $field
428
     * @param array $polygonsArray array of polygons
429
     * @return \Sokil\Mongo\Document
430
     */
431
    public function setMultyPolygon($field, array $polygonsArray)
432
    {
433
        return $this->setGeometry(
434
            $field,
435
            new \GeoJson\Geometry\MultiPolygon($polygonsArray)
436
        );
437
    }
438
439
    /**
440
     * Set collection of different geometries
441
     *
442
     * Requires MongoDB version 2.6 or above with 2dsparse index version 2
443
     * to use MultiPoint, MultiLineString, MultiPolygon and GeometryCollection.
444
     *
445
     * @link http://docs.mongodb.org/manual/core/2dsphere/#geometrycollection
446
     * @param string $field
447
     * @param array $geometryCollection
448
     * @return \Sokil\Mongo\Document
449
     */
450
    public function setGeometryCollection($field, array $geometryCollection)
451
    {
452
        return $this->setGeometry(
453
            $field,
454
            new \GeoJson\Geometry\GeometryCollection($geometryCollection)
455
        );
456
    }
457
458
    /**
459
     * Override in child class to define relations
460
     * @return array relation description
461
     */
462
    protected function relations()
463
    {
464
        // [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...
465
        return array();
466
    }
467
468
    /**
469
     * Relation definition through mapping is more prior to defined in class
470
     * @return array definition of relations
471
     */
472
    public function getRelationDefinition()
473
    {
474
        $relations = $this->getOption('relations');
475
        if(!is_array($relations)) {
476
            return $this->relations();
477
        }
478
479
        return $relations + $this->relations();
480
    }
481
482
    /**
483
     *
484
     * @return \Sokil\Mongo\Document\RelationManager
485
     */
486
    private function getRelationManager()
487
    {
488
        if($this->relationManager) {
489
            return $this->relationManager;
490
        }
491
492
        $this->relationManager = new RelationManager($this);
493
494
        return $this->relationManager;
495
    }
496
497
    /**
498
     * Get related documents
499
     * @param string $relationName
500
     * @return array|\Sokil\Mongo\Document related document or array of documents
501
     */
502
    public function getRelated($relationName)
503
    {
504
        return $this->getRelationManager()->getRelated($relationName);
505
    }
506
507
    public function addRelation($relationName, Document $document)
508
    {
509
        $this->getRelationManager()->addRelation($relationName, $document);
510
511
        return $this;
512
    }
513
514
    public function removeRelation($relationName, Document $document = null)
515
    {
516
        $this->getRelationManager()->removeRelation($relationName, $document);
517
518
        return $this;
519
    }
520
521
    /**
522
     * Manually trigger defined events
523
     * @param string $eventName event name
524
     * @return \Sokil\Mongo\Event
525
     */
526
    public function triggerEvent($eventName, Event $event = null)
527
    {
528
        if(!$event) {
529
            $event = new Event;
530
        }
531
532
        $event->setTarget($this);
533
534
        return $this->eventDispatcher->dispatch($eventName, $event);
535
    }
536
537
    /**
538
     * Attach event handler
539
     * @param string $event event name
540
     * @param callable|array|string $handler event handler
541
     * @return \Sokil\Mongo\Document
542
     */
543
    public function attachEvent($event, $handler, $priority = 0)
544
    {
545
        $this->eventDispatcher->addListener($event, $handler, $priority);
546
        return $this;
547
    }
548
549
    /**
550
     * Check if event attached
551
     *
552
     * @param string $event event name
553
     * @return bool
554
     */
555
    public function hasEvent($event)
556
    {
557
        return $this->eventDispatcher->hasListeners($event);
558
    }
559
560
    public function getId()
561
    {
562
        return $this->get('_id');
563
    }
564
565
    /**
566
     * Used to define id of stored document. This id must be already present in db
567
     *
568
     * @param \MongoId|string $id id of document
569
     * @return Document
570
     */
571 View Code Duplication
    public function defineId($id)
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...
572
    {
573
        if (!($id instanceof \MongoId)) {
574
            try {
575
                $id = new \MongoId($id);
576
            } catch (\MongoException $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
577
        }
578
579
        $this->mergeUnmodified(array('_id' => $id));
580
581
        return $this;
582
    }
583
584
    /**
585
     * Used to define id of not stored document.
586
     *
587
     * @param \MongoId|string $id id of document
588
     * @return Document
589
     */
590 View Code Duplication
    public function setId($id)
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...
591
    {
592
        if (!($id instanceof \MongoId)) {
593
            try {
594
                $id = new \MongoId($id);
595
            } catch (\MongoException $e) {}
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
596
        }
597
598
        return $this->set('_id', $id);
599
    }
600
601
    public function isStored()
602
    {
603
        return $this->get('_id') && !$this->isModified('_id');
604
    }
605
606
    /**
607
     *
608
     * @throws \Sokil\Mongo\Document\InvalidDocumentException
609
     * @return \Sokil\Mongo\Document
610
     */
611
    public function validate()
612
    {
613
        if($this->triggerEvent('beforeValidate')->isCancelled()) {
614
            return $this;
615
        }
616
617
        if (!$this->isValid()) {
618
            $exception = new InvalidDocumentException('Document not valid');
619
            $exception->setDocument($this);
620
621
            $this->triggerEvent('validateError');
622
623
            throw $exception;
624
        }
625
626
        $this->triggerEvent('afterValidate');
627
628
        return $this;
629
    }
630
631
    public function behaviors()
632
    {
633
        return array();
634
    }
635
636
    public function attachBehaviors(array $behaviors)
637
    {
638
        foreach ($behaviors as $name => $behavior) {
639
            $this->attachBehavior($name, $behavior);
640
        }
641
642
        return $this;
643
    }
644
645
    /**
646
     *
647
     * @param string $name unique name of attached behavior
648
     * @param string|array|\Sokil\Mongo\Behavior $behavior Behavior instance or behavior definition
649
     * @return \Sokil\Mongo\Document
650
     * @throws Exception
651
     */
652
    public function attachBehavior($name, $behavior)
653
    {
654
        if(is_string($behavior)) {
655
            // behavior defined as string
656
            $className = $behavior;
657
            $behavior = new $className();
658
        } elseif(is_array($behavior)) {
659
            // behavior defined as array
660
            if (empty($behavior['class'])) {
661
                throw new Exception('Behavior class not specified');
662
            }
663
            $className = $behavior['class'];
664
            unset($behavior['class']);
665
            $behavior = new $className($behavior);
666
        } elseif (!($behavior instanceof Behavior)) {
667
            // behavior bust be Behavior instance, but something else found
668
            throw new Exception('Wrong behavior specified with name ' . $name);
669
        }
670
671
        $behavior->setOwner($this);
672
673
        $this->behaviors[$name] = $behavior;
674
675
        return $this;
676
    }
677
678
    public function clearBehaviors()
679
    {
680
        $this->behaviors = array();
681
        return $this;
682
    }
683
684
    public function getOperator()
685
    {
686
        return $this->operator;
687
    }
688
689
    public function isModificationOperatorDefined()
690
    {
691
        return $this->operator->isDefined();
692
    }
693
694
    /**
695
     * Update value in local cache and in DB
696
     *
697
     * @param string $fieldName point-delimited field name
698
     * @param mixed $value value to store
699
     * @return \Sokil\Mongo\Document
700
     */
701
    public function set($fieldName, $value)
702
    {
703
        parent::set($fieldName, $value);
704
705
        // if document saved - save through update
706
        if ($this->getId()) {
707
            $this->operator->set($fieldName, $value);
708
        }
709
710
        return $this;
711
    }
712
713
    /**
714
     * Remove field
715
     * 
716
     * @param string $fieldName field name
717
     * @return \Sokil\Mongo\Document
718
     */
719
    public function unsetField($fieldName)
720
    {
721
        if (!$this->has($fieldName)) {
722
            return $this;
723
        }
724
725
        parent::unsetField($fieldName);
726
727
        if ($this->getId()) {
728
            $this->operator->unsetField($fieldName);
729
        }
730
731
        return $this;
732
    }
733
734
    public function __unset($fieldName)
735
    {
736
        $this->unsetField($fieldName);
737
    }
738
739
    /**
740
     * Get reference to document
741
     *
742
     * @throws Exception
743
     * @return array
744
     */
745
    public function createReference()
746
    {
747
        $documentId = $this->getId();
748
        if (null === $documentId) {
749
            throw new Exception('Document must be stored to get DBRef');
750
        }
751
752
        return $this
753
            ->getCollection()
754
            ->getMongoCollection()
755
            ->createDBRef($documentId);
756
    }
757
758
    /**
759
     * Store DBRef to specified field
760
     *
761
     * @param $name
762
     * @param Document $document
763
     * @return Document
764
     */
765
    public function setReference($name, Document $document)
766
    {
767
        return $this->set(
768
            $name,
769
            $document->createReference()
770
        );
771
    }
772
773
    /**
774
     * Get document by reference
775
     *
776
     * @param string    $name   name of field where reference stored
777
     * @return null|Document
778
     */
779
    public function getReferencedDocument($name)
780
    {
781
        $reference = $this->get($name);
782
        if (null === $reference) {
783
            return null;
784
        }
785
786
        return $this->collection
787
            ->getDatabase()
788
            ->getDocumentByReference($reference);
789
    }
790
791
    /**
792
     * Push reference to list
793
     *
794
     * @param string $name
795
     * @param Document $document
796
     * @return Document
797
     */
798
    public function pushReference($name, Document $document)
799
    {
800
        return $this->push(
801
            $name,
802
            $document->createReference()
803
        );
804
    }
805
806
    /**
807
     * Get document by reference
808
     *
809
     * @param string    $name   name of field where reference stored
810
     * @return null|Document
811
     */
812
    public function getReferencedDocumentList($name)
813
    {
814
        $referenceList = $this->get($name);
815
        if (null === $referenceList) {
816
            return null;
817
        }
818
819
        if (!isset($referenceList[0])) {
820
            throw new Exception('List of references not found');
821
        }
822
823
        // build list of referenced collections and ids
824
        $documentIdList = array();
825
        foreach ($referenceList as $reference) {
826
            if (empty($reference['$ref']) || empty($reference['$id'])) {
827
                throw new Exception(sprintf(
828
                    'Iinvalid reference in list for document %s in field %s',
829
                    $this->getId(),
830
                    $name
831
                ));
832
            }
833
834
            $documentIdList[$reference['$ref']][] = $reference['$id'];
835
        }
836
837
        // get list
838
        $documentList = array();
839
        $database = $this->collection->getDatabase();
840
        foreach ($documentIdList as $collectionName => $documentIdList) {
841
            $documentList += $database->getCollection($collectionName)->find()->byIdList($documentIdList)->findAll();
0 ignored issues
show
Bug introduced by
The method byIdList does only exist in Sokil\Mongo\Cursor, but not in Sokil\Mongo\Expression.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
842
        }
843
844
        return $documentList;
845
    }
846
847
    /**
848
     * @param array $data
849
     * @return Document
850
     */
851
    public function merge(array $data)
852
    {
853
        if ($this->isStored()) {
854
            foreach ($data as $fieldName => $value) {
855
                $this->set($fieldName, $value);
856
            }
857
        } else {
858
            parent::merge($data);
859
        }
860
861
        return $this;
862
    }
863
864
    /**
865
     * If field not exist - set value.
866
     * If field exists and is not array - convert to array and append
867
     * If field -s array - append
868
     *
869
     * @param type $selector
870
     * @param type $value
871
     * @return \Sokil\Mongo\Structure
872
     */
873
    public function append($selector, $value)
874
    {
875
        parent::append($selector, $value);
876
877
        // if document saved - save through update
878
        if ($this->getId()) {
879
            $this->operator->set($selector, $this->get($selector));
880
        }
881
882
        return $this;
883
    }
884
885
    /**
886
     * Push argument as single element to field value
887
     *
888
     * @param string $fieldName
889
     * @param mixed $value
890
     * @return \Sokil\Mongo\Document
891
      */
892
    public function push($fieldName, $value)
893
    {
894
        $oldValue = $this->get($fieldName);
895
896
        // check if old value is list or sub document
897
        // on sub document throw exception
898
        if (is_array($oldValue)) {
899
            $isSubDocument = (array_keys($oldValue) !== range(0, count($oldValue) - 1));
900
            if ($isSubDocument) {
901
                throw new InvalidOperationException(sprintf('The field "%s" must be an array but is of type Object', $fieldName));
902
            }
903
        }
904
905
        // prepare new value
906
        $value = Structure::prepareToStore($value);
907
908
        // field not exists
909
        if (!$oldValue) {
910
            if ($this->getId()) {
911
                $this->operator->push($fieldName, $value);
912
            }
913
            $value = array($value);
914
        } // field already exist and has single value
915 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...
916
            $value = array_merge((array) $oldValue, array($value));
917
            if ($this->getId()) {
918
                $this->operator->set($fieldName, $value);
919
            }
920
        } // field exists and is array
921
        else {
922
            if ($this->getId()) {
923
                $setValue = $this->operator->get('$set', $fieldName);
924
                if ($setValue) {
925
                    $setValue[] = $value;
926
                    $this->operator->set($fieldName, $setValue);
927
                } else {
928
                    $this->operator->push($fieldName, $value);
929
                }
930
            }
931
            $value = array_merge($oldValue, array($value));
932
        }
933
934
        // update local data
935
        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...
936
937
        return $this;
938
    }
939
940
    /**
941
     * Push each element of argument's array as single element to field value
942
     *
943
     * @param string $fieldName
944
     * @param array $values
945
     * @return \Sokil\Mongo\Document
946
     */
947
    public function pushEach($fieldName, array $values)
948
    {
949
        $oldValue = $this->get($fieldName);
950
951
        if ($this->getId()) {
952
            if (!$oldValue) {
953
                $this->operator->pushEach($fieldName, $values);
954 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...
955
                $values = array_merge((array) $oldValue, $values);
956
                $this->operator->set($fieldName, $values);
957
            } else {
958
                $this->operator->pushEach($fieldName, $values);
959
                $values = array_merge($oldValue, $values);
960
            }
961
        } else {
962
            if ($oldValue) {
963
                $values = array_merge((array) $oldValue, $values);
964
            }
965
        }
966
967
        // update local data
968
        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...
969
970
        return $this;
971
    }
972
973
    public function addToSet($fieldName, $value)
974
    {
975
        $set = $this->get($fieldName);
976
977
        // prepare new value
978
        $value = Structure::prepareToStore($value);
979
980
        // add to set
981
        if (empty($set)) {
982
            $updatedSet = array($value);
983
            if ($this->getId()) {
984
                $this->operator->addToSet($fieldName, $value);
985
            }
986
        } elseif (!is_array($set)) {
987
            if ($set === $value) {
988
                return $this;
989
            }
990
            $updatedSet = array($set, $value);
991
            if ($this->getId()) {
992
                $this->operator->set($fieldName, $updatedSet);
993
            }
994
        } elseif (array_keys($set) !== range(0, count($set) - 1)) {
995
            // check if old value is list or sub document
996
            // on sub document throw exception
997
            throw new InvalidOperationException(sprintf('The field "%s" must be an array but is of type Object', $fieldName));
998
        } else {
999
            // check if already in set
1000
            if (in_array($value, $set, true)) {
1001
                return $this;
1002
            }
1003
            $updatedSet = array_merge($set, array($value));
1004
            if ($this->getId()) {
1005
                $setValue = $this->operator->get('$set', $fieldName);
1006
                if ($setValue) {
1007
                    $this->operator->set($fieldName, $updatedSet);
1008
                } else {
1009
                    $this->operator->addToSet($fieldName, $value);
1010
                }
1011
            }
1012
        }
1013
1014
        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...
1015
1016
        return $this;
1017
    }
1018
1019
    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...
1020
    {
1021
        throw new \RuntimeException('Not implemented');
1022
    }
1023
1024
    /**
1025
     * Removes from an existing array all instances of a value or
1026
     * values that match a specified query
1027
     *
1028
     * @param integer|string|array|\Sokil\Mongo\Expression|callable $expression
1029
     * @param mixed|\Sokil\Mongo\Expression|callable $value
1030
     * @return \Sokil\Mongo\Document
1031
     */
1032
    public function pull($expression, $value = null)
1033
    {
1034
        $this->operator->pull($expression, $value);
1035
        return $this;
1036
    }
1037
1038
    public function increment($fieldName, $value = 1)
1039
    {
1040
        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...
1041
1042
        if ($this->getId()) {
1043
            $this->operator->increment($fieldName, $value);
1044
        }
1045
1046
1047
        return $this;
1048
    }
1049
1050
    public function decrement($fieldName, $value = 1)
1051
    {
1052
        return $this->increment($fieldName, -1 * $value);
1053
    }
1054
1055 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...
1056
    {
1057
        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...
1058
1059
        if ($this->getId()) {
1060
            $this->operator->bitwiceAnd($field, $value);
1061
        }
1062
1063
        return $this;
1064
    }
1065
1066 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...
1067
    {
1068
        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...
1069
1070
        if ($this->getId()) {
1071
            $this->operator->bitwiceOr($field, $value);
1072
        }
1073
1074
        return $this;
1075
    }
1076
1077
    public function bitwiceXor($field, $value)
1078
    {
1079
        $oldFieldValue = (int) $this->get($field);
1080
        $newValue = $oldFieldValue ^ $value;
1081
1082
        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...
1083
1084
        if ($this->getId()) {
1085
            if(version_compare($this->getCollection()->getDatabase()->getClient()->getDbVersion(), '2.6', '>=')) {
1086
                $this->operator->bitwiceXor($field, $value);
1087
            } else {
1088
                $this->operator->set($field, $newValue);
1089
            }
1090
        }
1091
1092
        return $this;
1093
    }
1094
1095
    /**
1096
     * Internal method to insert document
1097
     */
1098
    private function internalInsert()
1099
    {
1100
        if ($this->triggerEvent('beforeInsert')->isCancelled()) {
1101
            return;
1102
        }
1103
1104
        $document = $this->toArray();
1105
1106
        // try write document
1107
        try {
1108
            $this
1109
                ->collection
1110
                ->getMongoCollection()
1111
                ->insert($document);
1112
        } catch (\Exception $e) {
1113
            throw new WriteException(
1114
                $e->getMessage(),
1115
                $e->getCode(),
1116
                $e
1117
            );
1118
        }
1119
1120
        // set id
1121
        $this->defineId($document['_id']);
1122
1123
        // after insert event
1124
        $this->triggerEvent('afterInsert');
1125
    }
1126
1127
    /**
1128
     * Internal method to update document
1129
     *
1130
     * @throws WriteException
1131
     * @throws OptimisticLockFailureException
1132
     */
1133
    private function internalUpdate()
1134
    {
1135
        if ($this->triggerEvent('beforeUpdate')->isCancelled()) {
1136
            return;
1137
        }
1138
1139
        // locking
1140
        $query = array('_id' => $this->getId());
1141
        if ($this->getOption('lock') === Definition::LOCK_OPTIMISTIC) {
1142
            $query['__version__'] = $this->get('__version__');
1143
            $this->getOperator()->increment('__version__');
1144
        }
1145
1146
        // update
1147
        try {
1148
            $status = $this
1149
                ->collection
1150
                ->getMongoCollection()
1151
                ->update(
1152
                    $query,
1153
                    $this->getOperator()->toArray()
1154
                );
1155
        } catch (\Exception $e) {
1156
            throw new WriteException(
1157
                $e->getMessage(),
1158
                $e->getCode(),
1159
                $e
1160
            );
1161
        }
1162
1163
        // check update status
1164
        if ($status['ok'] != 1) {
1165
            throw new WriteException(
1166
                sprintf(
1167
                    'Update error: %s: %s',
1168
                    $status['err'],
1169
                    $status['errmsg']
1170
                )
1171
            );
1172
        }
1173
1174
        // check if document modified due to specified lock
1175
        if ($this->getOption('lock') === Definition::LOCK_OPTIMISTIC) {
1176
            if($status['n'] === 0) {
1177
                throw new OptimisticLockFailureException;
1178
            }
1179
        }
1180
1181
        if ($this->getOperator()->isReloadRequired()) {
1182
            $this->refresh();
1183
        } else {
1184
            $this->getOperator()->reset();
1185
        }
1186
1187
        $this->triggerEvent('afterUpdate');
1188
    }
1189
1190
    /**
1191
     * Save document
1192
     *
1193
     * @param bool $validate
1194
     * @return Document
1195
     * @throws WriteException
1196
     */
1197
    public function save($validate = true)
1198
    {
1199
        // if document already in db and not modified - skip this method
1200
        if (!$this->isSaveRequired()) {
1201
            return $this;
1202
        }
1203
1204
        if ($validate) {
1205
            $this->validate();
1206
        }
1207
1208
        // handle beforeSave event
1209
        if($this->triggerEvent('beforeSave')->isCancelled()) {
1210
            return $this;
1211
        }
1212
1213
        // write document
1214
        if ($this->isStored()) {
1215
            $this->internalUpdate();
1216
        } else {
1217
            $this->internalInsert();
1218
        }
1219
1220
        // handle afterSave event
1221
        $this->triggerEvent('afterSave');
1222
1223
        // set document unmodified
1224
        $this->apply();
1225
        
1226
        return $this;
1227
    }
1228
1229
    /**
1230
     * Check if document require save
1231
     *
1232
     * @return bool
1233
     */
1234
    public function isSaveRequired()
1235
    {
1236
        return !$this->isStored() || $this->isModified() || $this->isModificationOperatorDefined();
1237
    }
1238
1239
    /**
1240
     * Delete document
1241
     *
1242
     * @return Document
1243
     * @throws Exception
1244
     */
1245
    public function delete()
1246
    {
1247
        if ($this->triggerEvent('beforeDelete')->isCancelled()) {
1248
            return $this;
1249
        }
1250
1251
        $status = $this->collection->getMongoCollection()->remove(array(
1252
            '_id'   => $this->getId(),
1253
        ));
1254
1255
        if(true !== $status && $status['ok'] != 1) {
1256
            throw new \Sokil\Mongo\Exception(sprintf('Delete document error: %s', $status['err']));
1257
        }
1258
1259
        $this->triggerEvent('afterDelete');
1260
1261
        // drop from document's pool
1262
        $this->getCollection()->removeDocumentFromDocumentPool($this);
1263
1264
        return $this;
1265
    }
1266
1267
    /**
1268
     * Get revisions manager
1269
     *
1270
     * @return \Sokil\Mongo\Document\RevisionManager
1271
     */
1272
    public function getRevisionManager()
1273
    {
1274
        if(!$this->revisionManager) {
1275
            $this->revisionManager = new RevisionManager($this);
1276
        }
1277
1278
        return $this->revisionManager;
1279
    }
1280
1281
    /**
1282
     * @deprecated since 1.13.0 use self::getRevisionManager()->getRevisions()
1283
     */
1284
    public function getRevisions($limit = null, $offset = null)
1285
    {
1286
        return $this->getRevisionManager()->getRevisions($limit, $offset);
1287
    }
1288
1289
    /**
1290
     * @deprecated since 1.13.0 use self::getRevisionManager()->getRevision()
1291
     */
1292
    public function getRevision($id)
1293
    {
1294
        return $this->getRevisionManager()->getRevision($id);
1295
    }
1296
1297
    /**
1298
     * @deprecated since 1.13.0 use self::getRevisionManager()->getRevisionsCount()
1299
     */
1300
    public function getRevisionsCount()
1301
    {
1302
        return $this->getRevisionManager()->getRevisionsCount();
1303
    }
1304
1305
    /**
1306
     * @deprecated since 1.13.0 use self::getRevisionManager()->clearRevisions()
1307
     */
1308
    public function clearRevisions()
1309
    {
1310
        $this->getRevisionManager()->clearRevisions();
1311
        return $this;
1312
    }
1313
1314
}
1315