Skip to content

Instantly share code, notes, and snippets.

@rseon
Last active February 1, 2022 13:10
Show Gist options
  • Save rseon/59f02a8e0cb8b31f4a147aaf7379205d to your computer and use it in GitHub Desktop.
Save rseon/59f02a8e0cb8b31f4a147aaf7379205d to your computer and use it in GitHub Desktop.
[PHP] Simple database abstraction class (using PDO)
<?php
/**
* Simple MySQL database abstraction class (using PDO).
*
* @method Database::connect(array $config): Database
* @method Database::getInstance(): Database
* @method $Database->setConfig($key, $value): void
* @method $Database->getConfig(): array
* @method $Database->getDBO(): PDO
* @method $Database->printOnly(bool $flag): void
* @method $Database->prefix(string $prefix): void
* @method $Database->readOnly(bool $flag = true): void
* @method $Database->getAll(string $table, array $conditions = [], array $order = [], ?array|int $limit = null, array|string $only_fields = '*'): array
* @method $Database->getRow(string $table, array $conditions = [], array|string $only_fields = null): array
* @method $Database->getValue(string $fieldname, string $table, array $conditions = []): array|bool|mixed
* @method $Database->count(string $table, array $conditions = [], string $fieldsearch = '*'): int|false
* @method $Database->fetchAll(string $sql, array $data = []): array
* @method $Database->fetchRow(string $sql, array $data = []): array
* @method $Database->insert(string $table, array $data): int
* @method $Database->update(string $table, array $data, array $conditions): int
* @method $Database->delete(string $table, array $conditions): int
* @method $Database->routine(string $name, array $params = []): array
* @method $Database->truncate(string $table): bool
* @method $Database->drop(string $table): bool
* @method $Database->create(string $table, array $columns, array $indexes = [], ?string $extra = null): bool
* @method $Database->getLastError(): array
* @method $Database->getLastQuery(bool $raw_query = false): string
* @method $Database->getLastQueryParams(): array
* @method $Database->debug(): array
*/
class Database
{
const ON_ERROR_EXIT = 'exit';
const ON_ERROR_EXCEPTION = 'exception';
const ON_ERROR_CONTINUE = 'continue';
const RAW_QUERY = true;
private static $_instance;
protected $dbo;
protected $config;
protected $last_query = null;
protected $last_query_params = [];
protected $last_error = [];
protected $printOnly = false;
protected $prefix = '';
protected $readonly = false;
// These methods will be disabled if readonly mode is active
protected static $readonly_methods = ['insert', 'update', 'delete', 'truncate', 'drop', 'create'];
/**
* Connect to the database
*
* @example $db = Database::connect([
* 'host' => 'localhost',
* 'username' => 'root',
* 'password' => '',
* 'dbname' => 'my_db',
* 'port' => 3306,
* 'charset' => 'utf8',
* 'on_error' => ON_ERROR_EXIT | ON_ERROR_EXCEPTION | ON_ERROR_CONTINUE, // default ON_ERROR_EXIT
* ]);
*
* Error handling :
* ON_ERROR_EXIT : Display error message and stop script execution.
* ON_ERROR_EXCEPTION : Throw the Exception (stop script execution).
* ON_ERROR_CONTINUE : For debug purpose only, because execution is not stopped.
* Good practice is debugging with methods like getLastError() or
* debug() then stop execution with an `exit` or `die` instruction.
*
* @param array $config array(host, username, password, dbname [, port [, charset [, on_error] ]])
*
* @return Database
*/
public static function connect(array $config)
{
return new static($config);
}
/**
* Returns instance of connection
*
* @example Database::getInstance()->getAll()
*
* @return Database
* @throws Exception
*/
public static function getInstance()
{
if (!(static::$_instance instanceof static)) {
throw new Exception('Please connect first');
}
return static::$_instance;
}
/**
* Set configuration key
*
* @param mixed $value
*/
public function setConfig($key, $value)
{
$this->config[$key] = $value;
}
/**
* Get configuration
*
* @return array
*/
public function getConfig()
{
return $this->config;
}
/**
* Allows you to directly execute methods of the PDO class.
*
* Warning : prefix, readOnly and printOnly methods won't be executed
*
* @example $this->getDBO()->execute()
*
* @return PDO
*
* @link http://php.net/manual/fr/class.pdo.php
*/
public function getDBO()
{
return $this->dbo;
}
/**
* If true, all requests are not executed but printed.
*
* @param bool $flag
*/
public function printOnly(bool $flag = true)
{
$this->printOnly = $flag;
}
/**
* Set a prefix to tables.
*
* The prefix will be automatically added to the table name for the following methods :
* getAll, getRow, getValue, count, insert, update, delete
*
* @param string $prefix
*/
public function prefix(string $prefix)
{
$this->prefix = $prefix;
}
/**
* Set the database as readonly mode (insert, update, delete will not be available)
*
* @param bool $flag
*/
public function readOnly(bool $flag = true)
{
$this->readonly = $flag;
}
/**
* Returns the rows of the $table corresponding to the $conditions, ordered by $order and if needed only the $limit rows.
* You can get $only_fields.
*
* @example $this->getAll(
* 'my_table',
* [
* 'active' => 1, // WHERE `active` = 1
* 'id_user >=' => 1, // AND `id_user` >= 1
* 'name IS NOT NULL', // AND `name` IS NOT NULL
* 'id_client' => [3,4], // AND `id_client` IN (3,4)
* 'id_client NOT IN' => [5,6] // AND `id_client` NOT IN (5,6)
* ],
* ['id_user', 'active DESC'], // ORDER BY `id_user`, `active` DESC
* ['active', 'id_user' => 'user'] // SELECT active, id_user as user
* )
* @example $limit is an integer : returns the $limit first rows
* $this->getAll('my_table', [], [], 10) // LIMIT 10
* @example $limit is an array [$nb] : returns the $nb first rows
* $this->getAll('my_table', [], [], [10]) // LIMIT 10
* @example $limit is an array [$nb, $offset] : returns the $nb rows from the $offset'th
* $this->getAll('my_table', [], [], [10, 20]) // LIMIT 10 OFFSET 20
* @example $only_fields is a string
* $this->getAll('my_table', [], [], null, 'active, id_user as user') // SELECT active, id_user as user
* @example $only_fields is an array
* $this->getAll('my_table', [], [], null, ['active', 'id_user' => 'user']) // SELECT active, id_user as user
*
* @param string $table
* @param array $conditions
* @param array $order
* @param array|int $limit
* @param array|string $only_fields
*
* @return array
* @throws Exception
*/
public function getAll(string $table, array $conditions = [], array $order = [], $limit = null, $only_fields = '*')
{
$_fields = '*';
if (!empty($only_fields)) {
if (is_array($only_fields)) {
$_array_fields = [];
foreach ($only_fields as $k => $v) {
if (is_numeric($k)) {
$_array_fields[] = $v;
}
else {
$_array_fields[] = "{$k} as {$v}";
}
}
$only_fields = $_array_fields;
$_fields = implode(', ', $only_fields);
}
else {
$_fields = $only_fields;
}
}
$sql = "SELECT {$_fields} FROM `{$this->prefix}{$table}`";
$sql .= static::createWhere($conditions);
if ($order) {
$_order = implode(', ', array_map(function($v) {
if (strpos($v, ' ') !== false) {
list($a, $b) = explode(' ', $v);
return "`{$a}` {$b}";
}
return "`{$v}`";
}, $order));
$sql .= " ORDER BY {$_order}";
}
if ($limit) {
$limit_nb = null;
$limit_offset = 0;
if (is_numeric($limit)) {
$limit_nb = (int)$limit;
}
elseif (is_array($limit)) {
if (isset($limit[0])) {
$limit_nb = (int)$limit[0];
}
if (isset($limit[1])) {
$limit_offset = (int)$limit[1];
}
}
if ($limit_nb) {
$sql .= " LIMIT {$limit_nb} OFFSET {$limit_offset}";
}
}
if ($this->printOnly) {
return $this->printQuery($sql, $conditions);
}
$this->resetLastQuery();
$stmt = $this->dbo->prepare($sql);
$this->bindValues($stmt, $conditions);
$this->setLastQuery($stmt);
try {
$stmt->execute();
return $stmt->fetchAll();
} catch(\PDOException $e) {
$this->catchAndLaunchError($e, $stmt);
}
return [];
}
/**
* Returns one row of the $table corresponding to the $conditions
*
* @param string $table
* @param array $conditions
* @param array|string $only_fields
*
* @return array
* @throws Exception
*
* @see Database::getAll()
*/
public function getRow(string $table, array $conditions = [], $only_fields = null)
{
$res = $this->getAll($table, $conditions, [], 1, $only_fields);
if ($this->printOnly) {
return $res;
}
if (isset($res[0])) {
return $res[0];
}
return [];
}
/**
* Get the value of field $fieldname (false if unknown).
* If $fieldname is an array, returns an array of the values.
*
* @example $this->getValue('active', 'my_table', ['id_user' => 1]); // => string "1"
* @example $this->getValue(['id_user', 'active'], 'my_table', ['id_user' => 1]); // => array ['id_user' => 1, 'active' => 1];
* @example $this->getValue(['MAX(id_user) as nb', 'active'], 'my_table'); // => array ['nb' => 4, 'active' => 1];
*
* @param string|array $fieldname
* @param string $table
* @param array $conditions
*
* @return array|bool|mixed
* @throws Exception
*
* @see Database::getRow()
*/
public function getValue($fieldname, string $table, array $conditions = [])
{
$res = $this->getRow($table, $conditions, $fieldname);
if ($this->printOnly) {
return $res;
}
if (!$res) {
return false;
}
if (is_array($fieldname)) {
$returns = [];
foreach ($fieldname as $f) {
if (strpos(strtolower($f), ' as ') !== false) {
$f = explode(' as ', strtolower($f))[1];
}
if (isset($res[$f])) {
$returns[$f] = $res[$f];
}
else {
$returns[$f] = false;
}
}
if (count($returns) === 1) {
return array_values($returns)[0];
}
return $returns;
}
if (strpos(strtolower($fieldname), ' as ') !== false) {
$fieldname = explode(' as ', strtolower($fieldname))[1];
}
if (isset($res[$fieldname])) {
return $res[$fieldname];
}
return false;
}
/**
* Performs a COUNT request and return its result
*
* @example $this->count('my_table', ['active' => 1]); // => int 12
* @example $this->count('my_table', ['active' => 1], 'primary_key'); // => int 12
*
* @param string $table
* @param array $conditions
* @param string $fieldsearch
*
* @return int|false
* @throws Exception
*
* @see Database::getRow()
*/
public function count(string $table, array $conditions = [], string $fieldsearch = '*')
{
$fieldname = 'count';
$res = $this->getRow($table, $conditions, "COUNT({$fieldsearch}) as {$fieldname}");
if ($this->printOnly) {
return $res;
}
if (isset($res[$fieldname])) {
return (int) $res[$fieldname];
}
return false;
}
/**
* Returns all rows of the query $sql.
* For prepared statements you must fill in the array of values $data.
*
* @example $this->fetchAll('SELECT * FROM user')
* @example $this->fetchAll('SELECT * FROM user WHERE id_user = :id_user AND active = 1', ['id_user' => 1])
*
* @param string $sql
* @param array $data
*
* @return array
* @throws Exception
*/
public function fetchAll(string $sql, array $data = [])
{
// Check not allowed keywords
// @link https://dev.mysql.com/doc/refman/8.0/en/sql-statements.html
$not_allowed = [
'alter', 'change', 'create', 'deallocate', 'delete', 'describe', 'do', 'drop', 'execute', 'explain', 'handler', 'help', 'import', 'insert', 'load', 'lock', 'prepare', 'purge', 'replace', 'rename', 'reset', 'savepoint', 'set', 'show', 'start', 'stop', 'truncate', 'update', 'use', 'xa',
];
$sql_clean = strtolower($sql);
$sql_clean = preg_replace('/\s+/', ' ', $sql_clean); // Remove multiple whitespaces
foreach ($not_allowed as $not) {
if (strpos($sql_clean, $not.' ') !== false) {
$this->launchError('Not allowed keyword in this method ('.$not.')');
exit;
}
}
if ($this->printOnly) {
return $this->printQuery($sql, $data);
}
$this->resetLastQuery();
$stmt = $this->dbo->prepare($sql);
$this->bindValues($stmt, $data);
$this->setLastQuery($stmt);
try {
$stmt->execute();
return $stmt->fetchAll();
} catch(\PDOException $e) {
$this->catchAndLaunchError($e, $stmt);
}
return [];
}
/**
* Returns one row of the query $sql.
* For prepared statements you must fill in the array of values $data.
*
* @param $sql
* @param array $data
*
* @return array
* @throws Exception
*
* @see Database::fetchAll()
*/
public function fetchRow(string $sql, array $data = [])
{
$res = $this->fetchAll($sql, $data);
if ($this->printOnly) {
return $res;
}
if (isset($res[0])) {
return $res[0];
}
return [];
}
/**
* Insertion of $data in the $table and returns number of insertions.
* For multiple insertions, you must fill in an array of associative arrays with all the same keys.
*
* @example $this->insert('my_table', ['name' => 'foo', 'active' => 1])
* @example $this->insert('my_table', [['id' => 1, 'name' => 'foo'],['id' => 2, 'name' => 'bar']])
*
* @param string $table
* @param array $data
*
* @return int
* @throws Exception
*/
public function insert(string $table, array $data)
{
$keys = array_keys($data);
// Bulk insert
$bulk = false;
if (is_numeric($keys[0]) && is_array($data[0])) {
$bulk = true;
$base_keys = array_keys(array_values($data)[0]);
$_keys = implode(', ', array_map(function($v) { return "`{$v}`"; }, $base_keys));
$sql = "INSERT IGNORE INTO `{$this->prefix}{$table}` ({$_keys}) VALUES ";
$values = [];
$exec_values = [];
foreach ($data as $row) {
$_values = implode(', ', array_map(function($v) { return '?'; }, $row));
$values[] = "({$_values})";
$exec_values = array_merge($exec_values, array_values($row));
}
$sql .= implode(', ', $values);
}
else {
// Regular insert
$_keys = implode(', ', array_map(function($v) { return "`{$v}`"; }, $keys));
$_values = implode(', ', array_map(function($v) { return ":{$v}"; }, $keys));
$sql = "INSERT INTO `{$this->prefix}{$table}` ({$_keys}) VALUES ({$_values})";
}
if ($this->printOnly) {
if ($bulk) {
return $this->printQueryWithPlaceholders($sql, $exec_values);
}
return $this->printQuery($sql, $data);
}
$this->checkIsReadonly('insert');
$this->resetLastQuery();
$stmt = $this->dbo->prepare($sql);
$this->setLastQuery($stmt);
try {
$this->dbo->beginTransaction();
// Bulk insert
if ($bulk) {
$stmt->execute($exec_values);
}
else {
// Regular insert
$this->bindValues($stmt, $data);
$stmt->execute();
}
$id = $this->dbo->lastInsertId();
$this->dbo->commit();
return (int) $id;
} catch(\PDOException $e) {
$this->dbo->rollback();
$this->catchAndLaunchError($e, $stmt);
}
return 0;
}
/**
* Update $table with $data corresponding to the $conditions and returns number of updates
*
* @example $this->update('my_table', ['name' => 'Test'], ['id_user' => 1])
*
* @param string $table
* @param array $data
* @param array $conditions
*
* @return int
* @throws Exception
*/
public function update(string $table, array $data, array $conditions)
{
$sql = "UPDATE `{$this->prefix}{$table}` SET ";
foreach ($data as $field => $value) {
$sql .= "`{$field}` = :{$field}, ";
}
$sql = substr($sql, 0, -2);
$sql .= static::createWhere($conditions);
if ($this->printOnly) {
return $this->printQuery($sql, $data, $conditions);
}
$this->checkIsReadonly('update');
$this->resetLastQuery();
$stmt = $this->dbo->prepare($sql);
$this->setLastQuery($stmt);
try {
$this->bindValues($stmt, $data);
$this->bindValues($stmt, $conditions);
$this->dbo->beginTransaction();
$stmt->execute();
$nb_rows = $stmt->rowCount();
$this->dbo->commit();
return (int) $nb_rows;
} catch(\PDOException $e) {
$this->dbo->rollback();
$this->catchAndLaunchError($e, $stmt);
}
return 0;
}
/**
* Delete from $table corresponding to the $conditions and returns number of deletions
*
* @example $this->delete('my_table', ['id_user' => 1])
*
* @param string $table
* @param array $conditions
*
* @return int
* @throws Exception
*/
public function delete(string $table, array $conditions)
{
$sql = "DELETE FROM `{$this->prefix}{$table}`";
$sql .= static::createWhere($conditions);
if ($this->printOnly) {
return $this->printQuery($sql, $conditions);
}
$this->checkIsReadonly('delete');
$this->resetLastQuery();
$stmt = $this->dbo->prepare($sql);
$this->setLastQuery($stmt);
try {
$this->bindValues($stmt, $conditions);
$this->dbo->beginTransaction();
$stmt->execute();
$nb_rows = $stmt->rowCount();
$this->dbo->commit();
return (int) $nb_rows;
} catch(\PDOException $e) {
$this->dbo->rollback();
$this->catchAndLaunchError($e, $stmt);
}
return 0;
}
/**
* Call the routine $name with parameters $params
*
* @example $this->routine('My_Routine', ['foo', 'bar'])
*
* @param string $name
* @param array $params
*
* @return array
* @throws Exception
*/
public function routine(string $name, array $params = [])
{
$_params = implode(', ', array_fill(0, count($params), '?'));
$query = "CALL `{$name}`({$_params});";
if ($this->printOnly) {
return $this->printQueryWithPlaceholders($query, $params);
}
$this->checkIsReadonly('routine');
$this->resetLastQuery();
$stmt = $this->dbo->prepare($query);
$this->setLastQuery($stmt);
try {
$i = 0;
foreach ($params as $p) {
$stmt->bindValue(++$i, $p, PDO::PARAM_STR);
$this->setLastQueryParams($i, $p);
}
$stmt->execute();
return $stmt->fetchAll();
}
catch (Exception $e) {
$this->catchAndLaunchError($e, $stmt);
}
return [];
}
/**
* Truncate a table.
* Warning : you will lose all your data.
*
* @example $this->truncate('my_table')
*
* @param string $table
*
* @return bool
* @throws Exception
*/
public function truncate(string $table)
{
$sql = "TRUNCATE `{$this->prefix}{$table}`";
if ($this->printOnly) {
return $this->printQuery($sql);
}
$this->checkIsReadonly('truncate');
$this->resetLastQuery();
$stmt = $this->dbo->prepare($sql);
$this->setLastQuery($stmt);
try {
$this->dbo->beginTransaction();
$stmt->execute();
$this->dbo->commit();
return true;
} catch(\PDOException $e) {
$this->dbo->rollback();
$this->catchAndLaunchError($e, $stmt);
}
return false;
}
/**
* Drop a table.
* Warning : you will lose all your data.
*
* @example $this->drop('my_table')
*
* @param string $table
*
* @return bool
* @throws Exception
*/
public function drop(string $table)
{
$sql = "DROP TABLE `{$this->prefix}{$table}`";
if ($this->printOnly) {
return $this->printQuery($sql);
}
$this->checkIsReadonly('drop');
$this->resetLastQuery();
$stmt = $this->dbo->prepare($sql);
$this->setLastQuery($stmt);
try {
$this->dbo->beginTransaction();
$stmt->execute();
$this->dbo->commit();
return true;
} catch(\PDOException $e) {
$this->dbo->rollback();
$this->catchAndLaunchError($e, $stmt);
}
return false;
}
/**
* Create a table.
*
* @example $this->create('my_table', [
* 'id_test' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT',
* 'name' => 'VARCHAR(168) NOT NULL',
* 'description' => 'TEXT NULL',
* 'active' => 'TINYINT(1) NOT NULL DEFAULT 1',
* ], [
* 'PRIMARY KEY (`id_test`)'
* ], 'ENGINE = MyISAM');
*
* @param string $table
* @param array $columns
* @param array $indexes
* @param string $extra
*
* @return bool
* @throws Exception
*/
public function create(string $table, array $columns, array $indexes = [], string $extra = null)
{
$sql = "CREATE TABLE `{$this->prefix}{$table}` (";
$cols = [];
foreach ($columns as $name => $attributes) {
$cols[] = "`{$name}` {$attributes}";
}
$cols = array_merge($cols, $indexes);
$sql .= implode(', ', $cols) . ') ' . $extra;
if ($this->printOnly) {
return $this->printQuery($sql);
}
$this->checkIsReadonly('create');
$this->resetLastQuery();
$stmt = $this->dbo->prepare($sql);
$this->setLastQuery($stmt);
try {
$this->dbo->beginTransaction();
$stmt->execute();
$this->dbo->commit();
return true;
} catch(\PDOException $e) {
$this->dbo->rollback();
$this->catchAndLaunchError($e, $stmt);
}
return false;
}
/**
* Get last PDO error
*
* @return array
*/
public function getLastError()
{
return $this->last_error;
}
/**
* Get last query.
* If $raw_query, query parameters are replaced by their values.
*
* @param bool $raw_query
*
* @return string
*/
public function getLastQuery(bool $raw_query = false)
{
$query = $this->last_query;
if ($raw_query === static::RAW_QUERY) {
return $query;
}
foreach ($this->last_query_params as $field => $value) {
if (is_array($value)) {
continue;
}
$query = str_replace(":{$field}", "'{$value}'", $query);
}
if (strpos($query, '?') !== false) {
}
return $query;
}
/**
* Get parameters of last prepared query
*
* @return array
*/
public function getLastQueryParams()
{
return $this->last_query_params;
}
/**
* Returns an array for debugging with last error, raw query, query with parameters and parameters
*
* @return array
*/
public function debug()
{
return [
'error' => $this->getLastError(),
'raw_query' => $this->getLastQuery(static::RAW_QUERY),
'query' => $this->getLastQuery(),
'query_params' => $this->getLastQueryParams(),
];
}
/**
* Database constructor
*
* @param array $config
*
* @throws Exception
*/
protected function __construct(array $config)
{
$required = ['host','dbname','username','password'];
foreach ($required as $r) {
if (!isset($config[$r])) {
throw new Exception('Please set required configuration fields : ' . implode(', ', $required));
}
}
if (!array_key_exists('on_error', $config)) {
$config['on_error'] = static::ON_ERROR_EXIT;
}
$this->config = $config;
$pdoparams = [];
if (isset($config['charset'])) {
$pdoparams[PDO::MYSQL_ATTR_INIT_COMMAND] = "SET NAMES '{$config['charset']}'";
}
$dsn = "mysql:host={$config['host']};dbname={$config['dbname']}";
if (isset($config['port'])) {
$dsn .= ";port={$config['port']}";
}
try {
$this->dbo = new PDO($dsn, $config['username'], $config['password'], $pdoparams);
$this->dbo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$this->dbo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
}
catch(Exception $e) {
exit($e->getMessage().PHP_EOL);
}
static::$_instance = $this;
return $this;
}
/**
* Returns the type of parameter from value for prepared statement
*
* @param mixed $value
*
* @return bool|int
*/
protected static function getParamType($value)
{
if (is_int($value)) {
return \PDO::PARAM_INT;
}
if (is_bool($value)) {
return \PDO::PARAM_BOOL;
}
if (is_null($value)) {
return \PDO::PARAM_NULL;
}
if (is_string($value)) {
return \PDO::PARAM_STR;
}
return false;
}
/**
* Returns an array filled with the field and the operator for a WHERE clause
*
* @param string $value
*
* @return array
*
* @see Database::getAll()
*/
protected static function splitOperator(string $value)
{
$operator = '=';
if (strpos($value, ' ') !== false) {
$tmp = explode(' ', $value);
$value = array_shift($tmp);
$operator = implode(' ', $tmp);
unset($tmp);
}
return [$value, $operator];
}
/**
* Create the WHERE conditions.
*
* @param array $conditions
*
* @return string
*/
protected static function createWhere(array $conditions = [])
{
if (empty($conditions)) {
return '';
}
$where = ' WHERE ';
foreach ($conditions as $field => $value) {
$_where = '';
if (is_array($value)) {
if (!empty($value)) {
list($field, $operator) = static::splitOperator($field);
if ($operator === '=') {
$operator = 'IN';
}
$in_values = implode(', ', array_map(function($v){
return is_numeric($v) ? $v : "'{$v}'";
}, $value));
$_where = "`{$field}` {$operator} ({$in_values})";
}
}
else {
if (is_numeric($field)) {
list($value, $operator) = static::splitOperator($value);
$_where = "`{$value}` {$operator}";
unset($conditions[$field]);
}
else {
list($field, $operator) = static::splitOperator($field);
$_where = "`{$field}` {$operator} :{$field}";
}
}
if ($_where) {
$where .= "{$_where} AND ";
}
}
$where = substr($where, 0, -5);
return $where;
}
/**
* Bind the values for prepared queries.
*
* @param PDOStatement $stmt
* @param array $conditions
*/
protected function bindValues($stmt, array $conditions = [])
{
if (empty($conditions)) {
return;
}
foreach ($conditions as $field => $value) {
if (!is_array($value)) {
list($field, $operator) = static::splitOperator($field);
$stmt->bindValue(":{$field}", $value, static::getParamType($value));
$this->setLastQueryParams($field, $value);
}
}
}
/**
* Set last PDO error
*
* @param array $errorInfo
*/
protected function setLastError(array $errorInfo)
{
$this->last_error = $errorInfo;
}
/**
* Reset query debugging
*/
protected function resetLastQuery()
{
$this->last_query = null;
$this->last_query_params = [];
$this->last_error = [];
}
/**
* Set parameters of last query
*
* @param mixed $field
* @param mixed $value
*/
protected function setLastQueryParams($field, $value)
{
$this->last_query_params[$field] = $value;
}
/**
* Set last query and last error
*
* @param PDOStatement $stmt
*/
protected function setLastQuery($stmt)
{
$this->last_query = trim($stmt->queryString);
$this->last_error = $stmt->errorInfo();
}
/**
* Replace placeholders in query with values in params.
*
* @param string $query
* @param array $params
*
* @return string
*/
protected function printQuery(string $query, ...$params)
{
if (!empty($params)) {
array_map(function($params) use (&$query) {
$this->printQueryWalk($query, $params);
}, $params);
}
return $query;
}
/**
* Update the query with values
*
* @param string $query
* @param array $params
*/
protected function printQueryWalk(string &$query, array $params = [])
{
foreach ($params as $field => $value) {
if (is_array($value)) {
$this->printQueryWalk($query, $value);
}
else {
list($field, $operator) = static::splitOperator($field);
if (is_string($value)) {
$value = $this->dbo->quote($value, static::getParamType($value));
}
$query = str_replace(":{$field}", $value, $query);
}
}
}
/**
* Similar to the printQuery method but for query with '?' as placeholders.
*
* @param string $query
* @param array $params
*
* @return string
*/
protected function printQueryWithPlaceholders(string $query, array $params)
{
return $this->printQuery(vsprintf(str_replace("?", "%s", $query), array_map(function($v) {
return is_string($v) ? "'{$v}'" : $v;
}, $params)));
}
/**
* Catch error and stop execution
*
* @param PDOException
* @param PDOStatement
*
* @throws Exception
*/
protected function catchAndLaunchError($error, $statement)
{
$this->setLastError($statement->errorInfo());
$this->launchError($error->getMessage());
}
/**
* Stop the script execution with a message (exit or throw an exception, as decided for the error handling).
*
* @param string $message
*
* @throws Exception
*/
protected function launchError(string $message)
{
if ($this->config['on_error'] === static::ON_ERROR_EXCEPTION) {
throw new Exception($message);
}
print($message.PHP_EOL);
if ($this->config['on_error'] === static::ON_ERROR_EXIT) {
exit(1);
}
}
/**
* Check if method can be called in readOnly mode
*
* @throws Exception
*/
protected function checkIsReadonly(string $method)
{
if ($this->readonly !== false && in_array($method, static::$readonly_methods)) {
$this->launchError("Error: Method $method is not available (database is set as readonly mode).");
exit;
}
}
}
<?php
require_once 'Database.php';
/**
* 1.2 : Connect to the database.
* Then use $DB or Database::getInstance()
*/
$DB = Database::connect([
'host' => 'localhost',
'username' => 'root',
'password' => '',
'dbname' => 'my_db',
'port' => 3306,
'charset' => 'utf8',
]);
// Use PDO methods
$stmt = $DB->getDBO()->prepare('DROP TABLE IF EXISTS my_table');
$stmt->execute();
// 1.4 : Only print next requests without execute them
$DB->printOnly(true);
/*
SELECT COUNT(*) as nb, name
FROM `my_table`
WHERE `active` = 1 AND `id_user` >= 4 AND `name` IS NOT NULL AND `id_client` IN (3, 4) AND `id_client` NOT IN (5, 6)
ORDER BY `id_user`, `active` DESC
LIMIT 10 OFFSET 20
*/
$res = $DB->getAll('my_table', [
'active' => 1,
'id_user >=' => 4,
'name IS NOT NULL',
'id_client' => [3,4],
'id_client NOT IN' => [5,6],
], ['id_user', 'active DESC'], [10,20], ['COUNT(*)' => 'nb', 'name']);
// SELECT name FROM `my_table` WHERE `id_user` = 4 LIMIT 1 OFFSET 0
$res = $DB->getRow('my_table', ['id_user' => 4], 'name');
// Value
$res = $DB->getValue('active', 'my_table', ['id_user' => 1]); // SELECT active FROM `my_table` WHERE `id_user` = 1 LIMIT 1 OFFSET 0
$res = $DB->getValue(['id_user', 'active'], 'my_table', ['id_user' => 1]); // SELECT id_user, active FROM `my_table` WHERE `id_user` = 1 LIMIT 1 OFFSET 0
// Multiple rows
$res = $DB->fetchAll('SELECT * FROM my_table'); // return an array of arrays of datas
$res = $DB->fetchAll('SELECT * FROM my_table WHERE id_user > :id_user', ['id_user' => 1]); // return an array of arrays of datas
// One row
$res = $DB->fetchRow('SELECT * FROM my_table'); // return an array of datas
$res = $DB->fetchRow('SELECT * FROM my_table WHERE id_user = :id_user', ['id_user' => 1]); // return an array of datas
// Insert datas
$res = $DB->insert('my_table', ['name' => 'foo', 'active' => 1]); // INSERT INTO `my_table` (`name`, `active`) VALUES ('Test', '1')
$res = $DB->insert('my_table', [['id' => 1, 'name' => 'foo'], ['id' => 2, 'name' => 'bar']]); // INSERT IGNORE INTO `my_table` (`id`, `name`) VALUES ('1', 'foo'), ('2', 'bar');
// Update datas
$res = $DB->update('my_table', ['name' => 'foo'], ['id_user' => 1]); // UPDATE `my_table` SET `name` = 'foo' WHERE `id_user` = 1;
// Delete datas
$res = $DB->delete('my_table', ['id_user' => 1]); // DELETE FROM `my_table` WHERE `id_user` = 1
// 1.1 : Call routine
$res = $DB->routine('my_routine', ['foo', 'bar']); // CALL `my_routine`('foo', 'bar');
// 1.4 : Count results
$res = $DB->count('my_table', ['active' => 1]); // => int 12
// 1.5 : Add prefix
$DB->prefix('ps_');
$res = $DB->insert('my_table', ['name' => 'foo']); // INSERT INTO `ps_my_table` (`name`) VALUES ('Test')
// 1.5 : Readonly mode : will exit (or throw an exception) without execute the request
$DB->readonly();
$res = $DB->insert('my_table', ['name' => 'foo']);
// 1.6 : Truncate table
$DB->truncate('my_table'); // TRUNCATE my_table
// 1.6 : Drop table
$DB->drop('my_table'); // DROP TABLE IF EXISTS my_table
// 1.6 : Create table
/*
CREATE TABLE `my_table` (
`id_test` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`name` VARCHAR(168) NOT NULL,
`description` TEXT NULL,
`active` TINYINT(1) NOT NULL DEFAULT 1,
PRIMARY KEY (`id_test`),
) ENGINE = MyISAM
*/
$DB->create('my_table', [
'id_test' => 'INT UNSIGNED NOT NULL AUTO_INCREMENT',
'name' => 'VARCHAR(168) NOT NULL',
'description' => 'TEXT NULL',
'active' => 'TINYINT(1) NOT NULL DEFAULT 1',
], [
'PRIMARY KEY (`id_test`)'
], 'ENGINE = MyISAM');
//
// Error handling
//
// Exit with error message (default)
$DB->setConfig('on_error', Database::ON_ERROR_CONTINUE);
// Throw exception
$DB->setConfig('on_error', Database::ON_ERROR_EXCEPTION);
// Continue execution. Useful for debugging but can be dangerous.
// Stop execution with `die` or `exit` instruction just after debug.
$DB->setConfig('on_error', Database::ON_ERROR_CONTINUE);
//var_dump($DB->debug());
//die;
//
// Debugging
//
// Get last error
var_dump($DB->getLastError());
/*
array(3) {
[0]=>
string(5) "42S02"
[1]=>
int(1146)
[2]=>
string(45) "Table 'my_db.my_table' does not exist"
}
*/
// Get last query with binded parameters
var_dump($DB->getLastQuery());
// string(77) "SELECT name FROM `my_table` WHERE 1 AND `id_user` = '4' LIMIT 1 OFFSET 0"
// Get last raw query
var_dump($DB->getLastQuery(Database::RAW_QUERY));
// string(77) "SELECT name FROM `my_table` WHERE 1 AND `id_user` = :id_user LIMIT 1 OFFSET 0"
// Get last query parameters
var_dump($DB->getLastQueryParams());
/*
array(1) {
["id_user"]=>
int(4)
}
*/
// Alias to debug all previous debug methods
var_dump($DB->debug());
/*
array(4) {
["error"]=>
array(3) {
[0]=>
string(5) "42S02"
[1]=>
int(1146)
[2]=>
string(40) "Table 'my_db.my_table' doesn't exist"
}
["raw_query"]=>
string(77) "SELECT name FROM `my_table` WHERE 1 AND `id_user` = :id_user LIMIT 1 OFFSET 0"
["query"]=>
string(72) "SELECT name FROM `my_table` WHERE 1 AND `id_user` = '4' LIMIT 1 OFFSET 0"
["query_params"]=>
array(1) {
["id_user"]=>
int(4)
}
}
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment