'', 'user' => '', 'pass' => '', 'db' => ''); /** * Index of database server * * @var int * @access protected */ protected $serverIndex = 0; /** * Handle of currently processed recordset * * @var resource * @access protected */ protected $queryID = null; /** * DB type specific function mappings * * @var Array * @access protected */ protected $metaFunctions = Array (); /** * Function to handle sql errors * * @var Array|string * @access public */ public $errorHandler = ''; /** * Error code * * @var int * @access protected */ protected $errorCode = 0; /** * Error message * * @var string * @access protected */ protected $errorMessage = ''; /** * Defines if database connection * operations should generate debug * information * * @var bool * @access public */ public $debugMode = false; /** * Save query execution statistics * * @var bool * @access protected */ protected $_captureStatistics = false; /** * Last query to database * * @var string * @access public */ public $lastQuery = ''; /** * Total processed queries count * * @var int * @access protected */ protected $_queryCount = 0; /** * Total time, used for serving queries * * @var Array * @access protected */ protected $_queryTime = 0; /** * Indicates, that next database query could be cached, when memory caching is enabled * * @var bool * @access public */ public $nextQueryCachable = false; /** * For backwards compatibility with kDBLoadBalancer class * * @var bool * @access public */ public $nextQueryFromMaster = false; /** * Initializes connection class with * db type to used in future * * @param string $dbType * @param string $errorHandler * @param int $server_index * @access public */ public function __construct($dbType, $errorHandler = '', $server_index = 0) { if ( class_exists('kApplication') ) { // prevents "Fatal Error" on 2nd installation step (when database is empty) parent::__construct(); } $this->dbType = $dbType; $this->serverIndex = $server_index; // $this->initMetaFunctions(); if (!$errorHandler) { $this->errorHandler = Array(&$this, 'handleError'); } else { $this->errorHandler = $errorHandler; } $this->_captureStatistics = defined('DBG_CAPTURE_STATISTICS') && DBG_CAPTURE_STATISTICS && !(defined('ADMIN') && ADMIN); } /** * Set's custom error * * @param int $code * @param string $msg * @access protected */ protected function setError($code, $msg) { $this->errorCode = $code; $this->errorMessage = $msg; } /** * Checks if previous query execution * raised an error. * * @return bool * @access public */ public function hasError() { return $this->errorCode != 0; } /** * Caches function specific to requested * db type * * @access protected */ protected function initMetaFunctions() { $ret = Array (); switch ( $this->dbType ) { case 'mysql': $ret = Array (); // only define functions, that name differs from "dbType_" break; } $this->metaFunctions = $ret; } /** * Gets function for specific db type * based on it's meta name * * @param string $name * @return string * @access protected */ protected function getMetaFunction($name) { /*if ( !isset($this->metaFunctions[$name]) ) { $this->metaFunctions[$name] = $name; }*/ return $this->dbType . '_' . $name; } /** * Try to connect to database server * using specified parameters and set * database to $db if connection made * * @param string $host * @param string $user * @param string $pass * @param string $db * @param bool $force_new * @param bool $retry * @return bool * @access public */ public function Connect($host, $user, $pass, $db, $force_new = false, $retry = false) { $this->connectionParams = Array ('host' => $host, 'user' => $user, 'pass' => $pass, 'db' => $db); $func = $this->getMetaFunction('connect'); $this->connectionID = $func($host, $user, $pass, $force_new); if ($this->connectionID) { if (defined('DBG_SQL_MODE')) { $this->Query('SET sql_mode = \''.DBG_SQL_MODE.'\''); } if (defined('SQL_COLLATION') && defined('SQL_CHARSET')) { $this->Query('SET NAMES \''.SQL_CHARSET.'\' COLLATE \''.SQL_COLLATION.'\''); } $this->setError(0, ''); // reset error $this->setDB($db); } // process error (fatal in most cases) $func = $this->getMetaFunction('errno'); $this->errorCode = $this->connectionID ? $func($this->connectionID) : $func(); if ( is_resource($this->connectionID) && !$this->hasError() ) { $this->connectionOpened = true; return true; } $func = $this->getMetaFunction('error'); $this->errorMessage = $this->connectionID ? $func($this->connectionID) : $func(); $error_msg = 'Database connection failed, please check your connection settings.
Error (' . $this->errorCode . '): ' . $this->errorMessage; if ( (defined('IS_INSTALL') && IS_INSTALL) || $retry ) { trigger_error($error_msg, E_USER_WARNING); } else { throw new Exception($error_msg); } $this->connectionOpened = false; return false; } /** * Setups the connection according given configuration * * @param Array $config * @return bool * @access public */ public function setup($config) { if ( is_object($this->Application) ) { $this->debugMode = $this->Application->isDebugMode(); } return $this->Connect( $config['Database']['DBHost'], $config['Database']['DBUser'], $config['Database']['DBUserPassword'], $config['Database']['DBName'] ); } /** * Performs 3 reconnect attempts in case if connection to a DB was lost in the middle of script run (e.g. server restart) * * @param bool $force_new * @return bool * @access protected */ protected function ReConnect($force_new = false) { $retry_count = 0; $connected = false; $func = $this->getMetaFunction('close'); $func($this->connectionID); while ( $retry_count < 3 ) { sleep(5); // wait 5 seconds before each reconnect attempt $connected = $this->Connect( $this->connectionParams['host'], $this->connectionParams['user'], $this->connectionParams['pass'], $this->connectionParams['db'], $force_new, true ); if ( $connected ) { break; } $retry_count++; } return $connected; } /** * Shows error message from previous operation * if it failed * * @param string $sql * @param string $key_field * @param bool $no_debug * @return bool * @access protected */ protected function showError($sql = '', $key_field = null, $no_debug = false) { static $retry_count = 0; $func = $this->getMetaFunction('errno'); if (!$this->connectionID) { // no connection while doing mysql_query $this->errorCode = $func(); if ( $this->hasError() ) { $func = $this->getMetaFunction('error'); $this->errorMessage = $func(); $ret = $this->callErrorHandler($sql); if (!$ret) { exit; } } return false; } // checking if there was an error during last mysql_query $this->errorCode = $func($this->connectionID); if ( $this->hasError() ) { $func = $this->getMetaFunction('error'); $this->errorMessage = $func($this->connectionID); $ret = $this->callErrorHandler($sql); if ( ($this->errorCode == 2006 || $this->errorCode == 2013) && ($retry_count < 3) ) { // #2006 - MySQL server has gone away // #2013 - Lost connection to MySQL server during query $retry_count++; if ( $this->ReConnect() ) { return $this->Query($sql, $key_field, $no_debug); } } if (!$ret) { exit; } } else { $retry_count = 0; } return false; } /** * Sends db error to a predefined error handler * * @param $sql * @return bool * @access protected */ protected function callErrorHandler($sql) { if (is_array($this->errorHandler)) { $func = $this->errorHandler[1]; $ret = $this->errorHandler[0]->$func($this->errorCode, $this->errorMessage, $sql); } else { $func = $this->errorHandler; $ret = $func($this->errorCode, $this->errorMessage, $sql); } return $ret; } /** * Default error handler for sql errors * * @param int $code * @param string $msg * @param string $sql * @return bool * @access public */ public function handleError($code, $msg, $sql) { echo 'Processing SQL: ' . $sql . '
'; echo 'Error (' . $code . '): ' . $msg . '
'; return false; } /** * Set's database name for connection * to $new_name * * @param string $new_name * @return bool * @access protected */ protected function setDB($new_name) { if (!$this->connectionID) return false; $func = $this->getMetaFunction('select_db'); return $func($new_name, $this->connectionID); } /** * Returns first field of first line * of recordset if query ok or false * otherwise * * @param string $sql * @param int $offset * @return string * @access public */ public function GetOne($sql, $offset = 0) { $row = $this->GetRow($sql, $offset); if ( !$row ) { return false; } return array_shift($row); } /** * Returns first row of recordset * if query ok, false otherwise * * @param string $sql * @param int $offset * @return Array * @access public */ public function GetRow($sql, $offset = 0) { $sql .= ' ' . $this->getLimitClause($offset, 1); $ret = $this->Query($sql); if ( !$ret ) { return false; } return array_shift($ret); } /** * Returns 1st column of recordset as * one-dimensional array or false otherwise * Optional parameter $key_field can be used * to set field name to be used as resulting * array key * * @param string $sql * @param string $key_field * @return Array * @access public */ public function GetCol($sql, $key_field = null) { $rows = $this->Query($sql); if ( !$rows ) { return $rows; } $i = 0; $row_count = count($rows); $ret = Array (); if ( isset($key_field) ) { while ( $i < $row_count ) { $ret[$rows[$i][$key_field]] = array_shift($rows[$i]); $i++; } } else { while ( $i < $row_count ) { $ret[] = array_shift($rows[$i]); $i++; } } return $ret; } /** * Queries db with $sql query supplied * and returns rows selected if any, false * otherwise. Optional parameter $key_field * allows to set one of the query fields * value as key in string array. * * @param string $sql * @param string $key_field * @param bool $no_debug * @return Array * @access public */ public function Query($sql, $key_field = null, $no_debug = false) { $this->lastQuery = $sql; if ( !$no_debug ) { $this->_queryCount++; } if ( $this->debugMode && !$no_debug ) { return $this->debugQuery($sql, $key_field); } $query_func = $this->getMetaFunction('query'); // set 1st checkpoint: begin if ( $this->_captureStatistics ) { $start_time = microtime(true); } // set 1st checkpoint: end $this->setError(0, ''); // reset error $this->queryID = $query_func($sql, $this->connectionID); if ( is_resource($this->queryID) ) { $ret = Array (); $fetch_func = $this->getMetaFunction('fetch_assoc'); if ( isset($key_field) ) { while ( ($row = $fetch_func($this->queryID)) ) { $ret[$row[$key_field]] = $row; } } else { while ( ($row = $fetch_func($this->queryID)) ) { $ret[] = $row; } } // set 2nd checkpoint: begin if ( $this->_captureStatistics ) { $query_time = microtime(true) - $start_time; if ( $query_time > DBG_MAX_SQL_TIME && !$no_debug ) { $this->Application->logSlowQuery($sql, $query_time); } $this->_queryTime += $query_time; } // set 2nd checkpoint: end $this->Destroy(); return $ret; } else { // set 2nd checkpoint: begin if ( $this->_captureStatistics ) { $this->_queryTime += microtime(true) - $start_time; } // set 2nd checkpoint: end } return $this->showError($sql, $key_field, $no_debug); } /** * Performs sql query, that will change database content * * @param string $sql * @return bool * @access public */ public function ChangeQuery($sql) { $this->Query($sql); return $this->errorCode == 0 ? true : false; } /** * Queries db with $sql query supplied * and returns rows selected if any, false * otherwise. Optional parameter $key_field * allows to set one of the query fields * value as key in string array. * * Each database query will be profiled into Debugger. * * @param string $sql * @param string $key_field * @return Array * @access public */ protected function debugQuery($sql, $key_field = null) { global $debugger; $query_func = $this->getMetaFunction('query'); // set 1st checkpoint: begin $profileSQLs = defined('DBG_SQL_PROFILE') && DBG_SQL_PROFILE; if ( $profileSQLs ) { $queryID = $debugger->generateID(); $debugger->profileStart('sql_' . $queryID, $debugger->formatSQL($sql)); } // set 1st checkpoint: end $this->setError(0, ''); // reset error $this->queryID = $query_func($sql, $this->connectionID); if ( is_resource($this->queryID) ) { $ret = Array (); $fetch_func = $this->getMetaFunction('fetch_assoc'); if ( isset($key_field) ) { while ( ($row = $fetch_func($this->queryID)) ) { $ret[$row[$key_field]] = $row; } } else { while ( ($row = $fetch_func($this->queryID)) ) { $ret[] = $row; } } // set 2nd checkpoint: begin if ( $profileSQLs ) { $first_cell = count($ret) == 1 && count(current($ret)) == 1 ? current(current($ret)) : null; if ( strlen($first_cell) > 200 ) { $first_cell = substr($first_cell, 0, 50) . ' ...'; } $debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), $first_cell, $this->_queryCount, $this->nextQueryCachable, $this->serverIndex); $debugger->profilerAddTotal('sql', 'sql_' . $queryID); $this->nextQueryCachable = false; } // set 2nd checkpoint: end $this->Destroy(); return $ret; } else { // set 2nd checkpoint: begin if ( $profileSQLs ) { $debugger->profileFinish('sql_' . $queryID, null, null, $this->getAffectedRows(), null, $this->_queryCount, $this->nextQueryCachable, $this->serverIndex); $debugger->profilerAddTotal('sql', 'sql_' . $queryID); $this->nextQueryCachable = false; } // set 2nd checkpoint: end } return $this->showError($sql, $key_field); } /** * Free memory used to hold recordset handle * * @access public */ public function Destroy() { if ( $this->queryID ) { $free_func = $this->getMetaFunction('free_result'); $free_func($this->queryID); $this->queryID = null; } } /** * Returns auto increment field value from * insert like operation if any, zero otherwise * * @return int * @access public */ public function getInsertID() { $func = $this->getMetaFunction('insert_id'); return $func($this->connectionID); } /** * Returns row count affected by last query * * @return int * @access public */ public function getAffectedRows() { $func = $this->getMetaFunction('affected_rows'); return $func($this->connectionID); } /** * Returns LIMIT sql clause part for specific db * * @param int $offset * @param int $rows * @return string * @access public */ public function getLimitClause($offset, $rows) { if ( !($rows > 0) ) { return ''; } switch ( $this->dbType ) { default: return 'LIMIT ' . $offset . ',' . $rows; break; } } /** * If it's a string, adds quotes and backslashes (only work since PHP 4.3.0) * Otherwise returns as-is * * @param mixed $string * @return string * @access public */ public function qstr($string) { if ( is_null($string) ) { return 'NULL'; } # This will also quote numeric values. This should be harmless, # and protects against weird problems that occur when they really # _are_ strings such as article titles and string->number->string # conversion is not 1:1. return "'" . mysql_real_escape_string($string, $this->connectionID) . "'"; } /** * Escapes strings (only work since PHP 4.3.0) * * @param mixed $string * @return string * @access public */ public function escape($string) { if ( is_null($string) ) { return 'NULL'; } $string = mysql_real_escape_string($string, $this->connectionID); // prevent double-escaping of MySQL wildcard symbols ("%" and "_") in case if they were already escaped return str_replace(Array ('\\\\%', '\\\\_'), Array ('\\%', '\\_'), $string); } /** * Returns last error code occurred * * @return int * @access public */ public function getErrorCode() { return $this->errorCode; } /** * Returns last error message * * @return string * @access public */ public function getErrorMsg() { return $this->errorMessage; } /** * Performs insert of given data (useful with small number of queries) * or stores it to perform multiple insert later (useful with large number of queries) * * @param Array $fields_hash * @param string $table * @param string $type * @param bool $insert_now * @return bool * @access public */ public function doInsert($fields_hash, $table, $type = 'INSERT', $insert_now = true) { static $value_sqls = Array (); if ($insert_now) { $fields_sql = '`' . implode('`,`', array_keys($fields_hash)) . '`'; } $values_sql = ''; foreach ($fields_hash as $field_name => $field_value) { $values_sql .= $this->qstr($field_value) . ','; } // don't use preg here, as it may fail when string is too long $value_sqls[] = rtrim($values_sql, ','); $insert_result = true; if ($insert_now) { $insert_count = count($value_sqls); if (($insert_count > 1) && ($value_sqls[$insert_count - 1] == $value_sqls[$insert_count - 2])) { // last two records are the same array_pop($value_sqls); } $sql = strtoupper($type) . ' INTO `' . $table . '` (' . $fields_sql . ') VALUES (' . implode('),(', $value_sqls) . ')'; $value_sqls = Array (); // reset before query to prevent repeated call from error handler to insert 2 records instead of 1 $insert_result = $this->ChangeQuery($sql); } return $insert_result; } /** * Update given field values to given record using $key_clause * * @param Array $fields_hash * @param string $table * @param string $key_clause * @return bool * @access public */ public function doUpdate($fields_hash, $table, $key_clause) { if (!$fields_hash) return true; $fields_sql = ''; foreach ($fields_hash as $field_name => $field_value) { $fields_sql .= '`'.$field_name.'` = ' . $this->qstr($field_value) . ','; } // don't use preg here, as it may fail when string is too long $fields_sql = rtrim($fields_sql, ','); $sql = 'UPDATE `'.$table.'` SET '.$fields_sql.' WHERE '.$key_clause; return $this->ChangeQuery($sql); } /** * Allows to detect table's presence in database * * @param string $table_name * @param bool $force * @return bool * @access public */ public function TableFound($table_name, $force = false) { static $table_found = false; if ( $table_found === false ) { $table_found = array_flip($this->GetCol('SHOW TABLES')); } if ( !preg_match('/^' . preg_quote(TABLE_PREFIX, '/') . '(.*)/', $table_name) ) { $table_name = TABLE_PREFIX . $table_name; } if ( $force ) { if ( $this->Query('SHOW TABLES LIKE ' . $this->qstr($table_name)) ) { $table_found[$table_name] = 1; } else { unset($table_found[$table_name]); } } return isset($table_found[$table_name]); } /** * Returns query processing statistics * * @return Array * @access public */ public function getQueryStatistics() { return Array ('time' => $this->_queryTime, 'count' => $this->_queryCount); } /** * Get status information from SHOW STATUS in an associative array * * @param string $which * @return Array * @access public */ public function getStatus($which = '%') { $status = Array (); $records = $this->Query('SHOW STATUS LIKE "' . $which . '"'); foreach ($records as $record) { $status[ $record['Variable_name'] ] = $record['Value']; } return $status; } /** * Get slave replication lag. It will only work if the DB user has the PROCESS privilege. * * @return int * @access public */ public function getSlaveLag() { // don't use kDBConnection::Query method, since it will create an array of all server processes $rs = mysql_query('SHOW PROCESSLIST', $this->connectionID); $skip_states = Array ( 'Waiting for master to send event', 'Connecting to master', 'Queueing master event to the relay log', 'Waiting for master update', 'Requesting binlog dump', ); // find slave SQL thread while ( $row = mysql_fetch_array($rs) ) { if ( $row['User'] == 'system user' && !in_array($row['State'], $skip_states) ) { // this is it, return the time (except -ve) return $row['Time'] > 0x7fffffff ? false : $row['Time']; } } return false; } }