533 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			533 lines
		
	
	
		
			17 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nulib\db;
 | |
| 
 | |
| use nulib\cl;
 | |
| use nulib\str;
 | |
| use Traversable;
 | |
| 
 | |
| /**
 | |
|  * Class CapacitorChannel: un canal d'une instance de {@link ICapacitor}
 | |
|  */
 | |
| class CapacitorChannel implements ITransactor {
 | |
|   const NAME = null;
 | |
| 
 | |
|   const TABLE_NAME = null;
 | |
| 
 | |
|   protected function COLUMN_DEFINITIONS(): ?array {
 | |
|     return static::COLUMN_DEFINITIONS;
 | |
|   } const COLUMN_DEFINITIONS = null;
 | |
| 
 | |
|   const PRIMARY_KEYS = null;
 | |
| 
 | |
|   const MIGRATION = null;
 | |
| 
 | |
|   const MANAGE_TRANSACTIONS = true;
 | |
| 
 | |
|   const EACH_COMMIT_THRESHOLD = 100;
 | |
| 
 | |
|   static function verifix_name(?string &$name, ?string &$tableName=null): void {
 | |
|     if ($name !== null) {
 | |
|       $name = strtolower($name);
 | |
|       $tableName ??= str_replace("-", "_", $name) . "_channel";
 | |
|     } else {
 | |
|       $name = static::class;
 | |
|       if ($name === self::class) {
 | |
|         $name = "default";
 | |
|         $tableName ??= "default_channel";
 | |
|       } else {
 | |
|         $name = preg_replace('/^.*\\\\/', "", $name);
 | |
|         $name = preg_replace('/Channel$/', "", $name);
 | |
|         $name = lcfirst($name);
 | |
|         $tableName ??= str::camel2us($name);
 | |
|         $name = strtolower($name);
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   protected static function verifix_eachCommitThreshold(?int $eachCommitThreshold): ?int {
 | |
|     $eachCommitThreshold ??= static::EACH_COMMIT_THRESHOLD;
 | |
|     if ($eachCommitThreshold < 0) $eachCommitThreshold = null;
 | |
|     return $eachCommitThreshold;
 | |
|   }
 | |
| 
 | |
|   function __construct(?string $name=null, ?int $eachCommitThreshold=null, ?bool $manageTransactions=null) {
 | |
|     $name ??= static::NAME;
 | |
|     $tableName ??= static::TABLE_NAME;
 | |
|     self::verifix_name($name, $tableName);
 | |
|     $this->name = $name;
 | |
|     $this->tableName = $tableName;
 | |
|     $this->manageTransactions = $manageTransactions ?? static::MANAGE_TRANSACTIONS;
 | |
|     $this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
 | |
|     $this->setup = false;
 | |
|     $this->created = false;
 | |
|     $columnDefinitions = $this->COLUMN_DEFINITIONS();
 | |
|     $primaryKeys = cl::withn(static::PRIMARY_KEYS);
 | |
|     $migration = cl::withn(static::MIGRATION);
 | |
|     $lastMkey = 1;
 | |
|     if ($columnDefinitions !== null) {
 | |
|       # mettre à jour la liste des clés primaires et des migrations
 | |
|       $index = 0;
 | |
|       foreach ($columnDefinitions as $col => $def) {
 | |
|         if ($col === $index) {
 | |
|           $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;
 | |
|           } elseif ($def === "genserial") {
 | |
|             $primaryKeys[] = $col;
 | |
|           }
 | |
|         }
 | |
|       }
 | |
|     }
 | |
|     $this->columnDefinitions = $columnDefinitions;
 | |
|     $this->primaryKeys = $primaryKeys;
 | |
|     $this->migration = $migration;
 | |
|   }
 | |
| 
 | |
|   protected string $name;
 | |
| 
 | |
|   function getName(): string {
 | |
|     return $this->name;
 | |
|   }
 | |
| 
 | |
|   protected string $tableName;
 | |
| 
 | |
|   function getTableName(): string {
 | |
|     return $this->tableName;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @var bool indiquer si les modifications de each doivent être gérées dans
 | |
|    * une transaction. si false, l'utilisateur doit lui même gérer la
 | |
|    * transaction.
 | |
|    */
 | |
|   protected bool $manageTransactions;
 | |
| 
 | |
|   function isManageTransactions(): bool {
 | |
|     return $this->manageTransactions;
 | |
|   }
 | |
| 
 | |
|   function setManageTransactions(bool $manageTransactions=true): self {
 | |
|     $this->manageTransactions = $manageTransactions;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @var ?int nombre maximum de modifications dans une transaction avant un
 | |
|    * commit automatique dans {@link Capacitor::each()}. Utiliser null pour
 | |
|    * désactiver la fonctionnalité.
 | |
|    *
 | |
|    * ce paramètre n'a d'effet que si $manageTransactions==true
 | |
|    */
 | |
|   protected ?int $eachCommitThreshold;
 | |
| 
 | |
|   function getEachCommitThreshold(): ?int {
 | |
|     return $this->eachCommitThreshold;
 | |
|   }
 | |
| 
 | |
|   function setEachCommitThreshold(?int $eachCommitThreshold=null): self {
 | |
|     $this->eachCommitThreshold = self::verifix_eachCommitThreshold($eachCommitThreshold);
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * initialiser ce channel avant sa première utilisation.
 | |
|    */
 | |
|   protected function setup(): void {
 | |
|   }
 | |
| 
 | |
|   protected bool $setup;
 | |
| 
 | |
|   function ensureSetup() {
 | |
|     if (!$this->setup) {
 | |
|       $this->setup();
 | |
|       $this->setup = true;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   protected bool $created;
 | |
| 
 | |
|   function isCreated(): bool {
 | |
|     return $this->created;
 | |
|   }
 | |
| 
 | |
|   function setCreated(bool $created=true): void {
 | |
|     $this->created = $created;
 | |
|   }
 | |
| 
 | |
|   protected ?array $columnDefinitions;
 | |
| 
 | |
|   /**
 | |
|    * retourner un ensemble de définitions pour des colonnes supplémentaires à
 | |
|    * insérer lors du chargement d'une valeur
 | |
|    *
 | |
|    * la clé primaire "id_" a pour définition "integer primary key autoincrement".
 | |
|    * elle peut être redéfinie, et dans ce cas la valeur à utiliser doit être
 | |
|    * retournée par {@link getItemValues()}
 | |
|    *
 | |
|    * la colonne "item__" contient la valeur sérialisée de l'élément chargé. bien
 | |
|    * que ce soit possible techniquement, cette colonne n'a pas à être redéfinie
 | |
|    *
 | |
|    * les colonnes dont le nom se termine par "_" sont réservées.
 | |
|    * les colonnes dont le nom se termine par "__" sont automatiquement sérialisées
 | |
|    * lors de l'insertion dans la base de données, et automatiquement désérialisées
 | |
|    * avant d'être retournées à l'utilisateur (sans le suffixe "__")
 | |
|    */
 | |
|   function getColumnDefinitions(): ?array {
 | |
|     return $this->columnDefinitions;
 | |
|   }
 | |
| 
 | |
|   protected ?array $migration;
 | |
| 
 | |
|   function getMigration(?string $prefix=null): ?array {
 | |
|     if ($prefix === null || $this->migration === null) return $this->migration;
 | |
|     $migration = null;
 | |
|     str::add_suffix($prefix, ":");
 | |
|     foreach ($this->migration as $mkey => $mdef) {
 | |
|       if (str::starts_with($prefix, $mkey)) {
 | |
|         $migration[$mkey] = $mdef;
 | |
|       } elseif (strpos($mkey, ":") === false) {
 | |
|         $migration[$mkey] = $mdef;
 | |
|       }
 | |
|     }
 | |
|     return $migration;
 | |
|   }
 | |
| 
 | |
|   protected ?array $primaryKeys;
 | |
| 
 | |
|   function getPrimaryKeys(): ?array {
 | |
|     return $this->primaryKeys;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * calculer les valeurs des colonnes supplémentaires à insérer pour le
 | |
|    * chargement de $item. pour une même valeur de $item, la valeur de retour
 | |
|    * doit toujours être la même. pour rajouter des valeurs supplémentaires qui
 | |
|    * dépendent de l'environnement, il faut plutôt les retournner dans
 | |
|    * {@link self::onCreate()} ou {@link self::onUpdate()}
 | |
|    *
 | |
|    * Cette méthode est utilisée par {@link Capacitor::charge()}. Si la clé
 | |
|    * primaire est incluse (il s'agit généralement de "id_"), la ligne
 | |
|    * correspondate est mise à jour si elle existe.
 | |
|    * Retourner la clé primaire par cette méthode est l'unique moyen de
 | |
|    * déclencher une mise à jour plutôt qu'une nouvelle création.
 | |
|    *
 | |
|    * Bien que ce ne soit pas prévu à la base, si on veut modifier $item dans
 | |
|    * cette méthode pour des raisons pratiques, il suffit de retournerla valeur
 | |
|    * modifiée avec la clé "item"
 | |
|    *
 | |
|    * Retourner [false] pour annuler le chargement
 | |
|    */
 | |
|   function getItemValues($item): ?array {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Avant d'utiliser un id pour rechercher dans la base de donnée, corriger sa
 | |
|    * valeur le cas échéant.
 | |
|    *
 | |
|    * Cette fonction assume que la clé primaire n'est pas multiple. Elle n'est
 | |
|    * pas utilisée si une clé primaire multiple est définie.
 | |
|    */
 | |
|   function verifixId(string &$id): void {
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * retourne true si un nouvel élément ou un élément mis à jour a été chargé.
 | |
|    * false si l'élément chargé est identique au précédent.
 | |
|    *
 | |
|    * cette méthode doit être utilisée dans {@link self::onUpdate()}
 | |
|    */
 | |
|   function wasRowModified(array $row, array $prow): bool {
 | |
|     return $row["item__sum_"] !== $prow["item__sum_"];
 | |
|   }
 | |
| 
 | |
|   final function serialize($item): ?string {
 | |
|     return $item !== null? serialize($item): null;
 | |
|   }
 | |
| 
 | |
|   final function unserialize(?string $serial) {
 | |
|     return $serial !== null? unserialize($serial): null;
 | |
|   }
 | |
| 
 | |
|   final function sum(?string $serial, $value=null): ?string {
 | |
|     if ($serial === null) $serial = $this->serialize($value);
 | |
|     return $serial !== null? sha1($serial): null;
 | |
|   }
 | |
| 
 | |
|   final function isSerialCol(string &$key): bool {
 | |
|     return str::del_suffix($key, "__");
 | |
|   }
 | |
| 
 | |
|   final function getSumCols(string $key): array {
 | |
|     return ["${key}__", "${key}__sum_"];
 | |
|   }
 | |
| 
 | |
|   function getSum(string $key, $value): array {
 | |
|     $sumCols = $this->getSumCols($key);
 | |
|     $serial = $this->serialize($value);
 | |
|     $sum = $this->sum($serial, $value);
 | |
|     return array_combine($sumCols, [$serial, $sum]);
 | |
|   }
 | |
| 
 | |
|   function wasSumModified(string $key, $value, array $prow): bool {
 | |
|     $sumCol = $this->getSumCols($key)[1];
 | |
|     $sum = $this->sum(null, $value);
 | |
|     $psum = $prow[$sumCol] ?? $this->sum(null, $prow[$key] ?? null);
 | |
|     return $sum !== $psum;
 | |
|   }
 | |
| 
 | |
|   function _wasSumModified(string $key, array $raw, array $praw): bool {
 | |
|     $sumCol = $this->getSumCols($key)[1];
 | |
|     $sum = $raw[$sumCol] ?? null;
 | |
|     $psum = $praw[$sumCol] ?? null;
 | |
|     return $sum !== $psum;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * méthode appelée lors du chargement avec {@link Capacitor::charge()} pour
 | |
|    * créer un nouvel élément
 | |
|    *
 | |
|    * @param mixed $item l'élément à charger
 | |
|    * @param array $row la ligne à créer, calculée à partir de $item et des
 | |
|    * valeurs retournées par {@link getItemValues()}
 | |
|    * @return ?array le cas échéant, un tableau non null à merger dans $row et
 | |
|    * utilisé pour provisionner la ligne nouvellement créée.
 | |
|    * Retourner [false] pour annuler le chargement (la ligne n'est pas créée)
 | |
|    *
 | |
|    * Si $item est modifié dans cette méthode, il est possible de le retourner
 | |
|    * avec la clé "item" pour mettre à jour la colonne correspondante.
 | |
|    *
 | |
|    * la création ou la mise à jour est uniquement décidée en fonction des
 | |
|    * valeurs calculées par {@link self::getItemValues()}. Bien que cette méthode
 | |
|    * peut techniquement retourner de nouvelles valeurs pour la clé primaire, ça
 | |
|    * risque de créer des doublons
 | |
|    */
 | |
|   function onCreate($item, array $row, ?array $alwaysNull): ?array {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * méthode appelée lors du chargement avec {@link Capacitor::charge()} pour
 | |
|    * mettre à jour un élément existant
 | |
|    *
 | |
|    * @param mixed $item l'élément à charger
 | |
|    * @param array $row la nouvelle ligne, calculée à partir de $item et
 | |
|    * des valeurs retournées par {@link getItemValues()}
 | |
|    * @param array $prow la précédente ligne, chargée depuis la base de
 | |
|    * données
 | |
|    * @return ?array null s'il ne faut pas mettre à jour la ligne. sinon, ce
 | |
|    * tableau est mergé dans $row puis utilisé pour mettre à jour la ligne
 | |
|    * existante
 | |
|    * Retourner [false] pour annuler le chargement (la ligne n'est pas mise à
 | |
|    * jour)
 | |
|    *
 | |
|    * - Il est possible de mettre à jour $item en le retourant avec la clé "item"
 | |
|    * - La clé primaire (il s'agit généralement de "id_") ne peut pas être
 | |
|    * modifiée. si elle est retournée, elle est ignorée
 | |
|    */
 | |
|   function onUpdate($item, array $row, array $prow): ?array {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * méthode appelée lors du parcours des éléments avec
 | |
|    * {@link Capacitor::each()}
 | |
|    *
 | |
|    * @param ?array $row la ligne courante. l'élément courant est accessible via
 | |
|    * $row["item"]
 | |
|    * @return ?array le cas échéant, un tableau non null utilisé pour mettre à
 | |
|    * jour la ligne courante
 | |
|    *
 | |
|    * - Il est possible de mettre à jour $item en le retourant avec la clé "item"
 | |
|    * - La clé primaire (il s'agit généralement de "id_") ne peut pas être
 | |
|    * modifiée. si elle est retournée, elle est ignorée
 | |
|    */
 | |
|   function onEach(array $row): ?array {
 | |
|     return null;
 | |
|   }
 | |
|   const onEach = "->".[self::class, "onEach"][1];
 | |
| 
 | |
|   /**
 | |
|    * méthode appelée lors du parcours des éléments avec
 | |
|    * {@link Capacitor::delete()}
 | |
|    *
 | |
|    * @param ?array $row la ligne courante. l'élément courant est accessible via
 | |
|    * $row["item"]
 | |
|    * @return bool true s'il faut supprimer la ligne, false sinon
 | |
|    */
 | |
|   function onDelete(array $row): bool {
 | |
|     return true;
 | |
|   }
 | |
|   const onDelete = "->".[self::class, "onDelete"][1];
 | |
| 
 | |
|   #############################################################################
 | |
|   # Méthodes déléguées pour des workflows centrés sur le channel
 | |
| 
 | |
|   /**
 | |
|    * @var Capacitor|null instance de Capacitor par laquelle cette instance est
 | |
|    * utilisée
 | |
|    */
 | |
|   protected ?Capacitor $capacitor;
 | |
| 
 | |
|   function getCapacitor(): ?Capacitor {
 | |
|     return $this->capacitor;
 | |
|   }
 | |
| 
 | |
|   function setCapacitor(Capacitor $capacitor): self {
 | |
|     $this->capacitor = $capacitor;
 | |
|     return $this;
 | |
|   }
 | |
| 
 | |
|   function initStorage(CapacitorStorage $storage): self {
 | |
|     new Capacitor($storage, $this);
 | |
|     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 &$row=null): int {
 | |
|     return $this->capacitor->charge($item, $func, $args, $row);
 | |
|   }
 | |
| 
 | |
|   function chargeAll(?iterable $items, $func=null, ?array $args=null): int {
 | |
|     return $this->capacitor->chargeAll($items, $func, $args);
 | |
|   }
 | |
| 
 | |
|   function discharge(bool $reset=true): Traversable {
 | |
|     return $this->capacitor->discharge($reset);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * retourner le filtre de base: les filtres de toutes les fonctions ci-dessous
 | |
|    * sont fusionnées avec le filtre de base
 | |
|    *
 | |
|    * cela permet de limiter toutes les opérations à un sous-ensemble des données
 | |
|    * du canal
 | |
|    */
 | |
|   function getBaseFilter(): ?array {
 | |
|     return null;
 | |
|   }
 | |
| 
 | |
|   protected function verifixFilter(&$filter): void {
 | |
|     if ($filter !== null && !is_array($filter)) {
 | |
|       $primaryKeys = $this->primaryKeys ?? ["id_"];
 | |
|       $id = $filter;
 | |
|       $this->verifixId($id);
 | |
|       $filter = [$primaryKeys[0] => $id];
 | |
|     }
 | |
|     $filter = cl::merge($this->getBaseFilter(), $filter);
 | |
|   }
 | |
| 
 | |
|   function count($filter=null): int {
 | |
|     $this->verifixFilter($filter);
 | |
|     return $this->capacitor->count($filter);
 | |
|   }
 | |
| 
 | |
|   function one($filter, ?array $mergeQuery=null): ?array {
 | |
|     $this->verifixFilter($filter);
 | |
|     return $this->capacitor->one($filter, $mergeQuery);
 | |
|   }
 | |
| 
 | |
|   function all($filter, ?array $mergeQuery=null): Traversable {
 | |
|     $this->verifixFilter($filter);
 | |
|     return $this->capacitor->all($filter, $mergeQuery);
 | |
|   }
 | |
| 
 | |
|   function each($filter, $func=null, ?array $args=null, ?array $mergeQuery=null, ?int &$nbUpdated=null): int {
 | |
|     $this->verifixFilter($filter);
 | |
|     return $this->capacitor->each($filter, $func, $args, $mergeQuery, $nbUpdated);
 | |
|   }
 | |
| 
 | |
|   function delete($filter, $func=null, ?array $args=null): int {
 | |
|     $this->verifixFilter($filter);
 | |
|     return $this->capacitor->delete($filter, $func, $args);
 | |
|   }
 | |
| 
 | |
|   function dbAll(array $query, ?array $params=null): iterable {
 | |
|     return $this->capacitor->dbAll($query, $params);
 | |
|   }
 | |
| 
 | |
|   function dbOne(array $query, ?array $params=null): ?array {
 | |
|     return $this->capacitor->dbOne($query, $params);
 | |
|   }
 | |
| 
 | |
|   /** @return int|false */
 | |
|   function dbUpdate(array $query, ?array $params=null) {
 | |
|     return $this->capacitor->dbUpdate($query, $params);
 | |
|   }
 | |
| 
 | |
|   function close(): void {
 | |
|     $this->capacitor->close();
 | |
|   }
 | |
| }
 |