Completed
Push — master ( 85c5ae...c0f1d0 )
by Andreas
21s queued 11s
created

PersistentCollectionTrait   F

Complexity

Total Complexity 119

Size/Duplication

Total Lines 779
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 88.35%

Importance

Changes 0
Metric Value
wmc 119
lcom 1
cbo 3
dl 0
loc 779
ccs 235
cts 266
cp 0.8835
rs 1.821
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 takeSnapshot() 0 12 3
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 __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 */
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 402
    public function initialize()
127
    {
128 402
        if ($this->initialized || ! $this->mapping) {
129 394
            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 210
    private function changed()
167
    {
168 210
        if ($this->isDirty) {
169 132
            return;
170
        }
171
172 210
        $this->isDirty = true;
173
174 210
        if (! $this->needsSchedulingForSynchronization() || $this->owner === null) {
175 209
            return;
176
        }
177
178 1
        $this->uow->scheduleForSynchronization($this->owner);
179 1
    }
180
181
    /** {@inheritdoc} */
182 413
    public function isDirty()
183
    {
184 413
        if ($this->isDirty) {
185 275
            return true;
186
        }
187 363
        if (! $this->initialized && count($this->coll)) {
188
            // not initialized collection with added elements
189
            return true;
190
        }
191 363
        if ($this->initialized) {
192
            // if initialized let's check with last known snapshot
193 357
            return $this->coll->toArray() !== $this->snapshot;
194
        }
195
196 95
        return false;
197
    }
198
199
    /** {@inheritdoc} */
200 401
    public function setDirty($dirty)
201
    {
202 401
        $this->isDirty = $dirty;
203 401
    }
204
205
    /** {@inheritdoc} */
206 424
    public function setOwner(object $document, array $mapping)
207
    {
208 424
        $this->owner   = $document;
209 424
        $this->mapping = $mapping;
210 424
    }
211
212
    /** {@inheritdoc} */
213 296
    public function takeSnapshot()
214
    {
215 296
        if (CollectionHelper::isList($this->mapping['strategy'])) {
216 282
            $array = $this->coll->toArray();
217 282
            $this->coll->clear();
218 282
            foreach ($array as $document) {
219 257
                $this->coll->add($document);
220
            }
221
        }
222 296
        $this->snapshot = $this->coll->toArray();
223 296
        $this->isDirty  = false;
224 296
    }
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 95
    public function getDeleteDiff()
241
    {
242 95
        return array_udiff_assoc(
243 95
            $this->snapshot,
244 95
            $this->coll->toArray(),
245
            static function ($a, $b) {
246 48
                return $a === $b ? 0 : 1;
247 95
            }
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 95
    public function getInsertDiff()
270
    {
271 95
        return array_udiff_assoc(
272 95
            $this->coll->toArray(),
273 95
            $this->snapshot,
274
            static function ($a, $b) {
275 48
                return $a === $b ? 0 : 1;
276 95
            }
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 411
    public function getOwner() : ?object
299
    {
300 411
        return $this->owner;
301
    }
302
303
    /** {@inheritdoc} */
304 290
    public function getMapping()
305
    {
306 290
        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 250
    public function setInitialized($bool)
329
    {
330 250
        $this->initialized = $bool;
331 250
    }
332
333
    /** {@inheritdoc} */
334 18
    public function isInitialized()
335
    {
336 18
        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 116
    public function count()
454
    {
455
        // Workaround around not being able to directly count inverse collections anymore
456 116
        $this->initialize();
457
458 116
        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 169
    public function add($value)
473
    {
474 169
        return $this->doAdd($value, false);
475
    }
476
477
    /**
478
     * {@inheritdoc}
479
     */
480 384
    public function isEmpty()
481
    {
482 384
        return $this->initialized ? $this->coll->isEmpty() : $this->count() === 0;
483
    }
484
485
    /**
486
     * {@inheritdoc}
487
     */
488 327
    public function getIterator()
489
    {
490 327
        $this->initialize();
491
492 327
        return $this->coll->getIterator();
493
    }
494
495
    /**
496
     * {@inheritdoc}
497
     */
498 232
    public function map(Closure $func)
499
    {
500 232
        $this->initialize();
501
502 232
        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
     * @internal Tried to implement Serializable first but that did not work well
594
     *           with circular references. This solution seems simpler and works well.
595
     */
596 6
    public function __sleep()
597
    {
598 6
        return ['coll', 'initialized', 'mongoData', 'snapshot', 'isDirty', 'hints'];
599
    }
600
601
    /* ArrayAccess implementation */
602
603
    /**
604
     * @see containsKey()
605
     */
606 2
    public function offsetExists($offset)
607
    {
608 2
        $this->initialize();
609
610 2
        return $this->coll->offsetExists($offset);
611
    }
612
613
    /**
614
     * @see get()
615
     */
616 77
    public function offsetGet($offset)
617
    {
618 77
        $this->initialize();
619
620 77
        return $this->coll->offsetGet($offset);
621
    }
622
623
    /**
624
     * @see add()
625
     * @see set()
626
     */
627 42
    public function offsetSet($offset, $value)
628
    {
629 42
        if (! isset($offset)) {
630 41
            return $this->doAdd($value, true);
631
        }
632
633 3
        $this->doSet($offset, $value, true);
634 3
    }
635
636
    /**
637
     * @see remove()
638
     */
639 19
    public function offsetUnset($offset)
640
    {
641 19
        $this->doRemove($offset, true);
642 19
    }
643
644
    public function key()
645
    {
646
        return $this->coll->key();
647
    }
648
649
    /**
650
     * Gets the element of the collection at the current iterator position.
651
     */
652 1
    public function current()
653
    {
654 1
        return $this->coll->current();
655
    }
656
657
    /**
658
     * Moves the internal iterator position to the next element.
659
     */
660
    public function next()
661
    {
662
        return $this->coll->next();
663
    }
664
665
    /**
666
     * {@inheritdoc}
667
     */
668 412
    public function unwrap()
669
    {
670 412
        return $this->coll;
671
    }
672
673
    /**
674
     * Cleanup internal state of cloned persistent collection.
675
     *
676
     * The following problems have to be prevented:
677
     * 1. Added documents are added to old PersistentCollection
678
     * 2. New collection is not dirty, if reused on other document nothing
679
     * changes.
680
     * 3. Snapshot leads to invalid diffs being generated.
681
     * 4. Lazy loading grabs entities from old owner object.
682
     * 5. New collection is connected to old owner and leads to duplicate keys.
683
     */
684 8
    public function __clone()
685
    {
686 8
        if (is_object($this->coll)) {
687 8
            $this->coll = clone $this->coll;
688
        }
689
690 8
        $this->initialize();
691
692 8
        $this->owner    = null;
693 8
        $this->snapshot = [];
694
695 8
        $this->changed();
696 8
    }
697
698
    /**
699
     * Actual logic for adding an element to the collection.
700
     *
701
     * @param mixed $value
702
     * @param bool  $arrayAccess
703
     *
704
     * @return bool
705
     */
706 185
    private function doAdd($value, $arrayAccess)
707
    {
708
        /* Initialize the collection before calling add() so this append operation
709
         * uses the appropriate key. Otherwise, we risk overwriting original data
710
         * when $newObjects are re-added in a later call to initialize().
711
         */
712 185
        if (isset($this->mapping['strategy']) && CollectionHelper::isHash($this->mapping['strategy'])) {
713 15
            $this->initialize();
714
        }
715 185
        $arrayAccess ? $this->coll->offsetSet(null, $value) : $this->coll->add($value);
716 185
        $this->changed();
717
718 185
        if ($this->uow !== null && $this->isOrphanRemovalEnabled() && $value !== null) {
719 108
            $this->uow->unscheduleOrphanRemoval($value);
720
        }
721
722 185
        return true;
723
    }
724
725
    /**
726
     * Actual logic for removing element by its key.
727
     *
728
     * @param mixed $offset
729
     * @param bool  $arrayAccess
730
     *
731
     * @return bool
732
     */
733 25
    private function doRemove($offset, $arrayAccess)
734
    {
735 25
        $this->initialize();
736 25
        if ($arrayAccess) {
737 19
            $this->coll->offsetUnset($offset);
738 19
            $removed = true;
739
        } else {
740 6
            $removed = $this->coll->remove($offset);
741
        }
742
743 25
        if (! $removed && ! $arrayAccess) {
744
            return $removed;
745
        }
746
747 25
        $this->changed();
748
749 25
        return $removed;
750
    }
751
752
    /**
753
     * Actual logic for setting an element in the collection.
754
     *
755
     * @param mixed $offset
756
     * @param mixed $value
757
     * @param bool  $arrayAccess
758
     */
759 36
    private function doSet($offset, $value, $arrayAccess)
760
    {
761 36
        $arrayAccess ? $this->coll->offsetSet($offset, $value) : $this->coll->set($offset, $value);
762
763
        // Handle orphanRemoval
764 36
        if ($this->uow !== null && $this->isOrphanRemovalEnabled() && $value !== null) {
765 34
            $this->uow->unscheduleOrphanRemoval($value);
766
        }
767
768 36
        $this->changed();
769 36
    }
770
771
    /**
772
     * Returns whether or not this collection has orphan removal enabled.
773
     *
774
     * Embedded documents are automatically considered as "orphan removal enabled" because they might have references
775
     * that require to trigger cascade remove operations.
776
     *
777
     * @return bool
778
     */
779 203
    private function isOrphanRemovalEnabled()
780
    {
781 203
        if ($this->mapping === null) {
782 12
            return false;
783
        }
784
785 191
        if (isset($this->mapping['embedded'])) {
786 117
            return true;
787
        }
788
789 84
        return isset($this->mapping['reference']) && $this->mapping['isOwningSide'] && $this->mapping['orphanRemoval'];
790
    }
791
792
    /**
793
     * Checks whether collection owner needs to be scheduled for dirty change in case the collection is modified.
794
     *
795
     * @return bool
796
     */
797 210
    private function needsSchedulingForSynchronization()
798
    {
799 210
        return $this->owner && $this->dm && ! empty($this->mapping['isOwningSide'])
800 210
            && $this->dm->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify();
801
    }
802
}
803