PersistentCollectionTrait   F
last analyzed

Complexity

Total Complexity 120

Size/Duplication

Total Lines 776
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 88.35%

Importance

Changes 0
Metric Value
wmc 120
lcom 1
cbo 3
dl 0
loc 776
ccs 235
cts 266
cp 0.8835
rs 1.824
c 0
b 0
f 0

60 Methods

Rating   Name   Duplication   Size   Complexity  
A setDocumentManager() 0 5 1
A setMongoData() 0 4 1
A getMongoData() 0 4 1
A setHints() 0 4 1
A getHints() 0 4 1
B initialize() 0 36 7
A setDirty() 0 4 1
A setOwner() 0 5 1
A changed() 0 14 4
A isDirty() 0 16 5
A clearSnapshot() 0 5 1
A getSnapshot() 0 4 1
A getDeleteDiff() 0 10 2
A getDeletedDocuments() 0 15 5
A getInsertDiff() 0 10 2
A getInsertedDocuments() 0 15 5
A getOwner() 0 4 1
A getMapping() 0 4 1
A getTypeClass() 0 16 4
A setInitialized() 0 4 1
A isInitialized() 0 4 1
A first() 0 6 1
A last() 0 6 1
A remove() 0 4 1
A removeElement() 0 13 2
A containsKey() 0 6 1
A contains() 0 6 1
A exists() 0 6 1
A indexOf() 0 6 1
A get() 0 6 1
A getKeys() 0 6 1
A getValues() 0 6 1
A count() 0 7 1
A set() 0 4 1
A add() 0 4 1
A isEmpty() 0 4 2
A getIterator() 0 6 1
A map() 0 6 1
A filter() 0 6 1
A forAll() 0 6 1
A partition() 0 6 1
A toArray() 0 6 1
B clear() 0 30 8
A slice() 0 6 1
A takeSnapshot() 0 12 4
A __sleep() 0 4 1
A offsetExists() 0 6 1
A offsetGet() 0 6 1
A offsetSet() 0 8 2
A offsetUnset() 0 4 1
A key() 0 4 1
A current() 0 4 1
A next() 0 4 1
A unwrap() 0 4 1
A __clone() 0 13 2
B doAdd() 0 18 7
A doRemove() 0 18 4
A doSet() 0 11 5
A isOrphanRemovalEnabled() 0 12 5
A needsSchedulingForSynchronization() 0 5 4

How to fix   Complexity   

Complex Class

Complex classes like PersistentCollectionTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PersistentCollectionTrait, and based on these observations, apply Extract Interface, too.

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