Completed
Push — master ( 9aae6d...24b12a )
by Andreas
24s queued 10s
created

needsSchedulingForSynchronization()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 4
nc 4
nop 0
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\ODM\MongoDB\PersistentCollection;
6
7
use Closure;
8
use Doctrine\Common\Collections\Collection as BaseCollection;
9
use Doctrine\ODM\MongoDB\DocumentManager;
10
use Doctrine\ODM\MongoDB\MongoDBException;
11
use Doctrine\ODM\MongoDB\UnitOfWork;
12
use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
13
use function array_udiff;
14
use function array_udiff_assoc;
15
use function array_values;
16
use function count;
17
use function get_class;
18
use function is_object;
19
use function spl_object_hash;
20
21
/**
22
 * Trait with methods needed to implement PersistentCollectionInterface.
23
 */
24
trait PersistentCollectionTrait
25
{
26
    /**
27
     * A snapshot of the collection at the moment it was fetched from the database.
28
     * This is used to create a diff of the collection at commit time.
29
     *
30
     * @var array
31
     */
32
    private $snapshot = [];
33
34
    /**
35
     * Collection's owning entity
36
     *
37
     * @var object|null
38
     */
39
    private $owner;
40
41
    /** @var array */
42
    private $mapping;
43
44
    /**
45
     * Whether the collection is dirty and needs to be synchronized with the database
46
     * when the UnitOfWork that manages its persistent state commits.
47
     *
48
     * @var bool
49
     */
50
    private $isDirty = false;
51
52
    /**
53
     * Whether the collection has already been initialized.
54
     *
55
     * @var bool
56
     */
57
    private $initialized = true;
58
59
    /**
60
     * The wrapped Collection instance.
61
     *
62
     * @var BaseCollection
63
     */
64
    private $coll;
65
66
    /**
67
     * The DocumentManager that manages the persistence of the collection.
68
     *
69
     * @var DocumentManager|null
70
     */
71
    private $dm;
72
73
    /**
74
     * The UnitOfWork that manages the persistence of the collection.
75
     *
76
     * @var UnitOfWork
77
     */
78
    private $uow;
79
80
    /**
81
     * The raw mongo data that will be used to initialize this collection.
82
     *
83
     * @var array
84
     */
85
    private $mongoData = [];
86
87
    /**
88
     * Any hints to account for during reconstitution/lookup of the documents.
89
     *
90
     * @var array
91
     */
92
    private $hints = [];
93
94
    /** {@inheritdoc} */
95 4
    public function setDocumentManager(DocumentManager $dm)
96
    {
97 4
        $this->dm  = $dm;
98 4
        $this->uow = $dm->getUnitOfWork();
99 4
    }
100
101
    /** {@inheritdoc} */
102 166
    public function setMongoData(array $mongoData)
103
    {
104 166
        $this->mongoData = $mongoData;
105 166
    }
106
107
    /** {@inheritdoc} */
108 164
    public function getMongoData()
109
    {
110 164
        return $this->mongoData;
111
    }
112
113
    /** {@inheritdoc} */
114 247
    public function setHints(array $hints)
115
    {
116 247
        $this->hints = $hints;
117 247
    }
118
119
    /** {@inheritdoc} */
120 164
    public function getHints()
121
    {
122 164
        return $this->hints;
123
    }
124
125
    /** {@inheritdoc} */
126 401
    public function initialize()
127
    {
128 401
        if ($this->initialized || ! $this->mapping) {
129 393
            return;
130
        }
131
132 178
        $newObjects = [];
133
134 178
        if ($this->isDirty) {
135
            // Remember any NEW objects added through add()
136 17
            $newObjects = $this->coll->toArray();
137
        }
138
139 178
        $this->initialized = true;
140
141 178
        $this->coll->clear();
142 178
        $this->uow->loadCollection($this);
143 178
        $this->takeSnapshot();
144
145 178
        $this->mongoData = [];
146
147
        // Reattach any NEW objects added through add()
148 178
        if (! $newObjects) {
149 174
            return;
150
        }
151
152 17
        foreach ($newObjects as $key => $obj) {
153 17
            if (CollectionHelper::isHash($this->mapping['strategy'])) {
154
                $this->coll->set($key, $obj);
155
            } else {
156 17
                $this->coll->add($obj);
157
            }
158
        }
159
160 17
        $this->isDirty = true;
161 17
    }
162
163
    /**
164
     * Marks this collection as changed/dirty.
165
     */
166 211
    private function changed()
167
    {
168 211
        if ($this->isDirty) {
169 133
            return;
170
        }
171
172 211
        $this->isDirty = true;
173
174 211
        if (! $this->needsSchedulingForSynchronization() || $this->owner === null) {
175 209
            return;
176
        }
177
178 2
        $this->uow->scheduleForSynchronization($this->owner);
179 2
    }
180
181
    /** {@inheritdoc} */
182 412
    public function isDirty()
183
    {
184 412
        if ($this->isDirty) {
185 276
            return true;
186
        }
187 362
        if (! $this->initialized && count($this->coll)) {
188
            // not initialized collection with added elements
189
            return true;
190
        }
191 362
        if ($this->initialized) {
192
            // if initialized let's check with last known snapshot
193 356
            return $this->coll->toArray() !== $this->snapshot;
194
        }
195 95
        return false;
196
    }
197
198
    /** {@inheritdoc} */
199 400
    public function setDirty($dirty)
200
    {
201 400
        $this->isDirty = $dirty;
202 400
    }
203
204
    /** {@inheritdoc} */
205 423
    public function setOwner(object $document, array $mapping)
206
    {
207 423
        $this->owner   = $document;
208 423
        $this->mapping = $mapping;
209 423
    }
210
211
    /** {@inheritdoc} */
212 296
    public function takeSnapshot()
213
    {
214 296
        if (CollectionHelper::isList($this->mapping['strategy'])) {
215 282
            $array = $this->coll->toArray();
216 282
            $this->coll->clear();
217 282
            foreach ($array as $document) {
218 257
                $this->coll->add($document);
219
            }
220
        }
221 296
        $this->snapshot = $this->coll->toArray();
222 296
        $this->isDirty  = false;
223 296
    }
224
225
    /** {@inheritdoc} */
226 29
    public function clearSnapshot()
227
    {
228 29
        $this->snapshot = [];
229 29
        $this->isDirty  = $this->coll->count() ? true : false;
230 29
    }
231
232
    /** {@inheritdoc} */
233 1
    public function getSnapshot()
234
    {
235 1
        return $this->snapshot;
236
    }
237
238
    /** {@inheritdoc} */
239 95
    public function getDeleteDiff()
240
    {
241 95
        return array_udiff_assoc(
242 95
            $this->snapshot,
243 95
            $this->coll->toArray(),
244
            static function ($a, $b) {
245 48
                return $a === $b ? 0 : 1;
246 95
            }
247
        );
248
    }
249
250
    /** {@inheritdoc} */
251 157
    public function getDeletedDocuments()
252
    {
253
        $compare = static function ($a, $b) {
254 109
            $compareA = is_object($a) ? spl_object_hash($a) : $a;
255 109
            $compareb = is_object($b) ? spl_object_hash($b) : $b;
256 109
            return $compareA === $compareb ? 0 : ($compareA > $compareb ? 1 : -1);
257 157
        };
258 157
        return array_values(array_udiff(
259 157
            $this->snapshot,
260 157
            $this->coll->toArray(),
261 157
            $compare
262
        ));
263
    }
264
265
    /** {@inheritdoc} */
266 95
    public function getInsertDiff()
267
    {
268 95
        return array_udiff_assoc(
269 95
            $this->coll->toArray(),
270 95
            $this->snapshot,
271
            static function ($a, $b) {
272 48
                return $a === $b ? 0 : 1;
273 95
            }
274
        );
275
    }
276
277
    /** {@inheritdoc} */
278 4
    public function getInsertedDocuments()
279
    {
280
        $compare = static function ($a, $b) {
281 4
            $compareA = is_object($a) ? spl_object_hash($a) : $a;
282 4
            $compareb = is_object($b) ? spl_object_hash($b) : $b;
283 4
            return $compareA === $compareb ? 0 : ($compareA > $compareb ? 1 : -1);
284 4
        };
285 4
        return array_values(array_udiff(
286 4
            $this->coll->toArray(),
287 4
            $this->snapshot,
288 4
            $compare
289
        ));
290
    }
291
292
    /** {@inheritdoc} */
293 410
    public function getOwner() : ?object
294
    {
295 410
        return $this->owner;
296
    }
297
298
    /** {@inheritdoc} */
299 291
    public function getMapping()
300
    {
301 291
        return $this->mapping;
302
    }
303
304
    /** {@inheritdoc} */
305 5
    public function getTypeClass()
306
    {
307 5
        if ($this->dm === null) {
308 1
            throw new MongoDBException('No DocumentManager is associated with this PersistentCollection, please set one using setDocumentManager method.');
309
        }
310
311 4
        if (empty($this->mapping)) {
312 1
            throw new MongoDBException('No mapping is associated with this PersistentCollection, please set one using setOwner method.');
313
        }
314
315 3
        if (empty($this->mapping['targetDocument'])) {
316 1
            throw new MongoDBException('Specifying targetDocument is required for the ClassMetadata to be obtained.');
317
        }
318
319 2
        return $this->dm->getClassMetadata($this->mapping['targetDocument']);
320
    }
321
322
    /** {@inheritdoc} */
323 250
    public function setInitialized($bool)
324
    {
325 250
        $this->initialized = $bool;
326 250
    }
327
328
    /** {@inheritdoc} */
329 18
    public function isInitialized()
330
    {
331 18
        return $this->initialized;
332
    }
333
334
    /** {@inheritdoc} */
335 16
    public function first()
336
    {
337 16
        $this->initialize();
338 16
        return $this->coll->first();
339
    }
340
341
    /** {@inheritdoc} */
342 2
    public function last()
343
    {
344 2
        $this->initialize();
345 2
        return $this->coll->last();
346
    }
347
348
    /**
349
     * {@inheritdoc}
350
     */
351 6
    public function remove($key)
352
    {
353 6
        return $this->doRemove($key, false);
354
    }
355
356
    /**
357
     * {@inheritdoc}
358
     */
359 17
    public function removeElement($element)
360
    {
361 17
        $this->initialize();
362 17
        $removed = $this->coll->removeElement($element);
363
364 17
        if (! $removed) {
365
            return $removed;
366
        }
367
368 17
        $this->changed();
369
370 17
        return $removed;
371
    }
372
373
    /**
374
     * {@inheritdoc}
375
     */
376
    public function containsKey($key)
377
    {
378
        $this->initialize();
379
        return $this->coll->containsKey($key);
380
    }
381
382
    /**
383
     * {@inheritdoc}
384
     */
385 3
    public function contains($element)
386
    {
387 3
        $this->initialize();
388 3
        return $this->coll->contains($element);
389
    }
390
391
    /**
392
     * {@inheritdoc}
393
     */
394
    public function exists(Closure $p)
395
    {
396
        $this->initialize();
397
        return $this->coll->exists($p);
398
    }
399
400
    /**
401
     * {@inheritdoc}
402
     */
403 2
    public function indexOf($element)
404
    {
405 2
        $this->initialize();
406 2
        return $this->coll->indexOf($element);
407
    }
408
409
    /**
410
     * {@inheritdoc}
411
     */
412 20
    public function get($key)
413
    {
414 20
        $this->initialize();
415 20
        return $this->coll->get($key);
416
    }
417
418
    /**
419
     * {@inheritdoc}
420
     */
421
    public function getKeys()
422
    {
423
        $this->initialize();
424
        return $this->coll->getKeys();
425
    }
426
427
    /**
428
     * {@inheritdoc}
429
     */
430
    public function getValues()
431
    {
432
        $this->initialize();
433
        return $this->coll->getValues();
434
    }
435
436
    /**
437
     * {@inheritdoc}
438
     */
439 116
    public function count()
440
    {
441
        // Workaround around not being able to directly count inverse collections anymore
442 116
        $this->initialize();
443
444 116
        return $this->coll->count();
445
    }
446
447
    /**
448
     * {@inheritdoc}
449
     */
450 35
    public function set($key, $value)
451
    {
452 35
        return $this->doSet($key, $value, false);
453
    }
454
455
    /**
456
     * {@inheritdoc}
457
     */
458 170
    public function add($value)
459
    {
460 170
        return $this->doAdd($value, false);
461
    }
462
463
    /**
464
     * {@inheritdoc}
465
     */
466 382
    public function isEmpty()
467
    {
468 382
        return $this->initialized ? $this->coll->isEmpty() : $this->count() === 0;
469
    }
470
471
    /**
472
     * {@inheritdoc}
473
     */
474 325
    public function getIterator()
475
    {
476 325
        $this->initialize();
477 325
        return $this->coll->getIterator();
478
    }
479
480
    /**
481
     * {@inheritdoc}
482
     */
483 232
    public function map(Closure $func)
484
    {
485 232
        $this->initialize();
486 232
        return $this->coll->map($func);
487
    }
488
489
    /**
490
     * {@inheritdoc}
491
     */
492
    public function filter(Closure $p)
493
    {
494
        $this->initialize();
495
        return $this->coll->filter($p);
496
    }
497
498
    /**
499
     * {@inheritdoc}
500
     */
501
    public function forAll(Closure $p)
502
    {
503
        $this->initialize();
504
        return $this->coll->forAll($p);
505
    }
506
507
    /**
508
     * {@inheritdoc}
509
     */
510
    public function partition(Closure $p)
511
    {
512
        $this->initialize();
513
        return $this->coll->partition($p);
514
    }
515
516
    /**
517
     * {@inheritdoc}
518
     */
519 23
    public function toArray()
520
    {
521 23
        $this->initialize();
522 23
        return $this->coll->toArray();
523
    }
524
525
    /**
526
     * {@inheritdoc}
527
     */
528 33
    public function clear()
529
    {
530 33
        if ($this->initialized && $this->isEmpty()) {
531
            return;
532
        }
533
534 33
        if ($this->isOrphanRemovalEnabled()) {
535 32
            $this->initialize();
536 32
            foreach ($this->coll as $element) {
537 32
                $this->uow->scheduleOrphanRemoval($element);
538
            }
539
        }
540
541 33
        $this->mongoData = [];
542 33
        $this->coll->clear();
543
544
        // Nothing to do for inverse-side collections
545 33
        if (! $this->mapping['isOwningSide']) {
546
            return;
547
        }
548
549
        // Nothing to do if the collection was initialized but contained no data
550 33
        if ($this->initialized && empty($this->snapshot)) {
551 2
            return;
552
        }
553
554 31
        $this->changed();
555 31
        $this->uow->scheduleCollectionDeletion($this);
556 31
        $this->takeSnapshot();
557 31
    }
558
559
    /**
560
     * {@inheritdoc}
561
     */
562 1
    public function slice($offset, $length = null)
563
    {
564 1
        $this->initialize();
565 1
        return $this->coll->slice($offset, $length);
566
    }
567
568
    /**
569
     * Called by PHP when this collection is serialized. Ensures that the
570
     * internal state of the collection can be reproduced after serialization
571
     *
572
     * @internal Tried to implement Serializable first but that did not work well
573
     *           with circular references. This solution seems simpler and works well.
574
     */
575 6
    public function __sleep()
576
    {
577 6
        return ['coll', 'initialized', 'mongoData', 'snapshot', 'isDirty', 'hints'];
578
    }
579
580
    /* ArrayAccess implementation */
581
582
    /**
583
     * @see containsKey()
584
     */
585 2
    public function offsetExists($offset)
586
    {
587 2
        $this->initialize();
588 2
        return $this->coll->offsetExists($offset);
589
    }
590
591
    /**
592
     * @see get()
593
     */
594 77
    public function offsetGet($offset)
595
    {
596 77
        $this->initialize();
597 77
        return $this->coll->offsetGet($offset);
598
    }
599
600
    /**
601
     * @see add()
602
     * @see set()
603
     */
604 42
    public function offsetSet($offset, $value)
605
    {
606 42
        if (! isset($offset)) {
607 41
            return $this->doAdd($value, true);
608
        }
609
610 3
        $this->doSet($offset, $value, true);
611 3
    }
612
613
    /**
614
     * @see remove()
615
     */
616 19
    public function offsetUnset($offset)
617
    {
618 19
        $this->doRemove($offset, true);
619 19
    }
620
621
    public function key()
622
    {
623
        return $this->coll->key();
624
    }
625
626
    /**
627
     * Gets the element of the collection at the current iterator position.
628
     */
629 1
    public function current()
630
    {
631 1
        return $this->coll->current();
632
    }
633
634
    /**
635
     * Moves the internal iterator position to the next element.
636
     */
637
    public function next()
638
    {
639
        return $this->coll->next();
640
    }
641
642
    /**
643
     * {@inheritdoc}
644
     */
645 411
    public function unwrap()
646
    {
647 411
        return $this->coll;
648
    }
649
650
    /**
651
     * Cleanup internal state of cloned persistent collection.
652
     *
653
     * The following problems have to be prevented:
654
     * 1. Added documents are added to old PersistentCollection
655
     * 2. New collection is not dirty, if reused on other document nothing
656
     * changes.
657
     * 3. Snapshot leads to invalid diffs being generated.
658
     * 4. Lazy loading grabs entities from old owner object.
659
     * 5. New collection is connected to old owner and leads to duplicate keys.
660
     */
661 8
    public function __clone()
662
    {
663 8
        if (is_object($this->coll)) {
664 8
            $this->coll = clone $this->coll;
665
        }
666
667 8
        $this->initialize();
668
669 8
        $this->owner    = null;
670 8
        $this->snapshot = [];
671
672 8
        $this->changed();
673 8
    }
674
675
    /**
676
     * Actual logic for adding an element to the collection.
677
     *
678
     * @param mixed $value
679
     * @param bool  $arrayAccess
680
     *
681
     * @return bool
682
     */
683 186
    private function doAdd($value, $arrayAccess)
684
    {
685
        /* Initialize the collection before calling add() so this append operation
686
         * uses the appropriate key. Otherwise, we risk overwriting original data
687
         * when $newObjects are re-added in a later call to initialize().
688
         */
689 186
        if (isset($this->mapping['strategy']) && CollectionHelper::isHash($this->mapping['strategy'])) {
690 15
            $this->initialize();
691
        }
692 186
        $arrayAccess ? $this->coll->offsetSet(null, $value) : $this->coll->add($value);
693 186
        $this->changed();
694
695 186
        if ($this->uow !== null && $this->isOrphanRemovalEnabled() && $value !== null) {
696 108
            $this->uow->unscheduleOrphanRemoval($value);
697
        }
698
699 186
        return true;
700
    }
701
702
    /**
703
     * Actual logic for removing element by its key.
704
     *
705
     * @param mixed $offset
706
     * @param bool  $arrayAccess
707
     *
708
     * @return bool
709
     */
710 25
    private function doRemove($offset, $arrayAccess)
711
    {
712 25
        $this->initialize();
713 25
        if ($arrayAccess) {
714 19
            $this->coll->offsetUnset($offset);
715 19
            $removed = true;
716
        } else {
717 6
            $removed = $this->coll->remove($offset);
718
        }
719
720 25
        if (! $removed && ! $arrayAccess) {
721
            return $removed;
722
        }
723
724 25
        $this->changed();
725
726 25
        return $removed;
727
    }
728
729
    /**
730
     * Actual logic for setting an element in the collection.
731
     *
732
     * @param mixed $offset
733
     * @param mixed $value
734
     * @param bool  $arrayAccess
735
     */
736 36
    private function doSet($offset, $value, $arrayAccess)
737
    {
738 36
        $arrayAccess ? $this->coll->offsetSet($offset, $value) : $this->coll->set($offset, $value);
739
740
        // Handle orphanRemoval
741 36
        if ($this->uow !== null && $this->isOrphanRemovalEnabled() && $value !== null) {
742 34
            $this->uow->unscheduleOrphanRemoval($value);
743
        }
744
745 36
        $this->changed();
746 36
    }
747
748
    /**
749
     * Returns whether or not this collection has orphan removal enabled.
750
     *
751
     * Embedded documents are automatically considered as "orphan removal enabled" because they might have references
752
     * that require to trigger cascade remove operations.
753
     *
754
     * @return bool
755
     */
756 204
    private function isOrphanRemovalEnabled()
757
    {
758 204
        if ($this->mapping === null) {
759 12
            return false;
760
        }
761
762 192
        if (isset($this->mapping['embedded'])) {
763 117
            return true;
764
        }
765
766 85
        if (isset($this->mapping['reference']) && $this->mapping['isOwningSide'] && $this->mapping['orphanRemoval']) {
767 9
            return true;
768
        }
769
770 76
        return false;
771
    }
772
773
    /**
774
     * Checks whether collection owner needs to be scheduled for dirty change in case the collection is modified.
775
     *
776
     * @return bool
777
     */
778 211
    private function needsSchedulingForSynchronization()
779
    {
780 211
        return $this->owner && $this->dm && ! empty($this->mapping['isOwningSide'])
781 211
            && $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify();
782
    }
783
}
784