. */ #namespace Doctrine::ORM; /** * Base class for all Entities (objects with persistent state in a RDBMS that are * managed by Doctrine). Kind of a Layer Supertype. * * NOTE: Methods that are intended for internal use only but must be public * are marked INTERNAL: and begin with an underscore "_" to indicate that they * ideally would not be public and to minimize naming collisions. * * The "final" modifiers on most methods prevent accidental overrides. * It is not desirable that subclasses can override these methods. * The persistence layer should stay in the background as much as possible. * * @author Konsta Vesterinen * @author Roman Borschel * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.phpdoctrine.org * @since 2.0 * @version $Revision: 4342 $ */ abstract class Doctrine_Entity implements ArrayAccess, Serializable { /** * MANAGED * An Entity is in managed state when it has a primary key/identifier and is * managed by an EntityManager (registered in the identity map). */ const STATE_MANAGED = 1; /** * NEW * An Entity is new if it does not yet have an identifier/primary key * and is not (yet) managed by an EntityManager. */ const STATE_NEW = 2; /** * LOCKED STATE * An Entity is temporarily locked during deletes and saves. * * This state is used internally to ensure that circular deletes * and saves will not cause infinite loops. * @todo Not sure this is a good idea. It is a problematic solution because * it hides the original state while the locked state is active. */ const STATE_LOCKED = 6; /** * A detached Entity is an instance with a persistent identity that is not * (or no longer) associated with an EntityManager (and a UnitOfWork). * This means its no longer in the identity map. */ const STATE_DETACHED = 3; /** * A removed Entity instance is an instance with a persistent identity, * associated with an EntityManager, whose persistent state has been * deleted (or is scheduled for deletion). */ const STATE_DELETED = 4; /** * Index used for creating object identifiers (oid's). * * @var integer */ private static $_index = 1; /** * Boolean flag that indicates whether automatic accessor overriding is enabled. * * @var boolean */ private static $_useAutoAccessorOverride; /** * The accessor cache is used as a memory for the existance of custom accessors. * * @var array */ private static $_accessorCache = array(); /** * The mutator cache is used as a memory for the existance of custom mutators. * * @var array */ private static $_mutatorCache = array(); /** * The class descriptor. * * @var Doctrine::ORM::ClassMetadata */ private $_class; /** * The name of the Entity. * * @var string */ private $_entityName; /** * The values that make up the ID/primary key of the entity. * * @var array */ private $_id = array(); /** * The entity data. * * @var array */ private $_data = array(); /** * The state of the object. * * @var integer */ private $_state; /** * Name => Value map of join columns. * * @var array * @todo Not yet clear if needed. */ //private $_joinColumns = array(); /** * The changes that happened to fields of the entity. * Keys are field names, values oldValue => newValue tuples. * * @var array */ private $_dataChangeSet = array(); /** * The changes that happened to references of the entity to other entities. * Keys are field names, values oldReference => newReference tuples. * * With one-one associations, a reference change means the reference has been * swapped out / replaced by another one. * * With one-many, many-many associations, a reference change means the complete * collection has been sweapped out / replaced by another one. * * @var array */ private $_referenceChangeSet = array(); /** * The references for all associations of the entity to other entities. * Keys are field names, values object references. * * @var array */ private $_references = array(); /** * The EntityManager that is responsible for the persistent state of the entity. * * @var Doctrine::ORM::EntityManager */ private $_em; /** * The object identifier of the object. Each object has a unique identifier * during script execution. * * @var integer */ private $_oid; /** * Constructor. * Creates a new Entity instance. */ public function __construct() { $this->_entityName = get_class($this); $this->_em = Doctrine_EntityManagerFactory::getManager($this->_entityName); $this->_class = $this->_em->getClassMetadata($this->_entityName); $this->_oid = self::$_index++; $this->_data = $this->_em->_getTmpEntityData(); if ($this->_data) { $this->_extractIdentifier(); $this->_state = self::STATE_MANAGED; } else { $this->_state = self::STATE_NEW; } // @todo read from attribute the first time and move this initialization elsewhere. self::$_useAutoAccessorOverride = true; } /** * Returns the object identifier. * * @return integer */ final public function getOid() { return $this->_oid; } /** * Copies the identifier names and values from _data into _id. */ private function _extractIdentifier() { if ( ! $this->_class->isIdentifierComposite()) { // Single field identifier $name = $this->_class->getIdentifier(); $name = $name[0]; if (isset($this->_data[$name]) && $this->_data[$name] !== Doctrine_Null::$INSTANCE) { $this->_id[$name] = $this->_data[$name]; } } else { // Composite identifier $names = $this->_class->getIdentifier(); foreach ($names as $name) { if ($this->_data[$name] === Doctrine_Null::$INSTANCE) { $this->_id[$name] = null; } else { $this->_id[$name] = $this->_data[$name]; } } } } /** * Serializes the entity. * This method is automatically called when the entity is serialized. * * Part of the implementation of the Serializable interface. * * @return string */ public function serialize() { //$this->_em->getEventManager()->dispatchEvent(Event::preSerialize); //$this->_class->dispatchLifecycleEvent(Event::preSerialize, $this); $vars = get_object_vars($this); unset($vars['_references']); unset($vars['_em']); //$name = (array)$this->_table->getIdentifier(); $this->_data = array_merge($this->_data, $this->_id); foreach ($this->_data as $k => $v) { if ($v instanceof Doctrine_Entity && $this->_class->getTypeOfField($k) != 'object') { unset($vars['_data'][$k]); } else if ($v === Doctrine_Null::$INSTANCE) { unset($vars['_data'][$k]); } else { switch ($this->_class->getTypeOfField($k)) { case 'array': case 'object': $vars['_data'][$k] = serialize($vars['_data'][$k]); break; case 'gzip': $vars['_data'][$k] = gzcompress($vars['_data'][$k]); break; case 'enum': $vars['_data'][$k] = $this->_class->enumIndex($k, $vars['_data'][$k]); break; } } } $str = serialize($vars); //$this->postSerialize($event); return $str; } /** * Reconstructs the entity from it's serialized form. * This method is automatically called everytime the entity is unserialized. * * @param string $serialized Doctrine_Entity as serialized string * @throws Doctrine_Record_Exception if the cleanData operation fails somehow * @return void */ public function unserialize($serialized) { //$event = new Doctrine_Event($this, Doctrine_Event::RECORD_UNSERIALIZE); //$this->preUnserialize($event); $this->_entityName = get_class($this); $manager = Doctrine_EntityManagerFactory::getManager($this->_entityName); $connection = $manager->getConnection(); $this->_oid = self::$_index; self::$_index++; $this->_em = $manager; $array = unserialize($serialized); foreach($array as $k => $v) { $this->$k = $v; } $this->_class = $this->_em->getClassMetadata($this->_entityName); foreach ($this->_data as $k => $v) { switch ($this->_class->getTypeOfField($k)) { case 'array': case 'object': $this->_data[$k] = unserialize($this->_data[$k]); break; case 'gzip': $this->_data[$k] = gzuncompress($this->_data[$k]); break; case 'enum': $this->_data[$k] = $this->_class->enumValue($k, $this->_data[$k]); break; } } $this->_extractIdentifier(!$this->isNew()); //$this->postUnserialize($event); } /** * INTERNAL: * Gets or sets the state of this Entity. * * @param integer|string $state if set, this method tries to set the record state to $state * @see Doctrine_Entity::STATE_* constants * * @throws Doctrine_Record_State_Exception if trying to set an unknown state * @return null|integer */ final public function _state($state = null) { if ($state == null) { return $this->_state; } $this->_state = $state; } /** * Gets the current field values. * * @return array The fields and their values. */ final public function getData() { return $this->_data; } /** * Gets the value of a field (regular field or reference). * * @param $name Name of the field. * @return mixed Value of the field. * @throws Doctrine::ORM::Exceptions::EntityException If trying to get an unknown field. */ final protected function _get($fieldName) { $nullObj = Doctrine_Null::$INSTANCE; if (isset($this->_data[$fieldName])) { return $this->_data[$fieldName] !== $nullObj ? $this->_data[$fieldName] : null; } else if (isset($this->_references[$fieldName])) { return $this->_references[$fieldName] !== $nullObj ? $this->_references[$fieldName] : null; } else { if ($this->_class->hasField($fieldName)) { return null; } else if ($this->_class->hasAssociation($fieldName)) { $rel = $this->_class->getAssociationMapping($fieldName); if ($rel->isLazilyFetched()) { $this->_references[$fieldName] = $rel->lazyLoadFor($this); return $this->_references[$fieldName] !== $nullObj ? $this->_references[$fieldName] : null; } else { return null; } } else { throw Doctrine_Entity_Exception::invalidField($fieldName); } } } /** * Sets the value of a field (regular field or reference). * * @param $fieldName The name of the field. * @param $value The value of the field. * @return void * @throws Doctrine::ORM::Exceptions::EntityException */ final protected function _set($fieldName, $value) { if ($this->_class->hasField($fieldName)) { $old = isset($this->_data[$fieldName]) ? $this->_data[$fieldName] : null; // NOTE: Common case: $old != $value. Special case: null == 0 (TRUE), which // is addressed by the type comparison. if ($old != $value || gettype($old) != gettype($value)) { $this->_data[$fieldName] = $value; $this->_dataChangeSet[$fieldName] = array($old => $value); if ($this->isNew() && $this->_class->isIdentifier($fieldName)) { $this->_id[$fieldName] = $value; } $this->_registerDirty(); } } else if ($this->_class->hasAssociation($fieldName)) { $old = isset($this->_references[$fieldName]) ? $this->_references[$fieldName] : null; if ($old !== $value) { $this->_internalSetReference($fieldName, $value); $this->_referenceChangeSet[$fieldName] = array($old => $value); $this->_registerDirty(); //TODO: Allow arrays in $value. Wrap them in a collection transparently. if ($old instanceof Doctrine_Collection) { $this->_em->getUnitOfWork()->scheduleCollectionDeletion($old); } if ($value instanceof Doctrine_Collection) { $this->_em->getUnitOfWork()->scheduleCollectionRecreation($value); } } } else { throw Doctrine_Entity_Exception::invalidField($fieldName); } } private function _registerDirty() { if ($this->_state == self::STATE_MANAGED && ! $this->_em->getUnitOfWork()->isRegisteredDirty($this)) { $this->_em->getUnitOfWork()->registerDirty($this); } } /** * INTERNAL: * Gets the value of a field. * * NOTE: Use of this method in userland code is strongly discouraged. * This method does NOT check whether the field exists. * _get() in extending classes should be preferred. * * @param string $fieldName * @return mixed */ final public function _internalGetField($fieldName) { if ($this->_data[$fieldName] === Doctrine_Null::$INSTANCE) { return null; } return $this->_data[$fieldName]; } /** * INTERNAL: * Sets the value of a field. * * NOTE: Use of this method in userland code is strongly discouraged. * This method does NOT check whether the field exists. * _set() in extending classes should be preferred. * * @param string $fieldName * @param mixed $value */ final public function _internalSetField($fieldName, $value) { $this->_data[$fieldName] = $value; } /** * INTERNAL: * Gets a reference to another Entity. * * NOTE: Use of this method in userland code is strongly discouraged. * This method does NOT check whether the reference exists. * * @param string $fieldName */ final public function _internalGetReference($fieldName) { if ($this->_references[$fieldName] === Doctrine_Null::$INSTANCE) { return null; } return $this->_references[$fieldName]; } /** * INTERNAL: * Sets a reference to another entity or a collection of entities. * * NOTE: Use of this method in userland code is strongly discouraged. * * @param string $fieldName * @param mixed $value */ final public function _internalSetReference($name, $value) { if ($value === Doctrine_Null::$INSTANCE) { $this->_references[$name] = $value; return; } $rel = $this->_class->getAssociationMapping($name); if ($rel->isOneToOne() && ! $value instanceof Doctrine_Entity) { throw Doctrine_Entity_Exception::invalidValueForOneToOneReference(); } if ($rel->isOneToMany() && ! $value instanceof Doctrine_Collection) { throw Doctrine_Entity_Exception::invalidValueForOneToManyReference(); } if ($rel->isManyToMany() && ! $value instanceof Doctrine_Collection) { throw Doctrine_Entity_Exception::invalidValueForManyToManyReference(); } $this->_references[$name] = $value; } /** * Generic getter for all (persistent) fields of the entity. * * Invoked by Doctrine::ORM::Access#__get(). * * @param string $fieldName Name of the field. * @return mixed * @override */ final public function get($fieldName) { if ($getter = $this->_getCustomAccessor($fieldName)) { return $this->$getter(); } return $this->_get($fieldName); } /** * Gets the custom mutator method for a field, if it exists. * * @param string $fieldName The field name. * @return mixed The name of the custom mutator or FALSE, if the field does * not have a custom mutator. */ private function _getCustomMutator($fieldName) { if ( ! isset(self::$_mutatorCache[$this->_entityName][$fieldName])) { if (self::$_useAutoAccessorOverride) { $setterMethod = 'set' . Doctrine::classify($fieldName); if (method_exists($this, $setterMethod)) { self::$_mutatorCache[$this->_entityName][$fieldName] = $setterMethod; } else { self::$_mutatorCache[$this->_entityName][$fieldName] = false; } } if ($setter = $this->_class->getCustomMutator($fieldName)) { self::$_mutatorCache[$this->_entityName][$fieldName] = $setter; } else if ( ! isset(self::$_mutatorCache[$this->_entityName][$fieldName])) { self::$_mutatorCache[$this->_entityName][$fieldName] = false; } } return self::$_mutatorCache[$this->_entityName][$fieldName]; } /** * Gets the custom accessor method of a field, if it exists. * * @param string $fieldName The field name. * @return mixed The name of the custom accessor method, or FALSE if the * field does not have a custom accessor. */ private function _getCustomAccessor($fieldName) { if ( ! isset(self::$_accessorCache[$this->_entityName][$fieldName])) { if (self::$_useAutoAccessorOverride) { $getterMethod = 'get' . Doctrine::classify($fieldName); if (method_exists($this, $getterMethod)) { self::$_accessorCache[$this->_entityName][$fieldName] = $getterMethod; } else { self::$_accessorCache[$this->_entityName][$fieldName] = false; } } if ($getter = $this->_class->getCustomAccessor($fieldName)) { self::$_accessorCache[$this->_entityName][$fieldName] = $getter; } else if ( ! isset(self::$_accessorCache[$this->_entityName][$fieldName])) { self::$_accessorCache[$this->_entityName][$fieldName] = false; } } return self::$_accessorCache[$this->_entityName][$fieldName]; } /** * Gets the entity class name. * * @return string */ final public function getClassName() { return $this->_entityName; } /** * Generic setter for (persistent) fields of the entity. * * Invoked by Doctrine::ORM::Access#__set(). * * @param string $name The name of the field to set. * @param mixed $value The value of the field. * @override */ final public function set($fieldName, $value) { if ($setter = $this->_getCustomMutator($fieldName)) { return $this->$setter($value); } $this->_set($fieldName, $value); } /** * Checks whether a field is set (not null). * * NOTE: Invoked by Doctrine::ORM::Access#__isset(). * * @param string $name * @return boolean */ private function _contains($fieldName) { if (isset($this->_data[$fieldName])) { if ($this->_data[$fieldName] === Doctrine_Null::$INSTANCE) { return false; } return true; } if (isset($this->_id[$fieldName])) { return true; } if (isset($this->_references[$fieldName]) && $this->_references[$fieldName] !== Doctrine_Null::$INSTANCE) { return true; } return false; } /** * Clears the value of a field. * * @param string $name * @return void */ private function _unset($fieldName) { if (isset($this->_data[$fieldName])) { $this->_data[$fieldName] = null; } else if (isset($this->_references[$fieldName])) { $assoc = $this->_class->getAssociationMapping($fieldName); if ($assoc->isOneToOne() && $assoc->shouldDeleteOrphans()) { $this->_em->delete($this->_references[$fieldName]); } else if ($assoc->isOneToMany() && $assoc->shouldDeleteOrphans()) { foreach ($this->_references[$fieldName] as $entity) { $this->_em->delete($entity); } } $this->_references[$fieldName] = null; } } /** * INTERNAL: * Gets the changeset of the entities data. * * @return array */ final public function _getDataChangeSet() { return $this->_dataChangeSet; } /** * INTERNAL: * Gets the changeset of the entities references to other entities. * * @return array */ final public function _getReferenceChangeSet() { return $this->_referenceChangeSet; } /** * Checks whether the entity already has a persistent state. * * @return boolean TRUE if the object is new, FALSE otherwise. */ final public function isNew() { return $this->_state == self::STATE_NEW; } /** * Checks whether the entity has been modified since it was last synchronized * with the database. * * @return boolean TRUE if the object has been modified, FALSE otherwise. */ final public function isModified() { return count($this->_fieldChangeSet) > 0; } /** * INTERNAL: * Assigns an identifier to the entity. This is only intended for use by * the EntityPersisters or the UnitOfWork. * * @param mixed $id */ final public function _assignIdentifier($id) { if (is_array($id)) { foreach ($id as $fieldName => $value) { $this->_id[$fieldName] = $value; $this->_data[$fieldName] = $value; } } else { $name = $this->_class->getSingleIdentifierFieldName(); $this->_id[$name] = $id; $this->_data[$name] = $id; } $this->_dataChangeSet = array(); $this->_referenceChangeSet = array(); } /** * @todo Not yet clear if needed. */ /*final public function _setJoinColumn($columnName, $value) { $this->_joinColumns[$columnName] = $value; }*/ /** * @todo Not yet clear if needed. */ /*final public function _getJoinColumn($columnName) { return $this->_joinColumns[$columnName]; }*/ /** * INTERNAL: * Returns the primary keys of the entity (field => value pairs). * * @return array */ final public function _identifier() { return $this->_id; } /** * INTERNAL: * * getReferences * @return array all references */ final public function _getReferences() { return $this->_references; } /** * Gets the ClassMetadata object that describes the entity class. * * @return Doctrine::ORM::Mapping::ClassMetadata */ final public function getClass() { return $this->_class; } /** * Gets the EntityManager that is responsible for the persistence of * the entity. * * @return Doctrine::ORM::EntityManager */ final public function getEntityManager() { return $this->_em; } /** * Gets the EntityRepository of the Entity. * * @return Doctrine::ORM::EntityRepository */ final public function getRepository() { return $this->_em->getRepository($this->_entityName); } /** * Helps freeing the memory occupied by the entity. * Cuts all references the entity has to other entities and removes the entity * from the instance pool. * Note: The entity is no longer useable after free() has been called. Any operations * done with the entity afterwards can lead to unpredictable results. * * @param boolean $deep Whether to cascade the free() call to (loaded) associated entities. */ public function free($deep = false) { if ($this->_state != self::STATE_LOCKED) { $this->_em->detach($this); $this->_data = array(); $this->_id = array(); if ($deep) { foreach ($this->_references as $name => $reference) { if ( ! ($reference instanceof Doctrine_Null)) { $reference->free($deep); } } } $this->_references = array(); } } /** * Check if an offsetExists. * * Part of the ArrayAccess implementation. * * @param mixed $offset * @return boolean whether or not this object contains $offset */ public function offsetExists($offset) { return $this->_contains($offset); } /** * offsetGet an alias of get() * * Part of the ArrayAccess implementation. * * @see get, __get * @param mixed $offset * @return mixed */ public function offsetGet($offset) { return $this->get($offset); } /** * Part of the ArrayAccess implementation. * * sets $offset to $value * @see set, __set * @param mixed $offset * @param mixed $value * @return void */ public function offsetSet($offset, $value) { return $this->set($offset, $value); } /** * Part of the ArrayAccess implementation. * * unset a given offset * @see set, offsetSet, __set * @param mixed $offset */ public function offsetUnset($offset) { return $this->_unset($offset); } /** * __set * * @see set, offsetSet * @param $name * @param $value * @since 1.0 * @return void */ public function __set($name, $value) { $this->set($name, $value); } /** * __get * * @see get, offsetGet * @param mixed $name * @return mixed */ public function __get($name) { return $this->get($name); } /** * __isset() * * @param string $name * @since 1.0 * @return boolean whether or not this object contains $name */ public function __isset($name) { return $this->_contains($name); } /** * __unset() * * @param string $name * @since 1.0 * @return void */ public function __unset($name) { return $this->_unset($name); } /** * returns a string representation of this object */ public function __toString() { return (string)$this->_oid; } }