<?php
/**
 * RDO Mapper base class.
 *
 * @package RDO
 */

/**
 * RDO Mapper class. Controls mapping of entity obects (instances of
 * RDO) from and to RDO_Storage backends.
 *
 * $Horde: framework/RDO/RDO/Mapper.php,v 1.7.2.1 2005/10/18 11:01:23 jan Exp $
 *
 * @package RDO
 */
abstract class RDO_Mapper {

    /**
     * If this is true and fields named created and updated are
     * present, RDO will automatically set creation and last updated
     * timestamps. Timestamps are always GMT for portability.
     *
     * @var boolean
     */
    protected $setTimestamps = true;

    /**
     * What class should this Mapper create for objects? Defaults to
     * the Mapper subclass' name minus "Mapper". So if the RDO_Mapper
     * subclass is UserMapper, it will default to trying to create
     * User objects.
     *
     * @var string $classname
     */
    protected $classname;

    /**
     * The name of the container (SQL table, LDAP DN/objectClass,
     * etc.) that holds this Mapper's objects.
     *
     * @var string $container
     */
    protected $container;

    /**
     * RDO_Storage object that stores this Mapper's objects.
     *
     * @var RDO_Storage $storage
     */
    protected $storage;

    /**
     * RDO_MetaData object describing the fields of this kind of
     * object.
     *
     * @var RDO_MetaData $metadata
     */
    protected $metadata;

    /**
     * Associate a storage object with this mapper. Not needed in the
     * general case if getStorage() is overridden in the concrete
     * Mapper implementation.
     *
     * @param RDO_Storage $storage RDO_Storage instance to store objects.
     *
     * @see getStorage()
     */
    public function setStorage(RDO_Storage $storage)
    {
        $this->storage = $storage;
    }

    /**
     * getStorage() must be overridden by RDO_Mapper subclasses if
     * they don't provide $storage in some other way (by calling
     * setStorage() or on construction, for example).
     *
     * @see setStorage()
     *
     * @return RDO_Storage The storage object this Mapper's objects
     * are stored to.
     */
    public function getStorage()
    {
        throw new RDO_Exception('You must either override getStorage() or assign a RDO_Storage container by calling setStorage().');
    }

    /**
     * Lazy loading of $this->storage. Accessing $storage through this
     * function means you never have to call getStorage() yourself.
     *
     * @return RDO_Storage The storage object this Mapper's objects
     * are stored to.
     */
    public function storage()
    {
        if (!$this->storage) {
            $this->storage = $this->getStorage();
        }
        return $this->storage;
    }

    /**
     * Lazy laoding of the metadata object of this Mapper's fields and
     * properties.
     *
     * @return RDO_MetaData Description of this Mapper's fields.
     */
    public function describe()
    {
        if (!$this->metadata) {
            $this->metadata = new RDO_MetaData($this);
            $this->metadata->container = $this->container;
            $this->storage()->describe($this, $this->metadata);
        }
        return $this->metadata;
    }

    /**
     * Create an instance of $this->classname from a set of data.
     *
     * @param array $fields Field names/default values for the new object.
     *
     * @see $classname
     *
     * @return RDO An instance of $this->classname with $fields as initial data.
     */
    public function map($fields)
    {
        // Guess a classname if one isn't explicitly set.
        if (!$this->classname) {
            $this->classname = str_replace('Mapper', '', get_class($this));
        }

        $o = new $this->classname($fields);
        $o->setMapper($this);
        return $o;
    }

    /**
     * Count objects that match $criteria.
     *
     * @param mixed $criteria The criteria to count matches of.
     *
     * @return integer All objects matching $criteria.
     */
    public function count($criteria = null)
    {
        return $this->storage()->count($this, $criteria);
    }

    /**
     * Check if at least one object matches $criteria.
     *
     * @param mixed $criteria Either a primary key, an array of keys
     *                        => values, or an RDO_Criteria object.
     *
     * @return boolean True or false.
     */
    public function exists($criteria)
    {
        return (bool)$this->storage()->exists($this, $criteria);
    }

    /**
     * Create a new object in the backend with $fields as initial values.
     *
     * @param array $fields Array of field names => initial values.
     *
     * @return RDO The newly created object.
     */
    public function create($fields)
    {
        // If configured to record creation and update times, set them
        // here. We set updated to the initial creation time so it's
        // always set.
        if ($this->setTimestamps) {
            $time = gmmktime();
            if ($this->describe()->hasField('created')) {
                $fields['created'] = $time;
            }
            if ($this->describe()->hasField('updated')) {
                $fields['updated'] = $time;
            }
        }

        $id = $this->storage()->create($this, $fields);
        return $this->map(array_merge($fields, array($this->describe()->key => $id)));
    }

    /**
     * Updates a record in the backend. $object can be either a
     * primary key or an RDO object. If $object is an RDO instance
     * then $fields should be null as they will be pulled from the
     * object.
     *
     * @param string|RDO $object The RDO instance or unique id to update.
     * @param array $fields If passing a unique id, the array of field properties
     *                      to set for $object.
     *
     * @return boolean True, or throw an exception if an error occurs.
     */
    public function update($object, $fields = null)
    {
        if ($object instanceof RDO) {
            $key = $this->describe()->key;
            $id = $object->$key;
            $fields = $object->getFields();
        } else {
            $id = $object;
        }

        // If configured to record update time, set it here.
        if ($this->setTimestamps && $this->describe()->hasField('updated')) {
            $fields['updated'] = gmmktime();
        }

        return $this->storage()->update($this, $id, $fields);
    }

    /**
     * Updates a record in the backend. $object can be either a
     * primary key, an RDO_Criteria object, or an RDO object.
     *
     * @param string|RDO|RDO_Criteria $object The RDO instance, criteria object,
     *                                        or unique id to update.
     *
     * @return integer Number of objects deleted.
     */
    public function delete($object)
    {
        if ($object instanceof RDO) {
            $key = $this->describe()->key;
            $id = $object->$key;
            $criteria = array($key => $id);
        } elseif ($object instanceof RDO_Criteria) {
            $criteria = $object;
        } else {
            $key = $this->describe()->key;
            $criteria = array($key => $object);
        }

        return $this->storage()->delete($this, $criteria);
    }

    /**
     * Find can be called in numerous ways.
     *
     * Primary key: pass find() a single primary key or an array of
     * primary keys, and it will return either a single object or an
     * RDO_Results collecton of objects matching those primary keys.
     *
     * Find mode: otherwise the first argument to find should be a
     * find mode. The defaults modes are RDO::FIND_FIRST (returns the
     * first object matching the rest of the criteria), and
     * RDO::FIND_ALL (return an RDO_Results collection of all
     * matches).
     *
     * When using a find mode, the second argument can be blank (find
     * all objects, or the first object depending on find mode), an
     * associative array (keys are fields, values are the values those
     * fields must match exactly), or an RDO_Criteria object, which
     * defines complex matching criteria.
     */
    public function find()
    {
        $argc = func_num_args();
        $argv = func_get_args();

        // Make sure we have some sort of find criteria.
        if (!$argc) {
            throw new RDO_Exception('find() called with no arguments');
        }

        // Figure out what kind of criteria we have.
        if ($argc == 1) {
            if ($argv[0] == RDO::FIND_FIRST ||
                $argv[0] == RDO::FIND_ALL) {
                // Using a find mode with no criteria.
                $mode = array_shift($argv);
                $criteria = null;
            } else {
                // Find the name of our primary key.
                $key = $this->describe()->key;
                if (is_scalar($argv[0])) {
                    // Looking for one primary key. We'll just return
                    // the corresponding object, or thrown an
                    // exception.
                    $mode = RDO::FIND_FIRST;
                    $criteria = array($key => $argv[0]);
                } else {
                    // Looking for several primary keys. Build an OR
                    // search for them. We'll return an RDO_Results
                    // collection that iterates over them.
                    $mode = RDO::FIND_ALL;
                    $criteria = new RDO_Criteria();
                    foreach ($argv[0] as $id) {
                        $criteria->or($key, RDO::EQUALS, $id);
                    }
                }
            }
        } else {
            // Using a find mode with arbitrary criteria.
            $mode = array_shift($argv);
            $criteria = array_shift($argv);
        }

        $result = $this->storage()->find($this, $mode, $criteria);

        // The storage driver find() can limit results (if we're using
        // RDO::FIND_FIRST) but will always return a collection to
        // remain simpler. We take care fo returning either the
        // collection or a single object here.
        switch ($mode) {
        case RDO::FIND_FIRST:
            // If we only want one result, return null if there are no
            // results.
            if ($result->count()) {
                return $result->current();
            }
            return null;

        case RDO::FIND_ALL:
        default:
            return $result;
        }
    }

}
