/system/classes/databaseconnection.php
PHP | 843 lines | 445 code | 73 blank | 325 comment | 57 complexity | 107ac59cdced650d11ae8f3a10bd348e MD5 | raw file
Possible License(s): Apache-2.0
- <?php
- /**
- * @package Habari
- *
- */
- /**
- * Habari DatabaseConnection Class
- *
- * Actual database connection.
- */
- class DatabaseConnection
- {
- private $fetch_mode = PDO::FETCH_CLASS; // PDO Fetch mode
- private $fetch_class_name = 'QueryRecord'; // The default class name for fetching classes
- private $driver; // PDO driver name
- private $keep_profile = DEBUG; // keep profiling and timing information?
- protected $pdo = null; // handle to the PDO interface
- private $pdo_statement = null; // handle for a PDOStatement
- private $pdo_transaction = false; // handle for transaction status
- /**
- * @var array tables Habari knows about
- */
- private $tables = array(
- 'blocks',
- 'blocks_areas',
- 'commentinfo',
- 'comments',
- 'crontab',
- 'groups',
- 'group_token_permissions',
- 'log',
- 'log_types',
- 'object_terms',
- 'object_types',
- 'options',
- 'post_tokens',
- 'postinfo',
- 'posts',
- 'poststatus',
- 'posttype',
- 'rewrite_rules',
- 'scopes',
- 'sessions',
- 'tags',
- 'tag2post',
- 'terms',
- 'terminfo',
- 'tokens',
- 'userinfo',
- 'users',
- 'user_token_permissions',
- 'users_groups',
- 'vocabularies',
- );
- /**
- * @var array mapping of table name -> prefixed table name
- */
- private $sql_tables = array();
- private $sql_tables_repl = array();
- private $errors = array(); // an array of errors related to queries
- private $profiles = array(); // an array of query profiles
- protected $prefix = ''; // class protected storage of the database table prefix, defaults to ''
- private $current_table;
- /**
- * Returns the appropriate type of Connection class for the connect string passed or null on failure
- *
- * @param connection_string a PDO connection string
- * @return mixed returns appropriate DatabaseConnection child class instance or errors out if requiring the db class fails
- */
- public static function ConnectionFactory( $connect_string )
- {
- list( $engine ) = explode( ':', $connect_string, 2 );
- require_once( HABARI_PATH . "/system/schema/{$engine}/connection.php" );
- $engine .= 'Connection';
- return new $engine();
- }
- /**
- * Populate the table mapping.
- *
- * @return void
- */
- protected function load_tables()
- {
- if ( isset( Config::get( 'db_connection' )->prefix ) ) {
- $prefix = Config::get( 'db_connection' )->prefix;
- }
- else {
- $prefix = $this->prefix;
- }
- $this->prefix = $prefix;
- // build the mapping with prefixes
- foreach ( $this->tables as $t ) {
- $this->sql_tables[$t] = $prefix . $t;
- $this->sql_tables_repl[$t] = '{' . $t . '}';
- }
- }
- /**
- * Connect to a database server
- *
- * @param string $connect_string a PDO connection string
- * @param string $db_user the database user name
- * @param string $db_pass the database user password
- * @return boolean true on success, false on error
- */
- public function connect ( $connect_string, $db_user, $db_pass )
- {
- $this->pdo = @new PDO( $connect_string, $db_user, $db_pass );
- $this->pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_WARNING );
- $this->load_tables();
- return true;
- }
- /**
- * Disconnect from the database server.
- *
- * @return boolean true
- */
- public function disconnect()
- {
- $this->pdo = null; // this is the canonical way :o
- return true;
- }
- /**
- * Check whether there is an existing connection to a database.
- *
- * @return boolean
- */
- public function is_connected()
- {
- return ( null != $this->pdo );
- }
- /**
- * Check whether there is a transaction underway.
- *
- * @return boolean
- */
- public function in_transaction()
- {
- return $this->pdo_transaction;
- }
- /**
- * Get the full table name for the given table.
- *
- * @param string $name name of the table
- * @return string the full table name, or false if the table was not found
- */
- public function table( $name )
- {
- if ( isset( $this->sql_tables[$name] ) ) {
- return $this->sql_tables[$name];
- }
- else {
- return false;
- }
- }
- /**
- * Adds a table to the list of tables known to Habari. Used
- * by Theme and Plugin classes to inform the DB class about
- * custom tables used by the plugin
- *
- * @param name the table name
- */
- public function register_table( $name )
- {
- $this->tables[] = $name;
- $this->load_tables();
- }
- /**
- * Sets the fetch mode for return calls from PDOStatement
- *
- * @param mode One of the PDO::FETCH_MODE integers
- */
- public function set_fetch_mode( $mode )
- {
- $this->fetch_mode = $mode;
- }
- /**
- * Sets the class to fetch, if fetch mode is PDO::FETCH_CLASS
- *
- * @param class_name Name of class to create during fetch
- */
- public function set_fetch_class( $class_name )
- {
- $this->fetch_class_name = $class_name;
- }
- /**
- * Execute the given query on the database. Encapsulates PDO::exec.
- * WARNING: Make sure you don't call this with a SELECT statement.
- * PDO will buffer the results and leave your cursor dangling.
- *
- * @param string $query the query to run
- * @return boolean true on success, false on error
- */
- public function exec( $query )
- {
- // Allow plugins to modify the query
- $query = Plugins::filter( 'db_exec', $query, array() );
- // Translate the query for the database engine
- $query = $this->sql_t( $query, array() );
- // Replace braced table names in the query with their prefixed counterparts
- $query = self::filter_tables( $query );
- // Allow plugins to modify the query after it has been processed
- $query = Plugins::filter( 'db_exec_postprocess', $query, array() );
- return ( $this->pdo->exec( $query ) !== false );
- }
- /**
- * Execute a SQL statement.
- *
- * @param string $query the SQL statement
- * @param array $args values for the bound parameters
- * @return boolean true on success, false on failure
- */
- public function query( $query, $args = array() )
- {
- if ( $this->pdo_statement != null ) {
- $this->pdo_statement->closeCursor();
- }
- // Allow plugins to modify the query
- $query = Plugins::filter( 'query', $query, $args );
- // Translate the query for the database engine
- $query = $this->sql_t( $query, $args );
- // Replace braced table names in the query with their prefixed counterparts
- $query = self::filter_tables( $query );
- // Allow plugins to modify the query after it has been processed
- $query = Plugins::filter( 'query_postprocess', $query, $args );
- if ( $this->pdo_statement = $this->pdo->prepare( $query ) ) {
- if ( $this->fetch_mode == PDO::FETCH_CLASS ) {
- /* Try to get the result class autoloaded. */
- if ( ! class_exists( strtolower( $this->fetch_class_name ) ) ) {
- $tmp = $this->fetch_class_name;
- new $tmp();
- }
- /* Ensure that the class is actually available now, otherwise segfault happens (if we haven't died earlier anyway). */
- if ( class_exists( strtolower( $this->fetch_class_name ) ) ) {
- $this->pdo_statement->setFetchMode( PDO::FETCH_CLASS, $this->fetch_class_name, array() );
- }
- else {
- /* Die gracefully before the segfault occurs */
- echo '<br><br>' . _t( 'Attempt to fetch in class mode with a non-included class' ) . '<br><br>';
- return false;
- }
- }
- else {
- $this->pdo_statement->setFetchMode( $this->fetch_mode );
- }
- /* If we are profiling, then time the query */
- if ( $this->keep_profile ) {
- $profile = new QueryProfile( $query );
- $profile->params = $args;
- $profile->start();
- }
- if ( ! $this->pdo_statement->execute( $args ) ) {
- $this->add_error( array( 'query'=>$query,'error'=>$this->pdo_statement->errorInfo() ) );
- return false;
- }
- if ( $this->keep_profile ) {
- $profile->stop();
- $this->profiles[] = $profile;
- }
- return true;
- }
- else {
- $this->add_error( array(
- 'query' => $query,
- 'error' => $this->pdo->errorInfo(),
- ) );
- return false;
- }
- }
- /**
- * Execute a stored procedure
- *
- * @param procedure name of the stored procedure
- * @param args arguments for the procedure
- * @return mixed whatever the procedure returns...
- * @experimental
- * @todo EVERYTHING... :)
- * Implemented in child classes. Most RDBMS use ANSI-92 syntax,
- * @todo Make sure it's MultiByte safe
- * ( CALL procname ( param1, param2, ... ),
- * so they return the call to here. Those that don't, handle the call individually
- */
- public function execute_procedure( $procedure, $args = array() )
- {
- /* Local scope caching */
- $pdo = $this->pdo;
- $pdo_statement = $this->pdo_statement;
- if ( $pdo_statement != null ) {
- $pdo_statement->closeCursor();
- }
- $query = 'CALL ' . $procedure . '( ';
- if ( count( $args ) > 0 ) {
- $query .= str_repeat( '?,', count( $args ) ); // Add the placeholders
- $query = substr( $query, 0, strlen( $query ) - 1 ); // Strip the last comma
- }
- $query .= ' )';
- $query = $this->sql_t( $query, $args );
- if ( $pdo_statement = $pdo->prepare( $query ) ) {
- /* If we are profiling, then time the query */
- if ( $this->keep_profile ) {
- $profile = new QueryProfile( $query );
- $profile->start();
- }
- if ( ! $pdo_statement->execute( $args ) ) {
- $this->add_error( array( 'query'=>$query,'error'=>$pdo_statement->errorInfo() ) );
- return false;
- }
- if ( $this->keep_profile ) {
- $profile->stop();
- $this->profiles[] = $profile;
- }
- return true;
- }
- else {
- $this->add_error( array( 'query'=>$query,'error'=>$pdo_statement->errorInfo() ) );
- return false;
- }
- }
- /**
- * Start a transaction against the RDBMS in order to wrap multiple
- * statements in a safe ACID-compliant container
- */
- public function begin_transaction()
- {
- if ( ! $this->pdo_transaction ) {
- $this->pdo->beginTransaction();
- $this->pdo_transaction = true;
- }
- }
- /**
- * Rolls a currently running transaction back to the
- * prexisting state, or, if the RDBMS supports it, whenever
- * a savepoint was committed.
- */
- public function rollback()
- {
- if ( $this->pdo_transaction ) {
- $this->pdo->rollBack();
- $this->pdo_transaction = false;
- }
- }
- /**
- * Commit a currently running transaction
- */
- public function commit()
- {
- if ( $this->pdo_transaction ) {
- $this->pdo->commit();
- $this->pdo_transaction = false;
- }
- }
- /**
- * Returns query profiles
- *
- * @return array an array of query profiles
- */
- public function get_profiles()
- {
- return $this->profiles;
- }
- /**
- * Adds an error to the internal collection
- *
- * @param error array( 'query'=>query, 'error'=>errorInfo )
- */
- private function add_error( $error )
- {
- $backtrace1 = debug_backtrace();
- $backtrace = array();
- foreach ( $backtrace1 as $trace ) {
- $backtrace[] = array_intersect_key( $trace, array('file'=>1, 'line'=>1, 'function'=>1, 'class'=>1) );
- }
- $this->errors[] = array_merge( $error, array( 'backtrace'=> $backtrace ) );
- }
- /**
- * Returns error data gathered from database connection
- * @return array An array of error data
- */
- public function get_errors()
- {
- return $this->errors;
- }
- /**
- * Determines if there have been errors since the last clear_errors() call
- * @return boolean True if there were errors, false if not
- */
- public function has_errors()
- {
- return ( count( $this->errors ) > 0 );
- }
- /**
- * Updates the last error pointer to simulate resetting the error array
- */
- public function clear_errors()
- {
- $this->errors = array();
- }
- /**
- * Returns only the last error info
- * @return array Data for the last error
- */
- public function get_last_error()
- {
- $error = end( $this->errors );
- return ( array( 'query'=>$error['query'], 'message'=>$error['error'][2] ) );
- }
- /**
- * Execute a query and return the results as an array of objects
- * @param query the query to execute
- * @param args array of arguments to pass for prepared statements
- * @param string Optional class name for row result objects
- * @return array An array of QueryRecord or the named class each containing the row data
- * <code>$ary= DB::get_results( 'SELECT * FROM tablename WHERE foo= ?', array( 'fieldvalue' ), 'extendedQueryRecord' );</code>
- */
- public function get_results( $query, $args = array() )
- {
- if ( func_num_args() == 3 ) {
- /* Called expecting specific class return type */
- $class_name = func_get_arg( 2 );
- }
- else {
- $class_name = 'QueryRecord';
- }
- $this->set_fetch_mode( PDO::FETCH_CLASS );
- $this->set_fetch_class( $class_name );
- if ( $this->query( $query, $args ) ) {
- return $this->pdo_statement->fetchAll();
- }
- else {
- return false;
- }
- }
- /**
- * Returns a single row (the first in a multi-result set) object for a query
- * @param string The query to execute
- * @param array Arguments to pass for prepared statements
- * @param string Optional class name for row result object
- * @return object A QueryRecord or an instance of the named class containing the row data
- * <code>$obj= DB::get_row( 'SELECT * FROM tablename WHERE foo= ?', array( 'fieldvalue' ), 'extendedQueryRecord' );</code>
- */
- public function get_row( $query, $args = array() )
- {
- if ( func_num_args() == 3 ) {
- /* Called expecting specific class return type */
- $class_name = func_get_arg( 2 );
- }
- else {
- $class_name = 'QueryRecord';
- }
- $this->set_fetch_mode( PDO::FETCH_CLASS );
- $this->set_fetch_class( $class_name );
- if ( $this->query( $query, $args ) ) {
- return $this->pdo_statement->fetch();
- }
- else {
- return false;
- }
- }
- /**
- * Returns all values for a column for a query
- *
- * @param string The query to execute
- * @param array Arguments to pass for prepared statements
- * @return array An array containing the column data
- * <code>$ary= DB::get_column( 'SELECT col1 FROM tablename WHERE foo= ?', array( 'fieldvalue' ) );</code>
- */
- public function get_column( $query, $args = array() )
- {
- if ( $this->query( $query, $args ) ) {
- return $this->pdo_statement->fetchAll( PDO::FETCH_COLUMN );
- }
- else {
- return false;
- }
- }
- /**
- * Return a single value from the database
- *
- * @param string the query to execute
- * @param array Arguments to pass for prepared statements
- * @return mixed a single value ( int, string )
- */
- public function get_value( $query, $args = array() )
- {
- if ( $this->query( $query, $args ) ) {
- $result = $this->pdo_statement->fetch( PDO::FETCH_NUM );
- return $result[0];
- }
- else {
- return false;
- }
- }
- /**
- * Returns an associative array using the first returned column as the array key and the second as the array value
- *
- * @param string The query to execute
- * @param array Arguments to pass for prepared statements
- * @return array An array containing the associative data
- * <code>$ary= $dbconnection->get_keyvalue( 'SELECT keyfield, valuefield FROM tablename');</code>
- */
- public function get_keyvalue( $query, $args = array() )
- {
- if ( $this->query( $query, $args ) ) {
- $result = $this->pdo_statement->fetchAll( PDO::FETCH_NUM );
- $output = array();
- foreach ( $result as $item ) {
- $output[$item[0]] = $item[1];
- }
- return $output;
- }
- else {
- return false;
- }
- }
- /**
- * Inserts into the specified table values associated to the key fields
- * @param string The table name
- * @param array An associative array of fields and values to insert
- * @return boolean True on success, false if not
- * <code>DB::insert( 'mytable', array( 'fieldname' => 'value' ) );</code>
- */
- public function insert( $table, $fieldvalues )
- {
- ksort( $fieldvalues );
- $fields = array_keys( $fieldvalues );
- $values = array_values( $fieldvalues );
-
- $query = "INSERT INTO {$table} ( " . implode( ', ', $fields ) . ' ) VALUES ( ' . implode( ', ', array_fill( 0, count( $values ), '?' ) ) . ' )';
- // need to pass $table on to the $o singleton object;
- $this->current_table = $table;
- return $this->query( $query, $values );
- }
- /**
- * Checks for a record that matches the specific criteria
- * @param string Table to check
- * @param array Associative array of field values to match
- * @return boolean True if any matching record exists, false if not
- * <code>DB::exists( 'mytable', array( 'fieldname' => 'value' ) );</code>
- */
- public function exists( $table, $keyfieldvalues )
- {
- $qry = "SELECT 1 as c FROM {$table} WHERE 1=1 ";
- $values = array();
- foreach ( $keyfieldvalues as $keyfield => $keyvalue ) {
- $qry .= " AND {$keyfield} = ? ";
- $values[] = $keyvalue;
- }
- $result = $this->get_row( $qry, $values );
- return ( $result !== false );
- }
- /**
- * function update
- * Updates any record that matches the specific criteria
- * A new row is inserted if no existing record matches the criteria
- * @param string Table to update
- * @param array Associative array of field values to set
- * @param array Associative array of field values to match
- * @return boolean True on success, false if not
- * <code>DB::update( 'mytable', array( 'fieldname' => 'newvalue' ), array( 'fieldname' => 'value' ) );</code>
- */
- public function update( $table, $fieldvalues, $keyfields )
- {
- ksort( $fieldvalues );
- ksort( $keyfields );
- $keyfieldvalues = array();
- foreach ( $keyfields as $keyfield => $keyvalue ) {
- if ( is_numeric( $keyfield ) ) {
- // if the key is numeric, assume we were handed a simple list of fields that are keys and fetch its value from $fieldvalues
- $keyfieldvalues[$keyvalue] = $fieldvalues[$keyvalue];
- }
- else {
- // otherwise we were handed a key => value pair, use it as-is
- $keyfieldvalues[$keyfield] = $keyvalue;
- }
- }
- if ( $this->exists( $table, $keyfieldvalues ) ) {
- $qry = "UPDATE {$table} SET";
- $values = array();
- $comma = '';
- foreach ( $fieldvalues as $fieldname => $fieldvalue ) {
- $qry .= $comma . " {$fieldname} = ?";
- $values[] = $fieldvalue;
- $comma = ' ,';
- }
- $qry .= ' WHERE 1=1 ';
- foreach ( $keyfields as $keyfield => $keyvalue ) {
- $qry .= "AND {$keyfield} = ? ";
- $values[] = $keyvalue;
- }
- return $this->query( $qry, $values );
- }
- else {
- // We want the keyfieldvalues to be included in
- // the insert, with fieldvalues taking precedence.
- $fieldvalues = $fieldvalues + $keyfieldvalues;
- return $this->insert( $table, $fieldvalues );
- }
- }
- /**
- * Deletes any record that matches the specific criteria
- * @param string Table to delete from
- * @param array Associative array of field values to match
- * @return boolean True on success, false if not
- * <code>DB::delete( 'mytable', array( 'fieldname' => 'value' ) );</code>
- */
- public function delete( $table, $keyfields )
- {
- $qry = "DELETE FROM {$table} WHERE 1=1 ";
- foreach ( $keyfields as $keyfield => $keyvalue ) {
- $qry .= "AND {$keyfield} = ? ";
- $values[] = $keyvalue;
- }
- return $this->query( $qry, $values );
- }
- /**
- * Helper function to return the last inserted sequence or
- * auto_increment field. Useful when doing multiple inserts
- * within a single transaction -- for example, adding dependent
- * related rows.
- *
- * @return mixed The last sequence value ( RDBMS-dependent! )
- * @see http://us2.php.net/manual/en/function.pdo-lastinsertid.php
- */
- public function last_insert_id()
- {
- if ( $this->pdo->getAttribute( PDO::ATTR_DRIVER_NAME ) == 'pgsql' ) {
- return $this->pdo->lastInsertId( $this->current_table. '_pkey_seq' );
- }
- else {
- return $this->pdo->lastInsertId( func_num_args() == 1 ? func_get_arg( 0 ) : '' );
- }
- }
- /**
- * Automatic diffing function, used for determining required database upgrades.
- * Implemented in child classes.
- */
- public function dbdelta( $queries, $execute = true, $silent = true ){}
- public function upgrade_pre( $old_version ){}
- public function upgrade_post( $old_version ){}
- /**
- * Updates the content of the database between versions.
- * Implemented in child classes.
- *
- * @param integer $old_version The old Version::DB_VERSION
- */
- public function upgrade( $old_version, $upgrade_path = '' )
- {
- // Get all the upgrade files
- $upgrade_files = Utils::glob( "{$upgrade_path}/*.sql" );
- // Put the upgrade files into an array using the 0-padded revision + '_0' as the key
- $upgrades = array();
- foreach ( $upgrade_files as $file ) {
- if ( intval( basename( $file, '.sql' ) ) > $old_version ) {
- $upgrades[ sprintf( '%010s_0', basename( $file, '.sql' ) )] = $file;
- }
- }
- // Put the upgrade functions into an array using the 0-padded revision + '_1' as the key
- $upgrade_functions = get_class_methods( $this );
- foreach ( $upgrade_functions as $fn ) {
- if ( preg_match( '%^upgrade_([0-9]+)$%i', $fn, $matches ) ) {
- if ( intval( $matches[1] ) > $old_version ) {
- $upgrades[ sprintf( '%010s_1', $matches[1] )] = array( $this, $fn );
- }
- }
- }
- // Sort the upgrades by revision, ascending
- ksort( $upgrades );
- // Execute all of the upgrade functions
- $result = true;
- foreach ( $upgrades as $upgrade ) {
- if ( is_array( $upgrade ) ) {
- $result &= $upgrade();
- }
- else {
- $result &= $this->query_file( $upgrade );
- }
- if ( !$result ) {
- break;
- }
- }
- return $result;
- }
- /**
- * Load a file containing queries, replace the prefix, execute all queries present
- *
- * @param string $file The filename containing the queries
- * @return boolean True on successful execution of all queries
- */
- public function query_file( $file )
- {
- $upgrade_sql = trim( file_get_contents( $file ) );
- $upgrade_sql = str_replace( '{$prefix}', $this->prefix, $upgrade_sql );
- // Split up the queries
- $queries = explode( ';', $upgrade_sql );
- foreach ( $queries as $query ) {
- if ( trim( $query ) != '' ) {
- if ( !$this->query( $query ) ) {
- return false;
- }
- }
- }
- return true;
- }
- /**
- * Translates the query for the current database engine
- *
- * @param string $query The query to translate for the current database engine
- * @param array $args Arguments to the query
- * @return string The translated query
- */
- public function sql_t( $query )
- {
- return $query;
- }
- /**
- * Replace braced table names with their prefixed counterparts
- *
- * @param string $query The query with {braced} table names
- * @return string The translated query
- */
- public function filter_tables( $query )
- {
- return str_replace( $this->sql_tables_repl, $this->sql_tables, $query );
- }
- public function get_driver_name()
- {
- if ( ! $this->driver ) {
- $this->driver = $this->pdo->getAttribute( PDO::ATTR_DRIVER_NAME );
- }
- return $this->driver;
- }
- public function get_driver_version()
- {
- return $this->pdo->getAttribute( PDO::ATTR_SERVER_VERSION );
- }
- /**
- * Returns number of rows affected by the last DELETE, INSERT, or UPDATE
- *
- * @return int The number of rows affected.
- */
- public function row_count()
- {
- return $this->pdo_statement->rowCount();
- }
- /**
- * Returns a list of tables the DB currently knows about.
- *
- * @return array The list of tables.
- */
- public function list_tables()
- {
- return $this->sql_tables;
- }
- /**
- * Return a PDO-quoted string appropriate for the DB backend we're using.
- *
- * If you're using this then there's 99+% probability you're building your queries the wrong way!
- *
- * @param string $string The string to quote.
- * @return string A DB-safe quoted string.
- */
- public function quote ( $string )
- {
- return $this->pdo->quote( $string );
- }
- }
- ?>