1
0
mirror of synced 2024-12-13 22:56:04 +03:00
doctrine2/lib/Doctrine/Connection/UnitOfWork.php
2008-08-16 19:40:59 +00:00

966 lines
32 KiB
PHP

<?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
* <http://www.phpdoctrine.org>.
*/
#namespace Doctrine::ORM::Internal;
#use Doctrine::ORM::Entity;
#use Doctrine::ORM::EntityManager;
#use Doctrine::ORM::Exceptions::UnitOfWorkException;
/**
* The UnitOfWork is responsible for tracking changes to objects during an
* "object-level" transaction and for writing out changes to the database
* in the correct order.
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.phpdoctrine.org
* @since 2.0
* @version $Revision$
* @author Konsta Vesterinen <kvesteri@cc.hut.fi>
* @author Roman Borschel <roman@code-factory.org>
* @todo Rename: Doctrine::ORM::(Internal::)UnitOfWork.
* @todo Turn connection exceptions into UnitOfWorkExceptions.
*/
class Doctrine_Connection_UnitOfWork
{
/**
* 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 that need to be INSERTed.
*
* @var array
* @todo Index by class name.
* @todo Rename to _inserts?
*/
protected $_newEntities = array();
/**
* A list of all dirty entities.
*
* @var array
* @todo Rename to _updates?
*/
protected $_dirtyEntities = array();
/**
* A list of all deleted entities.
* Removed entities are entities that are "scheduled for removal" but have
* not yet been removed from the database.
*
* @var array
* @todo Rename to _deletions?
*/
protected $_deletedEntities = array();
/**
* All collection deletions.
*
* @var array
*/
protected $_collectionDeletions = array();
/**
* All collection creations.
*
* @var array
*/
protected $_collectionCreations = array();
/**
* All collection updates.
*
* @var array
*/
protected $_collectionUpdates = array();
/**
* The EntityManager the UnitOfWork belongs to.
*
* @var Doctrine::ORM::EntityManager
*/
protected $_em;
/**
* The calculator used to calculate the order in which changes to
* entities need to be written to the database.
*
* @var Doctrine::ORM::Internal::CommitOrderCalculator
*/
protected $_commitOrderCalculator;
/**
* Constructor.
* Creates a new UnitOfWork.
*
* @param Doctrine_EntityManager $em
*/
public function __construct(Doctrine_EntityManager $em)
{
$this->_em = $em;
//TODO: any benefit with lazy init?
$this->_commitOrderCalculator = new Doctrine_Internal_CommitOrderCalculator();
}
/**
* Commits the unit of work, executing all operations that have been postponed
* up to this point.
*
* @todo Impl
*/
public function commit()
{
// Detect changes in managed entities (mark dirty)
//TODO: Consider using registerDirty() in Entity#_set() instead if its
// more performant (SEE THERE).
/*foreach ($this->_identityMap as $entities) {
foreach ($entities as $entity) {
if ($entity->_state() == Doctrine_Entity::STATE_MANAGED
&& $entity->isModified()) {
$this->registerDirty($entity);
}
}
}*/
if (empty($this->_newEntities) &&
empty($this->_deletedEntities) &&
empty($this->_dirtyEntities)) {
return; // Nothing to do.
}
// Now we need a commit order to maintain referential integrity
$commitOrder = $this->_getCommitOrder();
//TODO: begin transaction here?
foreach ($commitOrder as $class) {
$this->_executeInserts($class);
$this->_executeUpdates($class);
}
//TODO: collection deletions
//TODO: collection updates (deleteRows, updateRows, insertRows)
//TODO: collection recreations
// 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]);
}
//TODO: commit transaction here?
// clear lists
$this->_newEntities = array();
$this->_dirtyEntities = array();
$this->_deletedEntities = array();
}
private function _executeInserts($class)
{
//TODO: Maybe $persister->addInsert($entity) in the loop and
// $persister->executeInserts() at the end to allow easy prepared
// statement reuse and maybe bulk operations in the persister.
// Same for update/delete.
$className = $class->getClassName();
$persister = $this->_em->getEntityPersister($className);
foreach ($this->_newEntities as $entity) {
if ($entity->getClass()->getClassName() == $className) {
$persister->insert($entity);
}
}
}
private function _executeUpdates($class)
{
$className = $class->getClassName();
$persister = $this->_em->getEntityPersister($className);
foreach ($this->_dirtyEntities as $entity) {
if ($entity->getClass()->getClassName() == $className) {
$persister->update($entity);
}
}
}
private function _executeDeletions($class)
{
$className = $class->getClassName();
$persister = $this->_em->getEntityPersister($className);
foreach ($this->_deletedEntities as $entity) {
if ($entity->getClass()->getClassName() == $className) {
$persister->delete($entity);
}
}
}
/**
* Gets the commit order.
*
* @return array
*/
private function _getCommitOrder()
{
//TODO: Once these 3 arrays are indexed by classname we can do this:
// Either way... do we need to care about duplicates?
/*$classesInChangeSet = array_merge(
array_keys($this->_newEntities),
array_keys($this->_dirtyEntities),
array_keys($this->_deletedEntities)
);*/
$entityChangeSet = array_merge($this->_newEntities, $this->_dirtyEntities, $this->_deletedEntities);
/* if (count($entityChangeSet) == 1) {
* return array($entityChangeSet[0]->getClass());
* }
*/
// 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) {
if ( ! $this->_commitOrderCalculator->hasNodeWithKey($entity->getClass()->getClassName())) {
$this->_commitOrderCalculator->addNodeWithItem(
$entity->getClass()->getClassName(), // index/key
$entity->getClass() // item
);
$newNodes[] = $this->_commitOrderCalculator->getNodeForKey($entity->getClass()->getClassName());
}
}
// Calculate dependencies for new nodes
foreach ($newNodes as $node) {
foreach ($node->getClass()->getAssociationMappings() as $assocMapping) {
//TODO: should skip target classes that are not in the changeset.
if ($assocMapping->isOwningSide()) {
$targetClass = $this->_em->getClassMetadata($assocMapping->getTargetEntityName());
$targetClassName = $targetClass->getClassName();
// if the target class does not yet have a node, create it
if ( ! $this->_commitOrderCalculator->hasNodeWithKey($targetClassName)) {
$this->_commitOrderCalculator->addNodeWithItem(
$targetClassName, // index/key
$targetClass // item
);
}
// add dependency
$otherNode = $this->_commitOrderCalculator->getNodeForKey($targetClassName);
$node->before($otherNode);
}
}
}
return $this->_commitOrderCalculator->getCommitOrder();
}
/**
* Register a new entity.
*
* @todo Rename to scheduleForInsert().
*/
public function registerNew(Doctrine_Entity $entity)
{
$oid = $entity->getOid();
/*if ( ! $entity->_identifier()) {
throw new Doctrine_Connection_Exception("Entity without identity cant be registered as new.");
}*/
if (isset($this->_dirtyEntities[$oid])) {
throw new Doctrine_Connection_Exception("Dirty object can't be registered as new.");
}
if (isset($this->_deletedEntities[$oid])) {
throw new Doctrine_Connection_Exception("Removed object can't be registered as new.");
}
if (isset($this->_newEntities[$oid])) {
throw new Doctrine_Connection_Exception("Object already registered as new. Can't register twice.");
}
$this->_newEntities[$oid] = $entity;
if ($entity->_identifier()) {
$this->addToIdentityMap($entity);
}
}
/**
* Checks whether an entity is registered as new on the unit of work.
*
* @param Doctrine_Entity $entity
* @return boolean
* @todo Rename to isScheduledForInsert().
*/
public function isRegisteredNew(Doctrine_Entity $entity)
{
return isset($this->_newEntities[$entity->getOid()]);
}
/**
* Registers a clean entity.
* The entity is simply put into the identity map.
*
* @param Doctrine::ORM::Entity $entity
*/
public function registerClean(Doctrine_Entity $entity)
{
$this->addToIdentityMap($entity);
}
/**
* Registers a dirty entity.
*
* @param Doctrine::ORM::Entity $entity
* @todo Rename to scheduleForUpdate().
*/
public function registerDirty(Doctrine_Entity $entity)
{
$oid = $entity->getOid();
if ( ! $entity->_identifier()) {
throw new Doctrine_Connection_Exception("Entity without identity "
. "can't be registered as dirty.");
}
if (isset($this->_deletedEntities[$oid])) {
throw new Doctrine_Connection_Exception("Removed object can't be registered as dirty.");
}
if ( ! isset($this->_dirtyEntities[$oid]) && ! isset($this->_newEntities[$oid])) {
$this->_dirtyEntities[$oid] = $entity;
}
}
/**
* 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().
*/
public function isRegisteredDirty(Doctrine_Entity $entity)
{
return isset($this->_dirtyEntities[$entity->getOid()]);
}
/**
* Registers a deleted entity.
*
* @todo Rename to scheduleForDelete().
*/
public function registerDeleted(Doctrine_Entity $entity)
{
$oid = $entity->getOid();
if ( ! $this->isInIdentityMap($entity)) {
return;
}
$this->removeFromIdentityMap($entity);
if (isset($this->_newEntities[$oid])) {
unset($this->_newEntities[$oid]);
return; // entity has not been persisted yet, so nothing more to do.
}
/* Seems unnecessary since _dirtyEntities is filled & cleared on commit, not earlier
if (isset($this->_dirtyEntities[$oid])) {
unset($this->_dirtyEntities[$oid]);
}*/
if ( ! isset($this->_deletedEntities[$oid])) {
$this->_deletedEntities[$oid] = $entity;
}
}
/**
* Checks whether an entity is registered as removed/deleted with the unit
* of work.
*
* @param Doctrine::ORM::Entity $entity
* @return boolean
* @todo Rename to isScheduledForDelete().
*/
public function isRegisteredRemoved(Doctrine_Entity $entity)
{
return isset($this->_deletedEntities[$entity->getOid()]);
}
/**
* Detaches an entity from the persistence management. It's persistence will
* no longer be managed by Doctrine.
*
* @param integer $oid object identifier
* @return boolean whether ot not the operation was successful
*/
public function detach(Doctrine_Entity $entity)
{
if ($this->isInIdentityMap($entity)) {
$this->removeFromIdentityMap($entity);
}
}
/**
* Enter description here...
*
* @param Doctrine_Entity $entity
* @return unknown
* @todo Rename to isScheduled()
*/
public function isEntityRegistered(Doctrine_Entity $entity)
{
$oid = $entity->getOid();
return isset($this->_newEntities[$oid]) ||
//isset($this->_dirtyEntities[$oid]) ||
isset($this->_deletedEntities[$oid]) ||
$this->isInIdentityMap($entity);
}
/**
* Detaches all currently managed entities.
* 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.
*
* @return integer The number of detached entities.
*/
public function detachAll($entityName = null)
{
//TODO: what do do with new/dirty/removed lists?
$numDetached = 0;
if ($entityName !== null && isset($this->_identityMap[$entityName])) {
$numDetached = count($this->_identityMap[$entityName]);
$this->_identityMap[$entityName] = array();
} else {
$numDetached = count($this->_identityMap);
$this->_identityMap = array();
}
return $numDetached;
}
/**
* Registers an entity in the identity map.
* Note that entities in a hierarchy are registered with the class name of
* the root entity.
*
* @param Doctrine::ORM::Entity $entity The entity to register.
* @return boolean TRUE if the registration was successful, FALSE if the identity of
* the entity in question is already managed.
*/
public function addToIdentityMap(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->getClass()->getRootClassName();
if (isset($this->_identityMap[$className][$idHash])) {
return false;
}
$this->_identityMap[$className][$idHash] = $entity;
$entity->_state(Doctrine_Entity::STATE_MANAGED);
return true;
}
/**
* Removes an entity from the identity map.
*
* @param Doctrine_Entity $entity
* @return unknown
*/
public function removeFromIdentityMap(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->getClass()->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 string $idHash
* @param string $rootClassName
* @return Doctrine::ORM::Entity
*/
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.
* The hash is just a concatenation of the identifier values.
* The identifiers are concatenated with a space.
*
* Note that this method always returns a string. If the given array is
* empty, an empty string is returned.
*
* @param array $id
* @return string The hash.
*/
public function getIdentifierHash(array $id)
{
return implode(' ', $id);
}
/**
* Checks whether an entity is registered in the identity map of the
* UnitOfWork.
*
* @param Doctrine_Entity $entity
* @return boolean
*/
public function isInIdentityMap(Doctrine_Entity $entity)
{
$idHash = $this->getIdentifierHash($entity->_identifier());
if ($idHash === '') {
return false;
}
return isset($this->_identityMap
[$entity->getClass()->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]);
}
/**
* Saves an entity as part of the current unit of work.
*
* @param Doctrine_Entity $entity The entity to save.
*/
public function save(Doctrine_Entity $entity)
{
$insertNow = array();
$visited = array();
$this->_doSave($entity, $visited, $insertNow);
if ( ! empty($insertNow)) {
// We have no choice. This means that there are either new entities
// with an IDENTITY key generation or with a natural identifier.
// In both cases we must commit the inserts instantly.
//TODO: Isnt it enough to only execute the inserts instead of full flush?
$this->commit();
/* The following may be better:
$commitOrder = $this->_getCommitOrder($insertNow);
foreach ($commitOrder as $class) {
$this->_executeInserts($class);
}
//... remove them from _newEntities, or dont store them there in the first place
*/
}
}
/**
* 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.
*
* @param Doctrine_Entity $entity The entity to save.
* @param array $visited The already visited entities.
*/
private function _doSave(Doctrine_Entity $entity, array &$visited, array &$insertNow)
{
if (isset($visited[$entity->getOid()])) {
return; // Prevent infinite recursion
}
$visited[$entity->getOid()] = $entity; // mark visited
$class = $entity->getClass();
switch ($entity->_state()) {
case Doctrine_Entity::STATE_MANAGED:
// nothing to do for $entity
break;
case Doctrine_Entity::STATE_NEW:
if ($class->isIdGeneratorIdentity()) {
$insertNow[$entity->getOid()] = $entity;
$this->_newEntities[$entity->getOid()] = $entity;
} else if ( ! $class->usesIdGenerator()) {
$insertNow[$entity->getOid()] = $entity;
$this->_newEntities[$entity->getOid()] = $entity;
//...
} else if ($class->isIdGeneratorSequence()) {
// Get the next sequence number
//TODO: sequence name?
$id = $this->_em->getConnection()->getSequenceManager()->nextId("foo");
$entity->set($class->getSingleIdentifierFieldName(), $id);
$this->registerNew($entity);
} else {
throw new Doctrine_Exception("Unable to handle ID generation of new entity.");
}
break;
case Doctrine_Entity::STATE_DETACHED:
//exception?
throw new Doctrine_Exception("Behavior of save() for a detached entity "
. "is not yet defined.");
case Doctrine_Entity::STATE_DELETED:
// $entity becomes managed again
if ($this->isRegisteredRemoved($entity)) {
//TODO: better a method for this?
unset($this->_deletedEntities[$entity->getOid()]);
} else {
//FIXME: There's more to think of here...
$this->registerNew($entity);
}
break;
default:
//TODO: throw UnitOfWorkException::invalidEntityState()
throw new Doctrine_Exception("Encountered invalid entity state.");
}
$this->_cascadeSave($entity, $visited, $insertNow);
}
/**
* Deletes an entity as part of the current unit of work.
*
* @param Doctrine_Entity $entity
*/
public function delete(Doctrine_Entity $entity)
{
$this->_doDelete($entity, array());
}
private function _doDelete(Doctrine_Entity $entity, array &$visited)
{
if (isset($visited[$entity->getOid()])) {
return; // Prevent infinite recursion
}
$visited[$entity->getOid()] = $entity; // mark visited
$class = $entity->getClass();
switch ($entity->_state()) {
case Doctrine_Entity::STATE_NEW:
case Doctrine_Entity::STATE_DELETED:
// nothing to do for $entity
break;
case Doctrine_Entity::STATE_MANAGED:
$this->registerDeleted($entity);
break;
case Doctrine_Entity::STATE_DETACHED:
//exception?
throw new Doctrine_Exception("A detached entity can't be deleted.");
default:
//TODO: throw UnitOfWorkException::invalidEntityState()
throw new Doctrine_Exception("Encountered invalid entity state.");
}
$this->_cascadeDelete($entity, $visited);
}
/**
* Cascades the save operation to associated entities.
*
* @param Doctrine_Entity $entity
* @param array $visited
*/
private function _cascadeSave(Doctrine_Entity $entity, array &$visited, array &$insertNow)
{
foreach ($entity->getClass()->getAssociationMappings() as $assocMapping) {
if ( ! $assocMapping->isCascadeSave()) {
continue;
}
$relatedEntities = $entity->get($assocMapping->getSourceFieldName());
if ($relatedEntities instanceof Doctrine_Entity) {
$this->_doSave($relatedEntities, $visited, $insertNow);
} else if ($relatedEntities instanceof Doctrine_Collection &&
count($relatedEntities) > 0) {
foreach ($relatedEntities as $relatedEntity) {
$this->_doSave($relatedEntity, $visited, $insertNow);
}
}
}
}
private function _cascadeDelete(Doctrine_Entity $entity)
{
}
public function getCommitOrderCalculator()
{
return $this->_commitOrderCalculator;
}
public function close()
{
//...
$this->_commitOrderCalculator->clear();
}
public function scheduleCollectionUpdate(Doctrine_Collection $coll)
{
$this->_collectionUpdates[] = $coll;
}
public function isCollectionScheduledForUpdate(Doctrine_Collection $coll)
{
//...
}
public function scheduleCollectionDeletion(Doctrine_Collection $coll)
{
//TODO: if $coll is already scheduled for recreation ... what to do?
// Just remove $coll from the scheduled recreations?
$this->_collectionDeletions[] = $coll;
}
public function isCollectionScheduledForDeletion(Doctrine_Collection $coll)
{
//...
}
public function scheduleCollectionRecreation(Doctrine_Collection $coll)
{
$this->_collectionRecreations[] = $coll;
}
public function isCollectionScheduledForRecreation(Doctrine_Collection $coll)
{
//...
}
// Stuff from 0.11/1.0 that we will need later (need to modify it though)
/**
* Collects all records that need to be deleted by applying defined
* application-level delete cascades.
*
* @param array $deletions Map of the records to delete. Keys=Oids Values=Records.
*/
/*private function _collectDeletions(Doctrine_Record $record, array &$deletions)
{
if ( ! $record->exists()) {
return;
}
$deletions[$record->getOid()] = $record;
$this->_cascadeDelete($record, $deletions);
}*/
/**
* Cascades an ongoing delete operation to related objects. Applies only on relations
* that have 'delete' in their cascade options.
* This is an application-level cascade. Related objects that participate in the
* cascade and are not yet loaded are fetched from the database.
* Exception: many-valued relations are always (re-)fetched from the database to
* make sure we have all of them.
*
* @param Doctrine_Record The record for which the delete operation will be cascaded.
* @throws PDOException If something went wrong at database level
* @return void
*/
/*protected function _cascadeDelete(Doctrine_Record $record, array &$deletions)
{
foreach ($record->getTable()->getRelations() as $relation) {
if ($relation->isCascadeDelete()) {
$fieldName = $relation->getAlias();
// if it's a xToOne relation and the related object is already loaded
// we don't need to refresh.
if ( ! ($relation->getType() == Doctrine_Relation::ONE && isset($record->$fieldName))) {
$record->refreshRelated($relation->getAlias());
}
$relatedObjects = $record->get($relation->getAlias());
if ($relatedObjects instanceof Doctrine_Record && $relatedObjects->exists()
&& ! isset($deletions[$relatedObjects->getOid()])) {
$this->_collectDeletions($relatedObjects, $deletions);
} else if ($relatedObjects instanceof Doctrine_Collection && count($relatedObjects) > 0) {
// cascade the delete to the other objects
foreach ($relatedObjects as $object) {
if ( ! isset($deletions[$object->getOid()])) {
$this->_collectDeletions($object, $deletions);
}
}
}
}
}
}*/
/**
* Executes the deletions for all collected records during a delete operation
* (usually triggered through $record->delete()).
*
* @param array $deletions Map of the records to delete. Keys=Oids Values=Records.
*/
/*private function _executeDeletions(array $deletions)
{
// collect class names
$classNames = array();
foreach ($deletions as $record) {
$classNames[] = $record->getTable()->getComponentName();
}
$classNames = array_unique($classNames);
// order deletes
$executionOrder = $this->buildFlushTree($classNames);
// execute
try {
$this->conn->beginInternalTransaction();
for ($i = count($executionOrder) - 1; $i >= 0; $i--) {
$className = $executionOrder[$i];
$table = $this->conn->getTable($className);
// collect identifiers
$identifierMaps = array();
$deletedRecords = array();
foreach ($deletions as $oid => $record) {
if ($record->getTable()->getComponentName() == $className) {
$veto = $this->_preDelete($record);
if ( ! $veto) {
$identifierMaps[] = $record->identifier();
$deletedRecords[] = $record;
unset($deletions[$oid]);
}
}
}
if (count($deletedRecords) < 1) {
continue;
}
// extract query parameters (only the identifier values are of interest)
$params = array();
$columnNames = array();
foreach ($identifierMaps as $idMap) {
while (list($fieldName, $value) = each($idMap)) {
$params[] = $value;
$columnNames[] = $table->getColumnName($fieldName);
}
}
$columnNames = array_unique($columnNames);
// delete
$tableName = $table->getTableName();
$sql = "DELETE FROM " . $this->conn->quoteIdentifier($tableName) . " WHERE ";
if ($table->isIdentifierComposite()) {
$sql .= $this->_buildSqlCompositeKeyCondition($columnNames, count($identifierMaps));
$this->conn->exec($sql, $params);
} else {
$sql .= $this->_buildSqlSingleKeyCondition($columnNames, count($params));
$this->conn->exec($sql, $params);
}
// adjust state, remove from identity map and inform postDelete listeners
foreach ($deletedRecords as $record) {
// currently just for bc!
$this->_deleteCTIParents($table, $record);
//--
$record->state(Doctrine_Record::STATE_TCLEAN);
$record->getTable()->removeRecord($record);
$this->_postDelete($record);
}
}
$this->conn->commit();
// trigger postDelete for records skipped during the deletion (veto!)
foreach ($deletions as $skippedRecord) {
$this->_postDelete($skippedRecord);
}
return true;
} catch (Exception $e) {
$this->conn->rollback();
throw $e;
}
}*/
/**
* Builds the SQL condition to target multiple records who have a single-column
* primary key.
*
* @param Doctrine_Table $table The table from which the records are going to be deleted.
* @param integer $numRecords The number of records that are going to be deleted.
* @return string The SQL condition "pk = ? OR pk = ? OR pk = ? ..."
*/
/*private function _buildSqlSingleKeyCondition($columnNames, $numRecords)
{
$idColumn = $this->conn->quoteIdentifier($columnNames[0]);
return implode(' OR ', array_fill(0, $numRecords, "$idColumn = ?"));
}*/
/**
* Builds the SQL condition to target multiple records who have a composite primary key.
*
* @param Doctrine_Table $table The table from which the records are going to be deleted.
* @param integer $numRecords The number of records that are going to be deleted.
* @return string The SQL condition "(pk1 = ? AND pk2 = ?) OR (pk1 = ? AND pk2 = ?) ..."
*/
/*private function _buildSqlCompositeKeyCondition($columnNames, $numRecords)
{
$singleCondition = "";
foreach ($columnNames as $columnName) {
$columnName = $this->conn->quoteIdentifier($columnName);
if ($singleCondition === "") {
$singleCondition .= "($columnName = ?";
} else {
$singleCondition .= " AND $columnName = ?";
}
}
$singleCondition .= ")";
$fullCondition = implode(' OR ', array_fill(0, $numRecords, $singleCondition));
return $fullCondition;
}*/
public function getIdentityMap()
{
return $this->_identityMap;
}
}