Collection.php 27.6 KB
Newer Older
lsmith's avatar
lsmith committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
<?php
/*
 *  $Id$
 *
 * 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.phpdoctrine.org>.
lsmith's avatar
lsmith committed
20
 */
21

22 23
#namespace Doctrine::ORM;

lsmith's avatar
lsmith committed
24
/**
25
 * A persistent collection.
romanb's avatar
romanb committed
26
 * 
27
 * A collection object is strongly typed in the sense that it can only contain
romanb's avatar
romanb committed
28 29 30 31 32 33
 * entities of a specific type or one of it's subtypes. A collection object is
 * basically a wrapper around an ordinary php array and just like a php array
 * it can have List or Map semantics.
 * 
 * A collection of entities represents only the associations (links) to those entities.
 * That means, if the collection is part of a many-many mapping and you remove
34
 * entities from the collection, only the links in the xref table are removed (on flush).
romanb's avatar
romanb committed
35
 * Similarly, if you remove entities from a collection that is part of a one-many
36
 * mapping this will only result in the nulling out of the foreign keys on flush
romanb's avatar
romanb committed
37 38 39 40
 * (or removal of the links in the xref table if the one-many is mapped through an
 * xref table). If you want entities in a one-many collection to be removed when
 * they're removed from the collection, use deleteOrphans => true on the one-many
 * mapping.
lsmith's avatar
lsmith committed
41
 *
romanb's avatar
romanb committed
42 43 44 45 46
 * @license   http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @since     1.0
 * @version   $Revision$
 * @author    Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @author    Roman Borschel <roman@code-factory.org>
47
 * @todo Add more typical Collection methods.
lsmith's avatar
lsmith committed
48
 */
romanb's avatar
romanb committed
49
class Doctrine_Collection implements Countable, IteratorAggregate, Serializable, ArrayAccess
50
{   
romanb's avatar
romanb committed
51 52 53 54 55
    /**
     * The base type of the collection.
     *
     * @var string
     */
romanb's avatar
romanb committed
56 57
    protected $_entityBaseType;
    
lsmith's avatar
lsmith committed
58
    /**
romanb's avatar
romanb committed
59 60
     * An array containing the entries of this collection.
     * This is the wrapped php array.
61
     *
romanb's avatar
romanb committed
62
     * @var array 
lsmith's avatar
lsmith committed
63
     */
64
    protected $_data = array();
65

zYne's avatar
zYne committed
66
    /**
67 68
     * 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.
69
     *
70
     * @var array
zYne's avatar
zYne committed
71 72
     */
    protected $_snapshot = array();
73

lsmith's avatar
lsmith committed
74
    /**
75
     * This entity that owns this collection.
76
     * 
77
     * @var Doctrine::ORM::Entity
lsmith's avatar
lsmith committed
78
     */
79
    protected $_owner;
80

lsmith's avatar
lsmith committed
81
    /**
82 83
     * The association mapping the collection belongs to.
     * This is currently either a OneToManyMapping or a ManyToManyMapping.
84
     *
85
     * @var Doctrine::ORM::Mapping::AssociationMapping             
lsmith's avatar
lsmith committed
86
     */
87
    protected $_association;
88

lsmith's avatar
lsmith committed
89
    /**
90
     * The name of the field that is used for collection key mapping.
91 92
     *
     * @var string
lsmith's avatar
lsmith committed
93
     */
romanb's avatar
romanb committed
94
    protected $_keyField;
95

lsmith's avatar
lsmith committed
96
    /**
97 98 99
     * Helper variable. Used for fast null value testing.
     *
     * @var Doctrine_Null
lsmith's avatar
lsmith committed
100
     */
101 102 103
    //protected static $null;
    
    /**
104
     * The EntityManager that manages the persistence of the collection.
105
     *
106
     * @var Doctrine::ORM::EntityManager
107 108
     */
    protected $_em;
109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
    
    /**
     * The name of the field on the target entities that points to the owner
     * of the collection. This is only set if the association is bidirectional.
     *
     * @var string
     */
    protected $_backRefFieldName;
    
    /**
     * Hydration flag.
     *
     * @var boolean
     * @see _setHydrationFlag()
     */
    protected $_hydrationFlag;
lsmith's avatar
lsmith committed
125 126

    /**
127
     * Constructor.
128
     * Creates a new persistent collection.
lsmith's avatar
lsmith committed
129
     */
romanb's avatar
romanb committed
130
    public function __construct($entityBaseType, $keyField = null)
lsmith's avatar
lsmith committed
131
    {
132
        $this->_entityBaseType = $entityBaseType;
romanb's avatar
romanb committed
133
        $this->_em = Doctrine_EntityManager::getActiveEntityManager();
134

romanb's avatar
romanb committed
135
        if ($keyField !== null) {
136
            if ( ! $this->_em->getClassMetadata($entityBaseType)->hasField($keyField)) {
137
                throw new Doctrine_Exception("Invalid field '$keyField' can't be uses as key.");
romanb's avatar
romanb committed
138 139
            }
            $this->_keyField = $keyField;
lsmith's avatar
lsmith committed
140 141
        }
    }
142

zYne's avatar
zYne committed
143 144 145 146
    /**
     * setData
     *
     * @param array $data
147
     * @todo Remove?
zYne's avatar
zYne committed
148 149 150
     */
    public function setData(array $data) 
    {
151
        $this->_data = $data;
zYne's avatar
zYne committed
152
    }
153

lsmith's avatar
lsmith committed
154
    /**
155
     * INTERNAL: Sets the key column for this collection
lsmith's avatar
lsmith committed
156 157
     *
     * @param string $column
zYne's avatar
zYne committed
158
     * @return Doctrine_Collection
lsmith's avatar
lsmith committed
159
     */
romanb's avatar
romanb committed
160
    public function setKeyField($fieldName)
lsmith's avatar
lsmith committed
161
    {
romanb's avatar
romanb committed
162
        $this->_keyField = $fieldName;
zYne's avatar
zYne committed
163
        return $this;
lsmith's avatar
lsmith committed
164
    }
165

lsmith's avatar
lsmith committed
166
    /**
167
     * INTERNAL: returns the name of the key column
lsmith's avatar
lsmith committed
168 169 170
     *
     * @return string
     */
romanb's avatar
romanb committed
171
    public function getKeyField()
lsmith's avatar
lsmith committed
172
    {
romanb's avatar
romanb committed
173
        return $this->_keyField;
lsmith's avatar
lsmith committed
174
    }
175

lsmith's avatar
lsmith committed
176 177 178 179 180
    /**
     * returns all the records as an array
     *
     * @return array
     */
181
    public function unwrap()
lsmith's avatar
lsmith committed
182
    {
183
        return $this->_data;
lsmith's avatar
lsmith committed
184
    }
185

lsmith's avatar
lsmith committed
186 187 188 189 190 191 192
    /**
     * returns the first record in the collection
     *
     * @return mixed
     */
    public function getFirst()
    {
193
        return reset($this->_data);
lsmith's avatar
lsmith committed
194
    }
195

lsmith's avatar
lsmith committed
196 197 198 199 200 201 202
    /**
     * returns the last record in the collection
     *
     * @return mixed
     */
    public function getLast()
    {
203
        return end($this->_data);
lsmith's avatar
lsmith committed
204
    }
205
    
206 207 208 209 210 211 212
    /**
     * returns the last record in the collection
     *
     * @return mixed
     */
    public function end()
    {
213
        return end($this->_data);
214
    }
215
    
216 217 218 219 220 221 222
    /**
     * returns the current key
     *
     * @return mixed
     */
    public function key()
    {
223
        return key($this->_data);
224
    }
225
    
lsmith's avatar
lsmith committed
226
    /**
227
     * INTERNAL:
228
     * Sets the collection owner. Used (only?) during hydration.
lsmith's avatar
lsmith committed
229 230 231
     *
     * @return void
     */
232
    public function _setOwner(Doctrine_Entity $entity, Doctrine_Association $relation)
lsmith's avatar
lsmith committed
233
    {
234
        $this->_owner = $entity;
235
        $this->_association = $relation;
236 237 238 239 240 241 242 243 244
        if ($relation->isInverseSide()) {
            // for sure bidirectional
            $this->_backRefFieldName = $relation->getMappedByFieldName();
        } else {
            $targetClass = $this->_em->getClassMetadata($relation->getTargetEntityName());
            if ($targetClass->hasInverseAssociationMapping($relation->getSourceFieldName())) {
                // bidirectional
                $this->_backRefFieldName = $targetClass->getInverseAssociationMapping(
                        $relation->getSourceFieldName())->getSourceFieldName();
lsmith's avatar
lsmith committed
245
            }
246
        }
lsmith's avatar
lsmith committed
247
    }
248

lsmith's avatar
lsmith committed
249
    /**
250
     * INTERNAL:
lsmith's avatar
lsmith committed
251 252 253 254
     * getReference
     *
     * @return mixed
     */
255
    public function _getOwner()
lsmith's avatar
lsmith committed
256
    {
257
        return $this->_owner;
lsmith's avatar
lsmith committed
258
    }
259

lsmith's avatar
lsmith committed
260
    /**
261
     * Removes an entity from the collection.
lsmith's avatar
lsmith committed
262 263 264 265 266 267
     *
     * @param mixed $key
     * @return boolean
     */
    public function remove($key)
    {
268 269
        $removed = $this->_data[$key];
        unset($this->_data[$key]);
romanb's avatar
romanb committed
270
        //TODO: Register collection as dirty with the UoW if necessary
271 272 273 274 275 276
        //$this->_em->getUnitOfWork()->scheduleCollectionUpdate($this);
        //TODO: delete entity if shouldDeleteOrphans
        /*if ($this->_association->isOneToMany() && $this->_association->shouldDeleteOrphans()) {
            $this->_em->delete($removed);
        }*/
        
lsmith's avatar
lsmith committed
277 278
        return $removed;
    }
romanb's avatar
romanb committed
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
    
    /**
     * __isset()
     *
     * @param string $name
     * @return boolean          whether or not this object contains $name
     */
    public function __isset($key)
    {
        return $this->containsKey($key);
    }
    
    /**
     * __unset()
     *
     * @param string $name
     * @since 1.0
296
     * @return mixed
romanb's avatar
romanb committed
297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357
     */
    public function __unset($key)
    {
        return $this->remove($key);
    }
    
    /**
     * Check if an offsetExists.
     * 
     * Part of the ArrayAccess implementation.
     *
     * @param mixed $offset
     * @return boolean          whether or not this object contains $offset
     */
    public function offsetExists($offset)
    {
        return $this->containsKey($offset);
    }

    /**
     * offsetGet    an alias of get()
     * 
     * Part of the ArrayAccess implementation.
     *
     * @see get,  __get
     * @param mixed $offset
     * @return mixed
     */
    public function offsetGet($offset)
    {
        return $this->get($offset);
    }

    /**
     * Part of the ArrayAccess implementation.
     * 
     * sets $offset to $value
     * @see set,  __set
     * @param mixed $offset
     * @param mixed $value
     * @return void
     */
    public function offsetSet($offset, $value)
    {
        if ( ! isset($offset)) {
            return $this->add($value);
        }
        return $this->set($offset, $value);
    }

    /**
     * Part of the ArrayAccess implementation.
     * 
     * unset a given offset
     * @see set, offsetSet, __set
     * @param mixed $offset
     */
    public function offsetUnset($offset)
    {
        return $this->remove($offset);
    }
358

lsmith's avatar
lsmith committed
359
    /**
360
     * Checks whether the collection contains an entity.
lsmith's avatar
lsmith committed
361 362 363 364
     *
     * @param mixed $key                    the key of the element
     * @return boolean
     */
romanb's avatar
romanb committed
365
    public function containsKey($key)
lsmith's avatar
lsmith committed
366
    {
367
        return isset($this->_data[$key]);
lsmith's avatar
lsmith committed
368
    }
369
    
romanb's avatar
romanb committed
370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
    /**
     * Enter description here...
     *
     * @param unknown_type $entity
     * @return unknown
     */
    public function contains($entity)
    {
        return in_array($entity, $this->_data, true);
    }
    
    /**
     * Enter description here...
     *
     * @param unknown_type $otherColl
     * @todo Impl
     */
    public function containsAll($otherColl)
    {
        //...
    }
    
392 393 394
    /**
     *
     */
395
    public function search(Doctrine_Entity $record)
zYne's avatar
zYne committed
396
    {
397
        return array_search($record, $this->_data, true);
zYne's avatar
zYne committed
398
    }
399

lsmith's avatar
lsmith committed
400 401 402 403 404 405
    /**
     * returns a record for given key
     *
     * Collection also maps referential information to newly created records
     *
     * @param mixed $key                    the key of the element
406
     * @return Doctrine_Entity              return a specified record
lsmith's avatar
lsmith committed
407 408 409
     */
    public function get($key)
    {
410 411
        if (isset($this->_data[$key])) {
            return $this->_data[$key];
412
        }
413
        return null;
lsmith's avatar
lsmith committed
414 415 416
    }

    /**
romanb's avatar
romanb committed
417 418 419 420
     * Gets all keys.
     * (Map method)
     * 
     * @return array
lsmith's avatar
lsmith committed
421
     */
romanb's avatar
romanb committed
422
    public function getKeys()
lsmith's avatar
lsmith committed
423
    {
romanb's avatar
romanb committed
424
        return array_keys($this->_data);
lsmith's avatar
lsmith committed
425
    }
romanb's avatar
romanb committed
426
    
lsmith's avatar
lsmith committed
427
    /**
romanb's avatar
romanb committed
428 429 430
     * Gets all values.
     * (Map method)
     *
lsmith's avatar
lsmith committed
431 432
     * @return array
     */
romanb's avatar
romanb committed
433
    public function getValues()
lsmith's avatar
lsmith committed
434
    {
romanb's avatar
romanb committed
435
        return array_values($this->_data);
lsmith's avatar
lsmith committed
436
    }
437

lsmith's avatar
lsmith committed
438
    /**
439 440 441
     * Returns the number of records in this collection.
     *
     * Implementation of the Countable interface.
lsmith's avatar
lsmith committed
442
     *
443
     * @return integer  The number of records in the collection.
lsmith's avatar
lsmith committed
444 445 446
     */
    public function count()
    {
447
        return count($this->_data);
lsmith's avatar
lsmith committed
448
    }
449

lsmith's avatar
lsmith committed
450
    /**
romanb's avatar
romanb committed
451 452 453
     * 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
454
     * @param integer $key
romanb's avatar
romanb committed
455
     * @param mixed $value
lsmith's avatar
lsmith committed
456 457
     * @return void
     */
458
    public function set($key, $value)
lsmith's avatar
lsmith committed
459
    {
460
        if ( ! $value instanceof Doctrine_Entity) {
461
            throw new Doctrine_Collection_Exception('Value variable in set is not an instance of Doctrine_Entity');
462
        }
463
        $this->_data[$key] = $value;
romanb's avatar
romanb committed
464 465
        //TODO: Register collection as dirty with the UoW if necessary
        $this->_changed();
lsmith's avatar
lsmith committed
466
    }
467

lsmith's avatar
lsmith committed
468
    /**
romanb's avatar
romanb committed
469
     * Adds an entry to the collection.
470
     * 
romanb's avatar
romanb committed
471 472
     * @param mixed $value
     * @param string $key 
lsmith's avatar
lsmith committed
473 474
     * @return boolean
     */
475
    public function add($value, $key = null)
lsmith's avatar
lsmith committed
476
    {
477
        //TODO: really only allow entities?
478
        if ( ! $value instanceof Doctrine_Entity) {
479
            throw new Doctrine_Record_Exception('Value variable in collection is not an instance of Doctrine_Entity.');
480
        }
481
        
482
        // TODO: Really prohibit duplicates?
romanb's avatar
romanb committed
483 484
        if (in_array($value, $this->_data, true)) {
            return false;
lsmith's avatar
lsmith committed
485 486 487
        }

        if (isset($key)) {
488
            if (isset($this->_data[$key])) {
lsmith's avatar
lsmith committed
489 490
                return false;
            }
491
            $this->_data[$key] = $value;
romanb's avatar
romanb committed
492 493
        } else {
            $this->_data[] = $value;
lsmith's avatar
lsmith committed
494
        }
romanb's avatar
romanb committed
495
        
496 497 498 499 500 501 502 503 504
        if ($this->_hydrationFlag) {
            if ($this->_backRefFieldName) {
                // set back reference to owner
                $value->_internalSetReference($this->_backRefFieldName, $this->_owner);
            }
        } else {
            //TODO: Register collection as dirty with the UoW if necessary
            $this->_changed();
        }
505
        
lsmith's avatar
lsmith committed
506 507
        return true;
    }
romanb's avatar
romanb committed
508 509 510 511 512 513 514 515 516 517 518 519 520
    
    /**
     * Adds all entities of the other collection to this collection.
     *
     * @param unknown_type $otherCollection
     * @todo Impl
     */
    public function addAll($otherCollection)
    {
        //...
        //TODO: Register collection as dirty with the UoW if necessary
        //$this->_changed();
    }
521

lsmith's avatar
lsmith committed
522
    /**
523
     * INTERNAL:
lsmith's avatar
lsmith committed
524 525 526 527
     * loadRelated
     *
     * @param mixed $name
     * @return boolean
romanb's avatar
romanb committed
528
     * @todo New implementation & maybe move elsewhere.
lsmith's avatar
lsmith committed
529
     */
530
    /*public function loadRelated($name = null)
lsmith's avatar
lsmith committed
531
    {
runa's avatar
runa committed
532
        $list = array();
533
        $query = new Doctrine_Query($this->_mapper->getConnection());
lsmith's avatar
lsmith committed
534 535

        if ( ! isset($name)) {
536
            foreach ($this->_data as $record) {
537 538 539
                // FIXME: composite key support
                $ids = $record->identifier();
                $value = count($ids) > 0 ? array_pop($ids) : null;
lsmith's avatar
lsmith committed
540 541 542
                if ($value !== null) {
                    $list[] = $value;
                }
543
            }
544 545 546 547
            $query->from($this->_mapper->getComponentName()
                    . '(' . implode(", ",$this->_mapper->getTable()->getPrimaryKeys()) . ')');
            $query->where($this->_mapper->getComponentName()
                    . '.id IN (' . substr(str_repeat("?, ", count($list)),0,-2) . ')');
lsmith's avatar
lsmith committed
548 549 550 551

            return $query;
        }

552
        $rel = $this->_mapper->getTable()->getRelation($name);
lsmith's avatar
lsmith committed
553 554

        if ($rel instanceof Doctrine_Relation_LocalKey || $rel instanceof Doctrine_Relation_ForeignKey) {
555
            foreach ($this->_data as $record) {
556 557
                $list[] = $record[$rel->getLocal()];
            }
lsmith's avatar
lsmith committed
558
        } else {
559
            foreach ($this->_data as $record) {
560 561
                $ids = $record->identifier();
                $value = count($ids) > 0 ? array_pop($ids) : null;
lsmith's avatar
lsmith committed
562 563 564
                if ($value !== null) {
                    $list[] = $value;
                }
565
            }
lsmith's avatar
lsmith committed
566
        }
567

568 569
        $dql = $rel->getRelationDql(count($list), 'collection');
        $coll = $query->query($dql, $list);
lsmith's avatar
lsmith committed
570 571

        $this->populateRelated($name, $coll);
572
    }*/
573

lsmith's avatar
lsmith committed
574
    /**
575
     * INTERNAL:
lsmith's avatar
lsmith committed
576 577 578 579 580
     * populateRelated
     *
     * @param string $name
     * @param Doctrine_Collection $coll
     * @return void
romanb's avatar
romanb committed
581
     * @todo New implementation & maybe move elsewhere.
lsmith's avatar
lsmith committed
582
     */
583
    /*protected function populateRelated($name, Doctrine_Collection $coll)
lsmith's avatar
lsmith committed
584
    {
585
        $rel     = $this->_mapper->getTable()->getRelation($name);
lsmith's avatar
lsmith committed
586 587 588 589 590
        $table   = $rel->getTable();
        $foreign = $rel->getForeign();
        $local   = $rel->getLocal();

        if ($rel instanceof Doctrine_Relation_LocalKey) {
591
            foreach ($this->_data as $key => $record) {
lsmith's avatar
lsmith committed
592 593
                foreach ($coll as $k => $related) {
                    if ($related[$foreign] == $record[$local]) {
594
                        $this->_data[$key]->_setRelated($name, $related);
lsmith's avatar
lsmith committed
595 596 597
                    }
                }
            }
598
        } else if ($rel instanceof Doctrine_Relation_ForeignKey) {
599
            foreach ($this->_data as $key => $record) {
zYne's avatar
zYne committed
600
                if ( ! $record->exists()) {
lsmith's avatar
lsmith committed
601 602
                    continue;
                }
603
                $sub = new Doctrine_Collection($rel->getForeignComponentName());
lsmith's avatar
lsmith committed
604 605 606 607 608 609 610 611

                foreach ($coll as $k => $related) {
                    if ($related[$foreign] == $record[$local]) {
                        $sub->add($related);
                        $coll->remove($k);
                    }
                }

612
                $this->_data[$key]->_setRelated($name, $sub);
lsmith's avatar
lsmith committed
613
            }
614
        } else if ($rel instanceof Doctrine_Relation_Association) {
615 616
            // @TODO composite key support
            $identifier = (array)$this->_mapper->getClassMetadata()->getIdentifier();
lsmith's avatar
lsmith committed
617 618 619
            $asf        = $rel->getAssociationFactory();
            $name       = $table->getComponentName();

620
            foreach ($this->_data as $key => $record) {
zYne's avatar
zYne committed
621
                if ( ! $record->exists()) {
lsmith's avatar
lsmith committed
622 623
                    continue;
                }
624
                $sub = new Doctrine_Collection($rel->getForeignComponentName());
lsmith's avatar
lsmith committed
625
                foreach ($coll as $k => $related) {
626 627
                    $idField = $identifier[0];
                    if ($related->get($local) == $record[$idField]) {
lsmith's avatar
lsmith committed
628 629 630
                        $sub->add($related->get($name));
                    }
                }
631
                $this->_data[$key]->_setRelated($name, $sub);
lsmith's avatar
lsmith committed
632 633 634

            }
        }
635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651
    }*/
    
    /**
     * INTERNAL:
     * Sets a flag that indicates whether the collection is currently being hydrated.
     * This has the following consequences:
     * 1) During hydration, bidirectional associations are completed automatically
     *    by setting the back reference.
     * 2) During hydration no change notifications are reported to the UnitOfWork.
     *    I.e. that means add() etc. do not cause the collection to be scheduled
     *    for an update.
     *
     * @param boolean $bool
     */
    public function _setHydrationFlag($bool)
    {
        $this->_hydrationFlag = $bool;
lsmith's avatar
lsmith committed
652
    }
653

lsmith's avatar
lsmith committed
654
    /**
romanb's avatar
romanb committed
655
     * INTERNAL: Takes a snapshot from this collection.
zYne's avatar
zYne committed
656
     *
romanb's avatar
romanb committed
657
     * Snapshots are used for diff processing, for example
zYne's avatar
zYne committed
658
     * when a fetched collection has three elements, then two of those
romanb's avatar
romanb committed
659
     * are being removed the diff would contain one element.
zYne's avatar
zYne committed
660
     *
romanb's avatar
romanb committed
661 662 663
     * Collection::save() attaches the diff with the help of last snapshot.
     * 
     * @return void
zYne's avatar
zYne committed
664
     */
665
    public function _takeSnapshot()
zYne's avatar
zYne committed
666
    {
667
        $this->_snapshot = $this->_data;
zYne's avatar
zYne committed
668
    }
669

zYne's avatar
zYne committed
670
    /**
romanb's avatar
romanb committed
671
     * INTERNAL: Returns the data of the last snapshot.
zYne's avatar
zYne committed
672 673 674
     *
     * @return array    returns the data in last snapshot
     */
675
    public function _getSnapshot()
zYne's avatar
zYne committed
676 677 678
    {
        return $this->_snapshot;
    }
679

zYne's avatar
zYne committed
680
    /**
681
     * INTERNAL: Processes the difference of the last snapshot and the current data.
zYne's avatar
zYne committed
682 683 684 685 686
     *
     * an example:
     * Snapshot with the objects 1, 2 and 4
     * Current data with objects 2, 3 and 5
     *
687
     * The process would remove objects 1 and 4
zYne's avatar
zYne committed
688 689
     *
     * @return Doctrine_Collection
690
     * @todo Move elsewhere
zYne's avatar
zYne committed
691 692 693
     */
    public function processDiff() 
    {
694
        foreach (array_udiff($this->_snapshot, $this->_data, array($this, "_compareRecords")) as $record) {
zYne's avatar
zYne committed
695 696 697 698
            $record->delete();
        }
        return $this;
    }
699

700
    /**
701
     * Creates an array representation of the collection.
702 703
     *
     * @param boolean $deep
704
     * @return array
705
     */
706
    public function toArray($deep = false, $prefixKey = false)
zYne's avatar
zYne committed
707
    {
708
        $data = array();
Jonathan.Wage's avatar
Jonathan.Wage committed
709
        foreach ($this as $key => $record) {
710 711
            $key = $prefixKey ? get_class($record) . '_' .$key:$key;
            $data[$key] = $record->toArray($deep, $prefixKey);
712
        }
713 714
        
        return $data;
zYne's avatar
zYne committed
715
    }
716
    
romanb's avatar
romanb committed
717 718 719 720 721
    /**
     * Checks whether the collection is empty.
     *
     * @return boolean TRUE if the collection is empty, FALSE otherwise.
     */
722 723 724 725
    public function isEmpty()
    {
        return $this->count() == 0;
    }
726 727

    /**
romanb's avatar
romanb committed
728
     * Populate a Doctrine_Collection from an array of data.
729 730 731 732
     *
     * @param string $array 
     * @return void
     */
733
    public function fromArray($array, $deep = true)
734 735
    {
        $data = array();
736
        foreach ($array as $rowKey => $row) {
737
            $this[$rowKey]->fromArray($row, $deep);
738 739
        }
    }
740

741
    /**
romanb's avatar
romanb committed
742
     * Synchronizes a Doctrine_Collection with data from an array.
743 744 745 746 747 748 749
     *
     * it expects an array representation of a Doctrine_Collection similar to the return
     * value of the toArray() method. It will create Dectrine_Records that don't exist
     * on the collection, update the ones that do and remove the ones missing in the $array
     *
     * @param array $array representation of a Doctrine_Collection
     */
jwage's avatar
jwage committed
750
    public function synchronizeFromArray(array $array)
751 752 753
    {
        foreach ($this as $key => $record) {
            if (isset($array[$key])) {
jwage's avatar
jwage committed
754
                $record->synchronizeFromArray($array[$key]);
755 756 757 758 759 760
                unset($array[$key]);
            } else {
                // remove records that don't exist in the array
                $this->remove($key);
            }
        }
jwage's avatar
jwage committed
761

762 763 764 765 766 767
        // create new records for each new row in the array
        foreach ($array as $rowKey => $row) {
            $this[$rowKey]->fromArray($row);
        }
    }

768 769 770 771 772 773
    /**
     * Export a Doctrine_Collection to one of the supported Doctrine_Parser formats
     *
     * @param string $type 
     * @param string $deep 
     * @return void
774
     * @todo Move elsewhere.
775
     */
romanb's avatar
romanb committed
776
    /*public function exportTo($type, $deep = false)
777 778 779 780 781 782
    {
        if ($type == 'array') {
            return $this->toArray($deep);
        } else {
            return Doctrine_Parser::dump($this->toArray($deep, true), $type);
        }
romanb's avatar
romanb committed
783
    }*/
784 785 786 787 788 789 790

    /**
     * Import data to a Doctrine_Collection from one of the supported Doctrine_Parser formats
     *
     * @param string $type 
     * @param string $data 
     * @return void
791
     * @todo Move elsewhere.
792
     */
romanb's avatar
romanb committed
793
    /*public function importFrom($type, $data)
794 795 796 797 798 799
    {
        if ($type == 'array') {
            return $this->fromArray($data);
        } else {
            return $this->fromArray(Doctrine_Parser::load($data, $type));
        }
romanb's avatar
romanb committed
800
    }*/
801 802

    /**
romanb's avatar
romanb committed
803
     * INTERNAL: getDeleteDiff
804
     *
romanb's avatar
romanb committed
805
     * @return array
806
     */
zYne's avatar
zYne committed
807 808
    public function getDeleteDiff()
    {
809
        return array_udiff($this->_snapshot, $this->_data, array($this, "_compareRecords"));
zYne's avatar
zYne committed
810
    }
811 812

    /**
romanb's avatar
romanb committed
813
     * INTERNAL getInsertDiff
814
     *
romanb's avatar
romanb committed
815
     * @return array
816
     */
zYne's avatar
zYne committed
817 818
    public function getInsertDiff()
    {
819
        return array_udiff($this->_data, $this->_snapshot, array($this, "_compareRecords"));
820
    }
821

822
    /**
823
     * Compares two records. To be used on _snapshot diffs using array_udiff.
romanb's avatar
romanb committed
824 825
     * 
     * @return integer
826
     */
827
    protected function _compareRecords($a, $b)
828
    {
829 830 831
        if ($a->getOid() == $b->getOid()) {
            return 0;
        }
832
        return ($a->getOid() > $b->getOid()) ? 1 : -1;
zYne's avatar
zYne committed
833
    }
834

lsmith's avatar
lsmith committed
835
    /**
836 837
     * Saves all records of this collection and processes the 
     * difference of the last snapshot and the current data.
lsmith's avatar
lsmith committed
838
     *
839
     * @param Doctrine_Connection $conn     optional connection parameter
zYne's avatar
zYne committed
840
     * @return Doctrine_Collection
lsmith's avatar
lsmith committed
841
     */
romanb's avatar
romanb committed
842
    /*public function save()
lsmith's avatar
lsmith committed
843
    {
844
        $conn = $this->_mapper->getConnection();
845
        
846 847 848 849 850 851 852 853 854 855 856 857 858
        try {
            $conn->beginInternalTransaction();
            
            $conn->transaction->addCollection($this);
            $this->processDiff();
            foreach ($this->getData() as $key => $record) {
                $record->save($conn);
            }
            
            $conn->commit();
        } catch (Exception $e) {
            $conn->rollback();
            throw $e;
zYne's avatar
zYne committed
859
        }
lsmith's avatar
lsmith committed
860

zYne's avatar
zYne committed
861
        return $this;
romanb's avatar
romanb committed
862
    }*/
863

lsmith's avatar
lsmith committed
864
    /**
865
     * Deletes all records from the collection.
romanb's avatar
romanb committed
866
     * Shorthand for calling delete() for all entities in the collection.
lsmith's avatar
lsmith committed
867
     *
868
     * @return void
lsmith's avatar
lsmith committed
869
     */
romanb's avatar
romanb committed
870
    /*public function delete()
871 872
    {  
        $conn = $this->_mapper->getConnection();
lsmith's avatar
lsmith committed
873

874 875 876 877 878 879 880
        try {
            $conn->beginInternalTransaction();
            
            $conn->transaction->addCollection($this);
            foreach ($this as $key => $record) {
                $record->delete($conn);
            }
lsmith's avatar
lsmith committed
881

882 883 884 885
            $conn->commit();            
        } catch (Exception $e) {
            $conn->rollback();
            throw $e;
lsmith's avatar
lsmith committed
886
        }
887
        
romanb's avatar
romanb committed
888 889
        $this->clear();
    }*/
890

891 892 893 894 895 896 897 898 899

    public function free($deep = false)
    {
        foreach ($this->getData() as $key => $record) {
            if ( ! ($record instanceof Doctrine_Null)) {
                $record->free($deep);
            }
        }

900
        $this->_data = array();
901

902 903 904
        if ($this->_owner) {
            $this->_owner->free($deep);
            $this->_owner = null;
905 906 907 908
        }
    }


lsmith's avatar
lsmith committed
909 910
    /**
     * getIterator
911
     * 
lsmith's avatar
lsmith committed
912 913 914 915
     * @return object ArrayIterator
     */
    public function getIterator()
    {
916
        $data = $this->_data;
lsmith's avatar
lsmith committed
917 918
        return new ArrayIterator($data);
    }
919

lsmith's avatar
lsmith committed
920 921 922 923 924 925 926
    /**
     * returns a string representation of this object
     */
    public function __toString()
    {
        return Doctrine_Lib::getCollectionAsString($this);
    }
927 928
    
    /**
929
     * INTERNAL: Gets the association mapping of the collection.
romanb's avatar
romanb committed
930 931
     * 
     * @return Doctrine::ORM::Mapping::AssociationMapping
932
     */
romanb's avatar
romanb committed
933
    public function getMapping()
934 935 936
    {
        return $this->relation;
    }
937
    
romanb's avatar
romanb committed
938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955
    /**
     * @todo Experiment. Waiting for 5.3 closures.
     * Example usage:
     * 
     * $map = $coll->mapElements(function($key, $entity) {
     *     return array($entity->id, $entity->name);
     * });
     * 
     * or:
     * 
     * $map = $coll->mapElements(function($key, $entity) {
     *     return array($entity->name, strtoupper($entity->name));
     * });
     * 
     */
    public function mapElements($lambda) {
        $result = array();
        foreach ($this->_data as $key => $entity) {
956
            list($key, $value) = each($lambda($key, $entity));
romanb's avatar
romanb committed
957 958 959 960 961 962 963 964 965 966
            $result[$key] = $value;
        }
        return $result;
    }
    
    /**
     * Clears the collection.
     *
     * @return void
     */
967 968
    public function clear()
    {
romanb's avatar
romanb committed
969
        //TODO: Register collection as dirty with the UoW if necessary
970 971 972 973 974 975 976
        //TODO: If oneToMany() && shouldDeleteOrphan() delete entities
        /*if ($this->_association->isOneToMany() && $this->_association->shouldDeleteOrphans()) {
            foreach ($this->_data as $entity) {
                $this->_em->delete($entity);
            }
        }*/
        $this->_data = array();
romanb's avatar
romanb committed
977 978 979 980 981 982
    }
    
    private function _changed()
    {
        /*if ( ! $this->_em->getUnitOfWork()->isCollectionScheduledForUpdate($this)) {
            $this->_em->getUnitOfWork()->scheduleCollectionUpdate($this);
983
        }*/  
984
    }
985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020
    
    /* Serializable implementation */
    
    /**
     * Serializes the collection.
     * This method is automatically called when the Collection is serialized.
     *
     * Part of the implementation of the Serializable interface.
     *
     * @return array
     */
    public function serialize()
    {
        $vars = get_object_vars($this);

        unset($vars['reference']);
        unset($vars['relation']);
        unset($vars['expandable']);
        unset($vars['expanded']);
        unset($vars['generator']);

        return serialize($vars);
    }

    /**
     * Reconstitutes the collection object from it's serialized form.
     * This method is automatically called everytime the Collection object is unserialized.
     *
     * Part of the implementation of the Serializable interface.
     *
     * @param string $serialized The serialized data
     *
     * @return void
     */
    public function unserialize($serialized)
    {
romanb's avatar
romanb committed
1021
        $manager = Doctrine_EntityManager::getActiveEntityManager();
1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035
        $connection = $manager->getConnection();
        
        $array = unserialize($serialized);

        foreach ($array as $name => $values) {
            $this->$name = $values;
        }

        $keyColumn = isset($array['keyField']) ? $array['keyField'] : null;

        if ($keyColumn !== null) {
            $this->_keyField = $keyColumn;
        }
    }
1036
}