Rajout de doctrine/orm
[zf2.biz/galerie.git] / vendor / doctrine / orm / lib / Doctrine / ORM / PersistentCollection.php
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\ORM;
21
22 use Doctrine\ORM\Mapping\ClassMetadata;
23
24 use Doctrine\Common\Collections\Collection;
25 use Doctrine\Common\Collections\ArrayCollection;
26 use Doctrine\Common\Collections\Selectable;
27 use Doctrine\Common\Collections\Criteria;
28 use Doctrine\Common\Collections\ExpressionBuilder;
29
30 use Closure;
31
32 /**
33  * A PersistentCollection represents a collection of elements that have persistent state.
34  *
35  * Collections of entities represent only the associations (links) to those entities.
36  * That means, if the collection is part of a many-many mapping and you remove
37  * entities from the collection, only the links in the relation table are removed (on flush).
38  * Similarly, if you remove entities from a collection that is part of a one-many
39  * mapping this will only result in the nulling out of the foreign keys on flush.
40  *
41  * @since     2.0
42  * @author    Konsta Vesterinen <kvesteri@cc.hut.fi>
43  * @author    Roman Borschel <roman@code-factory.org>
44  * @author    Giorgio Sironi <piccoloprincipeazzurro@gmail.com>
45  * @author    Stefano Rodriguez <stefano.rodriguez@fubles.com>
46  * @todo      Design for inheritance to allow custom implementations?
47  */
48 final class PersistentCollection implements Collection, Selectable
49 {
50     /**
51      * A snapshot of the collection at the moment it was fetched from the database.
52      * This is used to create a diff of the collection at commit time.
53      *
54      * @var array
55      */
56     private $snapshot = array();
57
58     /**
59      * The entity that owns this collection.
60      *
61      * @var object
62      */
63     private $owner;
64
65     /**
66      * The association mapping the collection belongs to.
67      * This is currently either a OneToManyMapping or a ManyToManyMapping.
68      *
69      * @var array
70      */
71     private $association;
72
73     /**
74      * The EntityManager that manages the persistence of the collection.
75      *
76      * @var \Doctrine\ORM\EntityManager
77      */
78     private $em;
79
80     /**
81      * The name of the field on the target entities that points to the owner
82      * of the collection. This is only set if the association is bi-directional.
83      *
84      * @var string
85      */
86     private $backRefFieldName;
87
88     /**
89      * The class descriptor of the collection's entity type.
90      */
91     private $typeClass;
92
93     /**
94      * Whether the collection is dirty and needs to be synchronized with the database
95      * when the UnitOfWork that manages its persistent state commits.
96      *
97      * @var boolean
98      */
99     private $isDirty = false;
100
101     /**
102      * Whether the collection has already been initialized.
103      *
104      * @var boolean
105      */
106     private $initialized = true;
107
108     /**
109      * The wrapped Collection instance.
110      *
111      * @var Collection
112      */
113     private $coll;
114
115     /**
116      * Creates a new persistent collection.
117      *
118      * @param EntityManager $em The EntityManager the collection will be associated with.
119      * @param ClassMetadata $class The class descriptor of the entity type of this collection.
120      * @param array The collection elements.
121      */
122     public function __construct(EntityManager $em, $class, $coll)
123     {
124         $this->coll      = $coll;
125         $this->em        = $em;
126         $this->typeClass = $class;
127     }
128
129     /**
130      * INTERNAL:
131      * Sets the collection's owning entity together with the AssociationMapping that
132      * describes the association between the owner and the elements of the collection.
133      *
134      * @param object $entity
135      * @param AssociationMapping $assoc
136      */
137     public function setOwner($entity, array $assoc)
138     {
139         $this->owner            = $entity;
140         $this->association      = $assoc;
141         $this->backRefFieldName = $assoc['inversedBy'] ?: $assoc['mappedBy'];
142     }
143
144     /**
145      * INTERNAL:
146      * Gets the collection owner.
147      *
148      * @return object
149      */
150     public function getOwner()
151     {
152         return $this->owner;
153     }
154
155     public function getTypeClass()
156     {
157         return $this->typeClass;
158     }
159
160     /**
161      * INTERNAL:
162      * Adds an element to a collection during hydration. This will automatically
163      * complete bidirectional associations in the case of a one-to-many association.
164      *
165      * @param mixed $element The element to add.
166      */
167     public function hydrateAdd($element)
168     {
169         $this->coll->add($element);
170
171         // If _backRefFieldName is set and its a one-to-many association,
172         // we need to set the back reference.
173         if ($this->backRefFieldName && $this->association['type'] === ClassMetadata::ONE_TO_MANY) {
174             // Set back reference to owner
175             $this->typeClass->reflFields[$this->backRefFieldName]->setValue(
176                 $element, $this->owner
177             );
178
179             $this->em->getUnitOfWork()->setOriginalEntityProperty(
180                 spl_object_hash($element), $this->backRefFieldName, $this->owner
181             );
182         }
183     }
184
185     /**
186      * INTERNAL:
187      * Sets a keyed element in the collection during hydration.
188      *
189      * @param mixed $key The key to set.
190      * $param mixed $value The element to set.
191      */
192     public function hydrateSet($key, $element)
193     {
194         $this->coll->set($key, $element);
195
196         // If _backRefFieldName is set, then the association is bidirectional
197         // and we need to set the back reference.
198         if ($this->backRefFieldName && $this->association['type'] === ClassMetadata::ONE_TO_MANY) {
199             // Set back reference to owner
200             $this->typeClass->reflFields[$this->backRefFieldName]->setValue(
201                 $element, $this->owner
202             );
203         }
204     }
205
206     /**
207      * Initializes the collection by loading its contents from the database
208      * if the collection is not yet initialized.
209      */
210     public function initialize()
211     {
212         if ($this->initialized || ! $this->association) {
213             return;
214         }
215
216         // Has NEW objects added through add(). Remember them.
217         $newObjects = array();
218
219         if ($this->isDirty) {
220             $newObjects = $this->coll->toArray();
221         }
222
223         $this->coll->clear();
224         $this->em->getUnitOfWork()->loadCollection($this);
225         $this->takeSnapshot();
226
227         // Reattach NEW objects added through add(), if any.
228         if ($newObjects) {
229             foreach ($newObjects as $obj) {
230                 $this->coll->add($obj);
231             }
232
233             $this->isDirty = true;
234         }
235
236         $this->initialized = true;
237     }
238
239     /**
240      * INTERNAL:
241      * Tells this collection to take a snapshot of its current state.
242      */
243     public function takeSnapshot()
244     {
245         $this->snapshot = $this->coll->toArray();
246         $this->isDirty  = false;
247     }
248
249     /**
250      * INTERNAL:
251      * Returns the last snapshot of the elements in the collection.
252      *
253      * @return array The last snapshot of the elements.
254      */
255     public function getSnapshot()
256     {
257         return $this->snapshot;
258     }
259
260     /**
261      * INTERNAL:
262      * getDeleteDiff
263      *
264      * @return array
265      */
266     public function getDeleteDiff()
267     {
268         return array_udiff_assoc(
269             $this->snapshot,
270             $this->coll->toArray(),
271             function($a, $b) { return $a === $b ? 0 : 1; }
272         );
273     }
274
275     /**
276      * INTERNAL:
277      * getInsertDiff
278      *
279      * @return array
280      */
281     public function getInsertDiff()
282     {
283         return array_udiff_assoc(
284             $this->coll->toArray(),
285             $this->snapshot,
286             function($a, $b) { return $a === $b ? 0 : 1; }
287         );
288     }
289
290     /**
291      * INTERNAL: Gets the association mapping of the collection.
292      *
293      * @return array
294      */
295     public function getMapping()
296     {
297         return $this->association;
298     }
299
300     /**
301      * Marks this collection as changed/dirty.
302      */
303     private function changed()
304     {
305         if ($this->isDirty) {
306             return;
307         }
308
309         $this->isDirty = true;
310
311         if ($this->association !== null &&
312             $this->association['isOwningSide'] &&
313             $this->association['type'] === ClassMetadata::MANY_TO_MANY &&
314             $this->owner &&
315             $this->em->getClassMetadata(get_class($this->owner))->isChangeTrackingNotify()) {
316             $this->em->getUnitOfWork()->scheduleForDirtyCheck($this->owner);
317         }
318     }
319
320     /**
321      * Gets a boolean flag indicating whether this collection is dirty which means
322      * its state needs to be synchronized with the database.
323      *
324      * @return boolean TRUE if the collection is dirty, FALSE otherwise.
325      */
326     public function isDirty()
327     {
328         return $this->isDirty;
329     }
330
331     /**
332      * Sets a boolean flag, indicating whether this collection is dirty.
333      *
334      * @param boolean $dirty Whether the collection should be marked dirty or not.
335      */
336     public function setDirty($dirty)
337     {
338         $this->isDirty = $dirty;
339     }
340
341     /**
342      * Sets the initialized flag of the collection, forcing it into that state.
343      *
344      * @param boolean $bool
345      */
346     public function setInitialized($bool)
347     {
348         $this->initialized = $bool;
349     }
350
351     /**
352      * Checks whether this collection has been initialized.
353      *
354      * @return boolean
355      */
356     public function isInitialized()
357     {
358         return $this->initialized;
359     }
360
361     /** {@inheritdoc} */
362     public function first()
363     {
364         $this->initialize();
365
366         return $this->coll->first();
367     }
368
369     /** {@inheritdoc} */
370     public function last()
371     {
372         $this->initialize();
373
374         return $this->coll->last();
375     }
376
377     /**
378      * {@inheritdoc}
379      */
380     public function remove($key)
381     {
382         // TODO: If the keys are persistent as well (not yet implemented)
383         //       and the collection is not initialized and orphanRemoval is
384         //       not used we can issue a straight SQL delete/update on the
385         //       association (table). Without initializing the collection.
386         $this->initialize();
387
388         $removed = $this->coll->remove($key);
389
390         if ( ! $removed) {
391             return $removed;
392         }
393
394         $this->changed();
395
396         if ($this->association !== null &&
397             $this->association['type'] & ClassMetadata::TO_MANY &&
398             $this->owner &&
399             $this->association['orphanRemoval']) {
400             $this->em->getUnitOfWork()->scheduleOrphanRemoval($removed);
401         }
402
403         return $removed;
404     }
405
406     /**
407      * {@inheritdoc}
408      */
409     public function removeElement($element)
410     {
411         if ( ! $this->initialized && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) {
412             if ($this->coll->contains($element)) {
413                 return $this->coll->removeElement($element);
414             }
415
416             $persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association);
417
418             if ($persister->removeElement($this, $element)) {
419                 return $element;
420             }
421
422             return null;
423         }
424
425         $this->initialize();
426
427         $removed = $this->coll->removeElement($element);
428
429         if ( ! $removed) {
430             return $removed;
431         }
432
433         $this->changed();
434
435         if ($this->association !== null &&
436             $this->association['type'] & ClassMetadata::TO_MANY &&
437             $this->owner &&
438             $this->association['orphanRemoval']) {
439             $this->em->getUnitOfWork()->scheduleOrphanRemoval($element);
440         }
441
442         return $removed;
443     }
444
445     /**
446      * {@inheritdoc}
447      */
448     public function containsKey($key)
449     {
450         $this->initialize();
451
452         return $this->coll->containsKey($key);
453     }
454
455     /**
456      * {@inheritdoc}
457      */
458     public function contains($element)
459     {
460         if ( ! $this->initialized && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) {
461             $persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association);
462
463             return $this->coll->contains($element) || $persister->contains($this, $element);
464         }
465
466         $this->initialize();
467
468         return $this->coll->contains($element);
469     }
470
471     /**
472      * {@inheritdoc}
473      */
474     public function exists(Closure $p)
475     {
476         $this->initialize();
477
478         return $this->coll->exists($p);
479     }
480
481     /**
482      * {@inheritdoc}
483      */
484     public function indexOf($element)
485     {
486         $this->initialize();
487
488         return $this->coll->indexOf($element);
489     }
490
491     /**
492      * {@inheritdoc}
493      */
494     public function get($key)
495     {
496         $this->initialize();
497
498         return $this->coll->get($key);
499     }
500
501     /**
502      * {@inheritdoc}
503      */
504     public function getKeys()
505     {
506         $this->initialize();
507
508         return $this->coll->getKeys();
509     }
510
511     /**
512      * {@inheritdoc}
513      */
514     public function getValues()
515     {
516         $this->initialize();
517
518         return $this->coll->getValues();
519     }
520
521     /**
522      * {@inheritdoc}
523      */
524     public function count()
525     {
526         if ( ! $this->initialized && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) {
527             $persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association);
528
529             return $persister->count($this) + ($this->isDirty ? $this->coll->count() : 0);
530         }
531
532         $this->initialize();
533
534         return $this->coll->count();
535     }
536
537     /**
538      * {@inheritdoc}
539      */
540     public function set($key, $value)
541     {
542         $this->initialize();
543
544         $this->coll->set($key, $value);
545
546         $this->changed();
547     }
548
549     /**
550      * {@inheritdoc}
551      */
552     public function add($value)
553     {
554         $this->coll->add($value);
555
556         $this->changed();
557
558         return true;
559     }
560
561     /**
562      * {@inheritdoc}
563      */
564     public function isEmpty()
565     {
566         $this->initialize();
567
568         return $this->coll->isEmpty();
569     }
570
571     /**
572      * {@inheritdoc}
573      */
574     public function getIterator()
575     {
576         $this->initialize();
577
578         return $this->coll->getIterator();
579     }
580
581     /**
582      * {@inheritdoc}
583      */
584     public function map(Closure $func)
585     {
586         $this->initialize();
587
588         return $this->coll->map($func);
589     }
590
591     /**
592      * {@inheritdoc}
593      */
594     public function filter(Closure $p)
595     {
596         $this->initialize();
597
598         return $this->coll->filter($p);
599     }
600
601     /**
602      * {@inheritdoc}
603      */
604     public function forAll(Closure $p)
605     {
606         $this->initialize();
607
608         return $this->coll->forAll($p);
609     }
610
611     /**
612      * {@inheritdoc}
613      */
614     public function partition(Closure $p)
615     {
616         $this->initialize();
617
618         return $this->coll->partition($p);
619     }
620
621     /**
622      * {@inheritdoc}
623      */
624     public function toArray()
625     {
626         $this->initialize();
627
628         return $this->coll->toArray();
629     }
630
631     /**
632      * {@inheritdoc}
633      */
634     public function clear()
635     {
636         if ($this->initialized && $this->isEmpty()) {
637             return;
638         }
639
640         $uow = $this->em->getUnitOfWork();
641
642         if ($this->association['type'] & ClassMetadata::TO_MANY &&
643             $this->association['orphanRemoval'] &&
644             $this->owner) {
645             // we need to initialize here, as orphan removal acts like implicit cascadeRemove,
646             // hence for event listeners we need the objects in memory.
647             $this->initialize();
648
649             foreach ($this->coll as $element) {
650                 $uow->scheduleOrphanRemoval($element);
651             }
652         }
653
654         $this->coll->clear();
655
656         $this->initialized = true; // direct call, {@link initialize()} is too expensive
657
658         if ($this->association['isOwningSide']) {
659             $this->changed();
660
661             $uow->scheduleCollectionDeletion($this);
662
663             $this->takeSnapshot();
664         }
665     }
666
667     /**
668      * Called by PHP when this collection is serialized. Ensures that only the
669      * elements are properly serialized.
670      *
671      * @internal Tried to implement Serializable first but that did not work well
672      *           with circular references. This solution seems simpler and works well.
673      */
674     public function __sleep()
675     {
676         return array('coll', 'initialized');
677     }
678
679     /* ArrayAccess implementation */
680
681     /**
682      * @see containsKey()
683      */
684     public function offsetExists($offset)
685     {
686         return $this->containsKey($offset);
687     }
688
689     /**
690      * @see get()
691      */
692     public function offsetGet($offset)
693     {
694         return $this->get($offset);
695     }
696
697     /**
698      * @see add()
699      * @see set()
700      */
701     public function offsetSet($offset, $value)
702     {
703         if ( ! isset($offset)) {
704             return $this->add($value);
705         }
706
707         return $this->set($offset, $value);
708     }
709
710     /**
711      * @see remove()
712      */
713     public function offsetUnset($offset)
714     {
715         return $this->remove($offset);
716     }
717
718     public function key()
719     {
720         return $this->coll->key();
721     }
722
723     /**
724      * Gets the element of the collection at the current iterator position.
725      */
726     public function current()
727     {
728         return $this->coll->current();
729     }
730
731     /**
732      * Moves the internal iterator position to the next element.
733      */
734     public function next()
735     {
736         return $this->coll->next();
737     }
738
739     /**
740      * Retrieves the wrapped Collection instance.
741      *
742      * @return \Doctrine\Common\Collections\Collection
743      */
744     public function unwrap()
745     {
746         return $this->coll;
747     }
748
749     /**
750      * Extract a slice of $length elements starting at position $offset from the Collection.
751      *
752      * If $length is null it returns all elements from $offset to the end of the Collection.
753      * Keys have to be preserved by this method. Calling this method will only return the
754      * selected slice and NOT change the elements contained in the collection slice is called on.
755      *
756      * @param int $offset
757      * @param int $length
758      *
759      * @return array
760      */
761     public function slice($offset, $length = null)
762     {
763         if ( ! $this->initialized && ! $this->isDirty && $this->association['fetch'] === Mapping\ClassMetadataInfo::FETCH_EXTRA_LAZY) {
764             $persister = $this->em->getUnitOfWork()->getCollectionPersister($this->association);
765
766             return $persister->slice($this, $offset, $length);
767         }
768
769         $this->initialize();
770
771         return $this->coll->slice($offset, $length);
772     }
773
774     /**
775      * Cleanup internal state of cloned persistent collection.
776      *
777      * The following problems have to be prevented:
778      * 1. Added entities are added to old PC
779      * 2. New collection is not dirty, if reused on other entity nothing
780      * changes.
781      * 3. Snapshot leads to invalid diffs being generated.
782      * 4. Lazy loading grabs entities from old owner object.
783      * 5. New collection is connected to old owner and leads to duplicate keys.
784      */
785     public function __clone()
786     {
787         if (is_object($this->coll)) {
788             $this->coll = clone $this->coll;
789         }
790
791         $this->initialize();
792
793         $this->owner    = null;
794         $this->snapshot = array();
795
796         $this->changed();
797     }
798
799     /**
800      * Select all elements from a selectable that match the expression and
801      * return a new collection containing these elements.
802      *
803      * @param \Doctrine\Common\Collections\Criteria $criteria
804      * @return Collection
805      */
806     public function matching(Criteria $criteria)
807     {
808         if ($this->initialized) {
809             return $this->coll->matching($criteria);
810         }
811
812         if ($this->association['type'] !== ClassMetadata::ONE_TO_MANY) {
813             throw new \RuntimeException("Matching Criteria on PersistentCollection only works on OneToMany assocations at the moment.");
814         }
815
816         // If there are NEW objects we have to check if any of them matches the criteria
817         $newObjects = array();
818
819         if ($this->isDirty) {
820             $newObjects = $this->coll->matching($criteria)->toArray();
821         }
822
823         $targetClass = $this->em->getClassMetadata(get_class($this->owner));
824
825         $id              = $targetClass->getSingleIdReflectionProperty()->getValue($this->owner);
826         $builder         = Criteria::expr();
827         $ownerExpression = $builder->eq($this->backRefFieldName, $id);
828         $expression      = $criteria->getWhereExpression();
829         $expression      = $expression ? $builder->andX($expression, $ownerExpression) : $ownerExpression;
830
831         $criteria->where($expression);
832
833         $persister = $this->em->getUnitOfWork()->getEntityPersister($this->association['targetEntity']);
834
835         return new ArrayCollection(array_merge($persister->loadCriteria($criteria), $newObjects));
836     }
837 }
838