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/CHANGES.md b/CHANGES.md index 60676ac..b4fd91d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,21 @@ +## 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 * `5beb5e6` corriger la prise en compte du proxy 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 diff --git a/bin/pman b/bin/pman index d4b91a9..80a1021 100755 --- a/bin/pman +++ b/bin/pman @@ -91,6 +91,48 @@ function init_config_action() { _push_branches } +function _init_composer() { + if [ ! -f .composer.pman.yml -o -n "$ForceCreate" ]; then + ac_set_tmpfile config + cat >"$config" < $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..0fee8b5 100644 --- a/php/src/db/Capacitor.php +++ b/php/src/db/Capacitor.php @@ -1,7 +1,7 @@ getChannel()->getTableName(); } + function getCreateSql(): string { + $channel = $this->channel; + return $this->storage->_getMigration($channel)->getSql(get_class($channel), $this->db()); + } + /** @var CapacitorChannel[] */ protected ?array $subChannels = null; @@ -87,7 +92,7 @@ class Capacitor implements ITransactor { if ($func !== null) { $commited = false; try { - nur_func::call($func, $this); + func::call($func, $this); if ($commit) { $this->commit(); $commited = true; @@ -120,10 +125,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 4495074..c7265bb 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; @@ -17,6 +17,8 @@ class CapacitorChannel { const PRIMARY_KEYS = null; + const MIGRATION = null; + const MANAGE_TRANSACTIONS = true; const EACH_COMMIT_THRESHOLD = 100; @@ -63,15 +65,50 @@ 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); + $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) { $index++; - if (preg_match('/\bprimary\s+key\s+\((.+)\)/i', $def, $ms)) { - $primaryKeys = preg_split('/\s*,\s*/', trim($ms[1])); + 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 { + # 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])); + } } } else { + # chaine: c'est une définition + $def = strval($def); if (preg_match('/\bprimary\s+key\b/i', $def)) { $primaryKeys[] = $col; } @@ -80,6 +117,7 @@ class CapacitorChannel { } $this->columnDefinitions = $columnDefinitions; $this->primaryKeys = $primaryKeys; + $this->migration = $migration; } protected string $name; @@ -192,6 +230,12 @@ class CapacitorChannel { return $this->columnDefinitions; } + protected ?array $migration; + + function getMigration(): ?array { + return $this->migration; + } + protected ?array $primaryKeys; function getPrimaryKeys(): ?array { @@ -245,9 +289,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; @@ -377,6 +418,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); } @@ -404,4 +481,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(); + } } diff --git a/php/src/db/CapacitorStorage.php b/php/src/db/CapacitorStorage.php index ec27fae..e7ff6af 100644 --- a/php/src/db/CapacitorStorage.php +++ b/php/src/db/CapacitorStorage.php @@ -2,8 +2,9 @@ namespace nulib\db; use nulib\cl; +use nulib\db\_private\_migration; use nulib\db\cache\cache; -use nulib\php\nur_func; +use nulib\php\func; use nulib\ValueException; use Traversable; @@ -35,14 +36,28 @@ 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"; + + 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__" => 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 { + protected function ColumnDefinitions(CapacitorChannel $channel, bool $ignoreMigrations=false): array { $definitions = []; if ($channel->getPrimaryKeys() === null) { $definitions[] = static::PRIMARY_KEY_DEFINITION; @@ -58,14 +73,36 @@ abstract class CapacitorStorage { foreach ($tmp as $col => $def) { if ($col === $index) { $index++; - $constraints[] = $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; + } } else { - $definitions[$col] = $def; + $definitions[$col] = self::sercol($def); } } 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; @@ -128,46 +165,97 @@ 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, true), ]; } - protected static function format_sql(CapacitorChannel $channel, string $sql): string { - $class = get_class($channel); - return << "varchar not null primary key", + "value" => "varchar", + ]; -EOT; + protected function _prepareMetadata(): void { + if (!$this->tableExists(static::METADATA_TABLE)) { + $db = $this->db(); + $db->exec([ + "drop table if exists", + "table" => self::CHANNELS_TABLE, + ]); + $db->exec([ + "drop table if exists", + "table" => _migration::MIGRATION_TABLE, + ]); + $db->exec([ + "create table", + "table" => static::METADATA_TABLE, + "cols" => static::METADATA_COLS, + ]); + $db->exec([ + "insert", + "into" => static::METADATA_TABLE, + "values" => [ + "name" => "version", + "value" => "1", + ], + ]); + } } - abstract function _getCreateSql(CapacitorChannel $channel): string; + abstract function _getMigration(CapacitorChannel $channel): _migration; - /** obtenir la requête SQL utilisée pour créer la table */ - function getCreateSql(?string $channel): string { - return $this->_getCreateSql($this->getChannel($channel)); + const CHANNELS_TABLE = "_channels"; + const CHANNELS_COLS = [ + "name" => "varchar not null 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 { $channel->ensureSetup(); if (!$channel->isCreated()) { - $this->db->exec($this->_createSql($channel)); + $this->_prepareMetadata(); + $this->_getMigration($channel)->migrate($this->db()); $this->_afterCreate($channel); $channel->setCreated(); } } /** 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)); @@ -183,12 +271,28 @@ 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é */ function _reset(CapacitorChannel $channel, bool $recreate=false): void { $this->_beforeReset($channel); - $this->db->exec([ + $this->db()->exec([ "drop table if exists", $channel->getTableName(), ]); @@ -230,10 +334,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 +360,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 +375,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([$item, $values, $pvalues])->invoke(); if ($updates === [false]) return 0; if (is_array($updates) && $updates) { if ($insert === null) $insert = false; @@ -295,8 +392,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([$item, $values, $pvalues]) + ->bind($channel) + ->invoke(); if ($updates === [false]) return 0; if (is_array($updates) && $updates) { if ($insert === null) $insert = false; @@ -510,8 +609,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); $db = $this->db(); # si on est déjà dans une transaction, désactiver la gestion des transactions $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction(); @@ -528,7 +626,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 +677,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); $db = $this->db(); # si on est déjà dans une transaction, désactiver la gestion des transactions $manageTransactions = $channel->isManageTransactions() && !$db->inTransaction(); @@ -596,7 +693,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/IDatabase.php b/php/src/db/IDatabase.php index 497e5be..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 @@ -15,5 +18,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/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/_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; - } - - 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); +abstract class _base extends _common { + 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 { - ## 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); + throw ValueException::invalid_kind($sql, "query"); } - } - 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 { + 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 { - ## 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); + $meta = ["isa" => "generic", "type" => null]; } } - $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"); - } + static function with($sql, ?array $params=null): array { + static::verifix($sql, $params); + return [$sql, $params]; } - 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/pdo/_config.php b/php/src/db/_private/_config.php similarity index 69% rename from php/src/db/pdo/_config.php rename to php/src/db/_private/_config.php index 5055d6f..4a9ab06 100644 --- a/php/src/db/pdo/_config.php +++ b/php/src/db/_private/_config.php @@ -1,7 +1,8 @@ configs as $key => $config) { - if (is_string($config) && !nur_func::is_method($config)) { - $pdo->exec($config); + if (is_string($config) && !func::is_method($config)) { + $db->exec($config); } else { - nur_func::ensure_func($config, $this, $args); - nur_func::call($config, $pdo, $key, ...$args); + func::with($config)->bind($this)->invoke([$db, $key]); } } } diff --git a/php/src/db/_private/_create.php b/php/src/db/_private/_create.php index 64c29a5..afb90c5 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,46 @@ class _create { "cols" => "?array", "suffix" => "?string", ]; + + static function isa(string $sql): bool { + #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 + $tmpsql = self::merge_seq($query); + self::consume('create(?:\s+table)?\b', $tmpsql); + $sql = ["create table"]; + if ($tmpsql) $sql[] = $tmpsql; + + ## préfixe + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; + + ## table + $table = $query["table"] ?? null; + if ($table !== null) $sql[] = $table; + + ## columns + $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 + $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 e79ec34..55409d9 100644 --- a/php/src/db/_private/_delete.php +++ b/php/src/db/_private/_delete.php @@ -1,11 +1,48 @@ "?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"]; + if ($tmpsql) $sql[] = $tmpsql; + + ## préfixe + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; + + ## table + $from = $query["from"] ?? null; + if ($from !== null) $sql[] = $from; + + ## where + $where = $query["where"] ?? null; + if ($where !== null) { + self::parse_conds($where, $wheresql, $bindings); + if ($wheresql) { + $sql[] = "where"; + $sql[] = implode(" and ", $wheresql); + } + } + + ## suffixe + $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 97d4b51..d71e157 100644 --- a/php/src/db/_private/_generic.php +++ b/php/src/db/_private/_generic.php @@ -1,7 +1,24 @@ "?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/_migration.php b/php/src/db/_private/_migration.php new file mode 100644 index 0000000..79d8686 --- /dev/null +++ b/php/src/db/_private/_migration.php @@ -0,0 +1,99 @@ +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; + + 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)", + ]; + + protected 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; + + /** @var callable[]|string[] */ + protected $migrations; + + function migrate(?IDatabase $db=null): void { + $db = ($this->db ??= $db); + $this->ensureTable(); + foreach ($this->migrations as $name => $migration) { + if ($this->isMigrated($name)) continue; + $this->setMigrated($name, false); + 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/_private/_select.php b/php/src/db/_private/_select.php index ee2bdbc..80b0460 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..7b297c4 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,43 @@ 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 + $prefix = $query["prefix"] ?? null; + if ($prefix !== null) $sql[] = $prefix; + + ## table + $table = $query["table"] ?? null; + if ($table !== null) $sql[] = $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 + $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 50b09d2..ace8beb 100644 --- a/php/src/db/mysql/MysqlStorage.php +++ b/php/src/db/mysql/MysqlStorage.php @@ -1,6 +1,7 @@ db = Mysql::with($mysql); } - /** @var Mysql */ - protected $db; + protected Mysql $db; function db(): Mysql { return $this->db; @@ -23,21 +23,40 @@ class MysqlStorage extends CapacitorStorage { "id_" => "integer primary key auto_increment", ]; - function _getCreateSql(CapacitorChannel $channel): string { - $query = new _query_base($this->_createSql($channel)); - return self::format_sql($channel, $query->getSql()); - } - - function _exists(CapacitorChannel $channel): bool { + protected function tableExists(string $tableName): bool { $db = $this->db; - $tableName = $db->get([ + $found = $db->get([ "select table_name from information_schema.tables", "where" => [ "table_schema" => $db->getDbname(), - "table_name" => $channel->getTableName(), + "table_name" => $tableName, ], ]); - return $tableName !== null; + return $found !== null; + } + + const METADATA_COLS = [ + "name" => "varchar(64) not null primary key", + "value" => "varchar(255)", + ]; + + function _getMigration(CapacitorChannel $channel): _mysqlMigration { + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _mysqlMigration($migrations, $channel->getName()); + } + + const CHANNELS_COLS = [ + "name" => "varchar(255) not null 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 close(): void { diff --git a/php/src/db/mysql/_mysqlMigration.php b/php/src/db/mysql/_mysqlMigration.php new file mode 100644 index 0000000..2d63db1 --- /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)", + ]; + + 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 new file mode 100644 index 0000000..b207161 --- /dev/null +++ b/php/src/db/mysql/_mysqlQuery.php @@ -0,0 +1,8 @@ + "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 @@ - $pdo->dbconn, "options" => $pdo->options, "config" => $pdo->config, - "migrate" => $pdo->migration, + "migration" => $pdo->migration, ], $params)); } else { return new static($pdo, $params); @@ -49,7 +49,7 @@ class Pdo implements IDatabase { protected const CONFIG = null; - protected const MIGRATE = null; + protected const MIGRATION = null; const dbconn_SCHEMA = [ "name" => "string", @@ -62,7 +62,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], ]; @@ -93,7 +93,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) { @@ -104,7 +104,7 @@ class Pdo implements IDatabase { protected ?array $dbconn; /** @var array|callable */ - protected array $options; + protected $options; /** @var array|string|callable */ protected $config; @@ -114,13 +114,17 @@ class Pdo implements IDatabase { protected ?\PDO $db = null; + function getSql($query, ?array $params=null): string { + $query = new _pdoQuery($query, $params); + return $query->getSql(); + } + function open(): self { if ($this->db === null) { $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)->invoke(); } $this->db = new \PDO($dbconn["name"], $dbconn["user"], $dbconn["pass"], $options); _config::with($this->config)->configure($this); @@ -143,21 +147,16 @@ 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_base($query, $params); - if ($query->useStmt($db, $stmt, $sql)) { + $query = new _pdoQuery($query, $params); + if ($query->_use_stmt($db, $stmt, $sql)) { if ($stmt->execute() === false) return false; if ($query->isInsert()) return $db->lastInsertId(); 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; } } @@ -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; @@ -222,11 +221,11 @@ class Pdo implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new _query_base($query, $params); + $query = new _pdoQuery($query, $params); $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); @@ -245,22 +244,18 @@ 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_base($query, $params); + $query = new _pdoQuery($query, $params); $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); } - 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/_pdoQuery.php b/php/src/db/pdo/_pdoQuery.php new file mode 100644 index 0000000..c1161fa --- /dev/null +++ b/php/src/db/pdo/_pdoQuery.php @@ -0,0 +1,30 @@ +sql); + //msg::info(var_export($this->bindings, true)); + } + if ($this->bindings !== null) { + $stmt = $db->prepare($this->sql); + foreach ($this->bindings as $name => $value) { + $this->verifixBindings($value); + $stmt->bindValue($name, $value); + } + return true; + } else { + $sql = $this->sql; + return false; + } + } +} diff --git a/php/src/db/pdo/_query_base.php b/php/src/db/pdo/_query_base.php deleted file mode 100644 index 921704d..0000000 --- a/php/src/db/pdo/_query_base.php +++ /dev/null @@ -1,76 +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]; - } - } - } - - 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)); - } - if ($this->bindings !== null) { - $stmt = $db->prepare($this->sql); - foreach ($this->bindings as $name => $value) { - $this->verifixBindings($value); - $stmt->bindValue($name, $value); - } - return true; - } else { - $sql = $this->sql; - return false; - } - } -} 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 @@ - $pgsql->dbconn, + "options" => $pgsql->options, + "config" => $pgsql->config, + "migration" => $pgsql->migration, + ], $params)); + } else { + return new static($pgsql, $params); + } + } + + + protected const OPTIONS = [ + "persistent" => true, + "force_new" => false, + "serial_support" => true, + ]; + + const CONFIG = null; + + const MIGRATION = null; + + const params_SCHEMA = [ + "dbconn" => ["array"], + "options" => ["?array|callable"], + "replace_config" => ["?array|callable"], + "config" => ["?array|callable"], + "migration" => ["?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["migration"] ?? static::MIGRATION; + # + $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 getSql($query, ?array $params=null): string { + $query = new _pgsqlQuery($query, $params); + return $query->getSql(); + } + + 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)->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(string $query): bool { + $result = pg_query($this->db(), $query); + if ($result === false) return false; + pg_free_result($result); + 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 _pgsqlQuery($query, $params); + $result = $query->_exec($db); + $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; + } + + /** @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 &$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 { + $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 { + $this->_exec("commit"); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->commit(); + } + } + } + + function rollback(): void { + $this->_exec("rollback"); + if ($this->transactors !== null) { + foreach ($this->transactors as $transactor) { + $transactor->rollback(); + } + } + } + + function get($query, ?array $params=null, bool $entireRow=false) { + $db = $this->db(); + $query = new _pgsqlQuery($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 { + return $this->get($query, $params, true); + } + + function all($query, ?array $params=null, $primaryKeys=null): iterable { + $db = $this->db(); + $query = new _pgsqlQuery($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 new file mode 100644 index 0000000..f9a500e --- /dev/null +++ b/php/src/db/pgsql/PgsqlException.php @@ -0,0 +1,15 @@ +getMessage(), $e->getCode(), $e); + } +} diff --git a/php/src/db/pgsql/PgsqlStorage.php b/php/src/db/pgsql/PgsqlStorage.php new file mode 100644 index 0000000..dd89e2a --- /dev/null +++ b/php/src/db/pgsql/PgsqlStorage.php @@ -0,0 +1,60 @@ +db = Pgsql::with($pgsql); + } + + protected Pgsql $db; + + function db(): Pgsql { + return $this->db; + } + + const PRIMARY_KEY_DEFINITION = [ + "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 { + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _pgsqlMigration($migrations, $channel->getName()); + } + + protected function _addToChannelsSql(CapacitorChannel $channel): array { + return cl::merge(parent::_addToChannelsSql($channel), [ + "suffix" => "on conflict (name) do nothing", + ]); + } + + function close(): void { + $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/pgsql/_pgsqlQuery.php b/php/src/db/pgsql/_pgsqlQuery.php new file mode 100644 index 0000000..34c7ed1 --- /dev/null +++ b/php/src/db/pgsql/_pgsqlQuery.php @@ -0,0 +1,44 @@ +sql; + $bindings = $this->bindings; + 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 + $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, $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 a1ebf5c..ae4ea99 100644 --- a/php/src/db/sqlite/Sqlite.php +++ b/php/src/db/sqlite/Sqlite.php @@ -3,10 +3,11 @@ 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; -use nulib\php\nur_func; +use nulib\php\func; use nulib\ValueException; use SQLite3; use SQLite3Result; @@ -29,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)); @@ -71,7 +72,7 @@ class Sqlite implements IDatabase { const CONFIG = null; - const MIGRATE = null; + const MIGRATION = null; const params_SCHEMA = [ "file" => ["string", ""], @@ -80,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], ]; @@ -108,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; @@ -145,11 +146,16 @@ 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); _config::with($this->config)->configure($this); - _migration::with($this->migration)->migrate($this); + _sqliteMigration::with($this->migration)->migrate($this); $this->inTransaction = false; } return $this; @@ -180,15 +186,10 @@ 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_base($query, $params); - if ($query->useStmt($db, $stmt, $sql)) { + $query = new _sqliteQuery($query, $params); + if ($query->_use_stmt($db, $stmt, $sql)) { try { $result = $stmt->execute(); if ($result === false) return false; @@ -201,7 +202,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(); } } @@ -237,7 +238,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; @@ -274,8 +275,8 @@ class Sqlite implements IDatabase { function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); - $query = new _query_base($query, $params); - if ($query->useStmt($db, $stmt, $sql)) { + $query = new _sqliteQuery($query, $params); + if ($query->_use_stmt($db, $stmt, $sql)) { try { $result = $this->checkResult($stmt->execute()); try { @@ -300,7 +301,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); @@ -317,14 +318,10 @@ 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_base($query, $params); - if ($query->useStmt($db, $stmt, $sql)) { + $query = new _sqliteQuery($query, $params); + 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 287a9f7..4621cb2 100644 --- a/php/src/db/sqlite/SqliteStorage.php +++ b/php/src/db/sqlite/SqliteStorage.php @@ -1,6 +1,7 @@ db = Sqlite::with($sqlite); } - /** @var Sqlite */ - protected $db; + protected Sqlite $db; function db(): Sqlite { return $this->db; @@ -23,74 +23,51 @@ class SqliteStorage extends CapacitorStorage { "id_" => "integer primary key autoincrement", ]; - function _getCreateSql(CapacitorChannel $channel): string { - $query = new _query_base($this->_createSql($channel)); - return self::format_sql($channel, $query->getSql()); - } - - function tableExists(string $tableName): bool { - $name = $this->db->get([ + 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 $name !== null; + return $found !== null; + } + + function _getMigration(CapacitorChannel $channel): _sqliteMigration { + $migrations = cl::merge([ + "0init" => [$this->_createSql($channel)], + ], $channel->getMigration()); + return new _sqliteMigration($migrations, $channel->getName()); } function channelExists(string $name): bool { - $name = $this->db->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 { + $sql = parent::_addToChannelsSql($channel); + $sql[0] = "insert or ignore"; + return $sql; } protected function _afterCreate(CapacitorChannel $channel): void { $db = $this->db; - if (!$this->tableExists("_channels")) { + 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 - $db->exec([ - "create table if not exists", - "table" => "_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 - $db->exec([ - "insert", - "into" => "_channels", - "values" => [ - "name" => $channel->getName(), - "table_name" => $channel->getTableName(), - "class" => get_class($channel), - ], - "suffix" => "on conflict do nothing", - ]); + $db->exec($this->_addToChannelsSql($channel)); } } - protected function _beforeReset(CapacitorChannel $channel): void { - $this->db->exec([ - "delete", - "from" => "_channels", - "where" => [ - "name" => $channel->getName(), - ], - ]); - } - - function _exists(CapacitorChannel $channel): bool { - return $this->tableExists($channel->getTableName()); - } - function close(): void { $this->db->close(); } diff --git a/php/src/db/sqlite/_config.php b/php/src/db/sqlite/_config.php deleted file mode 100644 index ea7553a..0000000 --- a/php/src/db/sqlite/_config.php +++ /dev/null @@ -1,36 +0,0 @@ -configs = $configs; - } - - /** @var array */ - protected $configs; - - function configure(Sqlite $sqlite): void { - foreach ($this->configs as $key => $config) { - if (is_string($config) && !nur_func::is_method($config)) { - $sqlite->exec($config); - } else { - nur_func::ensure_func($config, $this, $args); - nur_func::call($config, $sqlite, $key, ...$args); - } - } - } -} diff --git a/php/src/db/sqlite/_migration.php b/php/src/db/sqlite/_migration.php deleted file mode 100644 index d2adf93..0000000 --- a/php/src/db/sqlite/_migration.php +++ /dev/null @@ -1,55 +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) && !nur_func::is_method($migration)) { - $sqlite->exec($migration); - } else { - nur_func::ensure_func($migration, $this, $args); - nur_func::call($migration, $sqlite, $key, ...$args); - } - $sqlite->exec("update _migration set done = 1 where key = :key", [ - "key" => $key, - ]); - } - } - } -} diff --git a/php/src/db/sqlite/_query_base.php b/php/src/db/sqlite/_query_base.php deleted file mode 100644 index b397e0d..0000000 --- a/php/src/db/sqlite/_query_base.php +++ /dev/null @@ -1,62 +0,0 @@ -sql); #XXX - if ($this->bindings !== null) { - /** @var SQLite3Stmt $stmt */ - $stmt = SqliteException::check($db, $db->prepare($this->sql)); - $close = true; - try { - foreach ($this->bindings as $param => $value) { - $this->verifixBindings($value); - SqliteException::check($db, $stmt->bindValue($param, $value)); - } - $close = false; - return true; - } finally { - if ($close) $stmt->close(); - } - } else { - $sql = $this->sql; - return false; - } - } -} diff --git a/php/src/db/sqlite/_query_create.php b/php/src/db/sqlite/_query_create.php deleted file mode 100644 index 5aa7aa1..0000000 --- a/php/src/db/sqlite/_query_create.php +++ /dev/null @@ -1,10 +0,0 @@ -db->exec([ + "insert or replace", + "into" => static::MIGRATION_TABLE, + "values" => [ + "channel" => $this->channel, + "name" => $name, + "done" => $done? 1: 0, + ], + ]); + } +} diff --git a/php/src/db/sqlite/_sqliteQuery.php b/php/src/db/sqlite/_sqliteQuery.php new file mode 100644 index 0000000..04e6f1c --- /dev/null +++ b/php/src/db/sqlite/_sqliteQuery.php @@ -0,0 +1,39 @@ +sql); + //msg::info(var_export($this->bindings, true)); + } + if ($this->bindings !== null) { + /** @var SQLite3Stmt $stmt */ + $stmt = SqliteException::check($db, $db->prepare($this->sql)); + $close = true; + try { + foreach ($this->bindings as $param => $value) { + $this->verifixBindings($value); + SqliteException::check($db, $stmt->bindValue($param, $value)); + } + $close = false; + return true; + } finally { + if ($close) $stmt->close(); + } + } else { + $sql = $this->sql; + return false; + } + } +} diff --git a/php/src/file/tab/AbstractBuilder.php b/php/src/file/tab/AbstractBuilder.php index f1ec869..3ec767d 100644 --- a/php/src/file/tab/AbstractBuilder.php +++ b/php/src/file/tab/AbstractBuilder.php @@ -5,7 +5,7 @@ use DateTimeInterface; use nulib\cl; use nulib\file\TempStream; use nulib\os\path; -use nulib\php\nur_func; +use nulib\php\func; use nulib\php\time\DateTime; use nulib\web\http; @@ -35,13 +35,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); + $this->cookFunc = $cookFunc; $this->output = $params["output"] ?? static::OUTPUT; $maxMemory = $params["max_memory"] ?? null; $throwOnError = $params["throw_on_error"] ?? null; @@ -60,9 +55,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 +80,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([$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..ebf4c52 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)->invoke(); } } if ($seq) $dest[] = $value; diff --git a/php/src/php/func.php b/php/src/php/func.php index 63ea334..675d359 100644 --- a/php/src/php/func.php +++ b/php/src/php/func.php @@ -4,12 +4,14 @@ namespace nulib\php; use Closure; use Exception; 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 @@ -58,10 +60,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; @@ -82,11 +81,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; } } @@ -117,10 +116,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; @@ -138,11 +134,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; @@ -207,10 +201,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; @@ -265,18 +256,21 @@ 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; } + $method = new ReflectionMethod($c, $f); + if (!$method->isStatic()) return false; } else { - $reason = "$msg: not bound"; + $reason = "$c::$f: not bound"; } } $func = $cf; @@ -342,10 +336,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; @@ -401,18 +392,21 @@ 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; } + $method = new ReflectionMethod($c, $f); + if ($method->isStatic()) return false; } else { - $reason = "$msg: not bound"; + $reason = "$c::$f: not bound"; } } $func = $cf; @@ -446,7 +440,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); @@ -467,6 +461,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); } @@ -487,10 +487,45 @@ 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); } + /** + * 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) { @@ -561,6 +596,27 @@ class func { protected int $maxArgs; + function replaceArgs(?array $args): self { + $this->prefixArgs = $args?? []; + return $this; + } + + 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(?array $args, ?int $stripCount=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; @@ -596,11 +652,16 @@ class func { else return $this->bound && $this->object !== null; } - function bind($object): self { + function bind($object, bool $rebind=false, bool $replace=false): self { if ($this->type !== self::TYPE_METHOD) return $this; + if (!$rebind && $this->isBound()) 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/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..37bea48 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, ], @@ -37,25 +39,48 @@ 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 = [ + "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_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 = [ + "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 */ const LIST_NATURE_METASCHEMA = [ ]; + + const LIST_PARAMS_SCHEMA = [ + ]; } diff --git a/php/src/str.php b/php/src/str.php index 9a54876..347726b 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 * @@ -253,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/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; } 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/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", "b__" => "varchar", - "b__sum_" => self::SUM_DEFINITION, + "b__sum_" => "sersum", ]; function getItemValues($item): ?array { 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 6d92412..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_base::parse_conds(null, $sql, $params); + _sqliteQuery::parse_conds(null, $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_conds([], $sql, $params); + _sqliteQuery::parse_conds([], $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - _query_base::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_base::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_base::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_base::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_base::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_base::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_base::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_base::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_base::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_base::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_base::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_base::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_base::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_base::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_base::parse_set_values(null, $sql, $params); + _sqliteQuery::parse_set_values(null, $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - _query_base::parse_set_values([], $sql, $params); + _sqliteQuery::parse_set_values([], $sql, $params); self::assertNull($sql); self::assertNull($params); $sql = $params = null; - _query_base::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_base::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_base::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_base::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_base::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); } diff --git a/php/tests/db/sqlite/impl/MyChannel.php b/php/tests/db/sqlite/impl/MyChannel.php new file mode 100644 index 0000000..3cf5b69 --- /dev/null +++ b/php/tests/db/sqlite/impl/MyChannel.php @@ -0,0 +1,33 @@ + "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 3f580fa..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"], @@ -1102,12 +1102,45 @@ 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()); - self::assertSame(12, $func->bind(new C1(1))->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), 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(), true, 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(["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(["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()); } } } 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; - } - } -} 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"));