/framework/Rdo/lib/Horde/Rdo/Base.php
PHP | 449 lines | 200 code | 39 blank | 210 comment | 33 complexity | 2922f131843146fb29c130c48884ebe1 MD5 | raw file
- <?php
- /**
- * @category Horde
- * @package Rdo
- */
- /**
- * Horde_Rdo_Base abstract class (Rampage Data Objects). Entity
- * classes extend this baseline.
- *
- * @category Horde
- * @package Rdo
- */
- abstract class Horde_Rdo_Base implements IteratorAggregate, ArrayAccess
- {
- /**
- * The Horde_Rdo_Mapper instance associated with this Rdo object. The
- * Mapper takes care of all backend access.
- *
- * @see Horde_Rdo_Mapper
- * @var Horde_Rdo_Mapper
- */
- protected $_mapper;
- /**
- * This object's fields.
- *
- * @var array
- */
- protected $_fields = array();
- /**
- * Constructor. Can be called directly by a programmer, or is
- * called in Horde_Rdo_Mapper::map(). Takes an associative array
- * of initial object values.
- *
- * @param array $fields Initial values for the new object.
- *
- * @see Horde_Rdo_Mapper::map()
- */
- public function __construct($fields = array())
- {
- $this->setFields($fields);
- }
- /**
- * When Rdo objects are cloned, unset the unique id that
- * identifies them so that they can be modified and saved to the
- * backend as new objects. If you don't really want a new object,
- * don't clone.
- */
- public function __clone()
- {
- // @TODO Support composite primary keys
- unset($this->{$this->getMapper()->primaryKey});
- // @TODO What about associated objects?
- }
- /**
- * Fetch fields that haven't yet been loaded. Lazy-loaded fields
- * and lazy-loaded relationships are handled this way. Once a
- * field is retrieved, it is cached in the $_fields array so it
- * doesn't need to be fetched again.
- *
- * @param string $field The name of the field to access.
- *
- * @return mixed The value of $field or null.
- */
- public function __get($field)
- {
- // Honor any explicit getters.
- $fieldMethod = 'get' . ucfirst($field);
- // If an Rdo_Base subclass has a __call() method, is_callable
- // returns true on every method name, so use method_exists
- // instead.
- if (method_exists($this, $fieldMethod)) {
- return call_user_func(array($this, $fieldMethod));
- }
- if (isset($this->_fields[$field])) {
- return $this->_fields[$field];
- }
- $mapper = $this->getMapper();
- // Look for lazy fields first, then relationships.
- if (in_array($field, $mapper->lazyFields)) {
- // @TODO Support composite primary keys
- $query = new Horde_Rdo_Query($mapper);
- $query->setFields($field)
- ->addTest($mapper->primaryKey, '=', $this->{$mapper->primaryKey});
- list($sql, $params) = $query->getQuery();
- $this->_fields[$field] = $mapper->adapter->selectValue($sql, $params);;
- return $this->_fields[$field];
- } elseif (isset($mapper->lazyRelationships[$field])) {
- $rel = $mapper->lazyRelationships[$field];
- } else {
- return null;
- }
- // Try to find the Mapper class for the object the
- // relationship is with, and fail if we can't.
- if (isset($rel['mapper'])) {
- if ($mapper->factory) {
- $m = $mapper->factory->create($rel['mapper']);
- } else {
- // @TODO - should be getting this instance from somewhere
- // else external, and not passing the adapter along
- // automatically.
- $m = new $rel['mapper']($mapper->adapter);
- }
- } else {
- $m = $mapper->tableToMapper($field);
- if (is_null($m)) {
- return null;
- }
- }
- // Based on the kind of relationship, fetch the appropriate
- // objects and fill the cache.
- switch ($rel['type']) {
- case Horde_Rdo::ONE_TO_ONE:
- case Horde_Rdo::MANY_TO_ONE:
- if (isset($rel['query'])) {
- $query = $this->_fillPlaceholders($rel['query']);
- $this->_fields[$field] = $m->findOne($query);
- } elseif (!empty($this->{$rel['foreignKey']})) {
- $this->_fields[$field] = $m->findOne($this->{$rel['foreignKey']});
- if (empty($this->_fields[$field])) {
- throw new Horde_Rdo_Exception('The referenced object with key ' . $this->{$rel['foreignKey']} . ' does not exist. Your data is inconsistent');
- }
- } else {
- $this->_fields[$field] = null;
- }
- break;
- case Horde_Rdo::ONE_TO_MANY:
- $this->_fields[$field] = $m->find(array($rel['foreignKey'] => $this->{$rel['foreignKey']}));
- break;
- case Horde_Rdo::MANY_TO_MANY:
- $key = $mapper->primaryKey;
- $query = new Horde_Rdo_Query();
- $on = isset($rel['on']) ? $rel['on'] : $m->primaryKey;
- $query->addRelationship($field, array('mapper' => $mapper,
- 'table' => $rel['through'],
- 'type' => Horde_Rdo::MANY_TO_MANY,
- 'query' => array("$m->table.$on" => new Horde_Rdo_Query_Literal($rel['through'] . '.' . $on), $key => $this->$key)));
- $this->_fields[$field] = $m->find($query);
- break;
- }
- return $this->_fields[$field];
- }
- /**
- * Implements getter for ArrayAccess interface.
- *
- * @see __get()
- */
- public function offsetGet($field)
- {
- return $this->__get($field);
- }
- /**
- * Set a field's value.
- *
- * @param string $field The field to set
- * @param mixed $value The field's new value
- */
- public function __set($field, $value)
- {
- // Honor any explicit setters.
- $fieldMethod = 'set' . ucfirst($field);
- // If an Rdo_Base subclass has a __call() method, is_callable
- // returns true on every method name, so use method_exists
- // instead.
- if (method_exists($this, $fieldMethod)) {
- return call_user_func(array($this, $fieldMethod), $value);
- }
- $this->_fields[$field] = $value;
- }
- /**
- * Implements setter for ArrayAccess interface.
- *
- * @see __set()
- */
- public function offsetSet($field, $value)
- {
- $this->__set($field, $value);
- }
- /**
- * Allow using isset($rdo->foo) to check for field or
- * relationship presence.
- *
- * @param string $field The field name to check existence of.
- */
- public function __isset($field)
- {
- $m = $this->getMapper();
- return isset($this->_fields[$field])
- || isset($m->fields[$field])
- || isset($m->lazyFields[$field])
- || isset($m->relationships[$field])
- || isset($m->lazyRelationships[$field]);
- }
- /**
- * Implements isset() for ArrayAccess interface.
- *
- * @see __isset()
- */
- public function offsetExists($field)
- {
- return $this->__isset($field);
- }
- /**
- * Allow using unset($rdo->foo) to unset a basic
- * field. Relationships cannot be unset in this way.
- *
- * @param string $field The field name to unset.
- */
- public function __unset($field)
- {
- // @TODO Should unsetting a MANY_TO_MANY relationship remove
- // the relationship?
- unset($this->_fields[$field]);
- }
- /**
- * Implements unset() for ArrayAccess interface.
- *
- * @see __unset()
- */
- public function offsetUnset($field)
- {
- $this->__unset($field);
- }
- /**
- * Set field values for the object
- *
- * @param array $fields Initial values for the new object.
- *
- * @see Horde_Rdo_Mapper::map()
- */
- public function setFields($fields = array())
- {
- $this->_fields = $fields;
- }
- /**
- * Implement the IteratorAggregate interface. Looping over an Rdo
- * object goes through each property of the object in turn.
- *
- * @return Horde_Rdo_Iterator The Iterator instance.
- */
- public function getIterator()
- {
- return new Horde_Rdo_Iterator($this);
- }
- /**
- * Get a Mapper instance that can be used to manage this
- * object. The Mapper instance can come from a few places:
- *
- * - If the class <RdoClassName>Mapper exists, it will be used
- * automatically.
- *
- * - Any Rdo instance created with Horde_Rdo_Mapper::map() will have a
- * $mapper object set automatically.
- *
- * - Subclasses can override getMapper() to return the correct
- * mapper object.
- *
- * - The programmer can call $rdoObject->setMapper($mapper) to provide a
- * mapper object.
- *
- * A Horde_Rdo_Exception will be thrown if none of these
- * conditions are met.
- *
- * @return Horde_Rdo_Mapper The Mapper instance managing this object.
- */
- public function getMapper()
- {
- if (!$this->_mapper) {
- $class = get_class($this) . 'Mapper';
- if (class_exists($class)) {
- $this->_mapper = new $class();
- } else {
- throw new Horde_Rdo_Exception('No Horde_Rdo_Mapper object found. Override getMapper() or define the ' . get_class($this) . 'Mapper class.');
- }
- }
- return $this->_mapper;
- }
- /**
- * Associate this Rdo object with the Mapper instance that will
- * manage it. Called automatically by Horde_Rdo_Mapper:map().
- *
- * @param Horde_Rdo_Mapper $mapper The Mapper to manage this Rdo object.
- *
- * @see Horde_Rdo_Mapper::map()
- */
- public function setMapper($mapper)
- {
- $this->_mapper = $mapper;
- }
- /**
- * Adds a relation to one of the relationships defined in the mapper.
- *
- * - For one-to-one relations, simply updates the relation field.
- * - For one-to-many relations, updates the related object's relation field.
- * - For many-to-many relations, adds an entry in the "through" table.
- * - Performs a no-op if the peer is already related.
- *
- * This is a proxy to the mapper's addRelation() method.
- *
- * @param string $relationship The relationship key in the mapper.
- * @param Horde_Rdo_Base $peer The object to add the relation.
- *
- * @throws Horde_Rdo_Exception
- */
- public function addRelation($relationship, Horde_Rdo_Base $peer)
- {
- $this->mapper->addRelation($relationship, $this, $peer);
- }
- /**
- * Checks whether a relation to a peer is defined through one of the
- * relationships in the mapper.
- *
- * @param string $relationship The relationship key in the mapper.
- * @param Horde_Rdo_Base $peer The object to check for the relation.
- * If this is null, check if there is any peer
- * for this relation.
- *
- * @return boolean True if related.
- * @throws Horde_Rdo_Exception
- */
- public function hasRelation($relationship, Horde_Rdo_Base $peer = null)
- {
- $mapper = $this->getMapper();
- if (isset($mapper->relationships[$relationship])) {
- $rel = $mapper->relationships[$relationship];
- } elseif (isset($mapper->lazyRelationships[$relationship])) {
- $rel = $mapper->lazyRelationships[$relationship];
- } else {
- throw new Horde_Rdo_Exception('The requested relation is not defined in the mapper');
- }
- $result = $this->$relationship;
- switch ($rel['type']) {
- case Horde_Rdo::ONE_TO_ONE:
- case Horde_Rdo::MANY_TO_ONE:
- if (empty($peer) || empty($result)) {
- return (bool) $result;
- }
- $key = $result->mapper->primaryKey;
- return $result->$key == $peer->$key;
- case Horde_Rdo::ONE_TO_MANY:
- case Horde_Rdo::MANY_TO_MANY:
- if (empty($peer)) {
- return (bool) count($result);
- }
- $key = $peer->mapper->primaryKey;
- foreach ($result as $item) {
- if ($item->$key == $peer->$key) {
- return true;
- }
- }
- break;
- }
- return false;
- }
- /**
- * Removes a relation to one of the relationships defined in the mapper.
- *
- * - For one-to-one and one-to-many relations, simply sets the relation
- * field to 0.
- * - For many-to-many, either deletes all relations to this object or just
- * the relation to a given peer object.
- * - Performs a no-op if the peer is already unrelated.
- *
- * This is a proxy to the mapper's removeRelation method.
- *
- * @param string $relationship The relationship key in the mapper
- * @param Horde_Rdo_Base $peer The object to remove from the relation
- * @return integer The number of relations affected
- * @throws Horde_Rdo_Exception
- */
- public function removeRelation($relationship, Horde_Rdo_Base $peer = null)
- {
- return $this->mapper->removeRelation($relationship, $this, $peer);
- }
- /**
- * Save any changes to the backend.
- *
- * @return boolean Success.
- */
- public function save()
- {
- return $this->getMapper()->update($this) == 1;
- }
- /**
- * Delete this object from the backend.
- *
- * @return boolean Success or failure.
- */
- public function delete()
- {
- return $this->getMapper()->delete($this) == 1;
- }
- /**
- * Take a query array and replace @field@ placeholders with values
- * from this object.
- *
- * @param array $query The query to process placeholders on.
- *
- * @return array The query with placeholders filled in.
- */
- protected function _fillPlaceholders($query)
- {
- foreach (array_keys($query) as $field) {
- $value = $query[$field];
- if (preg_match('/^@(.*)@$/', $value, $matches)) {
- $query[$field] = $this->{$matches[1]};
- }
- }
- return $query;
- }
- }