Add DbObject
This commit is contained in:
parent
6c74b2a719
commit
347de8eeaf
11 changed files with 1024 additions and 29 deletions
|
@ -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\\.#"
|
||||
|
|
101
src/Date.php
101
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
|
||||
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();
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
71
src/Db.php
71
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';
|
||||
}
|
||||
}
|
||||
|
|
80
src/Db/Attr.php
Normal file
80
src/Db/Attr.php
Normal 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
49
src/Db/AttrBool.php
Normal 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
34
src/Db/AttrInt.php
Normal 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
27
src/Db/AttrStr.php
Normal 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
74
src/Db/AttrTimestamp.php
Normal 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
17
src/Db/DbException.php
Normal 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
582
src/Db/DbObject.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue