From 347de8eeaf4f41222cf8e4c741c98f7cf71ee0f9 Mon Sep 17 00:00:00 2001 From: Benjamin Renard Date: Sun, 18 Feb 2024 17:20:24 +0100 Subject: [PATCH] Add DbObject --- phpstan.neon | 1 + src/Date.php | 105 +++++-- src/Db.php | 71 ++++- src/Db/Attr.php | 80 ++++++ src/Db/AttrBool.php | 49 ++++ src/Db/AttrInt.php | 34 +++ src/Db/AttrStr.php | 27 ++ src/Db/AttrTimestamp.php | 74 +++++ src/Db/DbException.php | 17 ++ src/Db/DbObject.php | 582 +++++++++++++++++++++++++++++++++++++++ src/functions.php | 13 + 11 files changed, 1024 insertions(+), 29 deletions(-) create mode 100644 src/Db/Attr.php create mode 100644 src/Db/AttrBool.php create mode 100644 src/Db/AttrInt.php create mode 100644 src/Db/AttrStr.php create mode 100644 src/Db/AttrTimestamp.php create mode 100644 src/Db/DbException.php create mode 100644 src/Db/DbObject.php diff --git a/phpstan.neon b/phpstan.neon index 986038e..69f620a 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -6,6 +6,7 @@ parameters: - EesyPHP\HookEvent - EesyPHP\UrlRequest - EesyPHP\Auth\User + - EesyPHP\Db\DbObject ignoreErrors: - message: "#Property EesyPHP\\\\Auth\\\\Ldap::\\$connection has unknown class Net_LDAP2 as its type\\.#" diff --git a/src/Date.php b/src/Date.php index 8bc5e83..99e1446 100644 --- a/src/Date.php +++ b/src/Date.php @@ -2,6 +2,7 @@ namespace EesyPHP; + class Date { /** @@ -9,48 +10,98 @@ class Date { * @see strftime() * @var string */ - public static $date_format = "%d/%m/%Y"; + public static $date_format = "d/m/Y"; /** * The datetime format * @see strftime() * @var string */ - public static $date_time_format = "%d/%m/%Y %H:%M:%S"; + public static $date_time_format = "d/m/Y H:i:s"; /** * Format a timestamp as date/datetime string - * @param int $time The timestamp to format - * @param bool $with_time If true, include time in formatted string - * (optional, default: true) + * @param int|\DateTime $time The timestamp to format (or a DateTime object) + * @param bool|string $with_time_or_format If true, include time in formatted string, if string + * use it as date format (optional, default: true) + * @return string */ - public static function format($time, $with_time=true) { - if ($with_time) - return strftime(self :: $date_time_format, $time); - return strftime(self :: $date_format, $time); + public static function format($time, $with_time_or_format=true) { + $format = ( + is_string($with_time_or_format)? + $with_time_or_format: + ($with_time_or_format ? self :: $date_time_format : self :: $date_format) + ); + return is_a($time, "DateTime")?$time->format($format):date($format, $time); } /** * Parse a date/datetime string as timestamp * @param string $date The date string to parse - * @param bool $with_time If true, consider the date string included time - * (optional, default: true) + * @param bool|string $with_time_or_format If true, include time in formatted string, if string + * use it as date format (optional, default: true) + * @param \DateTimeZone|string|null $timezone Timezone of the loaded date + * (optional, default: system) + * @param bool $as_datetime If true, return a DateTime object instead of timestamp as integer + * @return ( $as_datetime is true ? \DateTime : int )|false */ - public static function parse($date, $with_time=true) { - if ($with_time) - $ptime = strptime($date, self :: $date_time_format); - else - $ptime = strptime($date, self :: $date_format); - if(is_array($ptime)) { - return mktime( - $ptime['tm_hour'], - $ptime['tm_min'], - $ptime['tm_sec'], - $ptime['tm_mon']+1, - $ptime['tm_mday'], - $ptime['tm_year']+1900 - ); - } - return false; + public static function parse( + $date, $with_time_or_format=true, $timezone=null, $as_datetime=false + ) { + $format = ( + is_string($with_time_or_format)? + $with_time_or_format: + ($with_time_or_format ? self :: $date_time_format : self :: $date_format) + ); + $date = \DateTime::createFromFormat($format, $date, self :: timezone($timezone)); + if ($as_datetime || $date === false) + return $date; + return $date->getTimestamp(); + } + + /** + * Get timezone + * @param \DateTimeZone|string|null $value Timezone name or 'system'|null for system timezone + * (optional) + * @param bool $as_string If true, return the name of the timezone instead of a DateTimeZone + * object (optional, default: false) + * @return ($as_string is true ? string|false : \DateTimeZone|false) + */ + public static function timezone($value=null, $as_string=false) { + $timezone = ( + is_null($value) || $value == 'system'? + get_system_timezone(): + timezone_open($value) + ); + if (!$as_string || !$timezone) + return $timezone; + return $timezone -> getName(); + } + + /** + * Create \DateTime object from timestamp + * @param int $value The timestamp + * @param \DateTimeZone|string|null $timezone Timezone name or 'system'|null for system timezone + * (optional) + * @return \DateTime + */ + public static function from_timestamp($value, $timezone=null) { + $date = new \DateTime(); + $date -> setTimestamp($value); + $date -> setTimezone(self :: timezone($timezone)); + return $date; + } + + /** + * Create \DateTime object + * @param int|null $timezone The expected timezone (optional, default: system one) + * @param \DateTimeZone|string|null $timezone Timezone name or 'system'|null for system timezone + * (optional) + * @return \DateTime + */ + public static function now($timezone=null) { + $date = new \DateTime(); + $date -> setTimezone(self :: timezone($timezone)); + return $date; } } diff --git a/src/Db.php b/src/Db.php index eebe19e..67b4d03 100644 --- a/src/Db.php +++ b/src/Db.php @@ -427,6 +427,48 @@ class Db { 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 @@ -487,10 +529,11 @@ class Db { /** * Helper method to parse PHP PDO DSN info - * @param string $dsn PHP PDO DSN to parse + * @param string|null $dsn PHP PDO DSN to parse (optional) * @return array|false */ - public static function parse_pdo_dsn($dsn) { + 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; @@ -502,4 +545,28 @@ class Db { 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'; + } } diff --git a/src/Db/Attr.php b/src/Db/Attr.php new file mode 100644 index 0000000..fd378f9 --- /dev/null +++ b/src/Db/Attr.php @@ -0,0 +1,80 @@ +|null $parameters Attribute parameters + */ + public function __construct($parameters=null) { + if (!is_array($parameters)) + return; + foreach($parameters as $key => $value) { + if (!property_exists($this, $key)) + throw new DbException("Attribute %s as no %s property", get_called_class(), $key); + $this -> $key = $value; + } + } + + /** + * Compute attribute default value + * @param mixed $value Override configured default value + * @return mixed + */ + public function default($value=null) { + $value = $value?$value:$this -> default; + return is_callable($value)?$value():$value; + } + + /** + * Compute attribute value from DB + * @param mixed $value The value as retrieved from debug + * @return mixed The attribute value + */ + public function from_db($value) { + return is_null($value)?$this -> default():$value; + } + + /** + * Compute attribute value for DB + * @param mixed $value The value as handled in PHP + * @return mixed The attribute value as stored in DB + */ + public function to_db($value) { + return is_null($value)?$this -> default():$value; + } + + /** + * Compute attribute value from string + * @param string $value The input value + * @return mixed The attribute value as handled in PHP + */ + public function from_string($value) { + return self :: from_db($value); + } + + /** + * Compute attribute value to string + * @param mixed $value The input value as handled in PHP + * @return string The attribute value as string + */ + public function to_string($value) { + $value = self :: to_db($value); + return is_null($value)?'':strval($value); + } + +} diff --git a/src/Db/AttrBool.php b/src/Db/AttrBool.php new file mode 100644 index 0000000..7a28a95 --- /dev/null +++ b/src/Db/AttrBool.php @@ -0,0 +1,49 @@ + timezone); + } + + /** + * Compute attribute value for DB + * @param \DateTime|int|null $value The value as handled in PHP + * @return int|null The attribute value as stored in DB + */ + public function to_db($value) { + $value = parent::from_db($value); + if (is_null($value)) return null; + $value = $value instanceof \DateTime?$value:Date :: from_timestamp($value); + return $value->getTimestamp(); + } + + /** + * Compute attribute value from string + * @param string $value The input value + * @return \DateTime|null The attribute value as handled in PHP + */ + public function from_string($value) { + if (!$value) return null; + $timestamp = Date :: parse($value, $this -> export_format, null, true); + if ($timestamp === false) + throw new DbException( + "Error parsing date '%s' from export using format '%s'", + $value, $this -> export_format + ); + return $timestamp; + } + + /** + * Compute attribute value to string + * @param \DateTime|int|null $value The input value as handled in PHP + * @return string The attribute value as string + */ + public function to_string($value) { + $value = parent::from_db($value); + if (is_null($value)) return ''; + $value = $value instanceof \DateTime?$value:Date :: from_timestamp($value); + return Date :: format($value->getTimestamp(), $this -> export_format); + } + +} diff --git a/src/Db/DbException.php b/src/Db/DbException.php new file mode 100644 index 0000000..4ca4ca6 --- /dev/null +++ b/src/Db/DbException.php @@ -0,0 +1,17 @@ + $_schema + * @property-read bool $_is_new + */ +class DbObject { + + /** + * Possible order directions + * @var array + */ + public const ORDER_DIRECTIONS = ['ASC', 'DESC']; + + /** + * Default number of objects by page + * @var int + */ + public const DEFAULT_NB_BY_PAGE = 25; + + /** + * The database class + * @var class-string + */ + protected const DB_CLASS = '\\EesyPHP\\Db'; + + /** + * The table name where are stored these objects + * @var string + */ + protected const TABLE = 'NOTSET'; + + /** + * Primary keys (optional, default: the first field of the schema) + * @var array + */ + protected const PRIMARY_KEYS = []; + + /** + * The default order (optional, default: the first field of the schema) + * @var string|null + */ + protected const DEFAULT_ORDER = null; + + /** + * The default order direction + * @var value-of + */ + protected const DEFAULT_ORDER_DIRECTION = 'DESC'; + + /** + * Possible orders (optional, default: all declared fields in schema) + * @var array|null + */ + protected const POSSIBLE_ORDERS = null; + + /** + * Object's properties values as stored in database + * @var array + */ + protected $_properties; + + /** + * Flag to known if it's a new object + * @var bool + */ + protected $_is_new; + + /** + * Cache of the object's property _schema + * @var array|null + */ + protected $__schema = null; + + /** + * Object constructor + * @param array $properties Object's properties as stored in database + * @param boolean $is_new Set to false if it's an existing object in database + */ + public function __construct($properties=null, $is_new=true) { + $this -> _properties = is_array($properties)?$properties:[]; + foreach($this -> _schema as $prop => $attr) + if (!array_key_exists($prop, $this -> _properties)) + $this -> _properties[$prop] = $attr->to_db(null); + $this -> _is_new = boolval($is_new); + } + + /** + * Get object schema + * @throws DbException + * @return array The object's schema + */ + protected static function get_schema() { + throw new DbException("The get_schema() method is not implemented for %s", get_called_class()); + } + + /** + * Get object's primary keys + * @param array|null $schema The object's schema (optional) + * @return array + */ + public static function primary_keys($schema=null) { + if (static :: PRIMARY_KEYS) + return static :: PRIMARY_KEYS; + $schema = $schema ? $schema : static :: get_schema(); + return [key($schema)]; + } + + /** + * Get object's primary keys value + * @return array + */ + public function get_primary_keys($with_value=false) { + $pks = []; + foreach(static :: primary_keys($this -> _schema) as $pk) + $pks[$pk] = $this -> $pk; + return $pks; + } + + /** + * Get WHERE clause from object primary keys + * @throws DbException + * @return array + */ + protected function get_primary_keys_where_clauses() { + $where = []; + foreach(static :: get_primary_keys($this -> _schema) as $prop => $value) { + if (is_null($value)) + throw new DbException( + "No value for primary key %s of %s object", + $prop, get_called_class() + ); + $where[$prop] = $this -> _schema[$prop] -> to_db($value); + } + return $where; + } + + /** + * Get default order + * @param array|null $schema The object's schema (optional) + * @return string + */ + public static function default_order($schema=null) { + if (static :: DEFAULT_ORDER) + return static :: DEFAULT_ORDER; + $schema = $schema ? $schema : static :: get_schema(); + return key($schema); + } + + /** + * Get default order direction + * @return value-of + */ + public static function default_order_direction() { + return static :: DEFAULT_ORDER_DIRECTION; + } + + /** + * Get possible orders + * @return array + */ + public static function possible_orders() { + if (static :: POSSIBLE_ORDERS) + return static :: POSSIBLE_ORDERS; + $schema = static :: get_schema(); + return array_keys($schema); + } + + /** + * Get an object's property + * @param string $prop The property name + * @throws DbException + * @return mixed + */ + public function __get($prop) { + switch ($prop) { + case '_schema': + if (is_null($this -> __schema)) + $this -> __schema = static :: get_schema(); + return $this -> __schema; + case '_is_new': + return $this -> _is_new; + } + if (!array_key_exists($prop, $this -> _schema)) + throw new DbException("%s object as no %s field", get_called_class(), $prop); + return $this -> _schema[$prop] -> from_db( + array_key_exists($prop, $this -> _properties)? + $this -> _properties[$prop]: + null + ); + } + + /** + * Set an object's property + * @param string $prop The property name + * @throws DbException + * @param mixed $value The property value + */ + public function __set($prop, $value) { + if (!array_key_exists($prop, $this -> _schema)) + throw new DbException("%s object as no %s field", get_called_class(), $prop); + $this -> _properties[$prop] = $this -> _schema[$prop] -> to_db($value); + } + + /** + * Helper to apply changes pass as an associative array on the object + * @param array $changes The changes to apply + * @throws DbException + * @return void + */ + public function apply($changes) { + foreach($changes as $attr => $value) + $this -> $attr = $value; + } + + /** + * Get one object from its primary keys + * @param array $args Primary keys value in the same order as primary keys as declared + * @see static :: PRIMARY_KEYS + * @throws DbException + * @return DbObject|null The object if found, else otherwise. + */ + public static function get(...$args) { + $class = get_called_class(); + $schema = static :: get_schema(); + $primary_keys = static :: primary_keys($schema); + $where = []; + foreach($primary_keys as $prop) { + if (empty($args)) + throw new DbException("No value provide for %s", $prop); + $value = array_shift($args); + $where[$prop] = $schema[$prop] -> to_db($value); + } + + if (!empty($args)) + throw new DbException( + "Too much arguments provided to get one %s. Expect the following fields: %s", + $class, + implode(', ', $primary_keys) + ); + + $properties = static :: DB_CLASS :: get_one( + static :: TABLE, + $where, + array_keys($schema) + ); + if ($properties === false) + return null; + + return new $class($properties, false); + } + + /** + * Save changes on object + * @return boolean + */ + public function save() { + Hook :: trigger(get_called_class().'::'.($this -> _is_new?'adding':'updating'), $this); + foreach($this -> _schema as $prop => $info) { + if (!$info->required || !is_null($this->$prop)) continue; + if ($this -> _is_new && property_exists($info, 'autoincrement') && $info->autoincrement) + continue; + Log :: warning( + "%s->save(): required property %s is missing, can't save it.", + get_called_class(), $prop + ); + return false; + } + if ($this -> _is_new) { + Log :: debug("%s->save(): object marked as new, use INSERT query", get_called_class()); + $values = []; + foreach($this -> _properties as $prop => $value) + if (!is_null($value)) + $values[$prop] = $value; + $result = static :: DB_CLASS :: insert(static :: TABLE, $values, true); + if ($result === false) + return false; + + // Store object ID if object as an unique integer primary key + if (is_int($result)) { + $pks = $this -> primary_keys($this -> _schema); + if (count($pks) == 1 && $this -> _schema[$this -> $pks[0]] instanceof AttrInt) + $this -> $pks[0] = $result; + } + Log :: info( + "New %s added (%s)", + get_called_class(), + implode_with_keys($this -> get_primary_keys()) + ); + + Hook :: trigger(get_called_class().'::added', $this); + return true; + } + + Log :: debug("%s->save(): object not marked as new, use UPDATE query", get_called_class()); + if (static :: DB_CLASS :: update( + static :: TABLE, + $this -> _properties, + $this -> get_primary_keys_where_clauses() + )) { + Log :: info( + "%s %s updated", + get_called_class(), + implode_with_keys($this -> get_primary_keys()) + ); + Hook :: trigger(get_called_class().'::updated', $this); + return True; + } + Log :: error( + "Error saving changes in %s %s.", + get_called_class(), + implode_with_keys($this -> get_primary_keys()) + ); + return False; + } + + /** + * Delete the object + * @return boolean + */ + public function delete() { + Hook :: trigger(get_called_class().'::deleting', $this); + if (!static :: DB_CLASS :: delete( + static :: TABLE, + $this -> get_primary_keys_where_clauses() + )) + return False; + $this -> _is_new = true; + Hook :: trigger(get_called_class().'::deleted', $this); + return True; + } + + /** + * List objects + * @param array $where Where clauses as associative array of field name and value + * @return array|false + */ + public static function list($where=null) { + $class = get_called_class(); + $schema = static :: get_schema(); + $rows = static :: DB_CLASS :: get_many( + static :: TABLE, + static :: _compute_where_clauses($where), + array_keys($schema) + ); + if (!is_array($rows)) + return false; + $objects = []; + foreach($rows as $row) + $objects[] = new $class($row, false); + return $objects; + } + + /** + * Compute WHERE clauses + * @param array|null $where Where clauses as associative array of field name and value + * @param array|null $schema The object's schema (optional) + * @throws DbException + * @return array + */ + protected static function _compute_where_clauses($where, $schema=null) { + if (is_null($where)) return []; + $schema = $schema?$schema:static :: get_schema(); + foreach(array_keys($where) as $prop) { + if (!array_key_exists($prop, $schema)) + throw new DbException("%s object as no %s field", get_called_class(), $prop); + $where[$prop] = $schema[$prop] -> to_db($where[$prop]); + } + return $where; + } + + /** + * Compute WHERE clauses from word pattern + * @throws DbException + * @return array + */ + public static function word_to_filters($word) { + throw new DbException( + "The word_to_filters() method is not implemented for %s", + get_called_class() + ); + } + + /** + * Search objects + * @param array $params Search parameters as an associative array + * [ + * 'pattern' => [pattern], + * 'filters' => ['field1' => 'value1', ...], + * 'where' => [WHERE clauses as expected by FPdo (aggregate with a AND logical operator)], + * 'order' => [order by field], + * 'order_direction' => [ordering direction], + * 'all' => bool, + * 'offset' => int, + * 'limit' => int, + * // or: + * 'page' => integer, // default: 1 + * 'nb_by_page' => integer, // default: static :: DEFAULT_NB_BY_PAGE + * ] + * @throws DbException + * @return array|false The search result as an array, or False in case of error. + * Search result format: + * [ + * 'first' => [range of the first item in result], + * 'last' => [range of the last item in result], + * 'count' => [total number of items return by the search], + * 'items' => [array of DbObjects], + * // paged search (simulate in non-paged search): + * 'page' => [page starting by 1], + * 'nb_pages' => [number of pages], + * // non-paged search: + * 'offset' => [the offset of the search], + * 'limit' => [the limit of the search], + * ] + */ + public static function search($params) { + $class = get_called_class(); + $schema = static :: get_schema(); + + $where = isset($params['where'])?$params['where']:[]; + if (isset($params['filters'])) + $where = array_merge(static :: _compute_where_clauses($params['filters']), $where); + + $patterns_where = array(); + if (isset($params['pattern']) && $params['pattern']) { + foreach(preg_split('/\s+/', trim($params['pattern'])) as $word) { + if (!$word) continue; + $patterns_where[] = static :: word_to_filters($word); + } + } + + $order = static :: default_order(); + if (isset($params['order'])) { + $orders = static :: possible_orders(); + if (!in_array($order, $orders)) + throw new DbException( + "Invalid order '%s' clause for objects %s", + $params['order'], $class + ); + $order = $params['order']; + } + + $order_direction = static :: default_order_direction(); + if (isset($params['order_direction']) && $params['order_direction']) { + if (!in_array($params['order_direction'], self :: ORDER_DIRECTIONS)) + throw new DbException( + "Invalid order direction '%s' clause for objects %s", + $params['order_direction'], $class + ); + $order_direction=$params['order_direction']; + } + + $orderby = "$order $order_direction"; + + $limit = null; + $offset = 0; + $page = 1; + $nb_by_page = static :: DEFAULT_NB_BY_PAGE; + if (isset($params['all'])) { + if (isset($params['limit'])) + $limit = intval($params['limit']); + if (isset($params['offset'])) + $offset = intval($params['offset']); + } + else { + if (isset($params['page']) && $params['page'] > 0) { + if (isset($params['nb_by_page']) && $params['nb_by_page'] > 0) { + $nb_by_page = intval($params['nb_by_page']); + } + $page = intval($params['page']); + } + $offset = ($page - 1) * $nb_by_page; + $limit = $nb_by_page; + } + + try { + $query = static :: DB_CLASS :: $fpdo -> from(static :: TABLE); + if (!empty($where)) + $query -> where($where); + foreach ($patterns_where as $patterns_word) + call_user_func_array( + array($query, 'where'), + array_merge( + array('('.implode(' OR ', array_keys($patterns_word)).')'), + array(array_values($patterns_word)) + ) + ); + $result = $query -> orderBy($orderby) + -> limit($limit) + -> offset($offset) + -> execute(); + + if ($result === false) { + Log :: error('%s :: search() : search in DB return false', $class); + return false; + } + + $items = []; + foreach ($result -> fetchAll() as $row) + $items[] = new $class($row, false); + + if (isset($params['all'])) { + return array( + 'count' => count($items), + 'first' => 1, + 'last' => count($items), + 'nb_pages' => 1, + 'page' => 1, + 'offset' => $offset, + 'limit' => $limit, + 'items' => $items + ); + } + $query_count = static :: DB_CLASS :: $fpdo -> from('item') + -> select(null) + -> select('COUNT(*) as count'); + if (!empty($where)) + $query_count -> where($where); + foreach ($patterns_where as $patterns_word) + call_user_func_array( + array($query_count, 'where'), + array_merge( + array('('.implode(' OR ', array_keys($patterns_word)).')'), + array(array_values($patterns_word)) + ) + ); + + $result_count = $query_count -> execute(); + + if ($result_count === false) { + Log :: debug('%s :: search() : search for count in DB return false', $class); + return False; + } + $count = $result_count -> fetch(); + return array( + 'count' => $count['count'], + 'first' => $offset+1, + 'last' => ( + $offset + $nb_by_page < $count['count']? + $offset + $nb_by_page: + $count['count'] + ), + 'nb_pages' => ceil($count['count']/$nb_by_page), + 'page' => $page, + 'items' => $items, + ); + } + catch (\Exception $e) { + Log :: exception( + $e, "An exception occurred searching %s with params %s in database", + get_called_class(), + preg_replace("/\n[ \t]*/", " ", print_r($params, true)) + ); + } + return false; + } + + /** + * Compute CSV export fields as expected by \EesyPHP\Export\CSV + * @param array|null $schema The object's schema (optional) + * @return array + */ + static protected function csv_export_fields($schema=null) { + $schema = $schema?$schema:static :: get_schema(); + $csv_fields = []; + foreach($schema as $field => $attr) + $csv_fields[$field] = [ + 'label' => $field, + 'from_string' => [$attr, 'from_string'], + 'to_string' => [$attr, 'to_string'], + ]; + return $csv_fields; + } + +} diff --git a/src/functions.php b/src/functions.php index 4efae8f..1e9d044 100644 --- a/src/functions.php +++ b/src/functions.php @@ -370,4 +370,17 @@ function generate_uuid() { ); } +/** + * Get system timezone + * @param bool $as_string Set to true to retrieve timezone name instead of a DateTimeZone object + * @return ($as_string is true ? string : \DateTimeZone) System timezone + */ +function get_system_timezone($as_string=false) { + $timezone = trim( + $_SERVER['TZ'] ?? + (file_get_contents('/etc/timezone') ?: file_get_contents('/etc/localtime')) + ); + return $as_string?$timezone:new \DateTimeZone($timezone); +} + # vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab