diff --git a/src/DI/TablesExtension.php b/src/DI/TablesExtension.php index 53894afaff6cc73492fdd84665132a890f981684..e07eee4708481e340f84af4c357857e34ec4abc0 100644 --- a/src/DI/TablesExtension.php +++ b/src/DI/TablesExtension.php @@ -7,6 +7,7 @@ namespace Grifart\Tables\DI; use Grifart\Tables\Database\Identifier; use Grifart\Tables\Scaffolding\PostgresReflector; use Grifart\Tables\Scaffolding\TablesDefinitions; +use Grifart\Tables\SingleConnectionTableManager; use Grifart\Tables\TableManager; use Grifart\Tables\TypeResolver; use Nette\DI\CompilerExtension; @@ -41,7 +42,8 @@ final class TablesExtension extends CompilerExtension $builder = $this->getContainerBuilder(); $builder->addDefinition($this->prefix('tableManager')) - ->setFactory(TableManager::class); + ->setType(TableManager::class) + ->setFactory(SingleConnectionTableManager::class); $builder->addDefinition($this->prefix('scaffolding')) ->setFactory(TablesDefinitions::class); diff --git a/src/SingleConnectionTableManager.php b/src/SingleConnectionTableManager.php new file mode 100644 index 0000000000000000000000000000000000000000..94127603689ca7f7556b13c3ae169ba11033fac7 --- /dev/null +++ b/src/SingleConnectionTableManager.php @@ -0,0 +1,245 @@ +<?php + +declare(strict_types=1); + +namespace Grifart\Tables; + +use Dibi\IConnection; +use Dibi\UniqueConstraintViolationException; +use Grifart\Tables\Conditions\Composite; +use Grifart\Tables\Conditions\Condition; +use Grifart\Tables\OrderBy\OrderBy; +use Grifart\Tables\OrderBy\OrderByDirection; +use Nette\Utils\Paginator; +use function Phun\map; +use function Phun\mapWithKeys; + +// todo: error handling +// todo: mapping of exceptions + +final class SingleConnectionTableManager implements TableManager +{ + + public function __construct( + private IConnection $connection, + ) {} + + /** + * @template TableType of Table + * @param TableType $table + * @param Modifications<TableType> $changes + * @throws RowWithGivenPrimaryKeyAlreadyExists + */ + public function insert(Table $table, Modifications $changes): void + { + \assert($changes->getPrimaryKey() === NULL); + + try { + $this->connection->query( + 'INSERT', + 'INTO %n.%n', $table::getSchema(), $table::getTableName(), + mapWithKeys( + $changes->getModifications(), + static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->toDatabase($value), + ), + ); + } catch (UniqueConstraintViolationException $e) { + throw new RowWithGivenPrimaryKeyAlreadyExists(previous: $e); + } + + \assert($this->connection->getAffectedRows() === 1); + } + + /** + * @template TableType of Table + * @param TableType $table + * @param PrimaryKey<TableType> $primaryKey + * @return ($required is true ? Row : Row|null) + * @throws RowNotFound + */ + public function find(Table $table, PrimaryKey $primaryKey, bool $required = true): ?Row + { + return $this->findOneBy($table, $primaryKey->getCondition($table), required: $required); + } + + /** + * @template TableType of Table + * @param TableType $table + * @param array<OrderBy|Expression<mixed>> $orderBy + * @return Row[] + */ + public function getAll(Table $table, array $orderBy = [], ?Paginator $paginator = null): array + { + return $this->findBy($table, [], $orderBy, $paginator); + } + + /** + * @template TableType of Table + * @param TableType $table + * @param Condition|Condition[] $conditions + * @param array<OrderBy|Expression<mixed>> $orderBy + * @return Row[] (subclass of row) + */ + public function findBy(Table $table, Condition|array $conditions, array $orderBy = [], ?Paginator $paginator = null): array + { + $result = $this->connection->query( + 'SELECT *', + 'FROM %n.%n', $table::getSchema(), $table::getTableName(), + 'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(), + 'ORDER BY %by', \count($orderBy) > 0 + ? map($orderBy, function (OrderBy|Expression $orderBy) { + if ($orderBy instanceof Expression) { + $orderBy = new OrderByDirection($orderBy); + } + + return $orderBy->toSql()->getValues(); + }) + : [['%sql', 'true::boolean']], + '%lmt', $paginator?->getItemsPerPage(), + '%ofs', $paginator?->getOffset(), + ); + + foreach ($table::getDatabaseColumns() as $column) { + $result->setType($column->getName(), NULL); + } + + $dibiRows = $result->fetchAll(); + + /** @var class-string<Row> $rowClass */ + $rowClass = $table::getRowClass(); + $modelRows = []; + foreach ($dibiRows as $dibiRow) { + \assert($dibiRow instanceof \Dibi\Row); + $modelRows[] = $rowClass::reconstitute( + mapWithKeys( + $dibiRow->toArray(), + static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->fromDatabase($value), + ), + ); + } + + if ($paginator !== null) { + $totalCount = $this->connection->query( + 'SELECT COUNT(*)', + 'FROM %n.%n', $table::getSchema(), $table::getTableName(), + 'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(), + )->fetchSingle(); + + \assert(\is_int($totalCount)); + $paginator->setItemCount($totalCount); + } + + return $modelRows; + } + + /** + * @template TableType of Table + * @param TableType $table + * @param Condition|Condition[] $conditions + * @param array<OrderBy|Expression<mixed>> $orderBy + * @return ($required is true ? Row : Row|null) + * @throws RowNotFound + */ + public function findOneBy(Table $table, Condition|array $conditions, array $orderBy = [], bool $required = true, bool $unique = true): ?Row + { + $result = $this->connection->query( + 'SELECT *', + 'FROM %n.%n', $table::getSchema(), $table::getTableName(), + 'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(), + 'ORDER BY %by', \count($orderBy) > 0 + ? map($orderBy, function (OrderBy|Expression $orderBy) { + if ($orderBy instanceof Expression) { + $orderBy = new OrderByDirection($orderBy); + } + + return $orderBy->toSql()->getValues(); + }) + : [['%sql', 'true::boolean']], + ); + + foreach ($table::getDatabaseColumns() as $column) { + $result->setType($column->getName(), NULL); + } + + $dibiRow = $result->fetch(); + if ($dibiRow === null) { + return ! $required ? null : throw new RowNotFound(); + } + + if ($unique && $result->fetch() !== null) { + throw new TooManyRowsFound(); + } + + /** @var class-string<Row> $rowClass */ + $rowClass = $table::getRowClass(); + \assert($dibiRow instanceof \Dibi\Row); + return $rowClass::reconstitute( + mapWithKeys( + $dibiRow->toArray(), + static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->fromDatabase($value), + ), + ); + } + + /** + * @template TableType of Table + * @param TableType $table + * @param Modifications<TableType> $changes + * @throws GivenSearchCriteriaHaveNotMatchedAnyRows if no rows matches given criteria + */ + public function update(Table $table, Modifications $changes): void + { + $primaryKey = $changes->getPrimaryKey(); + \assert($primaryKey !== NULL); + $this->connection->query( + 'UPDATE %n.%n', $table::getSchema(), $table::getTableName(), + 'SET %a', + mapWithKeys( + $changes->getModifications(), + static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->toDatabase($value), + ), + 'WHERE %ex', $primaryKey->getCondition($table)->toSql()->getValues(), + ); + $affectedRows = $this->connection->getAffectedRows(); + if ($affectedRows !== 1) { + if ($affectedRows === 0) { + throw new GivenSearchCriteriaHaveNotMatchedAnyRows(); + } + + throw new ProbablyBrokenPrimaryIndexImplementation($table, $affectedRows); + } + } + + /** + * @template TableType of Table + * @param TableType $table + * @param PrimaryKey<TableType> $primaryKey + */ + public function delete(Table $table, PrimaryKey $primaryKey): void + { + $this->connection->query( + 'DELETE', + 'FROM %n.%n', $table::getSchema(), $table::getTableName(), + 'WHERE %ex', $primaryKey->getCondition($table)->toSql()->getValues(), + ); + \assert($this->connection->getAffectedRows() === 1); + } + + /** + * @template TableType of Table + * @param TableType $table + * @param Modifications<TableType> $changes + * @throws RowWithGivenPrimaryKeyAlreadyExists + * @throws GivenSearchCriteriaHaveNotMatchedAnyRows + */ + public function save(Table $table, Modifications $changes): void { + if ($changes->getPrimaryKey() === NULL) { + // INSERT + $this->insert($table, $changes); + return; + } + + // UPDATE: + $this->update($table, $changes); + } +} diff --git a/src/TableManager.php b/src/TableManager.php index 98ab1507ca5da928bcaac803836844b60636481a..7575c1c8e0ea5acefac4a8183b093db270e7bf51 100644 --- a/src/TableManager.php +++ b/src/TableManager.php @@ -4,51 +4,20 @@ declare(strict_types=1); namespace Grifart\Tables; -use Dibi\IConnection; -use Dibi\UniqueConstraintViolationException; -use Grifart\Tables\Conditions\Composite; use Grifart\Tables\Conditions\Condition; use Grifart\Tables\OrderBy\OrderBy; -use Grifart\Tables\OrderBy\OrderByDirection; +use Grifart\Tables\Table as TableType; use Nette\Utils\Paginator; -use function Phun\map; -use function Phun\mapWithKeys; -// todo: error handling -// todo: mapping of exceptions - -final class TableManager +interface TableManager { - - public function __construct( - private IConnection $connection, - ) {} - /** * @template TableType of Table * @param TableType $table * @param Modifications<TableType> $changes * @throws RowWithGivenPrimaryKeyAlreadyExists */ - public function insert(Table $table, Modifications $changes): void - { - \assert($changes->getPrimaryKey() === NULL); - - try { - $this->connection->query( - 'INSERT', - 'INTO %n.%n', $table::getSchema(), $table::getTableName(), - mapWithKeys( - $changes->getModifications(), - static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->toDatabase($value), - ), - ); - } catch (UniqueConstraintViolationException $e) { - throw new RowWithGivenPrimaryKeyAlreadyExists(previous: $e); - } - - \assert($this->connection->getAffectedRows() === 1); - } + public function insert(Table $table, Modifications $changes): void; /** * @template TableType of Table @@ -57,21 +26,15 @@ final class TableManager * @return ($required is true ? Row : Row|null) * @throws RowNotFound */ - public function find(Table $table, PrimaryKey $primaryKey, bool $required = true): ?Row - { - return $this->findOneBy($table, $primaryKey->getCondition($table), required: $required); - } + public function find(Table $table, PrimaryKey $primaryKey, bool $required = true): ?Row; /** * @template TableType of Table * @param TableType $table - * @param OrderBy[] $orderBy + * @param array<OrderBy|Expression<mixed>> $orderBy * @return Row[] */ - public function getAll(Table $table, array $orderBy = [], ?Paginator $paginator = null): array - { - return $this->findBy($table, [], $orderBy, $paginator); - } + public function getAll(Table $table, array $orderBy = [], ?Paginator $paginator = null): array; /** * @template TableType of Table @@ -80,57 +43,7 @@ final class TableManager * @param array<OrderBy|Expression<mixed>> $orderBy * @return Row[] (subclass of row) */ - public function findBy(Table $table, Condition|array $conditions, array $orderBy = [], ?Paginator $paginator = null): array - { - $result = $this->connection->query( - 'SELECT *', - 'FROM %n.%n', $table::getSchema(), $table::getTableName(), - 'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(), - 'ORDER BY %by', \count($orderBy) > 0 - ? map($orderBy, function (OrderBy|Expression $orderBy) { - if ($orderBy instanceof Expression) { - $orderBy = new OrderByDirection($orderBy); - } - - return $orderBy->toSql()->getValues(); - }) - : [['%sql', 'true::boolean']], - '%lmt', $paginator?->getItemsPerPage(), - '%ofs', $paginator?->getOffset(), - ); - - foreach ($table::getDatabaseColumns() as $column) { - $result->setType($column->getName(), NULL); - } - - $dibiRows = $result->fetchAll(); - - /** @var class-string<Row> $rowClass */ - $rowClass = $table::getRowClass(); - $modelRows = []; - foreach ($dibiRows as $dibiRow) { - \assert($dibiRow instanceof \Dibi\Row); - $modelRows[] = $rowClass::reconstitute( - mapWithKeys( - $dibiRow->toArray(), - static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->fromDatabase($value), - ), - ); - } - - if ($paginator !== null) { - $totalCount = $this->connection->query( - 'SELECT COUNT(*)', - 'FROM %n.%n', $table::getSchema(), $table::getTableName(), - 'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(), - )->fetchSingle(); - - \assert(\is_int($totalCount)); - $paginator->setItemCount($totalCount); - } - - return $modelRows; - } + public function findBy(Table $table, Condition|array $conditions, array $orderBy = [], ?Paginator $paginator = null): array; /** * @template TableType of Table @@ -140,46 +53,7 @@ final class TableManager * @return ($required is true ? Row : Row|null) * @throws RowNotFound */ - public function findOneBy(Table $table, Condition|array $conditions, array $orderBy = [], bool $required = true, bool $unique = true): ?Row - { - $result = $this->connection->query( - 'SELECT *', - 'FROM %n.%n', $table::getSchema(), $table::getTableName(), - 'WHERE %ex', (\is_array($conditions) ? Composite::and(...$conditions) : $conditions)->toSql()->getValues(), - 'ORDER BY %by', \count($orderBy) > 0 - ? map($orderBy, function (OrderBy|Expression $orderBy) { - if ($orderBy instanceof Expression) { - $orderBy = new OrderByDirection($orderBy); - } - - return $orderBy->toSql()->getValues(); - }) - : [['%sql', 'true::boolean']], - ); - - foreach ($table::getDatabaseColumns() as $column) { - $result->setType($column->getName(), NULL); - } - - $dibiRow = $result->fetch(); - if ($dibiRow === null) { - return ! $required ? null : throw new RowNotFound(); - } - - if ($unique && $result->fetch() !== null) { - throw new TooManyRowsFound(); - } - - /** @var class-string<Row> $rowClass */ - $rowClass = $table::getRowClass(); - \assert($dibiRow instanceof \Dibi\Row); - return $rowClass::reconstitute( - mapWithKeys( - $dibiRow->toArray(), - static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->fromDatabase($value), - ), - ); - } + public function findOneBy(Table $table, Condition|array $conditions, array $orderBy = [], bool $required = true, bool $unique = true): ?Row; /** * @template TableType of Table @@ -187,43 +61,14 @@ final class TableManager * @param Modifications<TableType> $changes * @throws GivenSearchCriteriaHaveNotMatchedAnyRows if no rows matches given criteria */ - public function update(Table $table, Modifications $changes): void - { - $primaryKey = $changes->getPrimaryKey(); - \assert($primaryKey !== NULL); - $this->connection->query( - 'UPDATE %n.%n', $table::getSchema(), $table::getTableName(), - 'SET %a', - mapWithKeys( - $changes->getModifications(), - static fn(string $columnName, mixed $value) => $table->getTypeOf($columnName)->toDatabase($value), - ), - 'WHERE %ex', $primaryKey->getCondition($table)->toSql()->getValues(), - ); - $affectedRows = $this->connection->getAffectedRows(); - if ($affectedRows !== 1) { - if ($affectedRows === 0) { - throw new GivenSearchCriteriaHaveNotMatchedAnyRows(); - } - - throw new ProbablyBrokenPrimaryIndexImplementation($table, $affectedRows); - } - } + public function update(Table $table, Modifications $changes): void; /** * @template TableType of Table * @param TableType $table * @param PrimaryKey<TableType> $primaryKey */ - public function delete(Table $table, PrimaryKey $primaryKey): void - { - $this->connection->query( - 'DELETE', - 'FROM %n.%n', $table::getSchema(), $table::getTableName(), - 'WHERE %ex', $primaryKey->getCondition($table)->toSql()->getValues(), - ); - \assert($this->connection->getAffectedRows() === 1); - } + public function delete(Table $table, PrimaryKey $primaryKey): void; /** * @template TableType of Table @@ -232,34 +77,5 @@ final class TableManager * @throws RowWithGivenPrimaryKeyAlreadyExists * @throws GivenSearchCriteriaHaveNotMatchedAnyRows */ - public function save(Table $table, Modifications $changes): void { - if ($changes->getPrimaryKey() === NULL) { - // INSERT - $this->insert($table, $changes); - return; - } - - // UPDATE: - $this->update($table, $changes); - } - - /** - * @template T - * @param \Closure(): T $block - * @return T - */ - public function withConnection( - IConnection $connection, - \Closure $block, - ): mixed - { - $previousConnection = $this->connection; - $this->connection = $connection; - - try { - return $block(); - } finally { - $this->connection = $previousConnection; - } - } + public function save(Table $table, Modifications $changes): void; } diff --git a/tests/DI/TablesExtensionTest.phpt b/tests/DI/TablesExtensionTest.phpt index cb362b3d8ee8ea263d3da59663a5fa436d276d29..e602f9094b74ec7e66fe8bdd6f121feb6ddfbfe0 100644 --- a/tests/DI/TablesExtensionTest.phpt +++ b/tests/DI/TablesExtensionTest.phpt @@ -4,11 +4,10 @@ declare(strict_types=1); namespace Grifart\Tables\Tests\DI; -use Dibi\Connection; use Grifart\Tables\Database\Identifier; use Grifart\Tables\Scaffolding\TablesDefinitions; +use Grifart\Tables\SingleConnectionTableManager; use Grifart\Tables\TableManager; -use Grifart\Tables\Tests\Fixtures\UuidType; use Grifart\Tables\Tests\Fixtures\VersionType; use Grifart\Tables\TypeResolver; use Grifart\Tables\Types\DecimalType; @@ -40,7 +39,7 @@ $createContainer = function (string $configFile): Container (function () use ($createContainer) { $container = $createContainer('default'); - Assert::type(TableManager::class, $container->getByType(TableManager::class)); + Assert::type(SingleConnectionTableManager::class, $container->getByType(TableManager::class)); Assert::type(TablesDefinitions::class, $container->getByType(TablesDefinitions::class)); })(); diff --git a/tests/Fixtures/TestFixtures.php b/tests/Fixtures/TestFixtures.php index 212d43e7a11c3587ece4f9ee3b0958086c7c808e..a0f18aa620c31a3d26fe0508b4a65f506a6e7487 100644 --- a/tests/Fixtures/TestFixtures.php +++ b/tests/Fixtures/TestFixtures.php @@ -6,7 +6,7 @@ namespace Grifart\Tables\Tests\Fixtures; use Dibi\Connection; use Grifart\Tables\Database\Identifier; -use Grifart\Tables\TableManager; +use Grifart\Tables\SingleConnectionTableManager; use Grifart\Tables\TypeResolver; use Grifart\Tables\Types\ArrayType; use Grifart\Tables\Types\IntType; @@ -16,9 +16,9 @@ final class TestFixtures { use StaticClass; - public static function createTableManager(Connection $connection): TableManager + public static function createTableManager(Connection $connection): SingleConnectionTableManager { - return new TableManager($connection); + return new SingleConnectionTableManager($connection); } public static function createTypeResolver(Connection $connection): TypeResolver