Rev 77 |
Blame |
Compare with Previous |
Last modification |
View Log
| RSS feed
1
<?php
namespace PointedEars\PHPX\Db
;
require_once __DIR__
. '/../global.inc';
/**
* Generic database model class using PDO (PHP Data Objects)
*
* @property-read PDO $connection
* Database connection. Established on read access to this
* property if not yet established.
* @property-read array $lastError
* Last error information of the database operation.
* See {@link PDOStatement::errorInfo()}.
* @property-read string $lastInsertId
* ID of the last inserted row, or the last value from a sequence object,
* depending on the underlying driver. May not be supported by all databases.
* @property-read array $lastResult
* Last result of the database operation
* @property-read boolean $lastSuccess
* Last success value of the database operation
* @author Thomas Lahn
*/
class Database
extends \PointedEars\PHPX\AbstractModel
{
/* Access properties */
/**
* DSN of the database
* @var string
*/
protected $_dsn = '';
/**
* Username to access the database
* @var string
*/
protected $_username;
/**
* Password to access the database
* @var string
*/
protected $_password;
/**
* PDO driver-specific options
* @var array
*/
protected $_options = array();
/**
* Database-specific string to use for quoting a name or value
* left-hand side (for security reasons and to prevent a name
* from being parsed as a keyword).
* @var string
*/
protected $_leftQuote = '';
/**
* Database-specific string to use for quoting a name or value
* left-hand side (for security reasons and to prevent a name
* from being parsed as a keyword).
* @var string
*/
protected $_rightQuote = '';
/* Status properties */
/**
* Database connection
* @var PDO
*/
protected $_connection;
/**
* Last success value of the database operation
* @var boolean
*/
protected $_lastSuccess;
/**
* Last error information of the database operation
* @var array
*/
protected $_lastError;
/**
* Last result of the database operation
* @var array
*/
protected $_lastResult;
/**
* ID of the last inserted row, or the last value from a sequence object,
* depending on the underlying driver. May not be supported by all databases.
* @var string
*/
protected $_lastInsertId = '';
/**
* Creates a new <code>Database</code> instance.
*
* Each of the parameters is optional and can also be given
* by a protected property where the parameter name is preceded
* by <code>_</code>. Parameter values overwrite the default
* property values. It is recommended to use default property
* values of inheriting classes except for small applications
* and testing purposes.
*
* @param string $dsn
* @param string $username
* @param string $password
* @param array $options
* @see PDO::__construct()
*/
public function __construct
($dsn = '', $username = null,
$password = null, array $options = array())
{
if ($dsn !== '')
{
$this->_dsn
= $dsn;
}
if ($username !== null)
{
$this->_username
= $username;
}
if ($password !== null)
{
$this->_password
= $password;
}
if ($options)
{
$this->_options
= $options;
}
}
/**
* Reads the connection configuration for this database
* from the configuration file, .config
*
* There must be an INI section named "database:" followed
* by the value of the <code>$_dbname</code> property
* containing keys and values for the properties of the
* <code>Database</code> instance. Except for the key
* <code>dbname</code>, which allows for aliases, all
* keys are ignored if the corresponding properties
* were set. That is, definitions in the class file
* override those in the configuration file.
*
* @return array|boolean
* The configuration array if the configuration
* file could be read, <code>false</code> otherwise.
*/
public function readConfig
()
{
/* FIXME: Configuration file path should not be hardcoded */
$config = parse_ini_file('.config', true);
if ($config !== false)
{
$section = 'database:' . $this->_dbname
;
if (isset($config[$section]))
{
$dbconfig = $config[$section];
$options = array(
'host', 'port', 'dbname', 'username', 'password', 'charset'
);
foreach ($options as $key)
{
$property = "_$key";
if (isset($dbconfig[$key])
&& ($key == 'dbname'
|| (property_exists
($this, $property)
&& $this->$property === null)))
{
$this->$property = $dbconfig[$key];
}
}
return $config[$section];
}
}
return $config;
}
/**
* @return PDO
*/
public function getConnection
()
{
if ($this->_connection
=== null)
{
$this->_connection
=
new \PDO
($this->_dsn
, $this->_username
, $this->_password
, $this->_options
);
}
return $this->_connection
;
}
/**
* Creates a database according to the specified parameters
*
* Should be overwritten and called by inheriting classes.
*
* @param string $dsn
* Connection DSN (required; must not include the database
* name).
* @param string $username = null
* Connection username. The default is specified by the
* <code>$_username</code> property. Note that creating
* the database usually requires a user with more privileges
* than the one accessing the database or its tables.
* @param string $password = null
* Connection password. The default is specified by the
* <code>$_password</code> property.
* @param array? $options = null
* Connection options. The default is specified by the
* <code>$_options</code> property.
* @param string $spec = null
* Additional database specifications, like character encoding
* and collation.
* @param boolean $force = false
* If a true-value, the database will be attempted to be
* created even if there is a database of the name specified
* by the <code>$_dbname</code> property.
* @return int
* The number of rows affected by the CREATE DATABASE statement.
* @see PDO::__construct()
* @see PDO::exec()
*/
public function create
($dsn, $username = null, $password = null,
array $options = null, $dbspec = null, $force = false)
{
$connection = new \PDO
($dsn,
$username !== null ?
$username : $this->_username
,
$password !== null ?
$password : $this->_password
,
$options !== null ?
$options : $this->_options
);
$query = 'CREATE DATABASE'
. (!$force ?
' IF NOT EXISTS' : '')
. ' ' . $this->escapeName($this->_dbname
)
. ($dbspec ?
' ' . $dbspec : '');
return $connection->exec($query);
}
/**
* Maps column meta-information to a column definition.
*
* Should be overwritten and called by inheriting classes.
*
* @todo
* @param array $value
* @param string $column_name
* @return string
*/
protected function _columnDef
(array $data, $column_name)
{
$def = (isset($data['unsigned']) && $data['unsigned'] ?
'UNSIGNED ' : '')
. $data['type']
. (isset($data['not_null']) && $data['not_null'] ?
' NOT NULL' : ' NULL')
. (isset($data['default']) && $data['default'] ?
" DEFAULT {$data['default']}" : '')
. (isset($data['auto_inc']) && $data['auto_inc'] ?
' AUTO_INCREMENT' : '')
. (isset($data['unique']) && $data['unique'] ?
' UNIQUE KEY' : '')
. (isset($data['primary']) && $data['primary'] ?
' PRIMARY KEY' : '')
. (isset($data['comment']) && $data['comment'] ?
" COMMENT '{$data['comment']}'" : '');
return $this->escapeName($column_name) . ' ' . $def;
}
/**
* Creates a database table according to the specified parameters
*
* @todo
* @param string $name
* @param array $columns
* @param array $options = null
* @return bool
* @see PDOStatement::execute()
*/
public function createTable
($name, array $columns, array $options = null)
{
$class = \
get_class($this);
$query = 'CREATE TABLE '
. $this->escapeName($name)
. '('
. array_map(array($this, '_columnDef'), $columns, array_keys($columns)) . ')';
$stmt = $this->prepare($query);
/* DEBUG */
if (defined('DEBUG') && DEBUG
> 1)
{
debug
(array(
'query' => $query,
));
}
$success =& $this->_lastSuccess
;
$success = $stmt->execute();
$errorInfo =& $this->_lastError
;
$errorInfo = $stmt->errorInfo();
$this->_resetLastInsertId
();
$result =& $this->_lastResult
;
$result = $stmt->fetchAll();
if (defined('DEBUG') && DEBUG
> 1)
{
debug
(array(
'_lastSuccess' => $success,
'_lastError' => $errorInfo,
'_lastResult' => $result
));
}
return $success;
}
/**
* Initiates a transaction
*
* @return bool
* @see PDO::beginTransaction()
*/
public function beginTransaction
()
{
return $this->connection->beginTransaction();
}
/**
* Rolls back a transaction
*
* @return bool
* @see PDO::rollBack()
*/
public function rollBack
()
{
return $this->connection->rollBack();
}
/**
* Commits a transaction
*
* @return bool
* @see PDO::commit()
*/
public function commit
()
{
return $this->connection->commit();
}
/**
* Prepares a statement for execution with the database
* @param string $query
*/
public function prepare
($query, array $driver_options = array())
{
return $this->connection->prepare($query, $driver_options);
}
/**
* Returns the ID of the last inserted row, or the last value from
* a sequence object, depending on the underlying driver.
*
* @return int
*/
public function getLastInsertId
()
{
return $this->_lastInsertId
;
}
/**
* Returns the date of last modification of this database or
* one of its tables.
*
* To be overridden by inheriting classes.
*
* @param string $table (optional)
* Table name. If not provided, all tables of this database
* are considered.
* @return int|null
* Timestamp of last modification, or <code>null</code> if
* unavailable.
*/
public function getLastModified
($table = null)
{
return null;
}
/**
* Escapes a database name so that it can be used in a query.
*
* @param string $name
* The name to be escaped
* @return string
* The escaped name
*/
public function escapeName
($name)
{
return $this->_leftQuote
. $name . $this->_rightQuote
;
}
/**
* Determines if an array is associative (has not all integer keys).
*
* @author
* Algorithm courtesy of squirrel, <http://stackoverflow.com/a/5969617/855543>.
* @param array $a
* @return boolean
* <code>true</code> if <var>$a</var> is associative,
* <code>false</code> otherwise
*/
protected function _isAssociativeArray
(array $a)
{
for (reset($a); is_int(key($a)); next($a));
return !is_null(key($a));
}
/**
* Escapes an associative array so that its string representation can be used
* as list with table or column aliases in a query.
*
* This method does not actually escape anything; it only inserts the
* 'AS' keyword. It should be overridden by inheriting methods.
*
* NOTE: This method intentionally does not check whether the array actually
* is associative.
*
* @param array &$array
* The array to be escaped
* @return array
* The escaped array
*/
protected function _escapeAliasArray
(array &$array)
{
foreach ($array as $column => &$value)
{
$quotedColumn = $column;
if (strpos($column, $this->_leftQuote
) === false
&& strpos($column, $this->_rightQuote
) === false)
{
$quotedColumn = $this->_leftQuote
. $column . $this->_rightQuote
;
}
$value = $value . ' AS ' . $quotedColumn;
}
return $array;
}
/**
* @param array $a
* @param string $prefix
*/
private static
function _expand
(array $a, $prefix)
{
$a2 = array();
foreach ($a as $key => $value)
{
$a2[] = ':' . $prefix . ($key + 1);
}
return $a2;
}
/**
* Escapes an associative array so that its string representation can be used
* as value list in a query.
*
* This method should be overridden by inheriting classes to escape
* column names as fitting for the database schema they support. It is
* strongly recommended that the overriding methods call this method with
* an appropriate <var>$escape</var> parameter, pass all other parameters
* on unchanged, and return its return value.
*
* NOTE: Intentionally does not check whether the array actually is associative!
*
* @param array &$array
* The array to be escaped
* @param string $suffix
* The string to be appended to the column name for the value placeholder.
* The default is the empty string.
* @param array $escape
* The strings to use left-hand side (index 0) and right-hand side (index 1)
* of the column name. The default is the empty string, respectively.
* @return array
* The escaped array
*/
protected function _escapeValueArray
(array &$array, $suffix = '')
{
$result = array();
foreach ($array as $column => $value)
{
$op = '=';
$placeholder = ":{$column}";
if (is_array($value) && $this->_isAssociativeArray
($value))
{
reset($value);
$op = ' ' . key($value) . ' ';
$value = $value[key($value)];
}
if (is_array($value))
{
$placeholder = '(' . implode(', ', self::_expand
($value, $column)) . ')';
}
$result[] = $this->_leftQuote
. $column . $this->_rightQuote
. "{$op}{$placeholder}{$suffix}";
}
return $result;
}
/**
* Constructs the WHERE part of a query
*
* @param string|array $where
* Condition
* @param string $suffix
* The string to be appended to the column name for the value placeholder,
* passed on to {@link Database::_escapeValueArray()}. The default is
* the empty string.
* @return string
* @see Database::_escapeValueArray()
*/
protected function _where
($where, $suffix = '')
{
if (!is_null($where))
{
if (is_array($where))
{
if (count($where) < 1)
{
return '';
}
if ($this->_isAssociativeArray
($where))
{
$where = $this->_escapeValueArray
($where, $suffix);
}
$where = '(' . implode(') AND (', $where) . ')';
}
return ' WHERE ' . $where;
}
return '';
}
/**
* Selects data from one or more tables; the resulting records are stored
* in the <code>result</code> property and returned as an associative array,
* where the keys are the column (alias) names.
*
* @param string|array[string] $tables Table(s) to select from
* @param string|array[string] $columns Column(s) to select from (optional)
* @param string|array $where Condition (optional)
* @param string $order Sort order (optional)
* If provided, MUST start with ORDER BY or GROUP BY
* @param string $limit Limit (optional)
* @param int $fetch_style
* The mode that should be used for {@link PDOStatement::fetchAll()}.
* The default is {@link PDO::FETCH_ASSOC}.
* @param bool $fetchAll
* If <code>true</code> (default), fetches all rows at once.
* You can set this to <code>false</code> in case of memory problems,
* in which case this function will return the prepared
* {@link PDOStatement} instead of the result array. You can then use
* {@link PDOStatement::fetch()} to get the returned rows iteratively.
* <var>$fetch_style</var> will be ignored then, so that you can safely
* pass <code>null</code> for it, for example.
* @return array|PDOStatement
* @see Database::prepare()
* @see PDOStatement::fetchAll()
*/
public function select
($tables, $columns = null, $where = null,
$order = null, $limit = null, $fetch_style = \PDO
::FETCH_ASSOC,
$fetchAll = true)
{
if (is_null($columns))
{
$columns = array('*');
}
if (is_array($columns))
{
if ($this->_isAssociativeArray
($columns))
{
$columns = $this->_escapeAliasArray
($columns);
}
$columns = implode(', ', $columns);
}
if (is_array($tables))
{
if ($this->_isAssociativeArray
($columns))
{
$columns = $this->_escapeAliasArray
($columns);
}
$tables = implode(', ', $tables);
}
$query = "SELECT {$columns} FROM {$tables}" . $this->_where
($where);
if (!is_null($order))
{
if (is_array($order))
{
$order = 'ORDER BY ' . implode(', ', $order);
}
$query .= " $order";
}
if (!is_null($limit))
{
$query .= " LIMIT $limit";
}
$stmt = ($fetchAll
?
$this->prepare($query)
: $this->prepare($query, array(\PDO
::ATTR_CURSOR => \PDO
::CURSOR_SCROLL)));
$params = array();
if (is_array($where) && $this->_isAssociativeArray
($where))
{
/* FIXME: Export and reuse this */
foreach ($where as $column => $condition)
{
/* TODO: Also handle function calls as keys */
if (is_array($condition) && $this->_isAssociativeArray
($condition))
{
reset($condition);
$condition = $condition[key($condition)];
if (is_array($condition))
{
foreach (self::_expand
($condition, $column) as $param_index => $param_name)
{
$params[$param_name] = $condition[$param_index];
}
}
}
else
{
$params[":{$column}"] = $condition;
}
}
}
/* DEBUG */
if (defined('DEBUG') && DEBUG
> 1)
{
debug
(array(
'query' => $query,
'params' => $params
));
}
$success =& $this->_lastSuccess
;
$success = $stmt->execute($params);
$errorInfo =& $this->_lastError
;
$errorInfo = $stmt->errorInfo();
$result =& $this->_lastResult
;
$result = ($fetchAll
?
$stmt->fetchAll($fetch_style)
: $stmt);
if (defined('DEBUG') && DEBUG
> 1)
{
debug
(array(
'_lastSuccess' => $success,
'_lastError' => $errorInfo,
'_lastResult' => $result
));
}
return $result;
}
/**
* Sets and returns the ID of the last inserted row, or the last value from
* a sequence object, depending on the underlying driver.
*
* @param string $name
* Name of the sequence object from which the ID should be returned.
* @return string
*/
protected function _setLastInsertId
($name = null)
{
return ($this->_lastInsertId
= $this->connection->lastInsertId($name));
}
/**
* Resets the the ID of the last inserted row, or the last value from
* a sequence object, depending on the underlying driver.
*
* @return string
* The default value
*/
protected function _resetLastInsertId
()
{
return ($this->_lastInsertId
= '');
}
/**
* Updates one or more records
*
* @param string|array $tables
* Table name
* @param array $updates
* Associative array of column-value pairs
* @param array|string $where
* Only the records matching this condition are updated
* @return bool
* @see PDOStatement::execute()
*/
public function update
($tables, array $updates, $where = null)
{
if (!$tables)
{
throw new InvalidArgumentException
('No table specified');
}
if (is_array($tables))
{
$tables = implode(', ', $tables);
}
if (!$updates)
{
throw new InvalidArgumentException
('No values specified');
}
$params = array();
if ($this->_isAssociativeArray
($updates))
{
foreach ($updates as $key => $condition)
{
$params[":{$key}"] = $condition;
}
}
$updates = implode(', ', $this->_escapeValueArray
($updates));
/* TODO: Should escape table names with escapeName(), but what about aliases? */
$query = "UPDATE {$tables} SET {$updates}" . $this->_where
($where, '2');
$stmt = $this->prepare($query);
if (is_array($where) && $this->_isAssociativeArray
($where))
{
foreach ($where as $column => $condition)
{
if (is_array($condition) && $this->_isAssociativeArray
($condition))
{
reset($condition);
$condition = $condition[key($condition)];
if (is_array($condition))
{
foreach (self::_expand
($condition, $column) as $param_index => $param_name)
{
$params[$param_name] = $condition[$param_index];
}
}
}
else
{
$params[":{$column}2"] = $condition;
}
}
}
/* DEBUG */
if (defined('DEBUG') && DEBUG
> 1)
{
debug
(array(
'query' => $query,
'params' => $params
));
}
$success =& $this->_lastSuccess
;
$success = $stmt->execute($params);
$errorInfo =& $this->_lastError
;
$errorInfo = $stmt->errorInfo();
$this->_resetLastInsertId
();
$result =& $this->_lastResult
;
$result = $stmt->fetchAll();
if (defined('DEBUG') && DEBUG
> 1)
{
debug
(array(
'_lastSuccess' => $success,
'_lastError' => $errorInfo,
'_lastResult' => $result
));
}
return $success;
}
/**
* Inserts a record into a table.<p>The AUTO_INCREMENT value of the inserted
* row, if any (> 0), is stored in the {@link $lastInsertId} property of
* the <code>Database</code> instance.</p>
*
* @param string $table
* Table name
* @param array|string $values
* Associative array of column-value pairs, indexed array,
* or comma-separated list of values. If <var>$values</var> is not
* an associative array, <var>$cols</var> must be passed if the
* values are not in column order (see below).
* @param array|string $cols
* Indexed array, or comma-separated list of column names.
* Needs only be passed if <var>$values</var> is not an associative array
* and the values are not in column order (default: <code>null</code>);
* is ignored otherwise. <strong>You SHOULD NOT rely on column order.</strong>
* @return bool
* @see PDOStatement::execute()
*/
public function insert
($table, $values, $cols = null)
{
if ($cols != null)
{
$cols = ' ('
. (is_array($cols)
?
implode(', ', array_map(array($this, 'escapeName'), $cols))
: $cols) . ')';
}
else
{
$cols = '';
}
/* DEBUG */
if (defined('DEBUG') && DEBUG
> 2)
{
debug
(array('values' => $values));
}
$params = array();
if (is_array($values))
{
if ($this->_isAssociativeArray
($values))
{
foreach ($values as $key => $condition)
{
$params[":{$key}"] = $condition;
}
$values = $this->_escapeValueArray
($values);
$cols = '';
$values = 'SET ' . implode(', ', $values);
}
else
{
foreach ($values as &$value)
{
if (is_string($value))
{
$value = "'" . $value . "'";
}
}
$values = 'VALUES (' . implode(', ', $values) . ')';
}
}
/* TODO: Should escape table names with escapeName(), but what about aliases? */
$query = "INSERT INTO {$table} {$cols} {$values}";
$stmt = $this->prepare($query);
/* DEBUG */
if (defined('DEBUG') && DEBUG
> 1)
{
debug
(array(
'query' => $query,
'params' => $params
));
}
$success =& $this->_lastSuccess
;
$success = $stmt->execute($params);
$errorInfo =& $this->_lastError
;
$errorInfo = $stmt->errorInfo();
$this->_setLastInsertId
();
$result =& $this->_lastResult
;
$result = $stmt->fetchAll();
if (defined('DEBUG') && DEBUG
> 1)
{
debug
(array(
'_lastSuccess' => $success,
'_lastError' => $errorInfo,
'_lastInsertId' => $this->_lastInsertId
,
'_lastResult' => $result
));
}
return $success;
}
/**
* Retrieves all rows from a table
*
* @param int[optional] $fetch_style
* @param int[optional] $column_index
* @param array[optional] $ctor_args
* @return array
* @see PDOStatement::fetchAll()
*/
public function fetchAll
($table, $fetch_style = null, $column_index = null, array $ctor_args = null)
{
/* NOTE: Cannot use table name as statement parameter */
$stmt = $this->prepare("SELECT * FROM $table");
$this->_lastSuccess
= $stmt->execute();
$this->_lastError
= $stmt->errorInfo();
$result =& $this->_lastResult
;
if (is_null($fetch_style))
{
$fetch_style = \PDO
::FETCH_ASSOC;
}
if (!is_null($ctor_args))
{
$result = $stmt->fetchAll($fetch_style, $column_index, $ctor_args);
}
else if (!is_null($column_index))
{
$result = $stmt->fetchAll($fetch_style, $column_index);
}
else if (!is_null($fetch_style))
{
$result = $stmt->fetchAll($fetch_style);
}
else
{
$result = $stmt->fetchAll();
}
return $result;
}
/**
* Deletes one or more records
*
* @param string|array $tables
* Table name(s)
* @param array|string $where
* Only the records matching this condition are deleted
* @return bool
* @see PDOStatement::execute()
*/
public function delete
($tables, $where = null)
{
if (!$tables)
{
throw new InvalidArgumentException
('No table specified');
}
if (is_array($tables))
{
$tables = implode(', ', $tables);
}
$params = array();
$query = "DELETE FROM {$tables}" . $this->_where
($where);
$stmt = $this->prepare($query);
if ($this->_isAssociativeArray
($where))
{
foreach ($where as $column => $condition)
{
if (is_array($condition) && $this->_isAssociativeArray
($condition))
{
reset($condition);
$condition = $condition[key($condition)];
if (is_array($condition))
{
foreach (self::_expand
($condition, $column) as $param_index => $param_name)
{
$params[$param_name] = $condition[$param_index];
}
}
}
else
{
$params[":{$column}"] = $condition;
}
}
}
/* DEBUG */
if (defined('DEBUG') && DEBUG
> 1)
{
debug
(array(
'query' => $query,
'params' => $params
));
}
$success =& $this->_lastSuccess
;
$success = $stmt->execute($params);
$result =& $this->_lastResult
;
$result = $stmt->fetchAll();
$errorInfo =& $this->_lastError
;
$errorInfo = $stmt->errorInfo();
if (defined('DEBUG') && DEBUG
> 1)
{
debug
(array(
'_lastSuccess' => $success,
'_lastError' => $errorInfo,
'_lastResult' => $result
));
}
return $success;
}
}