diff --git a/.idea/codeception.xml b/.idea/codeception.xml new file mode 100644 index 0000000..9da3754 --- /dev/null +++ b/.idea/codeception.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/.idea/nulib.iml b/.idea/nulib.iml index da82e0e..3809c02 100644 --- a/.idea/nulib.iml +++ b/.idea/nulib.iml @@ -2,15 +2,10 @@ - - - - - - - - + + + diff --git a/.idea/phpspec.xml b/.idea/phpspec.xml new file mode 100644 index 0000000..ec7e1d4 --- /dev/null +++ b/.idea/phpspec.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/phpunit.xml b/.idea/phpunit.xml new file mode 100644 index 0000000..4f8104c --- /dev/null +++ b/.idea/phpunit.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/php/src/A.php b/php/src/A.php new file mode 100644 index 0000000..84ca718 --- /dev/null +++ b/php/src/A.php @@ -0,0 +1,39 @@ +wrappedArray(); + if ($array === null || $array === false) $array = []; + elseif ($array instanceof Traversable) $array = iterator_to_array($array); + else $array = [$array]; + return false; + } + + /** + * s'assurer que $array est un array s'il est non null. retourner true si + * $array n'a pas été modifié (s'il était déjà un array ou s'il valait null). + */ + static final function ensure_narray(&$array): bool { + if ($array === null || is_array($array)) return true; + if ($array instanceof IArrayWrapper) $array = $array->wrappedArray(); + if ($array === false) $array = []; + elseif ($array instanceof Traversable) $array = iterator_to_array($array); + else $array = [$array]; + return false; + } + +} diff --git a/php/src/DataException.php b/php/src/DataException.php deleted file mode 100644 index b68b8ed..0000000 --- a/php/src/DataException.php +++ /dev/null @@ -1,19 +0,0 @@ -userMessage = $user_message; - if ($tech_message === null) $tech_message = $user_message; - parent::__construct($tech_message, $code, $previous); + function __construct($userMessage, $techMessage=null, $code=0, ?Throwable $previous=null) { + $this->userMessage = $userMessage; + if ($techMessage === null) $techMessage = $userMessage; + parent::__construct($techMessage, $code, $previous); } + /** @var ?string */ protected $userMessage; function getUserMessage(): ?string { diff --git a/php/src/cl.php b/php/src/cl.php index 21c3202..2d11a48 100644 --- a/php/src/cl.php +++ b/php/src/cl.php @@ -5,7 +5,11 @@ use ArrayAccess; use Traversable; /** - * Class cl: gestion de tableau de valeurs scalaires + * Class cl: gestion de tableaux ou d'instances de {@link ArrayAccess} le cas + * échéant + * + * contrairement à {@link A}, les méthodes de cette classes sont plutôt conçues + * pour retourner un nouveau tableau */ class cl { /** retourner un array non null à partir de $array */ @@ -24,30 +28,6 @@ class cl { else return [$array]; } - /** - * s'assurer que $array est un array non null. retourner true si $array n'a - * pas été modifié (s'il était déjà un array), false sinon. - */ - static final function ensure_array(&$array): bool { - if (is_array($array)) return true; - elseif ($array === null || $array === false) $array = []; - elseif ($array instanceof Traversable) $array = iterator_to_array($array); - else $array = [$array]; - return false; - } - - /** - * s'assurer que $array est un array s'il est non null. retourner true si - * $array n'a pas été modifié (s'il était déjà un array ou s'il valait null). - */ - static final function ensure_narray(&$array): bool { - if ($array === null || is_array($array)) return true; - elseif ($array === false) $array = []; - elseif ($array instanceof Traversable) $array = iterator_to_array($array); - else $array = [$array]; - return false; - } - /** tester si $array a au moins une clé numérique */ static final function have_num_keys(?array $array): bool { if ($array === null) return false; @@ -159,7 +139,7 @@ class cl { static final function merge(...$arrays): ?array { $merges = []; foreach ($arrays as $array) { - self::ensure_narray($array); + A::ensure_narray($array); if ($array !== null) $merges[] = $array; } return $merges? array_merge(...$merges): null; @@ -170,7 +150,7 @@ class cl { /** * vérifier que le chemin $keys existe dans le tableau $array * - * si $keys est vide ou null, retourner true + * si $pkey est vide ou null, retourner true */ static final function phas($array, $pkey): bool { # optimisations @@ -214,7 +194,7 @@ class cl { /** * obtenir la valeur correspondant au chemin $keys dans $array * - * si $keys est vide ou null, retourner $default + * si $pkey est vide ou null, retourner $default */ static final function pget($array, $pkey, $default=null) { # optimisations @@ -264,7 +244,7 @@ class cl { * - pset($array, ["a", "b", ""], $value) est équivalent à $array["a"]["b"][] = $value * la clé "" n'a pas de propriété particulière quand elle n'est pas en dernière position * - * si $keys est vide ou null, $array est remplacé par $value + * si $pkey est vide ou null, $array est remplacé par $value */ static final function pset(&$array, $pkey, $value): void { # optimisations @@ -281,7 +261,7 @@ class cl { $pkey = explode(".", strval($pkey)); } # pset - self::ensure_array($array); + A::ensure_array($array); $current =& $array; $key = null; $last = count($pkey) - 1; @@ -297,7 +277,7 @@ class cl { $current = [$current]; } } else { - self::ensure_array($current[$key]); + A::ensure_array($current[$key]); $current =& $current[$key]; } $i++; @@ -318,7 +298,7 @@ class cl { * supprimer la valeur au chemin de clé $keys dans $array * * si $array vaut null ou false, sa valeur est inchangée. - * si $keys est vide ou null, $array devient null + * si $pkey est vide ou null, $array devient null */ static final function pdel(&$array, $pkey): void { # optimisations @@ -334,7 +314,7 @@ class cl { $pkey = explode(".", strval($pkey)); } # pdel - self::ensure_array($array); + A::ensure_array($array); $current =& $array; $key = null; $last = count($pkey) - 1; diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php new file mode 100644 index 0000000..6177bc4 --- /dev/null +++ b/php/src/db/Capacitor.php @@ -0,0 +1,56 @@ +storage = $storage; + $this->channel = $channel; + if ($ensureExists) $this->ensureExists(); + } + + /** @var CapacitorStorage */ + protected $storage; + + /** @var CapacitorChannel */ + protected $channel; + + function exists(): bool { + return $this->storage->_exists($this->channel); + } + + function ensureExists(): void { + $this->storage->_ensureExists($this->channel); + } + + function reset(): void { + $this->storage->_reset($this->channel); + } + + function charge($item, ?callable $func=null, ?array $args=null): int { + return $this->storage->_charge($this->channel, $item, $func, $args); + } + + function count($filter=null): int { + return $this->storage->_count($this->channel, $filter); + } + + function discharge($filter=null, ?bool $reset=null): iterable { + return $this->storage->_discharge($this->channel, $filter, $reset); + } + + function get($filter) { + return $this->storage->_get($this->channel, $filter); + } + + function each($filter, ?callable $func=null, ?array $args=null): int { + return $this->storage->_each($this->channel, $filter, $func, $args); + } + + function close(): void { + $this->storage->close(); + } +} diff --git a/php/src/db/CapacitorChannel.php b/php/src/db/CapacitorChannel.php new file mode 100644 index 0000000..b14308c --- /dev/null +++ b/php/src/db/CapacitorChannel.php @@ -0,0 +1,111 @@ +name = self::verifix_name($name ?? static::NAME); + $this->eachCommitThreshold = $eachCommitThreshold ?? static::EACH_COMMIT_THRESHOLD; + $this->created = false; + } + + /** @var string */ + protected $name; + + function getName(): string { + return $this->name; + } + + /** + * @var ?int nombre maximum de modifications dans une transaction avant un + * commit automatique dans {@link Capacitor::each()}. Utiliser null pour + * désactiver la fonctionnalité. + */ + protected $eachCommitThreshold; + + function getEachCommitThreshold(): ?int { + return $this->eachCommitThreshold; + } + + function getTableName(): string { + return $this->name."_channel"; + } + + protected $created; + + function isCreated(): bool { + return $this->created; + } + + function setCreated(bool $created=true): void { + $this->created = $created; + } + + /** + * retourner un ensemble de définitions pour des colonnes supplémentaires à + * insérer lors du chargement d'une valeur + * + * la colonne "_id" de définition "integer primary key autoincrement" est la + * clé primaire par défaut. elle peut être redéfinie, et dans ce cas la valeur + * à utiliser doit être retournée par {@link getKeyValues()} + */ + function getKeyDefinitions(): ?array { + return null; + } + + /** + * calculer les valeurs des colonnes supplémentaires à insérer pour le + * chargement de $item + * + * Si "_id" est retourné, la ligne existante est mise à jour le cas échéant. + */ + function getKeyValues($item): ?array { + return null; + } + + /** + * méthode appelée lors du chargement d'un élément avec + * {@link Capacitor::charge()} + * + * @param mixed $item l'élément à charger + * @param array $values les valeurs calculées par {@link getKeyValues()} + * @param ?array $row la ligne à mettre à jour. vaut null s'il faut insérer + * une nouvelle ligne + * @return ?array le cas échéant, un tableau non null à marger dans $values et + * utiliser pour provisionner la ligne nouvelle créée, ou mettre à jour la + * ligne existante + * + * Si $item est modifié dans cette méthode, il est possible de le retourner + * avec la clé "_item" pour mettre à jour la ligne correspondante + */ + function onCharge($item, array $values, ?array $row): ?array { + return null; + } + + /** + * méthode appelée lors du parcours des éléments avec + * {@link Capacitor::each()} + * + * @param mixed $item l'élément courant + * @param ?array $row la ligne à mettre à jour + * @return ?array le cas échéant, un tableau non null utilisé pour mettre à + * jour la ligne courante + * + * Si $item est modifié dans cette méthode, il est possible de le retourner + * avec la clé "_item" pour mettre à jour la ligne correspondante + */ + function onEach($item, array $row): ?array { + return null; + } +} diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php new file mode 100644 index 0000000..ed71dbd --- /dev/null +++ b/php/src/db/CapacitorStorage.php @@ -0,0 +1,92 @@ +_exists($this->getChannel($channel)); + } + + abstract function _ensureExists(CapacitorChannel $channel): void; + + /** s'assurer que le canal spécifié existe */ + function ensureExists(?string $channel): void { + $this->_ensureExists($this->getChannel($channel)); + } + + abstract function _reset(CapacitorChannel $channel): void; + + /** supprimer le canal spécifié */ + function reset(?string $channel): void { + $this->_reset($this->getChannel($channel)); + } + + abstract function _charge(CapacitorChannel $channel, $item, ?callable $func, ?array $args): int; + + /** + * charger une valeur dans le canal + * + * Si $func!==null, après avoir calculé les valeurs des clés supplémentaires + * avec {@link CapacitorChannel::getKeyValues()}, la fonction est appelée avec + * la signature ($item, $keyValues, $row, ...$args) + * Si la fonction retourne un tableau, il est utilisé pour modifier les valeurs + * insérées/mises à jour + * + * @return int 1 si l'objet a été chargé ou mis à jour, 0 s'il existait + * déjà à l'identique dans le canal + */ + function charge(?string $channel, $item, ?callable $func=null, ?array $args=null): int { + return $this->_charge($this->getChannel($channel), $item, $func, $args); + } + + abstract function _count(CapacitorChannel $channel, $filter): int; + + /** indiquer le nombre d'éléments du canal spécifié */ + function count(?string $channel, $filter=null): int { + return $this->_count($this->getChannel($channel), $filter); + } + + abstract function _discharge(CapacitorChannel $channel, $filter, ?bool $reset): iterable; + + /** décharger les données du canal spécifié */ + function discharge(?string $channel, $filter=null, ?bool $reset=null): iterable { + return $this->_discharge($this->getChannel($channel), $filter, $reset); + } + + abstract function _get(CapacitorChannel $channel, $filter); + + /** + * obtenir l'élément identifié par les clés spécifiées sur le canal spécifié + * + * si $filter n'est pas un tableau, il est transformé en ["_id" => $filter] + */ + function get(?string $channel, $filter) { + return $this->_get($this->getChannel($channel), $filter); + } + + abstract function _each(CapacitorChannel $channel, $filter, ?callable $func, ?array $args): int; + + /** + * appeler une fonction pour chaque élément du canal spécifié. + * + * $filter permet de filtrer parmi les élements chargés + * + * $func est appelé avec la signature ($item, $row, ...$args). si la fonction + * retourne un tableau, il est utilisé pour mettre à jour la ligne + * + * @return int le nombre de lignes parcourues + */ + function each(?string $channel, $filter, ?callable $func=null, ?array $args=null): int { + return $this->_each($this->getChannel($channel), $filter, $func, $args); + } + + abstract function close(): void; +} diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php new file mode 100644 index 0000000..df678e5 --- /dev/null +++ b/php/src/db/sqlite/Sqlite.php @@ -0,0 +1,226 @@ + $sqlite->file, + "flags" => $sqlite->flags, + "encryption_key" => $sqlite->encryptionKey, + "allow_wal" => $sqlite->allowWal, + "config" => $sqlite->config, + "migrate" => $sqlite->migration, + ], $params)); + } elseif (is_array($sqlite)) { + return new static(null, cl::merge($sqlite, $params)); + } else { + return new static($sqlite, $params); + } + } + + static function config_enableExceptions(self $sqlite): void { + $sqlite->db->enableExceptions(true); + } + + static function config_enableWalIfAllowed(self $sqlite): void { + if ($sqlite->isWalAllowed()) { + $sqlite->db->exec("PRAGMA journal_mode=WAL"); + } + } + + const ALLOW_WAL = null; + + const CONFIG = [ + [self::class, "config_enableExceptions"], + [self::class, "config_enableWalIfAllowed"], + ]; + + const MIGRATE = null; + + const SCHEMA = [ + "file" => ["string", ""], + "flags" => ["int", SQLITE3_OPEN_READWRITE + SQLITE3_OPEN_CREATE], + "encryption_key" => ["string", ""], + "allow_wal" => ["?bool"], + "config" => ["?array|callable"], + "migrate" => ["?array|string|callable"], + "auto_open" => ["bool", true], + ]; + + function __construct(?string $file=null, ?array $params=null) { + if ($file !== null) $params["file"] = $file; + ##schéma + $defaultFile = self::SCHEMA["file"][1]; + $this->file = $file = strval($params["file"] ?? $defaultFile); + $inMemory = $file === ":memory:"; + # + $defaultFlags = self::SCHEMA["flags"][1]; + $this->flags = intval($params["flags"] ?? $defaultFlags); + # + $defaultEncryptionKey = self::SCHEMA["encryption_key"][1]; + $this->encryptionKey = strval($params["encryption_key"] ?? $defaultEncryptionKey); + # + $defaultAllowWal = static::ALLOW_WAL ?? !$inMemory; + $this->allowWal = $params["allow_wal"] ?? $defaultAllowWal; + # configuration + $this->config = $params["config"] ?? static::CONFIG; + # migrations + $this->migration = $params["migrate"] ?? static::MIGRATE; + # + $defaultAutoOpen = self::SCHEMA["auto_open"][1]; + if ($params["auto_open"] ?? $defaultAutoOpen) { + $this->open(); + } + } + + /** @var string */ + protected $file; + + /** @var int */ + protected $flags; + + /** @var string */ + protected $encryptionKey; + + /** @var bool */ + protected $allowWal; + + /** vérifier s'il est autorisé de configurer le mode WAL */ + function isWalAllowed(): bool { + return $this->allowWal; + } + + /** @var array|string|callable */ + protected $config; + + /** @var array|string|callable */ + protected $migration; + + /** @var SQLite3 */ + protected $db; + + function open(): self { + if ($this->db === null) { + $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey); + _config::with($this->config)->configure($this); + _migration::with($this->migration)->migrate($this); + } + return $this; + } + + function close(): void { + if ($this->db !== null) { + $this->db->close(); + $this->db = null; + } + } + + protected function checkStmt($stmt): SQLite3Stmt { + return SqliteException::check($this->db, $stmt); + } + + protected function checkResult($result): SQLite3Result { + return SqliteException::check($this->db, $result); + } + + protected function db(): SQLite3 { + $this->open(); + return $this->db; + } + + function _exec(string $query): bool { + return $this->db()->exec($query); + } + + function exec($query, ?array $params=null): bool { + $db = $this->db(); + $query = new _query($query, $params); + if ($query->useStmt($db, $stmt, $sql)) { + try { + return $stmt->execute()->finalize(); + } finally { + $stmt->close(); + } + } else { + return $db->exec($sql); + } + } + + function beginTransaction(): void { + $this->db()->exec("begin"); + } + + function commit(): void { + $this->db()->exec("commit"); + } + + function rollback(): void { + $this->db()->exec("commit"); + } + + function _get(string $query, bool $entireRow=false) { + return $this->db()->querySingle($query, $entireRow); + } + + function get($query, ?array $params=null, bool $entireRow=false) { + $db = $this->db(); + $query = new _query($query, $params); + if ($query->useStmt($db, $stmt, $sql)) { + try { + $result = $this->checkResult($stmt->execute()); + try { + $row = $result->fetchArray(SQLITE3_ASSOC); + if ($row === false) return null; + elseif ($entireRow) return $row; + else return cl::first($row); + } finally { + $result->finalize(); + } + } finally { + $stmt->close(); + } + } else { + return $db->querySingle($sql, $entireRow); + } + } + + function one($query, ?array $params=null): ?array { + return $this->get($query, $params, true); + } + + protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null): Generator { + try { + while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) { + yield $row; + } + } finally { + $result->finalize(); + if ($stmt !== null) $stmt->close(); + } + } + + function all($query, ?array $params=null): iterable { + $db = $this->db(); + $query = new _query($query, $params); + if ($query->useStmt($db, $stmt, $sql)) { + $result = $this->checkResult($stmt->execute()); + return $this->_fetchResult($result, $stmt); + } else { + $result = $this->checkResult($db->query($sql)); + return $this->_fetchResult($result); + } + } +} diff --git a/php/src/db/sqlite/SqliteCapacitor.php b/php/src/db/sqlite/SqliteCapacitor.php new file mode 100644 index 0000000..3cb855c --- /dev/null +++ b/php/src/db/sqlite/SqliteCapacitor.php @@ -0,0 +1,241 @@ +sqlite = Sqlite::with($sqlite); + } + + /** @var Sqlite */ + protected $sqlite; + + function sqlite(): Sqlite { + return $this->sqlite; + } + + protected function _create(CapacitorChannel $channel): void { + if (!$channel->isCreated()) { + $columns = cl::merge([ + "_id" => "integer primary key autoincrement", + "_item" => "text", + "_sum" => "varchar(40)", + "_created" => "datetime", + "_modified" => "datetime", + ], $channel->getKeyDefinitions()); + $this->sqlite->exec([ + "create table if not exists", + "table" => $channel->getTableName(), + "cols" => $columns, + ]); + $channel->setCreated(); + } + } + + /** @var CapacitorChannel[] */ + protected $channels; + + function addChannel(CapacitorChannel $channel): CapacitorChannel { + $this->_create($channel); + $this->channels[$channel->getName()] = $channel; + return $channel; + } + + protected function getChannel(?string $name): CapacitorChannel { + $name = CapacitorChannel::verifix_name($name); + $channel = $this->channels[$name] ?? null; + if ($channel === null) { + $channel = $this->addChannel(new CapacitorChannel($name)); + } + return $channel; + } + + function _exists(CapacitorChannel $channel): bool { + $tableName = $this->sqlite->get([ + "select name from sqlite_schema", + "where" => [ + "name" => $channel->getTableName(), + ], + ]); + return $tableName !== null; + } + + function _ensureExists(CapacitorChannel $channel): void { + $this->_create($channel); + } + + function _reset(CapacitorChannel $channel): void { + $this->sqlite->exec([ + "drop table if exists", + $channel->getTableName(), + ]); + $channel->setCreated(false); + } + + function _charge(CapacitorChannel $channel, $item, ?callable $func, ?array $args): int { + $this->_create($channel); + $now = date("Y-m-d H:i:s"); + $_item = serialize($item); + $_sum = sha1($_item); + $values = cl::merge([ + "_item" => $_item, + "_sum" => $_sum, + ], $channel->getKeyValues($item)); + $row = null; + $id = $values["_id"] ?? null; + if ($id !== null) { + # modification + $row = $this->sqlite->one([ + "select _item, _sum, _created, _modified", + "from" => $channel->getTableName(), + "where" => ["_id" => $id], + ]); + } + $insert = null; + if ($row === null) { + # création + $values = cl::merge($values, [ + "_created" => $now, + "_modified" => $now, + ]); + $insert = true; + } elseif ($_sum !== $row["_sum"]) { + # modification + $values = cl::merge($values, [ + "_modified" => $now, + ]); + $insert = false; + } + + if ($func === null) $func = [$channel, "onCharge"]; + $onCharge = func::_prepare($func); + $args ??= []; + $updates = func::_call($onCharge, [$item, $values, $row, ...$args]); + if (is_array($updates)) { + if (array_key_exists("_item", $updates)) { + $_item = serialize($updates["_item"]); + $updates["_item"] = $_item; + $updates["_sum"] = sha1($_item); + if (!array_key_exists("_modified", $updates)) { + $updates["_modified"] = $now; + } + } + $values = cl::merge($values, $updates); + } + + if ($insert === null) { + # aucune modification + return 0; + } elseif ($insert) { + $this->sqlite->exec([ + "insert", + "into" => $channel->getTableName(), + "values" => $values, + ]); + } else { + $this->sqlite->exec([ + "update", + "table" => $channel->getTableName(), + "values" => $values, + "where" => ["_id" => $id], + ]); + } + return 1; + } + + function _count(CapacitorChannel $channel, $filter): int { + if ($filter !== null && !is_array($filter)) $filter = ["_id" => $filter]; + return $this->sqlite->get([ + "select count(*)", + "from" => $channel->getTableName(), + "where" => $filter, + ]); + } + + function _discharge(CapacitorChannel $channel, $filter, ?bool $reset): iterable { + if ($filter !== null && !is_array($filter)) $filter = ["_id" => $filter]; + if ($reset === null) $reset = $filter === null; + $rows = $this->sqlite->all([ + "select _item", + "from" => $channel->getTableName(), + "where" => $filter, + ]); + foreach ($rows as $row) { + $item = unserialize($row['_item']); + yield $item; + } + if ($reset) $this->_reset($channel); + } + + function _get(CapacitorChannel $channel, $filter) { + if ($filter === null) throw ValueException::null("keys"); + if (!is_array($filter)) $filter = ["_id" => $filter]; + $row = $this->sqlite->one([ + "select _item", + "from" => $channel->getTableName(), + "where" => $filter, + ]); + if ($row === null) return null; + else return unserialize($row["_item"]); + } + + function _each(CapacitorChannel $channel, $filter, ?callable $func, ?array $args): int { + if ($func === null) $func = [$channel, "onEach"]; + $onEach = func::_prepare($func); + if ($filter !== null && !is_array($filter)) $filter = ["_id" => $filter]; + $sqlite = $this->sqlite; + $tableName = $channel->getTableName(); + $commited = false; + $count = 0; + $sqlite->beginTransaction(); + $commitThreshold = $channel->getEachCommitThreshold(); + try { + $rows = $sqlite->all([ + "select", + "from" => $tableName, + "where" => $filter, + ]); + $args ??= []; + foreach ($rows as $row) { + $item = unserialize($row['_item']); + $updates = func::_call($onEach, [$item, $row, ...$args]); + if (is_array($updates)) { + if (array_key_exists("_item", $updates)) { + $updates["_item"] = serialize($updates["_item"]); + } + $sqlite->exec([ + "update", + "table" => $tableName, + "values" => $updates, + "where" => ["_id" => $row["_id"]], + ]); + if ($commitThreshold !== null) { + $commitThreshold--; + if ($commitThreshold == 0) { + $sqlite->commit(); + $commitThreshold = $channel->getEachCommitThreshold(); + } + } + } + $count++; + } + $sqlite->commit(); + $commited = true; + return $count; + } finally { + if (!$commited) $sqlite->rollback(); + } + } + + function close(): void { + $this->sqlite->close(); + } +} diff --git a/php/src/db/sqlite/SqliteException.php b/php/src/db/sqlite/SqliteException.php new file mode 100644 index 0000000..b71fc38 --- /dev/null +++ b/php/src/db/sqlite/SqliteException.php @@ -0,0 +1,18 @@ +lastErrorMsg(), $db->lastErrorCode()); + } + + static final function wrap(Exception $e): self{ + return new static($e->getMessage(), $e->getCode(), $e); + } +} diff --git a/php/src/db/sqlite/_config.php b/php/src/db/sqlite/_config.php new file mode 100644 index 0000000..f0ef2ed --- /dev/null +++ b/php/src/db/sqlite/_config.php @@ -0,0 +1,36 @@ +configs = $configs; + } + + /** @var array */ + protected $configs; + + function configure(Sqlite $sqlite): void { + foreach ($this->configs as $key => $config) { + if (is_string($config) && !func::is_method($config)) { + $sqlite->exec($config); + } else { + func::ensure_func($config, $this, $args); + func::call($config, $sqlite, $key, ...$args); + } + } + } +} diff --git a/php/src/db/sqlite/_migration.php b/php/src/db/sqlite/_migration.php new file mode 100644 index 0000000..6f80f04 --- /dev/null +++ b/php/src/db/sqlite/_migration.php @@ -0,0 +1,55 @@ +migrations); + } else { + return new static($migrations); + } + } + + const MIGRATE = null; + + function __construct($migrations) { + if ($migrations === null) $migrations = static::MIGRATE; + if ($migrations === null) $migrations = []; + elseif (is_string($migrations)) $migrations = [$migrations]; + elseif (is_callable($migrations)) $migrations = [$migrations]; + elseif (!is_array($migrations)) $migrations = [strval($migrations)]; + $this->migrations = $migrations; + } + + /** @var callable[]|string[] */ + protected $migrations; + + function migrate(Sqlite $sqlite): void { + $sqlite->exec("create table if not exists _migration(key varchar primary key, value varchar not null, done integer default 0)"); + foreach ($this->migrations as $key => $migration) { + $exists = $sqlite->get("select 1 from _migration where key = :key and done = 1", [ + "key" => $key, + ]); + if (!$exists) { + $sqlite->exec("insert or replace into _migration(key, value, done) values(:key, :value, :done)", [ + "key" => $key, + "value" => $migration, + "done" => 0, + ]); + if (is_string($migration) && !func::is_method($migration)) { + $sqlite->exec($migration); + } else { + func::ensure_func($migration, $this, $args); + func::call($migration, $sqlite, $key, ...$args); + } + $sqlite->exec("update _migration set done = 1 where key = :key", [ + "key" => $key, + ]); + } + } + } +} diff --git a/php/src/db/sqlite/_query.php b/php/src/db/sqlite/_query.php new file mode 100644 index 0000000..647cd6b --- /dev/null +++ b/php/src/db/sqlite/_query.php @@ -0,0 +1,212 @@ + $value) { + if ($key === $index) { + $index++; + if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) { + $sql .= " "; + } + $sql .= $value; + } + } + return $sql; + } + + protected static function is_sep(&$cond): bool { + if (!is_string($cond)) return false; + if (!preg_match('/^\s*(and|or|not)\s*$/i', $cond, $ms)) return false; + $cond = $ms[1]; + return true; + } + + static function parse_conds(?array $conds, ?array &$sql, ?array &$params): void { + if (!$conds) return; + $sep = null; + $index = 0; + $condsql = []; + foreach ($conds as $key => $cond) { + if ($key === $index) { + ## séquentiel + if ($index === 0 && self::is_sep($cond)) { + $sep = $cond; + } elseif (is_array($cond)) { + # condition récursive + self::parse_conds($cond, $condsql, $params); + } else { + # condition litérale + $condsql[] = strval($cond); + } + $index++; + } else { + ## associatif + # paramètre + $param = $key; + if ($params !== null && array_key_exists($param, $params)) { + $i = 1; + while (array_key_exists("$key$i", $params)) { + $i++; + } + $param = "$key$i"; + } + # value ou [operator, value] + if (is_array($cond)) { + #XXX implémenter le support de ["between", lower, upper] + # et aussi ["in", values] + $op = null; + $value = null; + $condkeys = array_keys($cond); + if (array_key_exists("op", $cond)) $op = $cond["op"]; + if (array_key_exists("value", $cond)) $value = $cond["value"]; + $condkey = 0; + if ($op === null && array_key_exists($condkey, $condkeys)) { + $op = $cond[$condkeys[$condkey]]; + $condkey++; + } + if ($value === null && array_key_exists($condkey, $condkeys)) { + $value = $cond[$condkeys[$condkey]]; + $condkey++; + } + } else { + $op = "="; + $value = $cond; + } + $cond = [$key, $op]; + if ($value !== null) { + $cond[] = ":$param"; + $params[$param] = $value; + } + $condsql[] = implode(" ", $cond); + } + } + if ($sep === null) $sep = "and"; + $count = count($condsql); + if ($count > 1) { + $sql[] = "(" . implode(" $sep ", $condsql) . ")"; + } elseif ($count == 1) { + $sql[] = $condsql[0]; + } + } + + static function parse_set_values(?array $values, ?array &$sql, ?array &$params): void { + if (!$values) return; + $index = 0; + $parts = []; + foreach ($values as $key => $part) { + if ($key === $index) { + ## séquentiel + if (is_array($part)) { + # paramètres récursifs + self::parse_set_values($part, $parts, $params); + } else { + # paramètre litéral + $parts[] = strval($part); + } + $index++; + } else { + ## associatif + # paramètre + $param = $key; + if ($params !== null && array_key_exists($param, $params)) { + $i = 1; + while (array_key_exists("$key$i", $params)) { + $i++; + } + $param = "$key$i"; + } + # value + $value = $part; + $part = [$key, "="]; + if ($value === null) { + $part[] = "null"; + } else { + $part[] = ":$param"; + $params[$param] = $value; + } + $parts[] = implode(" ", $part); + } + } + $sql = cl::merge($sql, $parts); + } + + protected static function check_eof(string $tmpsql, string $usersql): void { + self::consume(';\s*', $tmpsql); + if ($tmpsql) { + throw new ValueException("unexpected value at end: $usersql"); + } + } + + function __construct($sql, ?array $params=null) { + self::verifix($sql, $params); + $this->sql = $sql; + $this->params = $params; + } + + /** @var string */ + protected $sql; + + /** @var ?array */ + protected $params; + + function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool { + if ($this->params !== null) { + /** @var SQLite3Stmt $stmt */ + $stmt = SqliteException::check($db, $db->prepare($this->sql)); + $close = true; + try { + foreach ($this->params as $param => $value) { + SqliteException::check($db, $stmt->bindValue($param, $value)); + } + $close = false; + return true; + } finally { + if ($close) $stmt->close(); + } + } else { + $sql = $this->sql; + return false; + } + } +} diff --git a/php/src/db/sqlite/_query_create.php b/php/src/db/sqlite/_query_create.php new file mode 100644 index 0000000..84868ff --- /dev/null +++ b/php/src/db/sqlite/_query_create.php @@ -0,0 +1,47 @@ + "?string", + "table" => "string", + "schema" => "?array", + "cols" => "?array", + "suffix" => "?string", + ]; + + static function isa(string $sql): bool { + //return preg_match("/^create(?:\s+table)?\b/i", $sql); + #XXX implémentation minimale + return preg_match("/^create\s+table\b/i", $sql); + } + + static function parse(array $query, ?array &$params=null): string { + #XXX implémentation minimale + $sql = [self::merge_seq($query)]; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## table + $sql[] = $query["table"]; + + ## columns + $cols = $query["cols"]; + $index = 0; + foreach ($cols as $col => &$definition) { + if ($col === $index) { + $index++; + } else { + $definition = "$col $definition"; + } + }; unset($definition); + $sql[] = "(".implode(", ", $cols).")"; + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); + } +} diff --git a/php/src/db/sqlite/_query_delete.php b/php/src/db/sqlite/_query_delete.php new file mode 100644 index 0000000..7d24092 --- /dev/null +++ b/php/src/db/sqlite/_query_delete.php @@ -0,0 +1,44 @@ + "?string", + "from" => "?string", + "where" => "?array", + "suffix" => "?string", + ]; + + static function isa(string $sql): bool { + //return preg_match("/^delete(?:\s+from)?\b/i", $sql); + #XXX implémentation minimale + return preg_match("/^delete\s+from\b/i", $sql); + } + + static function parse(array $query, ?array &$params=null): string { + #XXX implémentation minimale + $sql = [self::merge_seq($query)]; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## table + $sql[] = $query["table"]; + + ## where + $where = $query["where"] ?? null; + if ($where !== null) { + _query::parse_conds($where, $wheresql, $params); + if ($wheresql) { + $sql[] = "where"; + $sql[] = implode(" and ", $wheresql); + } + } + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); + } +} diff --git a/php/src/db/sqlite/_query_generic.php b/php/src/db/sqlite/_query_generic.php new file mode 100644 index 0000000..78f37e4 --- /dev/null +++ b/php/src/db/sqlite/_query_generic.php @@ -0,0 +1,18 @@ + "?string", + "into" => "?string", + "schema" => "?array", + "cols" => "?array", + "values" => "?array", + "suffix" => "?string", + ]; + + static function isa(string $sql): bool { + return preg_match("/^insert\b/i", $sql); + } + + /** + * parser une chaine de la forme + * "insert [into] [TABLE] [(COLS)] [values (VALUES)]" + */ + static function parse(array $query, ?array &$params=null): string { + # fusionner d'abord toutes les parties séquentielles + $usersql = $tmpsql = self::merge_seq($query); + + ### vérifier la présence des parties nécessaires + $sql = []; + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## insert + self::consume('insert\s*', $tmpsql); + $sql[] = "insert"; + + ## into + self::consume('into\s*', $tmpsql); + $sql[] = "into"; + $into = $query["into"] ?? null; + if (self::consume('([a-z_][a-z0-9_]*)\s*', $tmpsql, $ms)) { + if ($into === null) $into = $ms[1]; + $sql[] = $into; + } elseif ($into !== null) { + $sql[] = $into; + } else { + throw new ValueException("expected table name: $usersql"); + } + + ## cols & values + $usercols = []; + $uservalues = []; + if (self::consume('\(([^)]*)\)\s*', $tmpsql, $ms)) { + $usercols = array_merge($usercols, preg_split("/\s*,\s*/", $ms[1])); + } + $cols = cl::withn($query["cols"] ?? null); + $values = cl::withn($query["values"] ?? null); + $schema = $query["schema"] ?? null; + if ($cols === null) { + if ($usercols) { + $cols = $usercols; + } elseif ($values) { + $cols = array_keys($values); + $usercols = array_merge($usercols, $cols); + } elseif ($schema && is_array($schema)) { + #XXX implémenter support AssocSchema + $cols = array_keys($schema); + $usercols = array_merge($usercols, $cols); + } + } + if (self::consume('values\s+\(\s*(.*)\s*\)\s*', $tmpsql, $ms)) { + if ($ms[1]) $uservalues[] = $ms[1]; + } + if ($cols !== null && !$uservalues) { + if (!$usercols) $usercols = $cols; + foreach ($cols as $col) { + $uservalues[] = ":$col"; + $params[$col] = $values[$col] ?? null; + } + } + $sql[] = "(" . implode(", ", $usercols) . ")"; + $sql[] = "values (" . implode(", ", $uservalues) . ")"; + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + self::check_eof($tmpsql, $usersql); + return implode(" ", $sql); + } +} diff --git a/php/src/db/sqlite/_query_select.php b/php/src/db/sqlite/_query_select.php new file mode 100644 index 0000000..a37e957 --- /dev/null +++ b/php/src/db/sqlite/_query_select.php @@ -0,0 +1,169 @@ + "?string", + "schema" => "?array", + "cols" => "?array", + "from" => "?string", + "where" => "?array", + "order by" => "?array", + "group by" => "?array", + "having" => "?array", + "suffix" => "?string", + ]; + + static function isa(string $sql): bool { + return preg_match("/^select\b/i", $sql); + } + + /** + * parser une chaine de la forme + * "select [COLS] [from TABLE] [where CONDS] [order by ORDERS] [group by GROUPS] [having CONDS]" + */ + static function parse(array $query, ?array &$params=null): string { + # fusionner d'abord toutes les parties séquentielles + $usersql = $tmpsql = self::merge_seq($query); + + ### vérifier la présence des parties nécessaires + $sql = []; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## select + self::consume('select\s*', $tmpsql); + $sql[] = "select"; + + ## cols + $usercols = []; + if (self::consume('(.*?)\s*(?=$|\bfrom\b)', $tmpsql, $ms)) { + if ($ms[1]) $usercols[] = $ms[1]; + } + $tmpcols = cl::withn($query["cols"] ?? null); + $schema = $query["schema"] ?? null; + if ($tmpcols !== null) { + $cols = []; + $index = 0; + foreach ($tmpcols as $key => $col) { + if ($key === $index) { + $index++; + $cols[] = $col; + $usercols[] = $col; + } else { + $cols[] = $key; + $usercols[] = "$col as $key"; + } + } + } else { + $cols = null; + if ($schema && is_array($schema) && !in_array("*", $usercols)) { + $cols = array_keys($schema); + $usercols = array_merge($usercols, $cols); + } + } + if (!$usercols && !$cols) $usercols = ["*"]; + $sql[] = implode(" ", $usercols); + + ## from + $from = $query["from"] ?? null; + if (self::consume('from\s+([a-z_][a-z0-9_]*)\s*(?=;?\s*$|\bwhere\b)', $tmpsql, $ms)) { + if ($from === null) $from = $ms[1]; + $sql[] = "from"; + $sql[] = $from; + } elseif ($from !== null) { + $sql[] = "from"; + $sql[] = $from; + } else { + throw new ValueException("expected table name: $usersql"); + } + + ## where + $userwhere = []; + if (self::consume('where\b\s*(.*?)(?=;?\s*$|\border\s+by\b)', $tmpsql, $ms)) { + if ($ms[1]) $userwhere[] = $ms[1]; + } + $where = cl::withn($query["where"] ?? null); + if ($where !== null) self::parse_conds($where, $userwhere, $params); + if ($userwhere) { + $sql[] = "where"; + $sql[] = implode(" and ", $userwhere); + } + + ## order by + $userorderby = []; + if (self::consume('order\s+by\b\s*(.*?)(?=;?\s*$|\bgroup\s+by\b)', $tmpsql, $ms)) { + if ($ms[1]) $userorderby[] = $ms[1]; + } + $orderby = cl::withn($query["order by"] ?? null); + if ($orderby !== null) { + $index = 0; + foreach ($orderby as $key => $value) { + if ($key === $index) { + $userorderby[] = $value; + $index++; + } else { + if ($value === null) $value = false; + if (!is_bool($value)) { + $userorderby[] = "$key $value"; + } elseif ($value) { + $userorderby[] = $key; + } + } + } + } + if ($userorderby) { + $sql[] = "order by"; + $sql[] = implode(", ", $userorderby); + } + ## group by + $usergroupby = []; + if (self::consume('group\s+by\b\s*(.*?)(?=;?\s*$|\bhaving\b)', $tmpsql, $ms)) { + if ($ms[1]) $usergroupby[] = $ms[1]; + } + $groupby = cl::withn($query["group by"] ?? null); + if ($groupby !== null) { + $index = 0; + foreach ($groupby as $key => $value) { + if ($key === $index) { + $usergroupby[] = $value; + $index++; + } else { + if ($value === null) $value = false; + if (!is_bool($value)) { + $usergroupby[] = "$key $value"; + } elseif ($value) { + $usergroupby[] = $key; + } + } + } + } + if ($usergroupby) { + $sql[] = "group by"; + $sql[] = implode(", ", $usergroupby); + } + + ## having + $userhaving = []; + if (self::consume('having\b\s*(.*?)(?=;?\s*$)', $tmpsql, $ms)) { + if ($ms[1]) $userhaving[] = $ms[1]; + } + $having = cl::withn($query["having"] ?? null); + if ($having !== null) self::parse_conds($having, $userhaving, $params); + if ($userhaving) { + $sql[] = "having"; + $sql[] = implode(" and ", $userhaving); + } + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + self::check_eof($tmpsql, $usersql); + return implode(" ", $sql); + } +} diff --git a/php/src/db/sqlite/_query_update.php b/php/src/db/sqlite/_query_update.php new file mode 100644 index 0000000..3620f04 --- /dev/null +++ b/php/src/db/sqlite/_query_update.php @@ -0,0 +1,50 @@ + "?string", + "table" => "?string", + "schema" => "?array", + "cols" => "?array", + "values" => "?array", + "where" => "?array", + "suffix" => "?string", + ]; + + static function isa(string $sql): bool { + return preg_match("/^update\b/i", $sql); + } + + static function parse(array $query, ?array &$params=null): string { + #XXX implémentation minimale + $sql = [self::merge_seq($query)]; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## table + $sql[] = $query["table"]; + + ## set + _query::parse_set_values($query["values"], $setsql, $params); + $sql[] = "set"; + $sql[] = implode(", ", $setsql); + + ## where + $where = $query["where"] ?? null; + if ($where !== null) { + _query::parse_conds($where, $wheresql, $params); + if ($wheresql) { + $sql[] = "where"; + $sql[] = implode(" and ", $wheresql); + } + } + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); + } +} diff --git a/php/src/file.php b/php/src/file.php index 360f35f..31334fe 100644 --- a/php/src/file.php +++ b/php/src/file.php @@ -1,12 +1,12 @@ lock(LOCK_SH); + } + /** * essayer de verrouiller le fichier en lecture. retourner true si l'opération * réussit. dans ce cas, il faut appeler {@link getReader()} avec l'argument @@ -248,8 +254,8 @@ class Stream extends AbstractIterator implements IReader, IWriter { * verrouiller en mode partagé puis retourner un objet permettant de lire le * fichier. */ - function getReader(bool $lockedByCanRead=false): IReader { - if ($this->useLocking && !$lockedByCanRead) $this->lock(LOCK_SH); + function getReader(bool $alreadyLocked=false): IReader { + if ($this->useLocking && !$alreadyLocked) $this->lock(LOCK_SH); return new class($this->fd, ++$this->serial, $this) extends Stream { function __construct($fd, int $serial, Stream $parent) { $this->parent = $parent; @@ -270,9 +276,9 @@ class Stream extends AbstractIterator implements IReader, IWriter { } /** retourner le contenu du fichier sous forme de chaine */ - function getContents(bool $close=true, bool $lockedByCanRead=false): string { + function getContents(bool $close=true, bool $alreadyLocked=false): string { $useLocking = $this->useLocking; - if ($useLocking && !$lockedByCanRead) $this->lock(LOCK_SH); + if ($useLocking && !$alreadyLocked) $this->lock(LOCK_SH); try { return IOException::ensure_valid(stream_get_contents($this->fd), $this->throwOnError); } finally { @@ -281,8 +287,8 @@ class Stream extends AbstractIterator implements IReader, IWriter { } } - function unserialize(?array $options=null, bool $close=true, bool $lockedByCanRead=false) { - $args = [$this->getContents($lockedByCanRead)]; + function unserialize(?array $options=null, bool $close=true, bool $alreadyLocked=false) { + $args = [$this->getContents($close, $alreadyLocked)]; if ($options !== null) $args[] = $options; return unserialize(...$args); } @@ -331,9 +337,10 @@ class Stream extends AbstractIterator implements IReader, IWriter { } /** @throws IOException */ - function ftruncate(int $size): self { + function ftruncate(int $size=0, bool $rewind=true): self { $fd = $this->getResource(); IOException::ensure_valid(ftruncate($fd, $size), $this->throwOnError); + if ($rewind) rewind($fd); return $this; } @@ -347,6 +354,14 @@ class Stream extends AbstractIterator implements IReader, IWriter { return $this; } + /** + * verrouiller le fichier en écriture de façon inconditionelle (ignorer la + * valeur de $useLocking). bloquer jusqu'à ce que le verrou soit disponible + */ + function lockWrite(): void { + $this->lock(LOCK_EX); + } + /** * essayer de verrouiller le fichier en écriture. retourner true si l'opération * réussit. dans ce cas, il faut appeler {@link getWriter()} avec l'argument @@ -361,8 +376,8 @@ class Stream extends AbstractIterator implements IReader, IWriter { * verrouiller en mode exclusif puis retourner un objet permettant d'écrire * dans le fichier */ - function getWriter(bool $lockedByCanWrite=false): IWriter { - if ($this->useLocking && !$lockedByCanWrite) $this->lock(LOCK_EX); + function getWriter(bool $alreadyLocked=false): IWriter { + if ($this->useLocking && !$alreadyLocked) $this->lock(LOCK_EX); return new class($this->fd, ++$this->serial, $this) extends Stream { function __construct($fd, int $serial, Stream $parent) { $this->parent = $parent; @@ -383,9 +398,9 @@ class Stream extends AbstractIterator implements IReader, IWriter { }; } - function putContents(string $contents, bool $close=true, bool $lockedByCanWrite=false): void { + function putContents(string $contents, bool $close=true, bool $alreadyLocked=false): void { $useLocking = $this->useLocking; - if ($useLocking && !$lockedByCanWrite) $this->lock(LOCK_EX); + if ($useLocking && !$alreadyLocked) $this->lock(LOCK_EX); try { $this->fwrite($contents); } finally { @@ -394,7 +409,7 @@ class Stream extends AbstractIterator implements IReader, IWriter { } } - function serialize($object, bool $close=true, bool $lockedByCanWrite=false): void { - $this->putContents(serialize($object), $lockedByCanWrite); + function serialize($object, bool $close=true, bool $alreadyLocked=false): void { + $this->putContents(serialize($object), $close, $alreadyLocked); } } diff --git a/php/src/file/base/TStreamFilter.php b/php/src/file/TStreamFilter.php similarity index 96% rename from php/src/file/base/TStreamFilter.php rename to php/src/file/TStreamFilter.php index 5f15ef2..93063ae 100644 --- a/php/src/file/base/TStreamFilter.php +++ b/php/src/file/TStreamFilter.php @@ -1,7 +1,6 @@ file; + } + /** @var string */ protected $mode; + function getMode(): string { + return $this->mode; + } + /** @return resource */ protected function open() { return IOException::ensure_valid(@fopen($this->file, $this->mode)); diff --git a/php/src/file/app/LockFile.php b/php/src/file/app/LockFile.php new file mode 100644 index 0000000..cc11b49 --- /dev/null +++ b/php/src/file/app/LockFile.php @@ -0,0 +1,87 @@ +file = new SharedFile($file); + $this->name = $name ?? static::NAME; + $this->title = $title ?? static::TITLE; + } + + /** @var SharedFile */ + protected $file; + + /** @var ?string */ + protected $name; + + /** @var ?string */ + protected $title; + + protected function initData(): array { + return [ + "name" => $this->name, + "title" => $this->title, + "locked" => false, + "date_lock" => null, + "date_release" => null, + ]; + } + + function read(bool $close=true): array { + $data = $this->file->unserialize(null, $close); + if (!is_array($data)) $data = $this->initData(); + return $data; + } + + function isLocked(?array &$data=null): bool { + $data = $this->read(); + return $data["locked"]; + } + + function warnIfLocked(?array $data=null): void { + if ($data === null) $data = $this->read(); + if ($data["locked"]) { + msg::warning("$data[name]: possède le verrou depuis $data[date_lock] -- $data[title]"); + } + } + + function lock(?array &$data=null): bool { + $file = $this->file; + $data = $this->read(false); + if ($data["locked"]) { + $file->close(); + return false; + } else { + $file->ftruncate(); + $file->serialize(cl::merge($data, [ + "locked" => true, + "date_lock" => new DateTime(), + "date_release" => null, + ])); + return true; + } + } + + function release(?array &$data=null): void { + $file = $this->file; + $data = $this->read(false); + $file->ftruncate(); + $file->serialize(cl::merge($data, [ + "locked" => false, + "date_release" => new DateTime(), + ])); + } +} diff --git a/php/src/file/app/RunFile.php b/php/src/file/app/RunFile.php new file mode 100644 index 0000000..c931804 --- /dev/null +++ b/php/src/file/app/RunFile.php @@ -0,0 +1,129 @@ +file = new SharedFile($file); + $this->name = $name ?? static::NAME; + } + + /** @var SharedFile */ + protected $file; + + /** @var ?string */ + protected $name; + + protected static function merge(array $data, array $merge): array { + return cl::merge($data, [ + "serial" => $data["serial"] + 1, + ], $merge); + } + + protected function initData(bool $withDateStart=true): array { + $dateStart = $withDateStart? new DateTime(): null; + return [ + "name" => $this->name, + "serial" => 0, + "date_start" => $dateStart, + "date_stop" => null, + "action" => null, + "action_date_start" => null, + "action_max_step" => null, + "action_current_step" => null, + "action_date_step" => null, + ]; + } + + function read(): array { + $data = $this->file->unserialize(); + if (!is_array($data)) $data = $this->initData(false); + return $data; + } + + /** tester si l'application est démarrée */ + function isStarted(): bool { + $data = $this->read(); + return $data["date_start"] !== null; + } + + /** tester si l'application est arrêtée */ + function isStopped(): bool { + $data = $this->read(); + return $data["date_stop"] !== null; + } + + function haveWorked(int $serial, ?int &$currentSerial=null): bool { + $data = $this->read(); + return $serial !== $data["serial"]; + } + + protected function willWrite(): array { + $file = $this->file; + $file->lockWrite(); + $data = $file->unserialize(null, false, true); + if (!is_array($data)) { + $data = $this->initData(); + $file->ftruncate(); + $file->serialize($data, false, true); + } + $file->ftruncate(); + return [$file, $data]; + } + + /** indiquer que l'application démarre */ + function start(): void { + $this->file->serialize($this->initData()); + } + + /** indiquer le début d'une action */ + function action(?string $title, ?int $maxSteps=null): void { + [$file, $data] = $this->willWrite(); + $file->serialize(self::merge($data, [ + "action" => $title, + "action_date_start" => new DateTime(), + "action_max_step" => $maxSteps, + "action_current_step" => 0, + ])); + } + + /** indiquer qu'une étape est franchie dans l'action en cours */ + function step(int $nbSteps=1): void { + [$file, $data] = $this->willWrite(); + $file->serialize(self::merge($data, [ + "action_date_step" => new DateTime(), + "action_current_step" => $data["action_current_step"] + $nbSteps, + ])); + } + + /** indiquer que l'application s'arrête */ + function stop(): void { + [$file, $data] = $this->willWrite(); + $file->serialize(self::merge($data, [ + "date_stop" => new DateTime(), + ])); + } + + function getLockFile(?string $name=null, ?string $title=null): LockFile { + $ext = self::LOCK_EXT; + if ($name !== null) $ext = ".$name$ext"; + $file = path::ensure_ext($this->file->getFile(), $ext, self::RUN_EXT); + $name = str::join("/", [$this->name, $name]); + return new LockFile($file, $name, $title); + } +} diff --git a/php/src/file/csv/csv_flavours.php b/php/src/file/csv/csv_flavours.php index 334c0f6..d21bd6e 100644 --- a/php/src/file/csv/csv_flavours.php +++ b/php/src/file/csv/csv_flavours.php @@ -2,7 +2,7 @@ namespace nulib\file\csv; use nulib\cl; -use nulib\ref\os\csv\ref_csv; +use nulib\ref\file\csv\ref_csv; class csv_flavours { const MAP = [ diff --git a/php/src/file/web/Upload.php b/php/src/file/web/Upload.php new file mode 100644 index 0000000..bcafcb9 --- /dev/null +++ b/php/src/file/web/Upload.php @@ -0,0 +1,104 @@ + "Ceci n'est pas un fichier téléversé", + "nofile" => "Aucun fichier n'a été fourni", + "toobig" => "Le fichier que vous avez fourni est trop volumineux.", + "unknown" => "Une erreur s'est produite pendant le transfert du fichier. Veuillez réessayer.", + ]; + + protected static function error(string $message) { + return new ValueException(static::MESSAGES[$message]); + } + + function __construct(?array $file, bool $required=true, bool $check=true) { + parent::__construct($file); + if ($check) $this->check($required); + } + + function check(bool $required=true, bool $throw=true): bool { + $file = $this->data; + if ($file) { + $name = $file["name"] ?? null; + $type = $file["type"] ?? null; + $error = $file["error"] ?? null; + if (!is_scalar($name) || !is_scalar($type) || !is_scalar($error)) { + if ($throw) throw static::error("invalid"); + else return false; + } + switch ($error) { + case UPLOAD_ERR_OK: + break; + case UPLOAD_ERR_NO_FILE: + if ($required) { + if ($throw) throw self::error("nofile"); + else return false; + } + break; + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + if ($throw) throw self::error("toobig"); + else return false; + default: + if ($throw) self::error("unknown"); + else return false; + } + } elseif ($required) { + if ($throw) throw static::error("nofile"); + else return false; + } + return true; + } + + const _AUTO_PROPERTIES = [ + "tmpName" => "tmp_name", + "fullPath" => "full_path", + ]; + function &__get($name) { + $name = static::_AUTO_PROPERTIES[$name] ?? $name; + return parent::__get($name); + } + + function isError(): bool { + $error = $this->error; + return $error !== UPLOAD_ERR_OK && $error !== UPLOAD_ERR_NO_FILE; + } + + function isValid(): bool { + return $this->error === UPLOAD_ERR_OK; + } + + /** @var ?string chemin du fichier, s'il a été déplacé */ + protected $file; + + function moveTo(string $dest): bool { + if ($this->file === null) { + $moved = move_uploaded_file($this->tmpName, $dest); + if ($moved) $this->file = $dest; + } else { + $moved = false; + } + return $moved; + } + + function getFile(): FileReader { + $file = $this->file ?? $this->tmpName; + return new FileReader($file, "r+b"); + } +} diff --git a/php/src/output/IContent.php b/php/src/output/IContent.php deleted file mode 100644 index 0b92311..0000000 --- a/php/src/output/IContent.php +++ /dev/null @@ -1,10 +0,0 @@ - ou renommer `say` en `console`, et `ui` en `say` -*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/output/_messenger.php b/php/src/output/_messenger.php index 0fe4033..49113c3 100644 --- a/php/src/output/_messenger.php +++ b/php/src/output/_messenger.php @@ -2,7 +2,6 @@ namespace nulib\output; use nulib\str; -use nulib\ValueException; /** * Class _messenger: classe de base pour say, log et msg @@ -59,13 +58,14 @@ abstract class _messenger { static function desc($content, ?int $level=null): void { static::get()->desc($content, $level); } static function action($content, ?callable $func=null, ?int $level=null): void { static::get()->action($content, $func, $level); } static function step($content, ?int $level=null): void { static::get()->step($content, $level); } - static function asuccess($content=null): void { static::get()->asuccess($content); } - static function afailure($content=null): void { static::get()->afailure($content); } - static function adone($content=null): void { static::get()->adone($content); } + static function asuccess($content=null, ?int $override_level=null): void { static::get()->asuccess($content, $override_level); } + static function afailure($content=null, ?int $override_level=null): void { static::get()->afailure($content, $override_level); } + static function adone($content=null, ?int $override_level=null): void { static::get()->adone($content, $override_level); } + static function aresult($result=null, ?int $override_level=null): void { static::get()->aresult($result, $override_level); } static function print($content, ?int $level=null): void { static::get()->print($content, $level); } static function info($content, ?int $level=null): void { static::get()->info($content, $level); } static function note($content, ?int $level=null): void { static::get()->note($content, $level); } - static function warn($content, ?int $level=null): void { static::get()->warn($content, $level); } + static function warning($content, ?int $level=null): void { static::get()->warning($content, $level); } static function error($content, ?int $level=null): void { static::get()->error($content, $level); } static function end(bool $all=false): void { static::get()->end($all); } @@ -74,6 +74,6 @@ abstract class _messenger { static function minor($content): void { self::info($content, self::MINOR);} static function important($content): void { self::info($content, self::MAJOR);} static function attention($content): void { self::note($content, self::MAJOR);} - static function critwarn($content): void { self::warn($content, self::MAJOR);} + static function critwarning($content): void { self::warning($content, self::MAJOR);} static function criterror($content): void { self::error($content, self::MAJOR);} } diff --git a/php/src/output/log.php b/php/src/output/log.php index b315f16..6b5aa46 100644 --- a/php/src/output/log.php +++ b/php/src/output/log.php @@ -1,8 +1,9 @@ msgs as $msg) { - $msg->title($content, null, $level); if ($msg instanceof _IMessenger) { $useFunc = true; $untils[] = $msg->_getTitleMark(); } + $msg->title($content, null, $level); } if ($useFunc && $func !== null) { try { @@ -59,7 +64,9 @@ class ProxyMessenger implements IMessenger { /** @var _IMessenger $msg */ $index = 0; foreach ($this->msgs as $msg) { - $msg->_endTitle($untils[$index++]); + if ($msg instanceof _IMessenger) { + $msg->_endTitle($untils[$index++]); + } } } } @@ -72,12 +79,25 @@ class ProxyMessenger implements IMessenger { $msg->action($content, null, $level); if ($msg instanceof _IMessenger) { $useFunc = true; - $untils[] = $msg->_getTitleMark(); + $untils[] = $msg->_getActionMark(); } } if ($useFunc && $func !== null) { try { - $func($this); + $result = $func($this); + /** @var _IMessenger $msg */ + $index = 0; + foreach ($this->msgs as $msg) { + if ($msg->_getActionMark() > $untils[$index++]) { + $msg->aresult($result); + } + } + } catch (Exception $e) { + /** @var _IMessenger $msg */ + foreach ($this->msgs as $msg) { + $msg->afailure($e); + } + throw $e; } finally { /** @var _IMessenger $msg */ $index = 0; @@ -88,13 +108,14 @@ class ProxyMessenger implements IMessenger { } } function step($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->step($content, $level); } } - function asuccess($content=null): void { foreach ($this->msgs as $msg) { $msg->asuccess($content); } } - function afailure($content=null): void { foreach ($this->msgs as $msg) { $msg->afailure($content); } } - function adone($content=null): void { foreach ($this->msgs as $msg) { $msg->adone($content); } } + function asuccess($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->asuccess($content, $overrideLevel); } } + function afailure($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->afailure($content, $overrideLevel); } } + function adone($content=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->adone($content, $overrideLevel); } } + function aresult($result=null, ?int $overrideLevel=null): void { foreach ($this->msgs as $msg) { $msg->aresult($result, $overrideLevel); } } function print($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->print($content, $level); } } function info($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->info($content, $level); } } function note($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->note($content, $level); } } - function warn($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->warn($content, $level); } } + function warning($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->warning($content, $level); } } function error($content, ?int $level=null): void { foreach ($this->msgs as $msg) { $msg->error($content, $level); } } function end(bool $all=false): void { foreach ($this->msgs as $msg) { $msg->end($all); } } } diff --git a/php/src/output/std/StdMessenger.php b/php/src/output/std/StdMessenger.php index 953dc0c..c9cc6b2 100644 --- a/php/src/output/std/StdMessenger.php +++ b/php/src/output/std/StdMessenger.php @@ -2,10 +2,11 @@ namespace nulib\output\std; use Exception; +use nulib\A; use nulib\cl; use nulib\ExceptionShadow; -use nulib\UserException; use nulib\output\IMessenger; +use nulib\UserException; use Throwable; class StdMessenger implements _IMessenger { @@ -40,7 +41,7 @@ class StdMessenger implements _IMessenger { "title" => [false, "TITLE!", null, "T", "", "==="], "desc" => ["DESC!", ">", ""], "error" => ["CRIT.ERROR!", "E!", ""], - "warn" => ["CRIT.WARN!", "W!", ""], + "warning" => ["CRIT.WARNING!", "W!", ""], "note" => ["ATTENTION!", "N!", ""], "info" => ["IMPORTANT!", "N!", ""], "step" => ["*", ".", ""], @@ -51,7 +52,7 @@ class StdMessenger implements _IMessenger { "title" => [false, "TITLE:", null, "T", "", "---"], "desc" => ["DESC:", ">", ""], "error" => ["ERROR:", "E", ""], - "warn" => ["WARN:", "W", ""], + "warning" => ["WARNING:", "W", ""], "note" => ["NOTE:", "N", ""], "info" => ["INFO:", "I", ""], "step" => ["*", ".", ""], @@ -62,7 +63,7 @@ class StdMessenger implements _IMessenger { "title" => [false, "title", null, "t", "", null], "desc" => ["desc", ">", ""], "error" => ["error", "E", ""], - "warn" => ["warn", "W", ""], + "warning" => ["warning", "W", ""], "note" => ["note", "N", ""], "info" => ["info", "I", ""], "step" => ["*", ".", ""], @@ -73,7 +74,7 @@ class StdMessenger implements _IMessenger { "title" => [false, "title", null, "t", "", null], "desc" => ["desc", ">", ""], "error" => ["debugE", "e", ""], - "warn" => ["debugW", "w", ""], + "warning" => ["debugW", "w", ""], "note" => ["debugN", "i", ""], "info" => ["debug", "D", ""], "step" => ["*", ".", ""], @@ -306,7 +307,7 @@ class StdMessenger implements _IMessenger { } if ($printContent && $printResult) { if ($rcontent) { - cl::ensure_array($content); + A::ensure_array($content); $content[] = ": "; $content[] = $rcontent; } @@ -579,18 +580,19 @@ class StdMessenger implements _IMessenger { if ($func !== null) { try { $result = $func($this); - if ($result !== null) { - if ($result === true) $this->asuccess(); - elseif ($result === false) $this->afailure(); - else $this->adone($result); + if ($this->_getActionMark() > $until) { + $this->aresult($result); } + } catch (Exception $e) { + $this->afailure($e); + throw $e; } finally { $this->_endAction($until); } } } - function printActions(bool $endAction=false): void { + function printActions(bool $endAction=false, ?int $overrideLevel=null): void { $this->printTitles(); $err = $this->err; $indentLevel = $this->getIndentLevel(false); @@ -599,7 +601,7 @@ class StdMessenger implements _IMessenger { foreach ($this->actions as &$action) { $mergeResult = $index++ == $lastIndex && $endAction; $linePrefix = $action["line_prefix"]; - $level = $action["level"]; + $level = $overrideLevel?? $action["level"]; $content = $action["content"]; $printContent = $action["print_content"]; $rsuccess = $action["result_success"]; @@ -628,25 +630,33 @@ class StdMessenger implements _IMessenger { $this->_printGenericOrException($level, "step", $content, $this->getIndentLevel(), $this->err); } - function asuccess($content=null): void { + function asuccess($content=null, ?int $overrideLevel=null): void { if (!$this->actions) $this->action(null); $this->action["result_success"] = true; $this->action["result_content"] = $content; - $this->printActions(true); + $this->printActions(true, $overrideLevel); } - function afailure($content=null): void { + function afailure($content=null, ?int $overrideLevel=null): void { if (!$this->actions) $this->action(null); $this->action["result_success"] = false; $this->action["result_content"] = $content; - $this->printActions(true); + $this->printActions(true, $overrideLevel); } - function adone($content=null): void { + function adone($content=null, ?int $overrideLevel=null): void { if (!$this->actions) $this->action(null); $this->action["result_success"] = null; $this->action["result_content"] = $content; - $this->printActions(true); + $this->printActions(true, $overrideLevel); + } + + function aresult($result=null, ?int $overrideLevel=null): void { + if (!$this->actions) $this->action(null); + if ($result === true) $this->asuccess(null, $overrideLevel); + elseif ($result === false) $this->afailure(null, $overrideLevel); + elseif ($result instanceof Exception) $this->afailure($result, $overrideLevel); + else $this->adone($result, $overrideLevel); } function _endAction(?int $until=null): void { @@ -674,8 +684,8 @@ class StdMessenger implements _IMessenger { $this->_printGenericOrException($level, "note", $content, $this->getIndentLevel(), $this->err); } - function warn($content, ?int $level=null): void { - $this->_printGenericOrException($level, "warn", $content, $this->getIndentLevel(), $this->err); + function warning($content, ?int $level=null): void { + $this->_printGenericOrException($level, "warning", $content, $this->getIndentLevel(), $this->err); } function error($content, ?int $level=null): void { diff --git a/php/src/output/std/StdOutput.php b/php/src/output/std/StdOutput.php index 9cc90c3..cfe4df0 100644 --- a/php/src/output/std/StdOutput.php +++ b/php/src/output/std/StdOutput.php @@ -3,10 +3,8 @@ namespace nulib\output\std; use Exception; use nulib\cl; -use nulib\os\file\Stream; +use nulib\file\Stream; use nulib\php\content\content; -use nulib\php\content\IContent; -use nulib\php\content\IPrintable; /** * Class StdOutput: affichage sur STDOUT, STDERR ou dans un fichier quelconque diff --git a/php/src/values/akey.php b/php/src/php/akey.php similarity index 94% rename from php/src/values/akey.php rename to php/src/php/akey.php index 403bf1d..10b0bef 100644 --- a/php/src/values/akey.php +++ b/php/src/php/akey.php @@ -1,7 +1,8 @@ offsetSet($key, ++$value); return $value; } else { - cl::ensure_array($array); + A::ensure_array($array); $value = (int)cl::get($array, $key); return $array[$key] = ++$value; } @@ -59,7 +60,7 @@ class akey { if ($allow_negative || $value > 0) $array->offsetSet($key, --$value); return $value; } else { - cl::ensure_array($array); + A::ensure_array($array); $value = (int)cl::get($array, $key); if ($allow_negative || $value > 0) $array[$key] = --$value; return $value; @@ -76,7 +77,7 @@ class akey { $value = cl::merge($value, $merge); $array->offsetSet($key, $value); } else { - cl::ensure_array($array); + A::ensure_array($array); $array[$key] = cl::merge($array[$key], $merge); } } @@ -91,7 +92,7 @@ class akey { cl::set($value, null, $value); $array->offsetSet($key, $value); } else { - cl::ensure_array($array); + A::ensure_array($array); cl::set($array[$key], null, $value); } } diff --git a/php/src/php/coll/BaseArray.php b/php/src/php/coll/BaseArray.php index 96f88fa..ff809fc 100644 --- a/php/src/php/coll/BaseArray.php +++ b/php/src/php/coll/BaseArray.php @@ -5,12 +5,13 @@ use ArrayAccess; use Countable; use Iterator; use nulib\cl; +use nulib\IArrayWrapper; /** * Class BaseArray: implémentation de base d'un objet array-like, qui peut aussi * servir comme front-end pour un array */ -class BaseArray implements ArrayAccess, Countable, Iterator { +class BaseArray implements ArrayAccess, Countable, Iterator, IArrayWrapper { function __construct(?array &$data=null) { $this->reset($data); } @@ -18,10 +19,11 @@ class BaseArray implements ArrayAccess, Countable, Iterator { /** @var array */ protected $data; + function &wrappedArray(): ?array { return $this->data; } + function __toString(): string { return var_export($this->data, true); } #function __debugInfo() { return $this->data; } function reset(?array &$data): void { $this->data =& $data; } - function &array(): ?array { return $this->data; } function count(): int { return $this->data !== null? count($this->data): 0; } function keys(): array { return $this->data !== null? array_keys($this->data): []; } diff --git a/php/src/php/func.php b/php/src/php/func.php index b100956..0df602a 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -3,9 +3,9 @@ namespace nulib\php; use Closure; use nulib\cl; -use nulib\ValueException; use nulib\ref\php\ref_func; use nulib\schema\Schema; +use nulib\ValueException; use ReflectionClass; use ReflectionFunction; use ReflectionMethod; diff --git a/php/src/values/mprop.php b/php/src/php/mprop.php similarity index 98% rename from php/src/values/mprop.php rename to php/src/php/mprop.php index 5236582..61e764f 100644 --- a/php/src/values/mprop.php +++ b/php/src/php/mprop.php @@ -1,9 +1,8 @@ y; @@ -24,6 +29,7 @@ class DateInterval extends \DateInterval { } function __construct($duration) { + if (is_int($duration)) $duration = "PT${duration}S"; if ($duration instanceof \DateInterval) { $this->y = $duration->y; $this->m = $duration->m; diff --git a/php/src/php/time/DateTime.php b/php/src/php/time/DateTime.php index 016287c..3c9572b 100644 --- a/php/src/php/time/DateTime.php +++ b/php/src/php/time/DateTime.php @@ -24,6 +24,11 @@ use InvalidArgumentException; * @property-read string $YmdHMSZ */ class DateTime extends \DateTime { + static function with($datetime): self { + if ($datetime instanceof static) return $datetime; + else return new static($datetime); + } + const DMY_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))?$/'; const YMD_PATTERN = '/^((?:\d{2})?\d{2})(\d{2})(\d{2})$/'; const DMYHIS_PATTERN = '/^(\d+)\/(\d+)(?:\/(\d+))? +(\d+)[h:.](\d+)(?:[:.](\d+))?$/'; diff --git a/php/src/php/time/Delay.php b/php/src/php/time/Delay.php index 8161351..14a5307 100644 --- a/php/src/php/time/Delay.php +++ b/php/src/php/time/Delay.php @@ -13,6 +13,7 @@ use InvalidArgumentException; * - une chaine de la forme "x[WDHMS]y" où x et y sont des nombres et la lettre * est l'unité de temps: W représente une semaine, D une journée, H une heure, * M une minute et S une seconde. + * - la chaine "INF" qui représente une durée infinie * * Dans cette dernière forme, le timestamp destination est calculé en ajoutant x * unités. puis l'unité inférieure est ramenée à (0 + y) @@ -24,6 +25,11 @@ use InvalidArgumentException; * NB: la valeur y pour l'unité S est ignorée */ class Delay { + static function with($delay, ?DateTimeInterface $from=null): self { + if ($delay instanceof static) return $delay; + else return new static($delay, $from); + } + /** valeurs par défaut de x et y pour les unités supportées */ const DEFAULTS = [ "w" => [0, 5], @@ -35,6 +41,7 @@ class Delay { static function compute_dest(int $x, string $u, ?int $y, DateTime $from): array { $dest = DateTime::clone($from); + $yu = null; switch ($u) { case "w": if ($x > 0) { @@ -43,29 +50,28 @@ class Delay { } $w = 7 - intval($dest->format("w")); $dest->add(new \DateInterval("P${w}D")); - $u = "h"; + $yu = "h"; break; case "d": $dest->add(new \DateInterval("P${x}D")); - $u = "h"; + $yu = "h"; break; case "h": $dest->add(new \DateInterval("PT${x}H")); - $u = "m"; + $yu = "m"; break; case "m": $dest->add(new \DateInterval("PT${x}M")); - $u = "s"; + $yu = "s"; break; case "s": $dest->add(new \DateInterval("PT${x}S")); - $u = null; break; } - if ($y !== null && $u !== null) { + if ($y !== null && $yu !== null) { $h = intval($dest->format("H")); $m = intval($dest->format("i")); - switch ($u) { + switch ($yu) { case "h": $dest->setTime($y, 0, 0, 0); break; @@ -77,13 +83,18 @@ class Delay { break; } } - $repr = $y !== null? "$x$y$y": "$x"; + $u = strtoupper($u); + $repr = $y !== null? "$x$u$y": "$x"; return [$dest, $repr]; } function __construct($delay, ?DateTimeInterface $from=null) { if ($from === null) $from = new DateTime(); - if (is_int($delay)) { + if ($delay === "INF") { + $dest = DateTime::clone($from); + $dest->add(new DateInterval("P9999Y")); + $repr = "INF"; + } elseif (is_int($delay)) { [$dest, $repr] = self::compute_dest($delay, "s", null, $from); } elseif (is_string($delay) && preg_match('/^\d+$/', $delay)) { $x = intval($delay); @@ -104,6 +115,13 @@ class Delay { $this->repr = $repr; } + function __serialize(): array { + return [$this->dest, $this->repr]; + } + function __unserialize(array $data): void { + [$this->dest, $this->repr] = $data; + } + /** @var DateTime */ protected $dest; @@ -111,6 +129,22 @@ class Delay { return $this->dest; } + function addDuration($duration) { + if (is_int($duration) && $duration < 0) { + $this->dest->sub(DateInterval::with(-$duration)); + } else { + $this->dest->add(DateInterval::with($duration)); + } + } + + function subDuration($duration) { + if (is_int($duration) && $duration < 0) { + $this->dest->add(DateInterval::with(-$duration)); + } else { + $this->dest->sub(DateInterval::with($duration)); + } + } + /** @var string */ protected $repr; @@ -125,7 +159,8 @@ class Delay { /** retourner true si le délai imparti est écoulé */ function isElapsed(?DateTimeInterface $now=null): bool { - return $this->_getDiff($now)->invert == 0; + if ($this->repr === "INF") return false; + else return $this->_getDiff($now)->invert == 0; } /** diff --git a/php/src/values/valm.php b/php/src/php/valm.php similarity index 99% rename from php/src/values/valm.php rename to php/src/php/valm.php index dc4c100..99d5961 100644 --- a/php/src/values/valm.php +++ b/php/src/php/valm.php @@ -1,5 +1,5 @@ NATURE, +]; +~~~ + +La nature indique le type de données représenté par le schéma. +* nature scalaire: modélise une donnée scalaire + ~~~php + const SCALAR_SCHEMA = [ + $type, [$default, $title, ...] + "" => "scalar", + ]; + ~~~ + Si le type est "array" ou "?array", on peut préciser le schéma de la donnée + ~~~php + const SCALAR_SCHEMA = [ + "?array", [$default, $title, ...] + "" => "scalar", + "schema" => NAKED_SCHEMA, + ]; + ~~~ +* nature tableau associatif: modélise un tableau associatif (le tableau peut + avoir des clés numériques ou chaines --> seules les clés décrites par le + schéma sont validées) + ~~~php + const ASSOC_SCHEMA = [ + KEY => VALUE_SCHEMA, + ... + "" => "assoc", + ]; + ~~~ + la nature "tableau associatif" est du sucre syntaxique pour une valeur + scalaire de type "?array" dont on précise le schéma + ~~~php + // la valeur ci-dessus est strictement équivalent à + const ASSOC_SCHEMA = [ + "?array", + "" => "scalar", + "schema" => [ + KEY => VALUE_SCHEMA, + ... + ], + ]; + ~~~ + +* nature liste: modélise une liste de valeurs du même type (le tableau peut + avoir des clés numériques ou chaines --> on ne modélise ni le type ni la + valeur des clés) + ~~~php + const LIST_SCHEMA = [ + "?array", [$default, $title, ...] + "" => "list", + "schema" => ITEM_SCHEMA, + ]; + ~~~ + +## Schéma d'une valeur scalaire + +Dans sa forme normalisée, une valeur scalaire est généralement modélisée de +cette manière: +~~~php +const SCALAR_SCHEMA = [ + "type" => "types autorisés de la valeur", + "default" => "valeur par défaut si la valeur n'existe pas", + "title" => "libellé de la valeur, utilisable par exemple dans un formulaire", + "required" => "la valeur est-elle requise? si oui, elle doit exister", + "nullable" => "si la valeur existe, peut-elle être nulle?", + "desc" => "description de la valeur", + "checker_func" => "une fonction qui vérifie une valeur et la classifie", + "parser_func" => "une fonction qui analyse une chaine pour produire la valeur", + "messages" => "messages à afficher en cas d'erreur d'analyse", + "formatter_func" => "une fonction qui formatte la valeur pour affichage", + "format" => "format à utiliser pour l'affichage", + "" => "scalar", + "schema" => "schéma de la valeur si le type est array ou ?array, null sinon", +]; +~~~ + +L'ordre des clés du schéma ci-dessus indique la clé associé à une valeur si elle +est fournie dans un tableau séquentiel. Par exemple, les deux schéma suivants +sont équivalents: +~~~php +const SCALAR_SCHEMA1 = [ + "string", null, "une valeur chaine", +]; +const SCALAR_SCHEMA2 = [ + "type" => "string", + "default" => null, + "title" => "une valeur chaine", + "" => "scalar", +]; +~~~ + +Si la nature du schéma n'est pas spécifiée, on considère que c'est un schéma de +nature scalaire si: +* c'est une chaine, qui représente alors le type, e.g `"string"` +* c'est un tableau avec un unique élément à l'index 0 de type chaine, qui est + aussi le type, e.g `["string"]` +* c'est un tableau avec un élément à l'index 0, ainsi que d'autres éléments, + e.g `["string", null, "required" => true]` + +message indique les messages à afficher en cas d'erreur d'analyse. les clés sont +normalisées et correspondent à différents états de la valeur tels qu'analysés +par `checker_func` +~~~php +const MESSAGE_SCHEMA = [ + "missing" => "message si la valeur n'existe pas dans la source et qu'elle est requise", + "unavailable" => "message si la valeur vaut false dans la source et qu'elle est requise", + "null" => "message si la valeur est nulle et qu'elle n'est pas nullable", + "empty" => "message si la valeur est une chaine vide et que ce n'est pas autorisé", + "invalid" => "message si la valeur est invalide", +]; +~~~ + +## Schéma d'un tableau associatif + +Dans sa forme *non normalisée*, un tableau associatif est généralement modélisé +de cette manière: +~~~php +const ASSOC_SCHEMA = [ + KEY => VALUE_SCHEMA, + ... + "" => "assoc", +]; +~~~ +où chaque occurrence de `KEY => VALUE_SCHEMA` définit le schéma de la valeur +dont la clé est `KEY` + +Si la nature du schéma n'est pas spécifiée, on considère que c'est un schéma de +nature associative si: +* c'est un tableau uniquement associatif avec aucun élément séquentiel, e.g + `["name" => "string", "age" => "int"]` + +VALUE_SCHEMA peut-être n'importe quel schéma valide, qui sera analysé +récursivement, avec cependant l'ajout de quelques clés supplémentaires: +* description de la valeur dans le contexte du tableau + ~~~php + VALUE_SCHEMA = [ + ... + "name" => "identifiant de la valeur", + "pkey" => "chemin de clé de la valeur dans le tableau associatif", + ]; + ~~~ +* s'il s'agit d'une valeur scalaire simple autre que array + ~~~php + VALUE_SCHEMA = [ + ... + "header" => "nom de l'en-tête s'il faut présenter cette donnée dans un tableau", + "composite" => "ce champ fait-il partie d'une valeur composite?", + ]; + ~~~ + +## Schéma d'une liste (tableau séquentiel ou associatif d'éléments du même type) + +Dans sa forme *non normalisée*, une liste est généralement modélisée de cette +manière: +~~~php +const LIST_SCHEMA = [ITEM_SCHEMA]; +~~~ +où ITEM_SCHEMA est le schéma des éléments de la liste + +Pour information, la forme normalisée est plutôt de la forme +~~~php +const LIST_SCHEMA = [ + "?array", + "" => "list", + "schema" => ITEM_SCHEMA, +]; +~~~ +le type "?array" ou "array" indique si la liste est nullable ou non. la valeur +par défaut est "?array" + +Si la nature du schéma n'est pas spécifiée, on considère que c'est un schéma de +nature liste si: +* c'est un tableau avec un unique élément de type tableau à l'index 0, e.g + `[["string", null, "required" => true]]` + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/schema/Result.php b/php/src/schema/Result.php new file mode 100644 index 0000000..af6c680 --- /dev/null +++ b/php/src/schema/Result.php @@ -0,0 +1,30 @@ +reset(); + } + + function isAssoc(?AssocResult &$assoc=null): bool { return false; } + function isList(?ListResult &$list=null): bool { return false; } + function isScalar(?ScalarResult &$scalar=null): bool { return false; } + + /** + * Obtenir la liste des clés valides pour les valeurs accessibles via cet + * objet + */ + abstract function getKeys(): array; + + /** obtenir un objet pour gérer la valeur spécifiée */ + abstract function getResult($key=null): Result; + + abstract function reset(): void; +} diff --git a/php/src/schema/Schema.php b/php/src/schema/Schema.php new file mode 100644 index 0000000..76c606b --- /dev/null +++ b/php/src/schema/Schema.php @@ -0,0 +1,94 @@ +newValue($destv, $dest, $destKey); + } + + /** + * @var array définition du schéma, à redéfinir le cas échéant dans une classe + * dérivée + */ + const SCHEMA = null; + + /** @var array */ + protected $definition; + + /** retourner true si le schéma est de nature tableau associatif */ + function isAssoc(?AssocSchema &$assoc=null): bool { return false; } + /** retourner true si le schéma est de nature liste */ + function isList(?ListSchema &$list=null): bool { return false; } + /** retourner true si le schéma est de nature scalaire */ + function isScalar(?ScalarSchema &$scalar=null): bool { return false; } + + abstract function newValue(?Value &$destv=null, &$dest=null, $destKey=null): Value; + + ############################################################################# + # key & properties + + function offsetExists($offset): bool { + return array_key_exists($offset, $this->definition); + } + function offsetGet($offset) { + if (!array_key_exists($offset, $this->definition)) return null; + else return $this->definition[$offset]; + } + function offsetSet($offset, $value): void { + throw AccessException::read_only(null, $offset); + } + function offsetUnset($offset): void { + throw AccessException::read_only(null, $offset); + } + + const _PROPERTY_PKEYS = []; + function __get($name) { + $pkey = cl::get(static::_PROPERTY_PKEYS, $name, $name); + return cl::pget($this->definition, $pkey); + } +} diff --git a/php/src/schema/SchemaException.php b/php/src/schema/SchemaException.php new file mode 100644 index 0000000..f6e3998 --- /dev/null +++ b/php/src/schema/SchemaException.php @@ -0,0 +1,22 @@ + "?string", + "b" => "?int", + ])->newValue(); + $dest = ["x_a" => 5, "x_b" => "10"], + $value->reset($dest, null, [ + "key_prefix" => "x_", + ]); + # $dest vaut ["x_a" => "5", "x_b" => 10]; + ~~~ + définir si le préfixe doit être spécifié sur le schéma ou sur la valeur... + actuellement, le code ne permet pas de définir de tels paramètres... + + alternative: c'est lors de la *définition* du schéma que le préfixe est ajouté + e.g + ~~~php + $value = Schema::ns($schema, [ + "a" => "?string", + "b" => "?int", + ], [ + "key_prefix" => "x_", + ])->newValue(); + $dest = ["x_a" => 5, "x_b" => "10"], + $value->reset($dest); + # $dest vaut ["x_a" => "5", "x_b" => 10]; + ~~~ +* dans la définition, `[type]` est remplacé par l'instance de IType lors de sa + résolution? +* implémenter l'instanciation de types avec des paramètres particuliers. *si* + des paramètres sont fournis, le type est instancié avec la signature + `IType($typeDefinition, $schemaDefinition)` e.g + ~~~php + const SCHEMA = ["type", default, "required" => true]; + # le type est instancié comme suit: + $type = new ttype(); + + const SCHEMA = [[["type", ...]], default, "required" => true]; + # le type est instancié comme suit: + # le tableau peut être une liste ou associatif, c'est au type de décider ce + # qu'il en fait + $type = new ttype(["type", ...], SCHEMA); + ~~~ +* ajouter à IType les méthodes getName() pour le nom officiel du type, + getAliases() pour les alias supportés, et getClass() pour la définition de la + classe dans les méthodes et propriétés + getName() et getAliases() sont juste pour information, ils ne sont pas utilisés + lors de la résolution du type effectif. +* si cela a du sens, dans AssocSchema, n'instancier les schémas de chaque clé qu'à la demande. + l'idée est de ne pas perdre du temps à instancier un schéma qui ne serait pas utilisé + + on pourrait avoir d'une manière générale quelque chose comme: + ~~~ + Schema::ensure(&$schema, ?array $def=null, $defKey=null): Schema; + ~~~ + * si $schema est une instance de Schema, la retourner + * si c'est un array, c'est une définition et il faut la remplacer par l'instance de Schema correspondant + * sinon, prendre $def comme définition + $key est la clé si $schema est dans un autre schema +* actuellement, pour un schéma associatif, si on normalise un tableau séquentiel, + chaque valeur correspond à la clé de même rang, eg. pour un schéma + ~~~php + const SCHEMA = ["first" => DEF, "second" => DEF]; + const ARRAY = ["first", "second"]; + ~~~ + la valeur normalisée de `ARRAY` est `["first" => "first", "second" => "second"]` + + cependant, dans certaines circonstances (notamment pour des paramètres), on + devrait pouvoir considérer une valeur indexée comme un flag, i.e la valeur + normalisée de ARRAY serait `["first" => true, "second" => true]` + + la définition de ces "circonstances" est encore à faire: soit un paramètre + lors de la définition du schéma, soit un truc magique du genre "toutes les + valeurs séquentielles sont des clés du schéma" + +-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary \ No newline at end of file diff --git a/php/src/schema/Value.php b/php/src/schema/Value.php new file mode 100644 index 0000000..4c512db --- /dev/null +++ b/php/src/schema/Value.php @@ -0,0 +1,85 @@ +getKeys() as $key) { + yield $key => $this->getValue($key); + } + } + + /** + * obtenir le résultat de l'appel d'une des fonctions {@link set()} ou + * {@link unset()} + */ + abstract function getResult(): Result; + + /** retourner true si la valeur existe */ + abstract function isPresent(): bool; + + /** retourner le type associé à la valeur */ + abstract function getType(): IType; + + /** retourner true si la valeur est disponible */ + abstract function isAvailable(): bool; + + /** retourner true si la valeur est valide */ + abstract function isValid(): bool; + + /** retourner true si la valeur est dans sa forme normalisée */ + abstract function isNormalized(): bool; + + /** obtenir la valeur */ + abstract function get($default=null); + + /** remplacer la valeur */ + abstract function set($value): self; + + /** supprimer la valeur */ + abstract function unset(): self; + + /** formatter la valeur pour affichage */ + abstract function format($format=null): string; + + ############################################################################# + # key & properties + + function offsetExists($offset): bool { + return in_array($offset, $this->getKeys()); + } + + function offsetGet($offset) { + return $this->getValue($offset); + } + + function offsetSet($offset, $value): void { + $this->getValue($offset)->set($value); + } + + function offsetUnset($offset): void { + $this->getValue($offset)->unset(); + } +} diff --git a/php/src/schema/_assoc/AssocResult.php b/php/src/schema/_assoc/AssocResult.php new file mode 100644 index 0000000..f698968 --- /dev/null +++ b/php/src/schema/_assoc/AssocResult.php @@ -0,0 +1,8 @@ +definition = $definition; + } + + function isAssoc(?AssocSchema &$assoc=null): bool { + $assoc = $this; + return true; + } + + function newValue(?Value &$destv=null, &$dest=null, $destKey=null): AssocValue { + if ($destv instanceof AssocValue) return $destv->reset($dest, $destKey); + else return ($destv = new AssocValue($this, $dest, $destKey)); + } +} diff --git a/php/src/schema/_assoc/AssocValue.php b/php/src/schema/_assoc/AssocValue.php new file mode 100644 index 0000000..94eeade --- /dev/null +++ b/php/src/schema/_assoc/AssocValue.php @@ -0,0 +1,18 @@ +definition = $definition; + } + + function isList(?ListSchema &$list=null): bool { + $list = $this; + return true; + } + + function newValue(?Value &$destv=null, &$dest=null, $destKey=null): ListValue { + if ($destv instanceof ListValue) return $destv->reset($dest, $destKey); + else return ($destv = new ListValue($this, $dest, $destKey)); + } +} diff --git a/php/src/schema/_list/ListValue.php b/php/src/schema/_list/ListValue.php new file mode 100644 index 0000000..21208d9 --- /dev/null +++ b/php/src/schema/_list/ListValue.php @@ -0,0 +1,19 @@ +result = array_merge( + array_fill_keys(static::KEYS, null), [ + "resultAvailable" => false, + "present" => false, + "available" => false, + "null" => false, + "valid" => false, + "normalized" => false, + ]); + } + + function __get(string $name) { + return $this->result[$name]; + } + + function __set(string $name, $value): void { + $this->result[$name] = $value; + } + + protected static function replace_key(string &$message, ?string $key): void { + if ($key) { + $message = str_replace("{key}", $key, $message); + } else { + $message = str_replace("{key}: ", "", $message); + $message = str_replace("cette valeur", "la valeur", $message); + } + } + + protected static function replace_orig(string &$message, $orig): void { + $message = str_replace("{orig}", strval($orig), $message); + } + + protected function getMessage(string $key, ScalarSchema $schema): string { + $message = cl::get($schema->messages, $key); + if ($message !== null) return $message; + return cl::get(ref_schema::MESSAGES, $key); + } + + function setMissing(ScalarSchema $schema): int { + $this->resultAvailable = true; + $this->present = false; + $this->available = false; + if (!$schema->required) { + $this->null = false; + $this->valid = true; + $this->normalized = true; + return ref_analyze::NORMALIZED; + } else { + $message = $this->getMessage("missing", $schema); + self::replace_key($message, $schema->name); + $this->message = $message; + return ref_analyze::MISSING; + } + } + + function setUnavailable(ScalarSchema $schema): int { + $this->resultAvailable = true; + $this->present = true; + $this->available = false; + if (!$schema->required) { + $this->null = false; + $this->valid = true; + $this->normalized = true; + return ref_analyze::NORMALIZED; + } else { + $message = $this->getMessage("unavailable", $schema); + self::replace_key($message, $schema->name); + $this->message = $message; + return ref_analyze::UNAVAILABLE; + } + } + + function setNull(ScalarSchema $schema): int { + $this->resultAvailable = true; + $this->present = true; + $this->available = true; + $this->null = true; + if ($schema->nullable) { + $this->valid = true; + $this->normalized = true; + return ref_analyze::NORMALIZED; + } else { + $message = $this->getMessage("null", $schema); + self::replace_key($message, $schema->name); + $this->message = $message; + return ref_analyze::NULL; + } + } + + function setInvalid($value, ScalarSchema $schema): int { + $this->resultAvailable = true; + $this->present = true; + $this->available = true; + $this->null = false; + $this->valid = false; + $this->orig = $value; + $message = $this->getMessage("invalid", $schema); + self::replace_key($message, $schema->name); + self::replace_orig($message, $schema->orig); + $this->message = $message; + return ref_analyze::INVALID; + } + + function setValid(): int { + $this->resultAvailable = true; + $this->present = true; + $this->available = true; + $this->null = false; + $this->valid = true; + return ref_analyze::VALID; + } + + function setNormalized(): int { + $this->resultAvailable = true; + $this->present = true; + $this->available = true; + $this->null = false; + $this->valid = true; + $this->normalized = true; + return ref_analyze::NORMALIZED; + } + + function throw(bool $throw): void { + if ($throw) throw new ValueException($this->message); + } +} diff --git a/php/src/schema/_scalar/ScalarSchema.php b/php/src/schema/_scalar/ScalarSchema.php new file mode 100644 index 0000000..823b4e5 --- /dev/null +++ b/php/src/schema/_scalar/ScalarSchema.php @@ -0,0 +1,193 @@ + 1; + } + + static function normalize($definition, $definitionKey=null): array { + if (!is_array($definition)) $definition = [$definition]; + # s'assurer que toutes les clés existent avec leur valeur par défaut + $index = 0; + foreach (array_keys(self::METASCHEMA) as $key) { + if (!array_key_exists($key, $definition)) { + if (array_key_exists($index, $definition)) { + $definition[$key] = $definition[$index]; + unset($definition[$index]); + $index++; + } else { + $definition[$key] = self::METASCHEMA[$key][1]; + } + } + } + # réordonner les clés numériques + if (cl::have_num_keys($definition)) { + $keys = array_keys($definition); + $index = 0; + foreach ($keys as $key) { + if (!is_int($key)) continue; + $definition[$index] = $definition[$key]; + unset($definition[$key]); + $index++; + } + } + # type + $types = []; + $deftype = $definition["type"]; + $nullable = $definition["nullable"]; + if ($deftype === null) { + $types[] = null; + $nullable = true; + } else { + if (!is_array($deftype)) { + if (!is_string($deftype)) throw SchemaException::invalid_type($deftype); + $deftype = explode("|", $deftype); + } + foreach ($deftype as $type) { + if ($type === null || $type === "null") { + $nullable = true; + continue; + } + if (!is_string($type)) throw SchemaException::invalid_type($type); + if (substr($type, 0, 1) == "?") { + $type = substr($type, 1); + $nullable = true; + } + if ($type === "") throw SchemaException::invalid_type($type); + $type = cl::get(ref_types::ALIASES, $type, $type); + $types = array_merge($types, explode("|", $type)); + } + if (!$types) throw SchemaException::invalid_schema("scalar: type is required"); + $types = array_keys(array_fill_keys($types, true)); + } + $definition["type"] = $types; + $definition["nullable"] = $nullable; + # nature + $nature = $definition[""]; + tarray::ensure_array($nature); + if (!array_key_exists(0, $nature) || $nature[0] !== "scalar") { + throw SchemaException::invalid_schema("expected scalar nature"); + } + $definition[""] = $nature; + # name, pkey, header + $name = $definition["name"]; + $pkey = $definition["pkey"]; + $header = $definition["header"]; + if ($name === null) $name = $definitionKey; + tstring::ensure_nstring($name); + tpkey::ensure_npkey($pkey); + tstring::ensure_nstring($header); + if ($pkey === null) $pkey = $name; + if ($header === null) $header = $name; + $definition["name"] = $name; + $definition["pkey"] = $pkey; + $definition["header"] = $header; + # autres éléments + tstring::ensure_nstring($definition["title"]); + tbool::ensure_bool($definition["required"]); + tbool::ensure_bool($definition["nullable"]); + tcontent::ensure_ncontent($definition["desc"]); + tcallable::ensure_ncallable($definition["analyzer_func"]); + tcallable::ensure_ncallable($definition["extractor_func"]); + tcallable::ensure_ncallable($definition["parser_func"]); + tcallable::ensure_ncallable($definition["normalizer_func"]); + tarray::ensure_narray($definition["messages"]); + tcallable::ensure_ncallable($definition["formatter_func"]); + tbool::ensure_nbool($definition["composite"]); + return $definition; + } + + function __construct($definition=null, $definitionKey=null, bool $normalize=true) { + if ($definition === null) $definition = static::SCHEMA; + if ($normalize) $definition = self::normalize($definition, $definitionKey); + $this->definition = $definition; + } + + function isScalar(?ScalarSchema &$scalar=null): bool { + $scalar = $this; + return true; + } + + function newValue(?Value &$destv=null, &$dest=null, $destKey=null): ScalarValue { + if ($destv instanceof ScalarValue) return $destv->reset($dest, $destKey); + else return ($destv = new ScalarValue($this, $dest, $destKey)); + } + + ############################################################################# + # key & properties + + const _PROPERTY_PKEYS = [ + "analyzerFunc" => "analyzer_func", + "extractorFunc" => "extractor_func", + "parserFunc" => "parser_func", + "normalizerFunc" => "normalizer_func", + "formatterFunc" => "formatter_func", + "nature" => ["", 0], + ]; +} diff --git a/php/src/schema/_scalar/ScalarValue.php b/php/src/schema/_scalar/ScalarValue.php new file mode 100644 index 0000000..70577dc --- /dev/null +++ b/php/src/schema/_scalar/ScalarValue.php @@ -0,0 +1,198 @@ +schema = $schema; + $this->defaultVerifix = $defaultVerifix; + $this->defaultThrow = $defaultThrow !== null? $defaultThrow: false; + $this->result = new ScalarResult(); + $this->reset($dest, $destKey); + $this->defaultThrow = $defaultThrow !== null? $defaultThrow: true; + } + + function isScalar(?ScalarValue &$scalar=null): bool { $scalar = $this; return true; } + + /** @var ScalarSchema schéma de cette valeur */ + protected $schema; + + /** @var Input source et destination de la valeur */ + protected $input; + + /** @var string|int|null clé de la valeur dans le tableau destination */ + protected $destKey; + + /** @var bool */ + protected $defaultVerifix; + + /** @var bool */ + protected $defaultThrow; + + /** @var IType type de la valeur après analyse */ + protected $type; + + /** @var ?ScalarResult résultat de l'analyse de la valeur */ + protected $result; + + function reset(&$dest, $destKey=null, ?bool $verifix=null): Value { + if ($dest instanceof Input) $input = $dest; + else $input = new Input($dest); + $this->input = $input; + $this->destKey = $destKey; + $this->type = null; + $this->_analyze(); + if ($verifix == null) $verifix = $this->defaultVerifix; + if ($verifix) $this->verifix(); + return $this; + } + + function getKeys(): array { + return [null]; + } + + function getValue($key=null): ScalarValue { + if ($key === null) return $this; + throw ValueException::invalid_key($key); + } + + /** analyser la valeur et résoudre son type */ + function _analyze(): int { + $schema = $this->schema; + $input = $this->input; + $destKey = $this->destKey; + $result = $this->result; + $result->reset(); + if (!$input->isPresent($destKey)) return $result->setMissing($schema); + $haveType = false; + $types = []; + $type = $firstType = null; + $haveValue = false; + $value = null; + # d'abord chercher un type pour lequel c'est une valeur normalisée + foreach ($schema->type as $name) { + $type = types::get($name); + if ($firstType === null) $firstType = $type; + $types[] = $type; + if ($type->isAvailable($input, $destKey)) { + if (!$haveValue) { + $value = $input->get($destKey); + $haveValue = true; + } + if ($type->isValid($value, $normalized) && $normalized) { + $haveType = true; + $this->type = $type; + break; + } + } + } + if (!$haveType) { + # ensuite chercher un type pour lequel la valeur est valide + foreach ($types as $type) { + if ($type->isAvailable($input, $destKey) && $type->isValid($value)) { + $haveType = true; + $this->type = $type; + break; + } + } + } + # sinon prendre le premier type + if (!$haveType) $type = $this->type = $firstType; + if (!$type->isAvailable($input, $destKey)) return $result->setUnavailable($schema); + $value = $input->get($destKey); + if ($type->isNull($value)) return $result->setNull($schema); + if ($type->isValid($value, $normalized)) { + if ($normalized) return $result->setNormalized(); + else return $result->setValid(); + } + if (is_string($value)) return ref_analyze::STRING; + else return $result->setInvalid($value, $schema); + } + + function verifix(?bool $throw=null): bool { + if ($throw === null) $throw = $this->defaultThrow; + $destKey = $this->destKey; + $verifix = false; + $result =& $this->result; + $modified = false; + if ($result->resultAvailable) { + if ($result->null) { + # forcer la valeur null, parce que la valeur actuelle est peut-être une + # valeur assimilée à null + $this->input->set(null, $destKey); + } elseif ($result->valid && !$result->normalized) { + # normaliser la valeur + $verifix = true; + } + } else { + $verifix = true; + } + if ($verifix) { + $value = $this->input->get($destKey); + $modified = $this->type->verifix($value, $result, $this->schema); + if ($result->valid) $this->input->set($value, $destKey); + } + if (!$result->valid) $result->throw($throw); + return $modified; + } + + function getResult(): ScalarResult { + return $this->result; + } + + function isPresent(): bool { + return $this->result->present; + } + + function getType(): IType { + return $this->type; + } + + function isAvailable(): bool { + return $this->result->available; + } + + function isValid(): bool { + return $this->result->valid; + } + + function isNormalized(): bool { + return $this->result->normalized; + } + + function get($default=null) { + if ($this->result->available) return $this->input->get($this->destKey); + else return $default; + } + + function set($value, ?bool $verifix=null): ScalarValue { + $this->input->set($value, $this->destKey); + $this->_analyze(); + if ($verifix === null) $verifix = $this->defaultVerifix; + if ($verifix) $this->verifix(); + return $this; + } + + function unset(?bool $verifix=null): ScalarValue { + $this->input->unset($this->destKey); + $this->_analyze(); + if ($verifix === null) $verifix = $this->defaultVerifix; + if ($verifix) $this->verifix(); + return $this; + } + + function format($format=null): string { + return $this->type->format($this->input->get($this->destKey), $format); + } +} diff --git a/php/src/schema/input/FormInput.php b/php/src/schema/input/FormInput.php new file mode 100644 index 0000000..701254d --- /dev/null +++ b/php/src/schema/input/FormInput.php @@ -0,0 +1,46 @@ +allowEmpty || $_POST[$key] !== ""; + } elseif (array_key_exists($key, $_GET)) { + return $this->allowEmpty || $_GET[$key] !== ""; + } else { + return false; + } + } + + function get($key=null) { + if ($key === null) return null; + if (array_key_exists($key, $_POST)) { + $value = $_POST[$key]; + if ($value === "" && !$this->allowEmpty) return null; + return $value; + } elseif (array_key_exists($key, $_GET)) { + $value = $_GET[$key]; + if ($value === "" && !$this->allowEmpty) return null; + return $value; + } else { + return null; + } + } +} diff --git a/php/src/schema/input/GetInput.php b/php/src/schema/input/GetInput.php new file mode 100644 index 0000000..d1ed48e --- /dev/null +++ b/php/src/schema/input/GetInput.php @@ -0,0 +1,35 @@ +allowEmpty || $_GET[$key] !== ""; + } else { + return false; + } + } + + function get($key=null) { + if ($key === null) return null; + if (array_key_exists($key, $_GET)) { + $value = $_GET[$key]; + if ($value === "" && !$this->allowEmpty) return null; + return $value; + } else { + return null; + } + } +} diff --git a/php/src/schema/input/Input.php b/php/src/schema/input/Input.php new file mode 100644 index 0000000..87fc08d --- /dev/null +++ b/php/src/schema/input/Input.php @@ -0,0 +1,61 @@ +dest =& $dest; + $allowEmpty = cl::get($params, "allow_empty"); + if ($allowEmpty === null) $allowEmpty = static::ALLOW_EMPTY; + $this->allowEmpty = $allowEmpty; + } + + protected $dest; + + /** tester si la valeur existe sans tenir compte de $allowEmpty */ + function isPresent($key=null): bool { + if ($key === null) return true; + $dest = $this->dest; + return $dest !== null && array_key_exists($key, $dest); + } + + /** + * @var bool comment considérer une chaine vide: "" si allowEmpty, null sinon + */ + protected $allowEmpty; + + /** tester si la valeur est disponible en tenant compte de $allowEmpty */ + function isAvailable($key=null): bool { + if ($key === null) return true; + $dest = $this->dest; + if ($dest === null || !array_key_exists($key, $dest)) return false; + return $this->allowEmpty || $dest[$key] !== ""; + } + + function get($key=null) { + $dest = $this->dest; + if ($key === null) return $dest; + if ($dest === null || !array_key_exists($key, $dest)) return null; + $value = $dest[$key]; + if ($value === "" && !$this->allowEmpty) return null; + return $value; + } + + function set($value, $key=null): void { + if ($key === null) $this->dest = $value; + else $this->dest[$key] = $value; + } + + function unset($key=null): void { + if ($key === null) $this->dest = null; + else unset($this->dest[$key]); + } +} diff --git a/php/src/schema/input/PostInput.php b/php/src/schema/input/PostInput.php new file mode 100644 index 0000000..88a0edd --- /dev/null +++ b/php/src/schema/input/PostInput.php @@ -0,0 +1,35 @@ +allowEmpty || $_POST[$key] !== ""; + } else { + return false; + } + } + + function get($key=null) { + if ($key === null) return null; + if (array_key_exists($key, $_POST)) { + $value = $_POST[$key]; + if ($value === "" && !$this->allowEmpty) return null; + return $value; + } else { + return null; + } + } +} diff --git a/php/src/schema/types.php b/php/src/schema/types.php new file mode 100644 index 0000000..a8e30ef --- /dev/null +++ b/php/src/schema/types.php @@ -0,0 +1,37 @@ +get($name); + } + + static function string(): tstring { return self::get("string"); } + static function bool(): tbool { return self::get("bool"); } + static function int(): tint { return self::get("int"); } + static function float(): tfloat { return self::get("float"); } + static function array(): tarray { return self::get("array"); } + static function callable(): tcallable { return self::get("callable"); } +} diff --git a/php/src/schema/types/IType.php b/php/src/schema/types/IType.php new file mode 100644 index 0000000..71017cf --- /dev/null +++ b/php/src/schema/types/IType.php @@ -0,0 +1,33 @@ + tstring::class, + "bool" => tbool::class, "boolean" => tbool::class, + "int" => tint::class, "integer" => tint::class, + "float" => tfloat::class, "flt" => tfloat::class, + "double" => tfloat::class, "dbl" => tfloat::class, + "array" => tarray::class, + "callable" => tcallable::class, + # types spéciaux + "key" => tkey::class, + "pkey" => tpkey::class, + "content" => tcontent::class, + ]; + + function __construct() { + $this->types = []; + } + + /** @var IType[] */ + protected $types; + + function get(string $name): IType { + $type = cl::get($this->types, $name); + if ($type === null) { + $class = self::TYPES[$name]; + $type = $this->types[$name] = new $class(); + } + return $type; + } +} diff --git a/php/src/schema/types/_tsimple.php b/php/src/schema/types/_tsimple.php new file mode 100644 index 0000000..ad2895e --- /dev/null +++ b/php/src/schema/types/_tsimple.php @@ -0,0 +1,14 @@ +isAvailable($destKey) && $input->get($destKey) !== false; + } + + function isNull($value): bool { + return $value === null || (is_string($value) && trim($value) === ""); + } +} diff --git a/php/src/schema/types/tarray.php b/php/src/schema/types/tarray.php new file mode 100644 index 0000000..f7b335b --- /dev/null +++ b/php/src/schema/types/tarray.php @@ -0,0 +1,27 @@ +isAvailable($destKey); + } + + function isValid($value, ?bool &$normalized=null): bool { + $normalized = is_bool($value); + if (is_string($value)) { + $value = trim($value); + $valid = self::is_yes($value) || self::is_no($value); + } else { + $valid = is_scalar($value); + } + return $valid; + } + + /** + * @var ScalarResult $result + * @var ScalarSchema $schema + */ + function verifix(&$value, Result &$result, Schema $schema): bool { + if (is_bool($value)) { + $result->setNormalized(); + return false; + } elseif (is_string($value)) { + $bool = trim($value); + if (self::is_yes($bool)) $value = true; + elseif (self::is_no($bool)) $value = false; + else return $result->setInvalid($value, $schema); + } elseif (is_scalar($value)) { + $value = boolval($value); + } else { + return $result->setInvalid($value, $schema); + } + $result->setValid(); + return true; + } + + const OUINON_FORMAT = ["Oui", "Non", false]; + const OUINONNULL_FORMAT = ["Oui", "Non", ""]; + const ON_FORMAT = ["O", "N", false]; + const ONN_FORMAT = ["O", "N", ""]; + const XN_FORMAT = ["X", "", false]; + const OZ_FORMAT = ["1", "", false]; + const FORMATS = [ + "ouinon" => self::OUINON_FORMAT, + "ouinonnull" => self::OUINONNULL_FORMAT, + "on" => self::ON_FORMAT, + "onn" => self::ONN_FORMAT, + "xn" => self::XN_FORMAT, + "oz" => self::OZ_FORMAT, + ]; + + const DEFAULT_FORMAT = self::OUINON_FORMAT; + + function format($value, $format=null): string { + if ($format === null) $format = static::DEFAULT_FORMAT; + if (is_string($format)) { + $oformat = $format; + $format = cl::get(self::FORMATS, strtolower($oformat)); + if ($format === null) throw ValueException::invalid_kind($oformat, "format"); + } + if ($value === null) { + $null = $format[2]; + if ($null !== false) return $null; + } + return $value? $format[0]: $format[1]; + } +} diff --git a/php/src/schema/types/tcallable.php b/php/src/schema/types/tcallable.php new file mode 100644 index 0000000..fab6e86 --- /dev/null +++ b/php/src/schema/types/tcallable.php @@ -0,0 +1,29 @@ +setNormalized(); + return false; + } elseif (is_string($value)) { + $float = trim($value); + if (is_numeric($float)) $value = floatval($float); + else return $result->setInvalid($value, $schema); + } elseif (is_scalar($value)) { + $value = floatval($value); + } else { + return $result->setInvalid($value, $schema); + } + $result->setValid(); + return true; + } + + function format($value, $format=null): string { + if ($format !== null) return sprintf($format, $value); + else return strval($value); + } +} diff --git a/php/src/schema/types/tint.php b/php/src/schema/types/tint.php new file mode 100644 index 0000000..ca59754 --- /dev/null +++ b/php/src/schema/types/tint.php @@ -0,0 +1,52 @@ +setNormalized(); + return false; + } elseif (is_string($value)) { + $int = trim($value); + if (is_numeric($int)) $value = intval($int); + else return $result->setInvalid($value, $schema); + } elseif (is_scalar($value)) { + $value = intval($value); + } else { + return $result->setInvalid($value, $schema); + } + $result->setValid(); + return true; + } + + function format($value, $format=null): string { + if ($format !== null) return sprintf($format, $value); + else return strval($value); + } +} diff --git a/php/src/schema/types/tkey.php b/php/src/schema/types/tkey.php new file mode 100644 index 0000000..120e729 --- /dev/null +++ b/php/src/schema/types/tkey.php @@ -0,0 +1,26 @@ +setNormalized(); + return false; + } elseif (is_scalar($value)) { + $value = strval($value); + $result->setValid(); + return true; + } else { + $result->setInvalid($value, $schema); + return false; + } + } + + function format($value, $format=null): string { + return strval($value); + } +} diff --git a/php/src/str.php b/php/src/str.php index 3ef054a..3a8353b 100644 --- a/php/src/str.php +++ b/php/src/str.php @@ -1,8 +1,6 @@ fem = $fem; + $this->le = $le; + $this->du = $du; + $this->au = $au; + $this->w = $spec; + } + + /** + * retourner le mot sans article + * + * @param bool|int $amount nombre du nom, avec l'équivalence false===0 et + * true===2. à partir de 2, le mot est ecrit au pluriel + * @param bool|string $fem genre du nom avec lequel accorder les adjectifs, + * avec l'équivalence false==="M" et true==="F" + */ + function w($amount=1, bool $upper1=false, $fem=false): string { + if ($amount === true) $amount = 2; + elseif ($amount === false) $amount = 0; + $amount = abs($amount); + $w = $this->w; + # marque du nombre + if ($amount <= 1) { + $w = preg_replace('/#[sx]/', "", $w); + } else { + $w = preg_replace('/#([sx])/', "$1", $w); + } + # marque du genre + if ($fem === "f" || $fem === "F") $fem = true; + elseif ($fem === "m" || $fem === "M") $fem = false; + $repl = $fem? "$1": ""; + $w = preg_replace('/#([e])/', $repl, $w); + # mise en majuscule + if ($upper1) { + if (strpos($w, "^") === false) { + # uniquement la première lettre + $w = txt::upper1($w); + } else { + # toutes les lettres qui suivent les occurences de ^ + $w = preg_replace_callback('/\^([[:alpha:]])/u', function ($ms) { + return mb_strtoupper($ms[1]); + }, $w); + } + } + return $w; + } + + /** + * retourner le mot sans article avec la première lettre en majuscule. + * alias pour $this->w($amount, true, $fem) + * + * @param bool|int $amount + */ + function u($amount=1, $fem=false): string { + return $this->w($amount, true, $fem); + } + + /** + * retourner l'adjectif accordé avec le genre spécifié. + * alias pour $this->w($amount, false, $fem) + * + * @param bool|int $amount + */ + function a($fem=false, $amount=1): string { + return $this->w($amount, false, $fem); + } + + /** retourner le mot sans article et avec la quantité */ + function q(int $amount=1, $fem=false): string { + return $amount." ".$this->w($amount, $fem); + } + + /** retourner le mot sans article et avec la quantité $amount/$max */ + function r(int $amount, int $max, $fem=false): string { + return "$amount/$max ".$this->w($amount, $fem); + } + + /** retourner le mot avec l'article indéfini et la quantité */ + function un(int $amount=1, $fem=false): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $aucun = $this->fem? "aucune ": "aucun "; + return $aucun.$this->w($amount, $fem); + } elseif ($abs_amount == 1) { + $un = $this->fem? "une ": "un "; + return $un.$this->w($amount, $fem); + } else { + return "les $amount ".$this->w($amount, $fem); + } + } + + function le(int $amount=1, $fem=false): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $le = $this->fem? "la 0 ": "le 0 "; + return $le.$this->w($amount, $fem); + } elseif ($abs_amount == 1) { + return $this->le.$this->w($amount, $fem); + } else { + return "les $amount ".$this->w($amount, $fem); + } + } + + function du(int $amount=1, $fem=false): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $du = $this->fem? "de la 0 ": "du 0 "; + return $du.$this->w($amount, $fem); + } elseif ($abs_amount == 1) { + return $this->du.$this->w($amount, $fem); + } else { + return "des $amount ".$this->w($amount, $fem); + } + } + + function au(int $amount=1, $fem=false): string { + $abs_amount = abs($amount); + if ($abs_amount == 0) { + $au = $this->fem? "à la 0 ": "au 0 "; + return $au.$this->w($amount, $fem); + } elseif ($abs_amount == 1) { + return $this->au.$this->w($amount, $fem); + } else { + return "aux $amount ".$this->w($amount, $fem); + } + } +} diff --git a/php/src/text/words.php b/php/src/text/words.php new file mode 100644 index 0000000..f11c392 --- /dev/null +++ b/php/src/text/words.php @@ -0,0 +1,14 @@ +q($count); + } + + static function r(int $count, int $max, string $spec, bool $adjective=true): string { + $word = new Word($spec, $adjective); + return $word->r($count, $max); + } +} diff --git a/php/src/web/ext/CurlException.php b/php/src/web/curl/CurlException.php similarity index 68% rename from php/src/web/ext/CurlException.php rename to php/src/web/curl/CurlException.php index 09fd4f1..53fda92 100644 --- a/php/src/web/ext/CurlException.php +++ b/php/src/web/curl/CurlException.php @@ -1,5 +1,5 @@ $value) { + if (is_array($value)) { + self::parse_files($files, "$pkey.$key", $value, $lastkey); + } else { + cl::pset($files, "$pkey.$key.$lastkey", $value); + } + } + } + + /** @var array */ + private static $_files; + + static function _files(?array $_files=null): ?array { + if (self::$_files === null) { + $files = []; + if ($_files === null) $_files = $_FILES; + foreach ($_files as $pkey => $values) { + $name = $values["name"] ?? null; + $type = $values["type"] ?? null; + $error = $values["error"] ?? null; + if (is_scalar($name) && is_scalar($type) && is_scalar($error)) { + $files[$pkey] = $values; + } else { + self::parse_files($files, $pkey, $values["name"], "name"); + self::parse_files($files, $pkey, $values["type"], "type"); + self::parse_files($files, $pkey, $values["tmp_name"], "tmp_name"); + self::parse_files($files, $pkey, $values["error"], "error"); + self::parse_files($files, $pkey, $values["size"], "size"); + $full_path = $values["full_path"] ?? null; + if ($full_path !== null) { + self::parse_files($files, $pkey, $full_path, "full_path"); + } + } + } + self::$_files = $files; + } + return self::$_files; + } + + static function get(string $pkey, bool $required=true, bool $check=true): Upload { + $_files = self::_files(); + return new Upload(cl::pget($_files, $pkey), $required, $check); + } + + static function all(string $pkey, bool $required=true, bool $check=true) { + $_files = self::_files(); + $uploads = []; + foreach (cl::pget($_files, $pkey) as $file) { + $uploads[] = new Upload($file, $required, $check); + } + return $uploads; + } +} diff --git a/php/tests/ValueExceptionTest.php b/php/tests/ValueExceptionTest.php deleted file mode 100644 index a41a937..0000000 --- a/php/tests/ValueExceptionTest.php +++ /dev/null @@ -1,14 +0,0 @@ -getMessage()); - - $e = ValueException::invalid_class(ValueException::class, self::class); - self::assertSame(ValueException::class.": class is invalid, expected ".self::class, $e->getMessage()); - } -} diff --git a/php/tests/db/sqlite/.gitignore b/php/tests/db/sqlite/.gitignore new file mode 100644 index 0000000..3d45538 --- /dev/null +++ b/php/tests/db/sqlite/.gitignore @@ -0,0 +1 @@ +/capacitor.db diff --git a/php/tests/db/sqlite/SqliteCapacitorTest.php b/php/tests/db/sqlite/SqliteCapacitorTest.php new file mode 100644 index 0000000..1d324c7 --- /dev/null +++ b/php/tests/db/sqlite/SqliteCapacitorTest.php @@ -0,0 +1,117 @@ +reset($channel); + $capacitor->charge($channel, "first"); + $capacitor->charge($channel, "second"); + $capacitor->charge($channel, "third"); + $items = iterator_to_array($capacitor->discharge($channel, null, false)); + self::assertSame(["first", "second", "third"], $items); + } + + function _testChargeArrays(SqliteCapacitor $capacitor, ?string $channel) { + $capacitor->reset($channel); + $capacitor->charge($channel, ["id" => 10, "name" => "first"]); + $capacitor->charge($channel, ["name" => "second", "id" => 20]); + $capacitor->charge($channel, ["name" => "third", "id" => "30"]); + } + + function testChargeStrings() { + $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db'); + $this->_testChargeStrings($capacitor, null); + $capacitor->close(); + } + + function testChargeArrays() { + $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db'); + $capacitor->addChannel(new class extends CapacitorChannel { + const NAME = "arrays"; + function getKeyDefinitions(): ?array { + return ["id" => "integer"]; + } + function getKeyValues($item): ?array { + return ["id" => $item["id"] ?? null]; + } + }); + + $this->_testChargeStrings($capacitor, "strings"); + $this->_testChargeArrays($capacitor, "arrays"); + $capacitor->close(); + } + + function testEach() { + $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db'); + $capacitor = new Capacitor($capacitor, new class extends CapacitorChannel { + const NAME = "each"; + + function getKeyDefinitions(): ?array { + return [ + "age" => "integer", + "done" => "integer default 0", + ]; + } + function getKeyValues($item): ?array { + return [ + "age" => $item["age"], + ]; + } + }); + + $capacitor->reset(); + $capacitor->charge(["name" => "first", "age" => 5]); + $capacitor->charge(["name" => "second", "age" => 10]); + $capacitor->charge(["name" => "third", "age" => 15]); + $capacitor->charge(["name" => "fourth", "age" => 20]); + + $setDone = function ($item, $row, $suffix=null) { + $updates = ["done" => 1]; + if ($suffix !== null) { + $item["name"] .= $suffix; + $updates["_item"] = $item; + } + return $updates; + }; + $capacitor->each(["age" => [">", 10]], $setDone, ["++"]); + $capacitor->each(["done" => 0], $setDone, null); + Txx(iterator_to_array($capacitor->discharge(null, false))); + + $capacitor->close(); + self::assertTrue(true); + } + + function testPrimayKey() { + $capacitor = new SqliteCapacitor(__DIR__.'/capacitor.db'); + $capacitor = new Capacitor($capacitor, new class extends CapacitorChannel { + const NAME = "pk"; + + function getKeyDefinitions(): ?array { + return [ + "_id" => "varchar primary key", + "done" => "integer default 0", + ]; + } + function getKeyValues($item): ?array { + return [ + "_id" => $item["numero"], + ]; + } + }); + + $capacitor->charge(["numero" => "a", "name" => "first", "age" => 5]); + $capacitor->charge(["numero" => "b", "name" => "second", "age" => 10]); + $capacitor->charge(["numero" => "c", "name" => "third", "age" => 15]); + $capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 20]); + sleep(2); + $capacitor->charge(["numero" => "b", "name" => "second", "age" => 100]); + $capacitor->charge(["numero" => "d", "name" => "fourth", "age" => 200]); + + $capacitor->close(); + self::assertTrue(true); + } +} diff --git a/php/tests/db/sqlite/SqliteTest.php b/php/tests/db/sqlite/SqliteTest.php new file mode 100644 index 0000000..b56855c --- /dev/null +++ b/php/tests/db/sqlite/SqliteTest.php @@ -0,0 +1,146 @@ + [ + self::CREATE_PERSON, + self::INSERT_JEPHTE, + ], + ]); + self::assertSame("clain", $sqlite->get("select nom, age from person")); + self::assertSame([ + "nom" => "clain", + "age" => 50, + ], $sqlite->get("select nom, age from person", null, true)); + + $sqlite->exec(self::INSERT_JEAN); + self::assertSame("payet", $sqlite->get("select nom, age from person where nom = 'payet'")); + self::assertSame([ + "nom" => "payet", + "age" => 32, + ], $sqlite->get("select nom, age from person where nom = 'payet'", null, true)); + + self::assertSame([ + ["key" => "0", "value" => self::CREATE_PERSON, "done" => 1], + ["key" => "1", "value" => self::INSERT_JEPHTE, "done" => 1], + ], iterator_to_array($sqlite->all("select key, value, done from _migration"))); + } + + function testException() { + $sqlite = new Sqlite(":memory:"); + self::assertException(Exception::class, [$sqlite, "exec"], "prout"); + self::assertException(SqliteException::class, [$sqlite, "exec"], ["prout"]); + } + + protected function assertInserted(Sqlite $sqlite, array $row, array $query): void { + $sqlite->exec($query); + self::assertSame($row, $sqlite->one("select * from mapping where i = :i", [ + "i" => $query["values"]["i"], + ])); + } + function testInsert() { + $sqlite = new Sqlite(":memory:", [ + "migrate" => "create table mapping (i integer, s varchar)", + ]); + $sqlite->exec(["insert into mapping", "values" => ["i" => 1, "s" => "un"]]); + $sqlite->exec(["insert mapping", "values" => ["i" => 2, "s" => "deux"]]); + $sqlite->exec(["insert into", "into" => "mapping", "values" => ["i" => 3, "s" => "trois"]]); + $sqlite->exec(["insert", "into" => "mapping", "values" => ["i" => 4, "s" => "quatre"]]); + $sqlite->exec(["insert into mapping(i)", "values" => ["i" => 5, "s" => "cinq"]]); + $sqlite->exec(["insert into (i)", "into" => "mapping", "values" => ["i" => 6, "s" => "six"]]); + $sqlite->exec(["insert into mapping(i) values ()", "values" => ["i" => 7, "s" => "sept"]]); + $sqlite->exec(["insert into mapping(i) values (8)", "values" => ["i" => 42, "s" => "whatever"]]); + $sqlite->exec(["insert into mapping(i, s) values (9, 'neuf')", "values" => ["i" => 43, "s" => "garbage"]]); + $sqlite->exec(["insert into mapping", "cols" => ["i"], "values" => ["i" => 10, "s" => "dix"]]); + + self::assertSame([ + ["i" => 1, "s" => "un"], + ["i" => 2, "s" => "deux"], + ["i" => 3, "s" => "trois"], + ["i" => 4, "s" => "quatre"], + ["i" => 5, "s" => null/*"cinq"*/], + ["i" => 6, "s" => null/*"six"*/], + ["i" => 7, "s" => null/*"sept"*/], + ["i" => 8, "s" => null/*"huit"*/], + ["i" => 9, "s" => "neuf"], + ["i" => 10, "s" => null/*"dix"*/], + ], iterator_to_array($sqlite->all("select * from mapping"))); + } + + function testSelect() { + $sqlite = new Sqlite(":memory:", [ + "migrate" => "create table user (name varchar, amount integer)", + ]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain1", "amount" => 1]]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain2", "amount" => 2]]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain5", "amount" => 5]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain7", "amount" => 7]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain9", "amount" => 9]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain10", "amount" => 10]]); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one("select * from user where name = 'jclain1'")); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select * from user where name = 'jclain1'"])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select from user where name = 'jclain1'"])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select from user where", "where" => ["name = 'jclain1'"]])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select from user", "where" => ["name = 'jclain1'"]])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select", "from" => "user", "where" => ["name = 'jclain1'"]])); + self::assertSame([ + "name" => "jclain1", + "amount" => 1, + ], $sqlite->one(["select", "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "name" => "jclain1", + ], $sqlite->one(["select name", "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "name" => "jclain1", + ], $sqlite->one(["select", "cols" => "name", "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "name" => "jclain1", + ], $sqlite->one(["select", "cols" => ["name"], "from" => "user", "where" => ["name" => "jclain1"]])); + self::assertSame([ + "plouf" => "jclain1", + ], $sqlite->one(["select", "cols" => ["plouf" => "name"], "from" => "user", "where" => ["name" => "jclain1"]])); + } + + function testSelectGroupBy() { + $sqlite = new Sqlite(":memory:", [ + "migrate" => "create table user (name varchar, amount integer)", + ]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain1", "amount" => 1]]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain2", "amount" => 1]]); + $sqlite->exec(["insert into user", "values" => ["name" => "jclain5", "amount" => 2]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain7", "amount" => 2]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain9", "amount" => 2]]); + $sqlite->exec(["insert into user", "values" => ["name" => "fclain10", "amount" => 3]]); + + self::assertSame([ + ["count" => 2], + ], iterator_to_array($sqlite->all(["select count(name) as count from user", "group by" => ["amount"], "having" => ["count(name) = 2"]]))); + } +} diff --git a/php/tests/db/sqlite/_queryTest.php b/php/tests/db/sqlite/_queryTest.php new file mode 100644 index 0000000..30290de --- /dev/null +++ b/php/tests/db/sqlite/_queryTest.php @@ -0,0 +1,86 @@ + 42, "string" => "value"], $sql, $params); + self::assertSame(["(int = :int and string = :string)"], $sql); + self::assertSame(["int" => 42, "string" => "value"], $params); + + $sql = $params = null; + _query::parse_conds(["or", "int" => 42, "string" => "value"], $sql, $params); + self::assertSame(["(int = :int or string = :string)"], $sql); + self::assertSame(["int" => 42, "string" => "value"], $params); + + $sql = $params = null; + _query::parse_conds([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + self::assertSame(["((int = :int and string = :string) and (int = :int1 and string = :string1))"], $sql); + self::assertSame(["int" => 42, "string" => "value", "int1" => 24, "string1" => "eulav"], $params); + + $sql = $params = null; + _query::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params); + self::assertSame(["(int is null and string <> :string)"], $sql); + self::assertSame(["string" => "value"], $params); + } + + function testParseValues(): void { + $sql = $params = null; + _query::parse_set_values(null, $sql, $params); + self::assertNull($sql); + self::assertNull($params); + + $sql = $params = null; + _query::parse_set_values([], $sql, $params); + self::assertNull($sql); + self::assertNull($params); + + $sql = $params = null; + _query::parse_set_values(["col = 'value'"], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _query::parse_set_values([["col = 'value'"]], $sql, $params); + self::assertSame(["col = 'value'"], $sql); + self::assertNull($params); + + $sql = $params = null; + _query::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); + self::assertSame(["int = :int", "string = :string"], $sql); + self::assertSame(["int" => 42, "string" => "value"], $params); + + $sql = $params = null; + _query::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); + self::assertSame(["int = :int", "string = :string"], $sql); + self::assertSame(["int" => 42, "string" => "value"], $params); + + $sql = $params = null; + _query::parse_set_values([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + self::assertSame(["int = :int", "string = :string", "int = :int1", "string = :string1"], $sql); + self::assertSame(["int" => 42, "string" => "value", "int1" => 24, "string1" => "eulav"], $params); + } + +} diff --git a/php/tests/file/base/FileReaderTest.php b/php/tests/file/base/FileReaderTest.php index 69bee08..de36b56 100644 --- a/php/tests/file/base/FileReaderTest.php +++ b/php/tests/file/base/FileReaderTest.php @@ -1,6 +1,7 @@ [null], + "default" => null, + "title" => null, + "required" => false, + "nullable" => true, + "desc" => null, + "analyzer_func" => null, + "extractor_func" => null, + "parser_func" => null, + "normalizer_func" => null, + "messages" => null, + "formatter_func" => null, + "format" => null, + "" => ["scalar"], + "name" => null, + "pkey" => null, + "header" => null, + "composite" => null, + ]; + + static function schema(array $schema): array { + return array_merge(self::NULL_SCHEMA, $schema); + } + + function testNormalize() { + self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize(null)); + self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize([])); + self::assertSame(self::NULL_SCHEMA, ScalarSchema::normalize([null])); + self::assertException(SchemaException::class, function () { + ScalarSchema::normalize([[]]); + }); + self::assertException(SchemaException::class, function () { + ScalarSchema::normalize([[null]]); + }); + + $string = self::schema(["type" => ["string"], "nullable" => false]); + self::assertSame($string, ScalarSchema::normalize("string")); + self::assertSame($string, ScalarSchema::normalize(["string"])); + + $nstring = self::schema(["type" => ["string"]]); + self::assertSame($nstring, ScalarSchema::normalize(["?string"])); + self::assertSame($nstring, ScalarSchema::normalize(["?string|null"])); + self::assertSame($nstring, ScalarSchema::normalize(["string|null"])); + self::assertSame($nstring, ScalarSchema::normalize([["?string", "null"]])); + self::assertSame($nstring, ScalarSchema::normalize([["string", "null"]])); + self::assertSame($nstring, ScalarSchema::normalize([["string", null]])); + + $key = self::schema(["type" => ["string", "int"], "nullable" => false]); + self::assertSame($key, ScalarSchema::normalize("string|int")); + + $nkey = self::schema(["type" => ["string", "int"], "nullable" => true]); + self::assertSame($nkey, ScalarSchema::normalize("?string|int")); + self::assertSame($nkey, ScalarSchema::normalize("string|?int")); + } +} diff --git a/php/tests/schema/types/boolTest.php b/php/tests/schema/types/boolTest.php new file mode 100644 index 0000000..8f990e3 --- /dev/null +++ b/php/tests/schema/types/boolTest.php @@ -0,0 +1,111 @@ +set(true); + self::assertSame(true, $destv->get()); + self::assertSame(true, $dest); + self::assertSame("Oui", $destv->format()); + self::assertSame("Oui", $destv->format("OuiNonNull")); + self::assertSame("O", $destv->format("ON")); + self::assertSame("O", $destv->format("ONN")); + + $destv->set(false); + self::assertSame(false, $destv->get()); + self::assertSame(false, $dest); + self::assertSame("Non", $destv->format()); + self::assertSame("Non", $destv->format("OuiNonNull")); + self::assertSame("N", $destv->format("ON")); + self::assertSame("N", $destv->format("ONN")); + + $destv->set("yes"); + self::assertSame(true, $destv->get()); + + $destv->set(" yes "); + self::assertSame(true, $destv->get()); + + $destv->set("12"); + self::assertSame(true, $destv->get()); + + $destv->set(12); + self::assertSame(true, $destv->get()); + + $destv->set("no"); + self::assertSame(false, $destv->get()); + + $destv->set(" no "); + self::assertSame(false, $destv->get()); + + $destv->set("0"); + self::assertSame(false, $destv->get()); + + $destv->set(0); + self::assertSame(false, $destv->get()); + + $destv->set(12.34); + self::assertSame(true, $destv->get()); + + self::assertException(Exception::class, $destvSetter("a")); + self::assertException(Exception::class, $destvSetter([])); + self::assertException(Exception::class, $destvSetter(["a"])); + + } + + function testBool() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "bool"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + self::assertException(Exception::class, $destvSetter("")); + self::assertException(Exception::class, $destvSetter(" ")); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testNbool() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "?bool"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("Non", $destv->format()); + self::assertSame("", $destv->format("OuiNonNull")); + self::assertSame("N", $destv->format("ON")); + self::assertSame("", $destv->format("ONN")); + + $destv->set(""); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("Non", $destv->format()); + self::assertSame("", $destv->format("OuiNonNull")); + self::assertSame("N", $destv->format("ON")); + self::assertSame("", $destv->format("ONN")); + + $destv->set(" "); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("Non", $destv->format()); + self::assertSame("", $destv->format("OuiNonNull")); + self::assertSame("N", $destv->format("ON")); + self::assertSame("", $destv->format("ONN")); + + $this->commonTests($destv, $dest, $destvSetter); + } +} diff --git a/php/tests/schema/types/floatTest.php b/php/tests/schema/types/floatTest.php new file mode 100644 index 0000000..193d407 --- /dev/null +++ b/php/tests/schema/types/floatTest.php @@ -0,0 +1,139 @@ +set(12); + self::assertSame(12.0, $destv->get()); + self::assertSame(12.0, $dest); + self::assertSame("12", $destv->format()); + self::assertSame("0012", $destv->format("%04u")); + + $destv->set("12"); + self::assertSame(12.0, $destv->get()); + + $destv->set(" 12 "); + self::assertSame(12.0, $destv->get()); + + $destv->set(12.34); + self::assertSame(12.34, $destv->get()); + + $destv->set(true); + self::assertSame(1.0, $destv->get()); + + self::assertException(Exception::class, $destvSetter("a")); + self::assertException(Exception::class, $destvSetter([])); + self::assertException(Exception::class, $destvSetter(["a"])); + } + + function testFloat() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "float"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + self::assertException(Exception::class, $destvSetter("")); + self::assertException(Exception::class, $destvSetter(" ")); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredFloat() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "float", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + self::assertException(Exception::class, $destvSetter("")); + self::assertException(Exception::class, $destvSetter(" ")); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testNfloat() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "?float"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(""); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(" "); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredNfloat() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "?float", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(""); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(" "); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } +} diff --git a/php/tests/schema/types/intTest.php b/php/tests/schema/types/intTest.php new file mode 100644 index 0000000..29de7ce --- /dev/null +++ b/php/tests/schema/types/intTest.php @@ -0,0 +1,139 @@ +set(12); + self::assertSame(12, $destv->get()); + self::assertSame(12, $dest); + self::assertSame("12", $destv->format()); + self::assertSame("0012", $destv->format("%04u")); + + $destv->set("12"); + self::assertSame(12, $destv->get()); + + $destv->set(" 12 "); + self::assertSame(12, $destv->get()); + + $destv->set(12.34); + self::assertSame(12, $destv->get()); + + $destv->set(true); + self::assertSame(1, $destv->get()); + + self::assertException(Exception::class, $destvSetter("a")); + self::assertException(Exception::class, $destvSetter([])); + self::assertException(Exception::class, $destvSetter(["a"])); + } + + function testInt() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "int"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + self::assertException(Exception::class, $destvSetter("")); + self::assertException(Exception::class, $destvSetter(" ")); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredInt() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "int", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + self::assertException(Exception::class, $destvSetter("")); + self::assertException(Exception::class, $destvSetter(" ")); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testNint() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "?int"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(""); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(" "); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredNint() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "?int", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(""); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + $destv->set(" "); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } +} diff --git a/php/tests/schema/types/strTest.php b/php/tests/schema/types/strTest.php new file mode 100644 index 0000000..78e33c9 --- /dev/null +++ b/php/tests/schema/types/strTest.php @@ -0,0 +1,123 @@ +set(""); + self::assertSame("", $destv->get()); + self::assertSame("", $dest); + + $destv->set(" "); + self::assertSame(" ", $destv->get()); + self::assertSame(" ", $dest); + + $destv->set("a"); + self::assertSame("a", $destv->get()); + self::assertSame("a", $dest); + + $destv->set("12"); + self::assertSame("12", $destv->get()); + + $destv->set(" 12 "); + self::assertSame(" 12 ", $destv->get()); + + $destv->set(12); + self::assertSame("12", $destv->get()); + + $destv->set(12.34); + self::assertSame("12.34", $destv->get()); + + $destv->set(true); + self::assertSame("1", $destv->get()); + + self::assertException(Exception::class, $destvSetter([])); + self::assertException(Exception::class, $destvSetter(["a"])); + } + + function testStr() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "string"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredStr() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "string", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + self::assertException(Exception::class, $destvSetter(null)); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testNstr() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, "?string"); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur non requise donc retourne null + $destv->set(false); + self::assertNull($destv->get()); + + $this->commonTests($destv, $dest, $destvSetter); + } + + function testRequiredNstr() { + /** @var ScalarValue $destv */ + Schema::nv($destv, $dest, null, $schema, [ + "?string", null, + "required" => true, + ]); + $destvSetter = function($value) use($destv) { + return function() use($destv, $value) { + $destv->set($value); + }; + }; + + $destv->set(null); + self::assertNull($destv->get()); + self::assertNull($dest); + self::assertSame("", $destv->format()); + + // valeur requise donc lance une exception + self::assertException(Exception::class, $destvSetter(false)); + + $this->commonTests($destv, $dest, $destvSetter); + } +} diff --git a/php/tests/schema/types/unionTest.php b/php/tests/schema/types/unionTest.php new file mode 100644 index 0000000..c208087 --- /dev/null +++ b/php/tests/schema/types/unionTest.php @@ -0,0 +1,29 @@ +set("12"); + self::assertSame("12", $si); + $siv->set(12); + self::assertSame(12, $si); + + # int puis string + Schema::nv($isv, $is, null, $iss, "int|string"); + + $isv->set("12"); + self::assertSame("12", $is); + $isv->set(12); + self::assertSame(12, $is); + } +} diff --git a/php/tests/strTest.php b/php/tests/strTest.php deleted file mode 100644 index ce654b1..0000000 --- a/php/tests/strTest.php +++ /dev/null @@ -1,56 +0,0 @@ - [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 'multiple' => [ + 'name' => [ + 0 => '', + 1 => '', + ], + 'type' => [ + 0 => '', + 1 => '', + ], + 'tmp_name' => [ + 0 => '', + 1 => '', + ], + 'error' => [ + 0 => 4, + 1 => 4, + ], + 'size' => [ + 0 => 0, + 1 => 0, + ], + ], + 'onelevel' => [ + 'name' => [ + 'a' => '', + 'b' => '', + ], + 'type' => [ + 'a' => '', + 'b' => '', + ], + 'tmp_name' => [ + 'a' => '', + 'b' => '', + ], + 'error' => [ + 'a' => 4, + 'b' => 4, + ], + 'size' => [ + 'a' => 0, + 'b' => 0, + ], + ], + 'multiplelevel' => [ + 'name' => [ + 'a' => [ + 0 => '', + 1 => '', + ], + 'b' => [ + 0 => '', + 1 => '', + ], + ], + 'type' => [ + 'a' => [ + 0 => '', + 1 => '', + ], + 'b' => [ + 0 => '', + 1 => '', + ], + ], + 'tmp_name' => [ + 'a' => [ + 0 => '', + 1 => '', + ], + 'b' => [ + 0 => '', + 1 => '', + ], + ], + 'error' => [ + 'a' => [ + 0 => 4, + 1 => 4, + ], + 'b' => [ + 0 => 4, + 1 => 4, + ], + ], + 'size' => [ + 'a' => [ + 0 => 0, + 1 => 0, + ], + 'b' => [ + 0 => 0, + 1 => 0, + ], + ], + ], + ]; + + const PARSED = [ + 'simple' => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 'multiple' => [ + 0 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 1 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + 'onelevel' => [ + 'a' => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 'b' => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + 'multiplelevel' => [ + 'a' => [ + 0 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 1 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + 'b' => [ + 0 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + 1 => [ + 'name' => '', + 'type' => '', + 'tmp_name' => '', + 'error' => 4, + 'size' => 0, + ], + ], + ], + ]; + + function test_files() { + self::assertSame(self::PARSED, uploads::_files(self::_FILES)); + } +}