From f2614385febbed9e2b4de387eee17d65b7a1ed72 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Tue, 25 Mar 2025 08:47:45 +0400 Subject: [PATCH 01/32] Init changelog & version 0.4.1p82 --- CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 60676ac..f054030 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,5 @@ +## Release 0.4.1p82 du 25/03/2025-08:47 + ## Release 0.4.1p74 du 25/03/2025-08:47 * `5beb5e6` corriger la prise en compte du proxy From 60ab13ff84416fd68dfcad97c8f1e3a0095ea390 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Fri, 28 Mar 2025 15:39:05 +0400 Subject: [PATCH 02/32] modifs.mineures sans commentaires --- php/src/ref/schema/ref_input.php | 42 +++++++++++++++++++++++++++++++ php/src/ref/schema/ref_schema.php | 20 +++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 php/src/ref/schema/ref_input.php diff --git a/php/src/ref/schema/ref_input.php b/php/src/ref/schema/ref_input.php new file mode 100644 index 0000000..b7b00b9 --- /dev/null +++ b/php/src/ref/schema/ref_input.php @@ -0,0 +1,42 @@ + ["int", self::ACCESS_AUTO, "type d'accès: clé ou propriété"], + "allow_empty" => ["bool", true, "la chaine vide est-elle autorisée?"], + "allow_null" => ["bool", true, "la valeur null est-elle autorisée?"], + ]; + + const ACCESS_PARAMS_SCHEMA = [ + "allow_empty" => ["bool", true, "la chaine vide est-elle autorisée?"], + "allow_null" => ["bool", null, "la valeur null est-elle autorisée?"], + "allow_false" => ["bool", null, "la valeur false est-elle autorisée?"], + "protect_dest" => ["bool", null, "faut-il protéger la destination?"], + ]; + + const VALUE_ACCESS_PARAMS_SCHEMA = [ + "allow_null" => ["bool", false], + "allow_false" => ["bool", true], + "protect_dest" => ["bool", false], + ]; + + const ARRAY_ACCESS_PARAMS_SCHEMA = [ + "allow_null" => ["bool", true], + "allow_false" => ["bool", false], + "protect_dest" => ["bool", true], + "key_prefix" => ["?string", null, "préfixe des clés pour les méthodes ensureXxx()"], + "key_suffix" => ["?string", null, "suffixe des clés pour les méthodes ensureXxx()"], + ]; + + const PROPERTY_ACCESS_PARAMS_SCHEMA = [ + "allow_null" => ["bool", true], + "allow_false" => ["bool", false], + "protect_dest" => ["bool", true], + "key_prefix" => ["?string", null, "préfixe des clés pour les méthodes ensureXxx()"], + "key_suffix" => ["?string", null, "suffixe des clés pour les méthodes ensureXxx()"], + "map_names" => ["bool", true, "faut-il mapper les clés en camelCase?"] + ]; +} diff --git a/php/src/ref/schema/ref_schema.php b/php/src/ref/schema/ref_schema.php index f1af52e..bc100b1 100644 --- a/php/src/ref/schema/ref_schema.php +++ b/php/src/ref/schema/ref_schema.php @@ -44,18 +44,38 @@ class ref_schema { "invalid" => "Cette valeur est invalide", ]; + const PARAMS_SCHEMA = [ + "analyze" => ["bool", true, "faut-il analyser la valeur?"], + "reanalyze" => ["bool", true, "faut-il forcer l'analyse de la valeur?"], + "normalize" => ["bool", true, "faut-il normaliser la valeur?"], + "renormalize" => ["bool", true, "faut-il forcer la normalisation de la valeur?"], + "throw" => ["bool", true, "faut-il lancer une exception en cas d'erreur?"], + //...ref_input::INPUT_PARAMS_SCHEMA, + ]; + /** @var array clés supplémentaires de schéma de la nature scalaire */ const SCALAR_NATURE_METASCHEMA = [ ]; + const SCALAR_PARAMS_SCHEMA = [ + ]; + /** @var array clés supplémentaires de schéma de la nature associative */ const ASSOC_NATURE_METASCHEMA = [ "ensure_array" => ["bool", false, "faut-il s'assurer que le tableau destination est non nul?"], + "ensure_assoc" => ["bool", true, "faut-il s'assurer que le tableau destination est associatif?"], "ensure_keys" => ["bool", true, "faut-il s'assurer que toutes les clés existent?"], "ensure_order" => ["bool", true, "faut-il s'assurer que les clés soient dans l'ordre?"], ]; + const ASSOC_PARAMS_SCHEMA = [ + //...self::ASSOC_NATURE_METASCHEMA, + ]; + /** @var array clés supplémentaires de schéma de la nature liste */ const LIST_NATURE_METASCHEMA = [ ]; + + const LIST_PARAMS_SCHEMA = [ + ]; } From a8d55d329af3e70769f372451906c35647413c87 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Fri, 28 Mar 2025 16:17:25 +0400 Subject: [PATCH 03/32] modifs.mineures sans commentaires --- php/src/ref/schema/ref_schema.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/php/src/ref/schema/ref_schema.php b/php/src/ref/schema/ref_schema.php index bc100b1..60392c8 100644 --- a/php/src/ref/schema/ref_schema.php +++ b/php/src/ref/schema/ref_schema.php @@ -62,14 +62,17 @@ class ref_schema { /** @var array clés supplémentaires de schéma de la nature associative */ const ASSOC_NATURE_METASCHEMA = [ - "ensure_array" => ["bool", false, "faut-il s'assurer que le tableau destination est non nul?"], - "ensure_assoc" => ["bool", true, "faut-il s'assurer que le tableau destination est associatif?"], - "ensure_keys" => ["bool", true, "faut-il s'assurer que toutes les clés existent?"], - "ensure_order" => ["bool", true, "faut-il s'assurer que les clés soient dans l'ordre?"], + "ensure_array" => ["bool", null, "faut-il s'assurer que le tableau destination est non nul?"], + "ensure_assoc" => ["bool", null, "faut-il s'assurer que le tableau destination est associatif?"], + "ensure_keys" => ["bool", null, "faut-il s'assurer que toutes les clés existent avec la valeur par défaut?"], + "ensure_order" => ["bool", null, "faut-il s'assurer que les clés soient dans l'ordre?"], ]; const ASSOC_PARAMS_SCHEMA = [ - //...self::ASSOC_NATURE_METASCHEMA, + "ensure_array" => ["bool", false], + "ensure_assoc" => ["bool", true], + "ensure_keys" => ["bool", true], + "ensure_order" => ["bool", true], ]; /** @var array clés supplémentaires de schéma de la nature liste */ From 1536e091fb0020858204f59462a7a80b5f9775d9 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Thu, 3 Apr 2025 06:22:29 +0400 Subject: [PATCH 04/32] =?UTF-8?q?am=C3=A9liorations=20func?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- php/src/php/func.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/php/src/php/func.php b/php/src/php/func.php index 63ea334..133fd5f 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -3,13 +3,16 @@ namespace nulib\php; use Closure; use Exception; +use Generator; use nulib\A; +use nulib\cl; use nulib\cv; use nulib\StateException; use nulib\ValueException; use ReflectionClass; use ReflectionFunction; use ReflectionMethod; +use Traversable; /** * Class func: outils pour appeler fonctions et méthodes dynamiquement @@ -491,6 +494,34 @@ class func { return self::with($func)->invoke($args); } + /** + * si $value est une fonction, l'appeler + * si $value ou le résultat de l'appel est un Traversable, le résoudre + * sinon retourner $value tel quel + * + * en définitive, la valeur de retour de cette fonction est soit un scalaire, + * soit un array, soit un objet qui n'est pas Traversable + * @return mixed + */ + static function get_value($value, ...$args) { + if ($value instanceof self) $value = $value->invoke($args); + elseif (is_callable($value)) $value = self::call($value, ...$args); + if ($value instanceof Traversable) $value = cl::all($value); + return $value; + } + + /** + * si $value est une fonction, l'appeler + * si $value ou le résultat de l'appel est un Traversable, le retourner + * sinon retourner $value en tant qu'array + */ + static function get_iterable($value, ...$args): ?iterable { + if ($value instanceof self) $value = $value->invoke($args); + elseif (is_callable($value)) $value = self::call($value, ...$args); + if ($value instanceof Traversable) return $value; + else return cl::withn($value); + } + ############################################################################# protected function __construct(int $type, $func, ?array $args=null, bool $bound=false, ?string $reason=null) { From bd1f901b70943e9e03ed56e15ebd4dbaece61b00 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Wed, 9 Apr 2025 23:18:06 +0400 Subject: [PATCH 05/32] =?UTF-8?q?r=C3=A9organiser=20le=20code=20de=20g?= =?UTF-8?q?=C3=A9n=C3=A9ration=20sql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 1 + php/src/db/_private/Tcreate.php | 39 --- php/src/db/_private/Tdelete.php | 38 --- php/src/db/_private/Tgeneric.php | 18 -- php/src/db/_private/Tinsert.php | 82 ------ php/src/db/_private/Tselect.php | 168 ------------ php/src/db/_private/Tupdate.php | 40 --- php/src/db/_private/_base.php | 252 +---------------- php/src/db/_private/_common.php | 255 ++++++++++++++++++ php/src/db/_private/_create.php | 37 ++- php/src/db/_private/_delete.php | 35 ++- php/src/db/_private/_generic.php | 16 +- php/src/db/_private/_insert.php | 80 +++++- php/src/db/_private/_select.php | 166 +++++++++++- php/src/db/_private/_update.php | 38 ++- php/src/db/mysql/MysqlStorage.php | 2 +- php/src/db/mysql/_query_base.php | 52 ---- php/src/db/mysql/_query_create.php | 10 - php/src/db/mysql/_query_delete.php | 10 - php/src/db/mysql/_query_generic.php | 10 - php/src/db/mysql/_query_insert.php | 10 - php/src/db/mysql/_query_select.php | 10 - php/src/db/mysql/_query_update.php | 10 - php/src/db/mysql/query.php | 9 +- php/src/db/pdo/Pdo.php | 6 +- php/src/db/pdo/_query_create.php | 10 - php/src/db/pdo/_query_delete.php | 10 - php/src/db/pdo/_query_generic.php | 10 - php/src/db/pdo/_query_insert.php | 10 - php/src/db/pdo/_query_select.php | 10 - php/src/db/pdo/_query_update.php | 10 - php/src/db/pdo/{_query_base.php => query.php} | 46 ++-- php/src/db/sqlite/Sqlite.php | 6 +- php/src/db/sqlite/SqliteStorage.php | 2 +- php/src/db/sqlite/_query_create.php | 10 - php/src/db/sqlite/_query_delete.php | 10 - php/src/db/sqlite/_query_generic.php | 10 - php/src/db/sqlite/_query_insert.php | 10 - php/src/db/sqlite/_query_select.php | 10 - php/src/db/sqlite/_query_update.php | 10 - .../db/sqlite/{_query_base.php => query.php} | 34 ++- php/tests/db/sqlite/_queryTest.php | 46 ++-- 42 files changed, 705 insertions(+), 943 deletions(-) delete mode 100644 php/src/db/_private/Tcreate.php delete mode 100644 php/src/db/_private/Tdelete.php delete mode 100644 php/src/db/_private/Tgeneric.php delete mode 100644 php/src/db/_private/Tinsert.php delete mode 100644 php/src/db/_private/Tselect.php delete mode 100644 php/src/db/_private/Tupdate.php create mode 100644 php/src/db/_private/_common.php delete mode 100644 php/src/db/mysql/_query_base.php delete mode 100644 php/src/db/mysql/_query_create.php delete mode 100644 php/src/db/mysql/_query_delete.php delete mode 100644 php/src/db/mysql/_query_generic.php delete mode 100644 php/src/db/mysql/_query_insert.php delete mode 100644 php/src/db/mysql/_query_select.php delete mode 100644 php/src/db/mysql/_query_update.php delete mode 100644 php/src/db/pdo/_query_create.php delete mode 100644 php/src/db/pdo/_query_delete.php delete mode 100644 php/src/db/pdo/_query_generic.php delete mode 100644 php/src/db/pdo/_query_insert.php delete mode 100644 php/src/db/pdo/_query_select.php delete mode 100644 php/src/db/pdo/_query_update.php rename php/src/db/pdo/{_query_base.php => query.php} (62%) delete mode 100644 php/src/db/sqlite/_query_create.php delete mode 100644 php/src/db/sqlite/_query_delete.php delete mode 100644 php/src/db/sqlite/_query_generic.php delete mode 100644 php/src/db/sqlite/_query_insert.php delete mode 100644 php/src/db/sqlite/_query_select.php delete mode 100644 php/src/db/sqlite/_query_update.php rename php/src/db/sqlite/{_query_base.php => query.php} (60%) diff --git a/composer.json b/composer.json index 7171bf4..346a248 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "ext-posix": "*", "ext-pcntl": "*", "ext-curl": "*", + "ext-pdo": "*", "ext-sqlite3": "*" }, "autoload": { diff --git a/php/src/db/_private/Tcreate.php b/php/src/db/_private/Tcreate.php deleted file mode 100644 index a85d118..0000000 --- a/php/src/db/_private/Tcreate.php +++ /dev/null @@ -1,39 +0,0 @@ - &$definition) { - if ($col === $index) { - $index++; - } else { - $definition = "$col $definition"; - } - }; unset($definition); - $sql[] = "(\n ".implode("\n, ", $cols)."\n)"; - - ## suffixe - if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; - - ## fin de la requête - return implode(" ", $sql); - } -} diff --git a/php/src/db/_private/Tdelete.php b/php/src/db/_private/Tdelete.php deleted file mode 100644 index 0eeb91a..0000000 --- a/php/src/db/_private/Tdelete.php +++ /dev/null @@ -1,38 +0,0 @@ - $col) { - if ($key === $index) { - $index++; - $cols[] = $col; - $usercols[] = self::add_prefix($col, $colPrefix); - } else { - $cols[] = $key; - $usercols[] = self::add_prefix($col, $colPrefix)." as $key"; - } - } - } else { - $cols = null; - if ($schema && is_array($schema) && !in_array("*", $usercols)) { - $cols = array_keys($schema); - foreach ($cols as $col) { - $usercols[] = self::add_prefix($col, $colPrefix); - } - } - } - if (!$usercols && !$cols) $usercols = [self::add_prefix("*", $colPrefix)]; - $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, $bindings); - 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, $bindings); - 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/_private/Tupdate.php b/php/src/db/_private/Tupdate.php deleted file mode 100644 index 4e1de5b..0000000 --- a/php/src/db/_private/Tupdate.php +++ /dev/null @@ -1,40 +0,0 @@ - $value) { - if ($key === $index) { - $index++; - if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) { - $sql .= " "; - } - $sql .= $value; - } - } - return $sql; + static function with($sql, ?array $params=null): array { + static::verifix($sql, $params); + return [$sql, $params]; } - 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 &$bindings): 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_bool($cond)) { - # ignorer les valeurs true et false - } elseif (is_array($cond)) { - # condition récursive - self::parse_conds($cond, $condsql, $bindings); - } else { - # condition litérale - $condsql[] = strval($cond); - } - $index++; - } elseif ($cond === false) { - ## associatif - # condition litérale ignorée car condition false - } elseif ($cond === true) { - # condition litérale sélectionnée car condition true - $condsql[] = strval($key); - } else { - ## associatif - # paramètre - $param0 = preg_replace('/^.+\./', "", $key); - $i = false; - if ($bindings !== null && array_key_exists($param0, $bindings)) { - $i = 2; - while (array_key_exists("$param0$i", $bindings)) { - $i++; - } - } - # value ou [operator, value] - $condprefix = $condsep = $condsuffix = null; - if (is_array($cond)) { - $condkey = 0; - $condkeys = array_keys($cond); - $op = null; - if (array_key_exists("op", $cond)) { - $op = $cond["op"]; - } elseif (array_key_exists($condkey, $condkeys)) { - $op = $cond[$condkeys[$condkey]]; - $condkey++; - } - $op = strtolower($op); - $condvalues = null; - switch ($op) { - case "between": - # ["between", $upper, $lower] - $condsep = " and "; - if (array_key_exists("lower", $cond)) { - $condvalues[] = $cond["lower"]; - } elseif (array_key_exists($condkey, $condkeys)) { - $condvalues[] = $cond[$condkeys[$condkey]]; - $condkey++; - } - if (array_key_exists("upper", $cond)) { - $condvalues[] = $cond["upper"]; - } elseif (array_key_exists($condkey, $condkeys)) { - $condvalues[] = $cond[$condkeys[$condkey]]; - $condkey++; - } - break; - case "any": - case "all": - case "not any": - case "not all": - # ["list", $values] - if ($op === "any" || $op === "all") { - $condprefix = $op; - $op = "="; - } elseif ($op === "not any" || $op === "not all") { - $condprefix = substr($op, strlen("not ")); - $op = "<>"; - } - $condprefix .= "(array["; - $condsep = ", "; - $condsuffix = "])"; - $condvalues = null; - if (array_key_exists("values", $cond)) { - $condvalues = cl::with($cond["values"]); - } elseif (array_key_exists($condkey, $condkeys)) { - $condvalues = cl::with($cond[$condkeys[$condkey]]); - $condkey++; - } - break; - case "in": - # ["in", $values] - $condprefix = "("; - $condsep = ", "; - $condsuffix = ")"; - $condvalues = null; - if (array_key_exists("values", $cond)) { - $condvalues = cl::with($cond["values"]); - } elseif (array_key_exists($condkey, $condkeys)) { - $condvalues = cl::with($cond[$condkeys[$condkey]]); - $condkey++; - } - break; - case "null": - case "is null": - $op = "is null"; - break; - case "not null": - case "is not null": - $op = "is not null"; - break; - default: - if (array_key_exists("value", $cond)) { - $condvalues = [$cond["value"]]; - } elseif (array_key_exists($condkey, $condkeys)) { - $condvalues = [$cond[$condkeys[$condkey]]]; - $condkey++; - } - } - } elseif ($cond !== null) { - $op = "="; - $condvalues = [$cond]; - } else { - $op = "is null"; - $condvalues = null; - } - $cond = [$key, $op]; - if ($condvalues !== null) { - $parts = []; - foreach ($condvalues as $condvalue) { - if (is_array($condvalue)) { - $first = true; - foreach ($condvalue as $value) { - if ($first) { - $first = false; - } else { - if ($sep === null) $sep = "and"; - $parts[] = " $sep "; - $parts[] = $key; - $parts[] = " $op "; - } - $param = "$param0$i"; - $parts[] = ":$param"; - $bindings[$param] = $value; - if ($i === false) $i = 2; - else $i++; - } - } else { - $param = "$param0$i"; - $parts[] = ":$param"; - $bindings[$param] = $condvalue; - if ($i === false) $i = 2; - else $i++; - } - } - $cond[] = $condprefix.implode($condsep, $parts).$condsuffix; - } - $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 &$bindings): 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, $bindings); - } else { - # paramètre litéral - $parts[] = strval($part); - } - $index++; - } else { - ## associatif - # paramètre - $param = $param0 = preg_replace('/^.+\./', "", $key); - if ($bindings !== null && array_key_exists($param0, $bindings)) { - $i = 2; - while (array_key_exists("$param0$i", $bindings)) { - $i++; - } - $param = "$param0$i"; - } - # value - $value = $part; - $part = [$key, "="]; - if ($value === null) { - $part[] = "null"; - } else { - $part[] = ":$param"; - $bindings[$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"); - } - } - - abstract protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void; - function __construct($sql, ?array $bindings=null) { static::verifix($sql, $bindings, $meta); $this->sql = $sql; diff --git a/php/src/db/_private/_common.php b/php/src/db/_private/_common.php new file mode 100644 index 0000000..575a53b --- /dev/null +++ b/php/src/db/_private/_common.php @@ -0,0 +1,255 @@ + $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 &$bindings): 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_bool($cond)) { + # ignorer les valeurs true et false + } elseif (is_array($cond)) { + # condition récursive + self::parse_conds($cond, $condsql, $bindings); + } else { + # condition litérale + $condsql[] = strval($cond); + } + $index++; + } elseif ($cond === false) { + ## associatif + # condition litérale ignorée car condition false + } elseif ($cond === true) { + # condition litérale sélectionnée car condition true + $condsql[] = strval($key); + } else { + ## associatif + # paramètre + $param0 = preg_replace('/^.+\./', "", $key); + $i = false; + if ($bindings !== null && array_key_exists($param0, $bindings)) { + $i = 2; + while (array_key_exists("$param0$i", $bindings)) { + $i++; + } + } + # value ou [operator, value] + $condprefix = $condsep = $condsuffix = null; + if (is_array($cond)) { + $condkey = 0; + $condkeys = array_keys($cond); + $op = null; + if (array_key_exists("op", $cond)) { + $op = $cond["op"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $op = $cond[$condkeys[$condkey]]; + $condkey++; + } + $op = strtolower($op); + $condvalues = null; + switch ($op) { + case "between": + # ["between", $upper, $lower] + $condsep = " and "; + if (array_key_exists("lower", $cond)) { + $condvalues[] = $cond["lower"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues[] = $cond[$condkeys[$condkey]]; + $condkey++; + } + if (array_key_exists("upper", $cond)) { + $condvalues[] = $cond["upper"]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues[] = $cond[$condkeys[$condkey]]; + $condkey++; + } + break; + case "any": + case "all": + case "not any": + case "not all": + # ["list", $values] + if ($op === "any" || $op === "all") { + $condprefix = $op; + $op = "="; + } elseif ($op === "not any" || $op === "not all") { + $condprefix = substr($op, strlen("not ")); + $op = "<>"; + } + $condprefix .= "(array["; + $condsep = ", "; + $condsuffix = "])"; + $condvalues = null; + if (array_key_exists("values", $cond)) { + $condvalues = cl::with($cond["values"]); + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues = cl::with($cond[$condkeys[$condkey]]); + $condkey++; + } + break; + case "in": + # ["in", $values] + $condprefix = "("; + $condsep = ", "; + $condsuffix = ")"; + $condvalues = null; + if (array_key_exists("values", $cond)) { + $condvalues = cl::with($cond["values"]); + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues = cl::with($cond[$condkeys[$condkey]]); + $condkey++; + } + break; + case "null": + case "is null": + $op = "is null"; + break; + case "not null": + case "is not null": + $op = "is not null"; + break; + default: + if (array_key_exists("value", $cond)) { + $condvalues = [$cond["value"]]; + } elseif (array_key_exists($condkey, $condkeys)) { + $condvalues = [$cond[$condkeys[$condkey]]]; + $condkey++; + } + } + } elseif ($cond !== null) { + $op = "="; + $condvalues = [$cond]; + } else { + $op = "is null"; + $condvalues = null; + } + $cond = [$key, $op]; + if ($condvalues !== null) { + $parts = []; + foreach ($condvalues as $condvalue) { + if (is_array($condvalue)) { + $first = true; + foreach ($condvalue as $value) { + if ($first) { + $first = false; + } else { + if ($sep === null) $sep = "and"; + $parts[] = " $sep "; + $parts[] = $key; + $parts[] = " $op "; + } + $param = "$param0$i"; + $parts[] = ":$param"; + $bindings[$param] = $value; + if ($i === false) $i = 2; + else $i++; + } + } else { + $param = "$param0$i"; + $parts[] = ":$param"; + $bindings[$param] = $condvalue; + if ($i === false) $i = 2; + else $i++; + } + } + $cond[] = $condprefix.implode($condsep, $parts).$condsuffix; + } + $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 &$bindings): 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, $bindings); + } else { + # paramètre litéral + $parts[] = strval($part); + } + $index++; + } else { + ## associatif + # paramètre + $param = $param0 = preg_replace('/^.+\./', "", $key); + if ($bindings !== null && array_key_exists($param0, $bindings)) { + $i = 2; + while (array_key_exists("$param0$i", $bindings)) { + $i++; + } + $param = "$param0$i"; + } + # value + $value = $part; + $part = [$key, "="]; + if ($value === null) { + $part[] = "null"; + } else { + $part[] = ":$param"; + $bindings[$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"); + } + } +} diff --git a/php/src/db/_private/_create.php b/php/src/db/_private/_create.php index 64c29a5..030c594 100644 --- a/php/src/db/_private/_create.php +++ b/php/src/db/_private/_create.php @@ -1,7 +1,7 @@ "?string", "table" => "string", @@ -9,4 +9,39 @@ class _create { "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 &$bindings=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[] = "(\n ".implode("\n, ", $cols)."\n)"; + + ## suffixe + if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + + ## fin de la requête + return implode(" ", $sql); + } } diff --git a/php/src/db/_private/_delete.php b/php/src/db/_private/_delete.php index e79ec34..fd78dbb 100644 --- a/php/src/db/_private/_delete.php +++ b/php/src/db/_private/_delete.php @@ -1,11 +1,44 @@ "?string", "from" => "?string", "where" => "?array", "suffix" => "?string", ]; + + static function isa(string $sql): bool { + return preg_match("/^delete(?:\s+from)?\b/i", $sql); + } + + static function parse(array $query, ?array &$bindings=null): string { + #XXX implémentation minimale + $tmpsql = self::merge_seq($query); + self::consume('delete(?:\s+from)?\b', $tmpsql); + $sql = ["delete from", $tmpsql]; + + ## préfixe + if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + + ## table + $sql[] = $query["from"]; + + ## where + $where = $query["where"] ?? null; + if ($where !== null) { + self::parse_conds($where, $wheresql, $bindings); + 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/_private/_generic.php b/php/src/db/_private/_generic.php index 97d4b51..1fef213 100644 --- a/php/src/db/_private/_generic.php +++ b/php/src/db/_private/_generic.php @@ -1,7 +1,21 @@ "?string", "into" => "?string", @@ -10,4 +13,79 @@ class _insert { "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 &$bindings=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+or\s+(?:ignore|replace))?)\s*', $tmpsql, $ms); + $sql[] = $ms[1]; + + ## 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"; + $bindings[$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/_private/_select.php b/php/src/db/_private/_select.php index ee2bdbc..ea343f1 100644 --- a/php/src/db/_private/_select.php +++ b/php/src/db/_private/_select.php @@ -1,7 +1,11 @@ "?string", "schema" => "?array", @@ -14,4 +18,164 @@ class _select { "having" => "?array", "suffix" => "?string", ]; + + static function isa(string $sql): bool { + return preg_match("/^select\b/i", $sql); + } + + private static function add_prefix(string $col, ?string $prefix): string { + if ($prefix === null) return $col; + if (strpos($col, ".") !== false) return $col; + return "$prefix$col"; + } + + /** + * 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 &$bindings=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*distinct)?)\s*', $tmpsql, $ms); + $sql[] = $ms[1]; + + ## cols + $usercols = []; + if (self::consume('(.*?)\s*(?=$|\bfrom\b)', $tmpsql, $ms)) { + if ($ms[1]) $usercols[] = $ms[1]; + } + $colPrefix = $query["col_prefix"] ?? null; + if ($colPrefix !== null) str::add_suffix($colPrefix, "."); + $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[] = self::add_prefix($col, $colPrefix); + } else { + $cols[] = $key; + $usercols[] = self::add_prefix($col, $colPrefix)." as $key"; + } + } + } else { + $cols = null; + if ($schema && is_array($schema) && !in_array("*", $usercols)) { + $cols = array_keys($schema); + foreach ($cols as $col) { + $usercols[] = self::add_prefix($col, $colPrefix); + } + } + } + if (!$usercols && !$cols) $usercols = [self::add_prefix("*", $colPrefix)]; + $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, $bindings); + 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, $bindings); + 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/_private/_update.php b/php/src/db/_private/_update.php index b5b2dc6..97cf75d 100644 --- a/php/src/db/_private/_update.php +++ b/php/src/db/_private/_update.php @@ -1,7 +1,7 @@ "?string", "table" => "?string", @@ -11,4 +11,40 @@ class _update { "where" => "?array", "suffix" => "?string", ]; + + static function isa(string $sql): bool { + return preg_match("/^update\b/i", $sql); + } + + static function parse(array $query, ?array &$bindings=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 + self::parse_set_values($query["values"], $setsql, $bindings); + $sql[] = "set"; + $sql[] = implode(", ", $setsql); + + ## where + $where = $query["where"] ?? null; + if ($where !== null) { + self::parse_conds($where, $wheresql, $bindings); + 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/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php index 50b09d2..4d8cab5 100644 --- a/php/src/db/mysql/MysqlStorage.php +++ b/php/src/db/mysql/MysqlStorage.php @@ -24,7 +24,7 @@ class MysqlStorage extends CapacitorStorage { ]; function _getCreateSql(CapacitorChannel $channel): string { - $query = new _query_base($this->_createSql($channel)); + $query = new query($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } diff --git a/php/src/db/mysql/_query_base.php b/php/src/db/mysql/_query_base.php deleted file mode 100644 index 614ec06..0000000 --- a/php/src/db/mysql/_query_base.php +++ /dev/null @@ -1,52 +0,0 @@ - "create", "type" => "ddl"]; - } elseif (_query_select::isa($prefix)) { - $sql = _query_select::parse($sql, $bindinds); - $meta = ["isa" => "select", "type" => "dql"]; - } elseif (_query_insert::isa($prefix)) { - $sql = _query_insert::parse($sql, $bindinds); - $meta = ["isa" => "insert", "type" => "dml"]; - } elseif (_query_update::isa($prefix)) { - $sql = _query_update::parse($sql, $bindinds); - $meta = ["isa" => "update", "type" => "dml"]; - } elseif (_query_delete::isa($prefix)) { - $sql = _query_delete::parse($sql, $bindinds); - $meta = ["isa" => "delete", "type" => "dml"]; - } elseif (_query_generic::isa($prefix)) { - $sql = _query_generic::parse($sql, $bindinds); - $meta = ["isa" => "generic", "type" => null]; - } else { - throw ValueException::invalid_kind($sql, "query"); - } - } else { - if (!is_string($sql)) $sql = strval($sql); - if (_query_create::isa($sql)) { - $meta = ["isa" => "create", "type" => "ddl"]; - } elseif (_query_select::isa($sql)) { - $meta = ["isa" => "select", "type" => "dql"]; - } elseif (_query_insert::isa($sql)) { - $meta = ["isa" => "insert", "type" => "dml"]; - } elseif (_query_update::isa($sql)) { - $meta = ["isa" => "update", "type" => "dml"]; - } elseif (_query_delete::isa($sql)) { - $meta = ["isa" => "delete", "type" => "dml"]; - } elseif (_query_generic::isa($sql)) { - $meta = ["isa" => "generic", "type" => null]; - } else { - $meta = ["isa" => "generic", "type" => null]; - } - } - } -} diff --git a/php/src/db/mysql/_query_create.php b/php/src/db/mysql/_query_create.php deleted file mode 100644 index 11f6602..0000000 --- a/php/src/db/mysql/_query_create.php +++ /dev/null @@ -1,10 +0,0 @@ -db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); if ($query->useStmt($db, $stmt, $sql)) { if ($stmt->execute() === false) return false; if ($query->isInsert()) return $db->lastInsertId(); @@ -222,7 +222,7 @@ class Pdo implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); $stmt = null; try { /** @var \PDOStatement $stmt */ @@ -251,7 +251,7 @@ class Pdo implements IDatabase { */ function all($query, ?array $params=null, $primaryKeys=null): Generator { $db = $this->db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); $stmt = null; try { /** @var \PDOStatement $stmt */ diff --git a/php/src/db/pdo/_query_create.php b/php/src/db/pdo/_query_create.php deleted file mode 100644 index 997349a..0000000 --- a/php/src/db/pdo/_query_create.php +++ /dev/null @@ -1,10 +0,0 @@ - "create", "type" => "ddl"]; - } elseif (_query_select::isa($prefix)) { - $sql = _query_select::parse($sql, $bindinds); + } elseif (_select::isa($prefix)) { + $sql = _select::parse($sql, $bindings); $meta = ["isa" => "select", "type" => "dql"]; - } elseif (_query_insert::isa($prefix)) { - $sql = _query_insert::parse($sql, $bindinds); + } elseif (_insert::isa($prefix)) { + $sql = _insert::parse($sql, $bindings); $meta = ["isa" => "insert", "type" => "dml"]; - } elseif (_query_update::isa($prefix)) { - $sql = _query_update::parse($sql, $bindinds); + } elseif (_update::isa($prefix)) { + $sql = _update::parse($sql, $bindings); $meta = ["isa" => "update", "type" => "dml"]; - } elseif (_query_delete::isa($prefix)) { - $sql = _query_delete::parse($sql, $bindinds); + } elseif (_delete::isa($prefix)) { + $sql = _delete::parse($sql, $bindings); $meta = ["isa" => "delete", "type" => "dml"]; - } elseif (_query_generic::isa($prefix)) { - $sql = _query_generic::parse($sql, $bindinds); + } elseif (_generic::isa($prefix)) { + $sql = _generic::parse($sql, $bindings); $meta = ["isa" => "generic", "type" => null]; } else { throw ValueException::invalid_kind($sql, "query"); } } else { if (!is_string($sql)) $sql = strval($sql); - if (_query_create::isa($sql)) { + if (_create::isa($sql)) { $meta = ["isa" => "create", "type" => "ddl"]; - } elseif (_query_select::isa($sql)) { + } elseif (_select::isa($sql)) { $meta = ["isa" => "select", "type" => "dql"]; - } elseif (_query_insert::isa($sql)) { + } elseif (_insert::isa($sql)) { $meta = ["isa" => "insert", "type" => "dml"]; - } elseif (_query_update::isa($sql)) { + } elseif (_update::isa($sql)) { $meta = ["isa" => "update", "type" => "dml"]; - } elseif (_query_delete::isa($sql)) { + } elseif (_delete::isa($sql)) { $meta = ["isa" => "delete", "type" => "dml"]; - } elseif (_query_generic::isa($sql)) { + } elseif (_generic::isa($sql)) { $meta = ["isa" => "generic", "type" => null]; } else { $meta = ["isa" => "generic", "type" => null]; diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index a1ebf5c..cf14dcd 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -187,7 +187,7 @@ class Sqlite implements IDatabase { function exec($query, ?array $params=null) { $db = $this->db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); if ($query->useStmt($db, $stmt, $sql)) { try { $result = $stmt->execute(); @@ -274,7 +274,7 @@ class Sqlite implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); if ($query->useStmt($db, $stmt, $sql)) { try { $result = $this->checkResult($stmt->execute()); @@ -323,7 +323,7 @@ class Sqlite implements IDatabase { */ function all($query, ?array $params=null, $primaryKeys=null): iterable { $db = $this->db(); - $query = new _query_base($query, $params); + $query = new query($query, $params); if ($query->useStmt($db, $stmt, $sql)) { $result = $this->checkResult($stmt->execute()); return $this->_fetchResult($result, $stmt, $primaryKeys); diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index 287a9f7..b29639b 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -24,7 +24,7 @@ class SqliteStorage extends CapacitorStorage { ]; function _getCreateSql(CapacitorChannel $channel): string { - $query = new _query_base($this->_createSql($channel)); + $query = new query($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } diff --git a/php/src/db/sqlite/_query_create.php b/php/src/db/sqlite/_query_create.php deleted file mode 100644 index 5aa7aa1..0000000 --- a/php/src/db/sqlite/_query_create.php +++ /dev/null @@ -1,10 +0,0 @@ - null], $sql, $params); + query::parse_conds(["col" => null], $sql, $params); self::assertSame(["col is null"], $sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_conds(["col = 'value'"], $sql, $params); + query::parse_conds(["col = 'value'"], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_conds([["col = 'value'"]], $sql, $params); + query::parse_conds([["col = 'value'"]], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_conds(["int" => 42, "string" => "value"], $sql, $params); + query::parse_conds(["int" => 42, "string" => "value"], $sql, $params); self::assertSame(["(int = :int and string = :string)"], $sql); self::assertSame(["int" => 42, "string" => "value"], $params); $sql = $params = null; - _query_base::parse_conds(["or", "int" => 42, "string" => "value"], $sql, $params); + 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_base::parse_conds([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + query::parse_conds([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); self::assertSame(["((int = :int and string = :string) and (int = :int2 and string = :string2))"], $sql); self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params); $sql = $params = null; - _query_base::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params); + query::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params); self::assertSame(["(int is null and string <> :string)"], $sql); self::assertSame(["string" => "value"], $params); $sql = $params = null; - _query_base::parse_conds(["col" => ["between", "lower", "upper"]], $sql, $params); + query::parse_conds(["col" => ["between", "lower", "upper"]], $sql, $params); self::assertSame(["col between :col and :col2"], $sql); self::assertSame(["col" => "lower", "col2" => "upper"], $params); $sql = $params = null; - _query_base::parse_conds(["col" => ["in", "one"]], $sql, $params); + query::parse_conds(["col" => ["in", "one"]], $sql, $params); self::assertSame(["col in (:col)"], $sql); self::assertSame(["col" => "one"], $params); $sql = $params = null; - _query_base::parse_conds(["col" => ["in", ["one", "two"]]], $sql, $params); + query::parse_conds(["col" => ["in", ["one", "two"]]], $sql, $params); self::assertSame(["col in (:col, :col2)"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - _query_base::parse_conds(["col" => ["=", ["one", "two"]]], $sql, $params); + query::parse_conds(["col" => ["=", ["one", "two"]]], $sql, $params); self::assertSame(["col = :col and col = :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - _query_base::parse_conds(["or", "col" => ["=", ["one", "two"]]], $sql, $params); + query::parse_conds(["or", "col" => ["=", ["one", "two"]]], $sql, $params); self::assertSame(["col = :col or col = :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - _query_base::parse_conds(["col" => ["<>", ["one", "two"]]], $sql, $params); + query::parse_conds(["col" => ["<>", ["one", "two"]]], $sql, $params); self::assertSame(["col <> :col and col <> :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - _query_base::parse_conds(["or", "col" => ["<>", ["one", "two"]]], $sql, $params); + query::parse_conds(["or", "col" => ["<>", ["one", "two"]]], $sql, $params); self::assertSame(["col <> :col or col <> :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); } function testParseValues(): void { $sql = $params = null; - _query_base::parse_set_values(null, $sql, $params); + query::parse_set_values(null, $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_set_values([], $sql, $params); + query::parse_set_values([], $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_set_values(["col = 'value'"], $sql, $params); + query::parse_set_values(["col = 'value'"], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_set_values([["col = 'value'"]], $sql, $params); + query::parse_set_values([["col = 'value'"]], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); + 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_base::parse_set_values(["int" => 42, "string" => "value"], $sql, $params); + 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_base::parse_set_values([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); + query::parse_set_values([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); self::assertSame(["int = :int", "string = :string", "int = :int2", "string = :string2"], $sql); self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params); } From ecd01777c1a84be889959f787924617ad05334d1 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Thu, 10 Apr 2025 09:42:09 +0400 Subject: [PATCH 06/32] =?UTF-8?q?migration=20de=20nur=5Ffunc=20=C3=A0=20fu?= =?UTF-8?q?nc?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- php/src/cl.php | 7 +- php/src/db/Capacitor.php | 4 +- php/src/db/CapacitorStorage.php | 33 +- php/src/db/pdo/Pdo.php | 7 +- php/src/db/pdo/_config.php | 7 +- php/src/db/sqlite/Sqlite.php | 4 +- php/src/db/sqlite/_config.php | 7 +- php/src/db/sqlite/_migration.php | 6 +- php/src/file/tab/AbstractBuilder.php | 19 +- php/src/output/msg.php | 17 +- php/src/php/content/c.php | 16 +- php/src/php/func.php | 30 +- php/src/php/mprop.php | 4 +- php/src/php/nur_func.php | 453 --------------------------- php/tests/appTest.php | 132 -------- php/tests/php/funcTest.php | 21 ++ php/tests/php/nur_funcTest.php | 292 ----------------- 17 files changed, 97 insertions(+), 962 deletions(-) delete mode 100644 php/src/php/nur_func.php delete mode 100644 php/tests/appTest.php delete mode 100644 php/tests/php/nur_funcTest.php diff --git a/php/src/cl.php b/php/src/cl.php index 2397fe9..c0e64e8 100644 --- a/php/src/cl.php +++ b/php/src/cl.php @@ -2,6 +2,7 @@ namespace nulib; use ArrayAccess; +use nulib\php\func; use nulib\php\nur_func; use Traversable; @@ -348,12 +349,12 @@ class cl { ############################################################################# - static final function map(callable $callback, ?iterable $array): array { + static final function map($func, ?iterable $array): array { $result = []; if ($array !== null) { - $ctx = nur_func::_prepare($callback); + $func = func::with($func); foreach ($array as $key => $value) { - $result[$key] = nur_func::_call($ctx, [$value, $key]); + $result[$key] = $func->invoke([$value, $key]); } } return $result; diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php index 90c3c9b..70c6f46 100644 --- a/php/src/db/Capacitor.php +++ b/php/src/db/Capacitor.php @@ -1,7 +1,7 @@ commit(); $commited = true; diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index ec27fae..c5cf345 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -3,7 +3,7 @@ namespace nulib\db; use nulib\cl; use nulib\db\cache\cache; -use nulib\php\nur_func; +use nulib\php\func; use nulib\ValueException; use Traversable; @@ -230,10 +230,7 @@ EOT; $db = $this->db(); $args ??= []; - $initFunc = [$channel, "getItemValues"]; - $initArgs = $args; - nur_func::ensure_func($initFunc, null, $initArgs); - $values = nur_func::call($initFunc, $item, ...$initArgs); + $values = func::call([$channel, "getItemValues"], $item, ...$args); if ($values === [false]) return 0; $row = cl::merge( @@ -259,9 +256,7 @@ EOT; "modified_" => $now, ]); $insert = true; - $initFunc = [$channel, "onCreate"]; - $initArgs = $args; - nur_func::ensure_func($initFunc, null, $initArgs); + $initFunc = func::with([$channel, "onCreate"], $args); $values = $this->unserialize($channel, $row); $pvalues = null; } else { @@ -276,14 +271,12 @@ EOT; } else { $row = cl::merge($prow, $row); } - $initFunc = [$channel, "onUpdate"]; - $initArgs = $args; - nur_func::ensure_func($initFunc, null, $initArgs); + $initFunc = func::with([$channel, "onUpdate"], $args); $values = $this->unserialize($channel, $row); $pvalues = $this->unserialize($channel, $prow); } - $updates = nur_func::call($initFunc, $item, $values, $pvalues, ...$initArgs); + $updates = $initFunc->prependArgs(null, [$item, $values, $pvalues])->invoke(); if ($updates === [false]) return 0; if (is_array($updates) && $updates) { if ($insert === null) $insert = false; @@ -295,8 +288,10 @@ EOT; } if ($func !== null) { - nur_func::ensure_func($func, $channel, $args); - $updates = nur_func::call($func, $item, $values, $pvalues, ...$args); + $updates = func::with($func) + ->prependArgs(null, [$item, $values, $pvalues]) + ->bind($channel, true) + ->invoke(); if ($updates === [false]) return 0; if (is_array($updates) && $updates) { if ($insert === null) $insert = false; @@ -510,8 +505,7 @@ EOT; function _each(CapacitorChannel $channel, $filter, $func, ?array $args, ?array $mergeQuery=null, ?int &$nbUpdated=null): int { $this->_create($channel); if ($func === null) $func = CapacitorChannel::onEach; - nur_func::ensure_func($func, $channel, $args); - $onEach = nur_func::_prepare($func); + $onEach = func::with($func)->bind($channel, true); $db = $this->db(); # si on est déjà dans une transaction, désactiver la gestion des transactions $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction(); @@ -528,7 +522,7 @@ EOT; $all = $this->_allCached("each", $channel, $filter, $mergeQuery); foreach ($all as $values) { $rowIds = $this->getRowIds($channel, $values); - $updates = nur_func::_call($onEach, [$values["item"], $values, ...$args]); + $updates = $onEach->invoke([$values["item"], $values, ...$args]); if (is_array($updates) && $updates) { if (!array_key_exists("modified_", $updates)) { $updates["modified_"] = date("Y-m-d H:i:s"); @@ -579,8 +573,7 @@ EOT; function _delete(CapacitorChannel $channel, $filter, $func, ?array $args): int { $this->_create($channel); if ($func === null) $func = CapacitorChannel::onDelete; - nur_func::ensure_func($func, $channel, $args); - $onEach = nur_func::_prepare($func); + $onEach = func::with($func)->bind($channel, true); $db = $this->db(); # si on est déjà dans une transaction, désactiver la gestion des transactions $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction(); @@ -596,7 +589,7 @@ EOT; $all = $this->_allCached("delete", $channel, $filter); foreach ($all as $values) { $rowIds = $this->getRowIds($channel, $values); - $delete = boolval(nur_func::_call($onEach, [$values["item"], $values, ...$args])); + $delete = boolval($onEach->invoke([$values["item"], $values, ...$args])); if ($delete) { $db->exec([ "delete", diff --git a/php/src/db/pdo/Pdo.php b/php/src/db/pdo/Pdo.php index b2b24c9..2fa8bf0 100644 --- a/php/src/db/pdo/Pdo.php +++ b/php/src/db/pdo/Pdo.php @@ -6,7 +6,7 @@ use nulib\cl; use nulib\db\_private\Tvalues; use nulib\db\IDatabase; use nulib\db\ITransactor; -use nulib\php\nur_func; +use nulib\php\func; use nulib\ValueException; class Pdo implements IDatabase { @@ -119,8 +119,7 @@ class Pdo implements IDatabase { $dbconn = $this->dbconn; $options = $this->options; if (is_callable($options)) { - nur_func::ensure_func($options, $this, $args); - $options = nur_func::call($options, ...$args); + $options = func::with($options)->bind($this, true)->invoke(); } $this->db = new \PDO($dbconn["name"], $dbconn["user"], $dbconn["pass"], $options); _config::with($this->config)->configure($this); @@ -191,7 +190,7 @@ class Pdo implements IDatabase { if ($func !== null) { $commited = false; try { - nur_func::call($func, $this); + func::call($func, $this); if ($commit) { $this->commit(); $commited = true; diff --git a/php/src/db/pdo/_config.php b/php/src/db/pdo/_config.php index 5055d6f..2cc9c96 100644 --- a/php/src/db/pdo/_config.php +++ b/php/src/db/pdo/_config.php @@ -1,7 +1,7 @@ configs as $key => $config) { - if (is_string($config) && !nur_func::is_method($config)) { + if (is_string($config) && !func::is_method($config)) { $pdo->exec($config); } else { - nur_func::ensure_func($config, $this, $args); - nur_func::call($config, $pdo, $key, ...$args); + func::with($config)->bind($this, true)->invoke([$pdo, $key]); } } } diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index cf14dcd..89db30b 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -6,7 +6,7 @@ use nulib\cl; use nulib\db\_private\Tvalues; use nulib\db\IDatabase; use nulib\db\ITransactor; -use nulib\php\nur_func; +use nulib\php\func; use nulib\ValueException; use SQLite3; use SQLite3Result; @@ -237,7 +237,7 @@ class Sqlite implements IDatabase { if ($func !== null) { $commited = false; try { - nur_func::call($func, $this); + func::call($func, $this); if ($commit) { $this->commit(); $commited = true; diff --git a/php/src/db/sqlite/_config.php b/php/src/db/sqlite/_config.php index ea7553a..bcfe8fb 100644 --- a/php/src/db/sqlite/_config.php +++ b/php/src/db/sqlite/_config.php @@ -1,7 +1,7 @@ configs as $key => $config) { - if (is_string($config) && !nur_func::is_method($config)) { + if (is_string($config) && !func::is_method($config)) { $sqlite->exec($config); } else { - nur_func::ensure_func($config, $this, $args); - nur_func::call($config, $sqlite, $key, ...$args); + func::with($config)->bind($this, true)->invoke([$sqlite, $key]); } } } diff --git a/php/src/db/sqlite/_migration.php b/php/src/db/sqlite/_migration.php index d2adf93..b5ee6f4 100644 --- a/php/src/db/sqlite/_migration.php +++ b/php/src/db/sqlite/_migration.php @@ -1,6 +1,7 @@ $migration, "done" => 0, ]); - if (is_string($migration) && !nur_func::is_method($migration)) { + if (is_string($migration) && !func::is_method($migration)) { $sqlite->exec($migration); } else { - nur_func::ensure_func($migration, $this, $args); - nur_func::call($migration, $sqlite, $key, ...$args); + func::with($migration)->bind($this, true)->invoke([$sqlite, $key]); } $sqlite->exec("update _migration set done = 1 where key = :key", [ "key" => $key, diff --git a/php/src/file/tab/AbstractBuilder.php b/php/src/file/tab/AbstractBuilder.php index f1ec869..77d61b7 100644 --- a/php/src/file/tab/AbstractBuilder.php +++ b/php/src/file/tab/AbstractBuilder.php @@ -5,6 +5,7 @@ use DateTimeInterface; use nulib\cl; use nulib\file\TempStream; use nulib\os\path; +use nulib\php\func; use nulib\php\nur_func; use nulib\php\time\DateTime; use nulib\web\http; @@ -35,13 +36,8 @@ abstract class AbstractBuilder extends TempStream implements IBuilder { $this->rows = $rows; $this->index = 0; $cookFunc = $params["cook_func"] ?? null; - $cookCtx = $cookArgs = null; - if ($cookFunc !== null) { - nur_func::ensure_func($cookFunc, $this, $cookArgs); - $cookCtx = nur_func::_prepare($cookFunc); - } - $this->cookCtx = $cookCtx; - $this->cookArgs = $cookArgs; + if ($cookFunc !== null) $cookFunc = func::with($cookFunc)->bind($this, true); + $this->cookFunc = $cookFunc; $this->output = $params["output"] ?? static::OUTPUT; $maxMemory = $params["max_memory"] ?? null; $throwOnError = $params["throw_on_error"] ?? null; @@ -60,9 +56,7 @@ abstract class AbstractBuilder extends TempStream implements IBuilder { protected ?string $output; - protected ?array $cookCtx; - - protected ?array $cookArgs; + protected ?func $cookFunc; protected function ensureHeaders(?array $row=null): void { if ($this->headers !== null || !$this->useHeaders) return; @@ -87,9 +81,8 @@ abstract class AbstractBuilder extends TempStream implements IBuilder { } protected function cookRow(?array $row): ?array { - if ($this->cookCtx !== null) { - $args = cl::merge([$row], $this->cookArgs); - $row = nur_func::_call($this->cookCtx, $args); + if ($this->cookFunc !== null) { + $row = $this->cookFunc->prependArgs(null, [$row])->invoke(); } if ($row !== null) { foreach ($row as &$col) { diff --git a/php/src/output/msg.php b/php/src/output/msg.php index 3576127..d180d18 100644 --- a/php/src/output/msg.php +++ b/php/src/output/msg.php @@ -2,7 +2,7 @@ namespace nulib\output; use nulib\output\std\ProxyMessenger; -use nulib\php\nur_func; +use nulib\php\func; /** * Class msg: inscrire un message dans les logs ET l'afficher à l'utilisateur @@ -39,30 +39,21 @@ class msg extends _messenger { if ($log !== null && $log !== false) { if ($log instanceof IMessenger) log::set_messenger($log); elseif (is_string($log)) log::set_messenger_class($log); - elseif (is_array($log)) { - nur_func::ensure_class($log, $args); - $log = nur_func::cons($log, $args); - } + else $log = func::call($log); log::set_messenger($log); $msgs[] = $log; } if ($console !== null && $console !== false) { if ($console instanceof IMessenger) console::set_messenger($console); elseif (is_string($console)) console::set_messenger_class($console); - elseif (is_array($console)) { - nur_func::ensure_class($console, $args); - $console = nur_func::cons($console, $args); - } + else $console = func::call($console); console::set_messenger($console); $msgs[] = $console; } if ($say !== null && $say !== false) { if ($say instanceof IMessenger) say::set_messenger($say); elseif (is_string($say)) say::set_messenger_class($say); - elseif (is_array($say)) { - nur_func::ensure_class($say, $args); - $say = nur_func::cons($say, $args); - } + else $say = func::call($say); say::set_messenger($say); $msgs[] = $say; } diff --git a/php/src/php/content/c.php b/php/src/php/content/c.php index 9506835..4105846 100644 --- a/php/src/php/content/c.php +++ b/php/src/php/content/c.php @@ -3,7 +3,7 @@ namespace nulib\php\content; use Closure; use nulib\cl; -use nulib\php\nur_func; +use nulib\php\func; /** * Class c: classe outil pour gérer du contenu @@ -62,8 +62,7 @@ class c { # contenu dynamique: le contenu est la valeur de retour de la fonction # ce contenu est rajouté à la suite après avoir été quoté avec self::q() $func = $value; - nur_func::ensure_func($func, $object_or_class, $args); - $values = self::q(nur_func::call($func, ...$args)); + $values = self::q(func::call($func)); self::add_static_content($dest, $values, $key, $seq); continue; } @@ -83,16 +82,7 @@ class c { $arg = self::resolve($arg, $object_or_class, false); if (!$array) $arg = $arg[0]; }; unset($arg); - if (nur_func::is_static($func)) { - nur_func::ensure_func($func, $object_or_class, $args); - $value = nur_func::call($func, ...$args); - } elseif (nur_func::is_class($func)) { - nur_func::fix_class_args($func, $args); - $value = nur_func::cons($func, ...$args); - } else { - nur_func::ensure_func($func, $object_or_class, $args); - $value = nur_func::call($func, ...$args); - } + $value = func::with($func, $args)->bind($object_or_class, true)->invoke(); } } if ($seq) $dest[] = $value; diff --git a/php/src/php/func.php b/php/src/php/func.php index 133fd5f..d81b416 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -592,6 +592,27 @@ class func { protected int $maxArgs; + function replaceArgs(?array $args): self { + $this->prefixArgs = $args?? []; + return $this; + } + + function prependArgs(?int $stripCount=null, ?array $args=null): self { + if ($stripCount !== null || $args !== null) { + array_splice($this->prefixArgs, 0, $stripCount ?? 0, $args); + } + return $this; + } + + function appendArgs(?int $stripCount=null, ?array $args=null): self { + if ($stripCount !== null || $args !== null) { + $stripCount ??= 0; + if ($stripCount > 0) array_splice($this->prefixArgs, -$stripCount); + $this->prefixArgs = array_merge($this->prefixArgs, $args); + } + return $this; + } + protected function updateReflection($reflection): void { $variadic = false; $minArgs = $maxArgs = 0; @@ -627,11 +648,16 @@ class func { else return $this->bound && $this->object !== null; } - function bind($object): self { + function bind($object, bool $unlessAlreadyBound=false, bool $replace=false): self { if ($this->type !== self::TYPE_METHOD) return $this; + if ($this->object !== null && $unlessAlreadyBound) return $this; [$c, $f] = $this->func; - if ($this->reflection === null) { + if ($replace) { + $c = $object; + $this->func = [$c, $f]; + $this->updateReflection(new ReflectionMethod($c, $f)); + } elseif ($this->reflection === null) { $this->func[0] = $c = $object; $this->updateReflection(new ReflectionMethod($c, $f)); } diff --git a/php/src/php/mprop.php b/php/src/php/mprop.php index a0bc28d..b036844 100644 --- a/php/src/php/mprop.php +++ b/php/src/php/mprop.php @@ -44,7 +44,7 @@ class mprop { } catch (ReflectionException $e) { return oprop::get($object, $property, $default); } - return nur_func::call([$object, $m], $default); + return func::call([$object, $m], $default); } /** spécifier la valeur d'une propriété */ @@ -60,7 +60,7 @@ class mprop { } catch (ReflectionException $e) { return oprop::_set($c, $object, $property, $value); } - nur_func::call([$object, $m], $value); + func::call([$object, $m], $value); return $value; } diff --git a/php/src/php/nur_func.php b/php/src/php/nur_func.php deleted file mode 100644 index ba4cc06..0000000 --- a/php/src/php/nur_func.php +++ /dev/null @@ -1,453 +0,0 @@ - 1) { - if (!array_key_exists(1, $func)) return false; - if (!is_string($func[1]) || strlen($func[1]) == 0) return false; - if (strpos($func[1], "\\") !== false) return false; - return true; - } - } - return false; - } - - /** - * si $func est une chaine de la forme "::method" alors la remplacer par la - * chaine "$class::method" - * - * si $func est un tableau de la forme ["method"] ou [null, "method"], alors - * le remplacer par [$class, "method"] - * - * on assume que {@link is_static()}($func) retourne true - * - * @return bool true si la correction a été faite - */ - static final function fix_static(&$func, $class): bool { - if (is_object($class)) $class = get_class($class); - - if (is_string($func) && substr($func, 0, 2) == "::") { - $func = "$class$func"; - return true; - } elseif (is_array($func) && array_key_exists(0, $func)) { - $count = count($func); - if ($count == 1) { - $func = [$class, $func[0]]; - return true; - } elseif ($count > 1 && $func[0] === null) { - $func[0] = $class; - return true; - } - } - return false; - } - - /** tester si $method est une chaine de la forme "->method" */ - private static function isam($method): bool { - return is_string($method) - && strlen($method) > 2 - && substr($method, 0, 2) == "->"; - } - - /** - * tester si $func est une chaine de la forme "->method" ou un tableau de la - * forme ["->method", ...] ou [anything, "->method", ...] - */ - static final function is_method($func): bool { - if (is_string($func)) { - return self::isam($func); - } elseif (is_array($func) && array_key_exists(0, $func)) { - if (self::isam($func[0])) { - # ["->method", ...] - return true; - } - if (array_key_exists(1, $func) && self::isam($func[1])) { - # [anything, "->method", ...] - return true; - } - } - return false; - } - - /** - * si $func est une chaine de la forme "->method" alors la remplacer par le - * tableau [$object, "method"] - * - * si $func est un tableau de la forme ["->method"] ou [anything, "->method"], - * alors le remplacer par [$object, "method"] - * - * @return bool true si la correction a été faite - */ - static final function fix_method(&$func, $object): bool { - if (!is_object($object)) return false; - - if (is_string($func)) { - if (self::isam($func)) { - $func = [$object, substr($func, 2)]; - return true; - } - } elseif (is_array($func) && array_key_exists(0, $func)) { - if (self::isam($func[0])) $func = array_merge([null], $func); - if (count($func) > 1 && array_key_exists(1, $func) && self::isam($func[1])) { - $func[0] = $object; - $func[1] = substr($func[1], 2); - return true; - } - } - return false; - } - - /** - * si $func est un tableau de plus de 2 éléments, alors déplacer les éléments - * supplémentaires au début de $args. par exemple: - * ~~~ - * $func = ["class", "method", "arg1", "arg2"]; - * $args = ["arg3"]; - * func::fix_args($func, $args) - * # $func === ["class", "method"] - * # $args === ["arg1", "arg2", "arg3"] - * ~~~ - * - * @return bool true si la correction a été faite - */ - static final function fix_args(&$func, ?array &$args): bool { - if ($args === null) $args = []; - if (is_array($func) && count($func) > 2) { - $prefix_args = array_slice($func, 2); - $func = array_slice($func, 0, 2); - $args = array_merge($prefix_args, $args); - return true; - } - return false; - } - - /** - * s'assurer que $func est un appel de méthode ou d'une méthode statique; - * et renseigner le cas échéant les arguments. si $func ne fait pas mention - * de la classe ou de l'objet, le renseigner avec $class_or_object. - * - * @return bool true si c'est une fonction valide. il ne reste plus qu'à - * l'appeler avec {@link call()} - */ - static final function check_func(&$func, $class_or_object, &$args=null): bool { - if ($func instanceof Closure) return true; - if (self::is_method($func)) { - # méthode - self::fix_method($func, $class_or_object); - self::fix_args($func, $args); - return true; - } elseif (self::is_static($func)) { - # méthode statique - self::fix_static($func, $class_or_object); - self::fix_args($func, $args); - return true; - } - return false; - } - - /** - * Comme {@link check_func()} mais lance une exception si la fonction est - * invalide - * - * @throws ValueException si $func n'est pas une fonction ou une méthode valide - */ - static final function ensure_func(&$func, $class_or_object, &$args=null): void { - if (!self::check_func($func, $class_or_object, $args)) { - throw ValueException::invalid_type($func, "callable"); - } - } - - static final function _prepare($func): array { - $object = null; - if (is_callable($func)) { - if (is_array($func)) { - $rf = new ReflectionMethod(...$func); - $object = $func[0]; - if (is_string($object)) $object = null; - } elseif ($func instanceof Closure) { - $rf = new ReflectionFunction($func); - } elseif (is_string($func) && strpos($func, "::") === false) { - $rf = new ReflectionFunction($func); - } else { - $rf = new ReflectionMethod($func); - } - } elseif ($func instanceof ReflectionMethod) { - $rf = $func; - } elseif ($func instanceof ReflectionFunction) { - $rf = $func; - } elseif (is_array($func) && count($func) == 2 && isset($func[0]) && isset($func[1]) - && ($func[1] instanceof ReflectionMethod || $func[1] instanceof ReflectionFunction)) { - $object = $func[0]; - if (is_string($object)) $object = null; - $rf = $func[1]; - } elseif (is_string($func) && strpos($func, "::") === false) { - $rf = new ReflectionFunction($func); - } else { - throw ValueException::invalid_type($func, "callable"); - } - $minArgs = $rf->getNumberOfRequiredParameters(); - $maxArgs = $rf->getNumberOfParameters(); - $variadic = $rf->isVariadic(); - return [$rf instanceof ReflectionMethod, $object, $rf, $minArgs, $maxArgs, $variadic]; - } - - static final function _fill(array $context, array &$args): void { - $minArgs = $context[3]; - $maxArgs = $context[4]; - $variadic = $context[5]; - if (!$variadic) $args = array_slice($args, 0, $maxArgs); - while (count($args) < $minArgs) $args[] = null; - } - - static final function _call($context, array $args) { - self::_fill($context, $args); - $use_object = $context[0]; - $object = $context[1]; - $method = $context[2]; - if ($use_object) { - if (count($args) === 0) return $method->invoke($object); - else return $method->invokeArgs($object, $args); - } else { - if (count($args) === 0) return $method->invoke(); - else return $method->invokeArgs($args); - } - } - - /** - * Appeler la fonction spécifiée avec les arguments spécifiés. - * Adapter $args en fonction du nombre réel d'arguments de $func - * - * @param callable|ReflectionFunction|ReflectionMethod $func - */ - static final function call($func, ...$args) { - return self::_call(self::_prepare($func), $args); - } - - /** remplacer $value par $func($value, ...$args) */ - static final function apply(&$value, $func, ...$args): void { - if ($func !== null) { - if ($args) $args = array_merge([$value], $args); - else $args = [$value]; - $value = self::call($func, ...$args); - } - } - - const MASK_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC; - const MASK_P = ReflectionMethod::IS_PUBLIC; - const METHOD_PS = ReflectionMethod::IS_PUBLIC | ReflectionMethod::IS_STATIC; - const METHOD_P = ReflectionMethod::IS_PUBLIC; - - private static function matches(string $name, array $includes, array $excludes): bool { - if ($includes) { - $matches = false; - foreach ($includes as $include) { - if (substr($include, 0, 1) == "/") { - # expression régulière - if (preg_match($include, $name)) { - $matches = true; - break; - } - } else { - # tester la présence de la sous-chaine - if (strpos($name, $include) !== false) { - $matches = true; - break; - } - } - } - if (!$matches) return false; - } - foreach ($excludes as $exclude) { - if (substr($exclude, 0, 1) == "/") { - # expression régulière - if (preg_match($exclude, $name)) return false; - } else { - # tester la présence de la sous-chaine - if (strpos($name, $exclude) !== false) return false; - } - } - return true; - } - - /** @var Schema */ - private static $call_all_params_schema; - - /** - * retourner la liste des méthodes de $class_or_object qui correspondent au - * filtre $options. le filtre doit respecter le schéme {@link CALL_ALL_PARAMS_SCHEMA} - */ - static function get_all($class_or_object, $params=null): array { - Schema::nv($paramsv, $params, null - , self::$call_all_params_schema, ref_func::CALL_ALL_PARAMS_SCHEMA); - if (is_callable($class_or_object, true) && is_array($class_or_object)) { - # callable sous forme de tableau - $class_or_object = $class_or_object[0]; - } - if (is_string($class_or_object)) { - # lister les méthodes publiques statiques de la classe - $mask = self::MASK_PS; - $expected = self::METHOD_PS; - $c = new ReflectionClass($class_or_object); - } elseif (is_object($class_or_object)) { - # lister les méthodes publiques de la classe - $c = new ReflectionClass($class_or_object); - $mask = $params["static_only"]? self::MASK_PS: self::MASK_P; - $expected = $params["static_only"]? self::METHOD_PS: self::METHOD_P; - } else { - throw new ValueException("$class_or_object: vous devez spécifier une classe ou un objet"); - } - $prefix = $params["prefix"]; $prefixlen = strlen($prefix); - $args = $params["args"]; - $includes = $params["include"]; - $excludes = $params["exclude"]; - $methods = []; - foreach ($c->getMethods() as $m) { - if (($m->getModifiers() & $mask) != $expected) continue; - $name = $m->getName(); - if (substr($name, 0, $prefixlen) != $prefix) continue; - if (!self::matches($name, $includes, $excludes)) continue; - $methods[] = cl::merge([$class_or_object, $name], $args); - } - return $methods; - } - - /** - * Appeler toutes les méthodes publiques de $object_or_class et retourner un - * tableau [$method_name => $return_value] des valeurs de retour. - */ - static final function call_all($class_or_object, $params=null): array { - $methods = self::get_all($class_or_object, $params); - $values = []; - foreach ($methods as $method) { - self::fix_args($method, $args); - $values[$method[1]] = self::call($method, ...$args); - } - return $values; - } - - /** - * tester si $func est une chaine de la forme "XXX" où XXX est une classe - * valide, ou un tableau de la forme ["XXX", ...] - * - * NB: il est possible d'avoir {@link is_static()} et {@link is_class()} - * vraies pour la même valeur. s'il faut supporter les deux cas, appeler - * {@link is_static()} d'abord, mais dans ce cas, on ne supporte que les - * classes qui sont dans un package - */ - static final function is_class($class): bool { - if (is_string($class)) { - return class_exists($class); - } elseif (is_array($class) && array_key_exists(0, $class)) { - return class_exists($class[0]); - } - return false; - } - - /** - * en assumant que {@link is_class()} est vrai, si $class est un tableau de - * plus de 1 éléments, alors déplacer les éléments supplémentaires au début de - * $args. par exemple: - * ~~~ - * $class = ["class", "arg1", "arg2"]; - * $args = ["arg3"]; - * func::fix_class_args($class, $args) - * # $class === "class" - * # $args === ["arg1", "arg2", "arg3"] - * ~~~ - * - * @return bool true si la correction a été faite - */ - static final function fix_class_args(&$class, ?array &$args): bool { - if ($args === null) $args = []; - if (is_array($class)) { - if (count($class) > 1) { - $prefix_args = array_slice($class, 1); - $class = array_slice($class, 0, 1)[0]; - $args = array_merge($prefix_args, $args); - } else { - $class = $class[0]; - } - return true; - } - return false; - } - - /** - * s'assurer que $class est une classe et renseigner le cas échéant les - * arguments. - * - * @return bool true si c'est une classe valide. il ne reste plus qu'à - * l'instancier avec {@link cons()} - */ - static final function check_class(&$class, &$args=null): bool { - if (self::is_class($class)) { - self::fix_class_args($class, $args); - return true; - } - return false; - } - - /** - * Comme {@link check_class()} mais lance une exception si la classe est - * invalide - * - * @throws ValueException si $class n'est pas une classe valide - */ - static final function ensure_class(&$class, &$args=null): void { - if (!self::check_class($class, $args)) { - throw ValueException::invalid_type($class, "class"); - } - } - - /** - * Instancier la classe avec les arguments spécifiés. - * Adapter $args en fonction du nombre réel d'arguments du constructeur - */ - static final function cons(string $class, ...$args) { - $c = new ReflectionClass($class); - $rf = $c->getConstructor(); - if ($rf === null) { - return $c->newInstance(); - } else { - if (!$rf->isVariadic()) { - $minArgs = $rf->getNumberOfRequiredParameters(); - $maxArgs = $rf->getNumberOfParameters(); - $args = array_slice($args, 0, $maxArgs); - while (count($args) < $minArgs) { - $args[] = null; - } - } - return $c->newInstanceArgs($args); - } - } -} diff --git a/php/tests/appTest.php b/php/tests/appTest.php deleted file mode 100644 index 8d86b6f..0000000 --- a/php/tests/appTest.php +++ /dev/null @@ -1,132 +0,0 @@ - $projdir, - "vendor" => [ - "bindir" => "$projdir/vendor/bin", - "autoload" => "$projdir/vendor/autoload.php", - ], - "appcode" => "nur-sery", - "cwd" => $cwd, - "datadir" => "$projdir/devel", - "etcdir" => "$projdir/devel/etc", - "vardir" => "$projdir/devel/var", - "logdir" => "$projdir/devel/log", - "profile" => "devel", - "appgroup" => null, - "name" => "my-application1", - "title" => null, - ], $app1->getParams()); - - $app2 = myapp::with(MyApplication2::class, $app1); - self::assertSame([ - "projdir" => $projdir, - "vendor" => [ - "bindir" => "$projdir/vendor/bin", - "autoload" => "$projdir/vendor/autoload.php", - ], - "appcode" => "nur-sery", - "cwd" => $cwd, - "datadir" => "$projdir/devel", - "etcdir" => "$projdir/devel/etc", - "vardir" => "$projdir/devel/var", - "logdir" => "$projdir/devel/log", - "profile" => "devel", - "appgroup" => null, - "name" => "my-application2", - "title" => null, - ], $app2->getParams()); - } - - function testInit() { - $projdir = config::get_projdir(); - $cwd = getcwd(); - - myapp::reset(); - myapp::init(MyApplication1::class); - self::assertSame([ - "projdir" => $projdir, - "vendor" => [ - "bindir" => "$projdir/vendor/bin", - "autoload" => "$projdir/vendor/autoload.php", - ], - "appcode" => "nur-sery", - "cwd" => $cwd, - "datadir" => "$projdir/devel", - "etcdir" => "$projdir/devel/etc", - "vardir" => "$projdir/devel/var", - "logdir" => "$projdir/devel/log", - "profile" => "devel", - "appgroup" => null, - "name" => "my-application1", - "title" => null, - ], myapp::get()->getParams()); - - myapp::init(MyApplication2::class); - self::assertSame([ - "projdir" => $projdir, - "vendor" => [ - "bindir" => "$projdir/vendor/bin", - "autoload" => "$projdir/vendor/autoload.php", - ], - "appcode" => "nur-sery", - "cwd" => $cwd, - "datadir" => "$projdir/devel", - "etcdir" => "$projdir/devel/etc", - "vardir" => "$projdir/devel/var", - "logdir" => "$projdir/devel/log", - "profile" => "devel", - "appgroup" => null, - "name" => "my-application2", - "title" => null, - ], myapp::get()->getParams()); - } - } -} - -namespace nulib\impl { - - use nulib\app\cli\Application; - use nulib\os\path; - use nulib\app; - - class config { - const PROJDIR = __DIR__.'/..'; - - static function get_projdir(): string { - return path::abspath(self::PROJDIR); - } - } - - class myapp extends app { - static function reset(): void { - self::$app = null; - } - } - - class MyApplication1 extends Application { - const PROJDIR = config::PROJDIR; - - function main() { - } - } - class MyApplication2 extends Application { - const PROJDIR = null; - - function main() { - } - } -} diff --git a/php/tests/php/funcTest.php b/php/tests/php/funcTest.php index 3f580fa..0f5200b 100644 --- a/php/tests/php/funcTest.php +++ b/php/tests/php/funcTest.php @@ -1108,6 +1108,27 @@ namespace nulib\php { self::assertException(ValueException::class, function() use ($func) { $func->bind(new C0())->invoke(); }); + self::assertSame(11, $func->bind(new C0(), false, true)->invoke()); + } + + function testModifyArgs() { + $closure = function(...$args) { return $args; }; + + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->replaceArgs(["x", "y", "z"])->invoke()); + + self::assertSame(["x", "y", "z", "a", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(null, ["x", "y", "z"])->invoke()); + self::assertSame(["x", "y", "z", "a", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(0, ["x", "y", "z"])->invoke()); + self::assertSame(["x", "y", "z", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(1, ["x", "y", "z"])->invoke()); + self::assertSame(["x", "y", "z", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(2, ["x", "y", "z"])->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->prependArgs(3, ["x", "y", "z"])->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->prependArgs(4, ["x", "y", "z"])->invoke()); + + self::assertSame(["a", "b", "c", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(null, ["x", "y", "z"])->invoke()); + self::assertSame(["a", "b", "c", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(0, ["x", "y", "z"])->invoke()); + self::assertSame(["a", "b", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(1, ["x", "y", "z"])->invoke()); + self::assertSame(["a", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(2, ["x", "y", "z"])->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(3, ["x", "y", "z"])->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(4, ["x", "y", "z"])->invoke()); } } } diff --git a/php/tests/php/nur_funcTest.php b/php/tests/php/nur_funcTest.php deleted file mode 100644 index 44fa744..0000000 --- a/php/tests/php/nur_funcTest.php +++ /dev/null @@ -1,292 +0,0 @@ -")); - self::assertFalse(nur_func::is_method([])); - self::assertFalse(nur_func::is_method([""])); - self::assertFalse(nur_func::is_method([null, "->"])); - self::assertFalse(nur_func::is_method(["xxx", "->"])); - - self::assertTrue(nur_func::is_method("->xxx")); - self::assertTrue(nur_func::is_method(["->xxx"])); - self::assertTrue(nur_func::is_method([null, "->yyy"])); - self::assertTrue(nur_func::is_method(["xxx", "->yyy"])); - self::assertTrue(nur_func::is_method([null, "->yyy", "aaa"])); - self::assertTrue(nur_func::is_method(["xxx", "->yyy", "aaa"])); - } - - function testFix_method() { - $object = new \stdClass(); - $func= "->xxx"; - nur_func::fix_method($func, $object); - self::assertSame([$object, "xxx"], $func); - $func= ["->xxx"]; - nur_func::fix_method($func, $object); - self::assertSame([$object, "xxx"], $func); - $func= [null, "->yyy"]; - nur_func::fix_method($func, $object); - self::assertSame([$object, "yyy"], $func); - $func= ["xxx", "->yyy"]; - nur_func::fix_method($func, $object); - self::assertSame([$object, "yyy"], $func); - $func= [null, "->yyy", "aaa"]; - nur_func::fix_method($func, $object); - self::assertSame([$object, "yyy", "aaa"], $func); - $func= ["xxx", "->yyy", "aaa"]; - nur_func::fix_method($func, $object); - self::assertSame([$object, "yyy", "aaa"], $func); - } - - function testCall() { - self::assertSame(36, nur_func::call("func36")); - self::assertSame(12, nur_func::call(TC::class."::method")); - self::assertSame(12, nur_func::call([TC::class, "method"])); - $closure = function() { - return 21; - }; - self::assertSame(21, nur_func::call($closure)); - } - - function test_prepare_fill() { - # vérifier que les arguments sont bien remplis, en fonction du fait qu'ils - # soient obligatoires, facultatifs ou variadiques - - # m1 - self::assertSame([null], nur_func::call("func_m1")); - self::assertSame([null], nur_func::call("func_m1", null)); - self::assertSame([null], nur_func::call("func_m1", null, null)); - self::assertSame([null], nur_func::call("func_m1", null, null, null)); - self::assertSame([null], nur_func::call("func_m1", null, null, null, null)); - self::assertSame([1], nur_func::call("func_m1", 1)); - self::assertSame([1], nur_func::call("func_m1", 1, 2)); - self::assertSame([1], nur_func::call("func_m1", 1, 2, 3)); - self::assertSame([1], nur_func::call("func_m1", 1, 2, 3, 4)); - - # o1 - self::assertSame([9], nur_func::call("func_o1")); - self::assertSame([null], nur_func::call("func_o1", null)); - self::assertSame([null], nur_func::call("func_o1", null, null)); - self::assertSame([null], nur_func::call("func_o1", null, null, null)); - self::assertSame([null], nur_func::call("func_o1", null, null, null, null)); - self::assertSame([1], nur_func::call("func_o1", 1)); - self::assertSame([1], nur_func::call("func_o1", 1, 2)); - self::assertSame([1], nur_func::call("func_o1", 1, 2, 3)); - self::assertSame([1], nur_func::call("func_o1", 1, 2, 3, 4)); - - # v - self::assertSame([], nur_func::call("func_v")); - self::assertSame([null], nur_func::call("func_v", null)); - self::assertSame([null, null], nur_func::call("func_v", null, null)); - self::assertSame([null, null, null], nur_func::call("func_v", null, null, null)); - self::assertSame([null, null, null, null], nur_func::call("func_v", null, null, null, null)); - self::assertSame([1], nur_func::call("func_v", 1)); - self::assertSame([1, 2], nur_func::call("func_v", 1, 2)); - self::assertSame([1, 2, 3], nur_func::call("func_v", 1, 2, 3)); - self::assertSame([1, 2, 3, 4], nur_func::call("func_v", 1, 2, 3, 4)); - - # m1o1 - self::assertSame([null, 9], nur_func::call("func_m1o1")); - self::assertSame([null, 9], nur_func::call("func_m1o1", null)); - self::assertSame([null, null], nur_func::call("func_m1o1", null, null)); - self::assertSame([null, null], nur_func::call("func_m1o1", null, null, null)); - self::assertSame([null, null], nur_func::call("func_m1o1", null, null, null, null)); - self::assertSame([1, 9], nur_func::call("func_m1o1", 1)); - self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2)); - self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2, 3)); - self::assertSame([1, 2], nur_func::call("func_m1o1", 1, 2, 3, 4)); - - # m1v - self::assertSame([null], nur_func::call("func_m1v")); - self::assertSame([null], nur_func::call("func_m1v", null)); - self::assertSame([null, null], nur_func::call("func_m1v", null, null)); - self::assertSame([null, null, null], nur_func::call("func_m1v", null, null, null)); - self::assertSame([null, null, null, null], nur_func::call("func_m1v", null, null, null, null)); - self::assertSame([1], nur_func::call("func_m1v", 1)); - self::assertSame([1, 2], nur_func::call("func_m1v", 1, 2)); - self::assertSame([1, 2, 3], nur_func::call("func_m1v", 1, 2, 3)); - self::assertSame([1, 2, 3, 4], nur_func::call("func_m1v", 1, 2, 3, 4)); - - # m1o1v - self::assertSame([null, 9], nur_func::call("func_m1o1v")); - self::assertSame([null, 9], nur_func::call("func_m1o1v", null)); - self::assertSame([null, null], nur_func::call("func_m1o1v", null, null)); - self::assertSame([null, null, null], nur_func::call("func_m1o1v", null, null, null)); - self::assertSame([null, null, null, null], nur_func::call("func_m1o1v", null, null, null, null)); - self::assertSame([1, 9], nur_func::call("func_m1o1v", 1)); - self::assertSame([1, 2], nur_func::call("func_m1o1v", 1, 2)); - self::assertSame([1, 2, 3], nur_func::call("func_m1o1v", 1, 2, 3)); - self::assertSame([1, 2, 3, 4], nur_func::call("func_m1o1v", 1, 2, 3, 4)); - - # o1v - self::assertSame([9], nur_func::call("func_o1v")); - self::assertSame([null], nur_func::call("func_o1v", null)); - self::assertSame([null, null], nur_func::call("func_o1v", null, null)); - self::assertSame([null, null, null], nur_func::call("func_o1v", null, null, null)); - self::assertSame([null, null, null, null], nur_func::call("func_o1v", null, null, null, null)); - self::assertSame([1], nur_func::call("func_o1v", 1)); - self::assertSame([1, 2], nur_func::call("func_o1v", 1, 2)); - self::assertSame([1, 2, 3], nur_func::call("func_o1v", 1, 2, 3)); - self::assertSame([1, 2, 3, 4], nur_func::call("func_o1v", 1, 2, 3, 4)); - } - - function testCall_all() { - $c1 = new C1(); - $c2 = new C2(); - $c3 = new C3(); - - self::assertSameValues([11, 12], nur_func::call_all(C1::class)); - self::assertSameValues([11, 12, 21, 22], nur_func::call_all($c1)); - self::assertSameValues([13, 11, 12], nur_func::call_all(C2::class)); - self::assertSameValues([13, 23, 11, 12, 21, 22], nur_func::call_all($c2)); - self::assertSameValues([111, 13, 12], nur_func::call_all(C3::class)); - self::assertSameValues([111, 121, 13, 23, 12, 22], nur_func::call_all($c3)); - - $options = "conf"; - self::assertSameValues([11], nur_func::call_all(C1::class, $options)); - self::assertSameValues([11, 21], nur_func::call_all($c1, $options)); - self::assertSameValues([11], nur_func::call_all(C2::class, $options)); - self::assertSameValues([11, 21], nur_func::call_all($c2, $options)); - self::assertSameValues([111], nur_func::call_all(C3::class, $options)); - self::assertSameValues([111, 121], nur_func::call_all($c3, $options)); - - $options = ["prefix" => "conf"]; - self::assertSameValues([11], nur_func::call_all(C1::class, $options)); - self::assertSameValues([11, 21], nur_func::call_all($c1, $options)); - self::assertSameValues([11], nur_func::call_all(C2::class, $options)); - self::assertSameValues([11, 21], nur_func::call_all($c2, $options)); - self::assertSameValues([111], nur_func::call_all(C3::class, $options)); - self::assertSameValues([111, 121], nur_func::call_all($c3, $options)); - - self::assertSameValues([11, 12], nur_func::call_all($c1, ["include" => "x"])); - self::assertSameValues([11, 21], nur_func::call_all($c1, ["include" => "y"])); - self::assertSameValues([11, 12, 21], nur_func::call_all($c1, ["include" => ["x", "y"]])); - - self::assertSameValues([21, 22], nur_func::call_all($c1, ["exclude" => "x"])); - self::assertSameValues([12, 22], nur_func::call_all($c1, ["exclude" => "y"])); - self::assertSameValues([22], nur_func::call_all($c1, ["exclude" => ["x", "y"]])); - - self::assertSameValues([12], nur_func::call_all($c1, ["include" => "x", "exclude" => "y"])); - } - - function testCons() { - $obj1 = nur_func::cons(WoCons::class, 1, 2, 3); - self::assertInstanceOf(WoCons::class, $obj1); - - $obj2 = nur_func::cons(WithEmptyCons::class, 1, 2, 3); - self::assertInstanceOf(WithEmptyCons::class, $obj2); - - $obj3 = nur_func::cons(WithCons::class, 1, 2, 3); - self::assertInstanceOf(WithCons::class, $obj3); - self::assertSame(1, $obj3->first); - } - } - - class WoCons { - } - class WithEmptyCons { - function __construct() { - } - } - class WithCons { - public $first; - function __construct($first) { - $this->first = $first; - } - } - - class TC { - static function method() { - return 12; - } - } - - class C1 { - static function confps1_xy() { - return 11; - } - static function ps2_x() { - return 12; - } - function confp1_y() { - return 21; - } - function p2() { - return 22; - } - } - class C2 extends C1 { - static function ps3() { - return 13; - } - function p3() { - return 23; - } - } - class C3 extends C2 { - static function confps1_xy() { - return 111; - } - function confp1_y() { - return 121; - } - } -} From bab9ba81fe90b393150d004a6fb7e2252fda00e5 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Thu, 10 Apr 2025 14:33:24 +0400 Subject: [PATCH 07/32] =?UTF-8?q?d=C3=A9but=20pgsql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- composer.json | 1 + php/src/cl.php | 1 - php/src/db/ITransactor.php | 5 +- php/src/db/{pdo => _private}/_config.php | 9 +- php/src/db/pdo/Pdo.php | 3 +- php/src/db/pgsql/Pgsql.php | 201 +++++++++++++++++++++++ php/src/db/pgsql/PgsqlException.php | 12 ++ php/src/db/sqlite/Sqlite.php | 1 + php/src/db/sqlite/_config.php | 35 ---- php/src/db/sqlite/_migration.php | 1 - php/src/file/tab/AbstractBuilder.php | 1 - 11 files changed, 226 insertions(+), 44 deletions(-) rename php/src/db/{pdo => _private}/_config.php (79%) create mode 100644 php/src/db/pgsql/Pgsql.php create mode 100644 php/src/db/pgsql/PgsqlException.php delete mode 100644 php/src/db/sqlite/_config.php diff --git a/composer.json b/composer.json index 346a248..f423241 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "ext-pcntl": "*", "ext-curl": "*", "ext-pdo": "*", + "ext-pgsql": "*", "ext-sqlite3": "*" }, "autoload": { diff --git a/php/src/cl.php b/php/src/cl.php index c0e64e8..4978afe 100644 --- a/php/src/cl.php +++ b/php/src/cl.php @@ -3,7 +3,6 @@ namespace nulib; use ArrayAccess; use nulib\php\func; -use nulib\php\nur_func; use Traversable; /** diff --git a/php/src/db/ITransactor.php b/php/src/db/ITransactor.php index 3738bb7..03dd549 100644 --- a/php/src/db/ITransactor.php +++ b/php/src/db/ITransactor.php @@ -11,12 +11,13 @@ interface ITransactor { */ function willUpdate(...$transactors): self; + /** Indiquer si une transaction est en cours */ function inTransaction(): bool; /** * démarrer une transaction * - * si $func!==null, l'apppeler. ensuite, si $commit===true, commiter la + * si $func!==null, l'apppeler. ensuite, si $commit===true, valider la * transaction. si une erreur se produit lors de l'appel de la fonction, * annuler la transaction * @@ -24,7 +25,9 @@ interface ITransactor { */ function beginTransaction(?callable $func=null, bool $commit=true): void; + /** valider la transaction */ function commit(): void; + /** annuler la transaction */ function rollback(): void; } diff --git a/php/src/db/pdo/_config.php b/php/src/db/_private/_config.php similarity index 79% rename from php/src/db/pdo/_config.php rename to php/src/db/_private/_config.php index 2cc9c96..e72b526 100644 --- a/php/src/db/pdo/_config.php +++ b/php/src/db/_private/_config.php @@ -1,6 +1,7 @@ configs as $key => $config) { if (is_string($config) && !func::is_method($config)) { - $pdo->exec($config); + $db->exec($config); } else { - func::with($config)->bind($this, true)->invoke([$pdo, $key]); + func::with($config)->bind($this, true)->invoke([$db, $key]); } } } diff --git a/php/src/db/pdo/Pdo.php b/php/src/db/pdo/Pdo.php index 2fa8bf0..fb8b9b8 100644 --- a/php/src/db/pdo/Pdo.php +++ b/php/src/db/pdo/Pdo.php @@ -3,6 +3,7 @@ namespace nulib\db\pdo; use Generator; use nulib\cl; +use nulib\db\_private\_config; use nulib\db\_private\Tvalues; use nulib\db\IDatabase; use nulib\db\ITransactor; @@ -104,7 +105,7 @@ class Pdo implements IDatabase { protected ?array $dbconn; /** @var array|callable */ - protected array $options; + protected $options; /** @var array|string|callable */ protected $config; diff --git a/php/src/db/pgsql/Pgsql.php b/php/src/db/pgsql/Pgsql.php new file mode 100644 index 0000000..7484cd8 --- /dev/null +++ b/php/src/db/pgsql/Pgsql.php @@ -0,0 +1,201 @@ + $pgsql->dbconn, + "options" => $pgsql->options, + "config" => $pgsql->config, + "migrate" => $pgsql->migration, + ], $params)); + } else { + return new static($pgsql, $params); + } + } + + + protected const OPTIONS = [ + "persistent" => true, + "force_new" => false, + ]; + + const CONFIG = null; + + const MIGRATE = null; + + const params_SCHEMA = [ + "dbconn" => ["array"], + "options" => ["?array|callable"], + "replace_config" => ["?array|callable"], + "config" => ["?array|callable"], + "migrate" => ["?array|string|callable"], + "auto_open" => ["bool", true], + ]; + + const dbconn_SCHEMA = [ + "" => "?string", + "host" => "string", + "hostaddr" => "?string", + "port" => "?int", + "dbname" => "string", + "user" => "string", + "password" => "string", + "connect_timeout" => "?int", + "options" => "?string", + "sslmode" => "?string", + "service" => "?string", + ]; + + protected const dbconn_MAP = [ + "name" => "dbname", + "pass" => "password", + ]; + + const options_SCHEMA = [ + "persistent" => ["bool", self::OPTIONS["persistent"]], + "force_new" => ["bool", self::OPTIONS["force_new"]], + ]; + + function __construct($dbconn=null, ?array $params=null) { + if ($dbconn !== null) { + if (!is_array($dbconn)) { + $dbconn = ["" => $dbconn]; + #XXX à terme, il faudra interroger config + #$tmp = config::db($dbconn); + #if ($tmp !== null) $dbconn = $tmp; + #else $dbconn = ["" => $dbconn]; + } + $params["dbconn"] = $dbconn; + } + # dbconn + $this->dbconn = $params["dbconn"] ?? null; + # options + $this->options = $params["options"] ?? static::OPTIONS; + # configuration + $config = $params["replace_config"] ?? null; + if ($config === null) { + $config = $params["config"] ?? static::CONFIG; + if (is_callable($config)) $config = [$config]; + } + $this->config = $config; + # migrations + $this->migration = $params["migrate"] ?? static::MIGRATE; + # + $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; + if ($params["auto_open"] ?? $defaultAutoOpen) { + $this->open(); + } + } + + protected ?array $dbconn; + + /** @var array|callable|null */ + protected $options; + + /** @var array|string|callable */ + protected $config; + + /** @var array|string|callable */ + protected $migration; + + /** @var resource */ + protected $db = null; + + function open(): self { + if ($this->db === null) { + $dbconn = $this->dbconn; + $connection_string = [$dbconn[""] ?? null]; + unset($dbconn[""]); + foreach ($dbconn as $key => $value) { + if ($value === null) continue; + $value = strval($value); + if ($value === "" || preg_match("/[ '\\\\]/", $value)) { + $value = str_replace("\\", "\\\\", $value); + $value = str_replace("'", "\\'", $value); + $value = "'$value'"; + } + $key = cl::get(self::dbconn_MAP, $key, $key); + $connection_string[] = "$key=$value"; + } + $connection_string = implode(" ", array_filter($connection_string)); + $options = $this->options; + if (is_callable($options)) { + $options = func::with($options)->bind($this, true)->invoke(); + } + $forceNew = $options["force_new"] ?? false; + $flags = $forceNew? PGSQL_CONNECT_FORCE_NEW: 0; + + if ($options["persistent"] ?? true) $db = pg_pconnect($connection_string, $flags); + else $db = pg_connect($connection_string, $flags); + if ($db === false) throw new PgsqlException("unable to connect"); + $this->db = $db; + + _config::with($this->config)->configure($this); + //_migration::with($this->migration)->migrate($this); + } + return $this; + } + + function close(): self { + if ($this->db !== null) { + pg_close($this->db); + $this->db = null; + } + return $this; + } + + protected function db() { + $this->open(); + return $this->db; + } + + function exec($query, ?array $params = null) { + // TODO: Implement exec() method. + } + + function willUpdate(...$transactors): \nulib\db\ITransactor { + // TODO: Implement willUpdate() method. + } + + function inTransaction(): bool { + // TODO: Implement inTransaction() method. + } + + function beginTransaction(?callable $func = null, bool $commit = true): void { + // TODO: Implement beginTransaction() method. + } + + function commit(): void { + // TODO: Implement commit() method. + } + + function rollback(): void { + // TODO: Implement rollback() method. + } + + function get($query, ?array $params = null, bool $entireRow = false) { + // TODO: Implement get() method. + } + + function one($query, ?array $params = null): ?array { + // TODO: Implement one() method. + } + + function all($query, ?array $params = null, $primaryKeys = null): iterable { + // TODO: Implement all() method. + } +} diff --git a/php/src/db/pgsql/PgsqlException.php b/php/src/db/pgsql/PgsqlException.php new file mode 100644 index 0000000..056bf41 --- /dev/null +++ b/php/src/db/pgsql/PgsqlException.php @@ -0,0 +1,12 @@ +getMessage(), $e->getCode(), $e); + } +} diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index 89db30b..b797eb9 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -3,6 +3,7 @@ namespace nulib\db\sqlite; use Generator; use nulib\cl; +use nulib\db\_private\_config; use nulib\db\_private\Tvalues; use nulib\db\IDatabase; use nulib\db\ITransactor; diff --git a/php/src/db/sqlite/_config.php b/php/src/db/sqlite/_config.php deleted file mode 100644 index bcfe8fb..0000000 --- a/php/src/db/sqlite/_config.php +++ /dev/null @@ -1,35 +0,0 @@ -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::with($config)->bind($this, true)->invoke([$sqlite, $key]); - } - } - } -} diff --git a/php/src/db/sqlite/_migration.php b/php/src/db/sqlite/_migration.php index b5ee6f4..cafb991 100644 --- a/php/src/db/sqlite/_migration.php +++ b/php/src/db/sqlite/_migration.php @@ -2,7 +2,6 @@ namespace nulib\db\sqlite; use nulib\php\func; -use nulib\php\nur_func; class _migration { static function with($migrations): self { diff --git a/php/src/file/tab/AbstractBuilder.php b/php/src/file/tab/AbstractBuilder.php index 77d61b7..23ab27a 100644 --- a/php/src/file/tab/AbstractBuilder.php +++ b/php/src/file/tab/AbstractBuilder.php @@ -6,7 +6,6 @@ use nulib\cl; use nulib\file\TempStream; use nulib\os\path; use nulib\php\func; -use nulib\php\nur_func; use nulib\php\time\DateTime; use nulib\web\http; From 5c6d55ed469d933f1e26e0bc4281680221665dff Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Thu, 10 Apr 2025 15:04:37 +0400 Subject: [PATCH 08/32] maj ordre func --- php/src/db/CapacitorStorage.php | 4 ++-- php/src/file/tab/AbstractBuilder.php | 2 +- php/src/php/func.php | 4 ++-- php/tests/php/funcTest.php | 26 ++++++++++++++------------ 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index c5cf345..1f6c759 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -276,7 +276,7 @@ EOT; $pvalues = $this->unserialize($channel, $prow); } - $updates = $initFunc->prependArgs(null, [$item, $values, $pvalues])->invoke(); + $updates = $initFunc->prependArgs([$item, $values, $pvalues])->invoke(); if ($updates === [false]) return 0; if (is_array($updates) && $updates) { if ($insert === null) $insert = false; @@ -289,7 +289,7 @@ EOT; if ($func !== null) { $updates = func::with($func) - ->prependArgs(null, [$item, $values, $pvalues]) + ->prependArgs([$item, $values, $pvalues]) ->bind($channel, true) ->invoke(); if ($updates === [false]) return 0; diff --git a/php/src/file/tab/AbstractBuilder.php b/php/src/file/tab/AbstractBuilder.php index 23ab27a..2affcc8 100644 --- a/php/src/file/tab/AbstractBuilder.php +++ b/php/src/file/tab/AbstractBuilder.php @@ -81,7 +81,7 @@ abstract class AbstractBuilder extends TempStream implements IBuilder { protected function cookRow(?array $row): ?array { if ($this->cookFunc !== null) { - $row = $this->cookFunc->prependArgs(null, [$row])->invoke(); + $row = $this->cookFunc->prependArgs([$row])->invoke(); } if ($row !== null) { foreach ($row as &$col) { diff --git a/php/src/php/func.php b/php/src/php/func.php index d81b416..79e9c78 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -597,14 +597,14 @@ class func { return $this; } - function prependArgs(?int $stripCount=null, ?array $args=null): self { + function prependArgs(?array $args, ?int $stripCount=null): self { if ($stripCount !== null || $args !== null) { array_splice($this->prefixArgs, 0, $stripCount ?? 0, $args); } return $this; } - function appendArgs(?int $stripCount=null, ?array $args=null): self { + function appendArgs(?array $args, ?int $stripCount=null): self { if ($stripCount !== null || $args !== null) { $stripCount ??= 0; if ($stripCount > 0) array_splice($this->prefixArgs, -$stripCount); diff --git a/php/tests/php/funcTest.php b/php/tests/php/funcTest.php index 0f5200b..45a0581 100644 --- a/php/tests/php/funcTest.php +++ b/php/tests/php/funcTest.php @@ -1103,8 +1103,10 @@ namespace nulib\php { function testRebind() { $func = func::with([C1::class, "tmethod"]); + // objets du même type self::assertSame(11, $func->bind(new C1(0))->invoke()); self::assertSame(12, $func->bind(new C1(1))->invoke()); + // objets de type différent self::assertException(ValueException::class, function() use ($func) { $func->bind(new C0())->invoke(); }); @@ -1116,19 +1118,19 @@ namespace nulib\php { self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->replaceArgs(["x", "y", "z"])->invoke()); - self::assertSame(["x", "y", "z", "a", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(null, ["x", "y", "z"])->invoke()); - self::assertSame(["x", "y", "z", "a", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(0, ["x", "y", "z"])->invoke()); - self::assertSame(["x", "y", "z", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(1, ["x", "y", "z"])->invoke()); - self::assertSame(["x", "y", "z", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(2, ["x", "y", "z"])->invoke()); - self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->prependArgs(3, ["x", "y", "z"])->invoke()); - self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->prependArgs(4, ["x", "y", "z"])->invoke()); + self::assertSame(["x", "y", "z", "a", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"])->invoke()); + self::assertSame(["x", "y", "z", "a", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"], 0)->invoke()); + self::assertSame(["x", "y", "z", "b", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"], 1)->invoke()); + self::assertSame(["x", "y", "z", "c"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"], 2)->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"], 3)->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->prependArgs(["x", "y", "z"], 4)->invoke()); - self::assertSame(["a", "b", "c", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(null, ["x", "y", "z"])->invoke()); - self::assertSame(["a", "b", "c", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(0, ["x", "y", "z"])->invoke()); - self::assertSame(["a", "b", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(1, ["x", "y", "z"])->invoke()); - self::assertSame(["a", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(2, ["x", "y", "z"])->invoke()); - self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(3, ["x", "y", "z"])->invoke()); - self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(4, ["x", "y", "z"])->invoke()); + self::assertSame(["a", "b", "c", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"])->invoke()); + self::assertSame(["a", "b", "c", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"], 0)->invoke()); + self::assertSame(["a", "b", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"], 1)->invoke()); + self::assertSame(["a", "x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"], 2)->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"], 3)->invoke()); + self::assertSame(["x", "y", "z"], func::with($closure, ["a", "b", "c"])->appendArgs(["x", "y", "z"], 4)->invoke()); } } } From ebbd9e06c06512ba9980cf12cc9a3efcbdbdf3ac Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Thu, 10 Apr 2025 15:27:19 +0400 Subject: [PATCH 09/32] modifs.mineures sans commentaires --- php/src/db/pgsql/Pgsql.php | 79 ++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/php/src/db/pgsql/Pgsql.php b/php/src/db/pgsql/Pgsql.php index 7484cd8..1f3dd1e 100644 --- a/php/src/db/pgsql/Pgsql.php +++ b/php/src/db/pgsql/Pgsql.php @@ -7,6 +7,7 @@ use nulib\db\_private\Tvalues; use nulib\db\IDatabase; use nulib\db\ITransactor; use nulib\php\func; +use nulib\ValueException; class Pgsql implements IDatabase { use Tvalues; @@ -163,39 +164,93 @@ class Pgsql implements IDatabase { return $this->db; } - function exec($query, ?array $params = null) { + /** + * @return resource|false + */ + function _exec(string $query) { + return pg_query($this->db(), $query); + } + + function exec($query, ?array $params=null) { // TODO: Implement exec() method. } - function willUpdate(...$transactors): \nulib\db\ITransactor { - // TODO: Implement willUpdate() method. + /** @var ITransactor[] */ + protected ?array $transactors = null; + + function willUpdate(...$transactors): self { + foreach ($transactors as $transactor) { + if ($transactor instanceof ITransactor) { + $this->transactors[] = $transactor; + $transactor->willUpdate(); + } else { + throw ValueException::invalid_type($transactor, ITransactor::class); + } + } + return $this; } - function inTransaction(): bool { - // TODO: Implement inTransaction() method. + function inTransaction(?bool &$inerror=null): bool { + $status = pg_transaction_status($this->db()); + if ($status === PGSQL_TRANSACTION_ACTIVE || $status === PGSQL_TRANSACTION_INTRANS) { + $inerror = false; + return true; + } elseif ($status === PGSQL_TRANSACTION_INERROR) { + $inerror = true; + return true; + } else { + return false; + } } - function beginTransaction(?callable $func = null, bool $commit = true): void { - // TODO: Implement beginTransaction() method. + function beginTransaction(?callable $func=null, bool $commit=true): void { + $this->_exec("begin"); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->beginTransaction(); + } + } + if ($func !== null) { + $commited = false; + try { + func::call($func, $this); + if ($commit) { + $this->commit(); + $commited = true; + } + } finally { + if ($commit && !$commited) $this->rollback(); + } + } } function commit(): void { - // TODO: Implement commit() method. + $this->_exec("commit"); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->commit(); + } + } } function rollback(): void { - // TODO: Implement rollback() method. + $this->_exec("rollback"); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->rollback(); + } + } } - function get($query, ?array $params = null, bool $entireRow = false) { + function get($query, ?array $params=null, bool $entireRow=false) { // TODO: Implement get() method. } - function one($query, ?array $params = null): ?array { + function one($query, ?array $params=null): ?array { // TODO: Implement one() method. } - function all($query, ?array $params = null, $primaryKeys = null): iterable { + function all($query, ?array $params=null, $primaryKeys=null): iterable { // TODO: Implement all() method. } } From 64c872cf3fb5bb36e3567fd34344659db93c133a Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Fri, 11 Apr 2025 18:20:27 +0400 Subject: [PATCH 10/32] modifs.mineures sans commentaires --- php/src/db/IDatabase.php | 4 +++ php/src/db/_private/_base.php | 46 ++++++++++++++++++++++++- php/src/db/pdo/Pdo.php | 15 ++------ php/src/db/pdo/query.php | 53 ----------------------------- php/src/db/pgsql/Pgsql.php | 51 ++++++++++++++++++++++----- php/src/db/pgsql/PgsqlException.php | 4 +++ php/src/db/pgsql/query.php | 36 ++++++++++++++++++++ php/src/db/sqlite/Sqlite.php | 13 ++----- php/src/db/sqlite/query.php | 25 -------------- 9 files changed, 136 insertions(+), 111 deletions(-) create mode 100644 php/src/db/pgsql/query.php diff --git a/php/src/db/IDatabase.php b/php/src/db/IDatabase.php index 497e5be..dc71720 100644 --- a/php/src/db/IDatabase.php +++ b/php/src/db/IDatabase.php @@ -15,5 +15,9 @@ interface IDatabase extends ITransactor { function one($query, ?array $params=null): ?array; + /** + * si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s) + * spécifiée(s) + */ function all($query, ?array $params=null, $primaryKeys=null): iterable; } diff --git a/php/src/db/_private/_base.php b/php/src/db/_private/_base.php index 14568cb..5e366e6 100644 --- a/php/src/db/_private/_base.php +++ b/php/src/db/_private/_base.php @@ -6,7 +6,51 @@ use nulib\str; use nulib\ValueException; abstract class _base extends _common { - abstract protected static function verifix(&$sql, ?array &$bindings=null, ?array &$meta=null): void; + protected static function verifix(&$sql, ?array &$bindings=null, ?array &$meta=null): void { + if (is_array($sql)) { + $prefix = $sql[0] ?? null; + if ($prefix === null) { + throw new ValueException("requête invalide"); + } elseif (_create::isa($prefix)) { + $sql = _create::parse($sql, $bindings); + $meta = ["isa" => "create", "type" => "ddl"]; + } elseif (_select::isa($prefix)) { + $sql = _select::parse($sql, $bindings); + $meta = ["isa" => "select", "type" => "dql"]; + } elseif (_insert::isa($prefix)) { + $sql = _insert::parse($sql, $bindings); + $meta = ["isa" => "insert", "type" => "dml"]; + } elseif (_update::isa($prefix)) { + $sql = _update::parse($sql, $bindings); + $meta = ["isa" => "update", "type" => "dml"]; + } elseif (_delete::isa($prefix)) { + $sql = _delete::parse($sql, $bindings); + $meta = ["isa" => "delete", "type" => "dml"]; + } elseif (_generic::isa($prefix)) { + $sql = _generic::parse($sql, $bindings); + $meta = ["isa" => "generic", "type" => null]; + } else { + throw ValueException::invalid_kind($sql, "query"); + } + } else { + if (!is_string($sql)) $sql = strval($sql); + if (_create::isa($sql)) { + $meta = ["isa" => "create", "type" => "ddl"]; + } elseif (_select::isa($sql)) { + $meta = ["isa" => "select", "type" => "dql"]; + } elseif (_insert::isa($sql)) { + $meta = ["isa" => "insert", "type" => "dml"]; + } elseif (_update::isa($sql)) { + $meta = ["isa" => "update", "type" => "dml"]; + } elseif (_delete::isa($sql)) { + $meta = ["isa" => "delete", "type" => "dml"]; + } elseif (_generic::isa($sql)) { + $meta = ["isa" => "generic", "type" => null]; + } else { + $meta = ["isa" => "generic", "type" => null]; + } + } + } static function with($sql, ?array $params=null): array { static::verifix($sql, $params); diff --git a/php/src/db/pdo/Pdo.php b/php/src/db/pdo/Pdo.php index fb8b9b8..286dbeb 100644 --- a/php/src/db/pdo/Pdo.php +++ b/php/src/db/pdo/Pdo.php @@ -143,11 +143,6 @@ class Pdo implements IDatabase { return $this->db()->exec($query); } - private static function is_insert(?string $sql): bool { - if ($sql === null) return false; - return preg_match('/^\s*insert\b/i', $sql); - } - function exec($query, ?array $params=null) { $db = $this->db(); $query = new query($query, $params); @@ -157,7 +152,7 @@ class Pdo implements IDatabase { else return $stmt->rowCount(); } else { $rowCount = $db->exec($sql); - if (self::is_insert($sql)) return $db->lastInsertId(); + if ($query->isInsert()) return $db->lastInsertId(); else return $rowCount; } } @@ -245,11 +240,7 @@ class Pdo implements IDatabase { return $this->get($query, $params, true); } - /** - * si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s) - * spécifiée(s) - */ - function all($query, ?array $params=null, $primaryKeys=null): Generator { + function all($query, ?array $params=null, $primaryKeys=null): iterable { $db = $this->db(); $query = new query($query, $params); $stmt = null; @@ -260,7 +251,7 @@ class Pdo implements IDatabase { } else { $stmt = $db->query($sql); } - if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys); + $primaryKeys = cl::withn($primaryKeys); while (($row = $stmt->fetch(\PDO::FETCH_ASSOC)) !== false) { $this->verifixRow($row); if ($primaryKeys !== null) { diff --git a/php/src/db/pdo/query.php b/php/src/db/pdo/query.php index f57cf4b..5d6962f 100644 --- a/php/src/db/pdo/query.php +++ b/php/src/db/pdo/query.php @@ -2,64 +2,11 @@ namespace nulib\db\pdo; use nulib\db\_private\_base; -use nulib\db\_private\_create; -use nulib\db\_private\_delete; -use nulib\db\_private\_generic; -use nulib\db\_private\_insert; -use nulib\db\_private\_select; -use nulib\db\_private\_update; use nulib\db\_private\Tbindings; -use nulib\ValueException; class query extends _base { use Tbindings; - protected static function verifix(&$sql, ?array &$bindings=null, ?array &$meta=null): void { - if (is_array($sql)) { - $prefix = $sql[0] ?? null; - if ($prefix === null) { - throw new ValueException("requête invalide"); - } elseif (_create::isa($prefix)) { - $sql = _create::parse($sql, $bindings); - $meta = ["isa" => "create", "type" => "ddl"]; - } elseif (_select::isa($prefix)) { - $sql = _select::parse($sql, $bindings); - $meta = ["isa" => "select", "type" => "dql"]; - } elseif (_insert::isa($prefix)) { - $sql = _insert::parse($sql, $bindings); - $meta = ["isa" => "insert", "type" => "dml"]; - } elseif (_update::isa($prefix)) { - $sql = _update::parse($sql, $bindings); - $meta = ["isa" => "update", "type" => "dml"]; - } elseif (_delete::isa($prefix)) { - $sql = _delete::parse($sql, $bindings); - $meta = ["isa" => "delete", "type" => "dml"]; - } elseif (_generic::isa($prefix)) { - $sql = _generic::parse($sql, $bindings); - $meta = ["isa" => "generic", "type" => null]; - } else { - throw ValueException::invalid_kind($sql, "query"); - } - } else { - if (!is_string($sql)) $sql = strval($sql); - if (_create::isa($sql)) { - $meta = ["isa" => "create", "type" => "ddl"]; - } elseif (_select::isa($sql)) { - $meta = ["isa" => "select", "type" => "dql"]; - } elseif (_insert::isa($sql)) { - $meta = ["isa" => "insert", "type" => "dml"]; - } elseif (_update::isa($sql)) { - $meta = ["isa" => "update", "type" => "dml"]; - } elseif (_delete::isa($sql)) { - $meta = ["isa" => "delete", "type" => "dml"]; - } elseif (_generic::isa($sql)) { - $meta = ["isa" => "generic", "type" => null]; - } else { - $meta = ["isa" => "generic", "type" => null]; - } - } - } - const DEBUG_QUERIES = false; function useStmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool { diff --git a/php/src/db/pgsql/Pgsql.php b/php/src/db/pgsql/Pgsql.php index 1f3dd1e..f45ba30 100644 --- a/php/src/db/pgsql/Pgsql.php +++ b/php/src/db/pgsql/Pgsql.php @@ -164,15 +164,27 @@ class Pgsql implements IDatabase { return $this->db; } - /** - * @return resource|false - */ - function _exec(string $query) { - return pg_query($this->db(), $query); + function _exec(string $query): bool { + $result = pg_query($this->db(), $query); + if ($result === false) return false; + pg_free_result($result); + return true; } function exec($query, ?array $params=null) { - // TODO: Implement exec() method. + $db = $this->db(); + $query = new query($query, $params); + $result = $query->_exec($db); + if ($query->isInsert()) { + $result = @pg_query($db, "select lastval()"); + if ($result === false) return false; + $lastInsertId = pg_fetch_row($result)[0]; + pg_free_result($result); + return $lastInsertId; + } + $affected_rows = pg_affected_rows($result); + pg_free_result($result); + return $affected_rows; } /** @var ITransactor[] */ @@ -243,14 +255,35 @@ class Pgsql implements IDatabase { } function get($query, ?array $params=null, bool $entireRow=false) { - // TODO: Implement get() method. + $db = $this->db(); + $query = new query($query, $params); + $result = $query->_exec($db); + $row = pg_fetch_assoc($result); + pg_free_result($result); + if ($row === false) return null; + $this->verifixRow($row); + if ($entireRow) return $row; + else return cl::first($row); } function one($query, ?array $params=null): ?array { - // TODO: Implement one() method. + return $this->get($query, $params, true); } function all($query, ?array $params=null, $primaryKeys=null): iterable { - // TODO: Implement all() method. + $db = $this->db(); + $query = new query($query, $params); + $result = $query->_exec($db); + $primaryKeys = cl::withn($primaryKeys); + while (($row = pg_fetch_assoc($result)) !== false) { + $this->verifixRow($row); + if ($primaryKeys !== null) { + $key = implode("-", cl::select($row, $primaryKeys)); + yield $key => $row; + } else { + yield $row; + } + } + pg_free_result($result); } } diff --git a/php/src/db/pgsql/PgsqlException.php b/php/src/db/pgsql/PgsqlException.php index 056bf41..afd21c4 100644 --- a/php/src/db/pgsql/PgsqlException.php +++ b/php/src/db/pgsql/PgsqlException.php @@ -6,6 +6,10 @@ use RuntimeException; use SQLite3; class PgsqlException extends RuntimeException { + static final function last_error($db): self { + return new static(pg_last_error($db)); + } + static final function wrap(Exception $e): self { return new static($e->getMessage(), $e->getCode(), $e); } diff --git a/php/src/db/pgsql/query.php b/php/src/db/pgsql/query.php new file mode 100644 index 0000000..c2b76b0 --- /dev/null +++ b/php/src/db/pgsql/query.php @@ -0,0 +1,36 @@ +sql); + //error_log(var_export($this->bindings, true)); + } + if ($this->bindings !== null) { + #XXX corriger les bindings et la requête *dans le constructeur* + $result = pg_query_params($db, $this->sql, $this->bindings); + } else { + $result = pg_query($db, $this->sql); + } + if ($result === false) throw PgsqlException::last_error($db); + return $result; + } +} diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index b797eb9..47ed23a 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -181,11 +181,6 @@ class Sqlite implements IDatabase { return $this->db()->exec($query); } - private static function is_insert(?string $sql): bool { - if ($sql === null) return false; - return preg_match('/^\s*insert\b/i', $sql); - } - function exec($query, ?array $params=null) { $db = $this->db(); $query = new query($query, $params); @@ -202,7 +197,7 @@ class Sqlite implements IDatabase { } else { $result = $db->exec($sql); if ($result === false) return false; - if (self::is_insert($sql)) return $db->lastInsertRowID(); + if ($query->isInsert()) return $db->lastInsertRowID(); else return $db->changes(); } } @@ -301,7 +296,7 @@ class Sqlite implements IDatabase { } protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null, $primaryKeys=null): Generator { - if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys); + $primaryKeys = cl::withn($primaryKeys); try { while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) { $this->verifixRow($row); @@ -318,10 +313,6 @@ class Sqlite implements IDatabase { } } - /** - * si $primaryKeys est fourni, le résultat est indexé sur la(es) colonne(s) - * spécifiée(s) - */ function all($query, ?array $params=null, $primaryKeys=null): iterable { $db = $this->db(); $query = new query($query, $params); diff --git a/php/src/db/sqlite/query.php b/php/src/db/sqlite/query.php index 2c53dda..294d583 100644 --- a/php/src/db/sqlite/query.php +++ b/php/src/db/sqlite/query.php @@ -17,31 +17,6 @@ use SQLite3Stmt; class query extends _base { use Tbindings; - protected static function verifix(&$sql, ?array &$bindings=null, ?array &$meta=null): void { - if (is_array($sql)) { - $prefix = $sql[0] ?? null; - if ($prefix === null) { - throw new ValueException("requête invalide"); - } elseif (_create::isa($prefix)) { - $sql = _create::parse($sql, $bindings); - } elseif (_select::isa($prefix)) { - $sql = _select::parse($sql, $bindings); - } elseif (_insert::isa($prefix)) { - $sql = _insert::parse($sql, $bindings); - } elseif (_update::isa($prefix)) { - $sql = _update::parse($sql, $bindings); - } elseif (_delete::isa($prefix)) { - $sql = _delete::parse($sql, $bindings); - } elseif (_generic::isa($prefix)) { - $sql = _generic::parse($sql, $bindings); - } else { - throw SqliteException::wrap(ValueException::invalid_kind($sql, "query")); - } - } elseif (!is_string($sql)) { - $sql = strval($sql); - } - } - const DEBUG_QUERIES = false; function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool { From 86136e75a533d38763db3699d9a9b2184dd57398 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Sun, 13 Apr 2025 18:09:24 +0400 Subject: [PATCH 11/32] modifs.mineures sans commentaires --- php/src/db/pgsql/Pgsql.php | 19 ++++++++++++------- php/src/db/pgsql/query.php | 31 +++++++++++++++++++------------ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/php/src/db/pgsql/Pgsql.php b/php/src/db/pgsql/Pgsql.php index f45ba30..2fef081 100644 --- a/php/src/db/pgsql/Pgsql.php +++ b/php/src/db/pgsql/Pgsql.php @@ -32,6 +32,7 @@ class Pgsql implements IDatabase { protected const OPTIONS = [ "persistent" => true, "force_new" => false, + "serial_support" => true, ]; const CONFIG = null; @@ -171,17 +172,21 @@ class Pgsql implements IDatabase { return true; } + function getLastSerial() { + $db = $this->db(); + $result = @pg_query($db, "select lastval()"); + if ($result === false) return false; + $lastSerial = pg_fetch_row($result)[0]; + pg_free_result($result); + return $lastSerial; + } + function exec($query, ?array $params=null) { $db = $this->db(); $query = new query($query, $params); $result = $query->_exec($db); - if ($query->isInsert()) { - $result = @pg_query($db, "select lastval()"); - if ($result === false) return false; - $lastInsertId = pg_fetch_row($result)[0]; - pg_free_result($result); - return $lastInsertId; - } + $serialSupport = $this->options["serial_support"] ?? true; + if ($serialSupport && $query->isInsert()) return $this->getLastSerial(); $affected_rows = pg_affected_rows($result); pg_free_result($result); return $affected_rows; diff --git a/php/src/db/pgsql/query.php b/php/src/db/pgsql/query.php index c2b76b0..c1c11ba 100644 --- a/php/src/db/pgsql/query.php +++ b/php/src/db/pgsql/query.php @@ -1,15 +1,9 @@ sql; + $bindings = $this->bindings; if (static::DEBUG_QUERIES) { #XXX - error_log($this->sql); + error_log($sql); //error_log(var_export($this->bindings, true)); } - if ($this->bindings !== null) { - #XXX corriger les bindings et la requête *dans le constructeur* - $result = pg_query_params($db, $this->sql, $this->bindings); + if ($bindings !== null) { + # trier d'abord les champ par ordre de longueur, pour éviter les overlaps + $names = array_keys($bindings); + usort($names, function ($a, $b) { + return -cv::compare(strlen(strval($a)), strlen(strval($b))); + }); + $bparams = []; + $number = 1; + foreach ($names as $name) { + $sql = str_replace(":$name", "\$$number", $sql); + $bparams[] = $bindings[$name]; + $number++; + } + $result = pg_query_params($db, $sql, $bparams); } else { - $result = pg_query($db, $this->sql); + $result = pg_query($db, $sql); } if ($result === false) throw PgsqlException::last_error($db); return $result; From d241ce656113d43e3257b3da69c468f1ed3c3d32 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Mon, 14 Apr 2025 10:42:30 +0400 Subject: [PATCH 12/32] ajout PgsqlStorage --- pgsql.php | 81 ++++++++++++++++++++++ php/src/db/CapacitorChannel.php | 3 - php/src/db/CapacitorStorage.php | 21 ++++-- php/src/db/mysql/MysqlStorage.php | 15 ++-- php/src/db/pdo/Pdo.php | 6 +- php/src/db/pdo/query.php | 9 +-- php/src/db/pgsql/PgsqlStorage.php | 84 +++++++++++++++++++++++ php/src/db/pgsql/query.php | 7 +- php/src/db/sqlite/Sqlite.php | 6 +- php/src/db/sqlite/SqliteStorage.php | 21 +++--- php/src/db/sqlite/query.php | 7 +- php/src/php/func.php | 46 +++++-------- php/tests/db/sqlite/SqliteStorageTest.php | 2 +- 13 files changed, 235 insertions(+), 73 deletions(-) create mode 100644 pgsql.php create mode 100644 php/src/db/pgsql/PgsqlStorage.php diff --git a/pgsql.php b/pgsql.php new file mode 100644 index 0000000..918568f --- /dev/null +++ b/pgsql.php @@ -0,0 +1,81 @@ + "pegase-dre.self", + "dbname" => "dre", + "user" => "root", + "password" => "admin", + #"user" => "reader", + #"password" => "reader", +]); + +function e($sql) { + global $pgsql; + $v = $pgsql->exec($sql); + $v = var_export($v, true); + echo "'$sql' --> $v\n"; +} +function g($sql) { + global $pgsql; + $v = $pgsql->get($sql); + $v = var_export($v, true); + echo "'$sql' --> $v\n"; +} +function o($sql) { + global $pgsql; + $r = $pgsql->one($sql); + $r = var_export($r, true); + echo "'$sql' --> $r\n"; +} +function a($sql) { + global $pgsql; + $rs = $pgsql->all($sql); + echo "'$sql'\n"; + foreach ($rs as $r) { + $r = var_export($r, true); + echo " --> $r\n"; + } +} + +g("select age from personnes where id=1"); +o("select name, age from personnes where id=2"); +a("select id, name, age from personnes"); + +$n = rand(); +$pgsql->exec([ + "insert", + "into" => "personnes", + "values" => [ + "name" => "prout$n", + "age" => $n, + ], +]); + +class MyChannel extends CapacitorChannel { + const COLUMN_DEFINITIONS = [ + "name" => "varchar", + "value" => "int", + ]; + + function getItemValues($item): ?array { + return $item; + } +} +$storage = new PgsqlStorage($pgsql); +$channel = new MyChannel(); +new Capacitor($storage, $channel); + +$channel->charge([ + "name" => "one", + "value" => rand(), +]); +foreach ($channel->all(null) as $row) { + var_export($row); +} \ No newline at end of file diff --git a/php/src/db/CapacitorChannel.php b/php/src/db/CapacitorChannel.php index 4495074..97819d4 100644 --- a/php/src/db/CapacitorChannel.php +++ b/php/src/db/CapacitorChannel.php @@ -245,9 +245,6 @@ class CapacitorChannel { return $serial !== null? unserialize($serial): null; } - const SERIAL_DEFINITION = "mediumtext"; - const SUM_DEFINITION = "varchar(40)"; - final function sum(?string $serial, $value=null): ?string { if ($serial === null) $serial = $this->serialize($value); return $serial !== null? sha1($serial): null; diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index 1f6c759..830e032 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -35,11 +35,15 @@ abstract class CapacitorStorage { /** DOIT être défini dans les classes dérivées */ const PRIMARY_KEY_DEFINITION = null; + const SERDATA_DEFINITION = "mediumtext"; + const SERSUM_DEFINITION = "varchar(40)"; + const SERTS_DEFINITION = "datetime"; + const COLUMN_DEFINITIONS = [ - "item__" => CapacitorChannel::SERIAL_DEFINITION, - "item__sum_" => CapacitorChannel::SUM_DEFINITION, - "created_" => "datetime", - "modified_" => "datetime", + "item__" => "serdata", + "item__sum_" => "sersum", + "created_" => "serts", + "modified_" => "serts", ]; protected function ColumnDefinitions(CapacitorChannel $channel): array { @@ -56,6 +60,11 @@ abstract class CapacitorStorage { $constraints = []; $index = 0; foreach ($tmp as $col => $def) { + switch ($def) { + case "serdata": $def = static::SERDATA_DEFINITION; break; + case "sersum": $def = static::SERSUM_DEFINITION; break; + case "serts": $def = static::SERTS_DEFINITION; break; + } if ($col === $index) { $index++; $constraints[] = $def; @@ -160,7 +169,7 @@ EOT; protected function _create(CapacitorChannel $channel): void { $channel->ensureSetup(); if (!$channel->isCreated()) { - $this->db->exec($this->_createSql($channel)); + $this->db()->exec($this->_createSql($channel)); $this->_afterCreate($channel); $channel->setCreated(); } @@ -188,7 +197,7 @@ EOT; /** supprimer le canal spécifié */ function _reset(CapacitorChannel $channel, bool $recreate=false): void { $this->_beforeReset($channel); - $this->db->exec([ + $this->db()->exec([ "drop table if exists", $channel->getTableName(), ]); diff --git a/php/src/db/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php index 4d8cab5..0c5e0f6 100644 --- a/php/src/db/mysql/MysqlStorage.php +++ b/php/src/db/mysql/MysqlStorage.php @@ -9,14 +9,13 @@ use nulib\db\CapacitorStorage; */ class MysqlStorage extends CapacitorStorage { function __construct($mysql) { - $this->db = Mysql::with($mysql); + $this->mysql = Mysql::with($mysql); } - /** @var Mysql */ - protected $db; + protected Mysql $mysql; function db(): Mysql { - return $this->db; + return $this->mysql; } const PRIMARY_KEY_DEFINITION = [ @@ -29,11 +28,11 @@ class MysqlStorage extends CapacitorStorage { } function _exists(CapacitorChannel $channel): bool { - $db = $this->db; - $tableName = $db->get([ + $mysql = $this->mysql; + $tableName = $mysql->get([ "select table_name from information_schema.tables", "where" => [ - "table_schema" => $db->getDbname(), + "table_schema" => $mysql->getDbname(), "table_name" => $channel->getTableName(), ], ]); @@ -41,6 +40,6 @@ class MysqlStorage extends CapacitorStorage { } function close(): void { - $this->db->close(); + $this->mysql->close(); } } diff --git a/php/src/db/pdo/Pdo.php b/php/src/db/pdo/Pdo.php index 286dbeb..091050a 100644 --- a/php/src/db/pdo/Pdo.php +++ b/php/src/db/pdo/Pdo.php @@ -146,7 +146,7 @@ class Pdo implements IDatabase { function exec($query, ?array $params=null) { $db = $this->db(); $query = new query($query, $params); - if ($query->useStmt($db, $stmt, $sql)) { + if ($query->_use_stmt($db, $stmt, $sql)) { if ($stmt->execute() === false) return false; if ($query->isInsert()) return $db->lastInsertId(); else return $stmt->rowCount(); @@ -221,7 +221,7 @@ class Pdo implements IDatabase { $stmt = null; try { /** @var \PDOStatement $stmt */ - if ($query->useStmt($db, $stmt, $sql)) { + if ($query->_use_stmt($db, $stmt, $sql)) { if ($stmt->execute() === false) return null; } else { $stmt = $db->query($sql); @@ -246,7 +246,7 @@ class Pdo implements IDatabase { $stmt = null; try { /** @var \PDOStatement $stmt */ - if ($query->useStmt($db, $stmt, $sql)) { + if ($query->_use_stmt($db, $stmt, $sql)) { if ($stmt->execute() === false) return; } else { $stmt = $db->query($sql); diff --git a/php/src/db/pdo/query.php b/php/src/db/pdo/query.php index 5d6962f..6fb05c9 100644 --- a/php/src/db/pdo/query.php +++ b/php/src/db/pdo/query.php @@ -3,16 +3,17 @@ namespace nulib\db\pdo; use nulib\db\_private\_base; use nulib\db\_private\Tbindings; +use nulib\output\msg; class query extends _base { use Tbindings; const DEBUG_QUERIES = false; - function useStmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool { - if (static::DEBUG_QUERIES) { #XXX - error_log($this->sql); - //error_log(var_export($this->bindings, true)); + function _use_stmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool { + if (static::DEBUG_QUERIES) {#XXX + msg::info($this->sql); + //msg::info(var_export($this->bindings, true)); } if ($this->bindings !== null) { $stmt = $db->prepare($this->sql); diff --git a/php/src/db/pgsql/PgsqlStorage.php b/php/src/db/pgsql/PgsqlStorage.php new file mode 100644 index 0000000..f47078a --- /dev/null +++ b/php/src/db/pgsql/PgsqlStorage.php @@ -0,0 +1,84 @@ +pgsql = Pgsql::with($pgsql); + } + + protected Pgsql $pgsql; + + function db(): Pgsql { + return $this->pgsql; + } + + const PRIMARY_KEY_DEFINITION = [ + "id_" => "serial primary key", + ]; + + function _getCreateSql(CapacitorChannel $channel): string { + $query = new query($this->_createSql($channel)); + return self::format_sql($channel, $query->getSql()); + } + + protected function _afterCreate(CapacitorChannel $channel): void { + $db = $this->pgsql; + $db->exec([ + "create table if not exists", + "table" => "_channels", + "cols" => [ + "name" => "varchar primary key", + "table_name" => "varchar", + "class" => "varchar", + ], + ]); + $db->exec([ + "insert", + "into" => "_channels", + "values" => [ + "name" => $channel->getName(), + "table_name" => $channel->getTableName(), + "class" => get_class($channel), + ], + "suffix" => "on conflict (name) do nothing", + ]); + } + + protected function _beforeReset(CapacitorChannel $channel): void { + $this->pgsql->exec([ + "delete", + "from" => "_channels", + "where" => [ + "name" => $channel->getName(), + ], + ]); + } + + function _exists(CapacitorChannel $channel): bool { + $tableName = $channel->getTableName(); + if (($index = strpos($tableName, ".")) !== false) { + $schemaName = substr($tableName, 0, $index); + $tableName = substr($tableName, $index + 1); + } else { + $schemaName = "public"; + } + return null !== $this->pgsql->get([ + "select tablename from pg_tables", + "where" => [ + "schemaname" => $schemaName, + "tablename" => $tableName, + ], + ]); + } + + function close(): void { + $this->pgsql->close(); + } +} diff --git a/php/src/db/pgsql/query.php b/php/src/db/pgsql/query.php index c1c11ba..3ab5b6d 100644 --- a/php/src/db/pgsql/query.php +++ b/php/src/db/pgsql/query.php @@ -4,6 +4,7 @@ namespace nulib\db\pgsql; use nulib\cv; use nulib\db\_private\_base; use nulib\db\_private\Tbindings; +use nulib\output\msg; class query extends _base { use Tbindings; @@ -16,9 +17,9 @@ class query extends _base { function _exec($db) { $sql = $this->sql; $bindings = $this->bindings; - if (static::DEBUG_QUERIES) { #XXX - error_log($sql); - //error_log(var_export($this->bindings, true)); + if (static::DEBUG_QUERIES) {#XXX + msg::info($sql); + //msg::info(var_export($bindings, true)); } if ($bindings !== null) { # trier d'abord les champ par ordre de longueur, pour éviter les overlaps diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index 47ed23a..4e22e11 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -184,7 +184,7 @@ class Sqlite implements IDatabase { function exec($query, ?array $params=null) { $db = $this->db(); $query = new query($query, $params); - if ($query->useStmt($db, $stmt, $sql)) { + if ($query->_use_stmt($db, $stmt, $sql)) { try { $result = $stmt->execute(); if ($result === false) return false; @@ -271,7 +271,7 @@ class Sqlite implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); $query = new query($query, $params); - if ($query->useStmt($db, $stmt, $sql)) { + if ($query->_use_stmt($db, $stmt, $sql)) { try { $result = $this->checkResult($stmt->execute()); try { @@ -316,7 +316,7 @@ class Sqlite implements IDatabase { function all($query, ?array $params=null, $primaryKeys=null): iterable { $db = $this->db(); $query = new query($query, $params); - if ($query->useStmt($db, $stmt, $sql)) { + if ($query->_use_stmt($db, $stmt, $sql)) { $result = $this->checkResult($stmt->execute()); return $this->_fetchResult($result, $stmt, $primaryKeys); } else { diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index b29639b..d304d93 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -9,14 +9,13 @@ use nulib\db\CapacitorStorage; */ class SqliteStorage extends CapacitorStorage { function __construct($sqlite) { - $this->db = Sqlite::with($sqlite); + $this->sqlite = Sqlite::with($sqlite); } - /** @var Sqlite */ - protected $db; + protected Sqlite $sqlite; function db(): Sqlite { - return $this->db; + return $this->sqlite; } const PRIMARY_KEY_DEFINITION = [ @@ -29,7 +28,7 @@ class SqliteStorage extends CapacitorStorage { } function tableExists(string $tableName): bool { - $name = $this->db->get([ + $name = $this->sqlite->get([ # depuis la version 3.33.0 le nom officiel de la table est sqlite_schema, # mais le nom sqlite_master est toujours valable pour le moment "select name from sqlite_master ", @@ -39,7 +38,7 @@ class SqliteStorage extends CapacitorStorage { } function channelExists(string $name): bool { - $name = $this->db->get([ + $name = $this->sqlite->get([ "select name from _channels", "where" => ["name" => $name], ]); @@ -47,11 +46,11 @@ class SqliteStorage extends CapacitorStorage { } protected function _afterCreate(CapacitorChannel $channel): void { - $db = $this->db; + $sqlite = $this->sqlite; if (!$this->tableExists("_channels")) { # ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un # verrou en écriture - $db->exec([ + $sqlite->exec([ "create table if not exists", "table" => "_channels", "cols" => [ @@ -64,7 +63,7 @@ class SqliteStorage extends CapacitorStorage { if (!$this->channelExists($channel->getName())) { # ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un # verrou en écriture - $db->exec([ + $sqlite->exec([ "insert", "into" => "_channels", "values" => [ @@ -78,7 +77,7 @@ class SqliteStorage extends CapacitorStorage { } protected function _beforeReset(CapacitorChannel $channel): void { - $this->db->exec([ + $this->sqlite->exec([ "delete", "from" => "_channels", "where" => [ @@ -92,6 +91,6 @@ class SqliteStorage extends CapacitorStorage { } function close(): void { - $this->db->close(); + $this->sqlite->close(); } } diff --git a/php/src/db/sqlite/query.php b/php/src/db/sqlite/query.php index 294d583..e8df936 100644 --- a/php/src/db/sqlite/query.php +++ b/php/src/db/sqlite/query.php @@ -19,8 +19,11 @@ class query extends _base { const DEBUG_QUERIES = false; - function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool { - if (static::DEBUG_QUERIES) msg::info($this->sql); #XXX + function _use_stmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool { + if (static::DEBUG_QUERIES) {#XXX + msg::info($this->sql); + //msg::info(var_export($this->bindings, true)); + } if ($this->bindings !== null) { /** @var SQLite3Stmt $stmt */ $stmt = SqliteException::check($db, $db->prepare($this->sql)); diff --git a/php/src/php/func.php b/php/src/php/func.php index 79e9c78..1309ec8 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -61,10 +61,7 @@ class func { * la fonction (ne pas uniquement faire une vérification syntaxique) */ static function verifix_function(&$func, bool $strict=true, ?string &$reason=null): bool { - if ($strict) { - $msg = var_export($func, true); - $reason = null; - } + if ($strict) $reason = null; if ($func instanceof ReflectionFunction) return true; if (is_string($func)) { $c = false; @@ -85,11 +82,11 @@ class func { if ($strict) { $reason = null; if (class_exists($f)) { - $reason = "$msg: is a class"; + $reason = "$f: is a class"; return false; } if (!function_exists($f)) { - $reason = "$msg: function not found"; + $reason = "$f: function not found"; return false; } } @@ -120,10 +117,7 @@ class func { * faire une vérification syntaxique) */ static function verifix_class(&$func, bool $strict=true, ?string &$reason=null): bool { - if ($strict) { - $msg = var_export($func, true); - $reason = null; - } + if ($strict) $reason = null; if ($func instanceof ReflectionClass) return true; if (is_string($func)) { $c = $func; @@ -141,11 +135,9 @@ class func { if (self::_parse_static($c)) return false; if (self::_parse_method($c)) return false; if ($f !== false) return false; - if ($strict) { - if (!class_exists($c)) { - $reason = "$msg: class not found"; - return false; - } + if ($strict && !class_exists($c)) { + $reason = "$c: class not found"; + return false; } $func = [$c, false]; return true; @@ -210,10 +202,7 @@ class func { * la méthode est liée (ne pas uniquement faire une vérification syntaxique) */ static function verifix_static(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { - if ($strict) { - $msg = var_export($func, true); - $reason = null; - } + if ($strict) $reason = null; if ($func instanceof ReflectionMethod) { $bound = false; return true; @@ -268,18 +257,19 @@ class func { return false; } if ($strict) { + [$c, $f] = $cf; $reason = null; if ($bound) { if (!class_exists($c)) { - $reason = "$msg: class not found"; + $reason = "$c: class not found"; return false; } if (!method_exists($c, $f)) { - $reason = "$msg: method not found"; + $reason = "$c::$f: method not found"; return false; } } else { - $reason = "$msg: not bound"; + $reason = "$c::$f: not bound"; } } $func = $cf; @@ -345,10 +335,7 @@ class func { * la méthode est liée (ne pas uniquement faire une vérification syntaxique) */ static function verifix_method(&$func, bool $strict=true, ?bool &$bound=null, ?string &$reason=null): bool { - if ($strict) { - $msg = var_export($func, true); - $reason = null; - } + if ($strict) $reason = null; if ($func instanceof ReflectionMethod) { $bound = false; return true; @@ -404,18 +391,19 @@ class func { return false; } if ($strict) { + [$c, $f] = $cf; $reason = null; if ($bound) { if (!is_object($c) && !class_exists($c)) { - $reason = "$msg: class not found"; + $reason = "$c: class not found"; return false; } if (!method_exists($c, $f)) { - $reason = "$msg: method not found"; + $reason = "$c::$f: method not found"; return false; } } else { - $reason = "$msg: not bound"; + $reason = "$c::$f: not bound"; } } $func = $cf; diff --git a/php/tests/db/sqlite/SqliteStorageTest.php b/php/tests/db/sqlite/SqliteStorageTest.php index e40ccdf..e7197a3 100644 --- a/php/tests/db/sqlite/SqliteStorageTest.php +++ b/php/tests/db/sqlite/SqliteStorageTest.php @@ -118,7 +118,7 @@ class SqliteStorageTest extends TestCase { const COLUMN_DEFINITIONS = [ "a__" => "varchar", "b__" => "varchar", - "b__sum_" => self::SUM_DEFINITION, + "b__sum_" => "sersum", ]; function getItemValues($item): ?array { From 2a46c12e0811bbab32794c727c98ae69dc1754f3 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Mon, 14 Apr 2025 18:23:15 +0400 Subject: [PATCH 13/32] modifs.mineures sans commentaires --- .gitignore | 2 + pgsql.php | 81 ------------------- php/src/db/Capacitor.php | 4 - php/src/db/CapacitorChannel.php | 9 +++ php/src/db/CapacitorStorage.php | 17 ++-- php/src/db/_private/_create.php | 36 +++++---- php/src/db/_private/_delete.php | 12 ++- php/src/db/_private/_generic.php | 2 +- php/src/db/_private/_migration.php | 45 +++++++++++ php/src/db/_private/_update.php | 9 ++- php/src/db/mysql/MysqlStorage.php | 2 +- php/src/db/mysql/_mysqlQuery.php | 7 ++ php/src/db/mysql/query.php | 5 -- php/src/db/pdo/Pdo.php | 14 ++-- php/src/db/pdo/{query.php => _pdoQuery.php} | 2 +- php/src/db/pgsql/Pgsql.php | 14 ++-- php/src/db/pgsql/PgsqlStorage.php | 2 +- .../db/pgsql/{query.php => _pgsqlQuery.php} | 2 +- php/src/db/sqlite/Sqlite.php | 16 ++-- php/src/db/sqlite/SqliteStorage.php | 27 +++++-- php/src/db/sqlite/_migration.php | 54 ------------- php/src/db/sqlite/_sqliteMigration.php | 57 +++++++++++++ .../db/sqlite/{query.php => _sqliteQuery.php} | 2 +- php/src/php/func.php | 17 +++- php/tests/db/sqlite/SqliteTest.php | 8 +- php/tests/db/sqlite/_queryTest.php | 46 +++++------ 26 files changed, 253 insertions(+), 239 deletions(-) delete mode 100644 pgsql.php create mode 100644 php/src/db/_private/_migration.php create mode 100644 php/src/db/mysql/_mysqlQuery.php delete mode 100644 php/src/db/mysql/query.php rename php/src/db/pdo/{query.php => _pdoQuery.php} (95%) rename php/src/db/pgsql/{query.php => _pgsqlQuery.php} (96%) delete mode 100644 php/src/db/sqlite/_migration.php create mode 100644 php/src/db/sqlite/_sqliteMigration.php rename php/src/db/sqlite/{query.php => _sqliteQuery.php} (97%) diff --git a/.gitignore b/.gitignore index 62033b5..df06735 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/test_* + /.composer.lock.runphp .~lock*# diff --git a/pgsql.php b/pgsql.php deleted file mode 100644 index 918568f..0000000 --- a/pgsql.php +++ /dev/null @@ -1,81 +0,0 @@ - "pegase-dre.self", - "dbname" => "dre", - "user" => "root", - "password" => "admin", - #"user" => "reader", - #"password" => "reader", -]); - -function e($sql) { - global $pgsql; - $v = $pgsql->exec($sql); - $v = var_export($v, true); - echo "'$sql' --> $v\n"; -} -function g($sql) { - global $pgsql; - $v = $pgsql->get($sql); - $v = var_export($v, true); - echo "'$sql' --> $v\n"; -} -function o($sql) { - global $pgsql; - $r = $pgsql->one($sql); - $r = var_export($r, true); - echo "'$sql' --> $r\n"; -} -function a($sql) { - global $pgsql; - $rs = $pgsql->all($sql); - echo "'$sql'\n"; - foreach ($rs as $r) { - $r = var_export($r, true); - echo " --> $r\n"; - } -} - -g("select age from personnes where id=1"); -o("select name, age from personnes where id=2"); -a("select id, name, age from personnes"); - -$n = rand(); -$pgsql->exec([ - "insert", - "into" => "personnes", - "values" => [ - "name" => "prout$n", - "age" => $n, - ], -]); - -class MyChannel extends CapacitorChannel { - const COLUMN_DEFINITIONS = [ - "name" => "varchar", - "value" => "int", - ]; - - function getItemValues($item): ?array { - return $item; - } -} -$storage = new PgsqlStorage($pgsql); -$channel = new MyChannel(); -new Capacitor($storage, $channel); - -$channel->charge([ - "name" => "one", - "value" => rand(), -]); -foreach ($channel->all(null) as $row) { - var_export($row); -} \ No newline at end of file diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php index 70c6f46..aa5da52 100644 --- a/php/src/db/Capacitor.php +++ b/php/src/db/Capacitor.php @@ -120,10 +120,6 @@ class Capacitor implements ITransactor { if ($db->inTransaction()) $db->rollback(); } - function getCreateSql(): string { - return $this->storage->_getCreateSql($this->channel); - } - function exists(): bool { return $this->storage->_exists($this->channel); } diff --git a/php/src/db/CapacitorChannel.php b/php/src/db/CapacitorChannel.php index 97819d4..a17e679 100644 --- a/php/src/db/CapacitorChannel.php +++ b/php/src/db/CapacitorChannel.php @@ -17,6 +17,8 @@ class CapacitorChannel { const PRIMARY_KEYS = null; + const MIGRATION = null; + const MANAGE_TRANSACTIONS = true; const EACH_COMMIT_THRESHOLD = 100; @@ -80,6 +82,7 @@ class CapacitorChannel { } $this->columnDefinitions = $columnDefinitions; $this->primaryKeys = $primaryKeys; + $this->migration = cl::withn(static::MIGRATION); } protected string $name; @@ -192,6 +195,12 @@ class CapacitorChannel { return $this->columnDefinitions; } + protected ?array $migration; + + function getMigration(): ?array { + return $this->migration; + } + protected ?array $primaryKeys; function getPrimaryKeys(): ?array { diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index 830e032..e2bc4ac 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -2,6 +2,7 @@ namespace nulib\db; use nulib\cl; +use nulib\db\_private\_migration; use nulib\db\cache\cache; use nulib\php\func; use nulib\ValueException; @@ -75,6 +76,10 @@ abstract class CapacitorStorage { return cl::merge($definitions, $constraints); } + protected function getMigration(CapacitorChannel $channel): ?array { + return $channel->getMigration(); + } + /** sérialiser les valeurs qui doivent l'être dans $values */ protected function serialize(CapacitorChannel $channel, ?array $values): ?array { if ($values === null) return null; @@ -137,11 +142,10 @@ abstract class CapacitorStorage { } protected function _createSql(CapacitorChannel $channel): array { - $cols = $this->ColumnDefinitions($channel); return [ "create table if not exists", "table" => $channel->getTableName(), - "cols" => $cols, + "cols" => $this->ColumnDefinitions($channel), ]; } @@ -156,12 +160,7 @@ $sql; EOT; } - abstract function _getCreateSql(CapacitorChannel $channel): string; - - /** obtenir la requête SQL utilisée pour créer la table */ - function getCreateSql(?string $channel): string { - return $this->_getCreateSql($this->getChannel($channel)); - } + abstract function _getMigration(CapacitorChannel $channel): _migration; protected function _afterCreate(CapacitorChannel $channel): void { } @@ -169,7 +168,7 @@ EOT; protected function _create(CapacitorChannel $channel): void { $channel->ensureSetup(); if (!$channel->isCreated()) { - $this->db()->exec($this->_createSql($channel)); + $this->_getMigration($channel)->migrate($this->db()); $this->_afterCreate($channel); $channel->setCreated(); } diff --git a/php/src/db/_private/_create.php b/php/src/db/_private/_create.php index 030c594..29e83eb 100644 --- a/php/src/db/_private/_create.php +++ b/php/src/db/_private/_create.php @@ -18,28 +18,36 @@ class _create extends _common { static function parse(array $query, ?array &$bindings=null): string { #XXX implémentation minimale - $sql = [self::merge_seq($query)]; + $tmpsql = self::merge_seq($query); + self::consume('create(?:\s+table)?\b', $tmpsql); + $sql = ["create table"]; + if ($tmpsql) $sql[] = $tmpsql; ## préfixe - if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; ## table - $sql[] = $query["table"]; + $table = $query["table"] ?? null; + if ($table !== null) $sql[] = $table; ## columns - $cols = $query["cols"]; - $index = 0; - foreach ($cols as $col => &$definition) { - if ($col === $index) { - $index++; - } else { - $definition = "$col $definition"; - } - }; unset($definition); - $sql[] = "(\n ".implode("\n, ", $cols)."\n)"; + $cols = $query["cols"] ?? null; + if ($cols !== null) { + $index = 0; + foreach ($cols as $col => &$definition) { + if ($col === $index) { + $index++; + } else { + $definition = "$col $definition"; + } + }; unset($definition); + $sql[] = "(\n ".implode("\n, ", $cols)."\n)"; + } ## suffixe - if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + $suffix = $query["suffix"] ?? null; + if ($suffix !== null) $sql[] = $suffix; ## fin de la requête return implode(" ", $sql); diff --git a/php/src/db/_private/_delete.php b/php/src/db/_private/_delete.php index fd78dbb..55409d9 100644 --- a/php/src/db/_private/_delete.php +++ b/php/src/db/_private/_delete.php @@ -17,13 +17,16 @@ class _delete extends _common { #XXX implémentation minimale $tmpsql = self::merge_seq($query); self::consume('delete(?:\s+from)?\b', $tmpsql); - $sql = ["delete from", $tmpsql]; + $sql = ["delete from"]; + if ($tmpsql) $sql[] = $tmpsql; ## préfixe - if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; ## table - $sql[] = $query["from"]; + $from = $query["from"] ?? null; + if ($from !== null) $sql[] = $from; ## where $where = $query["where"] ?? null; @@ -36,7 +39,8 @@ class _delete extends _common { } ## suffixe - if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + $suffix = $query["suffix"] ?? null; + if ($suffix !== null) $sql[] = $suffix; ## fin de la requête return implode(" ", $sql); diff --git a/php/src/db/_private/_generic.php b/php/src/db/_private/_generic.php index 1fef213..a2ec549 100644 --- a/php/src/db/_private/_generic.php +++ b/php/src/db/_private/_generic.php @@ -9,7 +9,7 @@ class _generic extends _common { ]; static function isa(string $sql): bool { - return preg_match('/^(?:drop\s+table)\b/i', $sql); + return preg_match('/^drop\s+table\b/i', $sql); } static function parse(array $query, ?array &$bindings=null): string { diff --git a/php/src/db/_private/_migration.php b/php/src/db/_private/_migration.php new file mode 100644 index 0000000..8ad75a6 --- /dev/null +++ b/php/src/db/_private/_migration.php @@ -0,0 +1,45 @@ +db = $db; + $this->channel = $channel; + if ($migrations === null) $migrations = static::MIGRATION; + 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; + } + + protected ?IDatabase $db; + + protected string $channel; + + /** @var callable[]|string[] */ + protected $migrations; + + abstract function setup(): void; + abstract function beforeMigrate(string $key): bool; + abstract function afterMigrate(string $key): void; + + function migrate(?IDatabase $db=null): void { + $db = ($this->db ??= $db); + $this->setup(); + foreach ($this->migrations as $key => $migration) { + if (!$this->beforeMigrate($key)) continue; + if (is_string($migration) || !func::is_callable($migration)) { + $db->exec($migration); + } else { + func::with($migration)->bind($this, true)->invoke([$db, $key]); + } + $this->afterMigrate($key); + } + } +} diff --git a/php/src/db/_private/_update.php b/php/src/db/_private/_update.php index 97cf75d..7b297c4 100644 --- a/php/src/db/_private/_update.php +++ b/php/src/db/_private/_update.php @@ -21,10 +21,12 @@ class _update extends _common { $sql = [self::merge_seq($query)]; ## préfixe - if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; ## table - $sql[] = $query["table"]; + $table = $query["table"] ?? null; + if ($table !== null) $sql[] = $table; ## set self::parse_set_values($query["values"], $setsql, $bindings); @@ -42,7 +44,8 @@ class _update extends _common { } ## suffixe - if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; + $suffix = $query["suffix"] ?? null; + if ($suffix !== null) $sql[] = $suffix; ## fin de la requête return implode(" ", $sql); diff --git a/php/src/db/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php index 0c5e0f6..ced2c6f 100644 --- a/php/src/db/mysql/MysqlStorage.php +++ b/php/src/db/mysql/MysqlStorage.php @@ -23,7 +23,7 @@ class MysqlStorage extends CapacitorStorage { ]; function _getCreateSql(CapacitorChannel $channel): string { - $query = new query($this->_createSql($channel)); + $query = new _mysqlQuery($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } diff --git a/php/src/db/mysql/_mysqlQuery.php b/php/src/db/mysql/_mysqlQuery.php new file mode 100644 index 0000000..83cfa1b --- /dev/null +++ b/php/src/db/mysql/_mysqlQuery.php @@ -0,0 +1,7 @@ + $pdo->dbconn, "options" => $pdo->options, "config" => $pdo->config, - "migrate" => $pdo->migration, + "migration" => $pdo->migration, ], $params)); } else { return new static($pdo, $params); @@ -50,7 +50,7 @@ class Pdo implements IDatabase { protected const CONFIG = null; - protected const MIGRATE = null; + protected const MIGRATION = null; const dbconn_SCHEMA = [ "name" => "string", @@ -63,7 +63,7 @@ class Pdo implements IDatabase { "options" => ["?array|callable"], "replace_config" => ["?array|callable"], "config" => ["?array|callable"], - "migrate" => ["?array|string|callable"], + "migration" => ["?array|string|callable"], "auto_open" => ["bool", true], ]; @@ -94,7 +94,7 @@ class Pdo implements IDatabase { } $this->config = $config; # migrations - $this->migration = $params["migrate"] ?? static::MIGRATE; + $this->migration = $params["migration"] ?? static::MIGRATION; # $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; if ($params["auto_open"] ?? $defaultAutoOpen) { @@ -145,7 +145,7 @@ class Pdo implements IDatabase { function exec($query, ?array $params=null) { $db = $this->db(); - $query = new query($query, $params); + $query = new _pdoQuery($query, $params); if ($query->_use_stmt($db, $stmt, $sql)) { if ($stmt->execute() === false) return false; if ($query->isInsert()) return $db->lastInsertId(); @@ -217,7 +217,7 @@ class Pdo implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new query($query, $params); + $query = new _pdoQuery($query, $params); $stmt = null; try { /** @var \PDOStatement $stmt */ @@ -242,7 +242,7 @@ class Pdo implements IDatabase { function all($query, ?array $params=null, $primaryKeys=null): iterable { $db = $this->db(); - $query = new query($query, $params); + $query = new _pdoQuery($query, $params); $stmt = null; try { /** @var \PDOStatement $stmt */ diff --git a/php/src/db/pdo/query.php b/php/src/db/pdo/_pdoQuery.php similarity index 95% rename from php/src/db/pdo/query.php rename to php/src/db/pdo/_pdoQuery.php index 6fb05c9..c1161fa 100644 --- a/php/src/db/pdo/query.php +++ b/php/src/db/pdo/_pdoQuery.php @@ -5,7 +5,7 @@ use nulib\db\_private\_base; use nulib\db\_private\Tbindings; use nulib\output\msg; -class query extends _base { +class _pdoQuery extends _base { use Tbindings; const DEBUG_QUERIES = false; diff --git a/php/src/db/pgsql/Pgsql.php b/php/src/db/pgsql/Pgsql.php index 2fef081..b5bf9d8 100644 --- a/php/src/db/pgsql/Pgsql.php +++ b/php/src/db/pgsql/Pgsql.php @@ -21,7 +21,7 @@ class Pgsql implements IDatabase { "dbconn" => $pgsql->dbconn, "options" => $pgsql->options, "config" => $pgsql->config, - "migrate" => $pgsql->migration, + "migration" => $pgsql->migration, ], $params)); } else { return new static($pgsql, $params); @@ -37,14 +37,14 @@ class Pgsql implements IDatabase { const CONFIG = null; - const MIGRATE = null; + const MIGRATION = null; const params_SCHEMA = [ "dbconn" => ["array"], "options" => ["?array|callable"], "replace_config" => ["?array|callable"], "config" => ["?array|callable"], - "migrate" => ["?array|string|callable"], + "migration" => ["?array|string|callable"], "auto_open" => ["bool", true], ]; @@ -95,7 +95,7 @@ class Pgsql implements IDatabase { } $this->config = $config; # migrations - $this->migration = $params["migrate"] ?? static::MIGRATE; + $this->migration = $params["migration"] ?? static::MIGRATION; # $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; if ($params["auto_open"] ?? $defaultAutoOpen) { @@ -183,7 +183,7 @@ class Pgsql implements IDatabase { function exec($query, ?array $params=null) { $db = $this->db(); - $query = new query($query, $params); + $query = new _pgsqlQuery($query, $params); $result = $query->_exec($db); $serialSupport = $this->options["serial_support"] ?? true; if ($serialSupport && $query->isInsert()) return $this->getLastSerial(); @@ -261,7 +261,7 @@ class Pgsql implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new query($query, $params); + $query = new _pgsqlQuery($query, $params); $result = $query->_exec($db); $row = pg_fetch_assoc($result); pg_free_result($result); @@ -277,7 +277,7 @@ class Pgsql implements IDatabase { function all($query, ?array $params=null, $primaryKeys=null): iterable { $db = $this->db(); - $query = new query($query, $params); + $query = new _pgsqlQuery($query, $params); $result = $query->_exec($db); $primaryKeys = cl::withn($primaryKeys); while (($row = pg_fetch_assoc($result)) !== false) { diff --git a/php/src/db/pgsql/PgsqlStorage.php b/php/src/db/pgsql/PgsqlStorage.php index f47078a..419e043 100644 --- a/php/src/db/pgsql/PgsqlStorage.php +++ b/php/src/db/pgsql/PgsqlStorage.php @@ -24,7 +24,7 @@ class PgsqlStorage extends CapacitorStorage { ]; function _getCreateSql(CapacitorChannel $channel): string { - $query = new query($this->_createSql($channel)); + $query = new _pgsqlQuery($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } diff --git a/php/src/db/pgsql/query.php b/php/src/db/pgsql/_pgsqlQuery.php similarity index 96% rename from php/src/db/pgsql/query.php rename to php/src/db/pgsql/_pgsqlQuery.php index 3ab5b6d..34c7ed1 100644 --- a/php/src/db/pgsql/query.php +++ b/php/src/db/pgsql/_pgsqlQuery.php @@ -6,7 +6,7 @@ use nulib\db\_private\_base; use nulib\db\_private\Tbindings; use nulib\output\msg; -class query extends _base { +class _pgsqlQuery extends _base { use Tbindings; const DEBUG_QUERIES = false; diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index 4e22e11..fffb8f1 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -30,7 +30,7 @@ class Sqlite implements IDatabase { "encryption_key" => $sqlite->encryptionKey, "allow_wal" => $sqlite->allowWal, "config" => $sqlite->config, - "migrate" => $sqlite->migration, + "migration" => $sqlite->migration, ], $params)); } elseif (is_array($sqlite)) { return new static(null, cl::merge($sqlite, $params)); @@ -72,7 +72,7 @@ class Sqlite implements IDatabase { const CONFIG = null; - const MIGRATE = null; + const MIGRATION = null; const params_SCHEMA = [ "file" => ["string", ""], @@ -81,7 +81,7 @@ class Sqlite implements IDatabase { "allow_wal" => ["?bool"], "replace_config" => ["?array|callable"], "config" => ["?array|callable"], - "migrate" => ["?array|string|callable"], + "migration" => ["?array|string|callable"], "auto_open" => ["bool", true], ]; @@ -109,7 +109,7 @@ class Sqlite implements IDatabase { } $this->config = $config; # migrations - $this->migration = $params["migrate"] ?? static::MIGRATE; + $this->migration = $params["migration"] ?? static::MIGRATION; # $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; $this->inTransaction = false; @@ -150,7 +150,7 @@ class Sqlite implements IDatabase { 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); + _sqliteMigration::with($this->migration)->migrate($this); $this->inTransaction = false; } return $this; @@ -183,7 +183,7 @@ class Sqlite implements IDatabase { function exec($query, ?array $params=null) { $db = $this->db(); - $query = new query($query, $params); + $query = new _sqliteQuery($query, $params); if ($query->_use_stmt($db, $stmt, $sql)) { try { $result = $stmt->execute(); @@ -270,7 +270,7 @@ class Sqlite implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new query($query, $params); + $query = new _sqliteQuery($query, $params); if ($query->_use_stmt($db, $stmt, $sql)) { try { $result = $this->checkResult($stmt->execute()); @@ -315,7 +315,7 @@ class Sqlite implements IDatabase { function all($query, ?array $params=null, $primaryKeys=null): iterable { $db = $this->db(); - $query = new query($query, $params); + $query = new _sqliteQuery($query, $params); if ($query->_use_stmt($db, $stmt, $sql)) { $result = $this->checkResult($stmt->execute()); return $this->_fetchResult($result, $stmt, $primaryKeys); diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index d304d93..3ba97d6 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -1,6 +1,7 @@ "integer primary key autoincrement", ]; + function _getMigration(CapacitorChannel $channel): _sqliteMigration { + return new _sqliteMigration(cl::merge([ + $this->_createSql($channel), + ], $channel->getMigration()), $channel->getName()); + } + function _getCreateSql(CapacitorChannel $channel): string { - $query = new query($this->_createSql($channel)); + $query = new _sqliteQuery($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } @@ -51,8 +58,7 @@ class SqliteStorage extends CapacitorStorage { # ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un # verrou en écriture $sqlite->exec([ - "create table if not exists", - "table" => "_channels", + "create table if not exists _channels", "cols" => [ "name" => "varchar primary key", "table_name" => "varchar", @@ -64,8 +70,7 @@ class SqliteStorage extends CapacitorStorage { # ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un # verrou en écriture $sqlite->exec([ - "insert", - "into" => "_channels", + "insert into _channels", "values" => [ "name" => $channel->getName(), "table_name" => $channel->getTableName(), @@ -77,9 +82,15 @@ class SqliteStorage extends CapacitorStorage { } protected function _beforeReset(CapacitorChannel $channel): void { - $this->sqlite->exec([ - "delete", - "from" => "_channels", + $sqlite = $this->sqlite; + $sqlite->exec([ + "delete from _migration", + "where" => [ + "channel" => $channel->getName(), + ], + ]); + $sqlite->exec([ + "delete from _channels", "where" => [ "name" => $channel->getName(), ], diff --git a/php/src/db/sqlite/_migration.php b/php/src/db/sqlite/_migration.php deleted file mode 100644 index cafb991..0000000 --- a/php/src/db/sqlite/_migration.php +++ /dev/null @@ -1,54 +0,0 @@ -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::with($migration)->bind($this, true)->invoke([$sqlite, $key]); - } - $sqlite->exec("update _migration set done = 1 where key = :key", [ - "key" => $key, - ]); - } - } - } -} diff --git a/php/src/db/sqlite/_sqliteMigration.php b/php/src/db/sqlite/_sqliteMigration.php new file mode 100644 index 0000000..1fdca37 --- /dev/null +++ b/php/src/db/sqlite/_sqliteMigration.php @@ -0,0 +1,57 @@ +db->exec([ + "create table if not exists _migration", + "cols" => [ + "channel" => "varchar not null", + "key" => "varchar not null", + "done" => "integer not null default 0", + ], + ]); + } + + function beforeMigrate(string $key): bool { + $db = $this->db; + $migrated = $db->get([ + "select 1 from _migration", + "where" => [ + "channel" => $this->channel, + "key" => $key, + "done" => 1, + ], + ]); + if ($migrated) return false; + $db->exec([ + "insert or replace into _migration", + "values" => [ + "channel" => $this->channel, + "key" => $key, + "done" => 0, + ], + ]); + return true; + } + + function afterMigrate(string $key): void { + $this->db->exec([ + "update _migration", + "values" => [ + "done" => 1, + ], + "where" => [ + "channel" => $this->channel, + "key" => $key, + ], + ]); + } +} diff --git a/php/src/db/sqlite/query.php b/php/src/db/sqlite/_sqliteQuery.php similarity index 97% rename from php/src/db/sqlite/query.php rename to php/src/db/sqlite/_sqliteQuery.php index e8df936..14fb1b0 100644 --- a/php/src/db/sqlite/query.php +++ b/php/src/db/sqlite/_sqliteQuery.php @@ -14,7 +14,7 @@ use nulib\ValueException; use SQLite3; use SQLite3Stmt; -class query extends _base { +class _sqliteQuery extends _base { use Tbindings; const DEBUG_QUERIES = false; diff --git a/php/src/php/func.php b/php/src/php/func.php index 1309ec8..26361f7 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -437,7 +437,7 @@ class func { return new ValueException($reason); } - static function with($func, ?array $args=null, bool $strict=true): self { + private static function _with($func, ?array $args=null, bool $strict=true, ?string &$reason=null): ?self { if (!is_array($func)) { if ($func instanceof Closure) { return new self(self::TYPE_CLOSURE, $func, $args); @@ -458,6 +458,12 @@ class func { } elseif (self::verifix_static($func, $strict, $bound, $reason)) { return new self(self::TYPE_STATIC, $func, $args, $bound, $reason); } + return null; + } + + static function with($func, ?array $args=null, bool $strict=true): self { + $func = self::_with($func, $args, $strict, $reason); + if ($func !== null) return $func; throw self::not_a_callable($func, $reason); } @@ -478,6 +484,13 @@ class func { } } + static function is_callable($func): bool { + $func = self::_with($func); + if ($func === null) return false; + if (!$func->isBound()) return false; + return $func->type !== self::TYPE_CLASS; + } + static function call($func, ...$args) { return self::with($func)->invoke($args); } @@ -638,7 +651,7 @@ class func { function bind($object, bool $unlessAlreadyBound=false, bool $replace=false): self { if ($this->type !== self::TYPE_METHOD) return $this; - if ($this->object !== null && $unlessAlreadyBound) return $this; + if ($this->bound && $unlessAlreadyBound) return $this; [$c, $f] = $this->func; if ($replace) { diff --git a/php/tests/db/sqlite/SqliteTest.php b/php/tests/db/sqlite/SqliteTest.php index b56855c..a6e23a1 100644 --- a/php/tests/db/sqlite/SqliteTest.php +++ b/php/tests/db/sqlite/SqliteTest.php @@ -11,7 +11,7 @@ class SqliteTest extends TestCase { function testMigration() { $sqlite = new Sqlite(":memory:", [ - "migrate" => [ + "migration" => [ self::CREATE_PERSON, self::INSERT_JEPHTE, ], @@ -49,7 +49,7 @@ class SqliteTest extends TestCase { } function testInsert() { $sqlite = new Sqlite(":memory:", [ - "migrate" => "create table mapping (i integer, s varchar)", + "migration" => "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"]]); @@ -78,7 +78,7 @@ class SqliteTest extends TestCase { function testSelect() { $sqlite = new Sqlite(":memory:", [ - "migrate" => "create table user (name varchar, amount integer)", + "migration" => "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]]); @@ -130,7 +130,7 @@ class SqliteTest extends TestCase { function testSelectGroupBy() { $sqlite = new Sqlite(":memory:", [ - "migrate" => "create table user (name varchar, amount integer)", + "migration" => "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]]); diff --git a/php/tests/db/sqlite/_queryTest.php b/php/tests/db/sqlite/_queryTest.php index 3337eb5..3d069dd 100644 --- a/php/tests/db/sqlite/_queryTest.php +++ b/php/tests/db/sqlite/_queryTest.php @@ -6,119 +6,119 @@ use PHPUnit\Framework\TestCase; class _queryTest extends TestCase { function testParseConds(): void { $sql = $params = null; - query::parse_conds(null, $sql, $params); + _sqliteQuery::parse_conds(null, $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - query::parse_conds([], $sql, $params); + _sqliteQuery::parse_conds([], $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - query::parse_conds(["col" => null], $sql, $params); + _sqliteQuery::parse_conds(["col" => null], $sql, $params); self::assertSame(["col is null"], $sql); self::assertNull($params); $sql = $params = null; - query::parse_conds(["col = 'value'"], $sql, $params); + _sqliteQuery::parse_conds(["col = 'value'"], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - query::parse_conds([["col = 'value'"]], $sql, $params); + _sqliteQuery::parse_conds([["col = 'value'"]], $sql, $params); self::assertSame(["col = 'value'"], $sql); self::assertNull($params); $sql = $params = null; - query::parse_conds(["int" => 42, "string" => "value"], $sql, $params); + _sqliteQuery::parse_conds(["int" => 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); + _sqliteQuery::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); + _sqliteQuery::parse_conds([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); self::assertSame(["((int = :int and string = :string) and (int = :int2 and string = :string2))"], $sql); self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params); $sql = $params = null; - query::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params); + _sqliteQuery::parse_conds(["int" => ["is null"], "string" => ["<>", "value"]], $sql, $params); self::assertSame(["(int is null and string <> :string)"], $sql); self::assertSame(["string" => "value"], $params); $sql = $params = null; - query::parse_conds(["col" => ["between", "lower", "upper"]], $sql, $params); + _sqliteQuery::parse_conds(["col" => ["between", "lower", "upper"]], $sql, $params); self::assertSame(["col between :col and :col2"], $sql); self::assertSame(["col" => "lower", "col2" => "upper"], $params); $sql = $params = null; - query::parse_conds(["col" => ["in", "one"]], $sql, $params); + _sqliteQuery::parse_conds(["col" => ["in", "one"]], $sql, $params); self::assertSame(["col in (:col)"], $sql); self::assertSame(["col" => "one"], $params); $sql = $params = null; - query::parse_conds(["col" => ["in", ["one", "two"]]], $sql, $params); + _sqliteQuery::parse_conds(["col" => ["in", ["one", "two"]]], $sql, $params); self::assertSame(["col in (:col, :col2)"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - query::parse_conds(["col" => ["=", ["one", "two"]]], $sql, $params); + _sqliteQuery::parse_conds(["col" => ["=", ["one", "two"]]], $sql, $params); self::assertSame(["col = :col and col = :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - query::parse_conds(["or", "col" => ["=", ["one", "two"]]], $sql, $params); + _sqliteQuery::parse_conds(["or", "col" => ["=", ["one", "two"]]], $sql, $params); self::assertSame(["col = :col or col = :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - query::parse_conds(["col" => ["<>", ["one", "two"]]], $sql, $params); + _sqliteQuery::parse_conds(["col" => ["<>", ["one", "two"]]], $sql, $params); self::assertSame(["col <> :col and col <> :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); $sql = $params = null; - query::parse_conds(["or", "col" => ["<>", ["one", "two"]]], $sql, $params); + _sqliteQuery::parse_conds(["or", "col" => ["<>", ["one", "two"]]], $sql, $params); self::assertSame(["col <> :col or col <> :col2"], $sql); self::assertSame(["col" => "one", "col2" => "two"], $params); } function testParseValues(): void { $sql = $params = null; - query::parse_set_values(null, $sql, $params); + _sqliteQuery::parse_set_values(null, $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - query::parse_set_values([], $sql, $params); + _sqliteQuery::parse_set_values([], $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - query::parse_set_values(["col = 'value'"], $sql, $params); + _sqliteQuery::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); + _sqliteQuery::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); + _sqliteQuery::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); + _sqliteQuery::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); + _sqliteQuery::parse_set_values([["int" => 42, "string" => "value"], ["int" => 24, "string" => "eulav"]], $sql, $params); self::assertSame(["int = :int", "string = :string", "int = :int2", "string = :string2"], $sql); self::assertSame(["int" => 42, "string" => "value", "int2" => 24, "string2" => "eulav"], $params); } From 146461a184df5f08f10acc30913e42285f69484f Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Tue, 15 Apr 2025 12:12:03 +0400 Subject: [PATCH 14/32] modifs.mineures sans commentaires --- php/src/db/CapacitorChannel.php | 20 +++++++-- php/src/db/CapacitorStorage.php | 60 +++++++++++++++++++++++-- php/src/db/_private/_create.php | 3 +- php/src/db/_private/_migration.php | 43 ++++++++++++++---- php/src/db/mysql/MysqlStorage.php | 29 +++++++++--- php/src/db/mysql/_mysqlMigration.php | 31 +++++++++++++ php/src/db/mysql/_mysqlQuery.php | 1 + php/src/db/pgsql/PgsqlStorage.php | 50 +++++++-------------- php/src/db/pgsql/_pgsqlMigration.php | 24 ++++++++++ php/src/db/sqlite/SqliteStorage.php | 61 ++++++++------------------ php/src/db/sqlite/_sqliteMigration.php | 44 +++---------------- 11 files changed, 227 insertions(+), 139 deletions(-) create mode 100644 php/src/db/mysql/_mysqlMigration.php create mode 100644 php/src/db/pgsql/_pgsqlMigration.php diff --git a/php/src/db/CapacitorChannel.php b/php/src/db/CapacitorChannel.php index a17e679..dbeebe7 100644 --- a/php/src/db/CapacitorChannel.php +++ b/php/src/db/CapacitorChannel.php @@ -65,15 +65,29 @@ class CapacitorChannel { $this->created = false; $columnDefinitions = cl::withn(static::COLUMN_DEFINITIONS); $primaryKeys = cl::withn(static::PRIMARY_KEYS); - if ($primaryKeys === null && $columnDefinitions !== null) { + $migration = cl::withn(static::MIGRATION); + if ($columnDefinitions !== null) { + # mettre à jour la liste des clés primaires et des migrations $index = 0; foreach ($columnDefinitions as $col => $def) { if ($col === $index) { + # si définition séquentielle, seules les définitions de clé + # primaires sont supportées $index++; if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) { $primaryKeys = preg_split('/\s*,\s*/', trim($ms[1])); } - } else { + } elseif (is_array($def)) { + # tableau: c'est une migration + $def = implode(" ", $def); + if ($def) { + $migration["add_$col"] = "alter table $tableName add column $col $def"; + } else { + $migration["drop_$col"] = "alter table $tableName drop column $col"; + } + } elseif (is_scalar($def)) { + # chaine: c'est une définition + $def = strval($def); if (preg_match('/\bprimary\s+key\b/i', $def)) { $primaryKeys[] = $col; } @@ -82,7 +96,7 @@ class CapacitorChannel { } $this->columnDefinitions = $columnDefinitions; $this->primaryKeys = $primaryKeys; - $this->migration = cl::withn(static::MIGRATION); + $this->migration = $migration; } protected string $name; diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index e2bc4ac..66acb17 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -47,7 +47,7 @@ abstract class CapacitorStorage { "modified_" => "serts", ]; - protected function ColumnDefinitions(CapacitorChannel $channel): array { + protected function ColumnDefinitions(CapacitorChannel $channel, bool $ignoreMigrations=false): array { $definitions = []; if ($channel->getPrimaryKeys() === null) { $definitions[] = static::PRIMARY_KEY_DEFINITION; @@ -69,8 +69,14 @@ abstract class CapacitorStorage { if ($col === $index) { $index++; $constraints[] = $def; - } else { - $definitions[$col] = $def; + } elseif (is_array($def)) { + # éventuellement, ignorer les migrations + $def = implode(" ", $def); + if ($def && !$ignoreMigrations) { + $definitions[$col] = $def; + } + } elseif (is_scalar($def)) { + $definitions[$col] = strval($def); } } return cl::merge($definitions, $constraints); @@ -145,7 +151,7 @@ abstract class CapacitorStorage { return [ "create table if not exists", "table" => $channel->getTableName(), - "cols" => $this->ColumnDefinitions($channel), + "cols" => $this->ColumnDefinitions($channel, true), ]; } @@ -162,7 +168,37 @@ EOT; abstract function _getMigration(CapacitorChannel $channel): _migration; + const CHANNELS_TABLE = "_channels"; + const CHANNELS_COLS = [ + "name" => "varchar primary key", + "table_name" => "varchar", + "class_name" => "varchar", + ]; + + protected function _createChannelsSql(): array { + return [ + "create table if not exists", + "table" => static::CHANNELS_TABLE, + "cols" => static::CHANNELS_COLS, + ]; + } + + protected function _addToChannelsSql(CapacitorChannel $channel): array { + return [ + "insert", + "into" => static::CHANNELS_TABLE, + "values" => [ + "name" => $channel->getName(), + "table_name" => $channel->getTableName(), + "class_name" => get_class($channel), + ], + ]; + } + protected function _afterCreate(CapacitorChannel $channel): void { + $db = $this->db(); + $db->exec($this->_createChannelsSql()); + $db->exec($this->_addToChannelsSql($channel)); } protected function _create(CapacitorChannel $channel): void { @@ -191,6 +227,22 @@ EOT; } protected function _beforeReset(CapacitorChannel $channel): void { + $db = $this->db; + $name = $channel->getName(); + $db->exec([ + "delete", + "from" => _migration::MIGRATION_TABLE, + "where" => [ + "channel" => $name, + ], + ]); + $db->exec([ + "delete", + "from" => static::CHANNELS_TABLE, + "where" => [ + "name" => $name, + ], + ]); } /** supprimer le canal spécifié */ diff --git a/php/src/db/_private/_create.php b/php/src/db/_private/_create.php index 29e83eb..afb90c5 100644 --- a/php/src/db/_private/_create.php +++ b/php/src/db/_private/_create.php @@ -11,9 +11,8 @@ class _create extends _common { ]; 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); + return preg_match("/^create(?:\s+table)?\b/i", $sql); } static function parse(array $query, ?array &$bindings=null): string { diff --git a/php/src/db/_private/_migration.php b/php/src/db/_private/_migration.php index 8ad75a6..5f33acb 100644 --- a/php/src/db/_private/_migration.php +++ b/php/src/db/_private/_migration.php @@ -5,6 +5,14 @@ use nulib\db\IDatabase; use nulib\php\func; abstract class _migration { + const MIGRATION_TABLE = "_migration"; + const MIGRATION_COLS = [ + "channel" => "varchar not null", + "name" => "varchar not null", + "done" => "integer not null default 0", + "primary key (channel, name)", + ]; + const MIGRATION = null; function __construct($migrations, string $channel="", ?IDatabase $db=null) { @@ -25,21 +33,40 @@ abstract class _migration { /** @var callable[]|string[] */ protected $migrations; - abstract function setup(): void; - abstract function beforeMigrate(string $key): bool; - abstract function afterMigrate(string $key): void; + function ensureTable(): void { + $this->db->exec([ + "create table if not exists", + "table" => static::MIGRATION_TABLE, + "cols" => static::MIGRATION_COLS, + ]); + } + + protected function isMigrated(string $name): bool { + return boolval($this->db->get([ + "select 1", + "from" => static::MIGRATION_TABLE, + "where" => [ + "channel" => $this->channel, + "name" => $name, + "done" => 1, + ], + ])); + } + + abstract protected function setMigrated(string $name, bool $done): void; function migrate(?IDatabase $db=null): void { $db = ($this->db ??= $db); - $this->setup(); - foreach ($this->migrations as $key => $migration) { - if (!$this->beforeMigrate($key)) continue; + $this->ensureTable(); + foreach ($this->migrations as $name => $migration) { + if ($this->isMigrated($name)) continue; + $this->setMigrated($name, false); if (is_string($migration) || !func::is_callable($migration)) { $db->exec($migration); } else { - func::with($migration)->bind($this, true)->invoke([$db, $key]); + func::with($migration)->bind($this, true)->invoke([$db, $name]); } - $this->afterMigrate($key); + $this->setMigrated($name, true); } } } diff --git a/php/src/db/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php index ced2c6f..9f23ec4 100644 --- a/php/src/db/mysql/MysqlStorage.php +++ b/php/src/db/mysql/MysqlStorage.php @@ -1,6 +1,7 @@ mysql = Mysql::with($mysql); + $this->db = Mysql::with($mysql); } - protected Mysql $mysql; + protected Mysql $db; function db(): Mysql { - return $this->mysql; + return $this->db; } const PRIMARY_KEY_DEFINITION = [ "id_" => "integer primary key auto_increment", ]; + function _getMigration(CapacitorChannel $channel): _mysqlMigration { + return new _mysqlMigration(cl::merge([ + $this->_createSql($channel), + ], $channel->getMigration()), $channel->getName()); + } + function _getCreateSql(CapacitorChannel $channel): string { $query = new _mysqlQuery($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } + const CHANNELS_COLS = [ + "name" => "varchar(255) primary key", + "table_name" => "varchar(64)", + "class_name" => "varchar(255)", + ]; + + protected function _addToChannelsSql(CapacitorChannel $channel): array { + return cl::merge(parent::_addToChannelsSql($channel), [ + "suffix" => "on duplicate key update name = name", + ]); + } + function _exists(CapacitorChannel $channel): bool { - $mysql = $this->mysql; + $mysql = $this->db; $tableName = $mysql->get([ "select table_name from information_schema.tables", "where" => [ @@ -40,6 +59,6 @@ class MysqlStorage extends CapacitorStorage { } function close(): void { - $this->mysql->close(); + $this->db->close(); } } diff --git a/php/src/db/mysql/_mysqlMigration.php b/php/src/db/mysql/_mysqlMigration.php new file mode 100644 index 0000000..ab107de --- /dev/null +++ b/php/src/db/mysql/_mysqlMigration.php @@ -0,0 +1,31 @@ + "varchar(64) not null", + "name" => "varchar(64) not null", + "done" => "integer not null default 0", + "primary key (channel, name)", + ]; + + static function with($migration): self { + if ($migration instanceof self) return $migration; + else return new static($migration); + } + + protected function setMigrated(string $name, bool $done): void { + $this->db->exec([ + "insert", + "into" => static::MIGRATION_TABLE, + "values" => [ + "channel" => $this->channel, + "name" => $name, + "done" => $done? 1: 0, + ], + "suffix" => "on duplicate key update done = :done", + ]); + } +} diff --git a/php/src/db/mysql/_mysqlQuery.php b/php/src/db/mysql/_mysqlQuery.php index 83cfa1b..b207161 100644 --- a/php/src/db/mysql/_mysqlQuery.php +++ b/php/src/db/mysql/_mysqlQuery.php @@ -4,4 +4,5 @@ namespace nulib\db\mysql; use nulib\db\pdo\_pdoQuery; class _mysqlQuery extends _pdoQuery { + const DEBUG_QUERIES = false; } diff --git a/php/src/db/pgsql/PgsqlStorage.php b/php/src/db/pgsql/PgsqlStorage.php index 419e043..e7560da 100644 --- a/php/src/db/pgsql/PgsqlStorage.php +++ b/php/src/db/pgsql/PgsqlStorage.php @@ -1,6 +1,7 @@ pgsql = Pgsql::with($pgsql); + $this->db = Pgsql::with($pgsql); } - protected Pgsql $pgsql; + protected Pgsql $db; function db(): Pgsql { - return $this->pgsql; + return $this->db; } const PRIMARY_KEY_DEFINITION = [ "id_" => "serial primary key", ]; + function _getMigration(CapacitorChannel $channel): _pgsqlMigration { + return new _pgsqlMigration(cl::merge([ + $this->_createSql($channel), + ], $channel->getMigration()), $channel->getName()); + } + function _getCreateSql(CapacitorChannel $channel): string { $query = new _pgsqlQuery($this->_createSql($channel)); return self::format_sql($channel, $query->getSql()); } - protected function _afterCreate(CapacitorChannel $channel): void { - $db = $this->pgsql; - $db->exec([ - "create table if not exists", - "table" => "_channels", - "cols" => [ - "name" => "varchar primary key", - "table_name" => "varchar", - "class" => "varchar", - ], - ]); - $db->exec([ - "insert", - "into" => "_channels", - "values" => [ - "name" => $channel->getName(), - "table_name" => $channel->getTableName(), - "class" => get_class($channel), - ], - "suffix" => "on conflict (name) do nothing", - ]); - } - - protected function _beforeReset(CapacitorChannel $channel): void { - $this->pgsql->exec([ - "delete", - "from" => "_channels", - "where" => [ - "name" => $channel->getName(), - ], + protected function _addToChannelsSql(CapacitorChannel $channel): array { + return cl::merge(parent::_addToChannelsSql($channel), [ + "suffix" => "on conflict do nothing", ]); } @@ -69,7 +49,7 @@ class PgsqlStorage extends CapacitorStorage { } else { $schemaName = "public"; } - return null !== $this->pgsql->get([ + return null !== $this->db->get([ "select tablename from pg_tables", "where" => [ "schemaname" => $schemaName, @@ -79,6 +59,6 @@ class PgsqlStorage extends CapacitorStorage { } function close(): void { - $this->pgsql->close(); + $this->db->close(); } } diff --git a/php/src/db/pgsql/_pgsqlMigration.php b/php/src/db/pgsql/_pgsqlMigration.php new file mode 100644 index 0000000..2a37c4b --- /dev/null +++ b/php/src/db/pgsql/_pgsqlMigration.php @@ -0,0 +1,24 @@ +db->exec([ + "insert", + "into" => static::MIGRATION_TABLE, + "values" => [ + "channel" => $this->channel, + "name" => $name, + "done" => $done? 1: 0, + ], + "suffix" => "on conflict (channel, name) do update set done = :done", + ]); + } +} diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index 3ba97d6..b63cac9 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -10,13 +10,13 @@ use nulib\db\CapacitorStorage; */ class SqliteStorage extends CapacitorStorage { function __construct($sqlite) { - $this->sqlite = Sqlite::with($sqlite); + $this->db = Sqlite::with($sqlite); } - protected Sqlite $sqlite; + protected Sqlite $db; function db(): Sqlite { - return $this->sqlite; + return $this->db; } const PRIMARY_KEY_DEFINITION = [ @@ -35,7 +35,7 @@ class SqliteStorage extends CapacitorStorage { } function tableExists(string $tableName): bool { - $name = $this->sqlite->get([ + $name = $this->db->get([ # depuis la version 3.33.0 le nom officiel de la table est sqlite_schema, # mais le nom sqlite_master est toujours valable pour le moment "select name from sqlite_master ", @@ -45,63 +45,38 @@ class SqliteStorage extends CapacitorStorage { } function channelExists(string $name): bool { - $name = $this->sqlite->get([ - "select name from _channels", + return null !== $this->db->get([ + "select name", + "from" => static::CHANNELS_TABLE, "where" => ["name" => $name], ]); - return $name !== null; + } + + protected function _addToChannelsSql(CapacitorChannel $channel): array { + return cl::merge(parent::_createChannelsSql(), [ + "suffix" => "on conflict ignore", + ]); } protected function _afterCreate(CapacitorChannel $channel): void { - $sqlite = $this->sqlite; - if (!$this->tableExists("_channels")) { + $db = $this->db; + if (!$this->tableExists(static::CHANNELS_TABLE)) { # ne pas créer si la table existe déjà, pour éviter d'avoir besoin d'un # verrou en écriture - $sqlite->exec([ - "create table if not exists _channels", - "cols" => [ - "name" => "varchar primary key", - "table_name" => "varchar", - "class" => "varchar", - ], - ]); + $db->exec($this->_createChannelsSql()); } if (!$this->channelExists($channel->getName())) { # ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un # verrou en écriture - $sqlite->exec([ - "insert into _channels", - "values" => [ - "name" => $channel->getName(), - "table_name" => $channel->getTableName(), - "class" => get_class($channel), - ], - "suffix" => "on conflict do nothing", - ]); + $this->_addToChannelsSql($channel); } } - protected function _beforeReset(CapacitorChannel $channel): void { - $sqlite = $this->sqlite; - $sqlite->exec([ - "delete from _migration", - "where" => [ - "channel" => $channel->getName(), - ], - ]); - $sqlite->exec([ - "delete from _channels", - "where" => [ - "name" => $channel->getName(), - ], - ]); - } - function _exists(CapacitorChannel $channel): bool { return $this->tableExists($channel->getTableName()); } function close(): void { - $this->sqlite->close(); + $this->db->close(); } } diff --git a/php/src/db/sqlite/_sqliteMigration.php b/php/src/db/sqlite/_sqliteMigration.php index 1fdca37..182c71c 100644 --- a/php/src/db/sqlite/_sqliteMigration.php +++ b/php/src/db/sqlite/_sqliteMigration.php @@ -9,48 +9,14 @@ class _sqliteMigration extends _migration { else return new static($migration); } - function setup(): void { + protected function setMigrated(string $name, bool $done): void { $this->db->exec([ - "create table if not exists _migration", - "cols" => [ - "channel" => "varchar not null", - "key" => "varchar not null", - "done" => "integer not null default 0", - ], - ]); - } - - function beforeMigrate(string $key): bool { - $db = $this->db; - $migrated = $db->get([ - "select 1 from _migration", - "where" => [ - "channel" => $this->channel, - "key" => $key, - "done" => 1, - ], - ]); - if ($migrated) return false; - $db->exec([ - "insert or replace into _migration", + "insert or replace", + "into" => static::MIGRATION_TABLE, "values" => [ "channel" => $this->channel, - "key" => $key, - "done" => 0, - ], - ]); - return true; - } - - function afterMigrate(string $key): void { - $this->db->exec([ - "update _migration", - "values" => [ - "done" => 1, - ], - "where" => [ - "channel" => $this->channel, - "key" => $key, + "name" => $name, + "done" => $done? 1: 0, ], ]); } From 2a50167241fe6b6d84d793aad748eb74c631eae9 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Tue, 15 Apr 2025 12:20:21 +0400 Subject: [PATCH 15/32] modifs.mineures sans commentaires --- php/src/db/_private/_migration.php | 22 +++++++++++----------- php/src/db/mysql/_mysqlMigration.php | 10 +++++----- php/src/db/pgsql/PgsqlStorage.php | 2 +- php/src/db/sqlite/SqliteStorage.php | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/php/src/db/_private/_migration.php b/php/src/db/_private/_migration.php index 5f33acb..761addf 100644 --- a/php/src/db/_private/_migration.php +++ b/php/src/db/_private/_migration.php @@ -5,14 +5,6 @@ use nulib\db\IDatabase; use nulib\php\func; abstract class _migration { - const MIGRATION_TABLE = "_migration"; - const MIGRATION_COLS = [ - "channel" => "varchar not null", - "name" => "varchar not null", - "done" => "integer not null default 0", - "primary key (channel, name)", - ]; - const MIGRATION = null; function __construct($migrations, string $channel="", ?IDatabase $db=null) { @@ -30,10 +22,15 @@ abstract class _migration { protected string $channel; - /** @var callable[]|string[] */ - protected $migrations; + const MIGRATION_TABLE = "_migration"; + const MIGRATION_COLS = [ + "channel" => "varchar not null", + "name" => "varchar not null", + "done" => "integer not null default 0", + "primary key (channel, name)", + ]; - function ensureTable(): void { + protected function ensureTable(): void { $this->db->exec([ "create table if not exists", "table" => static::MIGRATION_TABLE, @@ -55,6 +52,9 @@ abstract class _migration { abstract protected function setMigrated(string $name, bool $done): void; + /** @var callable[]|string[] */ + protected $migrations; + function migrate(?IDatabase $db=null): void { $db = ($this->db ??= $db); $this->ensureTable(); diff --git a/php/src/db/mysql/_mysqlMigration.php b/php/src/db/mysql/_mysqlMigration.php index ab107de..2d63db1 100644 --- a/php/src/db/mysql/_mysqlMigration.php +++ b/php/src/db/mysql/_mysqlMigration.php @@ -4,6 +4,11 @@ namespace nulib\db\mysql; use nulib\db\_private\_migration; class _mysqlMigration extends _migration { + static function with($migration): self { + if ($migration instanceof self) return $migration; + else return new static($migration); + } + const MIGRATION_COLS = [ "channel" => "varchar(64) not null", "name" => "varchar(64) not null", @@ -11,11 +16,6 @@ class _mysqlMigration extends _migration { "primary key (channel, name)", ]; - static function with($migration): self { - if ($migration instanceof self) return $migration; - else return new static($migration); - } - protected function setMigrated(string $name, bool $done): void { $this->db->exec([ "insert", diff --git a/php/src/db/pgsql/PgsqlStorage.php b/php/src/db/pgsql/PgsqlStorage.php index e7560da..d9ec58a 100644 --- a/php/src/db/pgsql/PgsqlStorage.php +++ b/php/src/db/pgsql/PgsqlStorage.php @@ -37,7 +37,7 @@ class PgsqlStorage extends CapacitorStorage { protected function _addToChannelsSql(CapacitorChannel $channel): array { return cl::merge(parent::_addToChannelsSql($channel), [ - "suffix" => "on conflict do nothing", + "suffix" => "on conflict (name) do nothing", ]); } diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index b63cac9..1f09520 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -53,7 +53,7 @@ class SqliteStorage extends CapacitorStorage { } protected function _addToChannelsSql(CapacitorChannel $channel): array { - return cl::merge(parent::_createChannelsSql(), [ + return cl::merge(parent::_addToChannelsSql($channel), [ "suffix" => "on conflict ignore", ]); } From ca129dfda4355c3452ecc96bf91e0a8c9659d8f8 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Tue, 15 Apr 2025 12:28:02 +0400 Subject: [PATCH 16/32] modifs.mineures sans commentaires --- composer.lock | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.lock b/composer.lock index 9fcac33..e19b980 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "266a079e97f3ceecc2cc0a84d6b9743b", + "content-hash": "a8b9dc80255663640bda855729ef2d47", "packages": [ { "name": "symfony/deprecation-contracts", @@ -2022,6 +2022,8 @@ "ext-posix": "*", "ext-pcntl": "*", "ext-curl": "*", + "ext-pdo": "*", + "ext-pgsql": "*", "ext-sqlite3": "*" }, "plugin-api-version": "2.2.0" From d4cc8bfa42ab55a74d89682764330b7ba6185bae Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Wed, 16 Apr 2025 12:19:05 +0400 Subject: [PATCH 17/32] config pman composer --- bin/pman | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/bin/pman b/bin/pman index d4b91a9..8fda195 100755 --- a/bin/pman +++ b/bin/pman @@ -91,6 +91,42 @@ function init_config_action() { _push_branches } +function _init_composer() { + if [ ! -f .composer.pman.yml -o -n "$ForceCreate" ]; then + ac_set_tmpfile config + cat >"$config" < Date: Wed, 16 Apr 2025 12:19:21 +0400 Subject: [PATCH 18/32] modifs.mineures sans commentaires --- bin/pman | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/pman b/bin/pman index 8fda195..169b187 100755 --- a/bin/pman +++ b/bin/pman @@ -116,12 +116,12 @@ EOF function init_composer_action() { local -a push_branches; local config - [ -f .composer.pman.yml -a -z "$ForceCreate" ] && die "La configuration composer pman a déjà été initialisée" + [ -f .composer.pman.yml -a -z "$ForceCreate" ] && die "La configuration pman composer a déjà été initialisée" resolve_should_push _init_composer || exit_with ewarn "Initialisation de la configuration annulée" - git commit -m "configuration composer pman" + git commit -m "configuration pman composer" push_branches+=("$CurrentBranch") _push_branches From 8ee13a85c0fe9da4a433bc328045f7d98e6cc247 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Wed, 16 Apr 2025 12:26:30 +0400 Subject: [PATCH 19/32] modifs.mineures sans commentaires --- bin/pman | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/pman b/bin/pman index 169b187..0648670 100755 --- a/bin/pman +++ b/bin/pman @@ -98,10 +98,10 @@ function _init_composer() { # -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8 composer: - profiles: [ develop, master ] - develop: + profiles: [ dev, dist ] + dev: link: true - master: + dist: link: false EOF "${EDITOR:-nano}" "$config" From 5e141b575e419f8a9fcabb46cbd7df112e7759af Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Wed, 16 Apr 2025 14:44:04 +0400 Subject: [PATCH 20/32] =?UTF-8?q?pman:=20ajout=20des=20cl=C3=A9s=20match?= =?UTF-8?q?=5Frequire=20et=20match=5Frequire-dev?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/pman | 6 ++++ php/src/db/sqlite/SqliteStorage.php | 2 +- php/src/str.php | 15 ++++++++++ php/src/tools/pman/ComposerFile.php | 2 +- php/src/tools/pman/ComposerPmanFile.php | 37 ++++++++++++++++++++++++- 5 files changed, 59 insertions(+), 3 deletions(-) diff --git a/bin/pman b/bin/pman index 0648670..27c6c57 100755 --- a/bin/pman +++ b/bin/pman @@ -98,11 +98,17 @@ function _init_composer() { # -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8 composer: + match_prefix: + match_prefix-dev: profiles: [ dev, dist ] dev: link: true + require: + reqire-dev: dist: link: false + require: + reqire-dev: EOF "${EDITOR:-nano}" "$config" [ -s "$config" ] || return 1 diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index 1f09520..9d52cc9 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -68,7 +68,7 @@ class SqliteStorage extends CapacitorStorage { if (!$this->channelExists($channel->getName())) { # ne pas insérer si la ligne existe déjà, pour éviter d'avoir besoin d'un # verrou en écriture - $this->_addToChannelsSql($channel); + $db->exec($this->_addToChannelsSql($channel)); } } diff --git a/php/src/str.php b/php/src/str.php index 9a54876..b0f3497 100644 --- a/php/src/str.php +++ b/php/src/str.php @@ -242,6 +242,21 @@ class str { return true; } + /** + * vérifier si $s a le préfixe $prefix + * - si $prefix commence par /, c'est une expression régulière, et elle doit + * matcher $s + * - sinon $s doit commencer par la chaine $prefix + */ + static final function match_prefix(?string $s, ?string $prefix): bool { + if ($s === null || $prefix === null) return false; + if (substr($prefix, 0, 1) === "/") { + return preg_match($prefix, $s); + } else { + return self::_starts_with($prefix, $s); + } + } + /** * ajouter $sep$prefix$text$suffix à $s si $text est non vide * diff --git a/php/src/tools/pman/ComposerFile.php b/php/src/tools/pman/ComposerFile.php index 1cb8ed0..c3dd7a1 100644 --- a/php/src/tools/pman/ComposerFile.php +++ b/php/src/tools/pman/ComposerFile.php @@ -78,7 +78,7 @@ class ComposerFile { ]; function selectProfile(string $profile, ComposerPmanFile $config): void { - $config = $config->getProfileConfig($profile); + $config = $config->getProfileConfig($profile, $this->getRequires(), $this->getRequireDevs()); // corriger les liens $deps = cl::merge(array_keys($config["require"]), array_keys($config["require-dev"])); $paths = []; diff --git a/php/src/tools/pman/ComposerPmanFile.php b/php/src/tools/pman/ComposerPmanFile.php index b0520c0..d8c6474 100644 --- a/php/src/tools/pman/ComposerPmanFile.php +++ b/php/src/tools/pman/ComposerPmanFile.php @@ -4,6 +4,7 @@ namespace nulib\tools\pman; use nulib\A; use nulib\ext\yaml; use nulib\os\path; +use nulib\str; use nulib\ValueException; class ComposerPmanFile { @@ -49,6 +50,8 @@ class ComposerPmanFile { $composer =& $data["composer"]; A::ensure_array($composer); A::ensure_array($composer["profiles"]); + A::ensure_array($composer["match_require"]); + A::ensure_array($composer["match_require-dev"]); foreach ($composer["profiles"] as $profileName) { $profile =& $composer[$profileName]; A::ensure_array($profile); @@ -61,11 +64,43 @@ class ComposerPmanFile { return $this->data; } - function getProfileConfig(string $profile): array { + function getProfileConfig(string $profile, ?array $composerRequires=null, ?array $composerRequireDevs=null): array { $config = $this->data["composer"][$profile] ?? null; if ($config === null) { throw new ValueException("$profile: profil invalide"); } + if ($composerRequires !== null) { + $matchRequires = $this->data["composer"]["match_require"]; + foreach ($composerRequires as $dep => $version) { + $found = false; + foreach ($matchRequires as $matchRequire) { + if (str::match_prefix($dep, $matchRequire)) { + $found = true; + break; + } + } + $require = $config["require"][$dep] ?? null; + if ($found && $require === null) { + $config["require"][$dep] = $version; + } + } + } + if ($composerRequireDevs !== null) { + $matchRequireDevs = $this->data["composer"]["match_require-dev"]; + foreach ($composerRequireDevs as $dep => $version) { + $found = false; + foreach ($matchRequireDevs as $matchRequireDev) { + if (str::match_prefix($dep, $matchRequireDev)) { + $found = true; + break; + } + } + $requireDev = $config["require-dev"][$dep] ?? null; + if ($found && $requireDev === null) { + $config["require"][$dep] = $version; + } + } + } return $config; } From 0e9be5f221c88caf27ad311c16e9773fb8d7ffad Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Wed, 16 Apr 2025 17:13:16 +0400 Subject: [PATCH 21/32] bug --- bin/pman | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/pman b/bin/pman index 27c6c57..80a1021 100755 --- a/bin/pman +++ b/bin/pman @@ -150,7 +150,7 @@ $MAIN: une branche du même nom existe dans l'origine function _ensure_develop_branch() { [ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie" - [ -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" + [ "$1" == init -o -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" } function init_develop_action() { @@ -161,7 +161,7 @@ function init_develop_action() { $DEVELOP: une branche du même nom existe dans l'origine git checkout $DEVELOP" _ensure_main_branch - _ensure_develop_branch + _ensure_develop_branch init resolve_should_push @@ -179,7 +179,7 @@ $DEVELOP: une branche du même nom existe dans l'origine function _ensure_upstream_branch() { [ -n "$UPSTREAM" ] || die "La branche UPSTREAM n'a pas été définie" - [ -n "$UpstreamBranch" ] || die "$UPSTREAM: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" + [ "$1" == init -o -n "$UpstreamBranch" ] || die "$UPSTREAM: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" } function init_upstream_action() { @@ -190,7 +190,7 @@ function init_upstream_action() { $UPSTREAM: une branche du même nom existe dans l'origine git checkout $UPSTREAM" _ensure_develop_branch - _ensure_upstream_branch + _ensure_upstream_branch init resolve_should_push @@ -224,7 +224,7 @@ $UPSTREAM: une branche du même nom existe dans l'origine function _ensure_dist_branch() { [ -n "$DIST" ] || die "La branche DIST n'a pas été définie" - [ -n "$DistBranch" ] || die "$DIST: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" + [ "$1" == init -o -n "$DistBranch" ] || die "$DIST: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" } function init_dist_action() { @@ -235,7 +235,7 @@ function init_dist_action() { $DIST: une branche du même nom existe dans l'origine git checkout $DIST" _ensure_main_branch - _ensure_dist_branch + _ensure_dist_branch init resolve_should_push From 91beea9c298a9181303c16ce6f62ebee42289183 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Wed, 16 Apr 2025 19:22:47 +0400 Subject: [PATCH 22/32] modifs.mineures sans commentaires --- php/src/db/sqlite/SqliteStorage.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index 9d52cc9..9850406 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -53,9 +53,9 @@ class SqliteStorage extends CapacitorStorage { } protected function _addToChannelsSql(CapacitorChannel $channel): array { - return cl::merge(parent::_addToChannelsSql($channel), [ - "suffix" => "on conflict ignore", - ]); + $sql = parent::_addToChannelsSql($channel); + $sql[0] = "insert or ignore"; + return $sql; } protected function _afterCreate(CapacitorChannel $channel): void { From d63a0cb704cf0102fb19731b66167901c243b4b8 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Fri, 18 Apr 2025 21:33:30 +0400 Subject: [PATCH 23/32] modifs.mineures sans commentaires --- php/src/ref/schema/ref_schema.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/php/src/ref/schema/ref_schema.php b/php/src/ref/schema/ref_schema.php index 60392c8..2114eb8 100644 --- a/php/src/ref/schema/ref_schema.php +++ b/php/src/ref/schema/ref_schema.php @@ -26,6 +26,8 @@ class ref_schema { "messages" => ["?array", null, "messages à afficher en cas d'erreur d'analyse"], "formatter_func" => ["?callable", null, "fonction qui formatte la valeur pour affichage"], "format" => [null, null, "format à utiliser pour l'affichage"], + "size" => ["?int", null, "nom de caractères ou de chiffres de la valeur"], + "precision" => ["?int", null, "nombre de chiffres après la virgule pour une valeur numérique flottante"], "" => ["array", ["scalar"], "nature du schéma", "schema" => self::NATURE_METASCHEMA, ], From b4e28ade023eec9399683a4818fa468a9d0ff4ad Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Sat, 19 Apr 2025 09:06:26 +0400 Subject: [PATCH 24/32] modifs.mineures sans commentaires --- php/src/ref/schema/ref_schema.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/php/src/ref/schema/ref_schema.php b/php/src/ref/schema/ref_schema.php index 2114eb8..37bea48 100644 --- a/php/src/ref/schema/ref_schema.php +++ b/php/src/ref/schema/ref_schema.php @@ -39,11 +39,11 @@ class ref_schema { ]; const MESSAGES = [ - "missing" => "Vous devez spécifier cette valeur", - "unavailable" => "Vous devez spécifier cette valeur", - "null" => "Cette valeur ne doit pas être nulle", - "empty" => "Cette valeur ne doit pas être vide", - "invalid" => "Cette valeur est invalide", + "missing" => "vous devez spécifier cette valeur", + "unavailable" => "vous devez spécifier cette valeur", + "null" => "cette valeur ne doit pas être nulle", + "empty" => "cette valeur ne doit pas être vide", + "invalid" => "cette valeur est invalide", ]; const PARAMS_SCHEMA = [ From b6cc62e010a95c2a9039c457f8042e2996111f00 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Wed, 23 Apr 2025 11:50:02 +0400 Subject: [PATCH 25/32] =?UTF-8?q?ajouter=20les=20m=C3=A9thodes=20d=C3=A9l?= =?UTF-8?q?=C3=A9gu=C3=A9es=20pour=20Capacitor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- php/src/db/CapacitorChannel.php | 42 ++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/php/src/db/CapacitorChannel.php b/php/src/db/CapacitorChannel.php index dbeebe7..b0649ae 100644 --- a/php/src/db/CapacitorChannel.php +++ b/php/src/db/CapacitorChannel.php @@ -8,7 +8,7 @@ use Traversable; /** * Class CapacitorChannel: un canal d'une instance de {@link ICapacitor} */ -class CapacitorChannel { +class CapacitorChannel implements ITransactor { const NAME = null; const TABLE_NAME = null; @@ -397,6 +397,42 @@ class CapacitorChannel { return $this; } + function willUpdate(...$transactors): ITransactor { + return $this->capacitor->willUpdate(...$transactors); + } + + function inTransaction(): bool { + return $this->capacitor->inTransaction(); + } + + function beginTransaction(?callable $func=null, bool $commit=true): void { + $this->capacitor->beginTransaction($func, $commit); + } + + function commit(): void { + $this->capacitor->commit(); + } + + function rollback(): void { + $this->capacitor->rollback(); + } + + function db(): IDatabase { + return $this->capacitor->getStorage()->db(); + } + + function exists(): bool { + return $this->capacitor->exists(); + } + + function ensureExists(): void { + $this->capacitor->ensureExists(); + } + + function reset(bool $recreate=false): void { + $this->capacitor->reset($recreate); + } + function charge($item, $func=null, ?array $args=null, ?array &$values=null): int { return $this->capacitor->charge($item, $func, $args, $values); } @@ -424,4 +460,8 @@ class CapacitorChannel { function delete($filter, $func=null, ?array $args=null): int { return $this->capacitor->delete($filter, $func, $args); } + + function close(): void { + $this->capacitor->close(); + } } From cae38dae95f9a726b8795010d436b1140c5e36db Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Mon, 28 Apr 2025 04:43:01 +0400 Subject: [PATCH 26/32] auto-migration des canaux --- php/src/db/CapacitorStorage.php | 40 ++++++++++++++++++++++++++++- php/src/db/_private/_generic.php | 11 +++++--- php/src/db/mysql/MysqlStorage.php | 24 ++++++++--------- php/src/db/pgsql/PgsqlStorage.php | 34 ++++++++++++------------ php/src/db/sqlite/SqliteStorage.php | 24 ++++++++--------- 5 files changed, 86 insertions(+), 47 deletions(-) diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index 66acb17..01412d4 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -166,6 +166,41 @@ $sql; EOT; } + abstract protected function tableExists(string $tableName): bool; + + const METADATA_TABLE = "_metadata"; + const METADATA_COLS = [ + "name" => "varchar not null primary key", + "value" => "varchar", + ]; + + protected function _prepareMetadata(): void { + if (!$this->tableExists(self::METADATA_TABLE)) { + $db = $this->db(); + $db->exec([ + "drop table", + "table" => self::CHANNELS_TABLE, + ]); + $db->exec([ + "drop table", + "table" => _migration::MIGRATION_TABLE + ]); + $db->exec([ + "create table", + "table" => self::METADATA_TABLE, + "cols" => self::METADATA_COLS, + ]); + $db->exec([ + "insert", + "into" => self::METADATA_TABLE, + "values" => [ + "name" => "version", + "value" => "1", + ], + ]); + } + } + abstract function _getMigration(CapacitorChannel $channel): _migration; const CHANNELS_TABLE = "_channels"; @@ -204,6 +239,7 @@ EOT; protected function _create(CapacitorChannel $channel): void { $channel->ensureSetup(); if (!$channel->isCreated()) { + $this->_prepareMetadata(); $this->_getMigration($channel)->migrate($this->db()); $this->_afterCreate($channel); $channel->setCreated(); @@ -211,7 +247,9 @@ EOT; } /** tester si le canal spécifié existe */ - abstract function _exists(CapacitorChannel $channel): bool; + function _exists(CapacitorChannel $channel): bool { + return $this->tableExists($channel->getTableName()); + } function exists(?string $channel): bool { return $this->_exists($this->getChannel($channel)); diff --git a/php/src/db/_private/_generic.php b/php/src/db/_private/_generic.php index a2ec549..dea829c 100644 --- a/php/src/db/_private/_generic.php +++ b/php/src/db/_private/_generic.php @@ -2,6 +2,7 @@ namespace nulib\db\_private; use nulib\cl; +use nulib\str; use nulib\ValueException; class _generic extends _common { @@ -13,9 +14,13 @@ class _generic extends _common { } static function parse(array $query, ?array &$bindings=null): string { - if (!cl::is_list($query)) { - throw new ValueException("Seuls les tableaux séquentiels sont supportés"); + $sql = ""; + foreach ($query as $value) { + if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) { + $sql .= " "; + } + $sql .= $value; } - return self::merge_seq($query); + return $sql; } } diff --git a/php/src/db/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php index 9f23ec4..de4e3ff 100644 --- a/php/src/db/mysql/MysqlStorage.php +++ b/php/src/db/mysql/MysqlStorage.php @@ -23,6 +23,18 @@ class MysqlStorage extends CapacitorStorage { "id_" => "integer primary key auto_increment", ]; + protected function tableExists(string $tableName): bool { + $db = $this->db; + $found = $db->get([ + "select table_name from information_schema.tables", + "where" => [ + "table_schema" => $db->getDbname(), + "table_name" => $tableName, + ], + ]); + return $found !== null; + } + function _getMigration(CapacitorChannel $channel): _mysqlMigration { return new _mysqlMigration(cl::merge([ $this->_createSql($channel), @@ -46,18 +58,6 @@ class MysqlStorage extends CapacitorStorage { ]); } - function _exists(CapacitorChannel $channel): bool { - $mysql = $this->db; - $tableName = $mysql->get([ - "select table_name from information_schema.tables", - "where" => [ - "table_schema" => $mysql->getDbname(), - "table_name" => $channel->getTableName(), - ], - ]); - return $tableName !== null; - } - function close(): void { $this->db->close(); } diff --git a/php/src/db/pgsql/PgsqlStorage.php b/php/src/db/pgsql/PgsqlStorage.php index d9ec58a..0dc93b7 100644 --- a/php/src/db/pgsql/PgsqlStorage.php +++ b/php/src/db/pgsql/PgsqlStorage.php @@ -24,6 +24,23 @@ class PgsqlStorage extends CapacitorStorage { "id_" => "serial primary key", ]; + protected function tableExists(string $tableName): bool { + if (($index = strpos($tableName, ".")) !== false) { + $schemaName = substr($tableName, 0, $index); + $tableName = substr($tableName, $index + 1); + } else { + $schemaName = "public"; + } + $found = $this->db->get([ + "select tablename from pg_tables", + "where" => [ + "schemaname" => $schemaName, + "tablename" => $tableName, + ], + ]); + return $found !== null; + } + function _getMigration(CapacitorChannel $channel): _pgsqlMigration { return new _pgsqlMigration(cl::merge([ $this->_createSql($channel), @@ -41,23 +58,6 @@ class PgsqlStorage extends CapacitorStorage { ]); } - function _exists(CapacitorChannel $channel): bool { - $tableName = $channel->getTableName(); - if (($index = strpos($tableName, ".")) !== false) { - $schemaName = substr($tableName, 0, $index); - $tableName = substr($tableName, $index + 1); - } else { - $schemaName = "public"; - } - return null !== $this->db->get([ - "select tablename from pg_tables", - "where" => [ - "schemaname" => $schemaName, - "tablename" => $tableName, - ], - ]); - } - function close(): void { $this->db->close(); } diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index 9850406..f031a56 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -23,6 +23,16 @@ class SqliteStorage extends CapacitorStorage { "id_" => "integer primary key autoincrement", ]; + protected function tableExists(string $tableName): bool { + $found = $this->db->get([ + # depuis la version 3.33.0 le nom officiel de la table est sqlite_schema, + # mais le nom sqlite_master est toujours valable pour le moment + "select name from sqlite_master ", + "where" => ["name" => $tableName], + ]); + return $found !== null; + } + function _getMigration(CapacitorChannel $channel): _sqliteMigration { return new _sqliteMigration(cl::merge([ $this->_createSql($channel), @@ -34,16 +44,6 @@ class SqliteStorage extends CapacitorStorage { return self::format_sql($channel, $query->getSql()); } - function tableExists(string $tableName): bool { - $name = $this->db->get([ - # depuis la version 3.33.0 le nom officiel de la table est sqlite_schema, - # mais le nom sqlite_master est toujours valable pour le moment - "select name from sqlite_master ", - "where" => ["name" => $tableName], - ]); - return $name !== null; - } - function channelExists(string $name): bool { return null !== $this->db->get([ "select name", @@ -72,10 +72,6 @@ class SqliteStorage extends CapacitorStorage { } } - function _exists(CapacitorChannel $channel): bool { - return $this->tableExists($channel->getTableName()); - } - function close(): void { $this->db->close(); } From 299b90c85e1a4077f4dabdb3147fbf0991f7ab87 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Mon, 28 Apr 2025 05:06:17 +0400 Subject: [PATCH 27/32] modifs.mineures sans commentaires --- php/src/db/CapacitorStorage.php | 14 +++++++------- php/src/db/_private/_select.php | 2 +- php/src/db/mysql/MysqlStorage.php | 7 ++++++- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index 01412d4..2801fca 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -175,24 +175,24 @@ EOT; ]; protected function _prepareMetadata(): void { - if (!$this->tableExists(self::METADATA_TABLE)) { + if (!$this->tableExists(static::METADATA_TABLE)) { $db = $this->db(); $db->exec([ - "drop table", + "drop table if exists", "table" => self::CHANNELS_TABLE, ]); $db->exec([ - "drop table", + "drop table if exists", "table" => _migration::MIGRATION_TABLE ]); $db->exec([ "create table", - "table" => self::METADATA_TABLE, - "cols" => self::METADATA_COLS, + "table" => static::METADATA_TABLE, + "cols" => static::METADATA_COLS, ]); $db->exec([ "insert", - "into" => self::METADATA_TABLE, + "into" => static::METADATA_TABLE, "values" => [ "name" => "version", "value" => "1", @@ -205,7 +205,7 @@ EOT; const CHANNELS_TABLE = "_channels"; const CHANNELS_COLS = [ - "name" => "varchar primary key", + "name" => "varchar not null primary key", "table_name" => "varchar", "class_name" => "varchar", ]; diff --git a/php/src/db/_private/_select.php b/php/src/db/_private/_select.php index ea343f1..80b0460 100644 --- a/php/src/db/_private/_select.php +++ b/php/src/db/_private/_select.php @@ -83,7 +83,7 @@ class _select extends _common { ## from $from = $query["from"] ?? null; - if (self::consume('from\s+([a-z_][a-z0-9_]*)\s*(?=;?\s*$|\bwhere\b)', $tmpsql, $ms)) { + 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; diff --git a/php/src/db/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php index de4e3ff..4f27f1a 100644 --- a/php/src/db/mysql/MysqlStorage.php +++ b/php/src/db/mysql/MysqlStorage.php @@ -35,6 +35,11 @@ class MysqlStorage extends CapacitorStorage { return $found !== null; } + const METADATA_COLS = [ + "name" => "varchar(64) not null primary key", + "value" => "varchar(255)", + ]; + function _getMigration(CapacitorChannel $channel): _mysqlMigration { return new _mysqlMigration(cl::merge([ $this->_createSql($channel), @@ -47,7 +52,7 @@ class MysqlStorage extends CapacitorStorage { } const CHANNELS_COLS = [ - "name" => "varchar(255) primary key", + "name" => "varchar(255) not null primary key", "table_name" => "varchar(64)", "class_name" => "varchar(255)", ]; From 9767028da6af103bf4d9622608df4377e885a30e Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Mon, 28 Apr 2025 05:43:34 +0400 Subject: [PATCH 28/32] =?UTF-8?q?pas=20de=20rebind=20par=20d=C3=A9faut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- php/src/db/CapacitorStorage.php | 6 +++--- php/src/db/_private/_config.php | 2 +- php/src/db/_private/_migration.php | 2 +- php/src/db/pdo/Pdo.php | 2 +- php/src/db/pgsql/Pgsql.php | 2 +- php/src/file/tab/AbstractBuilder.php | 2 +- php/src/php/content/c.php | 2 +- php/src/php/func.php | 4 ++-- php/tests/php/funcTest.php | 18 ++++++++++++++---- 9 files changed, 25 insertions(+), 15 deletions(-) diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index 2801fca..1773198 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -388,7 +388,7 @@ EOT; if ($func !== null) { $updates = func::with($func) ->prependArgs([$item, $values, $pvalues]) - ->bind($channel, true) + ->bind($channel) ->invoke(); if ($updates === [false]) return 0; if (is_array($updates) && $updates) { @@ -603,7 +603,7 @@ EOT; function _each(CapacitorChannel $channel, $filter, $func, ?array $args, ?array $mergeQuery=null, ?int &$nbUpdated=null): int { $this->_create($channel); if ($func === null) $func = CapacitorChannel::onEach; - $onEach = func::with($func)->bind($channel, true); + $onEach = func::with($func)->bind($channel); $db = $this->db(); # si on est déjà dans une transaction, désactiver la gestion des transactions $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction(); @@ -671,7 +671,7 @@ EOT; function _delete(CapacitorChannel $channel, $filter, $func, ?array $args): int { $this->_create($channel); if ($func === null) $func = CapacitorChannel::onDelete; - $onEach = func::with($func)->bind($channel, true); + $onEach = func::with($func)->bind($channel); $db = $this->db(); # si on est déjà dans une transaction, désactiver la gestion des transactions $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction(); diff --git a/php/src/db/_private/_config.php b/php/src/db/_private/_config.php index e72b526..4a9ab06 100644 --- a/php/src/db/_private/_config.php +++ b/php/src/db/_private/_config.php @@ -29,7 +29,7 @@ class _config { if (is_string($config) && !func::is_method($config)) { $db->exec($config); } else { - func::with($config)->bind($this, true)->invoke([$db, $key]); + func::with($config)->bind($this)->invoke([$db, $key]); } } } diff --git a/php/src/db/_private/_migration.php b/php/src/db/_private/_migration.php index 761addf..f11cbb8 100644 --- a/php/src/db/_private/_migration.php +++ b/php/src/db/_private/_migration.php @@ -64,7 +64,7 @@ abstract class _migration { if (is_string($migration) || !func::is_callable($migration)) { $db->exec($migration); } else { - func::with($migration)->bind($this, true)->invoke([$db, $name]); + func::with($migration)->bind($this)->invoke([$db, $name]); } $this->setMigrated($name, true); } diff --git a/php/src/db/pdo/Pdo.php b/php/src/db/pdo/Pdo.php index 2a970fa..78e38db 100644 --- a/php/src/db/pdo/Pdo.php +++ b/php/src/db/pdo/Pdo.php @@ -120,7 +120,7 @@ class Pdo implements IDatabase { $dbconn = $this->dbconn; $options = $this->options; if (is_callable($options)) { - $options = func::with($options)->bind($this, true)->invoke(); + $options = func::with($options)->bind($this)->invoke(); } $this->db = new \PDO($dbconn["name"], $dbconn["user"], $dbconn["pass"], $options); _config::with($this->config)->configure($this); diff --git a/php/src/db/pgsql/Pgsql.php b/php/src/db/pgsql/Pgsql.php index b5bf9d8..34d82dc 100644 --- a/php/src/db/pgsql/Pgsql.php +++ b/php/src/db/pgsql/Pgsql.php @@ -136,7 +136,7 @@ class Pgsql implements IDatabase { $connection_string = implode(" ", array_filter($connection_string)); $options = $this->options; if (is_callable($options)) { - $options = func::with($options)->bind($this, true)->invoke(); + $options = func::with($options)->bind($this)->invoke(); } $forceNew = $options["force_new"] ?? false; $flags = $forceNew? PGSQL_CONNECT_FORCE_NEW: 0; diff --git a/php/src/file/tab/AbstractBuilder.php b/php/src/file/tab/AbstractBuilder.php index 2affcc8..3ec767d 100644 --- a/php/src/file/tab/AbstractBuilder.php +++ b/php/src/file/tab/AbstractBuilder.php @@ -35,7 +35,7 @@ abstract class AbstractBuilder extends TempStream implements IBuilder { $this->rows = $rows; $this->index = 0; $cookFunc = $params["cook_func"] ?? null; - if ($cookFunc !== null) $cookFunc = func::with($cookFunc)->bind($this, true); + if ($cookFunc !== null) $cookFunc = func::with($cookFunc)->bind($this); $this->cookFunc = $cookFunc; $this->output = $params["output"] ?? static::OUTPUT; $maxMemory = $params["max_memory"] ?? null; diff --git a/php/src/php/content/c.php b/php/src/php/content/c.php index 4105846..ebf4c52 100644 --- a/php/src/php/content/c.php +++ b/php/src/php/content/c.php @@ -82,7 +82,7 @@ class c { $arg = self::resolve($arg, $object_or_class, false); if (!$array) $arg = $arg[0]; }; unset($arg); - $value = func::with($func, $args)->bind($object_or_class, true)->invoke(); + $value = func::with($func, $args)->bind($object_or_class)->invoke(); } } if ($seq) $dest[] = $value; diff --git a/php/src/php/func.php b/php/src/php/func.php index 26361f7..90a1cff 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -649,9 +649,9 @@ class func { else return $this->bound && $this->object !== null; } - function bind($object, bool $unlessAlreadyBound=false, bool $replace=false): self { + function bind($object, bool $rebind=false, bool $replace=false): self { if ($this->type !== self::TYPE_METHOD) return $this; - if ($this->bound && $unlessAlreadyBound) return $this; + if (!$rebind && $this->isBound()) return $this; [$c, $f] = $this->func; if ($replace) { diff --git a/php/tests/php/funcTest.php b/php/tests/php/funcTest.php index 45a0581..22fa882 100644 --- a/php/tests/php/funcTest.php +++ b/php/tests/php/funcTest.php @@ -1102,15 +1102,25 @@ namespace nulib\php { } function testRebind() { + # bind if not already bound + $func = func::with([C1::class, "tmethod"]); + // bind + self::assertSame(11, $func->bind(new C1(0))->invoke()); + // pas de bind, puis que déjà bound + self::assertSame(11, $func->bind(new C1(1))->invoke()); + // même si l'objet est de type différent, pas de bind + self::assertSame(11, $func->bind(new C0())->invoke()); + + # force rebind $func = func::with([C1::class, "tmethod"]); // objets du même type - self::assertSame(11, $func->bind(new C1(0))->invoke()); - self::assertSame(12, $func->bind(new C1(1))->invoke()); + self::assertSame(11, $func->bind(new C1(0), true)->invoke()); + self::assertSame(12, $func->bind(new C1(1), true)->invoke()); // objets de type différent self::assertException(ValueException::class, function() use ($func) { - $func->bind(new C0())->invoke(); + $func->bind(new C0(), true)->invoke(); }); - self::assertSame(11, $func->bind(new C0(), false, true)->invoke()); + self::assertSame(11, $func->bind(new C0(), true, true)->invoke()); } function testModifyArgs() { From 37354525ecf3bf0469b00626c61f324bcf4ec880 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Mon, 28 Apr 2025 09:21:47 +0400 Subject: [PATCH 29/32] =?UTF-8?q?am=C3=A9liorer=20le=20support=20des=20mig?= =?UTF-8?q?ration=20dans=20les=20canaux?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- php/src/db/Capacitor.php | 6 ++ php/src/db/CapacitorChannel.php | 45 ++++++++---- php/src/db/CapacitorStorage.php | 56 ++++++++------- php/src/db/IDatabase.php | 3 + php/src/db/_private/_base.php | 2 - php/src/db/_private/_generic.php | 2 - php/src/db/_private/_migration.php | 33 ++++++++- php/src/db/mysql/MysqlStorage.php | 12 ++-- php/src/db/pdo/Pdo.php | 6 +- php/src/db/pgsql/Pgsql.php | 5 ++ php/src/db/pgsql/PgsqlException.php | 1 - php/src/db/pgsql/PgsqlStorage.php | 12 ++-- php/src/db/sqlite/Sqlite.php | 5 ++ php/src/db/sqlite/SqliteStorage.php | 12 ++-- php/src/db/sqlite/_sqliteQuery.php | 7 -- php/src/php/func.php | 5 +- php/tests/db/sqlite/ChannelMigrationTest.php | 75 ++++++++++++++++++++ php/tests/db/sqlite/impl/MyChannel.php | 33 +++++++++ php/tests/db/sqlite/impl/MyChannelV2.php | 14 ++++ php/tests/db/sqlite/impl/MyChannelV3.php | 17 +++++ php/tests/php/funcTest.php | 2 +- 21 files changed, 274 insertions(+), 79 deletions(-) create mode 100644 php/tests/db/sqlite/ChannelMigrationTest.php create mode 100644 php/tests/db/sqlite/impl/MyChannel.php create mode 100644 php/tests/db/sqlite/impl/MyChannelV2.php create mode 100644 php/tests/db/sqlite/impl/MyChannelV3.php diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php index aa5da52..5528a68 100644 --- a/php/src/db/Capacitor.php +++ b/php/src/db/Capacitor.php @@ -39,6 +39,12 @@ class Capacitor implements ITransactor { return $this->getChannel()->getTableName(); } + function getCreateSql(): string { + $storage = $this->storage; + $channel = $this->channel; + return $storage->_getMigration($channel)->getSql(get_class($channel), $this->db()); + } + /** @var CapacitorChannel[] */ protected ?array $subChannels = null; diff --git a/php/src/db/CapacitorChannel.php b/php/src/db/CapacitorChannel.php index b0649ae..c7265bb 100644 --- a/php/src/db/CapacitorChannel.php +++ b/php/src/db/CapacitorChannel.php @@ -66,26 +66,47 @@ class CapacitorChannel implements ITransactor { $columnDefinitions = cl::withn(static::COLUMN_DEFINITIONS); $primaryKeys = cl::withn(static::PRIMARY_KEYS); $migration = cl::withn(static::MIGRATION); + $lastMkey = 1; if ($columnDefinitions !== null) { # mettre à jour la liste des clés primaires et des migrations $index = 0; foreach ($columnDefinitions as $col => $def) { if ($col === $index) { - # si définition séquentielle, seules les définitions de clé - # primaires sont supportées $index++; - if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) { - $primaryKeys = preg_split('/\s*,\s*/', trim($ms[1])); - } - } elseif (is_array($def)) { - # tableau: c'est une migration - $def = implode(" ", $def); - if ($def) { - $migration["add_$col"] = "alter table $tableName add column $col $def"; + if (is_array($def)) { + # tableau: c'est une migration + $mkey = null; + $mvalues = null; + $mdefs = $def; + $mindex = 0; + foreach ($mdefs as $mcol => $mdef) { + if ($mindex === 0 && $mcol === 0) { + $mindex++; + $mkey = $mdef; + } elseif ($mcol === $mindex) { + # si définition séquentielle, prendre la migration telle quelle + $mindex++; + $mvalues[] = $mdef; + } elseif ($mdef) { + # mise à jour d'une colonne + $mvalues[] = "alter table $tableName add column $mcol $mdef"; + } else { + # suppression d'une colonne + $mvalues[] = "alter table $tableName drop column $mcol"; + } + } + if ($mvalues !== null) { + if ($mkey === null) $mkey = $lastMkey++; + $migration[$mkey] = $mvalues; + } } else { - $migration["drop_$col"] = "alter table $tableName drop column $col"; + # si définition séquentielle, seules les définitions de clé + # primaires sont supportées + if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) { + $primaryKeys = preg_split('/\s*,\s*/', trim($ms[1])); + } } - } elseif (is_scalar($def)) { + } else { # chaine: c'est une définition $def = strval($def); if (preg_match('/\bprimary\s+key\b/i', $def)) { diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index 1773198..e7ff6af 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -40,6 +40,16 @@ abstract class CapacitorStorage { const SERSUM_DEFINITION = "varchar(40)"; const SERTS_DEFINITION = "datetime"; + protected static function sercol($def): string { + if (!is_string($def)) $def = strval($def); + switch ($def) { + case "serdata": $def = static::SERDATA_DEFINITION; break; + case "sersum": $def = static::SERSUM_DEFINITION; break; + case "serts": $def = static::SERTS_DEFINITION; break; + } + return $def; + } + const COLUMN_DEFINITIONS = [ "item__" => "serdata", "item__sum_" => "sersum", @@ -61,22 +71,29 @@ abstract class CapacitorStorage { $constraints = []; $index = 0; foreach ($tmp as $col => $def) { - switch ($def) { - case "serdata": $def = static::SERDATA_DEFINITION; break; - case "sersum": $def = static::SERSUM_DEFINITION; break; - case "serts": $def = static::SERTS_DEFINITION; break; - } if ($col === $index) { $index++; - $constraints[] = $def; - } elseif (is_array($def)) { - # éventuellement, ignorer les migrations - $def = implode(" ", $def); - if ($def && !$ignoreMigrations) { - $definitions[$col] = $def; + if (is_array($def)) { + if (!$ignoreMigrations) { + $mdefs = $def; + $mindex = 0; + foreach ($mdefs as $mcol => $mdef) { + if ($mcol === $mindex) { + $mindex++; + } else { + if ($mdef) { + $definitions[$mcol] = self::sercol($mdef); + } else { + unset($definitions[$mcol]); + } + } + } + } + } else { + $constraints[] = $def; } - } elseif (is_scalar($def)) { - $definitions[$col] = strval($def); + } else { + $definitions[$col] = self::sercol($def); } } return cl::merge($definitions, $constraints); @@ -155,17 +172,6 @@ abstract class CapacitorStorage { ]; } - protected static function format_sql(CapacitorChannel $channel, string $sql): string { - $class = get_class($channel); - return <<exec([ "drop table if exists", - "table" => _migration::MIGRATION_TABLE + "table" => _migration::MIGRATION_TABLE, ]); $db->exec([ "create table", diff --git a/php/src/db/IDatabase.php b/php/src/db/IDatabase.php index dc71720..1383c9a 100644 --- a/php/src/db/IDatabase.php +++ b/php/src/db/IDatabase.php @@ -2,6 +2,9 @@ namespace nulib\db; interface IDatabase extends ITransactor { + /** obtenir la requête SQL correspondant à $query */ + function getSql($query, ?array $params=null): string; + /** * - si c'est un insert, retourner l'identifiant autogénéré de la ligne * - sinon retourner le nombre de lignes modifiées en cas de succès, ou false diff --git a/php/src/db/_private/_base.php b/php/src/db/_private/_base.php index 5e366e6..2ca42f9 100644 --- a/php/src/db/_private/_base.php +++ b/php/src/db/_private/_base.php @@ -1,8 +1,6 @@ migrations as $name => $migration) { if ($this->isMigrated($name)) continue; $this->setMigrated($name, false); - if (is_string($migration) || !func::is_callable($migration)) { - $db->exec($migration); - } else { + if (func::is_callable($migration)) { func::with($migration)->bind($this)->invoke([$db, $name]); + } else { + foreach (cl::with($migration) as $query) { + $db->exec($query); + } } $this->setMigrated($name, true); } } + + protected static function sql_prefix(?string $source=null): string { + $prefix = "-- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8\n"; + if ($source !== null) $prefix .= "-- autogénéré à partir de $source\n"; + return $prefix; + } + + function getSql(?string $source=null, ?IDatabase $db=null): string { + $db = ($this->db ??= $db); + $lines = [self::sql_prefix($source)]; + foreach ($this->migrations as $name => $migration) { + $lines[] = "-- $name"; + if (func::is_callable($migration)) { + $lines[] = "-- "; + } else { + foreach (cl::with($migration) as $query) { + $sql = $db->getSql($query); + $lines[] = "$sql;"; + } + } + $lines[] = ""; + } + return implode("\n", $lines); + } } diff --git a/php/src/db/mysql/MysqlStorage.php b/php/src/db/mysql/MysqlStorage.php index 4f27f1a..ace8beb 100644 --- a/php/src/db/mysql/MysqlStorage.php +++ b/php/src/db/mysql/MysqlStorage.php @@ -41,14 +41,10 @@ class MysqlStorage extends CapacitorStorage { ]; function _getMigration(CapacitorChannel $channel): _mysqlMigration { - return new _mysqlMigration(cl::merge([ - $this->_createSql($channel), - ], $channel->getMigration()), $channel->getName()); - } - - function _getCreateSql(CapacitorChannel $channel): string { - $query = new _mysqlQuery($this->_createSql($channel)); - return self::format_sql($channel, $query->getSql()); + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _mysqlMigration($migrations, $channel->getName()); } const CHANNELS_COLS = [ diff --git a/php/src/db/pdo/Pdo.php b/php/src/db/pdo/Pdo.php index 78e38db..f34ff69 100644 --- a/php/src/db/pdo/Pdo.php +++ b/php/src/db/pdo/Pdo.php @@ -1,7 +1,6 @@ getSql(); + } + function open(): self { if ($this->db === null) { $dbconn = $this->dbconn; diff --git a/php/src/db/pgsql/Pgsql.php b/php/src/db/pgsql/Pgsql.php index 34d82dc..dbcbedf 100644 --- a/php/src/db/pgsql/Pgsql.php +++ b/php/src/db/pgsql/Pgsql.php @@ -117,6 +117,11 @@ class Pgsql implements IDatabase { /** @var resource */ protected $db = null; + function getSql($query, ?array $params=null): string { + $query = new _pgsqlQuery($query, $params); + return $query->getSql(); + } + function open(): self { if ($this->db === null) { $dbconn = $this->dbconn; diff --git a/php/src/db/pgsql/PgsqlException.php b/php/src/db/pgsql/PgsqlException.php index afd21c4..f9a500e 100644 --- a/php/src/db/pgsql/PgsqlException.php +++ b/php/src/db/pgsql/PgsqlException.php @@ -3,7 +3,6 @@ namespace nulib\db\pgsql; use Exception; use RuntimeException; -use SQLite3; class PgsqlException extends RuntimeException { static final function last_error($db): self { diff --git a/php/src/db/pgsql/PgsqlStorage.php b/php/src/db/pgsql/PgsqlStorage.php index 0dc93b7..dd89e2a 100644 --- a/php/src/db/pgsql/PgsqlStorage.php +++ b/php/src/db/pgsql/PgsqlStorage.php @@ -42,14 +42,10 @@ class PgsqlStorage extends CapacitorStorage { } function _getMigration(CapacitorChannel $channel): _pgsqlMigration { - return new _pgsqlMigration(cl::merge([ - $this->_createSql($channel), - ], $channel->getMigration()), $channel->getName()); - } - - function _getCreateSql(CapacitorChannel $channel): string { - $query = new _pgsqlQuery($this->_createSql($channel)); - return self::format_sql($channel, $query->getSql()); + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _pgsqlMigration($migrations, $channel->getName()); } protected function _addToChannelsSql(CapacitorChannel $channel): array { diff --git a/php/src/db/sqlite/Sqlite.php b/php/src/db/sqlite/Sqlite.php index fffb8f1..ae4ea99 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -146,6 +146,11 @@ class Sqlite implements IDatabase { protected bool $inTransaction; + function getSql($query, ?array $params=null): string { + $query = new _sqliteQuery($query, $params); + return $query->getSql(); + } + function open(): self { if ($this->db === null) { $this->db = new SQLite3($this->file, $this->flags, $this->encryptionKey); diff --git a/php/src/db/sqlite/SqliteStorage.php b/php/src/db/sqlite/SqliteStorage.php index f031a56..4621cb2 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -34,14 +34,10 @@ class SqliteStorage extends CapacitorStorage { } function _getMigration(CapacitorChannel $channel): _sqliteMigration { - return new _sqliteMigration(cl::merge([ - $this->_createSql($channel), - ], $channel->getMigration()), $channel->getName()); - } - - function _getCreateSql(CapacitorChannel $channel): string { - $query = new _sqliteQuery($this->_createSql($channel)); - return self::format_sql($channel, $query->getSql()); + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _sqliteMigration($migrations, $channel->getName()); } function channelExists(string $name): bool { diff --git a/php/src/db/sqlite/_sqliteQuery.php b/php/src/db/sqlite/_sqliteQuery.php index 14fb1b0..04e6f1c 100644 --- a/php/src/db/sqlite/_sqliteQuery.php +++ b/php/src/db/sqlite/_sqliteQuery.php @@ -2,15 +2,8 @@ namespace nulib\db\sqlite; use nulib\db\_private\_base; -use nulib\db\_private\_create; -use nulib\db\_private\_delete; -use nulib\db\_private\_generic; -use nulib\db\_private\_insert; -use nulib\db\_private\_select; -use nulib\db\_private\_update; use nulib\db\_private\Tbindings; use nulib\output\msg; -use nulib\ValueException; use SQLite3; use SQLite3Stmt; diff --git a/php/src/php/func.php b/php/src/php/func.php index 90a1cff..675d359 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -3,7 +3,6 @@ namespace nulib\php; use Closure; use Exception; -use Generator; use nulib\A; use nulib\cl; use nulib\cv; @@ -268,6 +267,8 @@ class func { $reason = "$c::$f: method not found"; return false; } + $method = new ReflectionMethod($c, $f); + if (!$method->isStatic()) return false; } else { $reason = "$c::$f: not bound"; } @@ -402,6 +403,8 @@ class func { $reason = "$c::$f: method not found"; return false; } + $method = new ReflectionMethod($c, $f); + if ($method->isStatic()) return false; } else { $reason = "$c::$f: not bound"; } diff --git a/php/tests/db/sqlite/ChannelMigrationTest.php b/php/tests/db/sqlite/ChannelMigrationTest.php new file mode 100644 index 0000000..1946179 --- /dev/null +++ b/php/tests/db/sqlite/ChannelMigrationTest.php @@ -0,0 +1,75 @@ +charge([ + "name" => $name, + "value" => $value, + "date_cre" => $dateCre, + "date_mod" => $dateMod, + "age" => $age, + ]); + } + } + + function testMigration() { + $storage = new SqliteStorage(__DIR__.'/capacitor.db'); + $data = [ + ["first", "premier", new DateTime(), new DateTime(), 15], + ["second", "deuxieme", new DateTime(), new DateTime(), 15], + ]; + + new Capacitor($storage, $channel = new MyChannel()); + $channel->reset(true); + $this->addData($channel, $data); + + new Capacitor($storage, $channel = new MyChannelV2()); + $this->addData($channel, $data); + + new Capacitor($storage, $channel = new MyChannelV3()); + $this->addData($channel, $data); + + $sql = $channel->getCapacitor()->getCreateSql(); + $class = MyChannelV3::class; + $expected = << "varchar not null primary key", + "value" => "varchar", + ]; + + const VERSION = 1; + + function __construct() { + parent::__construct(); + $this->version = static::VERSION; + } + + protected int $version; + + function getItemValues($item): ?array { + + return [ + "name" => "{$item["name"]}$this->version", + "value" => "{$item["value"]} v$this->version", + "date_cre" => $item["date_cre"] ?? null, + "date_mod" => $item["date_mod"] ?? null, + "age" => $item["age"] ?? null, + ]; + } +} diff --git a/php/tests/db/sqlite/impl/MyChannelV2.php b/php/tests/db/sqlite/impl/MyChannelV2.php new file mode 100644 index 0000000..68f18bf --- /dev/null +++ b/php/tests/db/sqlite/impl/MyChannelV2.php @@ -0,0 +1,14 @@ + "varchar", + "value" => "varchar", + ["dates", + "date_cre" => "datetime", + "date_mod" => "datetime", + ], + ]; +} diff --git a/php/tests/db/sqlite/impl/MyChannelV3.php b/php/tests/db/sqlite/impl/MyChannelV3.php new file mode 100644 index 0000000..e6e2170 --- /dev/null +++ b/php/tests/db/sqlite/impl/MyChannelV3.php @@ -0,0 +1,17 @@ + "varchar", + "value" => "varchar", + ["dates", + "date_cre" => "datetime", + "date_mod" => "datetime", + ], + ["infos", + "age" => "integer", + ], + ]; +} diff --git a/php/tests/php/funcTest.php b/php/tests/php/funcTest.php index 22fa882..f15ce27 100644 --- a/php/tests/php/funcTest.php +++ b/php/tests/php/funcTest.php @@ -562,7 +562,7 @@ namespace nulib\php { true, true, [SC::class, "tstatic"], ], [[SC::class, "tmethod"], - true, true, [SC::class, "tmethod"], + false, true, [SC::class, "tmethod"], true, true, [SC::class, "tmethod"], ], [[SC::class, "->tmethod"], From 737e9d17b62463c6b27657adc1013bd66296c9dd Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Mon, 28 Apr 2025 09:23:05 +0400 Subject: [PATCH 30/32] modifs.mineures sans commentaires --- php/src/db/Capacitor.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/php/src/db/Capacitor.php b/php/src/db/Capacitor.php index 5528a68..0fee8b5 100644 --- a/php/src/db/Capacitor.php +++ b/php/src/db/Capacitor.php @@ -40,9 +40,8 @@ class Capacitor implements ITransactor { } function getCreateSql(): string { - $storage = $this->storage; $channel = $this->channel; - return $storage->_getMigration($channel)->getSql(get_class($channel), $this->db()); + return $this->storage->_getMigration($channel)->getSql(get_class($channel), $this->db()); } /** @var CapacitorChannel[] */ From 3ee92ef338bdaa74dd5bcc993d8123bffa657089 Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Tue, 29 Apr 2025 07:37:15 +0400 Subject: [PATCH 31/32] ajout str::replace --- php/src/str.php | 15 +++++++++++++++ php/tests/strTest.php | 19 +++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/php/src/str.php b/php/src/str.php index b0f3497..347726b 100644 --- a/php/src/str.php +++ b/php/src/str.php @@ -268,6 +268,21 @@ class str { $s .= $prefix.$text.$suffix; } + /** + * dans $s, faire les remplacements $key => $value du tableau $replaces + * + * si $verifix_order, le tableau est réordonné par taille de chaine source + */ + static final function replace(?string $s, ?array $replaces, bool $verifix_order=true): ?string { + if ($s === null || $replaces === null) return $s; + if ($verifix_order) { + uksort($replaces, function ($a, $b) { + return -cv::compare(strlen($a), strlen($b)); + }); + } + return str_replace(array_keys($replaces), array_values($replaces), $s); + } + /** si $text est non vide, ajouter $prefix$text$suffix à $s en séparant la valeur avec un espace */ static final function add(?string &$s, ?string $text, ?string $prefix=null, ?string $suffix=null): void { self::addsep($s, " ", $text, $prefix, $suffix); diff --git a/php/tests/strTest.php b/php/tests/strTest.php index 2d69303..c03997e 100644 --- a/php/tests/strTest.php +++ b/php/tests/strTest.php @@ -5,7 +5,22 @@ namespace nulib; use nulib\tests\TestCase; class strTest extends TestCase { - function testSplit_tokens() { + function test_replace() { + self::assertSame("premier deuxieme", str::replace("first second", [ + "first" => "premier", + "second" => "deuxieme", + ])); + self::assertSame("avant OK", str::replace("prefix prefixsuffix", [ + "prefix" => "avant", + "prefixsuffix" => "OK", + ])); + self::assertSame("avant avantsuffix", str::replace("prefix prefixsuffix", [ + "prefix" => "avant", + "prefixsuffix" => "OK", + ], false)); + } + + function test_split_tokens() { self::assertNull(str::split_tokens(null)); self::assertSame([], str::split_tokens("")); self::assertSame(["token"], str::split_tokens("token")); @@ -13,7 +28,7 @@ class strTest extends TestCase { self::assertSame(["t", "u", "v", "w"], str::split_tokens("\nt\n\nu\r\nv\rw")); } - function testCamel2us() { + function test_camel2us() { self::assertSame("a", str::camel2us("a")); self::assertSame("aa", str::camel2us("aa")); self::assertSame("aaa", str::camel2us("aaa")); From 045f97eb960902856f6ab084c9f86e1398a76bbd Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Wed, 30 Apr 2025 04:32:10 +0400 Subject: [PATCH 32/32] Init changelog & version 0.5.0p74 --- CHANGES.md | 16 ++++++++++++++++ VERSION.txt | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index f054030..b4fd91d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,19 @@ +## Release 0.5.0p74 du 30/04/2025-04:31 + +* `3ee92ef` ajout str::replace +* `3735452` améliorer le support des migrations dans les canaux +* `9767028` pas de rebind par défaut +* `cae38da` auto-migration des canaux +* `b6cc62e` ajouter les méthodes déléguées pour Capacitor +* `5e141b5` pman: ajout des clés match_require et match_require-dev +* `d4cc8bf` config pman composer +* `d241ce6` ajout PgsqlStorage +* `5c6d55e` maj ordre func +* `bab9ba8` début pgsql +* `ecd0177` migration de nur_func à func +* `bd1f901` réorganiser le code de génération sql +* `1536e09` améliorations func + ## Release 0.4.1p82 du 25/03/2025-08:47 ## Release 0.4.1p74 du 25/03/2025-08:47 diff --git a/VERSION.txt b/VERSION.txt index 267577d..8f0916f 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -0.4.1 +0.5.0