<?php
/**
 * Main RDO file.
 *
 * @package RDO
 */

/**
 * RDO class (Rampage Data Objects). Entity classes extend this
 * base class.
 *
 * $Horde: framework/RDO/RDO.php,v 1.10.2.1 2005/10/18 11:01:22 jan Exp $
 *
 * @package RDO
 */
abstract class RDO implements IteratorAggregate {

    /**
     * Find mode for returning just the first matching result. The
     * backend will limit the search if possible, and only a single
     * object will be returned (or null).
     */
    const FIND_FIRST = 'RDO_FIND_FIRST';

    /**
     * Find mode for returning all results. Even if no results are
     * found an empty iterator will be returned.
     */
    const FIND_ALL = 'RDO_FIND_ALL';

    /**
     */
    const ONE_TO_ONE = 1;
    const ONE_TO_MANY = 2;
    const MANY_TO_ONE = 3;
    const MANY_TO_MANY = 4;

    /**
     * The RDO_Mapper instance associated with this RDO object. The
     * Mapper takes care of all backend access.
     *
     * @see RDO_Mapper
     * @var RDO_Mapper $mapper
     */
    protected $mapper;

    /**
     * This object's set of properties.
     *
     * @var array $fields
     */
    protected $fields = array();

    /**
     * If set to an array, this will be a deny list for all mass-set
     * functions (new RDO(), RDO::setFields()). Fields listed here
     * will not be set by those functions.
     *
     * If $fieldsAllow is set, $fieldsDeny <em>will not be used</em>
     * as $fieldsAllow is more restrictive.
     *
     * @see $fieldsAllow
     * @var array
     */
    protected $fieldsDeny = null;

    /**
     * If set to an array, this will be an explicit allow list for all
     * mass-set functions (new RDO(), RDO::setFields()). Fields
     * <em>not<em> listed here will be ignored.
     *
     * This will override any $fieldsDeny settings.
     *
     * @see $fieldsDeny
     * @var array
     */
    protected $fieldsAllow = null;

    /**
     *
     */
    protected $relationships = array();

    /**
     *
     */
    protected $relationshipCache = array();

    /**
     * RDO constructor. Can be called directly by a programmer, or is
     * called in RDO_Mapper::map(). Takes an associative array of
     * initial property values.
     *
     * @param array $fields Initial property values for the new object.
     *
     * @see RDO_Mapper::map()
     */
    public function __construct($fields = array())
    {
        $this->fields = $this->validate($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()
    {
        unset($this->fields[$this->mapper->describe()->key]);
    }

    /**
     * Allow using $rdo->foo to access the property (which may be a
     * field or a relationship) "foo". If a field or relationship
     * "foo" is not present then null is returned.
     *
     * @param string $property The name of the property to access.
     *
     * @return mixed The value of $property or null.
     */
    public function __get($property)
    {
        // Return fields first.
        if (isset($this->fields[$property])) {
            return $this->fields[$property];
        }

        // Check for cached relationship data.
        if ($relationship = $this->cache($property)) {
            return $relationship;
        }

        // Try to find a relationship first by the given name, then by
        // $property . '_id'.
        if (isset($this->relationships[$property])) {
            $rel = $this->relationships[$property];
        } elseif (isset($this->relationships[$property . '_id'])) {
            $rel = $this->relationships[$property . '_id'];
        }

        // If we didn't find a relationship, return.
        if (!$rel) {
            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'])) {
            $m = new $rel['mapper']();
        } elseif (class_exists(ucwords($property) . 'Mapper')) {
            $class = ucwords($property) . 'Mapper';
            $m = new $class();
        } else {
            return null;
        }

        // Based on the kind of relationship, fetch the appropriate
        // objects and fill the cache.
        switch ($rel['type']) {
        case self::ONE_TO_ONE:
            return $this->cache($property, $m->find($this->fields[$rel['foreignKey']]));

        case self::ONE_TO_MANY:
            return $this->cache($property,
                                $m->find(self::FIND_ALL,
                                         array($m->describe()->key => $this->fields[$rel['foreignKey']])));

        case self::MANY_TO_ONE:
            return $this->cache($property, $m->find($this->fields[$rel['foreignKey']]));

        case self::MANY_TO_MANY:
            $mapper = $this->getMapper();
            $key = $mapper->describe()->key;
            $criteria = new RDO_Criterion();
            $criteria->addRelationship($mapper, $rel['joinContainer'], array($key => $this->__get($key)));
            return $this->cache($property, $m->find(self::FIND_ALL, $criteria));
        }
    }

    /**
     * Allow using $rdo->foo = 'bar' to set property values.
     *
     * @param string $property The name of the property to set.
     * @param mixed $value The new value for $property.
     */
    public function __set($property, $value)
    {
        $this->fields[$property] = $value;
    }

    public function isCached($relationship)
    {
    }

    /**
     * Sets the cached value for $relationship, and returns the new
     * value.
     *
     * @param string $relationship The relationship to set.
     * @param mixed $value The relationship data.
     *
     * @return mixed The new cache value.
     */
    public function cache($relationship, $value = null)
    {
        // If we were given a new value, store it.
        if (!is_null($value)) {
            $this->relationshipCache[$relationship] = $value;
        }

        return isset($this->relationshipCache[$relationship]) ? $this->relationshipCache[$relationship] : null;
    }

    /**
     * Clear cached relationship data.
     *
     * @param string $relationship If no relationship is specified all relationships
     *                             will be cleared; otherwise only the given relationship
     *                             will be cleared.
     */
    public function clearCache($relationship = null)
    {
        if ($relationship) {
            if (isset($this->relationshipCache[$relationship])) {
                unset($this->relationshipCache[$relationship]);
            }
        } else {
            $this->relationshipCache = array();
        }
    }

    /**
     * Implement the IteratorAggregate pattern. When a single RDO
     * object is iterated over, we return an iterator that loops over
     * each property of the object.
     *
     * @return ArrayIterator The Iterator instance.
     */
    public function getIterator()
    {
        return new ArrayIterator($this->fields);
    }

    /**
     * Get an RDO_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 RDO_Mapper::map() will have a
     *   $mapper object set automatically.
     *
     * - Subclasses can override getMapper() to return the correct
     *   mapper object.
     *
     * - The programmer can call RDO::setMapper($mapper) to provide a
     *   mapper object.
     *
     * An RDO_Exception will be thrown if none of these conditions are
     * met.
     *
     * @return 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 RDO_Exception('No RDO_Mapper object found. Override getMapper() or define the ' . get_class($this) . 'Mapper class.');
            }
        }

        return $this->mapper;
    }

    /**
     * Associate this RDO object with the RDO_Mapper instance that
     * will manage it. Called automatically by RDO_Mapper:map().
     *
     * @param RDO_Mapper $mapper The RDO_Mapper to manage this RDO object.
     *
     * @see RDO_Mapper::map()
     */
    public function setMapper(RDO_Mapper $mapper)
    {
        $this->mapper = $mapper;
    }

    /**
     * Get an associative array of all of this object's data.
     *
     * @return array All properties for $this.
     */
    public function getFields()
    {
        return $this->fields;
    }

    /**
     * Set all of this object's fields in one go.
     *
     * @param array $fields An associative array of property names =>
     *                      property values.
     *
     * @see validate()
     */
    public function setFields($fields)
    {
        $this->fields = $this->validate($fields);
    }

    /**
     * Validate a set of property keys/values against $fieldsDeny and
     * $fieldsAllow. Subclasses can and should add additional
     * validation where appropriate.
     *
     * @param array $fields The properties to validate.
     *
     * @see $fieldsDeny
     * @see $fieldsAllow
     */
    public function validate($fields)
    {
        if ($this->fieldsAllow) {
            // Remove any fields that are not explicitly allowed.
            foreach (array_keys($fields) as $field) {
                if (!in_array($field, $this->fieldsAllow)) {
                    unset($fields[$field]);
                }
            }
        } elseif ($this->fieldsDeny) {
            // Remove any explicitly denied fields.
            foreach ($this->fieldsDeny as $field) {
                unset($fields[$field]);
            }
        }

        return $fields;
    }

    /**
     * Save any changes to the backend.
     *
     * @return boolean Success.
     */
    public function save()
    {
        return $this->getMapper()->update($this);
    }

    /**
     * Delete this object from the backend.
     *
     * @return boolean Success or failure.
     */
    public function delete()
    {
        return $this->getMapper()->delete($this) == 1;
    }

    /**
     * Return a string representation of this object. The default
     * implementation just returns a dump of the public properties.
     *
     * @return string String representation of $this.
     */
    public function toString()
    {
        return print_r($this, true);
    }

    /**
     * Return an XML representation of this object. The default
     * implementation is unlikely to be useful in most cases and
     * should be overridden by subclasses to be domain-appropriate.
     *
     * @return string XML representation of $this.
     */
    public function toXml()
    {
        $doc = new DOMDocument('1.0');

        $root = $doc->appendChild($doc->createElement(get_class($this)));
        foreach ($this->getFields() as $field => $value) {
            $f = $root->appendChild($doc->createElement($field));
            $f->appendChild($doc->createTextNode($value));
        }

        return $doc->saveXML();
    }

}
