UnitOfWork.php 56.5 KB
Newer Older
1 2
<?php
/*
romanb's avatar
romanb committed
3
 *  $Id: UnitOfWork.php 4947 2008-09-12 13:16:05Z romanb $
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>.
20
 */
21

22
namespace Doctrine\ORM;
23

24
use Doctrine\Common\Collections\Collection;
25
use Doctrine\Common\DoctrineException;
26
use Doctrine\Common\PropertyChangedListener;
27 28
use Doctrine\ORM\Internal\CommitOrderCalculator;
use Doctrine\ORM\Internal\CommitOrderNode;
29
use Doctrine\ORM\PersistentCollection;
30 31
use Doctrine\ORM\Mapping;
use Doctrine\ORM\Persisters;
32
use Doctrine\ORM\EntityManager;
33

34
/**
35
 * The UnitOfWork is responsible for tracking changes to objects during an
36
 * "object-level" transaction and for writing out changes to the database
37
 * in the correct order.
38 39
 *
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
40
 * @link        www.doctrine-project.org
41
 * @since       2.0
42
 * @version     $Revision$
43
 * @author      Roman Borschel <roman@code-factory.org>
44 45
 * @internal    This class contains performance-critical code. Work with care and
 *              regularly run the ORM performance tests.
46
 */
47
class UnitOfWork implements PropertyChangedListener
48
{
49
    /**
50
     * An entity is in managed state when it has a primary key/identifier (and
51 52 53 54 55 56 57 58
     * therefore persistent state) and is managed by an EntityManager
     * (registered in the identity map).
     * In MANAGED state the entity is associated with an EntityManager that manages
     * the persistent state of the Entity.
     */
    const STATE_MANAGED = 1;

    /**
59
     * An entity is new if it does not yet have an identifier/primary key
60 61 62 63 64
     * and is not (yet) managed by an EntityManager.
     */
    const STATE_NEW = 2;

    /**
65
     * A detached entity is an instance with a persistent identity that is not
66
     * (or no longer) associated with an EntityManager (and a UnitOfWork).
67
     * This means it is no longer in the identity map.
68 69 70 71
     */
    const STATE_DETACHED = 3;

    /**
72
     * A removed entity instance is an instance with a persistent identity,
73 74 75 76 77
     * associated with an EntityManager, whose persistent state has been
     * deleted (or is scheduled for deletion).
     */
    const STATE_DELETED = 4;

78 79
    /**
     * The identity map that holds references to all managed entities that have
80
     * an identity. The entities are grouped by their class name.
81 82
     * Since all classes in a hierarchy must share the same identifier set,
     * we always take the root class name of the hierarchy.
83
     *
84
     * @var array
85
     */
86
    private $_identityMap = array();
87

88 89 90 91 92 93 94
    /**
     * Map of all identifiers. Keys are object ids.
     *
     * @var array
     */
    private $_entityIdentifiers = array();

95 96 97
    /**
     * Map of the original entity data of entities fetched from the database.
     * Keys are object ids. This is used for calculating changesets at commit time.
98
     * Note that PHPs "copy-on-write" behavior helps a lot with memory usage.
99 100 101
     *
     * @var array
     */
102
    private $_originalEntityData = array();
103 104 105

    /**
     * Map of data changes. Keys are object ids.
106
     * Filled at the beginning of a commit of the UnitOfWork and cleaned at the end.
107 108 109
     *
     * @var array
     */
110
    private $_entityChangeSets = array();
111 112 113 114 115 116

    /**
     * The states of entities in this UnitOfWork.
     *
     * @var array
     */
117
    private $_entityStates = array();
118

119 120 121 122
    /**
     * Map of entities that are scheduled for dirty checking at commit time.
     * This is only used if automatic dirty checking is disabled.
     */
123
    private $_scheduledForDirtyCheck = array();
124

125
    /**
126
     * A list of all pending entity insertions.
127 128
     *
     * @var array
129
     */
130
    private $_entityInsertions = array();
131

132
    /**
133
     * A list of all pending entity updates.
134 135
     *
     * @var array
136
     */
137
    private $_entityUpdates = array();
138

139
    /**
140
     * A list of all pending entity deletions.
141 142
     *
     * @var array
143
     */
144
    private $_entityDeletions = array();
romanb's avatar
romanb committed
145 146
    
    /**
147
     * All pending collection deletions.
romanb's avatar
romanb committed
148 149 150
     *
     * @var array
     */
151
    private $_collectionDeletions = array();
romanb's avatar
romanb committed
152 153
    
    /**
154
     * All pending collection creations.
romanb's avatar
romanb committed
155 156 157
     *
     * @var array
     */
158
    private $_collectionCreations = array();
romanb's avatar
romanb committed
159 160 161 162 163 164
    
    /**
     * All collection updates.
     *
     * @var array
     */
165 166 167 168 169 170 171 172 173 174
    private $_collectionUpdates = array();

    /**
     * List of collections visited during a commit-phase of a UnitOfWork.
     * At the end of the UnitOfWork all these collections will make new snapshots
     * of their data.
     *
     * @var array
     */
    private $_visitedCollections = array();
175

176
    /**
177
     * The EntityManager that "owns" this UnitOfWork instance.
178
     *
179
     * @var Doctrine\ORM\EntityManager
180
     */
181
    private $_em;
182

183
    /**
184 185
     * The calculator used to calculate the order in which changes to
     * entities need to be written to the database.
186
     *
187
     * @var Doctrine\ORM\Internal\CommitOrderCalculator
188
     */
189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
    private $_commitOrderCalculator;

    /**
     * The entity persister instances used to persist entity instances.
     *
     * @var array
     */
    private $_persisters = array();

    /**
     * The collection persister instances used to persist collections.
     *
     * @var array
     */
    private $_collectionPersisters = array();
204

205
    /**
206
     * Initializes a new UnitOfWork instance, bound to the given EntityManager.
207
     *
208
     * @param Doctrine\ORM\EntityManager $em
209
     */
210
    public function __construct(EntityManager $em)
211 212
    {
        $this->_em = $em;
213
        //TODO: any benefit with lazy init?
214
        $this->_commitOrderCalculator = new CommitOrderCalculator();
215
    }
216

217
    /**
218
     * Commits the UnitOfWork, executing all operations that have been postponed
219 220
     * up to this point.
     */
221
    public function commit()
222
    {
223 224
        // Compute changes done since last commit.
        // This populates _entityUpdates and _collectionUpdates.
225 226
        $this->computeChangeSets();

227 228 229
        if (empty($this->_entityInsertions) &&
                empty($this->_entityDeletions) &&
                empty($this->_entityUpdates) &&
230 231
                empty($this->_collectionUpdates) &&
                empty($this->_collectionDeletions)) {
232 233 234 235 236 237
            return; // Nothing to do.
        }

        // Now we need a commit order to maintain referential integrity
        $commitOrder = $this->_getCommitOrder();

238 239 240
        $conn = $this->_em->getConnection();
        try {
            $conn->beginTransaction();
241

242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
            foreach ($commitOrder as $class) {
                $this->_executeInserts($class);
            }
            foreach ($commitOrder as $class) {
                $this->_executeUpdates($class);
            }

            // Collection deletions (deletions of complete collections)
            foreach ($this->_collectionDeletions as $collectionToDelete) {
                $this->getCollectionPersister($collectionToDelete->getMapping())
                        ->delete($collectionToDelete);
            }
            // Collection updates (deleteRows, updateRows, insertRows)
            foreach ($this->_collectionUpdates as $collectionToUpdate) {
                $this->getCollectionPersister($collectionToUpdate->getMapping())
                        ->update($collectionToUpdate);
            }
            //TODO: collection recreations (insertions of complete collections)

            // Entity deletions come last and need to be in reverse commit order
            for ($count = count($commitOrder), $i = $count - 1; $i >= 0; --$i) {
                $this->_executeDeletions($commitOrder[$i]);
            }
265

266 267 268 269
            $conn->commit();
        } catch (\Exception $e) {
            $conn->rollback();
            throw $e;
270 271
        }

272 273
        // Take new snapshots from visited collections
        foreach ($this->_visitedCollections as $coll) {
274
            $coll->takeSnapshot();
275 276
        }

277
        // Clear up
278 279 280
        $this->_entityInsertions = array();
        $this->_entityUpdates = array();
        $this->_entityDeletions = array();
281
        $this->_entityChangeSets = array();
282
        $this->_collectionUpdates = array();
283
        $this->_collectionDeletions = array();
284
        $this->_visitedCollections = array();
285 286 287
    }

    /**
288
     * Gets the changeset for an entity.
289 290 291
     *
     * @return array
     */
292
    public function getEntityChangeSet($entity)
293
    {
294
        $oid = spl_object_hash($entity);
295 296
        if (isset($this->_entityChangeSets[$oid])) {
            return $this->_entityChangeSets[$oid];
297 298 299 300 301
        }
        return array();
    }

    /**
302
     * Computes all the changes that have been done to entities and collections
303 304
     * since the last commit and stores these changes in the _entityChangeSet map
     * temporarily for access by the persisters, until the UoW commit is finished.
305 306 307
     *
     * @param array $entities The entities for which to compute the changesets. If this
     *          parameter is not specified, the changesets of all entities in the identity
308 309 310
     *          map are computed if automatic dirty checking is enabled (the default).
     *          If automatic dirty checking is disabled, only those changesets will be
     *          computed that have been scheduled through scheduleForDirtyCheck().
311
     */
312
    public function computeChangeSets(array $entities = null)
313 314
    {
        $entitySet = array();
315
        $newEntities = array();
316
        if ($entities !== null) {
317
            foreach ($entities as $entity) {
318
                $entitySet[get_class($entity)][] = $entity;
319
            }
320
            $newEntities = $entities;
321 322
        } else {
            $entitySet = $this->_identityMap;
323
            $newEntities = $this->_entityInsertions;
324
        }
325

326 327 328 329
        // Compute changes for NEW entities first. This must always happen.
        foreach ($newEntities as $entity) {
            $this->_computeEntityChanges($this->_em->getClassMetadata(get_class($entity)), $entity);
        }
330

331
        // Compute changes for MANAGED entities. Change tracking policies take effect here.
332 333
        foreach ($entitySet as $className => $entities) {
            $class = $this->_em->getClassMetadata($className);
334

335
            // Skip class if change tracking happens through notification
336 337 338
            if ($class->isChangeTrackingNotify()) {
                continue;
            }
339 340

            // If change tracking is explicit, then only compute changes on explicitly saved entities
341 342 343
            $entitiesToProcess = $class->isChangeTrackingDeferredExplicit() ?
                    $this->_scheduledForDirtyCheck[$className] : $entities;

344
            foreach ($entitiesToProcess as $entity) {
345 346 347 348
                // Only MANAGED entities are processed here.
                if ($this->getEntityState($entity) == self::STATE_MANAGED) {
                    $this->_computeEntityChanges($class, $entity);
                    // Look for changes in associations of the entity
349
                    foreach ($class->associationMappings as $assoc) {
romanb's avatar
romanb committed
350
                        $val = $class->reflFields[$assoc->sourceFieldName]->getValue($entity);
351 352
                        if ($val !== null) {
                            $this->_computeAssociationChanges($assoc, $val);
353
                        }
354
                    }
355 356 357 358
                }
            }
        }
    }
359

360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
    /**
     * Computes the changes done to a single entity.
     *
     * Modifies/populates the following properties:
     *
     * {@link _originalEntityData}
     * If the entity is NEW or MANAGED but not yet fully persisted (only has an id)
     * then it was not fetched from the database and therefore we have no original
     * entity data yet. All of the current entity data is stored as the original entity data.
     *
     * {@link _entityChangeSets}
     * The changes detected on all properties of the entity are stored there.
     * A change is a tuple array where the first entry is the old value and the second
     * entry is the new value of the property. Changesets are used by persisters
     * to INSERT/UPDATE the persistent entity state.
     *
     * {@link _entityUpdates}
     * If the entity is already fully MANAGED (has been fetched from the database before)
     * and any changes to its properties are detected, then a reference to the entity is stored
     * there to mark it for an update.
     *
     * {@link _collectionDeletions}
     * If a PersistentCollection has been de-referenced in a fully MANAGED entity,
     * then this collection is marked for deletion.
     *
     * @param ClassMetadata $class The class descriptor of the entity.
     * @param object $entity The entity for which to compute the changes.
     */
    private function _computeEntityChanges($class, $entity)
    {
        $oid = spl_object_hash($entity);
            
        if ( ! $class->isInheritanceTypeNone()) {
            $class = $this->_em->getClassMetadata(get_class($entity));
        }
        
        $actualData = array();
romanb's avatar
romanb committed
397
        foreach ($class->reflFields as $name => $refProp) {
398 399 400
            if ( ! $class->isIdentifier($name) || ! $class->isIdGeneratorIdentity()) {
                $actualData[$name] = $refProp->getValue($entity);
            }
401

402 403 404 405 406
            if ($class->isCollectionValuedAssociation($name)
                    && $actualData[$name] !== null
                    && ! ($actualData[$name] instanceof PersistentCollection)
                ) {
                //TODO: If $actualData[$name] is Collection then unwrap the array
romanb's avatar
romanb committed
407
                $assoc = $class->associationMappings[$name];
408
                //echo PHP_EOL . "INJECTING PCOLL into $name" . PHP_EOL;
409
                // Inject PersistentCollection
410
                $coll = new PersistentCollection($this->_em, $this->_em->getClassMetadata($assoc->targetEntityName),
411 412
                    $actualData[$name] ? $actualData[$name] : array());
                $coll->setOwner($entity, $assoc);
413 414 415
                if ( ! $coll->isEmpty()) {
                    $coll->setDirty(true);
                }
416
                $class->reflFields[$name]->setValue($entity, $coll);
417 418 419
                $actualData[$name] = $coll;
            }
        }
420

421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439
        if ( ! isset($this->_originalEntityData[$oid])) {
            // Entity is either NEW or MANAGED but not yet fully persisted
            // (only has an id). These result in an INSERT.
            $this->_originalEntityData[$oid] = $actualData;
            $this->_entityChangeSets[$oid] = array_map(
                function($e) { return array(null, $e); },
                $actualData
            );
        } else {
            // Entity is "fully" MANAGED: it was already fully persisted before
            // and we have a copy of the original data
            $originalData = $this->_originalEntityData[$oid];
            $changeSet = array();
            $entityIsDirty = false;

            foreach ($actualData as $propName => $actualValue) {
                $orgValue = isset($originalData[$propName]) ? $originalData[$propName] : null;
                if (is_object($orgValue) && $orgValue !== $actualValue) {
                    $changeSet[$propName] = array($orgValue, $actualValue);
440
                } else if ($orgValue != $actualValue || ($orgValue === null ^ $actualValue === null)) {
441 442 443 444
                    $changeSet[$propName] = array($orgValue, $actualValue);
                }

                if (isset($changeSet[$propName])) {
romanb's avatar
romanb committed
445 446
                    if (isset($class->associationMappings[$propName])) {
                        $assoc = $class->associationMappings[$propName];
447
                        if ($assoc->isOneToOne() && $assoc->isOwningSide) {
448 449 450 451 452
                            $entityIsDirty = true;
                        } else if ($orgValue instanceof PersistentCollection) {
                            // A PersistentCollection was de-referenced, so delete it.
                            if  ( ! in_array($orgValue, $this->_collectionDeletions, true)) {
                                $this->_collectionDeletions[] = $orgValue;
453 454
                            }
                        }
455 456
                    } else {
                        $entityIsDirty = true;
457
                    }
458 459
                }
            }
460 461 462 463 464 465 466
            if ($changeSet) {
                if ($entityIsDirty) {
                    $this->_entityUpdates[$oid] = $entity;
                }
                $this->_entityChangeSets[$oid] = $changeSet;
                $this->_originalEntityData[$oid] = $actualData;
            }
467
        }
468
    }
469

470
    /**
471
     * Computes the changes of an association.
472
     *
473 474
     * @param AssociationMapping $assoc
     * @param mixed $value The value of the association.
475
     */
476
    private function _computeAssociationChanges($assoc, $value)
477
    {
478
        if ($value instanceof PersistentCollection && $value->isDirty()) {
romanb's avatar
romanb committed
479
            if ($assoc->isOwningSide) {
480 481 482 483
                $this->_collectionUpdates[] = $value;
            }
            $this->_visitedCollections[] = $value;
        }
484

romanb's avatar
romanb committed
485
        if ( ! $assoc->isCascadeSave) {
486
            //echo "NOT CASCADING INTO " . $assoc->getSourceFieldName() . PHP_EOL;
487 488 489 490 491
            return; // "Persistence by reachability" only if save cascade specified
        }

        // Look through the entities, and in any of their associations, for transient
        // enities, recursively. ("Persistence by reachability")
492 493 494
        if ($assoc->isOneToOne()) {
            $value = array($value);
        }
romanb's avatar
romanb committed
495
        $targetClass = $this->_em->getClassMetadata($assoc->targetEntityName);
496 497 498 499 500
        foreach ($value as $entry) {
            $state = $this->getEntityState($entry);
            $oid = spl_object_hash($entry);
            if ($state == self::STATE_NEW) {
                // Get identifier, if possible (not post-insert)
501
                $idGen = $targetClass->getIdGenerator();
502
                if ( ! $idGen->isPostInsertGenerator()) {
503
                    $idValue = $idGen->generate($this->_em, $entry);
504
                    $this->_entityStates[$oid] = self::STATE_MANAGED;
505
                    if ( ! $idGen instanceof \Doctrine\ORM\Id\Assigned) {
506 507 508 509 510 511 512 513
                        $this->_entityIdentifiers[$oid] = array($idValue);
                        $targetClass->getSingleIdReflectionProperty()->setValue($entry, $idValue);
                    } else {
                        $this->_entityIdentifiers[$oid] = $idValue;
                    }
                    $this->addToIdentityMap($entry);
                }

514
                // Collect the original data and changeset, recursing into associations.
515
                $data = array();
516
                $changeSet = array();
romanb's avatar
romanb committed
517
                foreach ($targetClass->reflFields as $name => $refProp) {
518
                    $data[$name] = $refProp->getValue($entry);
519
                    $changeSet[$name] = array(null, $data[$name]);
romanb's avatar
romanb committed
520
                    if (isset($targetClass->associationMappings[$name])) {
521 522
                        //echo "RECURSING INTO $name" . PHP_EOL;
                        //TODO: Prevent infinite recursion
romanb's avatar
romanb committed
523
                        $this->_computeAssociationChanges($targetClass->associationMappings[$name], $data[$name]);
524
                    }
525
                }
526

527 528
                // NEW entities are INSERTed within the current unit of work.
                $this->_entityInsertions[$oid] = $entry;
529
                $this->_entityChangeSets[$oid] = $changeSet;
530 531
                $this->_originalEntityData[$oid] = $data;
            } else if ($state == self::STATE_DELETED) {
532 533
                throw DoctrineException::updateMe("Deleted entity in collection detected during flush."
                        . " Make sure you properly remove deleted entities from collections.");
534 535 536 537 538 539
            }
            // MANAGED associated entities are already taken into account
            // during changeset calculation anyway, since they are in the identity map.
        }
    }

romanb's avatar
romanb committed
540 541 542
    /**
     * Executes all entity insertions for entities of the specified type.
     *
543
     * @param Doctrine\ORM\Mapping\ClassMetadata $class
romanb's avatar
romanb committed
544
     */
545
    private function _executeInserts($class)
546
    {
547
        $className = $class->name;
548
        $persister = $this->getEntityPersister($className);
549
        foreach ($this->_entityInsertions as $oid => $entity) {
550
            if (get_class($entity) == $className) {
551
                $persister->addInsert($entity);
552
                unset($this->_entityInsertions[$oid]);
553 554 555 556 557 558 559 560 561 562 563 564 565
            }
        }
        $postInsertIds = $persister->executeInserts();
        if ($postInsertIds) {
            foreach ($postInsertIds as $id => $entity) {
                // Persister returned a post-insert ID
                $oid = spl_object_hash($entity);
                $idField = $class->identifier[0];
                $class->reflFields[$idField]->setValue($entity, $id);
                $this->_entityIdentifiers[$oid] = array($id);
                $this->_entityStates[$oid] = self::STATE_MANAGED;
                $this->_originalEntityData[$oid][$idField] = $id;
                $this->addToIdentityMap($entity);
566 567 568
            }
        }
    }
569

romanb's avatar
romanb committed
570 571 572
    /**
     * Executes all entity updates for entities of the specified type.
     *
573
     * @param Doctrine\ORM\Mapping\ClassMetadata $class
romanb's avatar
romanb committed
574
     */
575 576
    private function _executeUpdates($class)
    {
577
        $className = $class->name;
578
        $persister = $this->getEntityPersister($className);
579
        foreach ($this->_entityUpdates as $oid => $entity) {
580
            if (get_class($entity) == $className) {
581
                $persister->update($entity);
582
                unset($this->_entityUpdates[$oid]);
583 584
            }
        }
585
    }
586

romanb's avatar
romanb committed
587 588 589
    /**
     * Executes all entity deletions for entities of the specified type.
     *
590
     * @param Doctrine\ORM\Mapping\ClassMetadata $class
romanb's avatar
romanb committed
591
     */
592 593
    private function _executeDeletions($class)
    {
594
        $className = $class->name;
595
        $persister = $this->getEntityPersister($className);
596
        foreach ($this->_entityDeletions as $oid => $entity) {
597
            if (get_class($entity) == $className) {
598
                $persister->delete($entity);
599
                unset($this->_entityDeletions[$oid]);
600 601 602 603 604 605 606 607 608
            }
        }
    }

    /**
     * Gets the commit order.
     *
     * @return array
     */
romanb's avatar
romanb committed
609
    private function _getCommitOrder(array $entityChangeSet = null)
610
    {
611
        if ($entityChangeSet === null) {
612
            $entityChangeSet = array_merge(
613 614 615
                    $this->_entityInsertions,
                    $this->_entityUpdates,
                    $this->_entityDeletions);
romanb's avatar
romanb committed
616
        }
617 618 619

        // TODO: We can cache computed commit orders in the metadata cache!
        // Check cache at this point here!
620 621 622 623 624
        
        // See if there are any new classes in the changeset, that are not in the
        // commit order graph yet (dont have a node).
        $newNodes = array();
        foreach ($entityChangeSet as $entity) {
625 626
            $className = get_class($entity);
            if ( ! $this->_commitOrderCalculator->hasNodeWithKey($className)) {
627
                $this->_commitOrderCalculator->addNodeWithItem(
628 629
                        $className, // index/key
                        $this->_em->getClassMetadata($className) // item
630
                        );
631
                $newNodes[] = $this->_commitOrderCalculator->getNodeForKey($className);
632 633 634 635 636
            }
        }

        // Calculate dependencies for new nodes
        foreach ($newNodes as $node) {
637
            $class = $node->getClass();
638
            foreach ($class->associationMappings as $assocMapping) {
639
                //TODO: should skip target classes that are not in the changeset.
romanb's avatar
romanb committed
640 641
                if ($assocMapping->isOwningSide) {
                    $targetClass = $this->_em->getClassMetadata($assocMapping->targetEntityName);
642
                    $targetClassName = $targetClass->name;
643
                    // If the target class does not yet have a node, create it
644 645 646 647 648 649 650 651
                    if ( ! $this->_commitOrderCalculator->hasNodeWithKey($targetClassName)) {
                        $this->_commitOrderCalculator->addNodeWithItem(
                                $targetClassName, // index/key
                                $targetClass // item
                                );
                    }
                    // add dependency
                    $otherNode = $this->_commitOrderCalculator->getNodeForKey($targetClassName);
652
                    $otherNode->before($node);
653 654 655 656 657 658 659
                }
            }
        }

        return $this->_commitOrderCalculator->getCommitOrder();
    }

660
    /**
661 662
     * Registers a new entity. The entity will be scheduled for insertion.
     * If the entity already has an identifier, it will be added to the identity map.
663
     * 
664
     * @param object $entity
665
     * @todo Rename to scheduleForInsert().
666
     */
667
    public function registerNew($entity)
668
    {
669
        $oid = spl_object_hash($entity);
670

671
        if (isset($this->_entityUpdates[$oid])) {
672
            throw DoctrineException::updateMe("Dirty object can't be registered as new.");
673
        }
674
        if (isset($this->_entityDeletions[$oid])) {
675
            throw DoctrineException::updateMe("Removed object can't be registered as new.");
676
        }
677
        if (isset($this->_entityInsertions[$oid])) {
678
            throw DoctrineException::updateMe("Object already registered as new. Can't register twice.");
679
        }
680

681
        $this->_entityInsertions[$oid] = $entity;
682
        if (isset($this->_entityIdentifiers[$oid])) {
683 684
            $this->addToIdentityMap($entity);
        }
685
    }
686 687

    /**
688
     * Checks whether an entity is registered as new on this unit of work.
689
     *
690
     * @param object $entity
691 692 693
     * @return boolean
     * @todo Rename to isScheduledForInsert().
     */
694
    public function isRegisteredNew($entity)
695
    {
696
        return isset($this->_entityInsertions[spl_object_hash($entity)]);
697
    }
698

699 700
    /**
     * Registers a dirty entity.
701
     *
702
     * @param object $entity
703
     * @todo Rename to scheduleForUpdate().
704
     */
705
    public function registerDirty($entity)
706
    {
707 708
        $oid = spl_object_hash($entity);
        if ( ! isset($this->_entityIdentifiers[$oid])) {
709
            throw DoctrineException::updateMe("Entity without identity "
710 711
                    . "can't be registered as dirty.");
        }
712
        if (isset($this->_entityDeletions[$oid])) {
713
            throw DoctrineException::updateMe("Removed object can't be registered as dirty.");
714
        }
715

716 717
        if ( ! isset($this->_entityUpdates[$oid]) && ! isset($this->_entityInsertions[$oid])) {
            $this->_entityUpdates[$oid] = $entity;
718
        }
719
    }
720 721 722 723 724 725 726 727 728 729

    /**
     * Checks whether an entity is registered as dirty in the unit of work.
     * Note: Is not very useful currently as dirty entities are only registered
     * at commit time.
     *
     * @param Doctrine_Entity $entity
     * @return boolean
     * @todo Rename to isScheduledForUpdate().
     */
730
    public function isRegisteredDirty($entity)
731
    {
732
        return isset($this->_entityUpdates[spl_object_hash($entity)]);
733
    }
734 735

    /**
736
     * Registers a deleted entity.
737 738
     * 
     * @todo Rename to scheduleForDelete().
739
     */
740
    public function registerDeleted($entity)
741
    {
742
        $oid = spl_object_hash($entity);
743
        if ( ! $this->isInIdentityMap($entity)) {
744 745
            return;
        }
746 747

        $this->removeFromIdentityMap($entity);
748
        $className = get_class($entity);
749

750 751
        if (isset($this->_entityInsertions[$oid])) {
            unset($this->_entityInsertions[$oid]);
752
            return; // entity has not been persisted yet, so nothing more to do.
753
        }
romanb's avatar
romanb committed
754

755 756
        if (isset($this->_entityUpdates[$oid])) {
            unset($this->_entityUpdates[$oid]);
romanb's avatar
romanb committed
757
        }
758 759
        if ( ! isset($this->_entityDeletions[$oid])) {
            $this->_entityDeletions[$oid] = $entity;
760
        }
761 762
    }

763
    /**
764 765
     * Checks whether an entity is registered as removed/deleted with the unit
     * of work.
766
     *
767
     * @param Doctrine\ORM\Entity $entity
768 769
     * @return boolean
     * @todo Rename to isScheduledForDelete().
770
     */
771
    public function isRegisteredRemoved($entity)
772
    {
773
        return isset($this->_entityDeletions[spl_object_hash($entity)]);
774
    }
775

776
    /**
777 778
     * Detaches an entity from the persistence management. It's persistence will
     * no longer be managed by Doctrine.
779
     *
780
     * @param object $entity The entity to detach.
781
     */
782
    public function detach($entity)
783
    {
784 785
        $oid = spl_object_hash($entity);
        $this->removeFromIdentityMap($entity);
786 787
        unset($this->_entityInsertions[$oid], $this->_entityUpdates[$oid],
                $this->_entityDeletions[$oid], $this->_entityIdentifiers[$oid],
788
                $this->_entityStates[$oid]);
789
    }
790

791
    /**
792 793
     * Enter description here...
     *
794 795
     * @param object $entity
     * @return boolean
796
     * @todo Rename to isScheduled()
797
     */
798
    public function isEntityRegistered($entity)
799
    {
800
        $oid = spl_object_hash($entity);
801 802 803
        return isset($this->_entityInsertions[$oid]) ||
                isset($this->_entityUpdates[$oid]) ||
                isset($this->_entityDeletions[$oid]);
804
    }
805

806
    /**
807
     * Detaches all currently managed entities.
808 809 810 811
     * Alternatively, if an entity class name is given, all entities of that type
     * (or subtypes) are detached. Don't forget that entities are registered in
     * the identity map with the name of the root entity class. So calling detachAll()
     * with a class name that is not the name of a root entity has no effect.
812
     *
813
     * @return integer The number of detached entities.
814
     */
815
    public function detachAll($entityName = null)
816
    {
817 818 819
        $numDetached = 0;
        if ($entityName !== null && isset($this->_identityMap[$entityName])) {
            $numDetached = count($this->_identityMap[$entityName]);
820 821 822
            foreach ($this->_identityMap[$entityName] as $entity) {
                $this->detach($entity);
            }
823 824 825 826
            $this->_identityMap[$entityName] = array();
        } else {
            $numDetached = count($this->_identityMap);
            $this->_identityMap = array();
827 828 829
            $this->_entityInsertions = array();
            $this->_entityUpdates = array();
            $this->_entityDeletions = array();
830 831
        }

832
        return $numDetached;
833
    }
834

835
    /**
836
     * Registers an entity in the identity map.
837 838 839
     * Note that entities in a hierarchy are registered with the class name of
     * the root entity.
     *
840
     * @param object $entity  The entity to register.
841 842
     * @return boolean  TRUE if the registration was successful, FALSE if the identity of
     *                  the entity in question is already managed.
843
     */
844
    public function addToIdentityMap($entity)
845
    {
846
        $classMetadata = $this->_em->getClassMetadata(get_class($entity));
847
        $idHash = implode(' ', $this->_entityIdentifiers[spl_object_hash($entity)]);
848
        if ($idHash === '') {
849
            throw DoctrineException::updateMe("Entity with oid '" . spl_object_hash($entity)
850
                    . "' has no identity and therefore can't be added to the identity map.");
851
        }
852
        $className = $classMetadata->rootEntityName;
853
        if (isset($this->_identityMap[$className][$idHash])) {
854 855
            return false;
        }
856
        $this->_identityMap[$className][$idHash] = $entity;
857 858 859
        if ($entity instanceof \Doctrine\Common\NotifyPropertyChanged) {
            $entity->addPropertyChangedListener($this);
        }
860 861
        return true;
    }
862

863 864 865
    /**
     * Gets the state of an entity within the current unit of work.
     *
866
     * @param object $entity
867
     * @return int The entity state.
868 869 870
     */
    public function getEntityState($entity)
    {
871
        $oid = spl_object_hash($entity);
romanb's avatar
romanb committed
872
        if ( ! isset($this->_entityStates[$oid])) {
873 874 875 876 877 878 879
            /*if (isset($this->_entityInsertions[$oid])) {
                $this->_entityStates[$oid] = self::STATE_NEW;
            } else if ( ! isset($this->_entityIdentifiers[$oid])) {
                // Either NEW (if no ID) or DETACHED (if ID)
            } else {
                $this->_entityStates[$oid] = self::STATE_DETACHED;
            }*/
880
            if (isset($this->_entityIdentifiers[$oid]) && ! isset($this->_entityInsertions[$oid])) {
romanb's avatar
romanb committed
881 882 883 884 885 886
                $this->_entityStates[$oid] = self::STATE_DETACHED;
            } else {
                $this->_entityStates[$oid] = self::STATE_NEW;
            }
        }
        return $this->_entityStates[$oid];
887 888
    }

romanb's avatar
romanb committed
889
    /**
romanb's avatar
romanb committed
890 891
     * Removes an entity from the identity map. This effectively detaches the
     * entity from the persistence management of Doctrine.
romanb's avatar
romanb committed
892
     *
893
     * @param object $entity
894
     * @return boolean
romanb's avatar
romanb committed
895
     */
896
    public function removeFromIdentityMap($entity)
897
    {
romanb's avatar
romanb committed
898
        $oid = spl_object_hash($entity);
899
        $classMetadata = $this->_em->getClassMetadata(get_class($entity));
900
        $idHash = implode(' ', $this->_entityIdentifiers[$oid]);
901
        if ($idHash === '') {
902
            throw DoctrineException::updateMe("Entity with oid '" . spl_object_hash($entity)
903
                    . "' has no identity and therefore can't be removed from the identity map.");
904
        }
905
        $className = $classMetadata->rootEntityName;
906 907
        if (isset($this->_identityMap[$className][$idHash])) {
            unset($this->_identityMap[$className][$idHash]);
romanb's avatar
romanb committed
908
            $this->_entityStates[$oid] = self::STATE_DETACHED;
909 910 911 912 913
            return true;
        }

        return false;
    }
914

romanb's avatar
romanb committed
915
    /**
romanb's avatar
romanb committed
916
     * Gets an entity in the identity map by its identifier hash.
romanb's avatar
romanb committed
917
     *
918 919
     * @param string $idHash
     * @param string $rootClassName
920
     * @return object
romanb's avatar
romanb committed
921
     */
922
    public function getByIdHash($idHash, $rootClassName)
923
    {
924 925
        return $this->_identityMap[$rootClassName][$idHash];
    }
926 927 928 929 930

    /**
     * Tries to get an entity by its identifier hash. If no entity is found for
     * the given hash, FALSE is returned.
     *
romanb's avatar
romanb committed
931 932
     * @param string $idHash
     * @param string $rootClassName
933 934
     * @return mixed The found entity or FALSE.
     */
935 936 937 938 939 940 941
    public function tryGetByIdHash($idHash, $rootClassName)
    {
        if ($this->containsIdHash($idHash, $rootClassName)) {
            return $this->getByIdHash($idHash, $rootClassName);
        }
        return false;
    }
942

943
    /**
944
     * Checks whether an entity is registered in the identity map of this UnitOfWork.
945
     *
946
     * @param object $entity
947 948
     * @return boolean
     */
949
    public function isInIdentityMap($entity)
950
    {
951 952 953 954
        $oid = spl_object_hash($entity);
        if ( ! isset($this->_entityIdentifiers[$oid])) {
            return false;
        }
955
        $classMetadata = $this->_em->getClassMetadata(get_class($entity));
956
        $idHash = implode(' ', $this->_entityIdentifiers[$oid]);
957
        if ($idHash === '') {
958 959
            return false;
        }
960
        
romanb's avatar
romanb committed
961
        return isset($this->_identityMap
962
                [$classMetadata->rootEntityName]
963 964
                [$idHash]
                );
965
    }
966

romanb's avatar
romanb committed
967 968 969 970 971 972 973
    /**
     * Checks whether an identifier hash exists in the identity map.
     *
     * @param string $idHash
     * @param string $rootClassName
     * @return boolean
     */
974
    public function containsIdHash($idHash, $rootClassName)
975
    {
976
        return isset($this->_identityMap[$rootClassName][$idHash]);
977
    }
978 979 980 981

    /**
     * Saves an entity as part of the current unit of work.
     *
982
     * @param object $entity The entity to save.
983
     */
984
    public function save($entity)
985 986 987 988 989
    {
        $insertNow = array();
        $visited = array();
        $this->_doSave($entity, $visited, $insertNow);
        if ( ! empty($insertNow)) {
romanb's avatar
romanb committed
990
            // We have no choice. This means that there are new entities
991 992
            // with a post-insert ID generation strategy.
            $this->computeChangeSets($insertNow);
romanb's avatar
romanb committed
993 994 995 996
            $commitOrder = $this->_getCommitOrder($insertNow);
            foreach ($commitOrder as $class) {
                $this->_executeInserts($class);
            }
997 998
            // remove them from _entityInsertions and _entityChangeSets
            $this->_entityInsertions = array_diff_key($this->_entityInsertions, $insertNow);
999
            $this->_entityChangeSets = array_diff_key($this->_entityChangeSets, $insertNow);
1000 1001 1002 1003 1004 1005 1006 1007
        }
    }

    /**
     * Saves an entity as part of the current unit of work.
     * This method is internally called during save() cascades as it tracks
     * the already visited entities to prevent infinite recursions.
     *
1008
     * @param object $entity The entity to save.
romanb's avatar
romanb committed
1009
     * @param array $visited The already visited entities.
1010 1011
     * @param array $insertNow The entities that must be immediately inserted because of
     *                         post-insert ID generation.
1012
     */
1013
    private function _doSave($entity, array &$visited, array &$insertNow)
1014
    {
1015
        $oid = spl_object_hash($entity);
1016
        if (isset($visited[$oid])) {
1017 1018 1019
            return; // Prevent infinite recursion
        }

1020
        $visited[$oid] = $entity; // Mark visited
1021

1022 1023 1024
        $class = $this->_em->getClassMetadata(get_class($entity));
        switch ($this->getEntityState($entity)) {
            case self::STATE_MANAGED:
1025
                // Nothing to do, except if policy is "deferred explicit"
1026
                if ($class->isChangeTrackingDeferredExplicit()) {
1027 1028
                    $this->scheduleForDirtyCheck($entity);
                }
1029
                break;
1030
            case self::STATE_NEW:
1031
                //TODO: Better defer insert for post-insert ID generators also?
romanb's avatar
romanb committed
1032
                $idGen = $class->idGenerator;
1033
                if ($idGen->isPostInsertGenerator()) {
1034
                    $insertNow[$oid] = $entity;
1035
                } else {
1036
                    $idValue = $idGen->generate($this->_em, $entity);
1037
                    $this->_entityStates[$oid] = self::STATE_MANAGED;
1038
                    if ( ! $idGen instanceof \Doctrine\ORM\Id\Assigned) {
1039
                        $this->_entityIdentifiers[$oid] = array($idValue);
1040
                        $class->setIdentifierValues($entity, $idValue);
1041 1042
                    } else {
                        $this->_entityIdentifiers[$oid] = $idValue;
1043
                    }
1044
                }
romanb's avatar
romanb committed
1045
                $this->registerNew($entity);
1046
                break;
1047
            case self::STATE_DETACHED:
1048
                throw DoctrineException::updateMe("Behavior of save() for a detached entity "
1049
                        . "is not yet defined.");
1050
            case self::STATE_DELETED:
1051
                // Entity becomes managed again
1052
                if ($this->isRegisteredRemoved($entity)) {
1053
                    unset($this->_entityDeletions[$oid]);
1054 1055 1056 1057
                } else {
                    //FIXME: There's more to think of here...
                    $this->registerNew($entity);
                }
1058
                break;
1059
            default:
1060
                throw DoctrineException::updateMe("Encountered invalid entity state.");
1061
        }
1062
        $this->_cascadeSave($entity, $visited, $insertNow);
1063
    }
1064 1065 1066 1067

    /**
     * Deletes an entity as part of the current unit of work.
     *
1068
     * @param object $entity
1069
     */
1070
    public function delete($entity)
1071
    {
1072 1073
        $visited = array();
        $this->_doDelete($entity, $visited);
1074
    }
1075

romanb's avatar
romanb committed
1076
    /**
1077
     * Deletes an entity as part of the current unit of work.
1078
     * 
1079 1080
     * This method is internally called during delete() cascades as it tracks
     * the already visited entities to prevent infinite recursions.
romanb's avatar
romanb committed
1081
     *
1082 1083
     * @param object $entity The entity to delete.
     * @param array $visited The map of the already visited entities.
romanb's avatar
romanb committed
1084
     */
1085
    private function _doDelete($entity, array &$visited)
1086
    {
1087
        $oid = spl_object_hash($entity);
1088
        if (isset($visited[$oid])) {
1089 1090 1091
            return; // Prevent infinite recursion
        }

1092
        $visited[$oid] = $entity; // mark visited
1093

1094 1095 1096
        switch ($this->getEntityState($entity)) {
            case self::STATE_NEW:
            case self::STATE_DELETED:
1097
                // nothing to do
1098
                break;
1099
            case self::STATE_MANAGED:
1100 1101
                $this->registerDeleted($entity);
                break;
1102
            case self::STATE_DETACHED:
1103
                throw DoctrineException::updateMe("A detached entity can't be deleted.");
1104
            default:
1105
                throw DoctrineException::updateMe("Encountered invalid entity state.");
1106 1107 1108 1109
        }
        $this->_cascadeDelete($entity, $visited);
    }

1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137
    /**
     * Merges the state of the given detached entity into this UnitOfWork.
     *
     * @param object $entity
     * @return object The managed copy of the entity.
     */
    public function merge($entity)
    {
        $visited = array();
        return $this->_doMerge($entity, $visited);
    }

    /**
     * Executes a merge operation on an entity.
     *
     * @param object $entity
     * @param array $visited
     * @return object The managed copy of the entity.
     */
    private function _doMerge($entity, array &$visited, $prevManagedCopy = null, $assoc = null)
    {
        $class = $this->_em->getClassMetadata(get_class($entity));
        $id = $class->getIdentifierValues($entity);

        if ( ! $id) {
            throw new InvalidArgumentException('New entity passed to merge().');
        }

1138
        $managedCopy = $this->tryGetById($id, $class->rootEntityName);
1139 1140 1141 1142 1143
        if ($managedCopy) {
            if ($this->getEntityState($managedCopy) == self::STATE_DELETED) {
                throw new InvalidArgumentException('Can not merge with a deleted entity.');
            }
        } else {
1144
            $managedCopy = $this->_em->find($class->name, $id);
1145 1146 1147
        }

        // Merge state of $entity into existing (managed) entity
romanb's avatar
romanb committed
1148 1149
        foreach ($class->reflFields as $name => $prop) {
            if ( ! isset($class->associationMappings[$name])) {
1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160
                $prop->setValue($managedCopy, $prop->getValue($entity));
            }
            if ($class->isChangeTrackingNotify()) {
                //TODO
            }
        }
        if ($class->isChangeTrackingDeferredExplicit()) {
            //TODO
        }

        if ($prevManagedCopy !== null) {
romanb's avatar
romanb committed
1161
            $assocField = $assoc->sourceFieldName;
1162 1163
            $prevClass = $this->_em->getClassMetadata(get_class($prevManagedCopy));
            if ($assoc->isOneToOne()) {
1164
                $prevClass->reflFields[$assocField]->setValue($prevManagedCopy, $managedCopy);
1165
            } else {
1166
                $prevClass->reflFields[$assocField]->getValue($prevManagedCopy)->add($managedCopy);
1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180
            }
        }

        $this->_cascadeMerge($entity, $managedCopy, $visited);

        return $managedCopy;
    }

    /**
     * Cascades a merge operation to associated entities.
     */
    private function _cascadeMerge($entity, $managedCopy, array &$visited)
    {
        $class = $this->_em->getClassMetadata(get_class($entity));
1181
        foreach ($class->associationMappings as $assocMapping) {
romanb's avatar
romanb committed
1182
            if ( ! $assocMapping->isCascadeMerge) {
1183 1184
                continue;
            }
1185
            $relatedEntities = $class->reflFields[$assocMapping->getSourceFieldName()]
1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196
                    ->getValue($entity);
            if (($relatedEntities instanceof Collection) && count($relatedEntities) > 0) {
                foreach ($relatedEntities as $relatedEntity) {
                    $this->_doMerge($relatedEntity, $visited, $managedCopy, $assocMapping);
                }
            } else if (is_object($relatedEntities)) {
                $this->_doMerge($relatedEntities, $visited, $managedCopy, $assocMapping);
            }
        }
    }

1197 1198 1199
    /**
     * Cascades the save operation to associated entities.
     *
1200
     * @param object $entity
1201 1202
     * @param array $visited
     */
1203
    private function _cascadeSave($entity, array &$visited, array &$insertNow)
1204
    {
1205
        $class = $this->_em->getClassMetadata(get_class($entity));
1206
        foreach ($class->associationMappings as $assocMapping) {
romanb's avatar
romanb committed
1207
            if ( ! $assocMapping->isCascadeSave) {
1208 1209
                continue;
            }
romanb's avatar
romanb committed
1210
            $relatedEntities = $class->reflFields[$assocMapping->sourceFieldName]->getValue($entity);
1211
            if (($relatedEntities instanceof Collection || is_array($relatedEntities))
1212
                    && count($relatedEntities) > 0) {
1213 1214 1215
                foreach ($relatedEntities as $relatedEntity) {
                    $this->_doSave($relatedEntity, $visited, $insertNow);
                }
1216 1217
            } else if (is_object($relatedEntities)) {
                $this->_doSave($relatedEntities, $visited, $insertNow);
1218 1219 1220 1221
            }
        }
    }

romanb's avatar
romanb committed
1222 1223 1224
    /**
     * Cascades the delete operation to associated entities.
     *
1225
     * @param object $entity
romanb's avatar
romanb committed
1226
     */
1227
    private function _cascadeDelete($entity, array &$visited)
1228
    {
romanb's avatar
romanb committed
1229
        $class = $this->_em->getClassMetadata(get_class($entity));
1230
        foreach ($class->associationMappings as $assocMapping) {
romanb's avatar
romanb committed
1231
            if ( ! $assocMapping->isCascadeDelete) {
romanb's avatar
romanb committed
1232 1233
                continue;
            }
romanb's avatar
romanb committed
1234
            $relatedEntities = $class->reflFields[$assocMapping->sourceFieldName]
romanb's avatar
romanb committed
1235
                    ->getValue($entity);
1236
            if ($relatedEntities instanceof Collection || is_array($relatedEntities)
1237
                    && count($relatedEntities) > 0) {
romanb's avatar
romanb committed
1238
                foreach ($relatedEntities as $relatedEntity) {
1239
                    $this->_doDelete($relatedEntity, $visited);
romanb's avatar
romanb committed
1240 1241
                }
            } else if (is_object($relatedEntities)) {
1242
                $this->_doDelete($relatedEntities, $visited);
romanb's avatar
romanb committed
1243 1244
            }
        }
1245
    }
romanb's avatar
romanb committed
1246 1247 1248 1249 1250 1251

    /**
     * Gets the CommitOrderCalculator used by the UnitOfWork to order commits.
     *
     * @return Doctrine\ORM\Internal\CommitOrderCalculator
     */
1252 1253 1254 1255 1256
    public function getCommitOrderCalculator()
    {
        return $this->_commitOrderCalculator;
    }

romanb's avatar
romanb committed
1257
    /**
1258
     * Clears the UnitOfWork.
romanb's avatar
romanb committed
1259
     */
1260
    public function clear()
1261
    {
1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273
        $this->_identityMap = array();
        $this->_entityIdentifiers = array();
        $this->_originalEntityData = array();
        $this->_entityChangeSets = array();
        $this->_entityStates = array();
        $this->_scheduledForDirtyCheck = array();
        $this->_entityInsertions = array();
        $this->_entityUpdates = array();
        $this->_entityDeletions = array();
        $this->_collectionDeletions = array();
        $this->_collectionCreations = array();
        $this->_collectionUpdates = array();
1274 1275
        $this->_commitOrderCalculator->clear();
    }
1276
    
1277
    public function scheduleCollectionUpdate(PersistentCollection $coll)
romanb's avatar
romanb committed
1278 1279 1280 1281
    {
        $this->_collectionUpdates[] = $coll;
    }
    
1282
    public function isCollectionScheduledForUpdate(PersistentCollection $coll)
romanb's avatar
romanb committed
1283 1284 1285 1286
    {
        //...
    }
    
1287
    public function scheduleCollectionDeletion(PersistentCollection $coll)
romanb's avatar
romanb committed
1288 1289 1290 1291 1292 1293
    {
        //TODO: if $coll is already scheduled for recreation ... what to do?
        // Just remove $coll from the scheduled recreations?
        $this->_collectionDeletions[] = $coll;
    }
    
1294
    public function isCollectionScheduledForDeletion(PersistentCollection $coll)
romanb's avatar
romanb committed
1295 1296 1297 1298
    {
        //...
    }
    
1299
    public function scheduleCollectionRecreation(PersistentCollection $coll)
romanb's avatar
romanb committed
1300 1301 1302 1303
    {
        $this->_collectionRecreations[] = $coll;
    }
    
1304
    public function isCollectionScheduledForRecreation(PersistentCollection $coll)
romanb's avatar
romanb committed
1305 1306 1307
    {
        //...
    }
1308

1309
    /**
1310
     * INTERNAL:
1311
     * Creates an entity. Used for reconstitution of entities during hydration.
1312
     *
1313 1314
     * @param string $className  The name of the entity class.
     * @param array $data  The data for the entity.
1315
     * @return object
1316 1317
     * @internal Performance-sensitive method. Run the performance test suites when
     *           making modifications.
1318
     */
1319
    public function createEntity($className, array $data, $hints = array())
1320
    {
1321
        $class = $this->_em->getClassMetadata($className);
1322

1323 1324 1325
        if ($class->isIdentifierComposite) {
            $id = array();
            foreach ($class->identifier as $fieldName) {
1326 1327
                $id[] = $data[$fieldName];
            }
1328
            $idHash = implode(' ', $id);
1329
        } else {
1330
            $id = array($data[$class->identifier[0]]);
1331 1332
            $idHash = $id[0];
        }
1333
        $entity = $this->tryGetByIdHash($idHash, $class->rootEntityName);
1334 1335
        if ($entity) {
            $oid = spl_object_hash($entity);
1336 1337
            $overrideLocalChanges = false;
            //$overrideLocalChanges = $query->getHint('doctrine.refresh');
1338 1339
        } else {
            $entity = new $className;
1340 1341
            $oid = spl_object_hash($entity);
            $this->_entityIdentifiers[$oid] = $id;
1342
            $this->_entityStates[$oid] = self::STATE_MANAGED;
1343
            $this->_originalEntityData[$oid] = $data;
1344
            $this->addToIdentityMap($entity);
1345
            $overrideLocalChanges = true;
1346 1347 1348 1349
        }

        if ($overrideLocalChanges) {
            foreach ($data as $field => $value) {
1350 1351
                if (isset($class->reflFields[$field])) {
                    $class->reflFields[$field]->setValue($entity, $value);
1352
                }
1353 1354 1355
            }
        } else {
            foreach ($data as $field => $value) {
1356 1357
                if (isset($class->reflFields[$field])) {
                    $currentValue = $class->reflFields[$field]->getValue($entity);
1358
                    if ( ! isset($this->_originalEntityData[$oid][$field]) ||
1359
                            $currentValue == $this->_originalEntityData[$oid][$field]) {
1360
                        $class->reflFields[$field]->setValue($entity, $value);
1361
                    }
1362 1363 1364
                }
            }
        }
1365 1366

        return $entity;
1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377
    }

    /**
     * Gets the identity map of the UnitOfWork.
     *
     * @return array
     */
    public function getIdentityMap()
    {
        return $this->_identityMap;
    }
1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396

    /**
     * Gets the original data of an entity. The original data is the data that was
     * present at the time the entity was reconstituted from the database.
     *
     * @param object $entity
     * @return array
     */
    public function getOriginalEntityData($entity)
    {
        $oid = spl_object_hash($entity);
        if (isset($this->_originalEntityData[$oid])) {
            return $this->_originalEntityData[$oid];
        }
        return array();
    }

    /**
     * INTERNAL:
1397
     * For internal purposes only.
1398
     *
1399
     * Sets a property value of the original data array of an entity.
1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411
     *
     * @param string $oid
     * @param string $property
     * @param mixed $value
     */
    public function setOriginalEntityProperty($oid, $property, $value)
    {
        $this->_originalEntityData[$oid][$property] = $value;
    }

    /**
     * Gets the identifier of an entity.
1412
     * The returned value is always an array of identifier values. If the entity
1413
     * has a composite identifier then the identifier values are in the same
1414
     * order as the identifier field names as returned by ClassMetadata#getIdentifierFieldNames().
1415 1416 1417 1418 1419 1420 1421 1422
     *
     * @param object $entity
     * @return array The identifier values.
     */
    public function getEntityIdentifier($entity)
    {
        return $this->_entityIdentifiers[spl_object_hash($entity)];
    }
1423 1424

    /**
1425 1426
     * Tries to find an entity with the given identifier in the identity map of
     * this UnitOfWork.
1427
     *
1428 1429 1430 1431
     * @param mixed $id The entity identifier to look for.
     * @param string $rootClassName The name of the root class of the mapped entity hierarchy.
     * @return mixed Returns the entity with the specified identifier if it exists in
     *               this UnitOfWork, FALSE otherwise.
1432 1433 1434
     */
    public function tryGetById($id, $rootClassName)
    {
1435
        $idHash = implode(' ', (array)$id);
1436 1437 1438 1439 1440
        if (isset($this->_identityMap[$rootClassName][$idHash])) {
            return $this->_identityMap[$rootClassName][$idHash];
        }
        return false;
    }
1441

1442 1443 1444 1445 1446
    /**
     * Schedules an entity for dirty-checking at commit-time.
     *
     * @param object $entity The entity to schedule for dirty-checking.
     */
1447 1448
    public function scheduleForDirtyCheck($entity)
    {
1449
        $rootClassName = $this->_em->getClassMetadata(get_class($entity))->rootEntityName;
1450 1451 1452
        $this->_scheduledForDirtyCheck[$rootClassName] = $entity;
    }

1453 1454 1455 1456 1457 1458 1459 1460 1461 1462
    /**
     * Checks whether the UnitOfWork has any pending insertions.
     *
     * @return boolean TRUE if this UnitOfWork has pending insertions, FALSE otherwise.
     */
    public function hasPendingInsertions()
    {
        return ! empty($this->_entityInsertions);
    }

1463 1464 1465
    /**
     * Calculates the size of the UnitOfWork. The size of the UnitOfWork is the
     * number of entities in the identity map.
1466 1467
     *
     * @return integer
1468 1469 1470 1471 1472 1473 1474
     */
    public function size()
    {
        $count = 0;
        foreach ($this->_identityMap as $entitySet) $count += count($entitySet);
        return $count;
    }
1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485

    /**
     * Gets the EntityPersister for an Entity.
     *
     * @param string $entityName  The name of the Entity.
     * @return Doctrine\ORM\Persister\AbstractEntityPersister
     */
    public function getEntityPersister($entityName)
    {
        if ( ! isset($this->_persisters[$entityName])) {
            $class = $this->_em->getClassMetadata($entityName);
1486
            if ($class->isInheritanceTypeNone()) {
1487
                $persister = new Persisters\StandardEntityPersister($this->_em, $class);
1488 1489 1490
            } else if ($class->isInheritanceTypeSingleTable()) {
                $persister = new Persisters\SingleTablePersister($this->_em, $class);
            } else if ($class->isInheritanceTypeJoined()) {
1491
                $persister = new Persisters\JoinedSubclassPersister($this->_em, $class);
1492
            } else {
1493
                $persister = new Persisters\UnionSubclassPersister($this->_em, $class);
1494 1495 1496 1497 1498 1499
            }
            $this->_persisters[$entityName] = $persister;
        }
        return $this->_persisters[$entityName];
    }

1500 1501 1502 1503 1504 1505
    /**
     * Gets a collection persister for a collection-valued association.
     *
     * @param AssociationMapping $association
     * @return AbstractCollectionPersister
     */
1506 1507 1508 1509
    public function getCollectionPersister($association)
    {
        $type = get_class($association);
        if ( ! isset($this->_collectionPersisters[$type])) {
1510 1511 1512 1513
            if ($association instanceof Mapping\OneToManyMapping) {
                $persister = new Persisters\OneToManyPersister($this->_em);
            } else if ($association instanceof Mapping\ManyToManyMapping) {
                $persister = new Persisters\ManyToManyPersister($this->_em);
1514 1515 1516 1517 1518
            }
            $this->_collectionPersisters[$type] = $persister;
        }
        return $this->_collectionPersisters[$type];
    }
1519

1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535
    /**
     * INTERNAL:
     * Registers an entity as managed.
     *
     * @param object $entity The entity.
     * @param array $id The identifier values.
     * @param array $data The original entity data.
     */
    public function registerManaged($entity, $id, $data)
    {
        $oid = spl_object_hash($entity);
        $this->_entityIdentifiers[$oid] = $id;
        $this->_entityStates[$oid] = self::STATE_MANAGED;
        $this->_originalEntityData[$oid] = $data;
        $this->addToIdentityMap($entity);
    }
1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546
    
    /**
     * INTERNAL:
     * Clears the property changeset of the entity with the given OID.
     *
     * @param string $oid The entity's OID.
     */
    public function clearEntityChangeSet($oid)
    {
    	unset($this->_entityChangeSets[$oid]);
    }
1547

1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559
    /* PropertyChangedListener implementation */

    /**
     * Notifies this UnitOfWork of a property change in an entity.
     *
     * @param object $entity The entity that owns the property.
     * @param string $propertyName The name of the property that changed.
     * @param mixed $oldValue The old value of the property.
     * @param mixed $newValue The new value of the property.
     */
    public function propertyChanged($entity, $propertyName, $oldValue, $newValue)
    {
1560 1561 1562
        if ($this->getEntityState($entity) == self::STATE_MANAGED) {
            $oid = spl_object_hash($entity);
            $class = $this->_em->getClassMetadata(get_class($entity));
1563

1564 1565
            $this->_entityChangeSets[$oid][$propertyName] = array($oldValue, $newValue);

romanb's avatar
romanb committed
1566
            if (isset($class->associationMappings[$propertyName])) {
1567
                $assoc = $class->associationMappings[$propertyName];
romanb's avatar
romanb committed
1568
                if ($assoc->isOneToOne() && $assoc->isOwningSide) {
1569 1570 1571 1572 1573 1574
                    $this->_entityUpdates[$oid] = $entity;
                } else if ($oldValue instanceof PersistentCollection) {
                    // A PersistentCollection was de-referenced, so delete it.
                    if  ( ! in_array($orgValue, $this->_collectionDeletions, true)) {
                        $this->_collectionDeletions[] = $orgValue;
                    }
1575
                }
1576 1577
            } else {
                $this->_entityUpdates[$oid] = $entity;
1578 1579 1580
            }
        }
    }
1581
}