<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * Provides an object interface to a table row
 *
 * PHP version 5
 *
 * LICENSE: This source file is subject to version 3.0 of the PHP license
 * that is available through the world-wide-web at the following URI:
 * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
 * the PHP License and are unable to obtain it through the web, please
 * send a note to license@php.net so we can mail you a copy immediately.
 *
 * @category   Database
 * @package    DB
 * @author     Stig Bakken <stig@php.net>
 * @copyright  1997-2007 The PHP Group
 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
 * @version    CVS: $Id$
 * @link       http://pear.php.net/package/DB
 */

/**
 * Obtain the DB class so it can be extended from
 */
require_once 'DB.php';

/**
 * Provides an object interface to a table row
 *
 * It lets you add, delete and change rows using objects rather than SQL
 * statements.
 *
 * @category   Database
 * @package    DB
 * @author     Stig Bakken <stig@php.net>
 * @copyright  1997-2007 The PHP Group
 * @license    http://www.php.net/license/3_0.txt  PHP License 3.0
 * @version    Release: 1.9.2
 * @link       http://pear.php.net/package/DB
 */
class DB_storage extends PEAR
{
    // {{{ properties

    /** the name of the table (or view, if the backend database supports
     * updates in views) we hold data from */
    public $_table = null;

    /** which column(s) in the table contains primary keys, can be a
     * string for single-column primary keys, or an array of strings
     * for multiple-column primary keys */
    public $_keycolumn = null;

    /** DB connection handle used for all transactions */
    public $_dbh = null;

    /** an assoc with the names of database fields stored as properties
     * in this object */
    public $_properties = array();

    /** an assoc with the names of the properties in this object that
     * have been changed since they were fetched from the database */
    public $_changes = array();

    /** flag that decides if data in this object can be changed.
     * objects that don't have their table's key column in their
     * property lists will be flagged as read-only. */
    public $_readonly = false;

    /** function or method that implements a validator for fields that
     * are set, this validator function returns true if the field is
     * valid, false if not */
    public $_validator = null;

    // }}}
    // {{{ constructor

    /**
     * Constructor
     *
     * @param $table string the name of the database table
     *
     * @param $keycolumn mixed string with name of key column, or array of
     * strings if the table has a primary key of more than one column
     *
     * @param $dbh object database connection object
     *
     * @param $validator mixed function or method used to validate
     * each new value, called with three parameters: the name of the
     * field/column that is changing, a reference to the new value and
     * a reference to this object
     *
     */
    public function __construct($table, $keycolumn, &$dbh, $validator = null)
    {
        $this->PEAR('DB_Error');
        $this->_table = $table;
        $this->_keycolumn = $keycolumn;
        $this->_dbh = $dbh;
        $this->_readonly = false;
        $this->_validator = $validator;
    }

    // }}}
    // {{{ _makeWhere()

    /**
     * Create a new (empty) row in the configured table for this
     * object.
     * @param $newpk
     * @return |null
     */
    public function insert($newpk)
    {
        if (is_array($this->_keycolumn)) {
            $primarykey = $this->_keycolumn;
        } else {
            $primarykey = array($this->_keycolumn);
        }
        settype($newpk, "array");
        for ($i = 0; $i < sizeof($primarykey); $i++) {
            $pkvals[] = $this->_dbh->quote($newpk[$i]);
        }

        $sth = $this->_dbh->query("INSERT INTO $this->_table (" .
            implode(",", $primarykey) . ") VALUES(" .
            implode(",", $pkvals) . ")");
        if (DB::isError($sth)) {
            return $sth;
        }
        if (sizeof($newpk) == 1) {
            $newpk = $newpk[0];
        }
        $this->setup($newpk);
        return null;
    }

    // }}}
    // {{{ setup()

    /**
     * Method used to initialize a DB_storage object from the
     * configured table.
     *
     * @param $keyval mixed the key[s] of the row to fetch (string or array)
     *
     * @return int|object
     */
    public function setup($keyval)
    {
        $whereclause = $this->_makeWhere($keyval);
        $query = 'SELECT * FROM ' . $this->_table . ' WHERE ' . $whereclause;
        $sth = $this->_dbh->query($query);
        if (DB::isError($sth)) {
            return $sth;
        }
        $row = $sth->fetchRow(DB_FETCHMODE_ASSOC);
        if (DB::isError($row)) {
            return $row;
        }
        if (!$row) {
            return $this->raiseError(
                null,
                DB_ERROR_NOT_FOUND,
                null,
                null,
                $query,
                null,
                true
            );
        }
        foreach ($row as $key => $value) {
            $this->_properties[$key] = true;
            $this->$key = $value;
        }
        return DB_OK;
    }

    // }}}
    // {{{ insert()

    /**
     * Utility method to build a "WHERE" clause to locate ourselves in
     * the table.
     *
     * XXX future improvement: use rowids?
     *
     * @access private
     * @param null $keyval
     * @return mixed|string|null
     */
    public function _makeWhere($keyval = null)
    {
        if (is_array($this->_keycolumn)) {
            if ($keyval === null) {
                for ($i = 0; $i < sizeof($this->_keycolumn); $i++) {
                    $keyval[] = $this->{$this->_keycolumn[$i]};
                }
            }
            $whereclause = '';
            for ($i = 0; $i < sizeof($this->_keycolumn); $i++) {
                if ($i > 0) {
                    $whereclause .= ' AND ';
                }
                $whereclause .= $this->_keycolumn[$i];
                if (is_null($keyval[$i])) {
                    // there's not much point in having a NULL key,
                    // but we support it anyway
                    $whereclause .= ' IS NULL';
                } else {
                    $whereclause .= ' = ' . $this->_dbh->quote($keyval[$i]);
                }
            }
        } else {
            if ($keyval === null) {
                $keyval = @$this->{$this->_keycolumn};
            }
            $whereclause = $this->_keycolumn;
            if (is_null($keyval)) {
                // there's not much point in having a NULL key,
                // but we support it anyway
                $whereclause .= ' IS NULL';
            } else {
                $whereclause .= ' = ' . $this->_dbh->quote($keyval);
            }
        }
        return $whereclause;
    }

    // }}}
    // {{{ toString()

    /**
     * Output a simple description of this DB_storage object.
     * @return string object description
     */
    public function toString()
    {
        $info = strtolower(get_class($this));
        $info .= " (table=";
        $info .= $this->_table;
        $info .= ", keycolumn=";
        if (is_array($this->_keycolumn)) {
            $info .= "(" . implode(",", $this->_keycolumn) . ")";
        } else {
            $info .= $this->_keycolumn;
        }
        $info .= ", dbh=";
        if (is_object($this->_dbh)) {
            $info .= $this->_dbh->toString();
        } else {
            $info .= "null";
        }
        $info .= ")";
        if (sizeof($this->_properties)) {
            $info .= " [loaded, key=";
            $keyname = $this->_keycolumn;
            if (is_array($keyname)) {
                $info .= "(";
                for ($i = 0; $i < sizeof($keyname); $i++) {
                    if ($i > 0) {
                        $info .= ",";
                    }
                    $info .= $this->$keyname[$i];
                }
                $info .= ")";
            } else {
                $info .= $this->$keyname;
            }
            $info .= "]";
        }
        if (sizeof($this->_changes)) {
            $info .= " [modified]";
        }
        return $info;
    }

    // }}}
    // {{{ dump()

    /**
     * Dump the contents of this object to "standard output".
     */
    public function dump()
    {
        foreach ($this->_properties as $prop => $foo) {
            print "$prop = ";
            print htmlentities($this->$prop);
            print "<br />\n";
        }
    }

    // }}}
    // {{{ &create()

    /**
     * Static method used to create new DB storage objects.
     * @param $table
     * @param $data assoc. array where the keys are the names
     *              of properties/columns
     * @return object a new instance of DB_storage or a subclass of it
     */
    public function &create($table, &$data)
    {
        $classname = strtolower(get_class($this));
        $obj = new $classname($table);
        foreach ($data as $name => $value) {
            $obj->_properties[$name] = true;
            $obj->$name = &$value;
        }
        return $obj;
    }

    // }}}
    // {{{ loadFromQuery()

    /**
     * Loads data into this object from the given query.  If this
     * object already contains table data, changes will be saved and
     * the object re-initialized first.
     *
     * @param $query SQL query
     *
     * @param $params parameter list in case you want to use
     * prepare/execute mode
     *
     * @return int DB_OK on success, DB_WARNING_READ_ONLY if the
     * returned object is read-only (because the object's specified
     * key column was not found among the columns returned by $query),
     * or another DB error code in case of errors.
     */
    // XXX commented out for now
    /*
        function loadFromQuery($query, $params = null)
        {
            if (sizeof($this->_properties)) {
                if (sizeof($this->_changes)) {
                    $this->store();
                    $this->_changes = array();
                }
                $this->_properties = array();
            }
            $rowdata = $this->_dbh->getRow($query, DB_FETCHMODE_ASSOC, $params);
            if (DB::isError($rowdata)) {
                return $rowdata;
            }
            reset($rowdata);
            $found_keycolumn = false;
            while (list($key, $value) = each($rowdata)) {
                if ($key == $this->_keycolumn) {
                    $found_keycolumn = true;
                }
                $this->_properties[$key] = true;
                $this->$key = &$value;
                unset($value); // have to unset, or all properties will
                               // refer to the same value
            }
            if (!$found_keycolumn) {
                $this->_readonly = true;
                return DB_WARNING_READ_ONLY;
            }
            return DB_OK;
        }
     */

    // }}}
    // {{{ set()

    /**
     * Modify an attriute value.
     * @param $property
     * @param $newvalue
     * @return bool|object
     */
    public function set($property, $newvalue)
    {
        // only change if $property is known and object is not
        // read-only
        if ($this->_readonly) {
            return $this->raiseError(
                null,
                DB_WARNING_READ_ONLY,
                null,
                null,
                null,
                null,
                true
            );
        }
        if (@isset($this->_properties[$property])) {
            if (empty($this->_validator)) {
                $valid = true;
            } else {
                $valid = @call_user_func(
                    $this->_validator,
                    $this->_table,
                    $property,
                    $newvalue,
                    $this->$property,
                    $this
                );
            }
            if ($valid) {
                $this->$property = $newvalue;
                if (empty($this->_changes[$property])) {
                    $this->_changes[$property] = 0;
                } else {
                    $this->_changes[$property]++;
                }
            } else {
                return $this->raiseError(
                    null,
                    DB_ERROR_INVALID,
                    null,
                    null,
                    "invalid field: $property",
                    null,
                    true
                );
            }
            return true;
        }
        return $this->raiseError(
            null,
            DB_ERROR_NOSUCHFIELD,
            null,
            null,
            "unknown field: $property",
            null,
            true
        );
    }

    // }}}
    // {{{ &get()

    /**
     * Fetch an attribute value.
     *
     * @param string attribute name
     *
     * @return attribute contents, or null if the attribute name is
     * unknown
     */
    public function &get($property)
    {
        // only return if $property is known
        if (isset($this->_properties[$property])) {
            return $this->$property;
        }
        $tmp = null;
        return $tmp;
    }

    // }}}
    // {{{ _DB_storage()

    /**
     * Destructor, calls DB_storage::store() if there are changes
     * that are to be kept.
     */
    public function _DB_storage()
    {
        if (sizeof($this->_changes)) {
            $this->store();
        }
        $this->_properties = array();
        $this->_changes = array();
        $this->_table = null;
    }

    // }}}
    // {{{ store()

    /**
     * Stores changes to this object in the database.
     *
     * @return DB_OK|int
     */
    public function store()
    {
        $params = array();
        $vars = array();
        foreach ($this->_changes as $name => $foo) {
            $params[] = &$this->$name;
            $vars[] = $name . ' = ?';
        }
        if ($vars) {
            $query = 'UPDATE ' . $this->_table . ' SET ' .
                implode(', ', $vars) . ' WHERE ' .
                $this->_makeWhere();
            $stmt = $this->_dbh->prepare($query);
            $res = $this->_dbh->execute($stmt, $params);
            if (DB::isError($res)) {
                return $res;
            }
            $this->_changes = array();
        }
        return DB_OK;
    }

    // }}}
    // {{{ remove()

    /**
     * Remove the row represented by this object from the database.
     *
     * @return mixed DB_OK or a DB error
     */
    public function remove()
    {
        if ($this->_readonly) {
            return $this->raiseError(
                null,
                DB_WARNING_READ_ONLY,
                null,
                null,
                null,
                null,
                true
            );
        }
        $query = 'DELETE FROM ' . $this->_table . ' WHERE ' .
            $this->_makeWhere();
        $res = $this->_dbh->query($query);
        if (DB::isError($res)) {
            return $res;
        }
        foreach ($this->_properties as $prop => $foo) {
            unset($this->$prop);
        }
        $this->_properties = array();
        $this->_changes = array();
        return DB_OK;
    }

    // }}}
}

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 */