Skip to content

Commit

Permalink
Merge pull request #4 from compositephp/dev
Browse files Browse the repository at this point in the history
0.3.3 setup
  • Loading branch information
compositephp authored Oct 21, 2023
2 parents e4543eb + 63416c6 commit c63426c
Show file tree
Hide file tree
Showing 18 changed files with 224 additions and 125 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/.gitattributes export-ignore
/.gitignore export-ignore
/.github export-ignore
/doc export-ignore
/phpunit.xml export-ignore
/tests export-ignore
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"php": "^8.1",
"ext-pdo": "*",
"psr/simple-cache": "1 - 3",
"compositephp/entity": "^0.1.8",
"compositephp/entity": "^0.1.9",
"doctrine/dbal": "^3.5"
},
"require-dev": {
Expand Down
4 changes: 2 additions & 2 deletions src/AbstractCachedTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Composite\DB\Exceptions\DbException;
use Composite\Entity\AbstractEntity;
use Psr\SimpleCache\CacheInterface;
use Ramsey\Uuid\UuidInterface;

abstract class AbstractCachedTable extends AbstractTable
{
Expand Down Expand Up @@ -196,9 +197,8 @@ protected function findMultiCachedInternal(array $ids, null|int|\DateInterval $t

/**
* @param string|int|array<string, mixed>|AbstractEntity $keyOrEntity
* @throws \Composite\Entity\Exceptions\EntityException
*/
protected function getOneCacheKey(string|int|array|AbstractEntity $keyOrEntity): string
protected function getOneCacheKey(string|int|array|AbstractEntity|UuidInterface $keyOrEntity): string
{
if (!is_array($keyOrEntity)) {
$condition = $this->getPkCondition($keyOrEntity);
Expand Down
135 changes: 48 additions & 87 deletions src/AbstractTable.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
namespace Composite\DB;

use Composite\DB\MultiQuery\MultiInsert;
use Composite\DB\MultiQuery\MultiSelect;
use Composite\Entity\Helpers\DateTimeHelper;
use Composite\Entity\AbstractEntity;
use Composite\DB\Exceptions\DbException;
use Composite\Entity\Exceptions\EntityException;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\DBAL\Query\QueryBuilder;
use Ramsey\Uuid\UuidInterface;

abstract class AbstractTable
{
Expand Down Expand Up @@ -70,35 +72,20 @@ public function save(AbstractEntity &$entity): void
$entity->updated_at = new \DateTimeImmutable();
$changedColumns['updated_at'] = DateTimeHelper::dateTimeToString($entity->updated_at);
}

if ($this->config->hasOptimisticLock() && isset($entity->version)) {
$currentVersion = $entity->version;
try {
$connection->beginTransaction();
$connection->update(
$this->getTableName(),
$changedColumns,
$where
);
$versionUpdated = $connection->update(
$this->getTableName(),
['version' => $currentVersion + 1],
$where + ['version' => $currentVersion]
);
if (!$versionUpdated) {
throw new DbException('Failed to update entity version, concurrency modification, rolling back.');
}
$connection->commit();
} catch (\Throwable $e) {
$connection->rollBack();
throw $e;
}
} else {
$connection->update(
$this->getTableName(),
$changedColumns,
$where
);
if ($this->config->hasOptimisticLock()
&& method_exists($entity, 'getVersion')
&& method_exists($entity, 'incrementVersion')) {
$where['lock_version'] = $entity->getVersion();
$entity->incrementVersion();
$changedColumns['lock_version'] = $entity->getVersion();
}
$entityUpdated = $connection->update(
table: $this->getTableName(),
data: $changedColumns,
criteria: $where,
);
if ($this->config->hasOptimisticLock() && !$entityUpdated) {
throw new Exceptions\LockException('Failed to update entity version, concurrency modification, rolling back.');
}
$entity->resetChangedColumns();
}
Expand Down Expand Up @@ -211,66 +198,31 @@ protected function findByPkInternal(mixed $pk): ?array

/**
* @param array<string, mixed> $where
* @param array<string, string>|string $orderBy
* @return array<string, mixed>|null
* @throws \Doctrine\DBAL\Exception
*/
protected function findOneInternal(array $where): ?array
protected function findOneInternal(array $where, array|string $orderBy = []): ?array
{
$query = $this->select();
$this->buildWhere($query, $where);
$this->applyOrderBy($query, $orderBy);
return $query->fetchAssociative() ?: null;
}

/**
* @param array<int|string|array<string,mixed>> $pkList
* @return array<array<string, mixed>>
* @throws DbException
* @throws EntityException
* @throws \Doctrine\DBAL\Exception
*/
protected function findMultiInternal(array $pkList): array
{
if (!$pkList) {
return [];
}
/** @var class-string<AbstractEntity> $class */
$class = $this->config->entityClass;

$pkColumns = [];
foreach ($this->config->primaryKeys as $primaryKeyName) {
$pkColumns[$primaryKeyName] = $class::schema()->getColumn($primaryKeyName);
}
if (count($pkColumns) === 1) {
if (!array_is_list($pkList)) {
throw new DbException('Input argument $pkList must be list');
}
/** @var \Composite\Entity\Columns\AbstractColumn $pkColumn */
$pkColumn = reset($pkColumns);
$preparedPkValues = array_map(fn ($pk) => $pkColumn->uncast($pk), $pkList);
$query = $this->select();
$this->buildWhere($query, [$pkColumn->name => $preparedPkValues]);
} else {
$query = $this->select();
$expressions = [];
foreach ($pkList as $i => $pkArray) {
if (!is_array($pkArray)) {
throw new DbException('For tables with composite keys, input array must consist associative arrays');
}
$pkOrExpr = [];
foreach ($pkArray as $pkName => $pkValue) {
if (is_string($pkName) && isset($pkColumns[$pkName])) {
$preparedPkValue = $pkColumns[$pkName]->cast($pkValue);
$pkOrExpr[] = $query->expr()->eq($pkName, ':' . $pkName . $i);
$query->setParameter($pkName . $i, $preparedPkValue);
}
}
if ($pkOrExpr) {
$expressions[] = $query->expr()->and(...$pkOrExpr);
}
}
$query->where($query->expr()->or(...$expressions));
}
return $query->executeQuery()->fetchAllAssociative();
$multiSelect = new MultiSelect($this->getConnection(), $this->config, $pkList);
return $multiSelect->getQueryBuilder()->executeQuery()->fetchAllAssociative();
}

/**
Expand All @@ -294,22 +246,7 @@ protected function findAllInternal(
$query->setParameter($param, $value);
}
}
if ($orderBy) {
if (is_array($orderBy)) {
foreach ($orderBy as $column => $direction) {
$query->addOrderBy($column, $direction);
}
} else {
foreach (explode(',', $orderBy) as $orderByPart) {
$orderByPart = trim($orderByPart);
if (preg_match('/(.+)\s(asc|desc)$/i', $orderByPart, $orderByPartMatch)) {
$query->addOrderBy($orderByPartMatch[1], $orderByPartMatch[2]);
} else {
$query->addOrderBy($orderByPart);
}
}
}
}
$this->applyOrderBy($query, $orderBy);
if ($limit > 0) {
$query->setMaxResults($limit);
}
Expand Down Expand Up @@ -366,7 +303,7 @@ final protected function createEntities(mixed $data, ?string $keyColumnName = nu
* @return array<string, mixed>
* @throws EntityException
*/
protected function getPkCondition(int|string|array|AbstractEntity $data): array
protected function getPkCondition(int|string|array|AbstractEntity|UuidInterface $data): array
{
$condition = [];
if ($data instanceof AbstractEntity) {
Expand Down Expand Up @@ -395,7 +332,7 @@ protected function select(string $select = '*'): QueryBuilder
/**
* @param array<string, mixed> $where
*/
private function buildWhere(QueryBuilder $query, array $where): void
private function buildWhere(\Doctrine\DBAL\Query\QueryBuilder $query, array $where): void
{
foreach ($where as $column => $value) {
if ($value === null) {
Expand Down Expand Up @@ -433,4 +370,28 @@ private function formatData(array $data): array
}
return $data;
}

/**
* @param array<string, string>|string $orderBy
*/
private function applyOrderBy(QueryBuilder $query, string|array $orderBy): void
{
if (!$orderBy) {
return;
}
if (is_array($orderBy)) {
foreach ($orderBy as $column => $direction) {
$query->addOrderBy($column, $direction);
}
} else {
foreach (explode(',', $orderBy) as $orderByPart) {
$orderByPart = trim($orderByPart);
if (preg_match('/(.+)\s(asc|desc)$/i', $orderByPart, $orderByPartMatch)) {
$query->addOrderBy($orderByPartMatch[1], $orderByPartMatch[2]);
} else {
$query->addOrderBy($orderByPart);
}
}
}
}
}
7 changes: 7 additions & 0 deletions src/Exceptions/LockException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php declare(strict_types=1);

namespace Composite\DB\Exceptions;

class LockException extends DbException
{
}
64 changes: 64 additions & 0 deletions src/MultiQuery/MultiSelect.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php declare(strict_types=1);

namespace Composite\DB\MultiQuery;

use Composite\DB\Exceptions\DbException;
use Composite\DB\TableConfig;
use Composite\Entity\AbstractEntity;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Query\QueryBuilder;

class MultiSelect
{
private readonly QueryBuilder $queryBuilder;

public function __construct(
Connection $connection,
TableConfig $tableConfig,
array $condition,
) {
$query = $connection->createQueryBuilder()->select('*')->from($tableConfig->tableName);
/** @var class-string<AbstractEntity> $class */
$class = $tableConfig->entityClass;

$pkColumns = [];
foreach ($tableConfig->primaryKeys as $primaryKeyName) {
$pkColumns[$primaryKeyName] = $class::schema()->getColumn($primaryKeyName);
}

if (count($pkColumns) === 1) {
if (!array_is_list($condition)) {
throw new DbException('Input argument $pkList must be list');
}
/** @var \Composite\Entity\Columns\AbstractColumn $pkColumn */
$pkColumn = reset($pkColumns);
$preparedPkValues = array_map(fn ($pk) => $pkColumn->uncast($pk), $condition);
$query->andWhere($query->expr()->in($pkColumn->name, $preparedPkValues));
} else {
$expressions = [];
foreach ($condition as $i => $pkArray) {
if (!is_array($pkArray)) {
throw new DbException('For tables with composite keys, input array must consist associative arrays');
}
$pkOrExpr = [];
foreach ($pkArray as $pkName => $pkValue) {
if (is_string($pkName) && isset($pkColumns[$pkName])) {
$preparedPkValue = $pkColumns[$pkName]->cast($pkValue);
$pkOrExpr[] = $query->expr()->eq($pkName, ':' . $pkName . $i);
$query->setParameter($pkName . $i, $preparedPkValue);
}
}
if ($pkOrExpr) {
$expressions[] = $query->expr()->and(...$pkOrExpr);
}
}
$query->where($query->expr()->or(...$expressions));
}
$this->queryBuilder = $query;
}

public function getQueryBuilder(): QueryBuilder
{
return $this->queryBuilder;
}
}
12 changes: 11 additions & 1 deletion src/Traits/OptimisticLock.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,15 @@

trait OptimisticLock
{
public int $version = 1;
protected int $lock_version = 1;

public function getVersion(): int
{
return $this->lock_version;
}

public function incrementVersion(): void
{
$this->lock_version++;
}
}
Loading

0 comments on commit c63426c

Please sign in to comment.