<pman>Intégration de la branche rel82-0.5.0
This commit is contained in:
		
						commit
						9f8b6545e6
					
				
							
								
								
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -1,3 +1,5 @@ | ||||
| /test_* | ||||
| 
 | ||||
| /.composer.lock.runphp | ||||
| 
 | ||||
| .~lock*# | ||||
|  | ||||
							
								
								
									
										18
									
								
								CHANGES.md
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								CHANGES.md
									
									
									
									
									
								
							| @ -1,3 +1,21 @@ | ||||
| ## Release 0.5.0p82 du 30/04/2025-04:33 | ||||
| 
 | ||||
| ## 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 | ||||
|  | ||||
| @ -1 +1 @@ | ||||
| 0.4.1 | ||||
| 0.5.0 | ||||
|  | ||||
							
								
								
									
										59
									
								
								bin/pman
									
									
									
									
									
								
							
							
						
						
									
										59
									
								
								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" <<EOF | ||||
| # -*- coding: utf-8 mode: yaml -*- vim:sw=2:sts=2:et:ai:si:sta:fenc=utf-8 | ||||
| 
 | ||||
| composer: | ||||
|   match_prefix: | ||||
|   match_prefix-dev: | ||||
|   profiles: [ dev, dist ] | ||||
|   dev: | ||||
|     link: true | ||||
|     require: | ||||
|     reqire-dev: | ||||
|   dist: | ||||
|     link: false | ||||
|     require: | ||||
|     reqire-dev: | ||||
| EOF | ||||
|         "${EDITOR:-nano}" "$config" | ||||
|         [ -s "$config" ] || return 1 | ||||
| 
 | ||||
|         cp "$config" .composer.pman.yml | ||||
|         git add .composer.pman.yml | ||||
|     fi | ||||
|     return 0 | ||||
| } | ||||
| 
 | ||||
| function init_composer_action() { | ||||
|     local -a push_branches; local config | ||||
| 
 | ||||
|     [ -f .composer.pman.yml -a -z "$ForceCreate" ] && die "La configuration pman composer a déjà été initialisée" | ||||
| 
 | ||||
|     resolve_should_push | ||||
| 
 | ||||
|     _init_composer || exit_with ewarn "Initialisation de la configuration annulée" | ||||
|     git commit -m "configuration pman composer" | ||||
|     push_branches+=("$CurrentBranch") | ||||
| 
 | ||||
|     _push_branches | ||||
| } | ||||
| 
 | ||||
| function _ensure_main_branch() { | ||||
|     [ -n "$MAIN" ] || die "La branche MAIN n'a pas été définie" | ||||
|     [ -n "$MainBranch" ] || die "$MAIN: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" | ||||
| @ -108,7 +150,7 @@ $MAIN: une branche du même nom existe dans l'origine | ||||
| 
 | ||||
| function _ensure_develop_branch() { | ||||
|     [ -n "$DEVELOP" ] || die "La branche DEVELOP n'a pas été définie" | ||||
|     [ -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" | ||||
|     [ "$1" == init -o -n "$DevelopBranch" ] || die "$DEVELOP: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" | ||||
| } | ||||
| 
 | ||||
| function init_develop_action() { | ||||
| @ -119,7 +161,7 @@ function init_develop_action() { | ||||
| $DEVELOP: une branche du même nom existe dans l'origine | ||||
|     git checkout $DEVELOP" | ||||
|         _ensure_main_branch | ||||
|         _ensure_develop_branch | ||||
|         _ensure_develop_branch init | ||||
| 
 | ||||
|         resolve_should_push | ||||
| 
 | ||||
| @ -137,7 +179,7 @@ $DEVELOP: une branche du même nom existe dans l'origine | ||||
| 
 | ||||
| function _ensure_upstream_branch() { | ||||
|     [ -n "$UPSTREAM" ] || die "La branche UPSTREAM n'a pas été définie" | ||||
|     [ -n "$UpstreamBranch" ] || die "$UPSTREAM: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" | ||||
|     [ "$1" == init -o -n "$UpstreamBranch" ] || die "$UPSTREAM: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" | ||||
| } | ||||
| 
 | ||||
| function init_upstream_action() { | ||||
| @ -148,7 +190,7 @@ function init_upstream_action() { | ||||
| $UPSTREAM: une branche du même nom existe dans l'origine | ||||
|     git checkout $UPSTREAM" | ||||
|         _ensure_develop_branch | ||||
|         _ensure_upstream_branch | ||||
|         _ensure_upstream_branch init | ||||
| 
 | ||||
|         resolve_should_push | ||||
| 
 | ||||
| @ -182,7 +224,7 @@ $UPSTREAM: une branche du même nom existe dans l'origine | ||||
| 
 | ||||
| function _ensure_dist_branch() { | ||||
|     [ -n "$DIST" ] || die "La branche DIST n'a pas été définie" | ||||
|     [ -n "$DistBranch" ] || die "$DIST: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" | ||||
|     [ "$1" == init -o -n "$DistBranch" ] || die "$DIST: cette branche n'existe pas (le dépôt a-t-il été initialisé?)" | ||||
| } | ||||
| 
 | ||||
| function init_dist_action() { | ||||
| @ -193,7 +235,7 @@ function init_dist_action() { | ||||
| $DIST: une branche du même nom existe dans l'origine | ||||
|     git checkout $DIST" | ||||
|         _ensure_main_branch | ||||
|         _ensure_dist_branch | ||||
|         _ensure_dist_branch init | ||||
| 
 | ||||
|         resolve_should_push | ||||
| 
 | ||||
| @ -242,6 +284,7 @@ function init_action() { | ||||
|     case "$what" in | ||||
|     init|repo|r) init_repo_action "$@";; | ||||
|     config) init_config_action "$@";; | ||||
|     composer) init_composer_action "$@";; | ||||
|     main|m) checkout_main_action;; | ||||
|     develop|dev|d) init_develop_action "$@";; | ||||
|     upstream|up|u) init_upstream_action "$@";; | ||||
| @ -263,7 +306,8 @@ Origin= | ||||
| ForceCreate= | ||||
| args=( | ||||
|     "gérer un projet git" | ||||
|     "repo|config|develop|upstream|dist | ||||
|     "repo|config|composer | ||||
| develop|upstream|dist | ||||
| 
 | ||||
| INITIALISATION | ||||
| 
 | ||||
| @ -272,6 +316,7 @@ configurer certaines branches du dépôt si elles n'existent pas déjà | ||||
| 
 | ||||
|     repo | ||||
|         initialiser un dépôt vide et créer les branches $MAIN et $DEVELOP | ||||
| 
 | ||||
|     develop | ||||
|         créer la branche $DEVELOP | ||||
|     upstream | ||||
|  | ||||
| @ -24,6 +24,8 @@ | ||||
| 		"ext-posix": "*", | ||||
| 		"ext-pcntl": "*", | ||||
| 		"ext-curl": "*", | ||||
| 		"ext-pdo": "*", | ||||
| 		"ext-pgsql": "*", | ||||
| 		"ext-sqlite3": "*" | ||||
| 	}, | ||||
| 	"autoload": { | ||||
|  | ||||
							
								
								
									
										4
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							| @ -4,7 +4,7 @@ | ||||
|         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", | ||||
|         "This file is @generated automatically" | ||||
|     ], | ||||
|     "content-hash": "266a079e97f3ceecc2cc0a84d6b9743b", | ||||
|     "content-hash": "a8b9dc80255663640bda855729ef2d47", | ||||
|     "packages": [ | ||||
|         { | ||||
|             "name": "symfony/deprecation-contracts", | ||||
| @ -2022,6 +2022,8 @@ | ||||
|         "ext-posix": "*", | ||||
|         "ext-pcntl": "*", | ||||
|         "ext-curl": "*", | ||||
|         "ext-pdo": "*", | ||||
|         "ext-pgsql": "*", | ||||
|         "ext-sqlite3": "*" | ||||
|     }, | ||||
|     "plugin-api-version": "2.2.0" | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
| namespace nulib; | ||||
| 
 | ||||
| use ArrayAccess; | ||||
| use nulib\php\nur_func; | ||||
| use nulib\php\func; | ||||
| use Traversable; | ||||
| 
 | ||||
| /** | ||||
| @ -348,12 +348,12 @@ class cl { | ||||
| 
 | ||||
|   #############################################################################
 | ||||
| 
 | ||||
|   static final function map(callable $callback, ?iterable $array): array { | ||||
|   static final function map($func, ?iterable $array): array { | ||||
|     $result = []; | ||||
|     if ($array !== null) { | ||||
|       $ctx = nur_func::_prepare($callback); | ||||
|       $func = func::with($func); | ||||
|       foreach ($array as $key => $value) { | ||||
|         $result[$key] = nur_func::_call($ctx, [$value, $key]); | ||||
|         $result[$key] = $func->invoke([$value, $key]); | ||||
|       } | ||||
|     } | ||||
|     return $result; | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <?php | ||||
| namespace nulib\db; | ||||
| 
 | ||||
| use nulib\php\nur_func; | ||||
| use nulib\php\func; | ||||
| use nulib\ValueException; | ||||
| use Traversable; | ||||
| 
 | ||||
| @ -39,6 +39,11 @@ class Capacitor implements ITransactor { | ||||
|     return $this->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); | ||||
|   } | ||||
|  | ||||
| @ -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 (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(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -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 { | ||||
|         $definitions[$col] = $def; | ||||
|                 if ($mdef) { | ||||
|                   $definitions[$mcol] = self::sercol($mdef); | ||||
|                 } else { | ||||
|                   unset($definitions[$mcol]); | ||||
|                 } | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         } else { | ||||
|           $constraints[] = $def; | ||||
|         } | ||||
|       } else { | ||||
|         $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 <<<EOT | ||||
| -- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 | ||||
| -- autogénéré à partir de $class | ||||
|   abstract protected function tableExists(string $tableName): bool; | ||||
| 
 | ||||
| $sql; | ||||
|   const METADATA_TABLE = "_metadata"; | ||||
|   const METADATA_COLS = [ | ||||
|     "name" => "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", | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -1,39 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| trait Tcreate { | ||||
|   static function isa(string $sql): bool { | ||||
|     //return preg_match("/^create(?:\s+table)?\b/i", $sql);
 | ||||
|     #XXX implémentation minimale
 | ||||
|     return preg_match("/^create\s+table\b/i", $sql); | ||||
|   } | ||||
| 
 | ||||
|   static function parse(array $query, ?array &$bindings=null): string { | ||||
|     #XXX implémentation minimale
 | ||||
|     $sql = [self::merge_seq($query)]; | ||||
| 
 | ||||
|     ## préfixe
 | ||||
|     if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; | ||||
| 
 | ||||
|     ## table
 | ||||
|     $sql[] = $query["table"]; | ||||
| 
 | ||||
|     ## columns
 | ||||
|     $cols = $query["cols"]; | ||||
|     $index = 0; | ||||
|     foreach ($cols as $col => &$definition) { | ||||
|       if ($col === $index) { | ||||
|         $index++; | ||||
|       } else { | ||||
|         $definition = "$col $definition"; | ||||
|       } | ||||
|     }; unset($definition); | ||||
|     $sql[] = "(\n  ".implode("\n, ", $cols)."\n)"; | ||||
| 
 | ||||
|     ## suffixe
 | ||||
|     if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; | ||||
| 
 | ||||
|     ## fin de la requête
 | ||||
|     return implode(" ", $sql); | ||||
|   } | ||||
| } | ||||
| @ -1,38 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| trait Tdelete { | ||||
| 
 | ||||
|   static function isa(string $sql): bool { | ||||
|     return preg_match("/^delete(?:\s+from)?\b/i", $sql); | ||||
|   } | ||||
| 
 | ||||
|   static function parse(array $query, ?array &$bindings=null): string { | ||||
|     #XXX implémentation minimale
 | ||||
|     $tmpsql = self::merge_seq($query); | ||||
|     self::consume('delete(?:\s+from)?\b', $tmpsql); | ||||
|     $sql = ["delete from", $tmpsql]; | ||||
| 
 | ||||
|     ## préfixe
 | ||||
|     if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; | ||||
| 
 | ||||
|     ## table
 | ||||
|     $sql[] = $query["from"]; | ||||
| 
 | ||||
|     ## where
 | ||||
|     $where = $query["where"] ?? null; | ||||
|     if ($where !== null) { | ||||
|       self::parse_conds($where, $wheresql, $bindings); | ||||
|       if ($wheresql) { | ||||
|         $sql[] = "where"; | ||||
|         $sql[] = implode(" and ", $wheresql); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     ## suffixe
 | ||||
|     if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; | ||||
| 
 | ||||
|     ## fin de la requête
 | ||||
|     return implode(" ", $sql); | ||||
|   } | ||||
| } | ||||
| @ -1,18 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| use nulib\cl; | ||||
| use nulib\ValueException; | ||||
| 
 | ||||
| trait Tgeneric { | ||||
|   static function isa(string $sql): bool { | ||||
|     return preg_match('/^(?:drop\s+table)\b/i', $sql); | ||||
|   } | ||||
| 
 | ||||
|   static function parse(array $query, ?array &$bindings=null): string { | ||||
|     if (!cl::is_list($query)) { | ||||
|       throw new ValueException("Seuls les tableaux séquentiels sont supportés"); | ||||
|     } | ||||
|     return self::merge_seq($query); | ||||
|   } | ||||
| } | ||||
| @ -1,82 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| use nulib\cl; | ||||
| use nulib\ValueException; | ||||
| 
 | ||||
| trait Tinsert { | ||||
|   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); | ||||
|   } | ||||
| } | ||||
| @ -1,168 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| use nulib\cl; | ||||
| use nulib\str; | ||||
| use nulib\ValueException; | ||||
| 
 | ||||
| trait Tselect { | ||||
|   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); | ||||
|   } | ||||
| } | ||||
| @ -1,40 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| trait Tupdate { | ||||
|   static function isa(string $sql): bool { | ||||
|     return preg_match("/^update\b/i", $sql); | ||||
|   } | ||||
| 
 | ||||
|   static function parse(array $query, ?array &$bindings=null): string { | ||||
|     #XXX implémentation minimale
 | ||||
|     $sql = [self::merge_seq($query)]; | ||||
| 
 | ||||
|     ## préfixe
 | ||||
|     if (($prefix = $query["prefix"] ?? null) !== null) $sql[] = $prefix; | ||||
| 
 | ||||
|     ## table
 | ||||
|     $sql[] = $query["table"]; | ||||
| 
 | ||||
|     ## set
 | ||||
|     self::parse_set_values($query["values"], $setsql, $bindings); | ||||
|     $sql[] = "set"; | ||||
|     $sql[] = implode(", ", $setsql); | ||||
| 
 | ||||
|     ## where
 | ||||
|     $where = $query["where"] ?? null; | ||||
|     if ($where !== null) { | ||||
|       self::parse_conds($where, $wheresql, $bindings); | ||||
|       if ($wheresql) { | ||||
|         $sql[] = "where"; | ||||
|         $sql[] = implode(" and ", $wheresql); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     ## suffixe
 | ||||
|     if (($suffix = $query["suffix"] ?? null) !== null) $sql[] = $suffix; | ||||
| 
 | ||||
|     ## fin de la requête
 | ||||
|     return implode(" ", $sql); | ||||
|   } | ||||
| } | ||||
| @ -1,259 +1,59 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| use nulib\cl; | ||||
| use nulib\str; | ||||
| use nulib\ValueException; | ||||
| 
 | ||||
| abstract class _base { | ||||
|   protected static function consume(string $pattern, string &$string, ?array &$ms=null): bool { | ||||
|     if (!preg_match("/^$pattern/i", $string, $ms)) return false; | ||||
|     $string = substr($string, strlen($ms[0])); | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   /** fusionner toutes les parties séquentielles d'une requête */ | ||||
|   protected static function merge_seq(array $query): string { | ||||
|     $index = 0; | ||||
|     $sql = ""; | ||||
|     foreach ($query as $key => $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); | ||||
| 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 { | ||||
|           # 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++; | ||||
|         throw ValueException::invalid_kind($sql, "query"); | ||||
|       } | ||||
|     } else { | ||||
|               $param = "$param0$i"; | ||||
|               $parts[] = ":$param"; | ||||
|               $bindings[$param] = $condvalue; | ||||
|               if ($i === false) $i = 2; | ||||
|               else $i++; | ||||
|       if (!is_string($sql)) $sql = strval($sql); | ||||
|       if (_create::isa($sql)) { | ||||
|         $meta = ["isa" => "create", "type" => "ddl"]; | ||||
|       } elseif (_select::isa($sql)) { | ||||
|         $meta = ["isa" => "select", "type" => "dql"]; | ||||
|       } elseif (_insert::isa($sql)) { | ||||
|         $meta = ["isa" => "insert", "type" => "dml"]; | ||||
|       } elseif (_update::isa($sql)) { | ||||
|         $meta = ["isa" => "update", "type" => "dml"]; | ||||
|       } elseif (_delete::isa($sql)) { | ||||
|         $meta = ["isa" => "delete", "type" => "dml"]; | ||||
|       } elseif (_generic::isa($sql)) { | ||||
|         $meta = ["isa" => "generic", "type" => null]; | ||||
|       } else { | ||||
|         $meta = ["isa" => "generic", "type" => null]; | ||||
|       } | ||||
|     } | ||||
|           $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); | ||||
|   static function with($sql, ?array $params=null): array { | ||||
|     static::verifix($sql, $params); | ||||
|     return [$sql, $params]; | ||||
|   } | ||||
|         $index++; | ||||
|       } else { | ||||
|         ## associatif
 | ||||
|         # paramètre
 | ||||
|         $param = $param0 = preg_replace('/^.+\./', "", $key); | ||||
|         if ($bindings !== null && array_key_exists($param0, $bindings)) { | ||||
|           $i = 2; | ||||
|           while (array_key_exists("$param0$i", $bindings)) { | ||||
|             $i++; | ||||
|           } | ||||
|           $param = "$param0$i"; | ||||
|         } | ||||
|         # value
 | ||||
|         $value = $part; | ||||
|         $part = [$key, "="]; | ||||
|         if ($value === null) { | ||||
|           $part[] = "null"; | ||||
|         } else { | ||||
|           $part[] = ":$param"; | ||||
|           $bindings[$param] = $value; | ||||
|         } | ||||
|         $parts[] = implode(" ", $part); | ||||
|       } | ||||
|     } | ||||
|     $sql = cl::merge($sql, $parts); | ||||
|   } | ||||
| 
 | ||||
|   protected static function check_eof(string $tmpsql, string $usersql): void { | ||||
|     self::consume(';\s*', $tmpsql); | ||||
|     if ($tmpsql) { | ||||
|       throw new ValueException("unexpected value at end: $usersql"); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   abstract protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void; | ||||
| 
 | ||||
|   function __construct($sql, ?array $bindings=null) { | ||||
|     static::verifix($sql, $bindings, $meta); | ||||
|  | ||||
							
								
								
									
										255
									
								
								php/src/db/_private/_common.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										255
									
								
								php/src/db/_private/_common.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,255 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| use nulib\cl; | ||||
| use nulib\str; | ||||
| use nulib\ValueException; | ||||
| 
 | ||||
| class _common { | ||||
|   protected static function consume(string $pattern, string &$string, ?array &$ms=null): bool { | ||||
|     if (!preg_match("/^$pattern/i", $string, $ms)) return false; | ||||
|     $string = substr($string, strlen($ms[0])); | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   /** fusionner toutes les parties séquentielles d'une requête */ | ||||
|   protected static function merge_seq(array $query): string { | ||||
|     $index = 0; | ||||
|     $sql = ""; | ||||
|     foreach ($query as $key => $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"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,7 +1,8 @@ | ||||
| <?php | ||||
| namespace nulib\db\pdo; | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| use nulib\php\nur_func; | ||||
| use nulib\db\IDatabase; | ||||
| use nulib\php\func; | ||||
| 
 | ||||
| class _config { | ||||
|   static function with($configs): self { | ||||
| @ -23,13 +24,12 @@ class _config { | ||||
|   /** @var array */ | ||||
|   protected $configs; | ||||
| 
 | ||||
|   function configure(Pdo $pdo): void { | ||||
|   function configure(IDatabase $db): void { | ||||
|     foreach ($this->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]); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @ -1,7 +1,7 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| class _create { | ||||
| class _create extends _common { | ||||
|   const SCHEMA = [ | ||||
|     "prefix" => "?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); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,11 +1,48 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| class _delete { | ||||
| class _delete extends _common { | ||||
|   const SCHEMA = [ | ||||
|     "prefix" => "?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); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,24 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| class _generic { | ||||
| use nulib\str; | ||||
| 
 | ||||
| class _generic extends _common { | ||||
|   const SCHEMA = [ | ||||
|   ]; | ||||
| 
 | ||||
|   static function isa(string $sql): bool { | ||||
|     return preg_match('/^drop\s+table\b/i', $sql); | ||||
|   } | ||||
| 
 | ||||
|   static function parse(array $query, ?array &$bindings=null): string { | ||||
|     $sql = ""; | ||||
|     foreach ($query as $value) { | ||||
|       if ($sql && !str::ends_with(" ", $sql) && !str::starts_with(" ", $value)) { | ||||
|         $sql .= " "; | ||||
|       } | ||||
|       $sql .= $value; | ||||
|     } | ||||
|     return $sql; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,10 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| class _insert { | ||||
| use nulib\cl; | ||||
| use nulib\ValueException; | ||||
| 
 | ||||
| class _insert extends _common { | ||||
|   const SCHEMA = [ | ||||
|     "prefix" => "?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); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										99
									
								
								php/src/db/_private/_migration.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								php/src/db/_private/_migration.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| use nulib\cl; | ||||
| use nulib\db\IDatabase; | ||||
| use nulib\php\func; | ||||
| 
 | ||||
| abstract class _migration { | ||||
|   const MIGRATION = null; | ||||
| 
 | ||||
|   function __construct($migrations, string $channel="", ?IDatabase $db=null) { | ||||
|     $this->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[] = "-- <fonction PHP>"; | ||||
|       } else { | ||||
|         foreach (cl::with($migration) as $query) { | ||||
|           $sql = $db->getSql($query); | ||||
|           $lines[] = "$sql;"; | ||||
|         } | ||||
|       } | ||||
|       $lines[] = ""; | ||||
|     } | ||||
|     return implode("\n", $lines); | ||||
|   } | ||||
| } | ||||
| @ -1,7 +1,11 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| class _select { | ||||
| use nulib\cl; | ||||
| use nulib\str; | ||||
| use nulib\ValueException; | ||||
| 
 | ||||
| class _select extends _common { | ||||
|   const SCHEMA = [ | ||||
|     "prefix" => "?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); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| <?php | ||||
| namespace nulib\db\_private; | ||||
| 
 | ||||
| class _update { | ||||
| class _update extends _common { | ||||
|   const SCHEMA = [ | ||||
|     "prefix" => "?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); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| <?php | ||||
| namespace nulib\db\mysql; | ||||
| 
 | ||||
| use nulib\cl; | ||||
| use nulib\db\CapacitorChannel; | ||||
| use nulib\db\CapacitorStorage; | ||||
| 
 | ||||
| @ -12,8 +13,7 @@ class MysqlStorage extends CapacitorStorage { | ||||
|     $this->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 { | ||||
|  | ||||
							
								
								
									
										31
									
								
								php/src/db/mysql/_mysqlMigration.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								php/src/db/mysql/_mysqlMigration.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,31 @@ | ||||
| <?php | ||||
| namespace nulib\db\mysql; | ||||
| 
 | ||||
| use nulib\db\_private\_migration; | ||||
| 
 | ||||
| class _mysqlMigration extends _migration { | ||||
|   static function with($migration): self { | ||||
|     if ($migration instanceof self) return $migration; | ||||
|     else return new static($migration); | ||||
|   } | ||||
| 
 | ||||
|   const MIGRATION_COLS = [ | ||||
|     "channel" => "varchar(64) not null", | ||||
|     "name" => "varchar(64) not null", | ||||
|     "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", | ||||
|     ]); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								php/src/db/mysql/_mysqlQuery.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								php/src/db/mysql/_mysqlQuery.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| <?php | ||||
| namespace nulib\db\mysql; | ||||
| 
 | ||||
| use nulib\db\pdo\_pdoQuery; | ||||
| 
 | ||||
| class _mysqlQuery extends _pdoQuery { | ||||
|   const DEBUG_QUERIES = false; | ||||
| } | ||||
| @ -1,52 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\mysql; | ||||
| 
 | ||||
| use nulib\ValueException; | ||||
| 
 | ||||
| class _query_base extends \nulib\db\pdo\_query_base { | ||||
|   protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void { | ||||
|     if (is_array($sql)) { | ||||
|       $prefix = $sql[0] ?? null; | ||||
|       if ($prefix === null) { | ||||
|         throw new ValueException("requête invalide"); | ||||
|       } elseif (_query_create::isa($prefix)) { | ||||
|         $sql = _query_create::parse($sql, $bindinds); | ||||
|         $meta = ["isa" => "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]; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\mysql; | ||||
| 
 | ||||
| use nulib\db\_private\_create; | ||||
| use nulib\db\_private\Tcreate; | ||||
| 
 | ||||
| class _query_create extends _query_base { | ||||
|   use Tcreate; | ||||
|   const SCHEMA = _create::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\mysql; | ||||
| 
 | ||||
| use nulib\db\_private\_delete; | ||||
| use nulib\db\_private\Tdelete; | ||||
| 
 | ||||
| class _query_delete extends _query_base { | ||||
|   use Tdelete; | ||||
|   const SCHEMA = _delete::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\mysql; | ||||
| 
 | ||||
| use nulib\db\_private\_generic; | ||||
| use nulib\db\_private\Tgeneric; | ||||
| 
 | ||||
| class _query_generic extends _query_base { | ||||
|   use Tgeneric; | ||||
|   const SCHEMA = _generic::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\mysql; | ||||
| 
 | ||||
| use nulib\db\_private\_insert; | ||||
| use nulib\db\_private\Tinsert; | ||||
| 
 | ||||
| class _query_insert extends _query_base { | ||||
|   use Tinsert; | ||||
|   const SCHEMA = _insert::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\mysql; | ||||
| 
 | ||||
| use nulib\db\_private\_select; | ||||
| use nulib\db\_private\Tselect; | ||||
| 
 | ||||
| class _query_select extends _query_base { | ||||
|   use Tselect; | ||||
|   const SCHEMA = _select::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\mysql; | ||||
| 
 | ||||
| use nulib\db\_private\_update; | ||||
| use nulib\db\_private\Tupdate; | ||||
| 
 | ||||
| class _query_update extends _query_base { | ||||
|   use Tupdate; | ||||
|   const SCHEMA = _update::SCHEMA; | ||||
| } | ||||
| @ -1,12 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\mysql; | ||||
| 
 | ||||
| /** | ||||
|  * Class query: classe outil temporaire pour générer les requêtes | ||||
|  */ | ||||
| class query extends _query_base { | ||||
|   static function with($sql, ?array $params=null): array { | ||||
|     self::verifix($sql, $params); | ||||
|     return [$sql, $params]; | ||||
|   } | ||||
| } | ||||
| @ -1,12 +1,12 @@ | ||||
| <?php | ||||
| namespace nulib\db\pdo; | ||||
| 
 | ||||
| use Generator; | ||||
| use nulib\cl; | ||||
| use nulib\db\_private\_config; | ||||
| use nulib\db\_private\Tvalues; | ||||
| use nulib\db\IDatabase; | ||||
| use nulib\db\ITransactor; | ||||
| use nulib\php\nur_func; | ||||
| use nulib\php\func; | ||||
| use nulib\ValueException; | ||||
| 
 | ||||
| class Pdo implements IDatabase { | ||||
| @ -21,7 +21,7 @@ class Pdo implements IDatabase { | ||||
|         "dbconn" => $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) { | ||||
|  | ||||
							
								
								
									
										30
									
								
								php/src/db/pdo/_pdoQuery.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								php/src/db/pdo/_pdoQuery.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| <?php | ||||
| namespace nulib\db\pdo; | ||||
| 
 | ||||
| use nulib\db\_private\_base; | ||||
| use nulib\db\_private\Tbindings; | ||||
| use nulib\output\msg; | ||||
| 
 | ||||
| class _pdoQuery extends _base { | ||||
|   use Tbindings; | ||||
| 
 | ||||
|   const DEBUG_QUERIES = false; | ||||
| 
 | ||||
|   function _use_stmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool { | ||||
|     if (static::DEBUG_QUERIES) {#XXX
 | ||||
|       msg::info($this->sql); | ||||
|       //msg::info(var_export($this->bindings, true));
 | ||||
|     } | ||||
|     if ($this->bindings !== null) { | ||||
|       $stmt = $db->prepare($this->sql); | ||||
|       foreach ($this->bindings as $name => $value) { | ||||
|         $this->verifixBindings($value); | ||||
|         $stmt->bindValue($name, $value); | ||||
|       } | ||||
|       return true; | ||||
|     } else { | ||||
|       $sql = $this->sql; | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,76 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\pdo; | ||||
| 
 | ||||
| use nulib\db\_private\_base; | ||||
| use nulib\db\_private\Tbindings; | ||||
| use nulib\ValueException; | ||||
| 
 | ||||
| class _query_base extends _base { | ||||
|   use Tbindings; | ||||
| 
 | ||||
|   protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void { | ||||
|     if (is_array($sql)) { | ||||
|       $prefix = $sql[0] ?? null; | ||||
|       if ($prefix === null) { | ||||
|         throw new ValueException("requête invalide"); | ||||
|       } elseif (_query_create::isa($prefix)) { | ||||
|         $sql = _query_create::parse($sql, $bindinds); | ||||
|         $meta = ["isa" => "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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\pdo; | ||||
| 
 | ||||
| use nulib\db\_private\_create; | ||||
| use nulib\db\_private\Tcreate; | ||||
| 
 | ||||
| class _query_create extends _query_base { | ||||
|   use Tcreate; | ||||
|   const SCHEMA = _create::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\pdo; | ||||
| 
 | ||||
| use nulib\db\_private\_delete; | ||||
| use nulib\db\_private\Tdelete; | ||||
| 
 | ||||
| class _query_delete extends _query_base { | ||||
|   use Tdelete; | ||||
|   const SCHEMA = _delete::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\pdo; | ||||
| 
 | ||||
| use nulib\db\_private\_generic; | ||||
| use nulib\db\_private\Tgeneric; | ||||
| 
 | ||||
| class _query_generic extends _query_base { | ||||
|   use Tgeneric; | ||||
|   const SCHEMA = _generic::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\pdo; | ||||
| 
 | ||||
| use nulib\db\_private\_insert; | ||||
| use nulib\db\_private\Tinsert; | ||||
| 
 | ||||
| class _query_insert extends _query_base { | ||||
|   use Tinsert; | ||||
|   const SCHEMA = _insert::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\pdo; | ||||
| 
 | ||||
| use nulib\db\_private\_select; | ||||
| use nulib\db\_private\Tselect; | ||||
| 
 | ||||
| class _query_select extends _query_base { | ||||
|   use Tselect; | ||||
|   const SCHEMA = _select::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\pdo; | ||||
| 
 | ||||
| use nulib\db\_private\_update; | ||||
| use nulib\db\_private\Tupdate; | ||||
| 
 | ||||
| class _query_update extends _query_base { | ||||
|   use Tupdate; | ||||
|   const SCHEMA = _update::SCHEMA; | ||||
| } | ||||
							
								
								
									
										299
									
								
								php/src/db/pgsql/Pgsql.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								php/src/db/pgsql/Pgsql.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,299 @@ | ||||
| <?php | ||||
| namespace nulib\db\pgsql; | ||||
| 
 | ||||
| use nulib\cl; | ||||
| use nulib\db\_private\_config; | ||||
| use nulib\db\_private\Tvalues; | ||||
| use nulib\db\IDatabase; | ||||
| use nulib\db\ITransactor; | ||||
| use nulib\php\func; | ||||
| use nulib\ValueException; | ||||
| 
 | ||||
| class Pgsql implements IDatabase { | ||||
|   use Tvalues; | ||||
| 
 | ||||
|   static function with($pgsql, ?array $params=null): self { | ||||
|     if ($pgsql instanceof static) { | ||||
|       return $pgsql; | ||||
|     } elseif ($pgsql instanceof self) { | ||||
|       # recréer avec les mêmes paramètres
 | ||||
|       return new static(null, cl::merge([ | ||||
|         "dbconn" => $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); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										15
									
								
								php/src/db/pgsql/PgsqlException.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								php/src/db/pgsql/PgsqlException.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| <?php | ||||
| namespace nulib\db\pgsql; | ||||
| 
 | ||||
| use Exception; | ||||
| use RuntimeException; | ||||
| 
 | ||||
| class PgsqlException extends RuntimeException { | ||||
|   static final function last_error($db): self { | ||||
|     return new static(pg_last_error($db)); | ||||
|   } | ||||
| 
 | ||||
|   static final function wrap(Exception $e): self { | ||||
|     return new static($e->getMessage(), $e->getCode(), $e); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										60
									
								
								php/src/db/pgsql/PgsqlStorage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								php/src/db/pgsql/PgsqlStorage.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| <?php | ||||
| namespace nulib\db\pgsql; | ||||
| 
 | ||||
| use nulib\cl; | ||||
| use nulib\db\CapacitorChannel; | ||||
| use nulib\db\CapacitorStorage; | ||||
| 
 | ||||
| class PgsqlStorage extends CapacitorStorage { | ||||
|   const SERDATA_DEFINITION = "text"; | ||||
|   const SERSUM_DEFINITION = "varchar(40)"; | ||||
|   const SERTS_DEFINITION = "timestamp"; | ||||
| 
 | ||||
|   function __construct($pgsql) { | ||||
|     $this->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(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										24
									
								
								php/src/db/pgsql/_pgsqlMigration.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								php/src/db/pgsql/_pgsqlMigration.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| <?php | ||||
| namespace nulib\db\pgsql; | ||||
| 
 | ||||
| use nulib\db\_private\_migration; | ||||
| 
 | ||||
| class _pgsqlMigration extends _migration { | ||||
|   static function with($migration): self { | ||||
|     if ($migration instanceof self) return $migration; | ||||
|     else return new static($migration); | ||||
|   } | ||||
| 
 | ||||
|   protected function setMigrated(string $name, bool $done): void { | ||||
|     $this->db->exec([ | ||||
|       "insert", | ||||
|       "into" => static::MIGRATION_TABLE, | ||||
|       "values" => [ | ||||
|         "channel" => $this->channel, | ||||
|         "name" => $name, | ||||
|         "done" => $done? 1: 0, | ||||
|       ], | ||||
|       "suffix" => "on conflict (channel, name) do update set done = :done", | ||||
|     ]); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										44
									
								
								php/src/db/pgsql/_pgsqlQuery.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								php/src/db/pgsql/_pgsqlQuery.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,44 @@ | ||||
| <?php | ||||
| namespace nulib\db\pgsql; | ||||
| 
 | ||||
| use nulib\cv; | ||||
| use nulib\db\_private\_base; | ||||
| use nulib\db\_private\Tbindings; | ||||
| use nulib\output\msg; | ||||
| 
 | ||||
| class _pgsqlQuery extends _base { | ||||
|   use Tbindings; | ||||
| 
 | ||||
|   const DEBUG_QUERIES = false; | ||||
| 
 | ||||
|   /** | ||||
|    * @return resource | ||||
|    */ | ||||
|   function _exec($db) { | ||||
|     $sql = $this->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; | ||||
|   } | ||||
| } | ||||
| @ -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 { | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\cl; | ||||
| use nulib\db\CapacitorChannel; | ||||
| use nulib\db\CapacitorStorage; | ||||
| 
 | ||||
| @ -12,8 +13,7 @@ class SqliteStorage extends CapacitorStorage { | ||||
|     $this->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(); | ||||
|   } | ||||
|  | ||||
| @ -1,36 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\php\nur_func; | ||||
| 
 | ||||
| class _config { | ||||
|   static function with($configs): self { | ||||
|     if ($configs instanceof static) return $configs; | ||||
|     return new static($configs); | ||||
|   } | ||||
| 
 | ||||
|   const CONFIG = null; | ||||
| 
 | ||||
|   function __construct($configs) { | ||||
|     if ($configs === null) $configs = static::CONFIG; | ||||
|     if ($configs === null) $configs = []; | ||||
|     elseif (is_string($configs)) $configs = [$configs]; | ||||
|     elseif (is_callable($configs)) $configs = [$configs]; | ||||
|     elseif (!is_array($configs)) $configs = [strval($configs)]; | ||||
|     $this->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); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,55 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\php\nur_func; | ||||
| 
 | ||||
| class _migration { | ||||
|   static function with($migrations): self { | ||||
|     if ($migrations instanceof static) { | ||||
|       return $migrations; | ||||
|     } elseif ($migrations instanceof self) { | ||||
|       return new static($migrations->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, | ||||
|         ]); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,62 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\db\_private\_base; | ||||
| use nulib\db\_private\Tbindings; | ||||
| use nulib\output\msg; | ||||
| use nulib\ValueException; | ||||
| use SQLite3; | ||||
| use SQLite3Stmt; | ||||
| 
 | ||||
| class _query_base extends _base { | ||||
|   use Tbindings; | ||||
| 
 | ||||
|   protected static function verifix(&$sql, ?array &$bindinds=null, ?array &$meta=null): void { | ||||
|     if (is_array($sql)) { | ||||
|       $prefix = $sql[0] ?? null; | ||||
|       if ($prefix === null) { | ||||
|         throw new ValueException("requête invalide"); | ||||
|       } elseif (_query_create::isa($prefix)) { | ||||
|         $sql = _query_create::parse($sql, $bindinds); | ||||
|       } elseif (_query_select::isa($prefix)) { | ||||
|         $sql = _query_select::parse($sql, $bindinds); | ||||
|       } elseif (_query_insert::isa($prefix)) { | ||||
|         $sql = _query_insert::parse($sql, $bindinds); | ||||
|       } elseif (_query_update::isa($prefix)) { | ||||
|         $sql = _query_update::parse($sql, $bindinds); | ||||
|       } elseif (_query_delete::isa($prefix)) { | ||||
|         $sql = _query_delete::parse($sql, $bindinds); | ||||
|       } elseif (_query_generic::isa($prefix)) { | ||||
|         $sql = _query_generic::parse($sql, $bindinds); | ||||
|       } else { | ||||
|         throw SqliteException::wrap(ValueException::invalid_kind($sql, "query")); | ||||
|       } | ||||
|     } elseif (!is_string($sql)) { | ||||
|       $sql = strval($sql); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const DEBUG_QUERIES = false; | ||||
| 
 | ||||
|   function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool { | ||||
|     if (static::DEBUG_QUERIES) msg::info($this->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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\db\_private\_create; | ||||
| use nulib\db\_private\Tcreate; | ||||
| 
 | ||||
| class _query_create extends _query_base { | ||||
|   use Tcreate; | ||||
|   const SCHEMA = _create::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\db\_private\_delete; | ||||
| use nulib\db\_private\Tdelete; | ||||
| 
 | ||||
| class _query_delete extends _query_base { | ||||
|   use Tdelete; | ||||
|   const SCHEMA = _delete::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\db\_private\_generic; | ||||
| use nulib\db\_private\Tgeneric; | ||||
| 
 | ||||
| class _query_generic extends _query_base { | ||||
|   use Tgeneric; | ||||
|   const SCHEMA = _generic::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\db\_private\_insert; | ||||
| use nulib\db\_private\Tinsert; | ||||
| 
 | ||||
| class _query_insert extends _query_base { | ||||
|   use Tinsert; | ||||
|   const SCHEMA = _insert::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\db\_private\_select; | ||||
| use nulib\db\_private\Tselect; | ||||
| 
 | ||||
| class _query_select extends _query_base { | ||||
|   use Tselect; | ||||
|   const SCHEMA = _select::SCHEMA; | ||||
| } | ||||
| @ -1,10 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\db\_private\_update; | ||||
| use nulib\db\_private\Tupdate; | ||||
| 
 | ||||
| class _query_update extends _query_base { | ||||
|   use Tupdate; | ||||
|   const SCHEMA = _update::SCHEMA; | ||||
| } | ||||
							
								
								
									
										23
									
								
								php/src/db/sqlite/_sqliteMigration.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								php/src/db/sqlite/_sqliteMigration.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,23 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\db\_private\_migration; | ||||
| 
 | ||||
| class _sqliteMigration extends _migration { | ||||
|   static function with($migration): self { | ||||
|     if ($migration instanceof self) return $migration; | ||||
|     else return new static($migration); | ||||
|   } | ||||
| 
 | ||||
|   protected function setMigrated(string $name, bool $done): void { | ||||
|     $this->db->exec([ | ||||
|       "insert or replace", | ||||
|       "into" => static::MIGRATION_TABLE, | ||||
|       "values" => [ | ||||
|         "channel" => $this->channel, | ||||
|         "name" => $name, | ||||
|         "done" => $done? 1: 0, | ||||
|       ], | ||||
|     ]); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										39
									
								
								php/src/db/sqlite/_sqliteQuery.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								php/src/db/sqlite/_sqliteQuery.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\db\_private\_base; | ||||
| use nulib\db\_private\Tbindings; | ||||
| use nulib\output\msg; | ||||
| use SQLite3; | ||||
| use SQLite3Stmt; | ||||
| 
 | ||||
| class _sqliteQuery extends _base { | ||||
|   use Tbindings; | ||||
| 
 | ||||
|   const DEBUG_QUERIES = false; | ||||
| 
 | ||||
|   function _use_stmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool { | ||||
|     if (static::DEBUG_QUERIES) {#XXX
 | ||||
|       msg::info($this->sql); | ||||
|       //msg::info(var_export($this->bindings, true));
 | ||||
|     } | ||||
|     if ($this->bindings !== null) { | ||||
|       /** @var SQLite3Stmt $stmt */ | ||||
|       $stmt = SqliteException::check($db, $db->prepare($this->sql)); | ||||
|       $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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -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) { | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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,12 +134,10 @@ 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"; | ||||
|     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)); | ||||
|     } | ||||
|  | ||||
| @ -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; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -1,453 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib\php; | ||||
| 
 | ||||
| use Closure; | ||||
| use nulib\cl; | ||||
| use nulib\ref\php\ref_func; | ||||
| use nulib\ValueException; | ||||
| use ReflectionClass; | ||||
| use ReflectionFunction; | ||||
| use ReflectionMethod; | ||||
| 
 | ||||
| /** | ||||
|  * Class func: outils pour appeler des fonctions et méthodes dynamiquement | ||||
|  */ | ||||
| class nur_func { | ||||
|   /** | ||||
|    * tester si $func est une chaine de la forme "XXX::method" où XXX est une | ||||
|    * chaine quelconque éventuellement vide, ou un tableau de la forme ["method"] | ||||
|    * ou [anything, "method", ...] | ||||
|    *  | ||||
|    * Avec la forme tableau, "method" ne doit pas contenir le caractère '\', pour | ||||
|    * pouvoir utiliser conjointement {@link is_class()} | ||||
|    */ | ||||
|   static final function is_static($func, bool $allowClass=false): bool { | ||||
|     if (is_string($func)) { | ||||
|       $pos = strpos($func, "::"); | ||||
|       if ($pos === false) return false; | ||||
|       return $pos + 2 < strlen($func); | ||||
|     } elseif (is_array($func) && array_key_exists(0, $func)) { | ||||
|       $count = count($func); | ||||
|       if ($count == 1) { | ||||
|         if (!is_string($func[0]) || strlen($func[0]) == 0) return false; | ||||
|         if (strpos($func[0], "\\") !== false) return false; | ||||
|         return true; | ||||
|       } elseif ($count > 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); | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										42
									
								
								php/src/ref/schema/ref_input.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								php/src/ref/schema/ref_input.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| <?php | ||||
| namespace nulib\ref\schema; | ||||
| 
 | ||||
| class ref_input { | ||||
|   const ACCESS_AUTO = 0, ACCESS_KEY = 1, ACCESS_PROPERTY = 2; | ||||
| 
 | ||||
|   const INPUT_PARAMS_SCHEMA = [ | ||||
|     "access_type" => ["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?"] | ||||
|   ]; | ||||
| } | ||||
| @ -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 = [ | ||||
|   ]; | ||||
| } | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
| @ -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 = []; | ||||
|  | ||||
| @ -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; | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -1,132 +0,0 @@ | ||||
| <?php | ||||
| namespace nulib { | ||||
|   use nulib\tests\TestCase; | ||||
|   use nulib\impl\config; | ||||
|   use nulib\impl\myapp; | ||||
|   use nulib\impl\MyApplication1; | ||||
|   use nulib\impl\MyApplication2; | ||||
| 
 | ||||
|   class appTest extends TestCase { | ||||
|     function testWith() { | ||||
|       $projdir = config::get_projdir(); | ||||
|       $cwd = getcwd(); | ||||
| 
 | ||||
|       myapp::reset(); | ||||
|       $app1 = myapp::with(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, | ||||
|       ], $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() { | ||||
|     } | ||||
|   } | ||||
| } | ||||
							
								
								
									
										75
									
								
								php/tests/db/sqlite/ChannelMigrationTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								php/tests/db/sqlite/ChannelMigrationTest.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite; | ||||
| 
 | ||||
| use nulib\db\Capacitor; | ||||
| use nulib\db\sqlite\impl\MyChannel; | ||||
| use nulib\db\sqlite\impl\MyChannelV2; | ||||
| use nulib\db\sqlite\impl\MyChannelV3; | ||||
| use nulib\output\msg; | ||||
| use nulib\output\std\StdMessenger; | ||||
| use nulib\php\time\DateTime; | ||||
| use nulib\tests\TestCase; | ||||
| 
 | ||||
| class ChannelMigrationTest extends TestCase { | ||||
|   static function setUpBeforeClass(): void { | ||||
|     parent::setUpBeforeClass(); | ||||
|     msg::set_messenger_class(StdMessenger::class); | ||||
|   } | ||||
| 
 | ||||
|   protected function addData(MyChannel $channel, array $data): void { | ||||
|     foreach ($data as [$name, $value, $dateCre, $dateMod, $age]) { | ||||
|       $channel->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 = <<<EOT | ||||
| -- -*- coding: utf-8 mode: sql -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8 | ||||
| -- autogénéré à partir de $class | ||||
| 
 | ||||
| -- 0init | ||||
| create table  if not exists my ( | ||||
|   id_ integer primary key autoincrement | ||||
| , name varchar | ||||
| , value varchar | ||||
| , item__ mediumtext | ||||
| , item__sum_ varchar(40) | ||||
| , created_ datetime | ||||
| , modified_ datetime | ||||
| ); | ||||
| 
 | ||||
| -- dates | ||||
| alter table my add column date_cre datetime; | ||||
| alter table my add column date_mod datetime; | ||||
| 
 | ||||
| -- infos | ||||
| alter table my add column age integer; | ||||
| 
 | ||||
| EOT; | ||||
|     self::assertSame($expected, $sql); | ||||
|   } | ||||
| } | ||||
| @ -118,7 +118,7 @@ class SqliteStorageTest extends TestCase { | ||||
|       const COLUMN_DEFINITIONS = [ | ||||
|         "a__" => "varchar", | ||||
|         "b__" => "varchar", | ||||
|         "b__sum_" => self::SUM_DEFINITION, | ||||
|         "b__sum_" => "sersum", | ||||
|       ]; | ||||
| 
 | ||||
|       function getItemValues($item): ?array { | ||||
|  | ||||
| @ -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]]); | ||||
|  | ||||
| @ -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); | ||||
|   } | ||||
|  | ||||
							
								
								
									
										33
									
								
								php/tests/db/sqlite/impl/MyChannel.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								php/tests/db/sqlite/impl/MyChannel.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite\impl; | ||||
| 
 | ||||
| use nulib\db\CapacitorChannel; | ||||
| 
 | ||||
| class MyChannel extends CapacitorChannel { | ||||
|   const NAME = "my"; | ||||
|   const TABLE_NAME = "my"; | ||||
|   const COLUMN_DEFINITIONS = [ | ||||
|     "name" => "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, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										14
									
								
								php/tests/db/sqlite/impl/MyChannelV2.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								php/tests/db/sqlite/impl/MyChannelV2.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite\impl; | ||||
| 
 | ||||
| class MyChannelV2 extends MyChannel { | ||||
|   const VERSION = 2; | ||||
|   const COLUMN_DEFINITIONS = [ | ||||
|     "name" => "varchar", | ||||
|     "value" => "varchar", | ||||
|     ["dates", | ||||
|       "date_cre" => "datetime", | ||||
|       "date_mod" => "datetime", | ||||
|     ], | ||||
|   ]; | ||||
| } | ||||
							
								
								
									
										17
									
								
								php/tests/db/sqlite/impl/MyChannelV3.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								php/tests/db/sqlite/impl/MyChannelV3.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| <?php | ||||
| namespace nulib\db\sqlite\impl; | ||||
| 
 | ||||
| class MyChannelV3 extends MyChannel { | ||||
|   const VERSION = 3; | ||||
|   const COLUMN_DEFINITIONS = [ | ||||
|     "name" => "varchar", | ||||
|     "value" => "varchar", | ||||
|     ["dates", | ||||
|       "date_cre" => "datetime", | ||||
|       "date_mod" => "datetime", | ||||
|     ], | ||||
|     ["infos", | ||||
|       "age" => "integer", | ||||
|     ], | ||||
|   ]; | ||||
| } | ||||
| @ -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()); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,292 +0,0 @@ | ||||
| <?php | ||||
| 
 | ||||
| namespace { | ||||
|   function func36(): int { return 36; } | ||||
|    | ||||
|   function func_m1($a): array { return [$a]; } | ||||
|   function func_o1($b=9): array { return [$b]; } | ||||
|   function func_v(...$c): array { return [...$c]; } | ||||
|   function func_m1o1($a, $b=9): array { return [$a, $b]; } | ||||
|   function func_m1v($a, ...$c): array { return [$a, ...$c]; } | ||||
|   function func_m1o1v($a, $b=9, ...$c): array { return [$a, $b, ...$c]; } | ||||
|   function func_o1v($b=9, ...$c): array { return [$b, ...$c]; } | ||||
| } | ||||
| 
 | ||||
| namespace nulib\php { | ||||
| 
 | ||||
|   use nulib\tests\TestCase; | ||||
| 
 | ||||
|   class nur_funcTest extends TestCase { | ||||
|     function testIs_static() { | ||||
|       self::assertFalse(nur_func::is_static(null)); | ||||
|       self::assertFalse(nur_func::is_static("")); | ||||
|       self::assertFalse(nur_func::is_static("::")); | ||||
|       self::assertFalse(nur_func::is_static("xxx::")); | ||||
|       self::assertFalse(nur_func::is_static([])); | ||||
|       self::assertFalse(nur_func::is_static([""])); | ||||
|       self::assertFalse(nur_func::is_static([null, ""])); | ||||
|       self::assertFalse(nur_func::is_static(["xxx", ""])); | ||||
| 
 | ||||
|       self::assertTrue(nur_func::is_static("::xxx")); | ||||
|       self::assertTrue(nur_func::is_static(["xxx"])); | ||||
|       self::assertTrue(nur_func::is_static([null, "yyy"])); | ||||
|       self::assertTrue(nur_func::is_static(["xxx", "yyy"])); | ||||
|       self::assertTrue(nur_func::is_static([null, "yyy", "aaa"])); | ||||
|       self::assertTrue(nur_func::is_static(["xxx", "yyy", "aaa"])); | ||||
|     } | ||||
|      | ||||
|     function testFix_static() { | ||||
|       $class = "class"; | ||||
|       $func = "::xxx"; | ||||
|         nur_func::fix_static($func, $class); | ||||
|         self::assertSame("class::xxx", $func); | ||||
|       $func = ["xxx"]; | ||||
|         nur_func::fix_static($func, $class); | ||||
|         self::assertSame(["class", "xxx"], $func); | ||||
|       $func = [null, "yyy"]; | ||||
|         nur_func::fix_static($func, $class); | ||||
|         self::assertSame(["class", "yyy"], $func); | ||||
|       $func = ["xxx", "yyy"]; | ||||
|         nur_func::fix_static($func, $class); | ||||
|         self::assertSame(["xxx", "yyy"], $func); | ||||
|       $func = [null, "yyy", "aaa"]; | ||||
|         nur_func::fix_static($func, $class); | ||||
|         self::assertSame(["class", "yyy", "aaa"], $func); | ||||
|       $func = ["xxx", "yyy", "aaa"]; | ||||
|         nur_func::fix_static($func, $class); | ||||
|         self::assertSame(["xxx", "yyy", "aaa"], $func); | ||||
|     } | ||||
| 
 | ||||
|     function testIs_method() { | ||||
|       self::assertFalse(nur_func::is_method(null)); | ||||
|       self::assertFalse(nur_func::is_method("")); | ||||
|       self::assertFalse(nur_func::is_method("->")); | ||||
|       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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -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")); | ||||
|  | ||||
		Reference in New Issue
	
	Block a user