Add DbObject

This commit is contained in:
Benjamin Renard 2024-02-18 17:20:24 +01:00
parent 6c74b2a719
commit 347de8eeaf
Signed by: bn8
GPG key ID: 3E2E1CE1907115BC
11 changed files with 1024 additions and 29 deletions

View file

@ -6,6 +6,7 @@ parameters:
- EesyPHP\HookEvent - EesyPHP\HookEvent
- EesyPHP\UrlRequest - EesyPHP\UrlRequest
- EesyPHP\Auth\User - EesyPHP\Auth\User
- EesyPHP\Db\DbObject
ignoreErrors: ignoreErrors:
- -
message: "#Property EesyPHP\\\\Auth\\\\Ldap::\\$connection has unknown class Net_LDAP2 as its type\\.#" message: "#Property EesyPHP\\\\Auth\\\\Ldap::\\$connection has unknown class Net_LDAP2 as its type\\.#"

View file

@ -2,6 +2,7 @@
namespace EesyPHP; namespace EesyPHP;
class Date { class Date {
/** /**
@ -9,48 +10,98 @@ class Date {
* @see strftime() * @see strftime()
* @var string * @var string
*/ */
public static $date_format = "%d/%m/%Y"; public static $date_format = "d/m/Y";
/** /**
* The datetime format * The datetime format
* @see strftime() * @see strftime()
* @var string * @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 * Format a timestamp as date/datetime string
* @param int $time The timestamp to format * @param int|\DateTime $time The timestamp to format (or a DateTime object)
* @param bool $with_time If true, include time in formatted string * @param bool|string $with_time_or_format If true, include time in formatted string, if string
* (optional, default: true) * use it as date format (optional, default: true)
* @return string
*/ */
public static function format($time, $with_time=true) { public static function format($time, $with_time_or_format=true) {
if ($with_time) $format = (
return strftime(self :: $date_time_format, $time); is_string($with_time_or_format)?
return strftime(self :: $date_format, $time); $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 * Parse a date/datetime string as timestamp
* @param string $date The date string to parse * @param string $date The date string to parse
* @param bool $with_time If true, consider the date string included time * @param bool|string $with_time_or_format If true, include time in formatted string, if string
* (optional, default: true) * 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) { public static function parse(
if ($with_time) $date, $with_time_or_format=true, $timezone=null, $as_datetime=false
$ptime = strptime($date, self :: $date_time_format); ) {
else $format = (
$ptime = strptime($date, self :: $date_format); is_string($with_time_or_format)?
if(is_array($ptime)) { $with_time_or_format:
return mktime( ($with_time_or_format ? self :: $date_time_format : self :: $date_format)
$ptime['tm_hour'],
$ptime['tm_min'],
$ptime['tm_sec'],
$ptime['tm_mon']+1,
$ptime['tm_mday'],
$ptime['tm_year']+1900
); );
$date = \DateTime::createFromFormat($format, $date, self :: timezone($timezone));
if ($as_datetime || $date === false)
return $date;
return $date->getTimestamp();
} }
return false;
/**
* 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;
} }
} }

View file

@ -427,6 +427,48 @@ class Db {
return $expected_row_changes == $result; 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 * Handle date/datetime format
@ -487,10 +529,11 @@ class Db {
/** /**
* Helper method to parse PHP PDO DSN info * 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 * @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)) if (!preg_match('/^([^:]+):(.+)$/', $dsn, $dsn_parts))
return false; return false;
@ -502,4 +545,28 @@ class Db {
Log::trace('parse_pdo_dsn(%s): %s', $dsn, vardump($info)); Log::trace('parse_pdo_dsn(%s): %s', $dsn, vardump($info));
return $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';
}
} }

80
src/Db/Attr.php Normal file
View file

@ -0,0 +1,80 @@
<?php
namespace EesyPHP\Db;
class Attr {
/**
* Attribute default value
* @var mixed
*/
protected $default = null;
/**
* Attribute required flag
* @var bool
*/
public $required = false;
/**
* Constructor
* @param array<string,mixed>|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);
}
}

49
src/Db/AttrBool.php Normal file
View file

@ -0,0 +1,49 @@
<?php
namespace EesyPHP\Db;
class AttrBool extends Attr {
/**
* The value stored in database for true
* @var mixed
*/
public static $true_value = 1;
/**
* The value stored in database for false
* @var mixed
*/
public static $false_value = 0;
/**
* Compute attribute value from DB
* @param mixed $value The value as retrieved from debug
* @return bool|null The attribute value
*/
public function from_db($value) {
$value = parent::from_db($value);
switch ($value) {
case static :: $true_value:
return true;
case static :: $false_value:
return false;
case null:
return null;
}
throw new DbException("Unknown value '%s' retrieved for %s value", $value, get_called_class());
}
/**
* 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) {
$value = parent::from_db($value);
if(is_null($value))
return null;
return $value?static :: $true_value:static :: $false_value;
}
}

34
src/Db/AttrInt.php Normal file
View file

@ -0,0 +1,34 @@
<?php
namespace EesyPHP\Db;
class AttrInt extends Attr {
/**
* Auto-increment flag
* Note: use to set this attribute as optional on creation.
* @var bool
*/
public $autoincrement = false;
/**
* Compute attribute value from DB
* @param int|null $value The value as retrieved from debug
* @return int|null The attribute value
*/
public function from_db($value) {
$value = parent::from_db($value);
return is_null($value)?null:intval($value);
}
/**
* Compute attribute value for DB
* @param 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);
return is_null($value)?null:intval($value);
}
}

27
src/Db/AttrStr.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace EesyPHP\Db;
class AttrStr extends Attr {
/**
* Compute attribute value from DB
* @param string|null $value The value as retrieved from debug
* @return string|null The attribute value
*/
public function from_db($value) {
$value = parent::from_db($value);
return is_null($value)?null:strval($value);
}
/**
* Compute attribute value for DB
* @param string|null $value The value as handled in PHP
* @return string|null The attribute value as stored in DB
*/
public function to_db($value) {
$value = parent::from_db($value);
return is_null($value)?null:strval($value);
}
}

74
src/Db/AttrTimestamp.php Normal file
View file

@ -0,0 +1,74 @@
<?php
namespace EesyPHP\Db;
use EesyPHP\Date;
use function EesyPHP\get_system_timezone;
class AttrTimestamp extends Attr {
/**
* The timezone of the date
* @var \DateTimeZone|string|null
*/
protected $timezone = 'system';
/**
* The export format
* @var string
*/
protected $export_format = 'Y/m/d H:i:s';
/**
* Compute attribute value from DB
* @param int|null $value The value as retrieved from debug
* @return \DateTime|null The attribute value
*/
public function from_db($value) {
$value = parent::from_db($value);
if (is_null($value)) return null;
return Date :: from_timestamp($value, $this -> 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);
}
}

17
src/Db/DbException.php Normal file
View file

@ -0,0 +1,17 @@
<?php
namespace EesyPHP\Db;
class DbException extends \Exception {
public function __construct($message, ...$extra_args) {
// If extra arguments passed, format error message using sprintf
if ($extra_args) {
$message = call_user_func_array(
'sprintf',
array_merge(array($message), $extra_args)
);
}
parent::__construct($message);
}
}

582
src/Db/DbObject.php Normal file
View file

@ -0,0 +1,582 @@
<?php
namespace EesyPHP\Db;
use EesyPHP\Hook;
use EesyPHP\Log;
use function EesyPHP\implode_with_keys;
/**
* @property-read array<string,Attr> $_schema
* @property-read bool $_is_new
*/
class DbObject {
/**
* Possible order directions
* @var array<string>
*/
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<string>
*/
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<DbObject::ORDER_DIRECTIONS>
*/
protected const DEFAULT_ORDER_DIRECTION = 'DESC';
/**
* Possible orders (optional, default: all declared fields in schema)
* @var array<string>|null
*/
protected const POSSIBLE_ORDERS = null;
/**
* Object's properties values as stored in database
* @var array<string,mixed>
*/
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<string,Attr>|null
*/
protected $__schema = null;
/**
* Object constructor
* @param array<string,mixed> $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<string,Attr> 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<string,Attr>|null $schema The object's schema (optional)
* @return array<string>
*/
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<string,mixed>
*/
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<string,mixed>
*/
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<string,Attr>|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<DbObject::ORDER_DIRECTIONS>
*/
public static function default_order_direction() {
return static :: DEFAULT_ORDER_DIRECTION;
}
/**
* Get possible orders
* @return array<string>
*/
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<string,mixed> $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<mixed> $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<string,mixed> $where Where clauses as associative array of field name and value
* @return array<DbObject>|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<string,mixed>|null $where Where clauses as associative array of field name and value
* @param array<string,Attr>|null $schema The object's schema (optional)
* @throws DbException
* @return array<string,mixed>
*/
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<string,mixed> $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<string,Attr>|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;
}
}

View file

@ -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 # vim: tabstop=2 shiftwidth=2 softtabstop=2 expandtab