This libray was written quite some time ago, and it has so far been used in all sorts of small-ish projects.
I'm about to base a more complex, security (Access Control) related, open-source project on this library, so I thought it would be a good moment to ask for a code review.
Source: DbHandler
<?php
/**
* @package DbHandler
* @version 1.04
* @date 2010-09-27
* @copyright (c) 2010-2012 Netslik (http://netsilik.nl)
* @license EUPL (European Union Public Licence, v.1.1)
*
* Example of usage:
*
* // Object creation
* $dbPointer = new DbHandler('localhost', 'user', 'secret', 'testDatabase');
*
* // Executing queries
* $resultSet = $dbPointer->doQuery('SELECT * FROM test WHERE id = %i, 1);
* $records = $resultSet->fetch();
*
* var_dump( $records );
*/
require('class.dbResult.php');
/**
* Database Access abstraction object
*/
class DbHandler {
protected $_connection = null;
protected $_inTransaction;
/**
* @param string $dbHost Either a host name or the IP address for the MySQL database server
* @param string $dbUser The MySQL user name
* @param string $dbPass The MySQL password for this user
* @param string $dbName The optional name of the database to select
* @throws Exception
* @return object instance
*/
public function __construct ($dbHost, $dbUser, $dbPass, $dbName = false) {
$this->_connection = new mysqli($dbHost, $dbUser, $dbPass);
$this->_inTransaction = false;
/* This is the correct OO way to do setup a connection, BUT $connect_error is broken up till PHP 5.3.1.
if ( null !== ($errorMsg = $this->_connection->connect_error) ) {
$this->_connection = null;
throw new Exception('Could not connect to DB-server: '.$errorMsg);
}
*/
// So, let's use a backward compatible check
if (mysqli_connect_error()) {
$this->connection = null;
throw new Exception('Could not connect to DB-server: '.mysqli_connect_error());
}
// Select specified database (if any)
if ( $dbName !== false) {
$this->selectDb( $dbName );
}
}
/**
* Commit a transaction
* @return bool true on success, false on failure
*/
public function commit() {
if ( ! $this->_inTransaction) {
error('No transaction started', E_USER_NOTICE, 1);
return false;
}
$this->_inTransaction = false;
return $this->_connection->query('COMMIT');
}
/**
* Execute a prepared query
* @param string $query the query with zero or more parameter marker characters at the appropriate positions.
* Parameter markers are defined as a literal % followed by either
* i : corresponding variable will be interpreted as an integer
* d : corresponding variable will be interpreted as a double
* s : corresponding variable will be interpreted as a string
* b : corresponding variable will be interpreted as a blob and should be sent in packets (but this has not yet been implemented)
* @param mixed $params An optional set of variables, matching the parameter markers in $query. if an array is given, each element
* of the array will be interpreted as being of the type specified by the placeholder
* @throws Exception
* @return DbResult Object holding the result of the executed query
*/
public function doQuery($query) {
$query = trim($query);
$params = array_slice(func_get_args(), 1);
array_unshift($params, $this->_parse($query, $params));
if (strlen($params[0]) <> count($params) - 1) {
throw new Exception((count($params) - 1).' parameters specified, '.strlen($params[0]).' expected');
}
$statement = $this->_connection->prepare($query);
if ( ! $statement || $statement->errno > 0) {
throw new Exception('Query preparation failed: '.$this->_connection->error);
}
$startTime = microtime(true);
if ( (strlen($params[0]) > 0 && ! call_user_func_array(array($statement, 'bind_param'), $this->_referenceValues($params)) ) || $statement->errno > 0) {
throw new Exception('Parameter binding failed');
}
$statement->execute();
$queryTime = microtime(true) - $startTime;
if ($statement->errno > 0) {
throw new Exception('Query failed: '.$statement->error);
}
return new DbResult( $statement, $queryTime );
}
/**
* Fetch connection resource for this db connection
* @return resource mysqli connection
*/
public function getConnectionPtr() {
return $this->_connection;
}
/**
* Parse the query for parameter placeholders and, if appropriate, match them to the number of elemnts in the parameter
* @param string &$query a reference to the query string
* @param array &$params a reference to the array with
* @throws Exception
* @return string all tokens found by the parser
* @note both the original variables $query and $params passed to this method will be modified by reference!
*/
private function _parse(&$query, &$params) {
$s = $d = false; // quoted string quote type
$n = 0; // parameter index
$tokenString = '';
$queryLength = strlen($query);
for ($i = 0; $i < $queryLength; $i++) {
switch( $query{ $i } ) {
case '\\':
$i++;
break;
case '\'':
if (!$d) { $s = !$s; }
break;
case '"':
if (!$s) { $d = !$d; }
break;
case '%':
if (!$s && !$d && $i+1 < $queryLength) {
if ('i' === $query{$i+1} || 'd' === $query{$i+1} || 's' === $query{$i+1} || 'b' === $query{$i+1}) {
if ($n >= count($params)) {
throw new Exception('The number of parameters is not equal to the number of placeholders');
}
if (is_array($params[$n])) {
$elmentCount = count($params[$n]);
array_splice($params, $n, 1, $params[$n]);
$tokenString .= str_repeat($query{$i+1}, $elmentCount);
$query = substr_replace($query, implode(',', array_fill(0, $elmentCount, '?')), $i, 2);
$queryLength += $elmentCount * 2 - 3;
$n += $elmentCount;
} else {
$tokenString .= $query{$i+1};
$query = substr_replace($query, '?', $i, 2);
$queryLength--;
$n++;
}
}
}
break;
// case: check for the various comment start chars (not implemented yet)
}
}
return $tokenString;
}
/**
* PHP >= 5.3 expects the parametrs passed to mysqli_stmt::bind_param to be references. However, we will do the binding after query execution
* So, this functions quickly solves the issues by wrapping the arguments in an associative array.
* @param $array
* @return associative array for PHP >= 5.3, unchanged array otherwise
*/
private function _referenceValues($array){
if (strnatcmp(phpversion(),'5.3') >= 0) { // References are required for PHP 5.3+ (and noone knows why)
$references = array();
foreach($array as $key => $value) {
$references[$key] = &$array[$key];
}
return $references;
}
return $array;
}
/**
* Rollback a transaction
* @return bool true on success, false on failure
*/
public function rollback() {
if ( ! $this->_inTransaction) {
error('No transaction started', E_USER_NOTICE, 1);
return false;
}
$this->_inTransaction = false;
return $this->_connection->query('ROLLBACK');
}
/**
* Select a database to use on current connection
* @param string $dbName name of the database to select
* @return true on success, false otherwise
*/
public function selectDb ( $dbName ) {
return $this->_connection->select_db( $dbName );
}
/**
* Start a transaction
* @return bool true on success, false on failure
* @note 'advanced' features such as WITH CONSISTENT SNAPSHOT are not supported
*/
public function startTransaction() {
if ($this->_inTransaction) {
error('Implicit commit for previous transaction', E_USER_NOTICE, 1);
}
$this->_inTransaction = true;
return $this->_connection->query('START TRANSACTION');
}
/**
* Close current db connection on object destruction
*/
public function __destruct() {
if ($this->_connection !== null) {
$this->_connection->close();
$this->_connection = null;
}
}
}
Source: DbResult
<?php
/**
* @package DbHandler
* @version 1.04
* @date 2010-09-27
* @copyright (c) 2010-2012 Netslik (http://netsilik.nl)
* @license EUPL (European Union Public Licence, v.1.1)
*
*/
/**
* Result object, returned by the DbHandler whenever a valid query is executed
*/
class DbResult {
protected $_statement = null;
protected $_result = null; // holds a bound record as fieldname => value pairs; modified by reference
protected $_queryTime = 0;
/**
* @param MySQLi $statement Statement the statement for this result set
* @param int $queryTime the time in seconds the statement took to execute
* @return object instance
*/
public function __construct ($statement, $queryTime) {
$this->_statement = $statement;
$this->_queryTime = $queryTime;
if ( false !== ($metaData = $statement->result_metadata()) ) {
$statement->store_result();
while ($field = $metaData->fetch_field()) {
$params[] = &$this->_result[$field->name];
}
call_user_func_array(array($this->_statement, 'bind_result'), $params);
$metaData->close();
}
}
/**
* get the results for an SELECT statement
* @return array an associative array per record, containing all fields (columns)
*/
public function fetch() {
if (is_null($this->_result)) { // No result data available
return false;
}
$records = array();
while ($this->_statement->fetch()) {
foreach($this->_result as $name => $value) {
$column[$name] = $value;
}
$records[] = $column;
}
$this->_statement->data_seek(0);
return $records;
}
/**
* get the results for an SELECT statement
* @param int $column optional name of the column to fetch
* @return array an indexed array containing all fields for specified column or the first column if none specified
*/
public function fetchColumn($column = null) {
$records = $this->fetch();
$values = array();
foreach ($records as $record) {
foreach ($record as $key => $val) {
if ( ! is_null($column) && $column <> $key) {
continue;
}
$values[] = $val;
break;
}
}
return $values;
}
/**
* get the results for an SELECT statement
* @param int $field name of the column of the first record to fetch
* @return scalar the value of the specified field
*/
public function fetchField($field, $recordNum = 0) {
$records = $this->fetch();
return isset($records[$recordNum][$field]) ? $records[$recordNum][$field] : null;
}
/**
* get the results for an SELECT statement
* @param int $recordNum return the num-th record from the set to get
* @return array an associative array containing all fields (columns) for specified record
*/
public function fetchRecord($recordNum = 0) {
$records = $this->fetch();
return isset($records[$recordNum]) ? $records[$recordNum] : array();
}
/**
* get the total number of records changed, deleted, or inserted by the last executed statement
* @return int number of records affected by this statement
*/
public function getAffectedRecords() {
return $this->_statement->affected_rows;
}
/**
* get the number of fields (columns) in the result set
* @return int number of fields (columns) in the result set
*/
public function getFieldCount() {
return $this->_statement->field_count;
}
/**
* get the primary for this inserted record
* @return int the value of the primary for this inserted record, or 0 if nothing was inserted
*/
public function getInsertedId() {
return $this->_statement->insert_id;
}
/**
* get the query execution time
* @return int time in seconds for this query
*/
public function getQueryTime() {
return number_format($this->_queryTime, 4);
}
/**
* get the total number of records in this result set
* @return int number of records in this result set
*/
public function getRecordCount() {
return $this->_statement->num_rows;
}
/**
* dump query result to std-out as an html table
* @return void
*/
public function dump() {
echo "<div class=\"debug\">\n";
if (is_null($this->_result)) {
echo "<strong>Query OK, ".$this->getAffectedRecords()." rows affected (".$this->getQueryTime()." sec.)</strong>\n";
} elseif ( 0 == ($recordCount = $this->getRecordCount()) ) {
echo "<strong>empty set (".$this->getQueryTime()." sec.)</strong>\n";
} else {
$records = $this->fetch();
echo "<table cellspacing=\"0\" cellpadding=\"3\" border=\"1\">\n";
for ($i = 0; $i < $recordCount; $i++) {
if ($i == 0) {
$fieldnames = array_keys($records[0]);
echo "\t<tr>\n";
foreach ($fieldnames as $fieldname) {
echo "\t\t<th>".$fieldname."</th>\n";
}
echo "\t</tr>\n";
}
echo "\t<tr>\n";
foreach ($records[$i] as $field) {
echo "\t\t<td>";
echo (is_null($field)) ? '<em>NULL</em>' : htmlspecialchars($field, ENT_NOQUOTES);
echo "</td>\n";
}
echo "\t</tr>\n";
}
echo "</table>\n";
echo "<strong>$recordCount row in set (".$this->getQueryTime()." sec.)</strong>\n";
}
echo "</div>\n";
}
/**
* cleanup any unused memory
*/
public function __destruct () {
$this->_statement->close();
}
}
Any and all feedback is welcome.