PersistentCollection.php 12.1 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
use Doctrine\Common\DoctrineException;
25 26
use Doctrine\ORM\Mapping\AssociationMapping;

lsmith's avatar
lsmith committed
27
/**
28 29
 * A PersistentCollection represents a collection of elements that have persistent state.
 * Collections of entities represent only the associations (links) to those entities.
romanb's avatar
romanb committed
30
 * That means, if the collection is part of a many-many mapping and you remove
31
 * entities from the collection, only the links in the relation table are removed (on flush).
romanb's avatar
romanb committed
32
 * Similarly, if you remove entities from a collection that is part of a one-many
33
 * mapping this will only result in the nulling out of the foreign keys on flush
34 35
 * (or removal of the links in the relation table if the one-many is mapped through a
 * relation table). If you want entities in a one-many collection to be removed when
romanb's avatar
romanb committed
36 37
 * they're removed from the collection, use deleteOrphans => true on the one-many
 * mapping.
lsmith's avatar
lsmith committed
38
 *
romanb's avatar
romanb committed
39
 * @license   http://www.opensource.org/licenses/lgpl-license.php LGPL
romanb's avatar
romanb committed
40
 * @since     2.0
romanb's avatar
romanb committed
41
 * @version   $Revision: 4930 $
romanb's avatar
romanb committed
42 43
 * @author    Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @author    Roman Borschel <roman@code-factory.org>
lsmith's avatar
lsmith committed
44
 */
45
final class PersistentCollection extends \Doctrine\Common\Collections\Collection
46
{   
romanb's avatar
romanb committed
47 48 49 50 51
    /**
     * The base type of the collection.
     *
     * @var string
     */
52
    private $_type;
53

zYne's avatar
zYne committed
54
    /**
55 56
     * 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.
57
     *
58
     * @var array
zYne's avatar
zYne committed
59
     */
60
    private $_snapshot = array();
61

lsmith's avatar
lsmith committed
62
    /**
63
     * The entity that owns this collection.
64
     * 
65
     * @var object
lsmith's avatar
lsmith committed
66
     */
67
    private $_owner;
68

lsmith's avatar
lsmith committed
69
    /**
70 71
     * The association mapping the collection belongs to.
     * This is currently either a OneToManyMapping or a ManyToManyMapping.
72
     *
73
     * @var Doctrine\ORM\Mapping\AssociationMapping
lsmith's avatar
lsmith committed
74
     */
75
    private $_association;
76

lsmith's avatar
lsmith committed
77
    /**
78
     * The name of the field that is used for collection indexing.
79 80
     *
     * @var string
lsmith's avatar
lsmith committed
81
     */
82
    private $_keyField;
83 84
    
    /**
85
     * The EntityManager that manages the persistence of the collection.
86
     *
87
     * @var Doctrine\ORM\EntityManager
88
     */
89
    private $_em;
90 91 92
    
    /**
     * The name of the field on the target entities that points to the owner
93
     * of the collection. This is only set if the association is bi-directional.
94 95 96
     *
     * @var string
     */
97
    private $_backRefFieldName;
98 99 100 101 102
    
    /**
     * Hydration flag.
     *
     * @var boolean
103
     * @see setHydrationFlag()
104
     */
105
    private $_hydrationFlag = false;
lsmith's avatar
lsmith committed
106

107 108 109
    /**
     * The class descriptor of the owning entity.
     */
110
    private $_typeClass;
111

112 113 114 115 116 117
    /**
     * Whether the collection is dirty and needs to be synchronized with the database
     * when the UnitOfWork that manages its persistent state commits.
     *
     * @var boolean
     */
118
    private $_isDirty = false;
119 120
	
    /** Whether the collection has already been initialized. */
121 122
    private $_initialized = false;

lsmith's avatar
lsmith committed
123
    /**
124
     * Creates a new persistent collection.
lsmith's avatar
lsmith committed
125
     */
126
    public function __construct(EntityManager $em, $class, array $data = array())
lsmith's avatar
lsmith committed
127
    {
128
        parent::__construct($data);
129
        $this->_type = $class->name;
romanb's avatar
romanb committed
130
        $this->_em = $em;
131
        $this->_typeClass = $class;
lsmith's avatar
lsmith committed
132
    }
133

lsmith's avatar
lsmith committed
134
    /**
135
     * INTERNAL: Sets the key column for this collection
lsmith's avatar
lsmith committed
136 137
     *
     * @param string $column
zYne's avatar
zYne committed
138
     * @return Doctrine_Collection
lsmith's avatar
lsmith committed
139
     */
romanb's avatar
romanb committed
140
    public function setKeyField($fieldName)
lsmith's avatar
lsmith committed
141
    {
romanb's avatar
romanb committed
142
        $this->_keyField = $fieldName;
lsmith's avatar
lsmith committed
143
    }
144

lsmith's avatar
lsmith committed
145
    /**
146
     * INTERNAL: returns the name of the key column
lsmith's avatar
lsmith committed
147 148 149
     *
     * @return string
     */
romanb's avatar
romanb committed
150
    public function getKeyField()
lsmith's avatar
lsmith committed
151
    {
romanb's avatar
romanb committed
152
        return $this->_keyField;
lsmith's avatar
lsmith committed
153
    }
154
    
lsmith's avatar
lsmith committed
155
    /**
156
     * INTERNAL:
157
     * Sets the collection owner. Used (only?) during hydration.
lsmith's avatar
lsmith committed
158
     *
159
     * @param object $entity
160
     * @param AssociationMapping $assoc
lsmith's avatar
lsmith committed
161
     */
162
    public function setOwner($entity, AssociationMapping $assoc)
lsmith's avatar
lsmith committed
163
    {
164
        $this->_owner = $entity;
165 166
        $this->_association = $assoc;
        if ($assoc->isInverseSide()) {
167 168
            // For sure bi-directional
            $this->_backRefFieldName = $assoc->mappedByFieldName;
169
        } else {
170 171 172 173 174
            $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName);
            if (isset($targetClass->inverseMappings[$assoc->sourceFieldName])) {
                // Bi-directional
                $this->_backRefFieldName = $targetClass->inverseMappings[$assoc->sourceFieldName]
                        ->sourceFieldName;
lsmith's avatar
lsmith committed
175
            }
176
        }
lsmith's avatar
lsmith committed
177
    }
178

lsmith's avatar
lsmith committed
179
    /**
180
     * INTERNAL:
181
     * Gets the collection owner.
lsmith's avatar
lsmith committed
182
     *
183
     * @return object
lsmith's avatar
lsmith committed
184
     */
185
    public function getOwner()
lsmith's avatar
lsmith committed
186
    {
187
        return $this->_owner;
lsmith's avatar
lsmith committed
188
    }
189

190 191 192 193 194 195 196
    /**
     * Gets the class descriptor for the owning entity class.
     *
     * @return Doctrine\ORM\Mapping\ClassMetadata
     */
    public function getOwnerClass()
    {
197
        return $this->_typeClass;
198 199
    }

lsmith's avatar
lsmith committed
200
    /**
201
     * Removes an element from the collection.
lsmith's avatar
lsmith committed
202 203 204
     *
     * @param mixed $key
     * @return boolean
205
     * @override
lsmith's avatar
lsmith committed
206 207 208
     */
    public function remove($key)
    {
209 210 211 212
        //TODO: delete entity if shouldDeleteOrphans
        /*if ($this->_association->isOneToMany() && $this->_association->shouldDeleteOrphans()) {
            $this->_em->delete($removed);
        }*/
213
        $removed = parent::remove($key);
214 215 216
        if ($removed) {
            $this->_changed();
        }
217
        return $removed;
lsmith's avatar
lsmith committed
218
    }
219

lsmith's avatar
lsmith committed
220
    /**
romanb's avatar
romanb committed
221 222 223
     * When the collection is a Map this is like put(key,value)/add(key,value).
     * When the collection is a List this is like add(position,value).
     * 
lsmith's avatar
lsmith committed
224
     * @param integer $key
romanb's avatar
romanb committed
225
     * @param mixed $value
226
     * @override
lsmith's avatar
lsmith committed
227
     */
228
    public function set($key, $value)
lsmith's avatar
lsmith committed
229
    {
romanb's avatar
romanb committed
230
        parent::set($key, $value);
231 232 233
        if ( ! $this->_hydrationFlag) {
            $this->_changed();
        }
lsmith's avatar
lsmith committed
234
    }
235

lsmith's avatar
lsmith committed
236
    /**
237
     * Adds an element to the collection.
238
     * 
romanb's avatar
romanb committed
239 240
     * @param mixed $value
     * @param string $key 
241
     * @return boolean Always TRUE.
242
     * @override
lsmith's avatar
lsmith committed
243
     */
244
    public function add($value)
lsmith's avatar
lsmith committed
245
    {
246
        parent::add($value);
247

248 249
        if ($this->_hydrationFlag) {
            if ($this->_backRefFieldName) {
250 251
                // Set back reference to owner
                if ($this->_association->isOneToMany()) {
252
                    $this->_typeClass->getReflectionProperty($this->_backRefFieldName)
253 254 255
                            ->setValue($value, $this->_owner);
                } else {
                    // ManyToMany
256
                    $this->_typeClass->getReflectionProperty($this->_backRefFieldName)
257 258
                            ->getValue($value)->add($this->_owner);
                }
259 260 261 262
            }
        } else {
            $this->_changed();
        }
263
        
lsmith's avatar
lsmith committed
264 265
        return true;
    }
romanb's avatar
romanb committed
266 267
    
    /**
268
     * Adds all elements of the other collection to this collection.
romanb's avatar
romanb committed
269
     *
270
     * @param object $otherCollection
romanb's avatar
romanb committed
271
     * @todo Impl
272
     * @override
romanb's avatar
romanb committed
273 274 275
     */
    public function addAll($otherCollection)
    {
romanb's avatar
romanb committed
276
        parent::addAll($otherCollection);
romanb's avatar
romanb committed
277 278 279 280
        //...
        //TODO: Register collection as dirty with the UoW if necessary
        //$this->_changed();
    }
281

282 283 284 285
    /**
     * Checks whether an element is contained in the collection.
     * This is an O(n) operation.
     */
286 287
    public function contains($element)
    {
288 289 290 291
        
        if ( ! $this->_initialized) {
            //TODO: Probably need to hit the database here...?
            //return $this->_checkElementExistence($element);
292 293 294 295
        }
        return parent::contains($element);
    }

296 297 298 299 300 301 302 303 304 305 306
    /**
     * @override
     */
    public function count()
    {
        if ( ! $this->_initialized) {
            //TODO: Initialize
        }
        return parent::count();
    }

307 308 309
    private function _checkElementExistence($element)
    {
        
310 311 312 313 314
    }

    private function _initialize()
    {
        
315
    }
316 317 318 319
    
    /**
     * INTERNAL:
     * Sets a flag that indicates whether the collection is currently being hydrated.
320 321 322
     *
     * If the flag is set to TRUE, this has the following consequences:
     * 
323 324 325
     * 1) During hydration, bidirectional associations are completed automatically
     *    by setting the back reference.
     * 2) During hydration no change notifications are reported to the UnitOfWork.
326
     *    That means add() etc. do not cause the collection to be scheduled
327 328 329 330
     *    for an update.
     *
     * @param boolean $bool
     */
331
    public function setHydrationFlag($bool)
332 333
    {
        $this->_hydrationFlag = $bool;
lsmith's avatar
lsmith committed
334
    }
335

lsmith's avatar
lsmith committed
336
    /**
337 338
     * INTERNAL:
     * Tells this collection to take a snapshot of its current state.
zYne's avatar
zYne committed
339
     */
340
    public function takeSnapshot()
zYne's avatar
zYne committed
341
    {
342
        $this->_snapshot = $this->_elements;
zYne's avatar
zYne committed
343
    }
344

zYne's avatar
zYne committed
345
    /**
346
     * INTERNAL:
347
     * Returns the last snapshot of the elements in the collection.
zYne's avatar
zYne committed
348
     *
349
     * @return array The last snapshot of the elements.
zYne's avatar
zYne committed
350
     */
351
    public function getSnapshot()
zYne's avatar
zYne committed
352 353 354
    {
        return $this->_snapshot;
    }
355

356
    /**
357 358
     * INTERNAL:
     * getDeleteDiff
359
     *
romanb's avatar
romanb committed
360
     * @return array
361
     */
zYne's avatar
zYne committed
362 363
    public function getDeleteDiff()
    {
364
        return array_udiff($this->_snapshot, $this->_elements, array($this, '_compareRecords'));
zYne's avatar
zYne committed
365
    }
366 367

    /**
romanb's avatar
romanb committed
368
     * INTERNAL getInsertDiff
369
     *
romanb's avatar
romanb committed
370
     * @return array
371
     */
zYne's avatar
zYne committed
372 373
    public function getInsertDiff()
    {
374
        return array_udiff($this->_elements, $this->_snapshot, array($this, '_compareRecords'));
375
    }
376

377
    /**
378
     * Compares two records. To be used on _snapshot diffs using array_udiff.
romanb's avatar
romanb committed
379 380
     * 
     * @return integer
381
     */
382
    private function _compareRecords($a, $b)
383
    {
384
        return $a === $b ? 0 : 1;
zYne's avatar
zYne committed
385
    }
386 387
    
    /**
388
     * INTERNAL: Gets the association mapping of the collection.
romanb's avatar
romanb committed
389
     * 
390
     * @return Doctrine\ORM\Mapping\AssociationMapping
391
     */
romanb's avatar
romanb committed
392
    public function getMapping()
393
    {
394
        return $this->_association;
395
    }
396
    
romanb's avatar
romanb committed
397 398 399
    /**
     * Clears the collection.
     */
400 401
    public function clear()
    {
romanb's avatar
romanb committed
402
        //TODO: Register collection as dirty with the UoW if necessary
403 404 405 406 407 408
        //TODO: If oneToMany() && shouldDeleteOrphan() delete entities
        /*if ($this->_association->isOneToMany() && $this->_association->shouldDeleteOrphans()) {
            foreach ($this->_data as $entity) {
                $this->_em->delete($entity);
            }
        }*/
romanb's avatar
romanb committed
409
        parent::clear();
410
        $this->_changed();
romanb's avatar
romanb committed
411 412 413 414
    }
    
    private function _changed()
    {
415 416
        $this->_isDirty = true;
    }
417 418 419 420 421 422 423

    /**
     * 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.
     */
424 425 426 427
    public function isDirty()
    {
        return $this->_isDirty;
    }
428 429 430 431 432 433

    /**
     * Sets a boolean flag, indicating whether this collection is dirty.
     *
     * @param boolean $dirty Whether the collection should be marked dirty or not.
     */
434 435 436
    public function setDirty($dirty)
    {
        $this->_isDirty = $dirty;
437
    }
438 439 440 441 442 443 444 445 446 447 448 449 450 451

    /* Serializable implementation */

    /**
     * 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()
    {
        return array('_elements');
    }
452
}