ajout transactor

This commit is contained in:
Jephté Clain 2024-06-20 12:36:46 +04:00
parent ff6c5e8da4
commit ad603e8e81
10 changed files with 279 additions and 46 deletions

View File

@ -1,11 +1,14 @@
<?php
namespace nur\sery\db;
use nur\sery\php\func;
use nur\sery\ValueException;
/**
* Class Capacitor: un objet permettant d'attaquer un canal spécifique d'une
* instance de {@link CapacitorStorage}
*/
class Capacitor {
class Capacitor implements ITransactor {
function __construct(CapacitorStorage $storage, CapacitorChannel $channel, bool $ensureExists=true) {
$this->storage = $storage;
$this->channel = $channel;
@ -34,6 +37,80 @@ class Capacitor {
return $this->getChannel()->getTableName();
}
/** @var CapacitorChannel[] */
protected ?array $subChannels = null;
protected ?array $subManageTransactions = null;
function willUpdate(...$channels): self {
if ($this->subChannels === null) {
# désactiver la gestion des transaction sur le channel local aussi
$this->subChannels[] = $this->channel;
}
if ($channels) {
foreach ($channels as $channel) {
if ($channel instanceof Capacitor) $channel = $channel->getChannel();
if ($channel instanceof CapacitorChannel) {
$this->subChannels[] = $channel;
} else {
throw ValueException::invalid_type($channel, CapacitorChannel::class);
}
}
}
return $this;
}
function inTransaction(): bool {
return $this->db()->inTransaction();
}
function beginTransaction(?callable $func=null): void {
if ($this->subManageTransactions === null && $this->subChannels !== null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$this->subManageTransactions ??= [];
if (!array_key_exists($name, $this->subManageTransactions)) {
$this->subManageTransactions[$name] = $channel->isManageTransactions();
}
$channel->setManageTransactions(false);
}
$db = $this->db();
if (!$db->inTransaction()) $db->beginTransaction();
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
$this->commit();
$commited = true;
} finally {
if (!$commited) $this->rollback();
}
}
}
protected function beforeEndTransaction(): void {
if ($this->subManageTransactions !== null) {
foreach ($this->subChannels as $channel) {
$name = $channel->getName();
$channel->setManageTransactions($this->subManageTransactions[$name]);
}
$this->subManageTransactions = null;
}
}
function commit(): void {
$this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $this->db()->commit();
}
function rollback(): void {
$this->beforeEndTransaction();
$db = $this->db();
if ($db->inTransaction()) $this->db()->rollback();
}
function getCreateSql(): string {
return $this->storage->_getCreateSql($this->channel);
}
@ -51,6 +128,7 @@ class Capacitor {
}
function charge($item, $func=null, ?array $args=null, ?array &$values=null): int {
$this->beginTransaction();
return $this->storage->_charge($this->channel, $item, $func, $args, $values);
}
@ -71,10 +149,12 @@ class Capacitor {
}
function each($filter, $func=null, ?array $args=null): int {
$this->beginTransaction();
return $this->storage->_each($this->channel, $filter, $func, $args);
}
function delete($filter, $func=null, ?array $args=null): int {
$this->beginTransaction();
return $this->storage->_delete($this->channel, $filter, $func, $args);
}

View File

@ -204,6 +204,7 @@ EOT;
function _charge(CapacitorChannel $channel, $item, $func, ?array $args, ?array &$values=null): int {
$this->_create($channel);
$tableName = $channel->getTableName();
$db = $this->db();
$initFunc = [$channel, "getItemValues"];
$initArgs = $args;
@ -217,7 +218,7 @@ EOT;
$rowIds = $this->getRowIds($channel, $row, $primaryKeys);
if ($rowIds !== null) {
# modification
$prow = $this->db()->one([
$prow = $db->one([
"select",
"from" => $tableName,
"where" => $rowIds,
@ -276,28 +277,41 @@ EOT;
}
}
if ($insert === null) {
# aucune modification
return 0;
} elseif ($insert) {
$id = $this->db()->exec([
"insert",
"into" => $tableName,
"values" => $row,
]);
if (count($primaryKeys) == 1 && $rowIds === null) {
# mettre à jour avec l'id généré
$values[$primaryKeys[0]] = $id;
}
} else {
$this->db()->exec([
"update",
"table" => $tableName,
"values" => $row,
"where" => $rowIds,
]);
# aucune modification
if ($insert === null) return 0;
$manageTransactions = $channel->isManageTransactions();
if ($manageTransactions) {
$commited = false;
$db->beginTransaction();
}
try {
if ($insert) {
$id = $db->exec([
"insert",
"into" => $tableName,
"values" => $row,
]);
if (count($primaryKeys) == 1 && $rowIds === null) {
# mettre à jour avec l'id généré
$values[$primaryKeys[0]] = $id;
}
} else {
$db->exec([
"update",
"table" => $tableName,
"values" => $row,
"where" => $rowIds,
]);
}
if ($manageTransactions) {
$db->commit();
$commited = true;
}
return 1;
} finally {
if ($manageTransactions && !$commited) $db->rollback();
}
return 1;
}
function charge(?string $channel, $item, $func=null, ?array $args=null, ?array &$values=null): int {

View File

@ -1,13 +1,7 @@
<?php
namespace nur\sery\db;
interface IDatabase {
function beginTransaction(): void;
function commit(): void;
function rollback(): void;
interface IDatabase extends ITransactor {
/**
* - 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

22
src/db/ITransactor.php Normal file
View File

@ -0,0 +1,22 @@
<?php
namespace nur\sery\db;
/**
* Class ITransactor: un objet qui peut faire des opérations dans une
* transaction
*/
interface ITransactor {
/**
* Indiquer qu'une transaction va être étendue à tous les objets mentionnés
*/
function willUpdate(...$transactors): self;
function inTransaction(): bool;
/** si $func!==null, la lancer puis commiter la transaction */
function beginTransaction(?callable $func=null): void;
function commit(): void;
function rollback(): void;
}

View File

@ -4,9 +4,11 @@ namespace nur\sery\db\pdo;
use Generator;
use nur\sery\cl;
use nur\sery\db\IDatabase;
use nur\sery\db\ITransactor;
use nur\sery\php\func;
use nur\sery\php\time\Date;
use nur\sery\php\time\DateTime;
use nur\sery\ValueException;
class Pdo implements IDatabase {
static function with($pdo, ?array $params=null): self {
@ -144,16 +146,60 @@ class Pdo implements IDatabase {
}
}
function beginTransaction(): void {
/** @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 {
return $this->db()->inTransaction();
}
function beginTransaction(?callable $func=null): void {
$this->db()->beginTransaction();
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->beginTransaction();
}
}
if ($func !== null) {
$commited = false;
try {
func::call($func, $this);
$this->commit();
$commited = true;
} finally {
if (!$commited) $this->rollback();
}
}
}
function commit(): void {
$this->db()->commit();
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->commit();
}
}
}
function rollback(): void {
$this->db()->rollBack();
if ($this->transactors !== null) {
foreach ($this->transactors as $transactor) {
$transactor->rollback();
}
}
}
/**

View File

@ -82,7 +82,10 @@ class _query_base extends _base {
}
}
const DEBUG_QUERIES = false;
function useStmt(\PDO $db, ?\PDOStatement &$stmt=null, ?string &$sql=null): bool {
if (static::DEBUG_QUERIES) error_log($this->sql); #XXX
if ($this->bindings !== null) {
$stmt = $db->prepare($this->sql);
foreach ($this->bindings as $name => $value) {

View File

@ -4,6 +4,9 @@ namespace nur\sery\db\sqlite;
use Generator;
use nur\sery\cl;
use nur\sery\db\IDatabase;
use nur\sery\db\ITransactor;
use nur\sery\php\func;
use nur\sery\ValueException;
use SQLite3;
use SQLite3Result;
use SQLite3Stmt;
@ -82,6 +85,7 @@ class Sqlite implements IDatabase {
$this->migration = $params["migrate"] ?? static::MIGRATE;
#
$defaultAutoOpen = self::params_SCHEMA["auto_open"][1];
$this->inTransaction = false;
if ($params["auto_open"] ?? $defaultAutoOpen) {
$this->open();
}
@ -113,11 +117,14 @@ class Sqlite implements IDatabase {
/** @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;
}
@ -126,6 +133,7 @@ class Sqlite implements IDatabase {
if ($this->db !== null) {
$this->db->close();
$this->db = null;
$this->inTransaction = false;
}
}
@ -172,16 +180,64 @@ class Sqlite implements IDatabase {
}
}
function beginTransaction(): void {
/** @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): 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);
$this->commit();
$commited = true;
} finally {
if (!$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->db()->exec("commit");
$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) {

View File

@ -32,7 +32,10 @@ class _query_base extends _base {
}
}
const DEBUG_QUERIES = false;
function useStmt(SQLite3 $db, ?SQLite3Stmt &$stmt=null, ?string &$sql=null): bool {
if (static::DEBUG_QUERIES) error_log($this->sql); #XXX
if ($this->bindings !== null) {
/** @var SQLite3Stmt $stmt */
$stmt = SqliteException::check($db, $db->prepare($this->sql));

View File

@ -233,6 +233,18 @@ class DateTime extends \DateTime {
return static::with($this->add(new \DateInterval("P${nbDays}D")));
}
function getElapsedAt(?DateTime $now=null, ?int $resolution=null): string {
return Elapsed::format_at($this, $now, $resolution);
}
function getElapsedSince(?DateTime $now=null, ?int $resolution=null): string {
return Elapsed::format_since($this, $now, $resolution);
}
function getElapsedDelay(?DateTime $now=null, ?int $resolution=null): string {
return Elapsed::format_delay($this, $now, $resolution);
}
function __toString(): string {
return $this->format();
}

View File

@ -10,14 +10,16 @@ class Elapsed {
const DAY = 60 * 60 * 24;
/** @var int résolution */
const RES_SECONDS = 0, RES_MINUTES = 1, RES_HOURS = 2, RES_DAYS = 3;
const RESOLUTION_SECONDS = 0, RESOLUTION_MINUTES = 1, RESOLUTION_HOURS = 2, RESOLUTION_DAYS = 3;
const DEFAULT_RESOLUTION = self::RESOLUTION_SECONDS;
private static function format(int $seconds, int $resolution, ?string $prefix=null, ?string $zero=null): string {
if ($prefix === null) $prefix = "depuis";
switch ($resolution) {
case self::RES_DAYS: return self::format_days($seconds, $prefix, $zero);
case self::RES_HOURS: return self::format_hours($seconds, $prefix, $zero);
case self::RES_MINUTES: return self::format_minutes($seconds, $prefix, $zero);
case self::RESOLUTION_DAYS: return self::format_days($seconds, $prefix, $zero);
case self::RESOLUTION_HOURS: return self::format_hours($seconds, $prefix, $zero);
case self::RESOLUTION_MINUTES: return self::format_minutes($seconds, $prefix, $zero);
default: return self::format_seconds($seconds, $prefix, $zero);
}
}
@ -122,27 +124,28 @@ class Elapsed {
return self::format_generic($prefix, $d, 0, 0);
}
static function format_at(DateTime $start, ?DateTime $now=null): string {
static function format_at(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
$now ??= new DateTime();
$seconds = $now->getTimestamp() - $start->getTimestamp();
return (new self($seconds))->formatAt();
return (new self($seconds, $resolution))->formatAt();
}
static function format_since(DateTime $start, ?DateTime $now=null): string {
static function format_since(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
$now ??= new DateTime();
$seconds = $now->getTimestamp() - $start->getTimestamp();
return (new self($seconds))->formatSince();
return (new self($seconds, $resolution))->formatSince();
}
static function format_delay(DateTime $start, ?DateTime $now=null): string {
static function format_delay(DateTime $start, ?DateTime $now=null, ?int $resolution=null): string {
$now ??= new DateTime();
$seconds = $now->getTimestamp() - $start->getTimestamp();
return (new self($seconds))->formatDelay();
return (new self($seconds, $resolution))->formatDelay();
}
function __construct(int $seconds, int $resolution=self::RES_SECONDS) {
if ($resolution < self::RES_SECONDS) $resolution = self::RES_SECONDS;
elseif ($resolution > self::RES_DAYS) $resolution = self::RES_DAYS;
function __construct(int $seconds, ?int $resolution=null) {
$resolution ??= static::DEFAULT_RESOLUTION;
if ($resolution < self::RESOLUTION_SECONDS) $resolution = self::RESOLUTION_SECONDS;
elseif ($resolution > self::RESOLUTION_DAYS) $resolution = self::RESOLUTION_DAYS;
$this->seconds = $seconds;
$this->resolution = $resolution;
}