blob: a2c3b6f3855958163dc3a9547e5d13de26e9476a [file] [log] [blame]
<?php
/**
* File containing the ezcPersistentBasicIdentityMap class.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package PersistentObject
* @version //autogen//
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
*/
/**
* Default identity map used in ezcPersistentSessionIdentityDecorator.
*
* An instance of this class is used in {@link
* ezcPersistentSessionIdentityDecorator} to perform the internal work of
* storing and retrieving object identities.
*
* @package PersistentObject
* @version //autogen//
*/
class ezcPersistentBasicIdentityMap implements ezcPersistentIdentityMap
{
/**
* Object identities.
*
* Structure:
*
* <code>
* <?php
* array(
* '<className>' => array(
* '<id1>' => ezcPersistentIdentity(),
* '<id2>' => ezcPersistentIdentity(),
* // ...
* ),
* '<anotherClassName>' => array(
* '<idA>' => ezcPersistentIdentity(),
* '<idB>' => ezcPersistentIdentity(),
* // ...
* ),
* // ...
* );
* ?>
* </code>
*
* @var array(string=>array(mixed=>ezcPersistentIdentity))
*/
protected $identities = array();
/**
* Definition manager used by {@link ezcPersistentSession}.
*
* @var ezcPersistentDefinitionManager
*/
protected $definitionManager;
/**
* Creates a new identity map.
*
* Creates a new identity map, which makes use of the given
* $definitionManager to determine object identities and relations.
*
* @param ezcPersistentDefinitionManager $definitionManager
*/
public function __construct( ezcPersistentDefinitionManager $definitionManager )
{
$this->definitionManager = $definitionManager;
}
/**
* Records the identity of $object.
*
* Records the identity of $object. If an identity is already recorded for
* this object, it is silently replaced. The user of this method must take
* care of checking for already recorded identities of the given $object
* itself.
*
* @param ezcPersistentObject $object
*/
public function setIdentity( $object )
{
$class = get_class( $object );
$def = $this->definitionManager->fetchDefinition( $class );
$state = $object->getState();
$id = $state[$def->idProperty->propertyName];
if ( !isset( $this->identities[$class] ) )
{
$this->identities[$class] = array();
}
$newIdentity = new ezcPersistentIdentity( $object );
if ( isset( $this->identities[$class][$id] ) )
{
$this->replaceIdentityReferences(
$this->identities[$class][$id],
$newIdentity
);
}
$this->identities[$class][$id] = $newIdentity;
}
/**
* Returns the object of $class with $id or null.
*
* Returns the object of $class with $id, if its identity has already been
* recorded. Otherwise null is returned.
*
* @param string $class
* @param mixed $id
* @return object($class)|null
*/
public function getIdentity( $class, $id )
{
if ( !isset( $this->identities[$class] ) )
{
return null;
}
if ( !isset( $this->identities[$class][$id] ) )
{
return null;
}
return $this->identities[$class][$id]->object;
}
/**
* Removes the object of $class width $id from the map.
*
* Removes the identity of the object of $class with $id from the map and
* deletes all references of it. If the identity does not exist, the call
* is silently ignored.
*
* @param string $class
* @param mixed $id
*/
public function removeIdentity( $class, $id )
{
if ( isset( $this->identities[$class][$id] ) )
{
$this->removeIdentityReferences( $this->identities[$class][$id] );
unset( $this->identities[$class][$id] );
}
}
/**
* Records a set of $relatedObjects to $sourceObject.
*
* Records the given set of $relatedObjects for $sourceObject.
* $relationName is the optional name of the relation, which must be
* provided, if multiple relations from $sourceObject to the class of the
* objects in $relatedObjects exist.
*
* In case a set of related objects has already been recorded for
* $sourceObject and the class of the objects in $relatedObjects (and
* optionally $relationName), the existing set is silently replaced and all
* references to it are removed.
*
* If for any object in $relatedObjects no identity is recorded, yet, it
* will be recorded. Otherwise, the object will be replaced by its existing
* identity in the set. Except for if the $replaceIdentities parameter is
* set to true: In this case a new identity will be recorded for every
* object in $relatedObjects, replacing potentially existing ones silently.
*
* If the given array of $relatedObjects is inconsistent (any contained
* object is not of $relatedClass), an {@link
* ezcPersistentIdentityRelatedObjectsInconsistentException} is thrown.
*
* To avoid a call to {@link getRelatedObjects()} after this method has
* been called, the recorded set of related objects (including potentially
* replaced identities) is returned.
*
* @param ezcPersistentObject $sourceObject
* @param array(ezcPersistentObject) $relatedObjects
* @param string $relatedClass
* @param string $relationName
* @param bool $replaceIdentities
*
* @return array(mixed=>object($relatedClass))
*
* @throws ezcPersistentIdentityRelatedObjectsInconsistentException
* if an object in $relatedObjects is not of $relatedClass.
*
*/
public function setRelatedObjects( $sourceObject, array $relatedObjects, $relatedClass, $relationName = null, $replaceIdentities = false )
{
$srcClass = get_class( $sourceObject );
$srcDef = $this->definitionManager->fetchDefinition( $srcClass );
$srcState = $sourceObject->getState();
$srcId = $srcState[$srcDef->idProperty->propertyName];
$relDef = $this->definitionManager->fetchDefinition( $relatedClass );
if ( !isset( $this->identities[$srcClass][$srcId] ) )
{
throw new ezcPersistentIdentityMissingException(
$srcClass,
$srcId
);
}
$srcIdentity = $this->identities[$srcClass][$srcId];
// Sanity checks already performed while loading related objects
$relationStoreName = $this->createRelationStoreName(
$relatedClass,
$relationName
);
// Remove references before replacing a set
if ( isset( $srcIdentity->relatedObjects[$relationStoreName] ) )
{
$this->removeReferences( $srcIdentity->relatedObjects[$relationStoreName] );
}
$relStore = new ArrayObject();
foreach ( $relatedObjects as $relObj )
{
if ( !( $relObj instanceof $relatedClass ) )
{
// Cleanup already set references before bailing out
$this->removeReferences( $relStore );
throw new ezcPersistentIdentityRelatedObjectsInconsistentException(
$srcClass, $srcId, $relatedClass, get_class( $relObj )
);
}
$relState = $relObj->getState();
$relId = $relState[$relDef->idProperty->propertyName];
// Check and replace identities
if ( !isset( $this->identities[$relatedClass][$relId] ) )
{
$this->identities[$relatedClass][$relId] = new ezcPersistentIdentity(
$relObj
);
}
else if ( $replaceIdentities )
{
// Replace identities on re-fetch
$newIdentity = new ezcPersistentIdentity( $relObj );
$this->replaceIdentityReferences(
$this->identities[$relatedClass][$relId],
$newIdentity
);
$this->identities[$relatedClass][$relId] = $newIdentity;
}
else
{
$relObj = $this->identities[$relatedClass][$relId]->object;
}
$relStore[$relId] = $relObj;
// Store reference
$this->identities[$relatedClass][$relId]->references->attach( $relStore );
}
$srcIdentity->relatedObjects[$relationStoreName] = $relStore;
return $relStore->getArrayCopy();
}
/**
* Records a named set of $relatedObjects for $sourceObject.
*
* Records the given array of $relatedObjects with as a "named related
* object sub-set" for $sourceObject, using $setName. A named "named
* related object sub-set" contains only objects related to $sourceObject,
* but not necessarily all such objects of a certain class. Such a set is
* the result of {@link ezcPersistentSessionIdentityDecorator::find()} with
* a find query generated by {@link
* ezcPersistentSessionIdentityDecorator::createFindQueryWithRelations()}
* and manipulated using a WHERE clause.
*
* In case a named set of related objects with $setName has already been
* recorded for $sourceObject, this set is silently replaced.
*
* If for any of the objects in $relatedObjects no identity is recorded,
* yet, it will be recorded. Otherwise, the object will be replaced by its
* existing identity in the set. Except for if $replaceIdentities is set to
* true: In this case a new identity will be recorded for every object in
* $relatedObjects.
*
* The method returns the created set of related objects to avoid another
* call to {@link getRelatedObjectSet()} by the using objct.
*
* @param ezcPersistentObject $sourceObject
* @param array(ezcPersistentObject) $relatedObjects
* @param string $setName
* @param bool $replaceIdentities
*
* @return array(ezcPersistentObject)
*
* @throws ezcPersistentIdentityRelatedObjectsInconsistentException
* if an object in $relatedObjects is not of $relatedClass.
*/
public function setRelatedObjectSet( $sourceObject, array $relatedObjects, $setName, $replaceIdentities = false )
{
$srcClass = get_class( $sourceObject );
$srcDef = $this->definitionManager->fetchDefinition( $srcClass );
$srcState = $sourceObject->getState();
$srcId = $srcState[$srcDef->idProperty->propertyName];
// Sanity checks
if ( !isset( $this->identities[$srcClass][$srcId] ) )
{
throw new ezcPersistentIdentityMissingException(
$srcClass,
$srcId
);
}
$srcIdentity = $this->identities[$srcClass][$srcId];
// Remove references before replacing a set
if ( isset( $srcIdentity->namedRelatedObjectSets[$setName] ) )
{
$this->removeReferences( $srcIdentity->namedRelatedObjectSets[$setName] );
}
$relDefs = array();
$relStore = new ArrayObject();
foreach ( $relatedObjects as $relObj )
{
$relClass = get_class( $relObj );
if ( !isset( $relDefs[$relClass] ) )
{
$relDefs[$relClass] = $this->definitionManager->fetchDefinition( $relClass );
}
$relState = $relObj->getState();
$relId = $relState[$relDefs[$relClass]->idProperty->propertyName];
// Check and replace identities
if ( !isset( $this->identities[$relClass][$relId] ) )
{
$this->identities[$relClass][$relId] = new ezcPersistentIdentity(
$relObj
);
}
else if ( $replaceIdentities )
{
// Replace identities on re-fetch
$newIdentity = new ezcPersistentIdentity( $relObj );
$this->replaceIdentityReferences(
$this->identities[$relClass][$relId],
$newIdentity
);
$this->identities[$relClass][$relId] = $newIdentity;
}
else
{
$relObj = $this->identities[$relClass][$relId]->object;
}
$relStore[$relId] = $relObj;
// Store reference
$this->identities[$relClass][$relId]->references->attach( $relStore );
}
$srcIdentity->namedRelatedObjectSets[$setName] = $relStore;
return $relStore->getArrayCopy();
}
/**
* Appends a new $relatedObject to a related object set of $sourceObject.
*
* Appends the given $relatedObject to the set of related objects for
* $sourceObject with the class of $relatedObject and optionally
* $relationName.
*
* In case that no set of related objects with the specific class has been
* recorded for $object, yet, the call is ignored and related objects are
* newly fetched whenever {@link getRelatedObjects()} is called.
*
* Note: All named related object sub-sets for $relatedObject are
* automatically invalidated by a call to the method. The identity map can
* not determine, to which named related object sub-set the $relatedObject
* might be added.
*
* @param ezcPersistentObject $sourceObject
* @param ezcPersistentObject $relatedObject
* @param string $relationName
*
* @throws ezcPersistentRelationNotFoundException
* if no relation from $sourceObject to $relatedObject is defined.
* @throws ezcPersistentIdentityMissingException
* if no identity has been recorded for $sourceObject or
* $relatedObject, yet.
* @throws ezcPersistentIdentityRelatedObjectAlreadyExistsException
* if the given $relatedObject is already part of the set of
* related objects it should be added to.
*/
public function addRelatedObject( $sourceObject, $relatedObject, $relationName = null )
{
$srcClass = get_class( $sourceObject );
$relClass = get_class( $relatedObject );
$srcDef = $this->definitionManager->fetchDefinition( $srcClass );
$relDef = $this->definitionManager->fetchDefinition( $relClass );
$srcState = $sourceObject->getState();
$srcId = $srcState[$srcDef->idProperty->propertyName];
if ( !isset( $this->identities[$srcClass][$srcId] ) )
{
throw new ezcPersistentIdentityMissingException(
$srcClass,
$srcId
);
}
$srcIdentity = $this->identities[$srcClass][$srcId];
$relState = $relatedObject->getState();
$relId = $relState[$relDef->idProperty->propertyName];
if ( !isset( $this->identities[$relClass][$relId] ) )
{
throw new ezcPersistentIdentityMissingException(
$relClass,
$relId
);
}
$relStoreName = $this->createRelationStoreName(
$relClass,
$relationName
);
if ( !isset( $srcIdentity->relatedObjects[$relStoreName] ) )
{
// Ignore call, since related objects for $relClass have not been stored, yet
return null;
}
$relStore = $srcIdentity->relatedObjects[$relStoreName];
if ( isset( $relStore[$relId] ) )
{
throw new ezcPersistentIdentityRelatedObjectAlreadyExistsException(
$srcClass, $srcId, $relClass, $relId, $relationName
);
}
$relStore[$relId] = $relatedObject;
// Store new reference
$this->identities[$relClass][$relId]->references->attach(
$relStore
);
// Invalidate all named sets, since they might be inconsistent now
$this->removeAllReferences(
$srcIdentity->namedRelatedObjectSets
);
$srcIdentity->namedRelatedObjectSets = array();
}
/**
* Removes a $relatedObject from the sets of related objects of $sourceObject.
*
* Removes the $relatedObject from all recorded sets of related objects
* (named and unnamed) for $sourceObject. This method (in contrast to
* {@link addRelatedObject()}) does not invalidate named related object
* sets, but simply removes the $relatedObject from them.
*
* @param ezcPersistentObject $sourceObject
* @param ezcPersistentObject $relatedObject
* @param string $relationName
*
* @throws ezcPersistentIdentityMissingException
* if no identity for $sourceObject has been recorded, yet.
*/
public function removeRelatedObject( $sourceObject, $relatedObject, $relationName = null )
{
$srcClass = get_class( $sourceObject );
$relClass = get_class( $relatedObject );
$srcDef = $this->definitionManager->fetchDefinition( $srcClass );
$relDef = $this->definitionManager->fetchDefinition( $relClass );
$srcState = $sourceObject->getState();
$srcId = $srcState[$srcDef->idProperty->propertyName];
$relState = $relatedObject->getState();
$relId = $relState[$relDef->idProperty->propertyName];
if ( !isset( $this->identities[$srcClass][$srcId] ) )
{
throw new ezcPersistentIdentityMissingException(
$srcClass,
$srcId
);
}
if ( !isset( $this->identities[$relClass][$relId] ) )
{
// Ignore call
return null;
}
$relationStoreName = $this->createRelationStoreName(
$relClass,
$relationName
);
$srcIdentity = $this->identities[$srcClass][$srcId];
$relIdentity = $this->identities[$relClass][$relId];
if ( isset( $srcIdentity->relatedObjects[$relationStoreName] ) )
{
unset( $srcIdentity->relatedObjects[$relationStoreName][$relId] );
$relIdentity->references->detach( $srcIdentity->relatedObjects[$relationStoreName] );
}
foreach ( $srcIdentity->namedRelatedObjectSets as $setName => $rels )
{
if ( isset( $rels[$relId] ) && $rels[$relId] instanceof $relClass )
{
unset( $srcIdentity->namedRelatedObjectSets[$setName][$relId] );
$relIdentity->references->detach(
$srcIdentity->namedRelatedObjectSets[$setName]
);
}
}
}
/**
* Returns the set of related objects of $relatedClass for $sourceObject.
*
* Returns the set of related objects of $relatedClass for $sourceObject.
* This might also be an empty set (empty array). In case no related
* objects are recorded, yet, null is returned.
*
* @param ezcPersistentObject $sourceObject
* @param string $relatedClass
* @param string $relationName
*
* @return array(object($relatedClass))|null
*
* @throws ezcPersistentRelationNotFoundException
* if not relation between the class of $sourceObject and
* $relatedClass (with optionally $relationName) is defined.
*/
public function getRelatedObjects( $sourceObject, $relatedClass, $relationName = null )
{
$srcClass = get_class( $sourceObject );
$srcDef = $this->definitionManager->fetchDefinition( $srcClass );
$srcState = $sourceObject->getState();
$srcId = $srcState[$srcDef->idProperty->propertyName];
if ( !isset( $srcDef->relations[$relatedClass] ) )
{
throw new ezcPersistentRelationNotFoundException(
$srcClass,
$relatedClass,
$relationName
);
}
$relationStoreName = $this->createRelationStoreName(
$relatedClass,
$relationName
);
// Sanity checks
if ( !isset( $this->identities[$srcClass][$srcId] ) )
{
// No object identity
return null;
}
$srcIdentity = $this->identities[$srcClass][$srcId];
if ( isset( $srcIdentity->relatedObjects[$relationStoreName] ) )
{
// Return a real array here, not the ArrayObject stored
return $srcIdentity->relatedObjects[$relationStoreName]->getArrayCopy();
}
return null;
}
/**
* Returns the named set of related objects for $sourceObject with $setName.
*
* Returns the named set of related objects for $sourceObject identified by
* $setName. This might also be an empty set (empty array). In case no
* related objects with this name are recorded, yet, null is returned.
*
* @param ezcPersistentObject $sourceObject
* @param string $setName
* @return array(object($relatedClass))|null
*/
public function getRelatedObjectSet( $sourceObject, $setName )
{
$srcClass = get_class( $sourceObject );
$srcDef = $this->definitionManager->fetchDefinition( $srcClass );
$srcState = $sourceObject->getState();
$srcId = $srcState[$srcDef->idProperty->propertyName];
if ( !isset( $this->identities[$srcClass][$srcId] ) )
{
return null;
}
$identity = $this->identities[$srcClass][$srcId];
if ( isset( $identity->namedRelatedObjectSets[$setName] ) )
{
return $identity->namedRelatedObjectSets[$setName]->getArrayCopy();
}
return null;
}
/**
* Resets the complete identity map.
*
* Removes all stored identities from the map and resets it into its
* initial state.
*/
public function reset()
{
$this->identities = array();
}
/**
* Creates the related object set identifier for $relatedClass and $relationName.
*
* Determines the unique name for relations of $relatedClass with
* $relationName (can be null).
*
* @param string $relatedClass
* @param string $relationName
* @return string
*/
protected function createRelationStoreName( $relatedClass, $relationName )
{
return $relatedClass
. ( $relationName !== null ? "__{$relationName}" : '' );
}
/**
* Removes all references to all $sets from all objects in $sets.
*
* Removes all references to all object $sets from all objects contained in
* each of the $sets.
*
* @param array(ArrayObject) $sets
* @see removeReferences()
*/
protected function removeAllReferences( array $sets )
{
foreach ( $sets as $set )
{
$this->removeReferences( $set );
}
}
/**
* Removes all references to $set from the objects in $set.
*
* Maintains the {@link ezcPersistentIdentity::$references} attribute by
* removing all refereneces to $set from all object identities contained in
* $set.
*
* @param ArrayObject $set
*/
protected function removeReferences( ArrayObject $set )
{
foreach ( $set as $obj )
{
$class = get_class( $obj );
$def = $this->definitionManager->fetchDefinition( $class );
$state = $obj->getState();
$id = $state[$def->idProperty->propertyName];
if ( $this->identities[$class][$id]->references->contains( $set ) )
{
$this->identities[$class][$id]->references->detach( $set );
}
}
}
/**
* Replaces all references to $oldIdentity with a reference to $newIdentity.
*
* Scans all sets refered in {@link ezcPersistentIdentity::$references} of
* the $oldIdentity and replaces the references to $oldIdentity with
* $newIdentity in them. This mechanism is used whenever an identity is to
* be replaced by a new one.
*
* @param ezcPersistentIdentity $oldIdentity
* @param ezcPersistentIdentity $newIdentity
*/
protected function replaceIdentityReferences( ezcPersistentIdentity $oldIdentity, ezcPersistentIdentity $newIdentity )
{
foreach( $oldIdentity->references as $refList )
{
$replaceIds = array();
// Needs iteration here, to determine key
foreach ( $refList->getIterator() as $refId => $refItem )
{
if ( $refItem === $oldIdentity->object )
{
$replaceIds[] = $refId;
}
}
foreach ( $replaceIds as $replaceId )
{
// Replace object in related sets
$refList[$replaceId] = $newIdentity->object;
}
}
}
/**
* Removes all references to $identity.
*
* Scans all sets referred in {@link ezcPersistentIdentity::$references}
* and removes $identity from them. This is used, if the $identity is to be
* {@link removeIdenity()}, to ensure it does not occur in any related set
* or named related set anymore.
*
* @param ezcPersistentIdentity $identity
*/
protected function removeIdentityReferences( ezcPersistentIdentity $identity )
{
foreach( $identity->references as $refList )
{
$removeIds = array();
// Needs iteration here, to determine key
foreach ( $refList->getIterator() as $refId => $refItem )
{
if ( $refItem === $identity->object )
{
$removeIds[] = $refId;
}
}
foreach ( $removeIds as $removeId )
{
// Remove object from related set
unset( $refList[$removeId] );
}
}
}
}
?>