Completed
Pull Request — master (#1427)
by Andreas
06:15
created

PersistentCollectionTrait::changed()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3.0261

Importance

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