PersistentCollection.php 15.4 KB
Newer Older
lsmith's avatar
lsmith committed
1 2
<?php
/*
3
 *  $Id$
lsmith's avatar
lsmith committed
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
19
 * <http://www.doctrine-project.org>.
lsmith's avatar
lsmith committed
20
 */
21

22
namespace Doctrine\ORM;
23

24 25 26
use Doctrine\Common\DoctrineException,
    Doctrine\ORM\Mapping\AssociationMapping,
    \Closure;
27

lsmith's avatar
lsmith committed
28
/**
29
 * A PersistentCollection represents a collection of elements that have persistent state.
30
 *
31
 * Collections of entities represent only the associations (links) to those entities.
romanb's avatar
romanb committed
32
 * That means, if the collection is part of a many-many mapping and you remove
33
 * entities from the collection, only the links in the relation table are removed (on flush).
romanb's avatar
romanb committed
34
 * Similarly, if you remove entities from a collection that is part of a one-many
35
 * mapping this will only result in the nulling out of the foreign keys on flush.
lsmith's avatar
lsmith committed
36
 *
romanb's avatar
romanb committed
37
 * @license   http://www.opensource.org/licenses/lgpl-license.php LGPL
romanb's avatar
romanb committed
38
 * @since     2.0
romanb's avatar
romanb committed
39
 * @version   $Revision: 4930 $
romanb's avatar
romanb committed
40 41
 * @author    Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @author    Roman Borschel <roman@code-factory.org>
42
 * @author    Giorgio Sironi <piccoloprincipeazzurro@gmail.com>
lsmith's avatar
lsmith committed
43
 */
44
final class PersistentCollection implements \Doctrine\Common\Collections\Collection
45
{
zYne's avatar
zYne committed
46
    /**
47 48
     * A snapshot of the collection at the moment it was fetched from the database.
     * This is used to create a diff of the collection at commit time.
49
     *
50
     * @var array
zYne's avatar
zYne committed
51
     */
52
    private $_snapshot = array();
53

lsmith's avatar
lsmith committed
54
    /**
55
     * The entity that owns this collection.
56
     *
57
     * @var object
lsmith's avatar
lsmith committed
58
     */
59
    private $_owner;
60

lsmith's avatar
lsmith committed
61
    /**
62 63
     * The association mapping the collection belongs to.
     * This is currently either a OneToManyMapping or a ManyToManyMapping.
64
     *
65
     * @var Doctrine\ORM\Mapping\AssociationMapping
lsmith's avatar
lsmith committed
66
     */
67
    private $_association;
68

69
    /**
70
     * The EntityManager that manages the persistence of the collection.
71
     *
72
     * @var Doctrine\ORM\EntityManager
73
     */
74
    private $_em;
75

76 77
    /**
     * The name of the field on the target entities that points to the owner
78
     * of the collection. This is only set if the association is bi-directional.
79 80 81
     *
     * @var string
     */
82
    private $_backRefFieldName;
lsmith's avatar
lsmith committed
83

84
    /**
85
     * The class descriptor of the collection's entity type.
86
     */
87
    private $_typeClass;
88

89 90 91 92 93 94
    /**
     * Whether the collection is dirty and needs to be synchronized with the database
     * when the UnitOfWork that manages its persistent state commits.
     *
     * @var boolean
     */
95
    private $_isDirty = false;
96

97 98 99 100 101
    /**
     * Whether the collection has already been initialized.
     * 
     * @var boolean
     */
102
    private $_initialized = true;
103 104 105 106 107 108 109
    
    /**
     * The wrapped Collection instance.
     * 
     * @var Collection
     */
    private $_coll;
110

lsmith's avatar
lsmith committed
111
    /**
112
     * Creates a new persistent collection.
113 114 115 116
     * 
     * @param EntityManager $em The EntityManager the collection will be associated with.
     * @param ClassMetadata $class The class descriptor of the entity type of this collection.
     * @param array The collection elements.
lsmith's avatar
lsmith committed
117
     */
118
    public function __construct(EntityManager $em, $class, $coll)
lsmith's avatar
lsmith committed
119
    {
120
        $this->_coll = $coll;
romanb's avatar
romanb committed
121
        $this->_em = $em;
122
        $this->_typeClass = $class;
lsmith's avatar
lsmith committed
123
    }
124

lsmith's avatar
lsmith committed
125
    /**
126
     * INTERNAL:
127 128
     * Sets the collection's owning entity together with the AssociationMapping that
     * describes the association between the owner and the elements of the collection.
lsmith's avatar
lsmith committed
129
     *
130
     * @param object $entity
131
     * @param AssociationMapping $assoc
lsmith's avatar
lsmith committed
132
     */
133
    public function setOwner($entity, AssociationMapping $assoc)
lsmith's avatar
lsmith committed
134
    {
135
        $this->_owner = $entity;
136
        $this->_association = $assoc;
137
        
138
        // Check for bidirectionality
139
        if ( ! $assoc->isOwningSide) {
140 141
            // For sure bi-directional
            $this->_backRefFieldName = $assoc->mappedByFieldName;
142
        } else {
143
            if (isset($this->_typeClass->inverseMappings[$assoc->sourceEntityName][$assoc->sourceFieldName])) {
144
                // Bi-directional
145
                $this->_backRefFieldName = $this->_typeClass->inverseMappings[$assoc->sourceEntityName][$assoc->sourceFieldName]->sourceFieldName;
lsmith's avatar
lsmith committed
146
            }
147
        }
lsmith's avatar
lsmith committed
148
    }
149

lsmith's avatar
lsmith committed
150
    /**
151
     * INTERNAL:
152
     * Gets the collection owner.
lsmith's avatar
lsmith committed
153
     *
154
     * @return object
lsmith's avatar
lsmith committed
155
     */
156
    public function getOwner()
lsmith's avatar
lsmith committed
157
    {
158
        return $this->_owner;
lsmith's avatar
lsmith committed
159
    }
160

161 162 163 164 165 166 167
    /**
     * Gets the class descriptor for the owning entity class.
     *
     * @return Doctrine\ORM\Mapping\ClassMetadata
     */
    public function getOwnerClass()
    {
168
        return $this->_typeClass;
169 170
    }

171 172
    /**
     * INTERNAL:
173 174
     * Adds an element to a collection during hydration. This will automatically
     * complete bidirectional associations.
175
     * 
176
     * @param mixed $element The element to add.
177
     */
178
    public function hydrateAdd($element)
179
    {
romanb's avatar
romanb committed
180
        $this->_coll->add($element);
181
        
182 183 184
        // If _backRefFieldName is set, then the association is bidirectional
        // and we need to set the back reference.
        if ($this->_backRefFieldName) {
185 186 187
            // Set back reference to owner
            if ($this->_association->isOneToMany()) {
                // OneToMany
188
                $this->_typeClass->reflFields[$this->_backRefFieldName]
189
                        ->setValue($element, $this->_owner);
190 191
            } else {
                // ManyToMany
192 193
                $this->_typeClass->reflFields[$this->_backRefFieldName] 
                        ->getValue($element)->add($this->_owner);
194 195
            }
        }
lsmith's avatar
lsmith committed
196
    }
romanb's avatar
romanb committed
197 198
    
    /**
199 200
     * INTERNAL:
     * Sets a keyed element in the collection during hydration.
romanb's avatar
romanb committed
201
     *
202 203
     * @param mixed $key The key to set.
     * $param mixed $value The element to set.
romanb's avatar
romanb committed
204
     */
205
    public function hydrateSet($key, $element)
romanb's avatar
romanb committed
206
    {
romanb's avatar
romanb committed
207
        $this->_coll->set($key, $element);
208
        
209 210 211 212 213 214 215 216 217 218 219 220 221 222
        // If _backRefFieldName is set, then the association is bidirectional
        // and we need to set the back reference.
        if ($this->_backRefFieldName) {
            // Set back reference to owner
            if ($this->_association->isOneToMany()) {
                // OneToMany
                $this->_typeClass->reflFields[$this->_backRefFieldName]
                        ->setValue($element, $this->_owner);
            } else {
                // ManyToMany
                $this->_typeClass->reflFields[$this->_backRefFieldName] 
                        ->getValue($element)->set($key, $this->_owner);
            }
        }
romanb's avatar
romanb committed
223
    }
224

romanb's avatar
romanb committed
225
    /**
226 227
     * Initializes the collection by loading its contents from the database
     * if the collection is not yet initialized.
romanb's avatar
romanb committed
228
     */
229 230
    private function _initialize()
    {
231
        if ( ! $this->_initialized) {
232 233 234
            $this->_association->load($this->_owner, $this, $this->_em);
            $this->_initialized = true;
        }
lsmith's avatar
lsmith committed
235
    }
236

lsmith's avatar
lsmith committed
237
    /**
238 239
     * INTERNAL:
     * Tells this collection to take a snapshot of its current state.
zYne's avatar
zYne committed
240
     */
241
    public function takeSnapshot()
zYne's avatar
zYne committed
242
    {
243
        $this->_snapshot = $this->_coll->toArray();
zYne's avatar
zYne committed
244
    }
245

zYne's avatar
zYne committed
246
    /**
247
     * INTERNAL:
248
     * Returns the last snapshot of the elements in the collection.
zYne's avatar
zYne committed
249
     *
250
     * @return array The last snapshot of the elements.
zYne's avatar
zYne committed
251
     */
252
    public function getSnapshot()
zYne's avatar
zYne committed
253 254 255
    {
        return $this->_snapshot;
    }
256

257
    /**
258 259
     * INTERNAL:
     * getDeleteDiff
260
     *
romanb's avatar
romanb committed
261
     * @return array
262
     */
zYne's avatar
zYne committed
263 264
    public function getDeleteDiff()
    {
265
        return array_udiff($this->_snapshot, $this->_coll->toArray(), array($this, '_compareRecords'));
zYne's avatar
zYne committed
266
    }
267 268

    /**
romanb's avatar
romanb committed
269
     * INTERNAL getInsertDiff
270
     *
romanb's avatar
romanb committed
271
     * @return array
272
     */
zYne's avatar
zYne committed
273 274
    public function getInsertDiff()
    {
275
        return array_udiff($this->_coll->toArray(), $this->_snapshot, array($this, '_compareRecords'));
276
    }
277

278
    /**
279
     * Compares two records. To be used on _snapshot diffs using array_udiff.
280
     *
romanb's avatar
romanb committed
281
     * @return integer
282
     */
283
    private function _compareRecords($a, $b)
284
    {
285
        return $a === $b ? 0 : 1;
zYne's avatar
zYne committed
286
    }
287

288
    /**
289
     * INTERNAL: Gets the association mapping of the collection.
290
     *
291
     * @return Doctrine\ORM\Mapping\AssociationMapping
292
     */
romanb's avatar
romanb committed
293
    public function getMapping()
294
    {
295
        return $this->_association;
296
    }
297
   
romanb's avatar
romanb committed
298 299 300
    /**
     * Marks this collection as changed/dirty.
     */
romanb's avatar
romanb committed
301 302
    private function _changed()
    {
303 304
        $this->_isDirty = true;
    }
305 306 307 308 309 310 311

    /**
     * Gets a boolean flag indicating whether this colleciton is dirty which means
     * its state needs to be synchronized with the database.
     *
     * @return boolean TRUE if the collection is dirty, FALSE otherwise.
     */
312 313 314 315
    public function isDirty()
    {
        return $this->_isDirty;
    }
316 317 318 319 320 321

    /**
     * Sets a boolean flag, indicating whether this collection is dirty.
     *
     * @param boolean $dirty Whether the collection should be marked dirty or not.
     */
322 323 324
    public function setDirty($dirty)
    {
        $this->_isDirty = $dirty;
325
    }
326 327
    
    /**
328
     * Sets the initialized flag of the collection, forcing it into that state.
329
     * 
330
     * @param boolean $bool
331 332 333 334 335
     */
    public function setInitialized($bool)
    {
        $this->_initialized = $bool;
    }
336
    
337
    /**
338
     * Checks whether this collection has been initialized.
339
     *
340
     * @return boolean
341
     */
342
    public function isInitialized()
343
    {
344
        return $this->_initialized;
345 346
    }

347
    /** {@inheritdoc} */
348 349 350
    public function first()
    {
        $this->_initialize();
351
        return $this->_coll->first();
352 353
    }

354
    /** {@inheritdoc} */
355 356 357
    public function last()
    {
        $this->_initialize();
358
        return $this->_coll->last();
359 360 361
    }

    /**
362
     * {@inheritdoc}
363 364 365 366
     */
    public function remove($key)
    {
        $this->_initialize();
367
        $removed = $this->_coll->remove($key);
368 369 370 371 372 373 374 375 376 377
        if ($removed) {
            $this->_changed();
            if ($this->_association->isOneToMany() && $this->_association->orphanRemoval) {
                $this->_em->getUnitOfWork()->scheduleOrphanRemoval($removed);
            }
        }
        
        return $removed;
    }

378 379 380
    /**
     * {@inheritdoc}
     */
381 382 383
    public function removeElement($element)
    {
        $this->_initialize();
384
        $result = $this->_coll->removeElement($element);
385 386 387 388
        $this->_changed();
        return $result;
    }

389 390 391
    /**
     * {@inheritdoc}
     */
392 393 394
    public function containsKey($key)
    {
        $this->_initialize();
395
        return $this->_coll->containsKey($key);
396 397 398
    }

    /**
399
     * {@inheritdoc}
400 401 402 403
     */
    public function contains($element)
    {
        $this->_initialize();
404
        return $this->_coll->contains($element);
405 406
    }

407 408 409
    /**
     * {@inheritdoc}
     */
410 411 412
    public function exists(Closure $p)
    {
        $this->_initialize();
413
        return $this->_coll->exists($p);
414 415
    }

416 417 418
    /**
     * {@inheritdoc}
     */
419 420 421
    public function search($element)
    {
        $this->_initialize();
422
        return $this->_coll->search($element);
423 424
    }

425 426 427
    /**
     * {@inheritdoc}
     */
428 429 430
    public function get($key)
    {
        $this->_initialize();
431
        return $this->_coll->get($key);
432 433
    }

434 435 436
    /**
     * {@inheritdoc}
     */
437 438 439
    public function getKeys()
    {
        $this->_initialize();
440
        return $this->_coll->getKeys();
441 442
    }

443 444 445
    /**
     * {@inheritdoc}
     */
446
    public function getValues()
447 448
    {
        $this->_initialize();
449
        return $this->_coll->getValues();
450 451 452
    }

    /**
453
     * {@inheritdoc}
454 455 456 457
     */
    public function count()
    {
        $this->_initialize();
458
        return $this->_coll->count();
459 460 461
    }

    /**
462
     * {@inheritdoc}
463 464 465
     */
    public function set($key, $value)
    {
466
        $this->_coll->set($key, $value);
467 468 469 470
        $this->_changed();
    }

    /**
471
     * {@inheritdoc}
472 473 474
     */
    public function add($value)
    {
475
        $this->_coll->add($value);
476
        $this->_changed();
477
        return true;
478 479
    }

480 481 482
    /**
     * {@inheritdoc}
     */
483 484 485
    public function isEmpty()
    {
        $this->_initialize();
486
        return $this->_coll->isEmpty();
487
    }
488 489 490 491
    
    /**
     * {@inheritdoc}
     */
492 493 494
    public function getIterator()
    {
        $this->_initialize();
495
        return $this->_coll->getIterator();
496 497
    }

498 499 500
    /**
     * {@inheritdoc}
     */
501 502 503
    public function map(Closure $func)
    {
        $this->_initialize();
504
        $result = $this->_coll->map($func);
505 506 507 508
        $this->_changed();
        return $result;
    }

509 510 511
    /**
     * {@inheritdoc}
     */
512 513 514
    public function filter(Closure $p)
    {
        $this->_initialize();
515
        return $this->_coll->filter($p);
516
    }
517 518 519 520
    
    /**
     * {@inheritdoc}
     */
521 522 523
    public function forAll(Closure $p)
    {
        $this->_initialize();
524
        return $this->_coll->forAll($p);
525 526
    }

527 528 529
    /**
     * {@inheritdoc}
     */
530 531 532
    public function partition(Closure $p)
    {
        $this->_initialize();
533
        return $this->_coll->partition($p);
534 535 536
    }

    /**
537
     * {@inheritdoc}
538 539 540 541
     */
    public function clear()
    {
        $this->_initialize();
542
        $result = $this->_coll->clear();
543 544 545 546
        if ($this->_association->isOwningSide) {
            $this->_changed();
            $this->_em->getUnitOfWork()->scheduleCollectionDeletion($this);
        }
547
        
548
        return $result;
549
    }
550 551 552 553 554 555 556 557 558 559
    
    /**
     * Called by PHP when this collection is serialized. Ensures that only the
     * elements are properly serialized.
     *
     * @internal Tried to implement Serializable first but that did not work well
     *           with circular references. This solution seems simpler and works well.
     */
    public function __sleep()
    {
560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610
        return array('_coll');
    }
    
    /* ArrayAccess implementation */

    /**
     * @see containsKey()
     */
    public function offsetExists($offset)
    {
        return $this->containsKey($offset);
    }

    /**
     * @see get()
     */
    public function offsetGet($offset)
    {
        return $this->get($offset);
    }

    /**
     * @see add()
     * @see set()
     */
    public function offsetSet($offset, $value)
    {
        if ( ! isset($offset)) {
            return $this->add($value);
        }
        return $this->set($offset, $value);
    }

    /**
     * @see remove()
     */
    public function offsetUnset($offset)
    {
        return $this->remove($offset);
    }
    
    public function toArray()
    {
        return $this->_coll->toArray();
    }
    
    public function key()
    {
        return $this->_coll->key();
    }
    
611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629
    /**
     * Gets the element of the collection at the current iterator position.
     */
    public function current()
    {
        return $this->_coll->current();
    }
    
    /**
     * Moves the internal iterator position to the next element.
     */
    public function next()
    {
        return $this->_coll->next();
    }
    
    /**
     * Retrieves the wrapped Collection instance.
     */
630 631 632
    public function unwrap()
    {
        return $this->_coll;
633
    }
634
}