eesyphp/src/Db.php
2023-11-20 19:42:46 +01:00

473 lines
16 KiB
PHP

<?php
namespace EesyPHP;
use Exception;
use PDO;
use \Envms\FluentPDO\Query;
class Db {
/**
* The PDO object of the database connection
* @var PDO
*/
public $pdo;
/**
* The PDO object of the database connection
* @var \Envms\FluentPDO\Query
*/
public $fpdo;
/**
* Date format as returned by database
* @var string
*/
protected $date_format = '%Y-%m-%d';
/**
* Datetime format as returned by database
* @var string
*/
protected $datetime_format = '%Y-%m-%d %H:%M:%S';
/**
* Locale for time (as expected by LC_TIME)
* @var string|null
*/
protected $locale_time = null;
/**
* Keep trace of total queries times (in ns)
* @var int;
*/
public static $total_query_time = 0;
/**
* Connect to database and return FluentPDO Query object
* @param string $dsn Database DSN
* @param string|null $user Username (optional)
* @param string|null $password password (optional)
* @param string|array $options Connection options (optional)
* @param string|null $date_format Date format in DB (optional)
* @param string|null $datetime_format Datetime format in DB (optional)
* @param string|null $locale_time Locale for time (optional)
* @return void
*/
public function __construct($dsn, $user=null, $password=null, $options=null,
$date_format=null, $datetime_format=null, $locale_time=null) {
if (!$dsn)
Log :: fatal('Database DSN not configured');
if ($date_format) $this -> date_format = $date_format;
if ($datetime_format) $this -> datetime_format = $datetime_format;
if ($locale_time) $this -> locale_time = $locale_time;
try {
// Connect to database
$this -> pdo = new PDO($dsn, $user, $password, $options);
$this -> fpdo = new Query($this -> pdo);
// Register the debug query handler to log it
$this -> 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 function set_autocommit($value) {
$this -> pdo -> setAttribute(PDO::ATTR_AUTOCOMMIT, $value);
}
/*
* Simple request helpers
*/
/**
* Helper to retreive 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 (separeted 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 function get_one($table, $where, $fields=null, $joins=null) {
try {
$query = $this -> 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 retreive 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 (separeted 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 function get_many(
$table, $where=null, $fields=null, $order_by=null, $limit=null, $joins=null
) {
try {
$query = $this -> 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 retreive 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 function count($table, $where, $what=null, $joins=null) {
try {
$query = $this -> 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 (separeted 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 function _log_simple_select_query_error(
$multiple, $table, $e, $where=null, $fields=null, $order_by=null, $limit=null, $joins=null
) {
$msg = "Error occured 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 retreive 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 function insert($table, $values, $want_id=false) {
try {
$id = $this -> fpdo -> insertInto($table)
-> values($values)
-> execute();
}
catch (Exception $e) {
Log :: error(
"Error occured 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 function update($table, $changes, $where, $expected_row_changes=null) {
if (is_null($expected_row_changes)) $expected_row_changes = 1;
try {
$result = $this -> fpdo -> update($table)
-> set($changes)
-> where($where)
-> execute();
}
catch (Exception $e) {
Log :: error(
"Error occured 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 function delete($table, $where, $expected_row_changes=null) {
if (is_null($expected_row_changes)) $expected_row_changes = 1;
try {
$result = $this -> fpdo -> deleteFrom($table)
-> where($where)
-> execute();
}
catch (Exception $e) {
Log :: error(
"Error occured 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;
}
/*
* Handle date/datetime format
*/
public function set_locale() {
if ($this -> locale_time)
setlocale(LC_TIME, $this -> locale_time);
}
public function date2time($date) {
$this -> set_locale();
$pdate = strptime($date, $this -> 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 function time2date($time) {
$this -> set_locale();
return strftime($this -> date_format, $time);
}
public function datetime2time($date) {
$this -> set_locale();
$pdate = strptime($date, $this -> 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 function time2datetime($time) {
$this -> set_locale();
return strftime($this -> 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 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] = $this -> datetime2time($row[$field]);
// Convert date fields
if (is_array($date_fields))
foreach($date_fields as $field)
if ($row[$field])
$row[$field] = $this -> date2time($row[$field]);
return $row;
}
}