* 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 Paul M. Jones * @author David C. Morse * @author Mark Wiesemann * @license http://opensource.org/licenses/bsd-license.php New BSD License * @version CVS: $Id: Base.php,v 1.4 2007/12/13 16:52:14 wiesemann Exp $ * @link http://pear.php.net/package/DB_Table */ require_once 'PEAR.php'; // {{{ DB_Table_Base /** * Base class for DB_Table and DB_Table_Database * * @category Database * @package DB_Table * @author Paul M. Jones * @author David C. Morse * @author Mark Wiesemann * @version Release: 1.5.6 * @link http://pear.php.net/package/DB_Table */ class DB_Table_Base { // {{{ properties /** * The PEAR DB/MDB2 object that connects to the database. * * @var object * @access public */ var $db = null; /** * The backend type, which must be 'db' or 'mdb2' * * @var string * @access public */ var $backend = null; /** * If there is an error on instantiation, this captures that error. * * This property is used only for errors encountered in the constructor * at instantiation time. To check if there was an instantiation error... * * * $obj = new DB_Table_*(); * if ($obj->error) { * // ... error handling code here ... * } * * * @var object PEAR_Error * @access public */ var $error = null; /** * Baseline SELECT maps for buildSQL() and select*() methods. * * @var array * @access public */ var $sql = array(); /** * Format of rows in sets returned by the select() method * * This should be one of the DB/MDB2_FETCHMODE_* constant values, such as * MDB2_FETCHMODE_ASSOC, MDB2_FETCHMODE_ORDERED, or MDB2_FETCHMODE_OBJECT. * It determines whether select() returns represents individual rows as * associative arrays with column name keys, ordered/sequential arrays, * or objects with column names mapped to properties. Use corresponding * DB_FETCHMODE_* constants for use with the DB backend. It has no effect * upon the return value of selectResult(). * * If a 'fetchmode' element is set for a specific query array, the query * fetchmode will override this DB_Table or DB_Table_Database property. * If no value is set for the query or the DB_Table_Base object, the value * or default set in the underlying DB/MDB2 object will be used. * * @var int * @access public */ var $fetchmode = null; /** * Class of objects to use for rows returned as objects by select() * * When fetchmode is DB/MDB2_FETCHMODE_OBJECT, use this class for each * returned row in rsults of select(). May be overridden by value of * 'fetchmode_object_class'. If no class name is set in the query or * the DB_Table_Base, defaults to that set in the DB/MDB2 object, or * to default of StdObject. * * @var string * @access public */ var $fetchmode_object_class = null; /** * Upper case name of primary subclass, 'DB_TABLE' or 'DB_TABLE_DATABASE' * * This should be set in the constructor of the child class, and is * used in the DB_Table_Base::throwError() method to determine the * location of the relevant error codes and messages. Error codes and * error code messages are defined in class $this->_primary_subclass. * Messages are stored in $GLOBALS['_' . $this->_primary_subclass]['error'] * * @var string * @access private */ var $_primary_subclass = null; // }}} // {{{ Methods /** * Specialized version of throwError() modeled on PEAR_Error. * * Throws a PEAR_Error with an error message based on an error code * and corresponding error message defined in $this->_primary_subclass * * @param string $code An error code constant * @param string $extra Extra text for the error (in addition to the * regular error message). * @return object PEAR_Error * @access public * @static */ function &throwError($code, $extra = null) { // get the error message text based on the error code $index = '_' . $this->_primary_subclass; $text = $this->_primary_subclass . " Error - \n" . $GLOBALS[$index]['error'][$code]; // add any additional error text if ($extra) { $text .= ' ' . $extra; } // done! $error = PEAR::throwError($text, $code); return $error; } /** * Overwrites one or more error messages, e.g., to internationalize them. * * May be used to change messages stored in global array $GLOBALS[$class_key] * @param mixed $code If string, the error message with code $code will be * overwritten by $message. If array, each key is a code * and each value is a new message. * * @param string $message Only used if $key is not an array. * @return void * @access public */ function setErrorMessage($code, $message = null) { $index = '_' . $this->_primary_subclass; if (is_array($code)) { foreach ($code as $single_code => $single_message) { $GLOBALS[$index]['error'][$single_code] = $single_message; } } else { $GLOBALS[$index]['error'][$code] = $message; } } /** * Returns SQL SELECT string constructed from sql query array * * @param mixed $query SELECT query array, or key string of $this->sql * @param string $filter SQL snippet to AND with default WHERE clause * @param string $order SQL snippet to override default ORDER BY clause * @param int $start The row number from which to start result set * @param int $count The number of rows to list in the result set. * * @return string SQL SELECT command string (or PEAR_Error on failure) * * @access public */ function buildSQL($query, $filter = null, $order = null, $start = null, $count = null) { // Is $query a query array or a key of $this->sql ? if (!is_array($query)) { if (is_string($query)) { if (isset($this->sql[$query])) { $query = $this->sql[$query]; } else { return $this->throwError( constant($this->_primary_subclass . '_ERR_SQL_UNDEF'), $query); } } else { return $this->throwError( constant($this->_primary_subclass . '_ERR_SQL_NOT_STRING')); } } // Construct SQL command from parts $s = array(); if (isset($query['select'])) { $s[] = 'SELECT ' . $query['select']; } else { $s[] = 'SELECT *'; } if (isset($query['from'])) { $s[] = 'FROM ' . $query['from']; } elseif ($this->_primary_subclass == 'DB_TABLE') { $s[] = 'FROM ' . $this->table; } if (isset($query['join'])) { $s[] = $query['join']; } if (isset($query['where'])) { if ($filter) { $s[] = 'WHERE ( ' . $query['where'] . ' )'; $s[] = ' AND ( '. $filter . ' )'; } else { $s[] = 'WHERE ' . $query['where']; } } elseif ($filter) { $s[] = 'WHERE ' . $filter; } if (isset($query['group'])) { $s[] = 'GROUP BY ' . $query['group']; } if (isset($query['having'])) { $s[] = 'HAVING '. $query['having']; } // If $order parameter is set, override 'order' element if (!is_null($order)) { $s[] = 'ORDER BY '. $order; } elseif (isset($query['order'])) { $s[] = 'ORDER BY ' . $query['order']; } $cmd = implode("\n", $s); // add LIMIT if requested if (!is_null($start) && !is_null($count)) { $db =& $this->db; if ($this->backend == 'mdb2') { $db->setLimit($count, $start); } else { $cmd = $db->modifyLimitQuery( $cmd, $start, $count); } } // Return command string return $cmd; } /** * Selects rows using one of the DB/MDB2 get*() methods. * * @param string $query SQL SELECT query array, or a key of the * $this->sql property array. * @param string $filter SQL snippet to AND with default WHERE clause * @param string $order SQL snippet to override default ORDER BY clause * @param int $start The row number from which to start result set * @param int $count The number of rows to list in the result set. * @param array $params Parameters for placeholder substitutions, if any * @return mixed An array of records from the table if anything but * ('getOne'), a single value (if 'getOne'), or a PEAR_Error * @see DB::getAll() * @see MDB2::getAll() * @see DB::getAssoc() * @see MDB2::getAssoc() * @see DB::getCol() * @see MDB2::getCol() * @see DB::getOne() * @see MDB2::getOne() * @see DB::getRow() * @see MDB2::getRow() * @see DB_Table_Base::_swapModes() * @access public */ function select($query, $filter = null, $order = null, $start = null, $count = null, $params = array()) { // Is $query a query array or a key of $this->sql ? // On output from this block, $query is an array if (!is_array($query)) { if (is_string($query)) { if (isset($this->sql[$query])) { $query = $this->sql[$query]; } else { return $this->throwError( constant($this->_primary_subclass . '_ERR_SQL_UNDEF'), $query); } } else { return $this->throwError( constant($this->_primary_subclass . '_ERR_SQL_NOT_STRING')); } } // build the base command $sql = $this->buildSQL($query, $filter, $order, $start, $count); if (PEAR::isError($sql)) { return $sql; } // set the get*() method name if (isset($query['get'])) { $method = ucwords(strtolower(trim($query['get']))); $method = "get$method"; } else { $method = 'getAll'; } // DB_Table assumes you are using a shared PEAR DB/MDB2 object. // Record fetchmode settings, to be restored before returning. $db =& $this->db; $restore_mode = $db->fetchmode; if ($this->backend == 'mdb2') { $restore_class = $db->getOption('fetch_class'); } else { $restore_class = $db->fetchmode_object_class; } // swap modes $fetchmode = $this->fetchmode; $fetchmode_object_class = $this->fetchmode_object_class; if (isset($query['fetchmode'])) { $fetchmode = $query['fetchmode']; } if (isset($query['fetchmode_object_class'])) { $fetchmode_object_class = $query['fetchmode_object_class']; } $this->_swapModes($fetchmode, $fetchmode_object_class); // make sure params is an array if (!is_null($params)) { $params = (array) $params; } // get the result if ($this->backend == 'mdb2') { $result = $db->extended->$method($sql, null, $params); } else { switch ($method) { case 'getCol': $result = $db->$method($sql, 0, $params); break; case 'getAssoc': $result = $db->$method($sql, false, $params); break; default: $result = $db->$method($sql, $params); break; } } // restore old fetch_mode and fetch_object_class back $this->_swapModes($restore_mode, $restore_class); return $result; } /** * Selects rows as a DB_Result/MDB2_Result_* object. * * @param string $query The name of the SQL SELECT to use from the * $this->sql property array. * @param string $filter SQL snippet to AND to the default WHERE clause * @param string $order SQL snippet to override default ORDER BY clause * @param int $start The record number from which to start result set * @param int $count The number of records to list in result set. * @param array $params Parameters for placeholder substitutions, if any. * @return object DB_Result/MDB2_Result_* object on success * (PEAR_Error on failure) * @see DB_Table::_swapModes() * @access public */ function selectResult($query, $filter = null, $order = null, $start = null, $count = null, $params = array()) { // Is $query a query array or a key of $this->sql ? // On output from this block, $query is an array if (!is_array($query)) { if (is_string($query)) { if (isset($this->sql[$query])) { $query = $this->sql[$query]; } else { return $this->throwError( constant($this->_primary_subclass . '_ERR_SQL_UNDEF'), $query); } } else { return $this->throwError( constant($this->_primary_subclass . '_ERR_SQL_NOT_STRING')); } } // build the base command $sql = $this->buildSQL($query, $filter, $order, $start, $count); if (PEAR::isError($sql)) { return $sql; } // DB_Table assumes you are using a shared PEAR DB/MDB2 object. // Record fetchmode settings, to be restored afterwards. $db =& $this->db; $restore_mode = $db->fetchmode; if ($this->backend == 'mdb2') { $restore_class = $db->getOption('fetch_class'); } else { $restore_class = $db->fetchmode_object_class; } // swap modes $fetchmode = $this->fetchmode; $fetchmode_object_class = $this->fetchmode_object_class; if (isset($query['fetchmode'])) { $fetchmode = $query['fetchmode']; } if (isset($query['fetchmode_object_class'])) { $fetchmode_object_class = $query['fetchmode_object_class']; } $this->_swapModes($fetchmode, $fetchmode_object_class); // make sure params is an array if (!is_null($params)) { $params = (array) $params; } // get the result if ($this->backend == 'mdb2') { $stmt =& $db->prepare($sql); if (PEAR::isError($stmt)) { return $stmt; } $result =& $stmt->execute($params); } else { $result =& $db->query($sql, $params); } // swap modes back $this->_swapModes($restore_mode, $restore_class); // return the result return $result; } /** * Counts the number of rows which will be returned by a query. * * This function works identically to {@link select()}, but it * returns the number of rows returned by a query instead of the * query results themselves. * * @author Ian Eure * @param string $query The name of the SQL SELECT to use from the * $this->sql property array. * @param string $filter Ad-hoc SQL snippet to AND with the default * SELECT WHERE clause. * @param string $order Ad-hoc SQL snippet to override the default * SELECT ORDER BY clause. * @param int $start Row number from which to start listing in result * @param int $count Number of rows to list in result set * @param array $params Parameters to use in placeholder substitutions * (if any). * @return int Number of records from the table (or PEAR_Error on failure) * * @see DB_Table::select() * @access public */ function selectCount($query, $filter = null, $order = null, $start = null, $count = null, $params = array()) { // Is $query a query array or a key of $this->sql ? if (is_array($query)) { $sql_key = null; $count_query = $query; } else { if (is_string($query)) { if (isset($this->sql[$query])) { $sql_key = $query; $count_query = $this->sql[$query]; } else { return $this->throwError( constant($this->_primary_subclass . '_ERR_SQL_UNDEF'), $query); } } else { return $this->throwError( constant($this->_primary_subclass . '_ERR_SQL_NOT_STRING')); } } // Use Table name as default 'from' if child class is DB_TABLE if ($this->_primary_subclass == 'DB_TABLE') { if (!isset($query['from'])) { $count_query['from'] = $this->table; } } // If the query is a stored query in $this->sql, then create a corresponding // key for the count query, or check if the count-query already exists $ready = false; if ($sql_key) { // Create an sql key name for this count-query $count_key = '__count_' . $sql_key; // Check if a this count query alread exists in $this->sql if (isset($this->sql[$count_key])) { $ready = true; } } // If a count-query does not already exist, create $count_query array if ($ready) { $count_query = $this->sql[$count_key]; } else { // Is a count-field set for the query? if (!isset($count_query['count']) || trim($count_query['count']) == '') { $count_query['count'] = '*'; } // Replace the SELECT fields with a COUNT() command $count_query['select'] = "COUNT({$count_query['count']})"; // Replace the 'get' key so we only get one result item $count_query['get'] = 'one'; // Create a new count-query in $this->sql if ($sql_key) { $this->sql[$count_key] = $count_query; } } // Retrieve the count results return $this->select($count_query, $filter, $order, $start, $count, $params); } /** * Changes the $this->db PEAR DB/MDB2 object fetchmode and * fetchmode_object_class. * * @param string $new_mode A DB/MDB2_FETCHMODE_* constant. If null, * defaults to whatever the DB/MDB2 object is currently using. * * @param string $new_class The object class to use for results when * the $db object is in DB/MDB2_FETCHMODE_OBJECT fetch mode. If null, * defaults to whatever the the DB/MDB2 object is currently using. * * @return void * @access private */ function _swapModes($new_mode, $new_class) { // get the old (current) mode and class $db =& $this->db; $old_mode = $db->fetchmode; if ($this->backend == 'mdb2') { $old_class = $db->getOption('fetch_class'); } else { $old_class = $db->fetchmode_object_class; } // don't need to swap anything if the new modes are both // null or if the old and new modes already match. if ((is_null($new_mode) && is_null($new_class)) || ($old_mode == $new_mode && $old_class == $new_class)) { return; } // set the default new mode if (is_null($new_mode)) { $new_mode = $old_mode; } // set the default new class if (is_null($new_class)) { $new_class = $old_class; } // swap modes $db->setFetchMode($new_mode, $new_class); } /** * Returns SQL condition equating columns to literal values. * * The parameter $data is an associative array in which keys are * column names and values are corresponding values. The method * returns an SQL string that is true if the value of every * specified database columns is equal to the corresponding * value in $data. * * For example, if: * * $data = array( 'c1' => 'thing', 'c2' => 23, 'c3' => 0.32 ) * * then buildFilter($data) returns a string * * c1 => 'thing' AND c2 => 23 AND c3 = 0.32 * * in which string values are replaced by SQL literal values, * quoted and escaped as necessary. * * Values are quoted and escaped as appropriate for each data * type and the backend RDBMS, using the MDB2::quote() or * DB::smartQuote() method. The behavior depends on the PHP type * of the value: string values are quoted and escaped, while * integer and float numerical values are not. Boolean values * in $data are represented as 0 or 1, consistent with the way * booleans are stored by DB_Table. * * Null values: The treatment of 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 * @return string SQL expression equating values in $data to * values of columns named by keys. * @access public */ function buildFilter($data, $match = 'simple') { // Check $match type value if (!in_array($match, array('simple', 'partial', 'full'))) { return $this->throwError( DB_TABLE_DATABASE_ERR_MATCH_TYPE); } if (count($data) == 0) { return ''; } $filter = array(); foreach ($data as $key => $value) { if (!is_null($value)) { if ($match == 'full' && isset($found_null)) { return $this->throwError( DB_TABLE_DATABASE_ERR_FULL_KEY); } if (is_bool($value)) { $value = $value ? '1' : '0'; } else { if ($this->backend == 'mdb2') { $value = $this->db->quote($value); } else { $value = $this->db->quoteSmart($value); } } $filter[] = "$key = $value"; } else { if ($match == 'simple') { return ''; // if any value in $data is null } elseif ($match == 'full') { $found_null = true; } } } return implode(' AND ', $filter); } // }}} } // }}} /* Local variables: * tab-width: 4 * c-basic-offset: 4 * c-hanging-comment-ender-p: nil * End: */ ?>