* David C. Morse * Mark Wiesemann * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * The names of the authors may not be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * @category Database * @package DB_Table * @author David C. Morse * @license http://opensource.org/licenses/bsd-license.php New BSD License * @version CVS: $Id: Database.php,v 1.15 2007/12/13 16:52:14 wiesemann Exp $ * @link http://pear.php.net/package/DB_Table */ // {{{ Error code constants /** * Parameter is not a DB/MDB2 object */ define('DB_TABLE_DATABASE_ERR_DB_OBJECT', -201); /** * Error in addTable, parameter $table_obj is not a DB_Table object */ define('DB_TABLE_DATABASE_ERR_DBTABLE_OBJECT', -202); /** * Error for table name that does not exist in the database */ define('DB_TABLE_DATABASE_ERR_NO_TBL', -203); /** * Error for table name parameter that is not a string */ define('DB_TABLE_DATABASE_ERR_TBL_NOT_STRING', -204); /** * Error in getCol for a non-existent column name */ define('DB_TABLE_DATABASE_ERR_NO_COL', -205); /** * Error in getForeignCol for a non-existent foreign key column */ define('DB_TABLE_DATABASE_ERR_NO_FOREIGN_COL', -206); /** * Error for column name that is not a string */ define('DB_TABLE_DATABASE_ERR_COL_NOT_STRING', -207); /** * Error in addTable for multiple primary keys */ define('DB_TABLE_DATABASE_ERR_MULT_PKEY', -208); /** * Error in addRef for a non-existent foreign key table */ define('DB_TABLE_DATABASE_ERR_NO_FTABLE', -209); /** * Error in addRef for non-existence referenced table */ define('DB_TABLE_DATABASE_ERR_NO_RTABLE', -210); /** * Error in addRef for null referenced key in a table with no primary key */ define('DB_TABLE_DATABASE_ERR_NO_PKEY', -211); /** * Error in addRef for an invalid foreign key, neither string nor array */ define('DB_TABLE_DATABASE_ERR_FKEY', -212); /** * Error in addRef for referenced key that is not a string, string foreign key */ define('DB_TABLE_DATABASE_ERR_RKEY_NOT_STRING', -213); /** * Error in addRef for referenced key that is not an array, array foreign key */ define('DB_TABLE_DATABASE_ERR_RKEY_NOT_ARRAY', -214); /** * Error in addRef for wrong number of columns in referenced key */ define('DB_TABLE_DATABASE_ERR_RKEY_COL_NUMBER', -215); /** * Error in addRef for non-existence foreign key (referencing) column */ define('DB_TABLE_DATABASE_ERR_NO_FCOL', -216); /** * Error in addRef for non-existence referenced column */ define('DB_TABLE_DATABASE_ERR_NO_RCOL', -217); /** * Error in addRef for referencing and referenced columns of different types */ define('DB_TABLE_DATABASE_ERR_REF_TYPE', -218); /** * Error in addRef for multiple references from one table to another */ define('DB_TABLE_DATABASE_ERR_MULT_REF', -219); /** * Error due to invalid ON DELETE action name */ define('DB_TABLE_DATABASE_ERR_ON_DELETE_ACTION', -220); /** * Error due to invalid ON UPDATE action name */ define('DB_TABLE_DATABASE_ERR_ON_UPDATE_ACTION', -221); /** * Error in addLink due to missing required reference */ define('DB_TABLE_DATABASE_ERR_NO_REF_LINK', -222); /** * Error in validCol for a column name that does not exist in the datase */ define('DB_TABLE_DATABASE_ERR_NO_COL_DB', -223); /** * Error in validCol for column name that does not exist in the specified table */ define('DB_TABLE_DATABASE_ERR_NO_COL_TBL', -224); /** * Error in a buildSQL or select* method for an undefined key of $this->sql */ define('DB_TABLE_DATABASE_ERR_SQL_UNDEF', -225); /** * Error in a buildSQL or select* method for a key of $this->sql that is * not a string */ define('DB_TABLE_DATABASE_ERR_SQL_NOT_STRING', -226); /** * Error in buildFilter due to invalid match type */ define('DB_TABLE_DATABASE_ERR_MATCH_TYPE', -227); /** * Error in buildFilter due to invalid key for full match */ define('DB_TABLE_DATABASE_ERR_DATA_KEY', -228); /** * Error in buildFilter due to invalid key for full match */ define('DB_TABLE_DATABASE_ERR_FILT_KEY', -229); /** * Error in buildFilter due to invalid key for full match */ define('DB_TABLE_DATABASE_ERR_FULL_KEY', -230); /** * Error in insert for a failed foreign key constraint */ define('DB_TABLE_DATABASE_ERR_FKEY_CONSTRAINT', -231); /** * Error in delete due to a referentially triggered 'restrict' action */ define('DB_TABLE_DATABASE_ERR_RESTRICT_DELETE', -232); /** * Error in update due to a referentially triggered 'restrict' action */ define('DB_TABLE_DATABASE_ERR_RESTRICT_UPDATE', -233); /** * Error in fromXML for table with multiple auto_increment columns */ define('DB_TABLE_DATABASE_ERR_XML_MULT_AUTO_INC', -234); /** * Error in autoJoin, column and tables parameter both null */ define('DB_TABLE_DATABASE_ERR_NO_COL_NO_TBL', -235); /** * Error in autoJoin for ambiguous column name */ define('DB_TABLE_DATABASE_ERR_COL_NOT_UNIQUE', -236); /** * Error in autoJoin for non-unique set of join conditions */ define('DB_TABLE_DATABASE_ERR_AMBIG_JOIN', -237); /** * Error in autoJoin for failed construction of join */ define('DB_TABLE_DATABASE_ERR_FAIL_JOIN', -238); /** * Error in fromXML for PHP 4 (this function requires PHP 5) */ define('DB_TABLE_DATABASE_ERR_PHP_VERSION', -239); /** * Error parsing XML string in fromXML */ define('DB_TABLE_DATABASE_ERR_XML_PARSE', -240); // }}} // {{{ Includes /** * DB_Table_Base base class */ require_once 'DB/Table/Base.php'; /** * DB_Table table abstraction class */ require_once 'DB/Table.php'; /** * The PEAR class for errors */ require_once 'PEAR.php'; // }}} // {{{ Error messages /** * US-English default error messages. If you want to internationalize, you can * set the translated messages via $GLOBALS['_DB_TABLE_DATABASE']['error']. * You can also use DB_Table_Database::setErrorMessage(). Examples: * * * (1) $GLOBALS['_DB_TABLE_DATABASE']['error'] = array( * DB_TABLE_DATABASE_ERR_.. => '...', * DB_TABLE_DATABASE_ERR_.. => '...'); * (2) DB_Table_Database::setErrorMessage(DB_TABLE_DATABASE_ERR_.., '...'); * DB_Table_Database::setErrorMessage(DB_TABLE_DATABASE_ERR_.., '...'); * (3) DB_Table_Database::setErrorMessage(array( * DB_TABLE_DATABASE_ERR_.. => '...'); * DB_TABLE_DATABASE_ERR_.. => '...'); * (4) $obj =& new DB_Table(); * $obj->setErrorMessage(DB_TABLE_DATABASE_ERR_.., '...'); * $obj->setErrorMessage(DB_TABLE_DATABASE_ERR_.., '...'); * (5) $obj =& new DB_Table(); * $obj->setErrorMessage(array(DB_TABLE_DATABASE_ERR_.. => '...'); * DB_TABLE_DATABASE_ERR_.. => '...'); * * * For errors that can occur with-in the constructor call (i.e. e.g. creating * or altering the database table), only the code from examples (1) to (3) * will alter the default error messages early enough. For errors that can * occur later, examples (4) and (5) are also valid. */ $GLOBALS['_DB_TABLE_DATABASE']['default_error'] = array( DB_TABLE_DATABASE_ERR_DB_OBJECT => 'Invalid DB/MDB2 object parameter. Function', DB_TABLE_DATABASE_ERR_NO_TBL => 'Table does not exist in database. Method, Table =', DB_TABLE_DATABASE_ERR_TBL_NOT_STRING => 'Table name parameter is not a string in method', DB_TABLE_DATABASE_ERR_NO_COL => 'In getCol, non-existent column name parameter', DB_TABLE_DATABASE_ERR_NO_FOREIGN_COL => 'In getForeignCol, non-existent column name parameter', DB_TABLE_DATABASE_ERR_COL_NOT_STRING => 'Column name parameter is not a string in method', DB_TABLE_DATABASE_ERR_DBTABLE_OBJECT => 'Parameter of addTable is not a DB_Table object', DB_TABLE_DATABASE_ERR_MULT_PKEY => 'Multiple primary keys in one table detected in addTable. Table', DB_TABLE_DATABASE_ERR_NO_FTABLE => 'Foreign key reference from non-existent table in addRef. Reference', DB_TABLE_DATABASE_ERR_NO_RTABLE => 'Reference to a non-existent referenced table in addRef. Reference', DB_TABLE_DATABASE_ERR_NO_PKEY => 'Missing primary key of referenced table in addRef. Reference', DB_TABLE_DATABASE_ERR_FKEY => 'Foreign / referencing key is not a string or array in addRef', DB_TABLE_DATABASE_ERR_RKEY_NOT_STRING => 'Foreign key is a string, referenced key is not a string in addRef', DB_TABLE_DATABASE_ERR_RKEY_NOT_ARRAY => 'Foreign key is an array, referenced key is not an array in addRef', DB_TABLE_DATABASE_ERR_RKEY_COL_NUMBER => 'Wrong number of columns in referencing key in addRef', DB_TABLE_DATABASE_ERR_NO_FCOL => 'Nonexistent foreign / referencing key column in addRef. Reference', DB_TABLE_DATABASE_ERR_NO_RCOL => 'Nonexistent referenced key column in addRef. Reference', DB_TABLE_DATABASE_ERR_REF_TYPE => 'Different referencing and referenced column types in addRef. Reference', DB_TABLE_DATABASE_ERR_MULT_REF => 'Multiple references between two tables in addRef. Reference', DB_TABLE_DATABASE_ERR_ON_DELETE_ACTION => 'Invalid ON DELETE action. Reference', DB_TABLE_DATABASE_ERR_ON_UPDATE_ACTION => 'Invalid ON UPDATE action. Reference', DB_TABLE_DATABASE_ERR_NO_REF_LINK => 'Error in addLink due to missing required reference(s)', DB_TABLE_DATABASE_ERR_NO_COL_DB => 'In validCol, column name does not exist in database. Column', DB_TABLE_DATABASE_ERR_NO_COL_TBL => 'In validCol, column does not exist in specified table. Column', DB_TABLE_DATABASE_ERR_SQL_UNDEF => 'Query string is not a key of $sql property array. Key is', DB_TABLE_DATABASE_ERR_SQL_NOT_STRING => 'Query is neither an array nor a string', DB_TABLE_DATABASE_ERR_MATCH_TYPE => 'Invalid match parameter of buildFilter', DB_TABLE_DATABASE_ERR_DATA_KEY => 'Invalid data_key in buildFilter, neither string nor array', DB_TABLE_DATABASE_ERR_FILT_KEY => 'Incompatible data_key and filter_key in buildFilter', DB_TABLE_DATABASE_ERR_FULL_KEY => 'Invalid key value in buildFilter: Mixed null and not null', DB_TABLE_DATABASE_ERR_FKEY_CONSTRAINT => 'Foreign key constraint failure: Key does not reference any rows', DB_TABLE_DATABASE_ERR_RESTRICT_DELETE => 'Referentially trigger restrict of delete from table', DB_TABLE_DATABASE_ERR_RESTRICT_UPDATE => 'Referentially trigger restrict of update of table', DB_TABLE_DATABASE_ERR_NO_COL_NO_TBL => 'No columns or tables provided as parameters to autoJoin', DB_TABLE_DATABASE_ERR_COL_NOT_UNIQUE => 'Ambiguous column name in autoJoin. Column', DB_TABLE_DATABASE_ERR_AMBIG_JOIN => 'Ambiguous join in autoJoin, during join of table', DB_TABLE_DATABASE_ERR_FAIL_JOIN => 'Failed join in autoJoin, failed to join table', DB_TABLE_DATABASE_ERR_PHP_VERSION => 'PHP 5 is required for fromXML method. Interpreter version is', DB_TABLE_DATABASE_ERR_XML_PARSE => 'Error parsing XML in fromXML method' ); // merge default and user-defined error messages if (!isset($GLOBALS['_DB_TABLE_DATABASE']['error'])) { $GLOBALS['_DB_TABLE_DATABASE']['error'] = array(); } foreach ($GLOBALS['_DB_TABLE_DATABASE']['default_error'] as $code => $message) { if (!array_key_exists($code, $GLOBALS['_DB_TABLE_DATABASE']['error'])) { $GLOBALS['_DB_TABLE_DATABASE']['error'][$code] = $message; } } // }}} // {{{ DB_Table_Database /** * Relational database abstraction class * * DB_Table_Database is an abstraction class for a relational database. * It is a layer built on top of DB_Table, in which each table in a * database is represented as an instance of DB_Table. It provides: * * - an object-oriented representation of the database schema * - automated construction of SQL commands for simple joins * - an API for insert, update, and select commands very similar * to that of DB_Table, with optional emulation of standard SQL * foreign key integrity checks and referential triggered actions * such as cascading deletes. * - Serialization and unserialization of the database schema via * either php serialization or XML, using the MDB2 XML schema. * * @category Database * @package DB_Table * @author David C. Morse * @version Release: 1.5.6 * @link http://pear.php.net/package/DB_Table */ class DB_Table_Database extends DB_Table_Base { // {{{ properties /** * Name of the database * * @var string * @access public */ var $name = null; /** * Associative array of DB_Table object references. Keys are table names. * * Associative array in which keys are table names, values are references to * DB_Table objects. Each referenced DB_Table object represents one table in * the database. * * @var array * @access private */ var $_table = array(); /** * Array in which keys are table names, values are DB_Table subclass names. * * See the getTableSubclass() method docblock for further details. * * @var array * @access private */ var $_table_subclass = array(); /** * Path to directory containing DB_Table subclass declaration files * * See the setTableSubclassPath() method docblock for further details. * * @var string * @access private */ var $_table_subclass_path = ''; /** * Array in which keys are table names, values are primary keys. * * Each primary key value may be a column name string, a sequential array of * column name strings, or null. * * See the getPrimaryKey() method docblock for details. * * @var array * @access private */ var $_primary_key = array(); /** * Associative array that maps column names keys to table names. * * Each key is the name string of a column in the database. Each value * is a numerical array containing the names of all tables that contain * a column with that name. * * See the getCol() method docblock for details. * * @var array * @access private */ var $_col = array(); /** * Associative array that maps names of foreign key columns to table names * * Each key is the name string of a foreign key column. Each value is a * sequential array containing the names of all tables that contain a * foreign key column with that name. * * See the getForeignCol() method docblock for further details. * * @var array * @access private */ var $_foreign_col = array(); /** * Two-dimensional associative array of foreign key references. * * Keys are pairs of table names (referencing table first, referenced * table second). Each value is an array containing information about * the referencing and referenced keys, and about any referentially * triggered actions (e.g., cascading delete). * * See the getRef() docblock for further details. * * @var array * @access private */ var $_ref = array(); /** * Array in which each key is the names of a referenced tables, each value * an sequential array containing names of referencing tables. * * See the docblock for the getRefTo() method for further discussion. * * @var array * @access private */ var $_ref_to = array(); /** * Two-dimensional associative array of linking tables. * * Two-dimensional associative array in which pairs of keys are names * of pairs of tables that are linked by one or more linking/association * table. Each value is an array containing the names of all table that * link the tables specified by the pair of keys. A linking table is a * table that creates a many-to-many relationship between two linked * tables, via foreign key references from the linking table to the two * linked tables. The $_link property is used by the autoJoin() method * to join tables that are related only through such a linking table. * * See the getLink() method docblock for further details. * * @var array * @access private */ var $_link = array(); /** * Take on_update actions if $_act_on_update is true * * By default, on_update actions are enabled ($_act_on_update = true) * * @var boolean * @access private */ var $_act_on_update = true; /** * Take on_delete actions if $_act_on_delete is true * * By default, on_delete actions are enabled ($_act_on_delete = true) * * @var boolean * @access private */ var $_act_on_delete = true; /** * Validate foreign keys before insert or update if $_check_fkey is true * * By default, validation is disabled ($_check_fkey = false) * * @var boolean * @access private */ var $_check_fkey = false; /** * If the column keys in associative array return sets are fixed case * (all upper or lower case) this property should be set true. * * The column keys in rows of associative array return sets may either * preserve capitalization of the column names or they may be fixed case, * depending on the options set in the backend (DB/MDB2) and on phptype. * If these column names are returned with a fixed case (either upper * or lower), $_fix_case must be set true in order for php emulation of * ON DELETE and ON UPDATE actions to work correctly. Otherwise, the * $_fix_case property should be false (the default). * * The choice between mixed or fixed case column keys may be made by using * using the setFixCase() method, which resets both the behavior of the * backend and the $_fix_case property. It may also be changed by using the * setOption() method of the DB or MDB2 backend object to directly set the * DB_PORTABILITY_LOWERCASE or MDB2_PORTABILITY_FIX_CASE bits of the * DB/MDB2 'portability' option. * * By default, DB returns mixed case and MDB2 returns lower case. * * @see DB_Table_Database::setFixCase() * @see DB::setOption() * @see MDB2::setOption() * * @var boolean * @access private */ var $_fix_case = false; // }}} // {{{ Methods // {{{ function DB_Table_Database(&$db, $name) /** * Constructor * * If an error is encountered during instantiation, the error * message is stored in the $this->error property of the resulting * object. See $error property docblock for a discussion of error * handling. * * @param object &$db DB/MDB2 database connection object * @param string $name the database name * @return object DB_Table_Database * @access public */ function __construct(&$db, $name) { // Is $db an DB/MDB2 object or null? if (is_a($db, 'db_common')) { $this->backend = 'db'; $this->fetchmode = DB_FETCHMODE_ORDERED; } elseif (is_a($db, 'mdb2_driver_common')) { $this->backend = 'mdb2'; $this->fetchmode = MDB2_FETCHMODE_ORDERED; } else { $code = DB_TABLE_DATABASE_ERR_DB_OBJECT ; $text = $GLOBALS['_DB_TABLE_DATABASE']['error'][$code] . ' DB_Table_Database'; $this->error = PEAR::throwError($text, $code); return; } $this->db =& $db; $this->name = $name; $this->_primary_subclass = 'DB_TABLE_DATABASE'; $this->setFixCase(false); } // }}} // {{{ function setDBconnection(&$db) /** * Set DB/MDB2 connection instance for database and all tables * * Assign a reference to the DB/MDB2 object $db to $this->db, set * $this->backend to 'db' or 'mdb2', and set the same pair of * values for the $db and $backend properties of every DB_Table * object in the database. * * @param object &$db DB/MDB2 connection object * @return boolean True on success (PEAR_Error on failure) * * @throws PEAR_Error if * $db is not a DB or MDB2 object(DB_TABLE_DATABASE_ERR_DB_OBJECT) * * @access public */ function setDBconnection(&$db) { // Is the first argument a DB/MDB2 object ? if (is_subclass_of($db, 'DB_Common')) { $backend = 'db'; } elseif (is_subclass_of($db, 'MDB2_Driver_Common')) { $backend = 'mdb2'; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_DB_OBJECT, "setDBconnection"); } // Set db and backend for database and all of its tables $this->db =& $db; $this->backend = $backend; foreach ($this->_table as $name => $table) { $table->db =& $db; $table->backend = $backend; } return true; } // }}} // {{{ function setActOnDelete($flag = true) /** * Turns on (or off) automatic php emulation of SQL ON DELETE actions * * @param bool $flag True to enable action, false to disable * @return void * @access public */ function setActOnDelete($flag = true) { if ($flag) { $this->_act_on_delete = true; } else { $this->_act_on_delete = false; } } // }}} // {{{ function setActOnUpdate($flag = true) /** * Turns on (or off) automatic php emulation of ON UPDATE actions * * @param bool $flag True to enable action, false to disable * @return void * @access public */ function setActOnUpdate($flag = true) { if ($flag) { $this->_act_on_update = true; } else { $this->_act_on_update = false; } } // }}} // {{{ function setCheckFKey($flag = true) /** * Turns on (or off) validation of foreign key values on insert and update * * @param bool $flag True to enable foreign key validation, false to disable * @return void * @access public */ function setCheckFKey($flag = true) { if ($flag) { $this->_check_fkey = true; } else { $this->_check_fkey = false; } } // }}} // {{{ function setFixCase($flag = false) /** * Sets backend option such that column keys in associative array return * sets are converted to fixed case, if true, or mixed case, if false. * * Sets the DB/MDB2 'portability' option, and sets $this->_fix_case = $flag. * Because it sets an option in the underlying DB/MDB2 connection object, * this effects the behavior of all objects that share the connection. * * @param bool $flag True for fixed lower case, false for mixed * @return void * @access public */ function setFixCase($flag = false) { $flag = (bool) $flag; $option = $this->db->getOption('portability'); if ($this->backend == 'db') { $option = $option | DB_PORTABILITY_LOWERCASE; if (!$flag) { $option = $option ^ DB_PORTABILITY_LOWERCASE; } } else { $option = $option | MDB2_PORTABILITY_FIX_CASE; if (!$flag) { $option = $option ^ MDB2_PORTABILITY_FIX_CASE; } } $this->db->setOption('portability', $option); $this->_fix_case = $flag; } // }}} // {{{ function &getDBInstance() /** * Return reference to $this->db DB/MDB2 object wrapped by $this * * @return object Reference to DB/MDB2 object * @access public */ function &getDBInstance() { return $this->db; } // }}} // {{{ function getTable($name = null) /** * Returns all or part of $_table property array * * If $name is absent or null, return entire $_table property array. * If $name is a table name, return $this->_table[$name] DB_Table object * reference * * The $_table property is an associative array in which keys are table * name strings and values are references to DB_Table objects. Each of * the referenced objects represents one table in the database. * * @param string $name Name of table * @return mixed $_table property, or one element of $_table * (PEAR_Error on failure) * * @throws PEAR_Error if: * - $name is not a string ( DB_TABLE_DATABASE_ERR_TBL_NOT_STRING ) * - $name is not valid table name ( DB_TABLE_DATABASE_ERR_NO_TBL ) * * @access public */ function getTable($name = null) { if (is_null($name)) { return $this->_table; } elseif (is_string($name)) { if (isset($this->_table[$name])) { return $this->_table[$name]; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "getTable, $name"); } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_TBL_NOT_STRING, "getTable"); } } // }}} // {{{ function getPrimaryKey($name = null) /** * Returns all or part of the $_primary_key property array * * If $name is null, return the $this->_primary_key property array * If $name is a table name, return $this->_primary_key[$name] * * The $_primary_key property is an associative array in which each key * a table name, and each value is the primary key of that table. Each * primary key value may be a column name string, a sequential array of * column name strings (for a multi-column key), or null (if no primary * key has been declared). * * @param string $name Name of table * @return mixed $this->primary_key array or $this->_primary_key[$name] * (PEAR_Error on failure) * * @throws PEAR_Error if: * - $name is not a string ( DB_TABLE_DATABASE_ERR_TBL_NOT_STRING ) * - $name is not valid table name ( DB_TABLE_DATABASE_ERR_NO_TBL ) * * @access public */ function getPrimaryKey($name = null) { if (is_null($name)) { return $this->_primary_key; } elseif (is_string($name)) { if (isset($this->_primary_key[$name])) { return $this->_primary_key[$name]; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "getPrimaryKey, $name"); } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_TBL_NOT_STRING, "getPrimaryKey"); } } // }}} // {{{ function getTableSubclass($name = null) /** * Returns all or part of the $_table_subclass property array * * If $name is null, return the $this->_table_subclass property array * If $name is a table name, return $this->_table_subclass[$name] * * The $_table_subclass property is an associative array in which each key * is a table name string, and each value is the name of the corresponding * subclass of DB_Table. The value is null if the table is an instance of * DB_Table itself. * * Subclass names are set within the addTable method by applying the * built in get_class() function to a DB_Table object. The class names * returned by get_class() are stored unmodified. In PHP 4, get_class * converts all class names to lower case. In PHP 5, it preserves the * capitalization of the name used in the class definition. * * For autoloading of class definitions to work properly in the * __wakeup() method, the base name of each subclass definition * file (excluding the .php extension) should thus be a identical * to the class name in PHP 5, and a lower case version of the * class name in PHP 4 or * * @param string $name Name of table * @return mixed $_table_subclass array or $this->_table_subclass[$name] * (PEAR_Error on failure) * * @throws PEAR_Error if: * - $name is not a string ( DB_TABLE_DATABASE_TBL_NOT_STRING ) * - $name is not valid table name ( DB_TABLE_DATABASE_NO_TBL ) * * @access public * @ @see DB_Table_Database::__wakeup() */ function getTableSubclass($name = null) { if (is_null($name)) { return $this->_table_subclass; } elseif (is_string($name)) { if (isset($this->_table_subclass[$name])) { return $this->_table_subclass[$name]; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "getTableSubclass, $name"); } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_TBL_NOT_STRING, "getTableSubclass"); } } // }}} // {{{ function getCol($column_name = null) /** * Returns all or part of the $_col property array * * If $column_name is null, return $_col property array * If $column_name is valid, return $_col[$column_name] subarray * * The $_col property is an associative array in which each key is the * name of a column in the database, and each value is a numerical array * containing the names of all tables that contain a column with that * name. * * @param string $column_name a column name string * @return mixed $this->_col property array or $this->_col[$column_name] * (PEAR_Error on failure) * * @throws PEAR_Error if: * - $column_name is not a string (DB_TABLE_DATABASE_ERR_COL_NOT_STRING) * - $column_name is not valid column name (DB_TABLE_DATABASE_NO_COL) * * @access public */ function getCol($column_name = null) { if (is_null($column_name)) { return $this->_col; } elseif (is_string($column_name)) { if (isset($this->_col[$column_name])) { return $this->_col[$column_name]; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_COL, "'$column_name'"); } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_COL_NOT_STRING, 'getCol'); } } // }}} // {{{ function getForeignCol($column_name = null) /** * Returns all or part of the $_foreign_col property array * * If $column_name is null, return $this->_foreign_col property array * If $column_name is valid, return $this->_foreign_col[$column_name] * * The $_foreign_col property is an associative array in which each * key is the name string of a foreign key column, and each value is a * sequential array containing the names of all tables that contain a * foreign key column with that name. * * If a column $column in a referencing table $ftable is part of the * foreign key for references to two or more different referenced tables * tables, the name $ftable will also appear multiple times in the array * $this->_foreign_col[$column]. * * Returns a PEAR_Error with the following DB_TABLE_DATABASE_* error * codes if: * - $column_name is not a string ( _COL_NOT_STRING ) * - $column_name is not valid foreign column name ( _NO_FOREIGN_COL ) * * @param string column name string for foreign key column * @return array $_foreign_col property array * @access public */ function getForeignCol($column_name = null) { if (is_null($column_name)) { return $this->_foreign_col; } elseif (is_string($column_name)) { if (isset($this->_foreign_col[$column_name])) { return $this->_foreign_col[$column_name]; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_FOREIGN_COL, $column_name); } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_COL_NOT_STRING, 'getForeignCol'); } } // }}} // {{{ function getRef($table1 = null, $table2 = null) /** * Returns all or part of the $_ref two-dimensional property array * * Returns $this->_ref 2D property array if $table1 and $table2 are null. * Returns $this->_ref[$table1] subarray if only $table2 is null. * Returns $this->_ref[$table1][$table2] if both parameters are present. * * Returns null if $table1 is a table that references no others, or * if $table1 and $table2 are both valid table names, but there is no * reference from $table1 to $table2. * * The $_ref property is a two-dimensional associative array in which * the keys are pairs of table names, each value is an array containing * information about referenced and referencing keys, and referentially * triggered actions (if any). An element of the $_ref array is of the * form $ref[$ftable][$rtable] = $reference, where $ftable is the name * of a referencing (or foreign key) table and $rtable is the name of * a corresponding referenced table. The value $reference is an array * $reference = array($fkey, $rkey, $on_delete, $on_update) in which * $fkey and $rkey are the foreign (or referencing) and referenced * keys, respectively: Foreign key $fkey of table $ftable references * key $rkey of table $rtable. The values of $fkey and $rkey must either * both be valid column name strings for columns of the same type, or * they may both be sequential arrays of column name names, with equal * numbers of columns of corresponding types, for multi-column keys. The * $on_delete and $on_update values may be either null or string values * that indicate actions to be taken upon deletion or updating of a * referenced row (e.g., cascading deletes). A null value of $on_delete * or $on_update indicates that no referentially triggered action will * be taken. See addRef() for further details about allowed values of * these action strings. * * @param string $table1 name of referencing table * @param string $table2 name of referenced table * @return mixed $ref property array, sub-array, or value * * @throws a PEAR_Error if: * - $table1 or $table2 is not a string (.._DATABASE_ERR_TBL_NOT_STRING) * - $table1 or $table2 is not a table name (.._DATABASE_ERR_NO_TBL) * * @access public */ function getRef($table1 = null, $table2 = null) { if (is_null($table1)) { return $this->_ref; } elseif (is_string($table1)) { if (isset($this->_ref[$table1])) { if (is_null($table2)) { return $this->_ref[$table1]; } elseif (is_string($table2)) { if (isset($this->_ref[$table1][$table2])) { return $this->_ref[$table1][$table2]; } else { if (isset($this->_table[$table2])) { // Valid table names but no references to return null; } else { // Invalid table name return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "getRef, $table2"); } } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_TBL_NOT_STRING, "getRef"); } } else { if (isset($this->_table[$table1])) { // Valid table name, but no references from return null; } else { // Invalid table name return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "getRef, $table1"); } } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_TBL_NOT_STRING, "getRef"); } } // }}} // {{{ function getRefTo($table_name = null) /** * Returns all or part of the $_ref_to property array * * Returns $this->_ref_to property array if $table_name is null. * Returns $this->_ref_to[$table_name] if $table_name is not null. * * The $_ref_to property is an associative array in which each key * is the name of a referenced table, and each value is a sequential * array containing the names of all tables that contain foreign keys * that reference that table. Each element is thus of the form * $_ref_to[$rtable] = array($ftable1, $ftable2,...), where * $ftable1, $ftable2, ... are the names of tables that reference * the table named $rtable. * * @param string $table_name name of table * @return mixed $_ref_to property array or subarray * (PEAR_Error on failure) * * @throws PEAR_Error if: * - $table_name is not a string ( .._DATABASE_ERR_TBL_NOT_STRING ) * - $table_name is not a table name ( .._DATABASE_ERR_NO_TBL ) * * @access public */ function getRefTo($table_name = null) { if (is_null($table_name)) { return $this->_ref_to; } elseif (is_string($table_name)) { if (isset($this->_ref_to[$table_name])) { return $this->_ref_to[$table_name]; } else { if (isset($this->_table[$table_name])) { // Valid table name, but no references to return null; } else { // Invalid table name return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "getRefTo, $table_name"); } } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_TBL_NOT_STRING, "getRefTo"); } } // }}} // {{{ function getLink($table1 = null, $table2 = null) /** * Returns all or part of the $link two-dimensional property array * * Returns $this->_link 2D property array if $table1 and $table2 are null. * Returns $this->_link[$table1] subarray if only $table2 is null. * Returns $this->_link[$table1][$table2] if both parameters are present. * * Returns null if $table1 is a valid table with links to no others, or * if $table1 and $table2 are both valid table names but there is no * link between them. * * The $_link property is a two-dimensional associative array with * elements of the form $this->_link[$table1][$table2] = array($link1, ...), * in which the value is an array containing the names of all tables * that `link' tables named $table1 and $table2, and thereby create a * many-to-many relationship between these two tables. * * The $_link property is used in the autoJoin method to join tables * that are related by a many-to-many relationship via a linking table, * rather than via a direct foreign key reference. A table that is * declared to be linking table for tables $table1 and $table2 must * contain foreign keys that reference both of these tables. * * Each binary link in a database is listed twice in $_link, in * $_link[$table1][$table2] and in $_link[$table2][$table1]. If a * linking table contains foreign key references to N tables, with * N > 2, each of the resulting binary links is listed separately. * For example, a table with references to 3 tables A, B, and C can * create three binary links (AB, AC, and BC) and six entries in the * link property array (i.e., in $_link[A][B], $_link[B][A], ... ). * * Linking tables may be added to the $_link property by using the * addLink method or deleted using the delLink method. Alternatively, * all possible linking tables can be identified and added to the * $_link array at once by the addAllLinks() method. * * @param string $table1 name of linked table * @param string $table2 name of linked table * @return mixed $_link property array, sub-array, or value * * @throws PEAR_Error: * - $table1 or $table2 is not a string (..DATABASE_ERR_TBL_NOT_STRING) * - $table1 or $table2 is not a table name (..DATABASE_ERR_NO_TBL) * * @access public */ function getLink($table1 = null, $table2 = null) { if (is_null($table1)) { return $this->_link; } elseif (is_string($table1)) { if (isset($this->_link[$table1])) { if (is_null($table2)) { return $this->_link[$table1]; } elseif (is_string($table2)) { if (isset($this->_link[$table1][$table2])) { return $this->_link[$table1][$table2]; } else { if (isset($this->_table[$table2])) { // Valid table names, but no links return null; } else { // Invalid 2nd table name string return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "getLink, $table2"); } } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_TBL_NOT_STRING, "getLink"); } } else { if (isset($this->_table[$table1])) { // Valid first table name, but no links return null; } else { // Invalid 1st table name string return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "getLink, $table1"); } } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_TBL_NOT_STRING, "getLink"); } } // }}} // {{{ function setTableSubclassPath($path) /** * Sets path to a directory containing DB_Table subclass definitions. * * This method sets the $_table_subclass_path string property. The value of * this property is the path to the directory containing DB_Table subclass * definitions, without a trailing directory separator. * * This path may be used by the __wakeup(), if necessary, in an attempt to * autoload class definitions when unserializing a DB_Table_Database object * and its child DB_Table objects. If a DB_Table subclass $subclass_name * has not been defined when it is needed in DB_Table_Database::__wakeup(), * to unserialize an instance of this class, the __wakeup() method attempts * to include a class definition file from this directory, as follows: * * $dir = $this->_table_subclass_path; * require_once $dir . '/' . $subclass . '.php'; * * See the getTableSubclass() docblock for a discusion of capitalization * conventions in PHP 4 and 5 for subclass file names. * * @param string $path path to directory containing class definitions * @return void * @access public * * @see DB_Table_Database::getTableSubclass() */ function setTableSubclassPath($path) { $this->_table_subclass_path = $path; } // }}} // {{{ function addTable(&$table_obj) /** * Adds a table to the database. * * Creates references between $this DB_Table_Database object and * the child DB_Table object, by adding a reference to $table_obj * to the $this->_table array, and setting $table_obj->database = * $this. * * Adds the primary key to $this->_primary_key array. The relevant * element of $this->_primary_key is set to null if no primary key * index is declared. Returns an error if more than one primary key * is declared. * * Returns true on success, and PEAR error on failure. Returns the * following DB_TABLE_DATABASE_ERR_* error codes if: * - $table_obj is not a DB_Table ( _DBTABLE_OBJECT ) * - more than one primary key is defined ( _ERR_MULT_PKEY ) * * @param object &$table_obj the DB_Table object (reference) * @return boolean true on success (PEAR_Error on failure) * @access public */ function addTable(&$table_obj) { // Check that $table_obj is a DB_Table object // Identify subclass name, if any if (is_subclass_of($table_obj, 'DB_Table')) { $subclass = get_class($table_obj); } elseif (is_a($table_obj, 'DB_Table')) { $subclass = null; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_DBTABLE_OBJECT); } // Identify table name and table object (sub)class name $table = $table_obj->table; // Set $this->_primary_key[$table] $this->_primary_key[$table] = null; foreach ($table_obj->idx as $idx_name => $idx_def) { if ($idx_def['type'] == 'primary') { if (is_null($this->_primary_key[$table])) { $this->_primary_key[$table] = $idx_def['cols']; } else { // More than one primary key defined in the table unset($this->_primary_key[$table]); return $this->throwError( DB_TABLE_DATABASE_ERR_MULT_PKEY, $table); } } } // Add references between $this parent and child table object $this->_table[$table] =& $table_obj; $table_obj->setDatabaseInstance($this); // Add subclass name (if any) to $this->_table_subclass $this->_table_subclass[$table] = $subclass; // Set shared properties $table_obj->db =& $this->db; $table_obj->backend = $this->backend; $table_obj->fetchmode = $this->fetchmode; // Add all columns to $_col property foreach ($table_obj->col as $key => $def) { if (!isset($this->_col[$key])) { $this->_col[$key] = array(); } $this->_col[$key][] = $table; } return true; } // }}} // {{{ function deleteTable($table) /** * Deletes a table from $this database object. * * Removes all dependencies on $table from the database model. The table * is removed from $_table and $_primary_key properties. Its columns are * removed from the $_col and $_foreign_col properties. References to * and from the table are removed from the $_ref, $_ref_to, and $_link * properties. Referencing columns are removed from $_foreign_col. * * @param string $table name of table to be deleted * @return void * @access public */ function deleteTable($table) { if (isset($this->_table[$table])) { $table_obj =& $this->_table[$table]; } else { return; } // Remove reference to database from table object $null_instance = null; $table_obj->setDatabaseInstance($null_instance); // Remove columns from $_col and $_foreign_col property arrays foreach ($table_obj->col as $column => $def) { $key = array_search($table, $this->_col[$column]); if (is_integer($key)) { unset($this->_col[$column][$key]); if (count($this->_col[$column]) == 0) { unset($this->_col[$column]); } else { $new = array_values($this->_col[$column]); $this->_col[$column] = $new; } } if (isset($this->_foreign_col[$column])) { $key = array_search($table, $this->_foreign_col[$column]); if (is_integer($key)) { unset($this->_foreign_col[$column][$key]); if (count($this->_foreign_col[$column]) == 0) { unset($this->_foreign_col[$column]); } else { $new = array_values($this->_foreign_col[$column]); $this->_foreign_col[$column] = $new; } } } } // Remove all references involving the deleted table. // Corresponding links are removed from $this->_link by deleteRef // Referencing columns are removed from $this->_foreign_col by deleteRef foreach ($this->_ref as $ftable => $referenced) { foreach ($referenced as $rtable => $ref) { if ($ftable == $table || $rtable == $table) { $this->deleteRef($ftable, $rtable); } } } // Remove table from $this->_table and $this->_primary_key unset($this->_table[$table]); unset($this->_primary_key[$table]); } // }}} // {{{ function addRef($ftable, $fkey, $rtable, [$rkey], [$on_delete], [$on_update]) /** * Adds a foreign key reference to the database. * * Adds a reference from foreign key $fkey of table $ftable to * referenced key $rkey of table named $rtable to the $this->_ref * property. The values of $fkey and $rkey (if not null) may either * both be column name strings (for single column keys) or they * may both be numerically indexed arrays of corresponding column * names (for multi-column keys). If $rkey is null (the default), * the referenced key taken to be the primary key of $rtable, if * any. * * The $on_delete and $on_update parameters may be either be null, * or may have string values 'restrict', 'cascade', 'set null', or * 'set default' that indicate referentially triggered actions to be * taken deletion or updating of referenced row in $rtable. Each of * these actions corresponds to a standard SQL action (e.g., cascading * delete) that may be taken upon referencing rows of table $ftable * when a referenced row of $rtable is deleted or updated. A PHP * null value for either parameter (the default) signifies that no * such action will be taken upon deletion or updating. * * There may no more than one reference from a table to another, though * reference may contain multiple columns. * * Returns true on success, and PEAR error on failure. Returns the * following DB_TABLE_DATABASE_ERR_* error codes if: * - $ftable does not exist ( _NO_FTABLE ) * - $rtable does not exist ( _NO_RTABLE ) * - $rkey is null and $rtable has no primary key ( _NO_PKEY ) * - $fkey is neither a string nor an array ( _FKEY ) * - $rkey is not a string, $fkey is a string ( _RKEY_NOT_STRING ) * - $rkey is not an array, $fkey is an array ( _RKEY_NOT_ARRAY ) * - A column of $fkey does not exist ( _NO_FCOL ) * - A column of $rkey does not exist ( _NO_RCOL ) * - A column of $fkey and $rkey have different types ( _REF_TYPE ) * - A reference from $ftable to $rtable already exists ( _MULT_REF ) * * @param string $ftable name of foreign/referencing table * @param mixed $fkey foreign key in referencing table * @param string $rtable name of referenced table * @param mixed $rkey referenced key in referenced table * @param string $on_delete action upon delete of a referenced row. * @param string $on_update action upon update of a referenced row. * @return boolean true on success (PEAR_Error on failure) * @access public */ function addRef($ftable, $fkey, $rtable, $rkey = null, $on_delete = null, $on_update = null) { // Check existence of $ftable is a key in $this->_table. if (isset($this->_table[$ftable])) { $ftable_obj =& $this->_table[$ftable]; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_FTABLE, "$ftable => $rtable"); } // Check existence of referenced table if (isset($this->_table[$rtable])) { $rtable_obj =& $this->_table[$rtable]; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_RTABLE, "$ftable => $rtable"); } // If referenced key is null, set it to the primary key if (!$rkey) { if (isset($this->_primary_key[$rtable])) { $rkey = $this->_primary_key[$rtable]; } else { // Error: null referenced key and no primary key return $this->throwError( DB_TABLE_DATABASE_ERR_NO_PKEY, "$ftable => $rtable"); } } // Check $fkey and $rkey types and compatibility if (is_string($fkey)) { if (!is_string($rkey)) { return $this->throwError( DB_TABLE_DATABASE_ERR_RKEY_NOT_STRING, "$ftable => $rtable"); } if (!isset($ftable_obj->col[$fkey])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_FCOL, "$ftable.$fkey => $rtable.$rkey"); } if (!isset($rtable_obj->col[$rkey])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_RCOL, "$ftable.$fkey => $rtable.$rkey"); } $ftype = $ftable_obj->col[$fkey]['type']; $rtype = $rtable_obj->col[$rkey]['type']; if (!($rtype == $ftype)) { return $this->throwError( DB_TABLE_DATABASE_ERR_REF_TYPE, "$ftable.$fkey => $rtable.$rkey"); } } elseif (is_array($fkey)) { if (!is_array($rkey)) { return $this->throwError( DB_TABLE_DATABASE_ERR_RKEY_NOT_ARRAY, "$ftable => $rtable"); } if (!(count($fkey) == count($rkey))) { return $this->throwError( DB_TABLE_DATABASE_ERR_RKEY_COL_NUMBER, "$ftable => $rtable"); } for ($i=0 ; $i < count($rkey) ; $i++) { $fcol = $fkey[$i]; $rcol = $rkey[$i]; if (!isset($ftable_obj->col[$fcol])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_FCOL, "$ftable.$fcol => $rtable.$rcol"); } if (!isset($rtable_obj->col[$rcol])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_RCOL, "$ftable.$fcol => $rtable.$rcol"); } $ftype = $ftable_obj->col[$fcol]['type']; $rtype = $rtable_obj->col[$rcol]['type']; if (!($rtype == $ftype)) { return $this->throwError( DB_TABLE_DATABASE_ERR_REF_TYPE, "$ftable.$fcol => $rtable.$rcol"); } } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_FKEY, "$ftable => $rtable"); } // Check validity of on_delete and on_update actions $valid_actions = array(null, 'cascade', 'set null', 'set default', 'restrict'); if (!in_array($on_delete, $valid_actions)) { return $this->throwError( DB_TABLE_DATABASE_ERR_ON_DELETE_ACTION, "$ftable => $rtable"); } if (!in_array($on_update, $valid_actions)) { return $this->throwError( DB_TABLE_DATABASE_ERR_ON_UPDATE_ACTION, "$ftable => $rtable"); } // Add reference to $this->_ref; $ref = array( 'fkey' => $fkey, 'rkey' => $rkey, 'on_delete' => $on_delete, 'on_update' => $on_update); if (!isset($this->_ref[$ftable])) { $this->_ref[$ftable] = array(); } else { if (isset($this->_ref[$ftable][$rtable])) { // Multiple references from $ftable to $rtable return $this->throwError( DB_TABLE_DATABASE_ERR_MULT_REF, "$ftable => $rtable"); } } $this->_ref[$ftable][$rtable] = $ref; // Add referencing table $ftable to $ref_to property if (!isset($this->_ref_to[$rtable])) { $this->_ref_to[$rtable] = array(); } $this->_ref_to[$rtable][] = $ftable; // Add foreign key columns to $this->_foreign_col if (is_string($fkey)) { if (!isset($this->_foreign_col[$fkey])) { $this->_foreign_col[$fkey] = array(); } $this->_foreign_col[$fkey][] = $ftable; } elseif (is_array($fkey)) { foreach ($fkey as $fcol) { if (!isset($this->_foreign_col[$fcol])) { $this->_foreign_col[$fcol] = array(); } $this->_foreign_col[$fcol][] = $ftable; } } // Normal completion return true; } // }}} // {{{ function deleteRef($ftable, $rtable) /** * Deletes one reference from database model * * Removes reference from referencing (foreign key) table named * $ftable to referenced table named $rtable. Unsets relevant elements * of the $ref, $_ref_to, and $_link property arrays, and removes the * foreign key columns of $ftable from the $_foreign_col property. * * Does nothing, silently, if no such reference exists, i.e., if * $this->_ref[$ftable][$rtable] is not set. * * @param $ftable name of referencing (foreign key) table * @param $rtable name of referenced table * @return void * @access public */ function deleteRef($ftable, $rtable) { // Delete from $_ref property if (isset($this->_ref[$ftable])) { if (isset($this->_ref[$ftable][$rtable])) { $fkey = $this->_ref[$ftable][$rtable]['fkey']; unset($this->_ref[$ftable][$rtable]); } else { // No such reference, abort silently return; } } // Remove foreign key columns from $foreign_col property if (isset($fkey)) { if (is_string($fkey)) { $fkey = array($fkey); } foreach ($fkey as $column) { if (isset($this->_foreign_col[$column])) { $key = array_search($ftable, $this->_foreign_col[$column]); if (is_integer($key)) { unset($this->_foreign_col[$column][$key]); if (count($this->_foreign_col[$column]) == 0) { unset($this->_foreign_col[$column]); } else { $new = array_values($this->_foreign_col[$column]); $this->_foreign_col[$column] = $new; } } } } } // Delete from $_ref_to property if (isset($this->_ref_to[$rtable])) { $key = array_search($ftable, $this->_ref_to[$rtable]); // Unset element unset($this->_ref_to[$rtable][$key]); if (count($this->_ref_to[$rtable]) == 0) { unset($this->_ref_to[$rtable]); } else { // Redefine numerical keys of remaining elements $ref_to = array_values($this->_ref_to[$rtable]); $this->_ref_to[$rtable] = $ref_to; } } // Delete all relevant links from $_link property if (isset($this->_link[$rtable])) { foreach ($this->_link[$rtable] as $table2 => $links) { if (in_array($ftable, $links)) { $this->deleteLink($rtable, $table2, $ftable); } } } } // }}} // {{{ function setOnDelete($ftable, $rtable, $action) /** * Modifies the on delete action for one foreign key reference. * * Modifies the value of the on_delete action associated with a reference * from $ftable to $rtable. The parameter action may be one of the action * strings 'cascade', 'restrict', 'set null', or 'set default', or it may * be php null. A null value of $action indicates that no action should be * taken upon deletion of a referenced row. * * Returns true on success, and PEAR error on failure. Returns the error * code DB_TABLE_DATABASE_ERR_REF_TRIG_ACTION if $action is a neither a * valid action string nor null. Returns true, and does nothing, if * $this->_ref[$ftable][$rtable] is not set. * * @param string $ftable name of referencing (foreign key) table * @param string $rtable name of referenced table * @param string $action on delete action (action string or null) * @return boolean true on normal completion (PEAR_Error on failure) * @access public */ function setOnDelete($ftable, $rtable, $action) { $valid_actions = array(null, 'cascade', 'set null', 'set default', 'restrict'); if (isset($this->_ref[$ftable])) { if (isset($this->_ref[$ftable][$rtable])) { if (!in_array($action, $valid_actions)) { return $this->throwError( DB_TABLE_DATABASE_ERR_REF_ON_DELETE_ACTION, "$ftable => $rtable"); } $this->_ref[$ftable][$rtable]['on_delete'] = $action; } } return true; } // }}} // {{{ function setOnUpdate($ftable, $rtable, $action) /** * Modifies on update action for one foreign key reference. * * Similar to setOnDelete. See setOnDelete for further details. * * @param string $ftable name of referencing (foreign key) table * @param string $rtable name of referenced table * @param array $action on update action (action string or null) * @return boolean true on normal completion (PEAR_Error on failure) * @access public */ function setOnUpdate($ftable, $rtable, $action) { $valid_actions = array(null, 'cascade', 'set null', 'set default', 'restrict'); if (isset($this->_ref[$ftable])) { if (isset($this->_ref[$ftable][$rtable])) { if (!in_array($action, $valid_actions)) { return $this->throwError( DB_TABLE_DATABASE_ERR_REF_ON_UPDATE_ACTION, "$ftable => $rtable"); } $this->_ref[$ftable][$rtable]['on_update'] = $action; } } return true; } // }}} // {{{ function addLink($table1, $table2, $link) /** * Identifies a linking/association table that links two others * * Adds table name $link to $this->_link[$table1][$table2] and * to $this->_link[$table2][$table1]. * * Returns true on success, and PEAR error on failure. Returns the * following DB_TABLE_DATABASE_ERR_* error codes if: * - $ftable does not exist ( _NO_FTABLE ) * - $rtable does not exist ( _NO_RTABLE ) * * @param string $table1 name of 1st linked table * @param string $table2 name of 2nd linked table * @param string $link name of linking/association table. * @return boolean true on success (PEAR_Error on failure) * @access public */ function addLink($table1, $table2, $link) { // Check for existence of all three tables if (is_string($table1)) { if (!isset($this->_table[$table1])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "addLink, $table1"); } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "addLink, $table1"); } if (!isset($this->_table[$table2])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "addLink, $table2"); } if (!isset($this->_table[$link])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "addLink, $link"); } if (!isset($this->_ref[$link])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_REF_LINK, "$link => $table1, $table2"); } else { if (!isset($this->_ref[$link][$table1])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_REF_LINK, "$link => $table1"); } if (!isset($this->_ref[$link][$table2])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_REF_LINK, "$link => $table2"); } } // Add $this_link[$table1][$table2] if (!key_exists($table1, $this->_link)) { $this->_link[$table1] = array(); } if (!key_exists($table2, $this->_link[$table1])) { $this->_link[$table1][$table2] = array(); } $this->_link[$table1][$table2][] = $link; // Add $this_link[$table2][$table1] if (!key_exists($table2, $this->_link)) { $this->_link[$table2] = array(); } if (!key_exists($table1, $this->_link[$table2])) { $this->_link[$table2][$table1] = array(); } $this->_link[$table2][$table1][] = $link; } // }}} // {{{ function addAllLink() /** * Adds all possible linking tables to the $_link property array * * Identifies all potential linking tables in the datbase, and adds * them all to the $_link property. Table $link is taken to be a * link between tables $table1 and $table2 if it contains foreign * key references to both $table1 and $table2. * * @return void * @access public */ function addAllLinks() { foreach ($this->_table as $link => $link_obj) { if (isset($this->_ref[$link])) { $ref = $this->_ref[$link]; $n = count($ref); $names = array_keys($ref); if ($n > 1) { $is_link = true; } else { $is_link = false; } if ($is_link) { if ($n == 2) { $table1 = $names[0]; $table2 = $names[1]; $this->addLink($table1, $table2, $link); } elseif ($n > 2) { for ($i=1 ; $i < $n; $i++) { for ($j=0 ; $j < $i; $j++) { $table1 = $names[$j]; $table2 = $names[$i]; $this->addLink($table1, $table2, $link); } } } } } } } // }}} // {{{ function deleteLink($table1, $table2, $link = null) /** * Removes a link between two tables from the $_link property * * If $link is not null, remove table $link from the list of links * between $table1 and $table2, if present. If $link is null, delete * all links between $table1 and $table2. * * @param string $table1 name of 1st linked table * @param string $table2 name of 2nd linked table * @param string $link name of linking table * @return void * @access public */ function deleteLink($table1, $table2, $link = null) { if (isset($this->_link[$table1])) { if (isset($this->_link[$table1][$table2])) { if ($link) { // Find numerical key of $link in _link[$table1][$table2] $key = array_search($link, $this->_link[$table1][$table2]); if (is_integer($key)) { unset($this->_link[$table1][$table2][$key]); if (count($this->_link[$table1][$table2]) == 0) { unset($this->_link[$table1][$table2]); unset($this->_link[$table2][$table1]); if (count($this->_link[$table1]) == 0) { unset($this->_link[$table1]); } if (count($this->_link[$table2]) == 0) { unset($this->_link[$table2]); } } else { // Reset remaining indices sequentially from zero $new = array_values($this->_link[$table1][$table2]); $this->_link[$table1][$table2] = $new; $this->_link[$table2][$table1] = $new; } } } else { unset($this->_link[$table1][$table2]); unset($this->_link[$table2][$table1]); if (count($this->_link[$table1]) == 0) { unset($this->_link[$table1]); } if (count($this->_link[$table2]) == 0) { unset($this->_link[$table2]); } } } } } // }}} // {{{ function validCol($col, $from = null) /** * Validates and (if necessary) disambiguates a column name. * * The parameter $col is a string may be either a column name or * a column name qualified by a table name, using the SQL syntax * "$table.$column". If $col contains a table name, and is valid, * an array($table, $column) is returned. If $col is not qualified * by a column name, an array array($table, $column) is returned, * in which $table is either the name of one table, or an array * containing the names of two or more tables containing a column * named $col. * * The $from parameter, if present, is a numerical array of * names of tables with which $col should be associated, if no * explicit table name is provided, and if possible. If one * or more of the tables in $from contains a column $col, the * returned table or set of tables is restricted to those in * array $from. * * If the table name remains ambiguous after testing for tables in * the $from set, and $col is not a foreign key in one or more of * the remaining tables, the returned table or set of tables is * restricted to those in which $col is not a foreign key. * * Returns a PEAR_Error with the following DB_TABLE_DATABASE_ERR_* error * codes if: * - column $col does not exist in the database ( _NO_COL_DB ) * - column $col does not exist in the specified table ( _NO_COL_TBL ) * * @param string $col column name, optionally qualified by a table name * @param array $from array of tables from which $col should be chosen, * if possible. * @return array array($table, $column), or PEAR_Error on failure * $column is a string, $table is a string or array * @access public */ function validCol($col, $from = null) { $col = explode('.',trim($col)); if (count($col) == 1) { // Parameter $col is a column name with no table name $column = $col[0]; // Does $column exist in database ? if (!isset($this->_col[$column])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_COL_DB, "$column"); } $table = $this->_col[$column]; // If $table is not unique, try restricting to arrays in $from if (count($table) > 1 && $from) { $ptable = array_intersect($table, $from); if (count($ptable) > 0) { $table = array_values($ptable); } } // If count($table)>1, try excluding foreign key columns if (count($table) > 1 && isset($this->_foreign_col[$column])) { $ptable = array_diff($table, $this->_foreign_col[$column]); if (count($ptable) > 0) { $table = array_values($ptable); } } // If only one table remains, set $table = table name string if (count($table) == 1) { $table = $table[0]; } } elseif (count($col) == 2) { // parameter $col is qualified by a table name $table = $col[0]; $column = $col[1]; if (isset($this->_table[$table])) { $table_obj =& $this->_table[$table]; $col_array = $table_obj->col; if (!isset($col_array[$column])) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_COL_TBL, "$table.$column"); } } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "validCol, $table"); } } return array($table, $column); } // }}} // {{{ function createTables($flag = 'safe') /** * Creates all the tables in a database in a RDBMS * * Note: this method creates all the tables in a database, but does * NOT create the parent database or set it to the current or default * database -- the database must exist before the method is called. * * If creation of any table fails, the method immediately returns the * PEAR error returned by DB_Table::create($flag). * * @param mixed $flag The automatic database creation mode, which is * applied to each table in the database. It can have * values: * - 'safe' to create a table only if it does not exist * - 'drop' to drop and recreate any existing table with the same name * * @return boolean true on sucess (PEAR_Error on failure of any table) * @access public * * @see DB_Table::create() */ function createTables($flag = 'safe') { foreach ($this->_table as $name => $table) { $result = $table->create($flag); if (PEAR::isError($result)) { return $result; } } return true; } // }}} // {{{ function validForeignKeys($table_name, $data) /** * Check validity of any foreign key values in associative array $data * containing values to be inserted or updated in table $table_name. * * Returns true if each foreign key in $data matches a row in the * referenced table, or if there are no foreign key columns in $data. * Returns a PEAR_Error if any foreign key column in associative array * $data (which may contain a full or partial row of $table_name), does * not match the the value of the referenced column in any row of the * referenced table. * * @param $table_name name of the referencing table containing $data * @param @data associative array containing all or part of a row * of data of $table_name, with column name keys. * @return bool true if all foreign keys are valid, returns PEAR_Error * if foreign keys are invalid or if an error is thrown * by a required query * * @throws PEAR error if: * - Error thrown by _buildFKeyFilter method (bubbles up) * - Error thrown by select method for required query (bubbles up) * * @access public */ function validForeignKeys($table_name, $data) { if (isset($this->_ref[$table_name])) { foreach ($this->_ref[$table_name] as $rtable_name => $ref) { $fkey = $ref['fkey']; $rkey = $ref['rkey']; $rtable_obj =& $this->_table[$rtable_name]; // Construct select where clause for referenced rows, // $filter = '' if $data contains no foreign key columns, $filter = $this->_buildFKeyFilter($data, $fkey, $rkey); if (PEAR::isError($filter)) { return $filter; } // If inserted data contain FK columns referenced by rtable, // select referenced row of rtable, return error if none is // found if ($filter) { $sql = array('select'=> '*', 'from' => $rtable_name, 'where' => $filter); $referenced_rows = $this->select($sql); // Check for failed query if (PEAR::isError($referenced_rows)) { return $referenced_rows; } // Check for failed foreign key constraint if (count($referenced_rows) == 0) { return $this->throwError( DB_TABLE_DATABASE_ERR_FKEY_CONSTRAINT); } } } } return true; } // }}} // {{{ function insert($table_name, $data) /** * Inserts a single table row * * Wrapper for insert method of the corresponding DB_Table object. * * Data will be validated before insertion using validForeignKey(), * if foreign key validation in enabled. * * @param string $table_name Name of table into which to insert data * @param array $data Associative array, in which each key is a column * name and each value is that column's value. * This is the data that will be inserted into * the table. Data is checked against the column * names and data types for validity. * @return boolean true on success (PEAR_Error on failure) * @access public */ function insert($table_name, $data) { // Dereference table object if (isset($this->_table[$table_name])) { $table_obj =& $this->_table[$table_name]; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "insert, $table_name"); } // Insert into $table_obj $result = $table_obj->insert($data); // Return value: true or PEAR_Error if (PEAR::isError($result)) { return $result; } else { return true; } } // }}} // {{{ function autoValidInsert($flag = true) /** * Turns on or off automatic validation of inserted data for all tables * * @param bool $flag true to turn on auto-validation, false to turn off. * @return void * @access public */ function autoValidInsert($flag = true) { foreach ($this->_table as $table_obj) { $table_obj->autoValidInsert($flag); } } // }}} // {{{ function update($table_name, $data, $where) /** * Updates all row(s) of table that match a custom where clause. * * Wrapper for insert method of the corresponding DB_Table object. * * Data will be validated before insertion using validForeignKey(), * if foreign key validation in enabled. * * Implements any required ON UPDATE actions on tables that * reference updated columns, if on update actions are enabled. * * @param string $table_name name of table to update * @param array $data associative array in which keys are names of * columns to be updated values are new values. * @param string $where SQL WHERE clause that limits the set of * records to update. * @return boolean true on success (PEAR_Error on failure) * @access public */ function update($table_name, $data, $where) { // Dereference table object if (isset($this->_table[$table_name])) { $table_obj =& $this->_table[$table_name]; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "update, $table_name"); } // Apply update $result = $table_obj->update($data, $where); // Return value: true or PEAR_Error if (PEAR::isError($result)) { return $result; } else { return true; } } // }}} // {{{ function autoValidUpdate($flag = true) /** * Turns on (or off) automatic validation of updated data for all tables. * * @param bool $flag true to turn on auto-validation, false to turn off * @return void * @access public */ function autoValidUpdate($flag = true) { foreach ($this->_table as $table_obj) { $table_obj->autoValidUpdate($flag); } } // }}} // {{{ function onUpdateAction(&$table_obj, $data, $where) /** * Implements any ON UPDATE actions triggered by updating of rows of * $table_obj that match logical condition $where. * * This method is called by the DB_Table::update() method if the table * has a parent DB_Table_Database object, and if ON UPDATE actions are * enabled in the database object. It is called indirectly by the * DB_Table_Database::delete() method, which is simply a wrapper for * the DB_Table method. * * @param object &$table_obj Reference to a DB_Table object * @param array $data Data to updated, column name keys, data values * @param string $where SQL logical condition for updated rows * @return boolean true on success (PEAR_Error on failure) * @access public */ function onUpdateAction(&$table_obj, $data, $where) { $table_name = $table_obj->table; if ($this->_act_on_update and isset($this->_ref_to[$table_name])) { $update_rows = null; foreach ($this->_ref_to[$table_name] as $ftable_name) { $ref = $this->_ref[$ftable_name][$table_name]; $action = isset($ref['on_update']) ? $ref['on_update'] : null; if (is_null($action)) { continue; } $rtable_obj =& $this->_table[$table_name]; $ftable_obj =& $this->_table[$ftable_name]; $fkey = $ref['fkey']; $rkey = $ref['rkey']; // Check if any column(s) of referenced $rkey are updated $rkey_updated = false; foreach ($data as $key => $value) { if (is_string($rkey)){ if ($key == $rkey) { $rkey_updated = true; break; } } else { if (in_array($key, $rkey)) { $rkey_updated = true; break; } } } // If $rkey is not updated, continue to next referencing table if (!$rkey_updated) { continue; } // Select rows to be updated, if not done previously if ($update_rows === null) { if ($this->backend == 'mdb2') { $fetchmode_assoc = MDB2_FETCHMODE_ASSOC; } else { $fetchmode_assoc = DB_FETCHMODE_ASSOC; } $sql = array('select' => '*', 'from' => $table_name, 'where' => $where, 'fetchmode' => $fetchmode_assoc); $update_rows = $this->select($sql); if (PEAR::isError($update_rows)) { return $update_rows; } } // Construct $fdata array if cascade, set null, or set default $fdata = null; if ($action == 'cascade') { if (is_string($rkey)) { if (array_key_exists($rkey, $data)) { $fdata = array($fkey => $data[$rkey]); } } else { $fdata = array(); for ($i=0; $i < count($rkey); $i++) { $rcol = $rkey[$i]; $fcol = $fkey[$i]; if (array_key_exists($rcol, $data)) { $fdata[$fcol] = $data[$rcol]; } } if (count($fdata) == 0) { $fdata = null; } } } elseif ($action == 'set null' or $action == 'set default') { if (is_string($fkey)) { if ($action == 'set default') { $value = isset($ftable_obj->col[$fkey]['default']) ? $ftable_obj->col[$fkey]['default'] : null; } else { $value = null; } $fdata = array($fkey => $value); } else { $fdata = array(); foreach ($fkey as $fcol) { if ($action == 'set default') { $value = isset($ftable_obj->col[$fcol]['default']) ? $ftable_obj->col[$fcol]['default'] : null; } else { $value = null; } $fdata[$fcol] = $value; } if (count($fdata) == 0) { $fdata = null; } } } elseif ($action == 'restrict') { $fdata = true; } elseif ($action == null) { $fdata = null; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_ON_UPDATE_ACTION, "$ftable_name => $table_name"); } if (!is_null($fdata)) { // Loop over rows to be updated from $table foreach ($update_rows as $update_row) { // If necessary, restore case of column names if ($this->_fix_case) { $cols = array_keys($table_obj->col); $update_row = $this->_replaceKeys($update_row, $cols); } // Construct filter for rows that reference $update_row $filter = $this->_buildFKeyFilter($update_row, $rkey, $fkey); // Apply action to foreign/referencing rows if ($action == 'restrict') { $sql = array('select'=>'*', 'from' => $ftable_name, 'where' => $filter); $frows = $this->select($sql); if (PEAR::isError($frows)) { return $frows; } if (count($frows) > 0) { return $this->throwError( DB_TABLE_DATABASE_ERR_RESTRICT_UPDATE, $table_name); } } else { // If 'cascade', 'set null', or 'set default', // then update the referencing foreign key. // Note: Turn off foreign key validity check // during update, then restore original value $check_fkey = $this->_check_fkey; $this->_check_fkey = false; $result = $this->update($ftable_name, $fdata, $filter); $this->_check_fkey = $check_fkey; if (PEAR::isError($result)) { return $result; } } } // foreach ($update_row) } // if (!is_null($fdata)) } // foreach loop over referencing tables } // end if // Normal completion return true; } // }}} // {{{ function autoRecast($flag = true) /** * Turns on (or off) automatic recasting of insert and update data * for all tables * * @param bool $flag True to automatically recast insert and update * data, in all tables, false to not do so. * @return void * @access public */ function autoRecast($flag = true) { foreach ($this->_table as $table_obj) { $table_obj->autoRecast($flag); } } // }}} // {{{ function autoInc($flag = true) /** * Turns on (or off) php implementation of auto-incrementing on insertion * for all tables * * @param bool $flag True to turn on auto-incrementing, false to turn off * @return void * @access public */ function autoInc($flag = true) { foreach ($this->_table as $table_obj) { $table_obj->auto_inc = $flag; } } // }}} // {{{ function delete($table_name, $where) /** * Deletes all row(s) of table that match a custom where clause. * * Wrapper for insert method of the corresponding DB_Table object. * * Implements any required ON DELETE action on tables that reference * deleted rows, if on delete actions are enabled. * * @param string $table_name name of table from which to delete * @param string $where SQL WHERE clause that limits the set * of records to delete * @return boolean true on success (PEAR_Error on failure) * @access public */ function delete($table_name, $where) { // Dereference table object if (isset($this->_table[$table_name])) { $table_obj =& $this->_table[$table_name]; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_TBL, "delete, $table_name"); } // Delete from $table_obj $result = $table_obj->delete($where); // Return value: true or PEAR_Error if (PEAR::isError($result)) { return $result; } else { return true; } } // }}} // {{{ function onDeleteAction(&$table_obj, $where) /** * Implements ON DELETE actions triggered by deletion of rows of * $table_obj that match logical condition $where. * * This method is called by the DB_Table::delete() method if the table * has a parent DB_Table_Database object, and if ON DELETE actions are * enabled in the database object. It is called indirectly by the * DB_Table_Database::delete() method, which is simply a wrapper for * the DB_Table method. * * @param object &$table_obj Reference to a DB_Table object * @param string $where SQL logical condition for deleted rows * @return boolean true on success (PEAR_Error on failure) * @access public */ function onDeleteAction(&$table_obj, $where) { $table_name = $table_obj->table; if ($this->_act_on_delete and isset($this->_ref_to[$table_name])) { $delete_rows = null; foreach ($this->_ref_to[$table_name] as $ftable_name) { $ref = $this->_ref[$ftable_name][$table_name]; $action = $ref['on_delete']; if (is_null($action)) { continue; } $ftable_obj =& $this->_table[$ftable_name]; $rtable_obj =& $this->_table[$table_name]; $fkey = $ref['fkey']; $rkey = $ref['rkey']; // Select rows to be deleted, if not done previously if ($delete_rows === null) { if ($this->backend == 'mdb2') { $fetchmode_assoc = MDB2_FETCHMODE_ASSOC; } else { $fetchmode_assoc = DB_FETCHMODE_ASSOC; } $sql = array('select' => '*', 'from' => $table_name, 'where' => $where, 'fetchmode' => $fetchmode_assoc); $delete_rows = $this->select($sql); if (PEAR::isError($delete_rows)) { return $delete_rows; } } // If set null or set default, construct update $fdata // $fdata contains data for updating referencing rows if ($action == 'set null' or $action == 'set default') { if (is_string($fkey)) { if ($action == 'set default') { $value = isset($ftable_obj->col[$fkey]['default']) ? $ftable_obj->col[$fkey]['default'] : null; } else { $value = null; } $fdata = array($fkey => $value); } else { $fdata = array(); foreach ($fkey as $fcol) { if ($action == 'set default') { $value = isset($ftable_obj->col[$fcol]['default']) ? $ftable_obj->col[$fcol]['default'] : null; } else { $value = null; } $fdata[$fcol] = $value; } } } // Loop over rows to be deleted from $table_name foreach ($delete_rows as $delete_row) { // If necessary, restore case of $delete_row column names if ($this->_fix_case) { $cols = array_keys($table_obj->col); $delete_row = $this->_replaceKeys($delete_row, $cols); } // Construct filter for referencing rows in $ftable_name $filter = $this->_buildFKeyFilter($delete_row, $rkey, $fkey); // Apply action for one deleted row if ($action == 'restrict') { // Select for referencing rows throw error if found $sql = array('select'=>'*', 'from' => $ftable_name, 'where' => $filter); $frows = $this->select($sql); if (PEAR::isError($frows)) { return $frows; } if (count($frows) > 0) { return $this->throwError( DB_TABLE_DATABASE_ERR_RESTRICT_DELETE, $table_name); } } elseif ($action == 'cascade') { // Delete referencing rows // Note: Recursion on delete $result = $this->delete($ftable_name, $filter); if (PEAR::isError($result)) { return $result; } } elseif ($action == 'set null' OR $action == 'set default') { // Update referencing rows, using $fdata // Note: Turn off foreign key validity check during // update of referencing key to null or default, then // restore $this->_check_fkey to original value $check_fkey = $this->_check_fkey; $this->_check_fkey = false; $result = $this->update($ftable_name, $fdata, $filter); $this->_check_fkey = $check_fkey; #$result = $ftable_obj->update($fdata, $filter); if (PEAR::isError($result)) { return $result; } } else { // Invalid $action name, throw Error return $this->throwError( DB_TABLE_DATABASE_ERR_ON_DELETE_ACTION, "$ftable_name => $table_name"); } } // end foreach ($delete_rows) } // end foreach ($this->_ref_to[...] as $ftable_name) } // end if // Normal completion return true; } // }}} // {{{ function _replaceKeys($data, $keys) /** * Returns array in which keys of associative array $data are replaced * by values of sequential array $keys. * * This function is used by the onDeleteAction() and onUpdateAction() * methods to restore the case of column names in associative arrays * that are returned from an automatically generated query "SELECT * * FROM $table WHERE ...", when these column name keys are returned * with a fixed case. In this usage, $keys is a sequential array of * the names of all columns in $table. * * @param array $data associative array * @param array $key numerical array of replacement key names * @return array associative array in which keys of $data have been * replaced by the values of array $keys. * @access private */ function _replaceKeys($data, $keys) { $new_data = array(); $i = 0; foreach ($data as $old_key => $value) { $new_key = $keys[$i]; $new_data[$new_key] = $value; $i = $i + 1; } return $new_data; } // }}} // {{{ function autoJoin($cols = null, $tables = null, $filter = null) /** * Builds a select command involving joined tables from * a list of column names and/or a list of table names. * * Returns an query array of the form used in $this->buildSQL, * constructed on the basis of a sequential array $cols of * column names and/or a sequential array $tables of table * names. The 'FROM' clause in the resulting SQL contains * all the table listed in the $tables parameter and all * those containing the columns listed in the $cols array, * as well as any linking tables required to establish * many to many relationships between these tables. The * 'WHERE' clause is constructed so as to create an inner * join of these tables. * * The $cols parameter is a sequential array in which the * values are column names. Column names may be qualified * by a table name, using the SQL table.column syntax, but * need not be qualified if they are unambiguous. The * values in $cols can only be column names, and may not * be functions or more complicated SQL expressions. If * cols is null, the resulting SQL command will start with * 'SELECT * FROM ...' . * * The $tables parameter is a sequential array in which the * values are table names. If $tables is null, the FROM * clause is constructed from the tables containing the * columns in the $cols. * * The $params array is an associative array can have * 'filter', and 'order' keys, which are both optional. * A value $params['filter'] is an condition string to * add (i.e., AND) to the automatically constructed set * of join conditions. A value $params['order'] is an * SQL 'ORDER BY' clause, with no 'ORDER BY' prefix. * * The function returns an associative array with keys * ('select', 'from', 'where', ['order']), for which the * associated values are strings containing the SELECT, * FROM, WHERE and (optionally) ORDER BY clauses of the * select statement. The entire SELECT command string * can be obtained by passing the resulting array to * the buildSQL method. * * @param array $cols sequential array of column names * @param array $tables sequential array of table names * @param array $filter SQL logical expression to be added * (ANDed) to the where clause * @return array sql query array for select statement * @access public */ function autoJoin($cols = null, $tables = null, $filter = null) { // initialize array containing clauses of select statement $query = array(); if (is_null($tables)) { if (is_null($cols)) { return $this->throwError( DB_TABLE_DATABASE_ERR_NO_COL_NO_TBL); } $tables = array(); } if (!$cols) { // If no columns specified, SELECT * FROM ... $query['select'] = '*'; } else { // Qualify unqualified columns with table names $all_tables = $tables; foreach ($cols as $key => $col) { $col_array = $this->validCol($col, $tables); if (PEAR::isError($col_array)) { return $col_array; } $table = $col_array[0]; $column = $col_array[1]; if (is_array($table)) { return $this->throwError( DB_TABLE_DATABASE_ERR_COL_NOT_UNIQUE, $col); } $cols[$key] = "$table.$column"; if (!in_array($table, $all_tables)) { $all_tables[] = $table; } } $tables = $all_tables; // Construct select clause $query['select'] = implode(', ', $cols); } // Construct array $joins of join conditions $n_tables = count($tables); if ($n_tables == 1) { $query['from'] = $tables[0]; } else { $join_tables = array($tables[0]); // list of joined tables $link_tables = array(); // list of required linking tables $joins = array(); // list of join conditions // Initialize linked list of unjoined tables $next = array(); for ( $i=1 ; $i < $n_tables-1 ; $i++) { $next[$tables[$i]] = $tables[$i+1]; $prev[$tables[$i+1]] = $tables[$i]; } $next[$tables[$n_tables-1]] = $tables[1]; $prev[$tables[1]] = $tables[$n_tables-1]; $n_remain = $n_tables - 1; $head = $tables[1]; $table1 = $tables[1]; $joined = false; $direct = true; while ($n_remain > 0) { if ($direct) { // Search for references from table1 to joined tables if (isset($this->_ref[$table1])) { $list = $this->_ref[$table1]; foreach ($list as $table2 => $def) { if (in_array($table2, $join_tables)) { if ($joined) { return $this->throwError( DB_TABLE_DATABASE_ERR_AMBIG_JOIN, $table1); } $fkey = $def['fkey']; $rkey = $def['rkey']; if (is_string($fkey)) { $joins[] = "$table1.$fkey = $table2.$rkey"; } else { for ($i=0; $i < count($fkey); $i++ ) { $fcol = $fkey[$i]; $rcol = $rkey[$i]; $joins[] = "$table1.$fcol = $table2.$rcol"; } } $joined = true; } } } // Search for references to table1 from joined tables if (isset($this->_ref_to[$table1])) { $list = $this->_ref_to[$table1]; foreach ($list as $table2) { if (in_array($table2, $join_tables)) { if ($joined) { return $this->throwError( DB_TABLE_DATABASE_ERR_AMBIG_JOIN, $table1); } $def = $this->_ref[$table2][$table1]; $fkey = $def['fkey']; $rkey = $def['rkey']; if (is_string($fkey)) { $joins[] = "$table2.$fkey = $table1.$rkey"; } else { for ($i=0; $i < count($fkey); $i++ ) { $fcol = $fkey[$i]; $rcol = $rkey[$i]; $joins[] = "$table2.$fcol = $table1.$rcol"; } } $joined = true; } } } } else { // Search for indirect linking table to table1 if (isset($this->_link[$table1])) { foreach ($this->_link[$table1] as $table2 => $links) { if (in_array($table2, $join_tables)) { $n_link = count($links); if ($n_link > 1) { return $this->throwError( DB_TABLE_DATABASE_ERR_AMBIG_JOIN, $table1); } if ($joined and $n_link > 0) { return $this->throwError( DB_TABLE_DATABASE_ERR_AMBIG_JOIN, $table1); } $link = $links[0]; $def1 = $this->_ref[$link][$table1]; $fkey1 = $def1['fkey']; $rkey1 = $def1['rkey']; if (is_string($fkey1)) { $joins[] = "$link.$fkey1 = $table1.$rkey1"; } else { for ($i=0; $i < count($fkey1); $i++ ) { $fcol1 = $fkey1[$i]; $rcol1 = $rkey1[$i]; $joins[] = "$link.$fcol1 = $table1.$rcol1"; } } $def2 = $this->_ref[$link][$table2]; $fkey2 = $def2['fkey']; $rkey2 = $def2['rkey']; if (is_string($fkey2)) { $joins[] = "$link.$fkey2 = $table2.$rkey2"; } else { for ($i=0; $i < count($fkey2); $i++ ) { $fcol2 = $fkey2[$i]; $rcol2 = $rkey2[$i]; $joins[] = "$link.$fcol2 = $table2.$rcol2"; } } $link_tables[] = $link; $joined = true; } } } } if ($joined) { $join_tables[] = $table1; $n_remain = $n_remain - 1; if ($n_remain > 0) { $head = $next[$table1]; $tail = $prev[$table1]; $prev[$head] = $tail; $next[$tail] = $head; $table1 = $head; $joined = false; $direct = true; } } else { $table1 = $next[$table1]; if ($table1 == $head) { if ($direct) { $direct = false; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_FAIL_JOIN,$table1); } } } } // Add any required linking tables to $tables array if ($link_tables) { foreach ($link_tables as $link) { if (!in_array($link, $tables)) { $tables[] = $link; } } } // Construct from clause from $tables array $query['from'] = implode(', ', $tables); // Construct where clause from $joins array $query['where'] = implode("\n AND ", $joins); } // Add $filter condition, if present if ($filter) { if (isset($query['where'])) { $query['where'] = '( ' . $query['where'] . " )\n" . ' AND ( ' . $filter . ')'; } else { $query['where'] = $filter; } } return $query; } // }}} // {{{ function _buildFKeyFilter($data, $data_key = null, $filt_key = null, $match = 'simple') /** * Returns WHERE clause equating values of $data array to database column * values * * Usage: The function is designed to return an SQL logical * expression that equates the values of a set of foreign key columns in * associative array $data, which is a row to be inserted or updated in * one table, to the values of the corresponding columns of a referenced * table. In this usage, $data_key is the foreign key (a column name or * numerical array of column names), and $filt_key is the corresponding * referenced key. * * Parameters: Parameter $data is an associative array containing data to * be inserted into or used to update one row of a database table, in which * array keys are column names. When present, $data_key contains either * the name of a single array key of interest, or a numerical array of such * keys. These are usually the names of the columns of a foreign key in * that table. When, $data_key is null or absent, it is taken to be equal * to an array containing all of the keys of $data. When present, $filt_key * contains either a string or a numerical array of strings that are * aliases for the keys in $data_key. These are usually the names of the * corresponding columns in the referenced table. When $filt_key is null * or absent, it is equated with $data_key internally. The function * returns an SQL logical expression that equates the values in $data * whose keys are specified by $data_key, to the values of database * columns whose names are specified in $filt_key. * * General case: _buildFKeyFilter returns a SQL logical expression that * equates the values of $data whose keys are given in $data_key with the * values of database columns with names given in $filt_key. For example, * if * * $data = array( 'k1' => $v1, 'k2' => $v2, ... , 'k10' => $v10 ); * $data_key = array('k2', 'k5', 'k7'); * $filt_key = array('c2', 'c5', 'c7'); * * then buildFilter($data, $data_key, $filt_key) returns a string * * "c2 = $v2 AND c5 = $v5 AND c7 = $v7" * * in which the values $v2, $v5, $v7 are replaced by properly quoted * SQL literal values. If, in the above example, $data_key = 'k5' * and $filt_key = 'c5', then the function will return * * "c5 = $v5" * * where (again) $v5 is replaced by an SQL literal. * * Simple case: If parameters $data_key and $filt_key are null, the * behavior is the same as that of the DB_Table_Base::buildFilter() method. * For example, if * * $data = array( 'c1' => $v1, 'c2' => $v2, 'c3' => $v3) * * then _buildFKeyFilter($data) returns a string * * "c1 => $val1 AND c2 => $val2 AND c3 = $v3" * * in which the values $v1, $v2, $v3 are replaced by SQL literal values, * quoted and escaped as appropriate for each data type and the backend. * * Quoting is done by the DB_Table_Database::quote() method, based on * the php type of the values in $array. The treatment of null values * in $data depends upon the value of the $match parameter. * * Null values: The treatment to null values in $data depends upon * the value of the $match parameter . If $match == 'simple', an empty * string is returned if any $value of $data with a key in $data_key * is null. If $match == 'partial', the returned SQL expression * equates only the relevant non-null values of $data to the values of * corresponding database columns. If $match == 'full', the function * returns an empty string if all of the relevant values of data are * null, and returns a PEAR_Error if some of the selected values are * null and others are not null. * * @param array $data associative array, keys are column names * @param mixed $data_key string or numerical array of strings, in which * values are a set of keys of interest in $data * @param mixed $data_key string or numerical array of strings, in which * values are names of a corresponding set of * database column names. * @return string SQL expression equating values in $data, for which keys * also appear in $data_key, to values of corresponding * database columns named in $filt_key. * @access private */ function _buildFKeyFilter($data, $data_key = null, $filt_key = null, $match = 'simple') { // Check $match type value if (!in_array($match, array('simple', 'partial', 'full'))) { return $this->throwError( DB_TABLE_DATABASE_ERR_MATCH_TYPE); } // Simple case: Build filter from $data array alone if (is_null($data_key) && is_null($filt_key)) { return $this->buildFilter($data, $match); } // Defaults for $data_key and $filt_key: if (is_null($data_key)) { $data_key = array_keys($data); } if (is_null($filt_key)) { $filt_key = $data_key; } // General case: $data_key and/or $filt_key not null if (is_string($data_key)) { if (!is_string($filt_key)) { return $this->throwError( DB_TABLE_DATABASE_ERR_FILT_KEY); } if (array_key_exists($data_key, $data)) { $value = $data[$data_key]; if (!is_null($value)) { $value = (string) $this->quote($data[$data_key]); return "$filt_key = $value"; } else { return ''; } } else { return ''; } } elseif (is_array($data_key)) { if (!is_array($filt_key)) { return $this->throwError( DB_TABLE_DATABASE_ERR_FILT_KEY); } $filter = array(); for ($i=0; $i < count($data_key); $i++) { $data_col = $data_key[$i]; $filt_col = $filt_key[$i]; if (array_key_exists($data_col, $data)) { $value = $data[$data_col]; if (!is_null($value)) { if ($match == 'full' && isset($found_null)) { return $this->throwError( DB_TABLE_DATABASE_ERR_FULL_KEY); } $value = $this->quote($value); $filter[] = "$filt_col = $value"; } else { $found_null = true; } } } if ($match == 'simple' && isset($found_null)) { return ''; } if (count($filter) == 0) { return ''; } return implode(' AND ', $filter); } else { // Invalid data key return $this->throwError( DB_TABLE_DATABASE_ERR_DATA_KEY); } } // }}} // {{{ function quote($value) /** * Returns SQL literal string representation of a php value * * Calls MDB2::quote() or DB_Common::quoteSmart() to enquote and * escape string values. If $value is: * - a string, return the string enquoted and escaped * - a number, return cast of number to string, without quotes * - a boolean, return '1' for true and '0' for false * - null, return the string 'NULL' * * @param mixed $value * @return string Representation of value as an SQL literal * * @access public * * @see DB_Common::quoteSmart() * @see MDB2::quote() */ function quote($value) { if (is_bool($value)) { return $value ? '1' : '0'; } if ($this->backend == 'mdb2') { $value = $this->db->quote($value); } else { $value = $this->db->quoteSmart($value); } return (string) $value; } // }}} // {{{ function __sleep() /** * Serializes all table references and sets $db = null, $backend = null * * @return array names of all properties * @access public */ function __sleep() { $this->db = null; $this->backend = null; // needed in setDatabaseInstance, where null is passed by reference $null_variable = null; foreach ($this->_table as $name => $table_obj) { $table_obj->db = null; $table_obj->setDatabaseInstance($null_variable); $this->_table[$name] = serialize($table_obj); } return array_keys(get_object_vars($this)); } // }}} // {{{ function __wakeup() /** * Unserializes DB_Table_Database object and all child DB_Table objects * * Immediately after unserialization, a DB_Table_Database object * has null $db and $backend properties, as do all of its child * DB_Table objects. The DB_Table_Database::setDB method should * be called immediately after unserialization to re-establish * the database connection, like so: * * $db_object = unserialize($serialized_db); * $db_object->setDB($conn); * * where $conn is a DB/MDB2 object. This establishes a DB/MDB2 * connection for both the parent database and all child tables. * * This method unserializes all of the child DB_Table objects of * a DB_Table_Database object. It must thus have access to the * definitions of the associated DB_Table subclasses. These are * listed in the $_table_subclass property. If a required subclass * named $subclass is not defined, the __wakeup() method attempts * to autoload a file "$subclass.php" in the directory specified * by $this->table_subclass_path. * * @return void * @access public */ function __wakeup() { foreach ($this->_table as $name => $table_string) { // Check for subclass definition, and autoload if necessary. $subclass = $this->_table_subclass[$name]; if (!is_null($subclass)) { if (!class_exists($subclass)) { $dir = $this->_table_subclass_path; require_once $dir . '/' . $subclass . '.php'; } } // Unserialize table $table_obj = unserialize($table_string); // Reset references between database and table objects $table_obj->setDatabaseInstance($this); $this->_table[$name] = $table_obj; } } // }}} // {{{ function toXML() /** * Returns XML string representation of database declaration * * @param string $indent string of whitespace, prefix to each line * @return string XML string representation * @access public */ function toXML($indent = '') { require_once 'DB/Table/XML.php'; $s = array(); $s[] = DB_Table_XML::openTag('database', $indent); foreach ($this->_table as $name => $table_obj) { $s[] = $table_obj->toXML($indent); } $s[] = DB_Table_XML::closeTag('database', $indent); return implode("\n", $s); } // }}} // {{{ function fromXML($xml_string, $conn) /** * Returns a DB_Table_Database object constructed from an XML string * * Uses the MDB2 XML schema for a database element, including a new * syntax for foreign key indices. * * NOTE: This function requires PHP 5. It throws an error if used * with PHP 4. * * @param string XML string representation * @return object DB_Table_Database object on success (PEAR_Error on failure) * * @throws PEAR_Error if: * - PHP version is not >= 5.0.0 (...DATABASE_ERR_PHP_VERSION ) * - Parsing by simpleXML fails (...DATABASE_ERR_XML_PARSE ) * * @access public */ function fromXML($xml_string, $conn) { // Check PHP version. Throw error if not >= PHP 5.0.0 $version = phpversion(); if (version_compare($version, '5.0.0', "<")) { return $this->throwError( DB_TABLE_DATABASE_ERR_PHP_VERSION, $version); } $xml = simplexml_load_string($xml_string); if ($xml == false) { return $this->throwError( DB_TABLE_DATABASE_ERR_XML_PARSE); } // Instantiate database object $database_name = (string) $xml->name; $database_obj = new DB_Table_Database($conn, $database_name); // Create array of foreign key references $ref = array(); // Loop over tables foreach ($xml->table as $table) { $table_name = (string) $table->name; // Instantiate table object $table_obj = new DB_Table($conn, $table_name); // Add columns to table object $declaration = $table->declaration; foreach ($declaration->field as $field) { $col_name = (string) $field->name; $type = (string) $field->type; $def = array('type' => $type); if (isset($field->length)) { $def['size'] = (integer) $field->length; } if (isset($field->notnull)) { if ($field->notnull) { $def['require'] = true; } else { $def['require'] = false; } } if (isset($field->default)) { $def['default'] = $field->default; } if (isset($field->autoincrement)) { if (is_null($table_obj->auto_inc_col)) { $table_obj->auto_inc_col = $col_name; } else { return $this->throwError( DB_TABLE_DATABASE_ERR_XML_MULT_AUTO_INC); } } $table_obj->col[$col_name] = $def; } // Add indices foreach ($declaration->index as $index) { if (isset($index->name)) { $name = (string) $index->name; } else { $name = null; } $def = array(); if (isset($index->primary)) { $def['type'] = 'primary'; } elseif (isset($index->unique)) { $def['type'] = 'unique'; } else { $def['type'] = 'normal'; } foreach ($index->field as $field) { $def['cols'][] = (string) $field; } if ($name) { $table_obj->idx[$name] = $def; } else { $table_obj->idx[] = $def; } } // Add table object to database object $database_obj->addTable($table_obj); // Foreign key references foreach ($declaration->foreign as $foreign) { if (isset($foreign->name)) { $name = (string) $foreign->name; } else { $name = null; } $fkey = array(); foreach ($foreign->field as $field) { $fkey[] = (string) $field; } if (count($fkey) == 1) { $fkey = $fkey[0]; } $rtable = (string) $foreign->references->table; if (isset($foreign->references->field)) { $rkey = array(); foreach ($foreign->references->field as $field) { $rkey[] = (string) $field; } if (count($rkey)==1) { $rkey = $rkey[0]; } } else { $rkey = null; } if (isset($foreign->ondelete)) { $on_delete = (string) $foreign->ondelete; } else { $on_delete = null; } if (isset($foreign->onupdate)) { $on_update = (string) $foreign->onupdate; } else { $on_update = null; } // Add reference definition to $ref array $def = array(); $def['fkey'] = $fkey; $def['rkey'] = $rkey; $def['on_delete'] = $on_delete; $def['on_update'] = $on_update; if (!isset($ref[$table_name])) { $ref[$table_name] = array(); } $ref[$table_name][$rtable] = $def; } // Release variable $table_obj to refer to another table unset($table_obj); } // Add all references to database object foreach ($ref as $ftable => $list) { foreach ($list as $rtable => $def) { $fkey = $def['fkey']; $rkey = $def['rkey']; $on_delete = $def['on_delete']; $on_update = $def['on_update']; $database_obj->addRef($ftable, $fkey, $rtable, $rkey, $on_delete, $on_update); } } return $database_obj; } // }}} // }}} } // }}} /* Local variables: * tab-width: 4 * c-basic-offset: 4 * c-hanging-comment-ender-p: nil * End: */ ?>