Completed
Pull Request — master (#1383)
by Maciej
17:22
created

needsSchedulingForDirtyCheck()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 4

Importance

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