$sqlite->file, "flags" => $sqlite->flags, "encryption_key" => $sqlite->encryptionKey, "allow_wal" => $sqlite->allowWal, "config" => $sqlite->config, "migrate" => $sqlite->migration, ], $params)); } elseif (is_array($sqlite)) { return new static(null, cl::merge($sqlite, $params)); } else { return new static($sqlite, $params); } } static function config_enableExceptions(self $sqlite): void { $sqlite->db->enableExceptions(true); } const CONFIG_enableExceptions = [self::class, "config_enableExceptions"]; /** * @var int temps maximum à attendre que la base soit accessible si elle est * verrouillée */ protected const BUSY_TIMEOUT = 30 * 1000; static function config_busyTimeout(self $sqlite): void { $sqlite->db->busyTimeout(static::BUSY_TIMEOUT); } const CONFIG_busyTimeout = [self::class, "config_busyTimeout"]; static function config_enableWalIfAllowed(self $sqlite): void { if ($sqlite->isWalAllowed()) { $sqlite->db->exec("PRAGMA journal_mode=WAL"); } } const CONFIG_enableWalIfAllowed = [self::class, "config_enableWalIfAllowed"]; const ALLOW_WAL = null; const DEFAULT_CONFIG = [ self::CONFIG_enableExceptions, self::CONFIG_busyTimeout, self::CONFIG_enableWalIfAllowed, ]; const CONFIG = null; const MIGRATE = null; const params_SCHEMA = [ "file" => ["string", ""], "flags" => ["int", SQLITE3_OPEN_READWRITE + SQLITE3_OPEN_CREATE], "encryption_key" => ["string", ""], "allow_wal" => ["?bool"], "replace_config" => ["?array|callable"], "config" => ["?array|callable"], "migrate" => ["?array|string|callable"], "auto_open" => ["bool", true], ]; function __construct(?string $file=null, ?array $params=null) { if ($file !== null) $params["file"] = $file; ##schéma $defaultFile = self::params_SCHEMA["file"][1]; $this->file = $file = strval($params["file"] ?? $defaultFile); $inMemory = $file === ":memory:" || $file === ""; # $defaultFlags = self::params_SCHEMA["flags"][1]; $this->flags = intval($params["flags"] ?? $defaultFlags); # $defaultEncryptionKey = self::params_SCHEMA["encryption_key"][1]; $this->encryptionKey = strval($params["encryption_key"] ?? $defaultEncryptionKey); # $defaultAllowWal = static::ALLOW_WAL ?? !$inMemory; $this->allowWal = $params["allow_wal"] ?? $defaultAllowWal; # configuration $config = $params["replace_config"] ?? null; if ($config === null) { $config = $params["config"] ?? static::CONFIG; if (is_callable($config)) $config = [$config]; $config = cl::merge(static::DEFAULT_CONFIG, $config); } $this->config = $config; # migrations $this->migration = $params["migrate"] ?? static::MIGRATE; # $defaultAutoOpen = self::params_SCHEMA["auto_open"][1]; $this->inTransaction = false; if ($params["auto_open"] ?? $defaultAutoOpen) { $this->open(); } } /** @var string */ protected $file; /** @var int */ protected $flags; /** @var string */ protected $encryptionKey; /** @var bool */ protected $allowWal; /** vérifier s'il est autorisé de configurer le mode WAL */ function isWalAllowed(): bool { return $this->allowWal; } /** @var array|string|callable */ protected $config; /** @var array|string|callable */ protected $migration; /** @var SQLite3 */ protected $db; protected bool $inTransaction; 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); $this->inTransaction = false; } return $this; } function close(): void { if ($this->db !== null) { $this->db->close(); $this->db = null; $this->inTransaction = false; } } protected function checkStmt($stmt): SQLite3Stmt { return SqliteException::check($this->db, $stmt); } protected function checkResult($result): SQLite3Result { return SqliteException::check($this->db, $result); } protected function db(): SQLite3 { $this->open(); return $this->db; } function _exec(string $query): bool { 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($query, $params); if ($query->useStmt($db, $stmt, $sql)) { try { $result = $stmt->execute(); if ($result === false) return false; $result->finalize(); if ($query->isInsert()) return $db->lastInsertRowID(); else return $db->changes(); } finally { $stmt->close(); } } else { $result = $db->exec($sql); if ($result === false) return false; if (self::is_insert($sql)) return $db->lastInsertRowID(); else return $db->changes(); } } /** @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 { #XXX très imparfait, mais y'a rien de mieux pour le moment :-( return $this->inTransaction; } function beginTransaction(?callable $func=null, bool $commit=true): void { $this->db()->exec("begin"); $this->inTransaction = true; 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->inTransaction = false; $this->db()->exec("commit"); if ($this->transactors !== null) { foreach ($this->transactors as $transactor) { $transactor->commit(); } } } function rollback(): void { $this->inTransaction = false; $this->db()->exec("rollback"); if ($this->transactors !== null) { foreach ($this->transactors as $transactor) { $transactor->rollback(); } } } function _get(string $query, bool $entireRow=false) { return $this->db()->querySingle($query, $entireRow); } function get($query, ?array $params=null, bool $entireRow=false) { $db = $this->db(); $query = new query($query, $params); if ($query->useStmt($db, $stmt, $sql)) { try { $result = $this->checkResult($stmt->execute()); try { $row = $result->fetchArray(SQLITE3_ASSOC); if ($row === false) return null; $this->verifixRow($row); if ($entireRow) return $row; else return cl::first($row); } finally { $result->finalize(); } } finally { $stmt->close(); } } else { return $db->querySingle($sql, $entireRow); } } function one($query, ?array $params=null): ?array { return $this->get($query, $params, true); } protected function _fetchResult(SQLite3Result $result, ?SQLite3Stmt $stmt=null, $primaryKeys=null): Generator { if ($primaryKeys !== null) $primaryKeys = cl::with($primaryKeys); try { while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) { $this->verifixRow($row); if ($primaryKeys !== null) { $key = implode("-", cl::select($row, $primaryKeys)); yield $key => $row; } else { yield $row; } } } finally { $result->finalize(); if ($stmt !== null) $stmt->close(); } } /** * 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($query, $params); if ($query->useStmt($db, $stmt, $sql)) { $result = $this->checkResult($stmt->execute()); return $this->_fetchResult($result, $stmt, $primaryKeys); } else { $result = $this->checkResult($db->query($sql)); return $this->_fetchResult($result, null, $primaryKeys); } } }