.
*/
#namespace Doctrine::ORM::Internal;
/**
* The UnitOfWork is responsible for writing out changes to the database at
* the correct time and in the correct order.
*
* Some terminology:
*
* New entity: A new entity is an entity that already has an identity but
* is not yet persisted into the database. This is usually the case for all
* newly saved entities that use a SEQUENCE id generator. Entities with an
* IDENTITY id generator get persisted as soon as they're saved in order to
* obtain the identifier. Therefore entities that use an IDENTITY id generator
* never appear in the list of new entities of the UoW.
*
* Dirty entity: A dirty entity is a managed entity whose values have
* been altered.
*
* Removed entity: A removed entity is a managed entity that is scheduled
* for deletion from the database.
*
* Clean entity: A clean entity is a managed entity that has been fetched
* from the database and whose values have not yet been altered.
*
* @package Doctrine
* @subpackage Connection
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.phpdoctrine.org
* @since 2.0
* @version $Revision$
* @author Konsta Vesterinen
* @author Roman Borschel
* @todo package:orm. Figure out a useful implementation.
*/
class Doctrine_Connection_UnitOfWork extends Doctrine_Connection_Module
{
/**
* A map of all currently managed entities.
*
* @var array
* @deprecated Only here to keep the saveAll() functionality working. We don't need
* this in the future.
*/
protected $_managedEntities = array();
/**
* The identity map that holds references to all managed entities that have
* an identity. The entities are grouped by their class name.
* Since all classes in a hierarchy must share the same identifier set,
* we always take the root class name of the hierarchy.
*
* @var array
*/
protected $_identityMap = array();
/**
* A list of all new entities.
*/
protected $_newEntities = array();
/**
* A list of all dirty entities.
*/
protected $_dirtyEntities = array();
/**
* A list of all removed entities.
*/
protected $_removedEntities = array();
/**
* The EntityManager the unit of work belongs to.
*/
protected $_em;
/**
* The dbal connection used by the unit of work.
*
* @var Doctrine_Connection
* @todo Not needed in the future. Remove.
*/
protected $_conn;
/**
* Commits the unit of work, executing all operations that have been postponed
* up to this point.
*
*/
public function commit()
{
$this->_orderCommits();
$this->_insertNew();
$this->_updateDirty();
$this->_deleteRemoved();
}
private function _orderCommits()
{
}
/**
* Register a new entity.
*/
public function registerNew(Doctrine_Entity $entity)
{
if ( ! $entity->identifier()) {
throw new Doctrine_Connection_Exception("Entity without identity "
. "can't be registered as new.");
}
$oid = $entity->getOid();
if (isset($this->_dirtyEntities[$oid])) {
throw new Doctrine_Connection_Exception("Dirty object can't be registered as new.");
} else if (isset($this->_removedEntities[$oid])) {
throw new Doctrine_Connection_Exception("Removed object can't be registered as new.");
} else if (isset($this->_newEntities[$oid])) {
throw new Doctrine_Connection_Exception("Object already registered as new. Can't register twice.");
}
$this->_newEntities[$oid] = $entity;
}
public function isRegisteredNew(Doctrine_Entity $entity)
{
return isset($this->_newEntities[$entity->getOid()]);
}
/**
* Registers a clean entity.
*/
public function registerClean(Doctrine_Entity $entity)
{
$this->registerIdentity($entity);
}
/**
* Registers a dirty entity.
*/
public function registerDirty(Doctrine_Entity $entity)
{
if ( ! $entity->identifier()) {
throw new Doctrine_Connection_Exception("Entity without identity "
. "can't be registered as dirty.");
}
$oid = $entity->getOid();
if (isset($this->_removedEntities[$entity->getOid()])) {
throw new Doctrine_Connection_Exception("Removed object can't be registered as dirty.");
}
if ( ! isset($this->_dirtyEntities[$oid], $this->_newEntities[$oid])) {
$this->_dirtyEntities[$entity->getOid()] = $entity;
}
}
public function isRegisteredDirty(Doctrine_Entity $entity)
{
return isset($this->_dirtyEntities[$entity->getOid()]);
}
/**
* Registers a deleted entity.
*/
public function registerRemoved(Doctrine_Entity $entity)
{
if ($entity->isTransient()) {
return;
}
$this->unregisterIdentity($entity);
$oid = $entity->getOid();
if (isset($this->_newEntities[$oid])) {
unset($this->_newEntities[$oid]);
return;
}
if (isset($this->_dirtyEntities[$oid])) {
unset($this->_dirtyEntities[$oid]);
}
if ( ! isset($this->_removedEntities[$oid])) {
$this->_removedEntities[$oid] = $entity;
}
}
public function isRegisteredRemoved(Doctrine_Entity $entity)
{
return isset($this->_removedEntities[$entity->getOid()]);
}
/**
* buildFlushTree
* builds a flush tree that is used in transactions
*
* The returned array has all the initialized components in
* 'correct' order. Basically this means that the records of those
* components can be saved safely in the order specified by the returned array.
*
* @param array $tables an array of Doctrine_Table objects or component names
* @return array an array of component names in flushing order
*/
public function buildFlushTree(array $mappers)
{
$tree = array();
foreach ($mappers as $k => $mapper) {
if ( ! ($mapper instanceof Doctrine_Mapper)) {
$mapper = $this->conn->getMapper($mapper);
}
$nm = $mapper->getComponentName();
$index = array_search($nm, $tree);
if ($index === false) {
$tree[] = $nm;
$index = max(array_keys($tree));
}
$rels = $mapper->getClassMetadata()->getRelations();
// group relations
foreach ($rels as $key => $rel) {
if ($rel instanceof Doctrine_Relation_ForeignKey) {
unset($rels[$key]);
array_unshift($rels, $rel);
}
}
foreach ($rels as $rel) {
$name = $rel->getTable()->getComponentName();
$index2 = array_search($name, $tree);
$type = $rel->getType();
// skip self-referenced relations
if ($name === $nm) {
continue;
}
if ($rel instanceof Doctrine_Relation_ForeignKey) {
if ($index2 !== false) {
if ($index2 >= $index)
continue;
unset($tree[$index]);
array_splice($tree,$index2,0,$nm);
$index = $index2;
} else {
$tree[] = $name;
}
} else if ($rel instanceof Doctrine_Relation_LocalKey) {
if ($index2 !== false) {
if ($index2 <= $index)
continue;
unset($tree[$index2]);
array_splice($tree, $index, 0, $name);
} else {
array_unshift($tree,$name);
$index++;
}
} else if ($rel instanceof Doctrine_Relation_Association) {
$t = $rel->getAssociationFactory();
$n = $t->getComponentName();
if ($index2 !== false) {
unset($tree[$index2]);
}
array_splice($tree, $index, 0, $name);
$index++;
$index3 = array_search($n, $tree);
if ($index3 !== false) {
if ($index3 >= $index)
continue;
unset($tree[$index]);
array_splice($tree, $index3, 0, $n);
$index = $index2;
} else {
$tree[] = $n;
}
}
}
}
return $tree;
}
/**
* saveAll
* persists all the pending records from all tables
*
* @throws PDOException if something went wrong at database level
* @return void
* @deprecated
*/
public function saveAll()
{
$this->conn->beginInternalTransaction();
// get the flush tree
$tree = $this->buildFlushTree($this->conn->getMappers());
$tree = array_combine($tree, array_fill(0, count($tree), array()));
foreach ($this->_managedEntities as $oid => $entity) {
$className = $entity->getClassName();
$tree[$className][] = $entity;
}
// save all records
foreach ($tree as $className => $entities) {
$mapper = $this->conn->getMapper($className);
foreach ($entities as $entity) {
$mapper->saveSingleRecord($entity);
}
}
// save all associations
foreach ($tree as $className => $entities) {
$mapper = $this->conn->getMapper($className);
foreach ($entities as $entity) {
$mapper->saveAssociations($entity);
}
}
$this->conn->commit();
}
/**
* Adds an entity to the pool of managed entities.
* @deprecated
*/
public function manage(Doctrine_Entity $entity)
{
$oid = $entity->getOid();
if ( ! isset($this->_managedEntities[$oid])) {
$this->_managedEntities[$oid] = $entity;
return true;
}
return false;
}
/**
* @param integer $oid object identifier
* @return boolean whether ot not the operation was successful
* @deprecated The new implementation of detach() should remove the entity
* from the identity map.
*/
public function detach(Doctrine_Entity $entity)
{
$oid = $entity->getOid();
if ( ! isset($this->_managedEntities[$oid])) {
return false;
}
unset($this->_managedEntities[$oid]);
return true;
}
/**
* Detaches all currently managed entities.
*
* @return integer The number of detached entities.
* @todo Deprecated. The new implementation should remove all entities from
* the identity map.
*/
public function detachAll()
{
$numDetached = count($this->_managedEntities);
$this->_managedEntities = array();
return $numDetached;
}
/**
* Registers an entity in the identity map.
*
* @return boolean TRUE if the registration was successful, FALSE if the identity of
* the entity in question is already managed.
* @throws Doctrine_Connection_Exception If the entity has no (database) identity.
*/
public function registerIdentity(Doctrine_Entity $entity)
{
$idHash = $this->getIdentifierHash($entity->identifier());
if ( ! $idHash) {
throw new Doctrine_Connection_Exception("Entity with oid '" . $entity->getOid()
. "' has no identity and therefore can't be added to the identity map.");
}
$className = $entity->getClassMetadata()->getRootClassName();
if (isset($this->_identityMap[$className][$idHash])) {
return false;
}
$this->_identityMap[$className][$idHash] = $entity;
return true;
}
/**
* Enter description here...
*
* @param unknown_type $entityName
* @todo unify with detachAll()
*/
public function clearIdentitiesForEntity($entityName)
{
$this->_identityMap[$entityName] = array();
}
/**
* Removes an entity from the identity map.
*
* @param Doctrine_Entity $entity
* @return unknown
* @todo This will be the new detach().
*/
public function unregisterIdentity(Doctrine_Entity $entity)
{
$idHash = $this->getIdentifierHash($entity->identifier());
if ( ! $idHash) {
throw new Doctrine_Connection_Exception("Entity with oid '" . $entity->getOid()
. "' has no identity and therefore can't be removed from the identity map.");
}
$className = $entity->getClassMetadata()->getRootClassName();
if (isset($this->_identityMap[$className][$idHash])) {
unset($this->_identityMap[$className][$idHash]);
return true;
}
return false;
}
/**
* Finds an entity in the identity map by its identifier hash.
*
* @param unknown_type $idHash
* @param unknown_type $rootClassName
* @return unknown
*/
public function getByIdHash($idHash, $rootClassName)
{
return $this->_identityMap[$rootClassName][$idHash];
}
public function tryGetByIdHash($idHash, $rootClassName)
{
if ($this->containsIdHash($idHash, $rootClassName)) {
return $this->getByIdHash($idHash, $rootClassName);
}
return false;
}
/**
* Gets the identifier hash for a set of identifier values.
*
* @param array $id
* @return string
*/
public function getIdentifierHash(array $id)
{
return implode(' ', $id);
}
/**
* Checks whether an entity is registered in the identity map.
*
* @param Doctrine_Entity $entity
* @return boolean
*/
public function contains(Doctrine_Entity $entity)
{
$idHash = $this->getIdentifierHash($entity->identifier());
if ( ! $idHash) {
return false;
}
return isset($this->_identityMap
[$entity->getClassMetadata()->getRootClassName()]
[$idHash]);
}
/**
* Checks whether an identifier hash exists in the identity map.
*
* @param string $idHash
* @param string $rootClassName
* @return boolean
*/
public function containsIdHash($idHash, $rootClassName)
{
return isset($this->_identityMap[$rootClassName][$idHash]);
}
}