572 lines
19 KiB
PHP
572 lines
19 KiB
PHP
<?php
|
|
|
|
namespace EesyPHP;
|
|
|
|
use Exception;
|
|
use PDO;
|
|
use \Envms\FluentPDO\Query;
|
|
|
|
class Db {
|
|
|
|
/**
|
|
* Configuration prefix
|
|
* @var string
|
|
*/
|
|
protected static $config_prefix = 'db';
|
|
|
|
/**
|
|
* The PDO object of the database connection
|
|
* @var PDO|null
|
|
*/
|
|
public static $pdo = null;
|
|
|
|
/**
|
|
* The PDO object of the database connection
|
|
* @var \Envms\FluentPDO\Query|null
|
|
*/
|
|
public static $fpdo = null;
|
|
|
|
/**
|
|
* Keep trace of total queries times (in ns)
|
|
* @var int;
|
|
*/
|
|
public static $total_query_time = 0;
|
|
|
|
/**
|
|
* Initialization
|
|
* @return void
|
|
*/
|
|
public static function init() {
|
|
// In phpstan context, do not initialize
|
|
// @phpstan-ignore-next-line
|
|
if (defined('__PHPSTAN_RUNNING__') && constant('__PHPSTAN_RUNNING__'))
|
|
return;
|
|
|
|
// Set config default values
|
|
App :: set_default(
|
|
static :: $config_prefix,
|
|
array(
|
|
'dsn' => null,
|
|
'user' => null,
|
|
'password' => null,
|
|
'options' => [],
|
|
'date_format' => '%Y-%m-%d',
|
|
'datetime_format' => '%Y-%m-%d %H:%M:%S',
|
|
'locale_time' => null,
|
|
'auto_connect' => true,
|
|
),
|
|
);
|
|
|
|
if (!App::get(static :: $config_prefix.".dsn"))
|
|
Log :: fatal('Database DSN not configured (%s.dsn)', static :: $config_prefix);
|
|
|
|
if (App::get(static :: $config_prefix.".auto_connect"))
|
|
self :: connect();
|
|
|
|
}
|
|
|
|
/**
|
|
* Connection
|
|
* @return void
|
|
*/
|
|
public static function connect() {
|
|
if (self :: $pdo && self :: $fpdo)
|
|
return;
|
|
$dsn = App::get(static :: $config_prefix.".dsn");
|
|
try {
|
|
// Connect to database
|
|
self :: $pdo = new PDO(
|
|
$dsn,
|
|
App::get(static :: $config_prefix.".user"),
|
|
App::get(static :: $config_prefix.".password"),
|
|
App::get(static :: $config_prefix.".options"),
|
|
);
|
|
self :: $fpdo = new Query(self :: $pdo);
|
|
|
|
// Register the debug query handler to log it
|
|
self :: $fpdo -> debug = array(self :: class, 'debug_query');
|
|
|
|
Log :: trace("DB connection established (DSN: '%s')", $dsn);
|
|
}
|
|
catch(Exception $e) {
|
|
Log :: error("Fail to connect to DB (DSN : '%s') : %s", $dsn, $e->getMessage());
|
|
Log :: fatal(I18n::_('Unable to connect to the database.'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Debug a query
|
|
* @param \Envms\FluentPDO\Queries\Base $q
|
|
* @return void
|
|
*/
|
|
public static function debug_query($q) {
|
|
$error = $q->getMessage();
|
|
// Execution time not available in case of execution error
|
|
if (!$error)
|
|
self :: $total_query_time += intval(ceil($q->getExecutionTime() * 1000000000));
|
|
$msg = "# DB query";
|
|
if ($q->getResult())
|
|
$msg .= sprintf(
|
|
" (%0.3f ms; rows = %d)",
|
|
$q->getExecutionTime() * 1000,
|
|
$q->getResult()->rowCount()
|
|
);
|
|
$msg .= ": ".$q->getQuery();
|
|
|
|
$parameters = $q->getParameters();
|
|
if ($parameters) {
|
|
if (is_array($parameters)) {
|
|
$msg .= "\n# Parameters: '" . implode("', '", $parameters) . "'";
|
|
}
|
|
else { // @phpstan-ignore-line
|
|
$msg .= "\n# Parameters: '" . vardump($parameters) . "'";
|
|
}
|
|
}
|
|
if ($error)
|
|
$msg .= "\n# ERROR: $error";
|
|
|
|
Log :: debug($msg);
|
|
}
|
|
|
|
/**
|
|
* Set autocommit (only available on OCI, Firebird or MySQL connection)
|
|
* @param bool $value
|
|
* @see https://www.php.net/manual/en/pdo.setattribute.php
|
|
* @return void
|
|
*/
|
|
public static function set_autocommit($value) {
|
|
self :: $pdo -> setAttribute(PDO::ATTR_AUTOCOMMIT, $value);
|
|
}
|
|
|
|
/*
|
|
* Simple request helpers
|
|
*/
|
|
|
|
/**
|
|
* Helper to retrieve one row from a table of the database
|
|
* @param string $table The table name
|
|
* @param array|string $where WHERE clause(s) as expected by Envms\FluentPDO\Query
|
|
* @param array|string|null $fields The expected fields as string (separated by comma) or an
|
|
* array (optional, default: all table fields will be returned)
|
|
* @param array<array{0: 'LEFT'|'RIGHT'|'INNER'|'OUTER'|'FULL', 1: string, 2: string}>|array{0: 'LEFT'|'RIGHT'|'INNER'|'OUTER'|'FULL', 1: string, 2: string} $joins Join specification as array (see apply_joins())
|
|
* @return array|false
|
|
*/
|
|
public static function get_one($table, $where, $fields=null, $joins=null) {
|
|
try {
|
|
$query = self :: $fpdo -> from($table) -> where($where);
|
|
if ($joins)
|
|
self :: apply_joins($query, $joins);
|
|
if ($fields)
|
|
$query -> select(null) -> select($fields);
|
|
if ($query->execute() === false)
|
|
return false;
|
|
$return = $query->fetchAll();
|
|
if (is_array($return) && count($return) == 1)
|
|
return $return[0];
|
|
}
|
|
catch (Exception $e) {
|
|
self :: _log_simple_select_query_error(
|
|
false, $table, $e, $where, $fields, null, null, $joins
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Helper to retrieve multiple rows from a table of the database
|
|
* @param string $table The table name
|
|
* @param array|string $where WHERE clause(s) as expected by Envms\FluentPDO\Query
|
|
* @param array|string|null $fields The expected fields as string (separated by comma) or an
|
|
* array (optional, default: all table fields will be returned)
|
|
* @param string|null $order_by An optional ORDER clause as a string
|
|
* @param string|null $limit An optional LIMIT clause as a string
|
|
* @param array<array{0: 'LEFT'|'RIGHT'|'INNER'|'OUTER'|'FULL', 1: string, 2: string}>|array{0: 'LEFT'|'RIGHT'|'INNER'|'OUTER'|'FULL', 1: string, 2: string} $joins Join specification as array (see apply_joins())
|
|
* @return array|false
|
|
*/
|
|
public static function get_many(
|
|
$table, $where=null, $fields=null, $order_by=null, $limit=null, $joins=null
|
|
) {
|
|
try {
|
|
$query = self :: $fpdo -> from($table);
|
|
if ($joins)
|
|
self :: apply_joins($query, $joins);
|
|
if ($fields)
|
|
$query -> select(null) -> select($fields);
|
|
if ($where)
|
|
$query -> where($where);
|
|
if ($order_by)
|
|
$query -> orderBy($order_by);
|
|
if ($query->execute() === false)
|
|
return false;
|
|
$return = $query->fetchAll();
|
|
if (is_array($return))
|
|
return $return;
|
|
}
|
|
catch (Exception $e) {
|
|
self :: _log_simple_select_query_error(
|
|
false, $table, $e, $where, $fields, $order_by, $limit, $joins
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Helper to retrieve a count from a table of the database
|
|
* @param string $table The table name
|
|
* @param array|string $where WHERE clause(s) as expected by Envms\FluentPDO\Query
|
|
* @param array|string|null $what What to count (optional, default: "*")
|
|
* @param array<array{0: 'LEFT'|'RIGHT'|'INNER'|'OUTER'|'FULL', 1: string, 2: string}>|array{0: 'LEFT'|'RIGHT'|'INNER'|'OUTER'|'FULL', 1: string, 2: string} $joins Join specification as array (see apply_joins())
|
|
* @return int|false
|
|
*/
|
|
public static function count($table, $where, $what=null, $joins=null) {
|
|
try {
|
|
$query = self :: $fpdo -> from($table) -> where($where);
|
|
if ($joins)
|
|
self :: apply_joins($query, $joins);
|
|
$query -> select(null) -> select(sprintf("COUNT(%s) as count", $what?$what:"*"));
|
|
if ($query->execute() === false)
|
|
return false;
|
|
$return = $query->fetchAll();
|
|
if (is_array($return) && count($return) == 1)
|
|
return $return[0]["count"];
|
|
}
|
|
catch (Exception $e) {
|
|
self :: _log_simple_select_query_error(
|
|
false, $table, $e, $where, null, null, null, $joins
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Mapping of JOIN type with corresponding query object method
|
|
* @var array<string,string>
|
|
*/
|
|
private static $join_type_to_query_method = array(
|
|
'LEFT' => 'leftJoin',
|
|
'RIGHT' => 'rightJoin',
|
|
'INNER' => 'innerJoin',
|
|
'OUTER' => 'outerJoin',
|
|
'FULL' => 'fullJoin',
|
|
);
|
|
|
|
/**
|
|
* Apply JOIN clauses on a query object
|
|
* @param \Envms\FluentPDO\Query $query The reference of the query object
|
|
* @param array<array{0: 'LEFT'|'RIGHT'|'INNER'|'OUTER'|'FULL', 1: string, 2: string}>|array{0: 'LEFT'|'RIGHT'|'INNER'|'OUTER'|'FULL', 1: string, 2: string} $joins Join specification as array: ['type', 'table', 'ON clause']
|
|
* - type: LEFT, RIGHT, INNER, OUTER or FULL
|
|
* - table: the joined table name
|
|
* - ON clause: the ON clause (ex: "user.id = article.user_id")
|
|
* @return void
|
|
*/
|
|
public static function apply_joins(&$query, &$joins) {
|
|
if (!$joins) return;
|
|
if (isset($joins[0]) && !is_array($joins[0]))
|
|
$joins = [$joins];
|
|
foreach ($joins as $join) {
|
|
if (!is_array($join) || count($join) != 3) {
|
|
throw new Exception(sprintf("Invalid JOIN clause provided: %s", vardump($join)));
|
|
}
|
|
if (!array_key_exists(strtoupper($join[0]), self :: $join_type_to_query_method)) {
|
|
throw new Exception(sprintf("Invalid JOIN type '%s'", $join[0]));
|
|
}
|
|
$method = self :: $join_type_to_query_method[strtoupper($join[0])];
|
|
call_user_func([$query, $method], sprintf("%s ON %s", $join[1], $join[2]));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to log error during simple select query
|
|
* @param bool $multiple True if expected multiple rows, False instead
|
|
* @param string $table The table name
|
|
* @param Exception $e The exception
|
|
* @param array|string $where WHERE clause(s) as expected by Envms\FluentPDO\Query
|
|
* @param array|string|null $fields The expected fields as string (separated by comma) or an
|
|
* array (optional, default: all table fields will be returned)
|
|
* @param string|null $order_by An optional ORDER clause as a string
|
|
* @param string|null $limit An optional LIMIT clause as a string
|
|
* @param array<array<string>>|null $joins Join specification as array (see apply_joins())
|
|
* @return void
|
|
*/
|
|
private static function _log_simple_select_query_error(
|
|
$multiple, $table, $e, $where=null, $fields=null, $order_by=null, $limit=null, $joins=null
|
|
) {
|
|
$msg = "Error occurred getting %s of the table %s";
|
|
$params = [
|
|
$multiple?"rows":"one row",
|
|
$table,
|
|
];
|
|
if (is_array($joins)) {
|
|
foreach($joins as $join) {
|
|
$msg .= ", %s join with table %s on '%s'";
|
|
$params = array_merge($params, $join);
|
|
}
|
|
}
|
|
$extra_clauses = [];
|
|
if ($where)
|
|
$extra_clauses['where'] = (
|
|
is_array($where)?
|
|
preg_replace("/\n */", " ", print_r($where, true)):
|
|
vardump($where)
|
|
);
|
|
if ($fields)
|
|
$extra_clauses['selected fields'] = is_array($fields)?implode(',', $fields):$fields;
|
|
if ($order_by)
|
|
$extra_clauses['order by'] = $order_by;
|
|
if ($limit)
|
|
$extra_clauses['limit'] = $limit;
|
|
$msg .= "in database (%s): %s";
|
|
$params[] = implode_with_keys($extra_clauses);
|
|
$params[] = $e->getMessage();
|
|
Log :: error($msg, $params);
|
|
}
|
|
|
|
/**
|
|
* Helper to insert a row in a table
|
|
* @param string $table The table name
|
|
* @param array<string,mixed> $values The values of the row
|
|
* @param boolean $want_id Set to true if you want to retrieve the ID of the inserted row
|
|
* @return bool|int The ID of the inserted row if $want_id, or true/false in case of success/error
|
|
*/
|
|
public static function insert($table, $values, $want_id=false) {
|
|
try {
|
|
$id = self :: $fpdo -> insertInto($table)
|
|
-> values($values)
|
|
-> execute();
|
|
}
|
|
catch (Exception $e) {
|
|
Log :: error(
|
|
"Error occurred inserting row in the table %s of the database: %s\nValues:\n%s",
|
|
$table,
|
|
$e->getMessage(),
|
|
vardump($values)
|
|
);
|
|
return false;
|
|
}
|
|
if ($id !== false) {
|
|
Log::debug('Row insert in table %s with ID #%s', $table, $id);
|
|
return $want_id?$id:true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Helper to update a row in database
|
|
* @param string $table The table name
|
|
* @param array<string,mixed> $changes Associative array of changes
|
|
* @param array|string $where WHERE clause(s) as expected by Envms\FluentPDO\Query
|
|
* @param null|int|false $expected_row_changes The number of expected row affected by the query
|
|
* or false if you don't want to check it (optional,
|
|
* default: null == 1)
|
|
* @return bool
|
|
*/
|
|
public static function update($table, $changes, $where, $expected_row_changes=null) {
|
|
if (is_null($expected_row_changes)) $expected_row_changes = 1;
|
|
try {
|
|
$result = self :: $fpdo -> update($table)
|
|
-> set($changes)
|
|
-> where($where)
|
|
-> execute();
|
|
}
|
|
catch (Exception $e) {
|
|
Log :: error(
|
|
"Error occurred updating %s in the table %s of the database (where %s): %s\nChanges:\n%s",
|
|
$expected_row_changes == 1?'row':'rows',
|
|
$table,
|
|
is_array($where)?
|
|
preg_replace("/\n */", " ", print_r($where, true)):
|
|
vardump($where),
|
|
$e->getMessage(),
|
|
vardump($changes)
|
|
);
|
|
return false;
|
|
}
|
|
if (!is_int($result))
|
|
return false;
|
|
if ($expected_row_changes === false)
|
|
return true;
|
|
return $expected_row_changes == $result;
|
|
}
|
|
|
|
/**
|
|
* Helper to delete one or multiple rows in a table of the database
|
|
* @param string $table The table name
|
|
* @param array|string $where WHERE clause(s) as expected by Envms\FluentPDO\Query
|
|
* @param null|int|false $expected_row_changes The number of expected row affected by the query
|
|
* or false if you don't want to check it (optional,
|
|
* default: null == 1)
|
|
* @return bool
|
|
*/
|
|
public static function delete($table, $where, $expected_row_changes=null) {
|
|
if (is_null($expected_row_changes)) $expected_row_changes = 1;
|
|
try {
|
|
$result = self :: $fpdo -> deleteFrom($table)
|
|
-> where($where)
|
|
-> execute();
|
|
}
|
|
catch (Exception $e) {
|
|
Log :: error(
|
|
"Error occurred deleting %s in the table %s of the database (where %s): %s",
|
|
$expected_row_changes == 1?'one row':'some rows',
|
|
$table,
|
|
is_array($where)?
|
|
preg_replace("/\n */", " ", print_r($where, true)):
|
|
vardump($where),
|
|
$e->getMessage()
|
|
);
|
|
return false;
|
|
}
|
|
// Bad return type declared in \Envms\FluentPDO\Queries\Delete
|
|
// @phpstan-ignore-next-line
|
|
if (!is_int($result))
|
|
return false;
|
|
// @phpstan-ignore-next-line
|
|
if ($expected_row_changes === false)
|
|
return true;
|
|
return $expected_row_changes == $result;
|
|
}
|
|
|
|
/**
|
|
* Helper to truncate a table of the database
|
|
* Note: FluentPDO does not provide a way to execute TRUNCATE SQL query, directly use PDO object.
|
|
* @param string $raw_table The table name
|
|
* @return bool
|
|
*/
|
|
public static function truncate($raw_table) {
|
|
Log :: debug(
|
|
"truncate(%s): FluentPDO does not provide a way to execute TRUNCATE SQL query, ".
|
|
"directly use PDO object.", $raw_table
|
|
);
|
|
$table = preg_replace('/[^A-Za-z0-9_]+/', '', $raw_table);
|
|
if ($raw_table != $table)
|
|
Log :: debug("truncate(%s):: Table name cleaned as '%s'", $raw_table, $table);
|
|
try {
|
|
if (self :: is_sqlite()) {
|
|
Log :: debug(
|
|
'truncate(%s): Sqlite does not support TRUNCATE SQL query, use DELETE instead.',
|
|
$table
|
|
);
|
|
$statement = self :: $pdo -> prepare("DELETE FROM $table");
|
|
}
|
|
else {
|
|
$statement = self :: $pdo -> prepare("TRUNCATE $table");
|
|
}
|
|
if (!$statement -> execute()) {
|
|
Log :: error("Unknown error occurred truncating the table %s of the database", $table);
|
|
return false;
|
|
}
|
|
}
|
|
catch (Exception $e) {
|
|
Log :: error(
|
|
"Error occurred truncating the table %s of the database: %s",
|
|
$table,
|
|
$e->getMessage()
|
|
);
|
|
return false;
|
|
}
|
|
Log::info("Table %s truncated", $table);
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Handle date/datetime format
|
|
*/
|
|
public static function set_locale() {
|
|
if (App::get(static :: $config_prefix.".locale_time"))
|
|
setlocale(LC_TIME, App::get(static :: $config_prefix.".locale_time"));
|
|
}
|
|
|
|
public static function date2time($date) {
|
|
self :: set_locale();
|
|
$pdate = strptime($date, App::get(static :: $config_prefix.".date_format"));
|
|
return mktime(
|
|
$pdate['tm_hour'], $pdate['tm_min'], $pdate['tm_sec'],
|
|
$pdate['tm_mon'] + 1, $pdate['tm_mday'], $pdate['tm_year'] + 1900
|
|
);
|
|
}
|
|
|
|
public static function time2date($time) {
|
|
self :: set_locale();
|
|
return strftime(App::get(static :: $config_prefix.".date_format"), $time);
|
|
}
|
|
|
|
public static function datetime2time($date) {
|
|
self :: set_locale();
|
|
$pdate = strptime($date, App::get(static :: $config_prefix.".datetime_format"));
|
|
return mktime(
|
|
$pdate['tm_hour'], $pdate['tm_min'], $pdate['tm_sec'],
|
|
$pdate['tm_mon'] + 1, $pdate['tm_mday'], $pdate['tm_year'] + 1900
|
|
);
|
|
}
|
|
|
|
public static function time2datetime($time) {
|
|
self :: set_locale();
|
|
return strftime(App::get(static :: $config_prefix.".datetime_format"), $time);
|
|
}
|
|
|
|
/**
|
|
* Helper method to format row info
|
|
* @param array<string,mixed> $row The raw row info
|
|
* @param array<string> $datetime_fields List of field in datetime format
|
|
* @param array<string> $date_fields List of field in date format
|
|
* @return array
|
|
*/
|
|
public static function format_row_info($row, $datetime_fields=null, $date_fields=null) {
|
|
// Convert datetime fields
|
|
if (is_array($datetime_fields))
|
|
foreach($datetime_fields as $field)
|
|
if ($row[$field])
|
|
$row[$field] = self :: datetime2time($row[$field]);
|
|
// Convert date fields
|
|
if (is_array($date_fields))
|
|
foreach($date_fields as $field)
|
|
if ($row[$field])
|
|
$row[$field] = self :: date2time($row[$field]);
|
|
return $row;
|
|
}
|
|
|
|
/**
|
|
* Helper method to parse PHP PDO DSN info
|
|
* @param string|null $dsn PHP PDO DSN to parse (optional)
|
|
* @return array|false
|
|
*/
|
|
public static function parse_pdo_dsn($dsn=null) {
|
|
$dsn = $dsn?$dsn:App::get(static :: $config_prefix.".dsn");
|
|
if (!preg_match('/^([^:]+):(.+)$/', $dsn, $dsn_parts))
|
|
return false;
|
|
|
|
$info = ['protocol' => $dsn_parts[1]];
|
|
foreach(preg_split('/;+/', $dsn_parts[2]) as $part) {
|
|
if (preg_match('/^([^=]+)=(.*)$/', $part, $dsn_part))
|
|
$info[$dsn_part[1]] = $dsn_part[2];
|
|
}
|
|
Log::trace('parse_pdo_dsn(%s): %s', $dsn, vardump($info));
|
|
return $info;
|
|
}
|
|
|
|
/**
|
|
* Get current database type
|
|
* @return string
|
|
*/
|
|
public static function get_db_type() {
|
|
return static :: $pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
|
|
}
|
|
|
|
/**
|
|
* Helper method to test if we use a sqlite database
|
|
* @return bool
|
|
*/
|
|
public static function is_sqlite() {
|
|
return static :: get_db_type() == 'sqlite';
|
|
}
|
|
|
|
/**
|
|
* Helper method to test if we use a PostgreSQL database
|
|
* @return bool
|
|
*/
|
|
public static function is_pgsql() {
|
|
return static :: get_db_type() == 'pgsql';
|
|
}
|
|
}
|