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