<?php
//
// +----------------------------------------------------------------------+
// | PHP version 4.0                                                      |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2001 The PHP Group                                |
// +----------------------------------------------------------------------+
// | This source file is subject to version 2.02 of the PHP license,      |
// | that is bundled with this package in the file LICENSE, and is        |
// | available at through the world-wide-web at                           |
// | http://www.php.net/license/2_02.txt.                                 |
// | If you did not receive a copy of the PHP license and are unable to   |
// | obtain it through the world-wide-web, please send a note to          |
// | license@php.net so we can mail you a copy immediately.               |
// +----------------------------------------------------------------------+
// | Authors: Stig Bakken <ssb@fast.no>                                   |
// |                                                                      |
// +----------------------------------------------------------------------+
//
// DB_storage: a class that lets you return SQL data as objects that
// can be manipulated and that updates the database accordingly.
//

require_once "PEAR.php";
require_once "DB.php";

function DB_storage_destructor() {
    global $DB_storage_object_list;

    if (is_array($DB_storage_object_list)) {
	reset($DB_storage_object_list);
	while (list($ind, $obj) = each($DB_storage_object_list)) {
	    $obj->destroy();
	}
	$DB_storage_object_list = null;
    }
}

class DB_storage extends PEAR
{
    /** the name of the table (or view, if the backend database supports
        updates in views) we hold data from */
    var $_table = null;
    /** which column in the table contains primary keys */
    var $_keycolumn = null;
    /** DB connection handle used for all transactions */
    var $_dbh = null;
    /** an assoc with the names of database fields stored as properties
	in this object */
    var $_properties = array();
    /** an assoc with the names of the properties in this object that
	have been changed since they were fetched from the database */
    var $_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. */
    var $_readonly = false;

    /**
     * Constructor, adds itself to the DB_storage class's list of
     * objects that should have their "destroy" method called when
     * PHP shuts down (poor man's destructors).
     */
    function DB_storage($table, $keycolumn, &$dbh) {
	global $DB_storage_object_list;
	if (is_array($DB_storage_object_list)) {
	    $DB_storage_object_list[] = &$this;
	} else {
	    $DB_storage_object_list = array(&$this);
	}
	$this->_table = $table;
	$this->_keycolumn = $keycolumn;
	$this->_dbh = $dbh;
	$this->_readonly = false;
    }

    /**
     * Method used to initialize a DB_storage object from the
     * configured table.
     * @param $keyval the key of the row to fetch
     * @return int DB_OK on success, DB error if not
     */
    function setup($keyval) {
	if (is_int($keyval)) {
	    $qval = "$keyval";
	} else {
	    $qval = "'" . $this->_dbh->quoteString($keyval) . "'";
	}
	$sth = $this->_dbh->query("SELECT * FROM " .
				  $this->_table . " WHERE " .
				  $this->_keycolumn . " = $qval");
	if (DB::isError($sth)) {
	    return $sth;
	}
	while ($row = $sth->fetchRow(DB_FETCHMODE_ASSOC)) {
	    reset($row);
	    while (list($key, $value) = each($row)) {
		$this->_properties[$key] = true;
		$this->$key = &$value;
		unset($value);
	    }
	}
    }

    /**
     * Create a new (empty) row in the configured table for this
     * object.
     */
    function insert($newid = false) {
	if (is_int($newid)) {
	    $qid = "$newid";
	} else {
	    $qid = "'" . $this->_dbh->quoteString($newid) . "'";
	}
	$sth = $this->_dbh->query("INSERT INTO " .
				  $this->_table . " (" .
				  $this->_keycolumn .
				  ") VALUES($qid)");
	if (DB::isError($sth)) {
	    return $sth;
	}
	$this->setup($newid);
    }

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

    /**
     * Dump the contents of this object to "standard output".
     */
    function dump() {
	reset($this->_properties);
	while (list($prop, $foo) = each($this->_properties)) {
	    print "$prop = ";
	    print htmlentities($this->$prop);
	    print "<BR>\n";
	}
    }

    /**
     * Static method used to create new DB storage objects.
     * @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
     */
    function &create($table, &$data) {
	$classname = get_class(&$this);
	$obj = new $classname($table);
	reset($data);
	while (list($name, $value) = each($data)) {
	    $obj->_properties[$name] = true;
	    $obj->$name = &$value;
	}
	return $obj;
    }

    /**
     * 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.
     */
    function loadFromQuery($query, $params = false) {
	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;
    }

    function set($property, &$newvalue) {
	// only change if $property is known and object is not
	// read-only
	if (!$this->_readonly && isset($this->_properties[$property])) {
	    $this->$property = $newvalue;
	    $this->_changes[$property]++;
	    return true;
	}
	return false;
    }

    function &get($property) {
	// only return if $property is known
	if (isset($this->_properties[$property])) {
	    return $this->$property;
	}
	return null;
    }

    function destroy($discard = false) {
	if (!$discard && sizeof($this->_changes)) {
	    $this->store();
	}
	$this->_properties = array();
	$this->_changes = array();
	$this->_table = null;
    }

    function store() {
	while (list($name, $changed) = each($this->_changes)) {
	    $params[] = &$this->$name;
	    $vars[] = $name . ' = ?';
	}
	if ($vars) {
	    $query = 'UPDATE ' . $this->_table . ' SET ' .
		implode(', ', $vars) . ' WHERE id = ?';
	    $params[] = $this->id;
	    $stmt = $this->_dbh->prepare($query);
	    $res = $this->_dbh->execute($stmt, &$params);
	    if (DB::isError($res)) {
		return $res;
	    }
	    $this->_changes = array();
	}
	return DB_OK;
    }
}

?>
