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));
+ }
+}