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} $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} $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} $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 */ 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} $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>|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 $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 $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; } /* * 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 $row The raw row info * @param array $datetime_fields List of field in datetime format * @param array $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 $dsn PHP PDO DSN to parse * @return array|false */ public static function parse_pdo_dsn($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; } }