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