Skip to content

Commit c63426c

Browse files
authored
Merge pull request #4 from compositephp/dev
0.3.3 setup
2 parents e4543eb + 63416c6 commit c63426c

18 files changed

+224
-125
lines changed

.gitattributes

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/.gitattributes export-ignore
22
/.gitignore export-ignore
3+
/.github export-ignore
34
/doc export-ignore
45
/phpunit.xml export-ignore
56
/tests export-ignore

composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"php": "^8.1",
1616
"ext-pdo": "*",
1717
"psr/simple-cache": "1 - 3",
18-
"compositephp/entity": "^0.1.8",
18+
"compositephp/entity": "^0.1.9",
1919
"doctrine/dbal": "^3.5"
2020
},
2121
"require-dev": {

src/AbstractCachedTable.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Composite\DB\Exceptions\DbException;
66
use Composite\Entity\AbstractEntity;
77
use Psr\SimpleCache\CacheInterface;
8+
use Ramsey\Uuid\UuidInterface;
89

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

197198
/**
198199
* @param string|int|array<string, mixed>|AbstractEntity $keyOrEntity
199-
* @throws \Composite\Entity\Exceptions\EntityException
200200
*/
201-
protected function getOneCacheKey(string|int|array|AbstractEntity $keyOrEntity): string
201+
protected function getOneCacheKey(string|int|array|AbstractEntity|UuidInterface $keyOrEntity): string
202202
{
203203
if (!is_array($keyOrEntity)) {
204204
$condition = $this->getPkCondition($keyOrEntity);

src/AbstractTable.php

+48-87
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
namespace Composite\DB;
44

55
use Composite\DB\MultiQuery\MultiInsert;
6+
use Composite\DB\MultiQuery\MultiSelect;
67
use Composite\Entity\Helpers\DateTimeHelper;
78
use Composite\Entity\AbstractEntity;
89
use Composite\DB\Exceptions\DbException;
910
use Composite\Entity\Exceptions\EntityException;
1011
use Doctrine\DBAL\Connection;
1112
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
1213
use Doctrine\DBAL\Query\QueryBuilder;
14+
use Ramsey\Uuid\UuidInterface;
1315

1416
abstract class AbstractTable
1517
{
@@ -70,35 +72,20 @@ public function save(AbstractEntity &$entity): void
7072
$entity->updated_at = new \DateTimeImmutable();
7173
$changedColumns['updated_at'] = DateTimeHelper::dateTimeToString($entity->updated_at);
7274
}
73-
74-
if ($this->config->hasOptimisticLock() && isset($entity->version)) {
75-
$currentVersion = $entity->version;
76-
try {
77-
$connection->beginTransaction();
78-
$connection->update(
79-
$this->getTableName(),
80-
$changedColumns,
81-
$where
82-
);
83-
$versionUpdated = $connection->update(
84-
$this->getTableName(),
85-
['version' => $currentVersion + 1],
86-
$where + ['version' => $currentVersion]
87-
);
88-
if (!$versionUpdated) {
89-
throw new DbException('Failed to update entity version, concurrency modification, rolling back.');
90-
}
91-
$connection->commit();
92-
} catch (\Throwable $e) {
93-
$connection->rollBack();
94-
throw $e;
95-
}
96-
} else {
97-
$connection->update(
98-
$this->getTableName(),
99-
$changedColumns,
100-
$where
101-
);
75+
if ($this->config->hasOptimisticLock()
76+
&& method_exists($entity, 'getVersion')
77+
&& method_exists($entity, 'incrementVersion')) {
78+
$where['lock_version'] = $entity->getVersion();
79+
$entity->incrementVersion();
80+
$changedColumns['lock_version'] = $entity->getVersion();
81+
}
82+
$entityUpdated = $connection->update(
83+
table: $this->getTableName(),
84+
data: $changedColumns,
85+
criteria: $where,
86+
);
87+
if ($this->config->hasOptimisticLock() && !$entityUpdated) {
88+
throw new Exceptions\LockException('Failed to update entity version, concurrency modification, rolling back.');
10289
}
10390
$entity->resetChangedColumns();
10491
}
@@ -211,66 +198,31 @@ protected function findByPkInternal(mixed $pk): ?array
211198

212199
/**
213200
* @param array<string, mixed> $where
201+
* @param array<string, string>|string $orderBy
214202
* @return array<string, mixed>|null
215203
* @throws \Doctrine\DBAL\Exception
216204
*/
217-
protected function findOneInternal(array $where): ?array
205+
protected function findOneInternal(array $where, array|string $orderBy = []): ?array
218206
{
219207
$query = $this->select();
220208
$this->buildWhere($query, $where);
209+
$this->applyOrderBy($query, $orderBy);
221210
return $query->fetchAssociative() ?: null;
222211
}
223212

224213
/**
225214
* @param array<int|string|array<string,mixed>> $pkList
226215
* @return array<array<string, mixed>>
227216
* @throws DbException
228-
* @throws EntityException
229217
* @throws \Doctrine\DBAL\Exception
230218
*/
231219
protected function findMultiInternal(array $pkList): array
232220
{
233221
if (!$pkList) {
234222
return [];
235223
}
236-
/** @var class-string<AbstractEntity> $class */
237-
$class = $this->config->entityClass;
238-
239-
$pkColumns = [];
240-
foreach ($this->config->primaryKeys as $primaryKeyName) {
241-
$pkColumns[$primaryKeyName] = $class::schema()->getColumn($primaryKeyName);
242-
}
243-
if (count($pkColumns) === 1) {
244-
if (!array_is_list($pkList)) {
245-
throw new DbException('Input argument $pkList must be list');
246-
}
247-
/** @var \Composite\Entity\Columns\AbstractColumn $pkColumn */
248-
$pkColumn = reset($pkColumns);
249-
$preparedPkValues = array_map(fn ($pk) => $pkColumn->uncast($pk), $pkList);
250-
$query = $this->select();
251-
$this->buildWhere($query, [$pkColumn->name => $preparedPkValues]);
252-
} else {
253-
$query = $this->select();
254-
$expressions = [];
255-
foreach ($pkList as $i => $pkArray) {
256-
if (!is_array($pkArray)) {
257-
throw new DbException('For tables with composite keys, input array must consist associative arrays');
258-
}
259-
$pkOrExpr = [];
260-
foreach ($pkArray as $pkName => $pkValue) {
261-
if (is_string($pkName) && isset($pkColumns[$pkName])) {
262-
$preparedPkValue = $pkColumns[$pkName]->cast($pkValue);
263-
$pkOrExpr[] = $query->expr()->eq($pkName, ':' . $pkName . $i);
264-
$query->setParameter($pkName . $i, $preparedPkValue);
265-
}
266-
}
267-
if ($pkOrExpr) {
268-
$expressions[] = $query->expr()->and(...$pkOrExpr);
269-
}
270-
}
271-
$query->where($query->expr()->or(...$expressions));
272-
}
273-
return $query->executeQuery()->fetchAllAssociative();
224+
$multiSelect = new MultiSelect($this->getConnection(), $this->config, $pkList);
225+
return $multiSelect->getQueryBuilder()->executeQuery()->fetchAllAssociative();
274226
}
275227

276228
/**
@@ -294,22 +246,7 @@ protected function findAllInternal(
294246
$query->setParameter($param, $value);
295247
}
296248
}
297-
if ($orderBy) {
298-
if (is_array($orderBy)) {
299-
foreach ($orderBy as $column => $direction) {
300-
$query->addOrderBy($column, $direction);
301-
}
302-
} else {
303-
foreach (explode(',', $orderBy) as $orderByPart) {
304-
$orderByPart = trim($orderByPart);
305-
if (preg_match('/(.+)\s(asc|desc)$/i', $orderByPart, $orderByPartMatch)) {
306-
$query->addOrderBy($orderByPartMatch[1], $orderByPartMatch[2]);
307-
} else {
308-
$query->addOrderBy($orderByPart);
309-
}
310-
}
311-
}
312-
}
249+
$this->applyOrderBy($query, $orderBy);
313250
if ($limit > 0) {
314251
$query->setMaxResults($limit);
315252
}
@@ -366,7 +303,7 @@ final protected function createEntities(mixed $data, ?string $keyColumnName = nu
366303
* @return array<string, mixed>
367304
* @throws EntityException
368305
*/
369-
protected function getPkCondition(int|string|array|AbstractEntity $data): array
306+
protected function getPkCondition(int|string|array|AbstractEntity|UuidInterface $data): array
370307
{
371308
$condition = [];
372309
if ($data instanceof AbstractEntity) {
@@ -395,7 +332,7 @@ protected function select(string $select = '*'): QueryBuilder
395332
/**
396333
* @param array<string, mixed> $where
397334
*/
398-
private function buildWhere(QueryBuilder $query, array $where): void
335+
private function buildWhere(\Doctrine\DBAL\Query\QueryBuilder $query, array $where): void
399336
{
400337
foreach ($where as $column => $value) {
401338
if ($value === null) {
@@ -433,4 +370,28 @@ private function formatData(array $data): array
433370
}
434371
return $data;
435372
}
373+
374+
/**
375+
* @param array<string, string>|string $orderBy
376+
*/
377+
private function applyOrderBy(QueryBuilder $query, string|array $orderBy): void
378+
{
379+
if (!$orderBy) {
380+
return;
381+
}
382+
if (is_array($orderBy)) {
383+
foreach ($orderBy as $column => $direction) {
384+
$query->addOrderBy($column, $direction);
385+
}
386+
} else {
387+
foreach (explode(',', $orderBy) as $orderByPart) {
388+
$orderByPart = trim($orderByPart);
389+
if (preg_match('/(.+)\s(asc|desc)$/i', $orderByPart, $orderByPartMatch)) {
390+
$query->addOrderBy($orderByPartMatch[1], $orderByPartMatch[2]);
391+
} else {
392+
$query->addOrderBy($orderByPart);
393+
}
394+
}
395+
}
396+
}
436397
}

src/Exceptions/LockException.php

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Composite\DB\Exceptions;
4+
5+
class LockException extends DbException
6+
{
7+
}

src/MultiQuery/MultiSelect.php

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php declare(strict_types=1);
2+
3+
namespace Composite\DB\MultiQuery;
4+
5+
use Composite\DB\Exceptions\DbException;
6+
use Composite\DB\TableConfig;
7+
use Composite\Entity\AbstractEntity;
8+
use Doctrine\DBAL\Connection;
9+
use Doctrine\DBAL\Query\QueryBuilder;
10+
11+
class MultiSelect
12+
{
13+
private readonly QueryBuilder $queryBuilder;
14+
15+
public function __construct(
16+
Connection $connection,
17+
TableConfig $tableConfig,
18+
array $condition,
19+
) {
20+
$query = $connection->createQueryBuilder()->select('*')->from($tableConfig->tableName);
21+
/** @var class-string<AbstractEntity> $class */
22+
$class = $tableConfig->entityClass;
23+
24+
$pkColumns = [];
25+
foreach ($tableConfig->primaryKeys as $primaryKeyName) {
26+
$pkColumns[$primaryKeyName] = $class::schema()->getColumn($primaryKeyName);
27+
}
28+
29+
if (count($pkColumns) === 1) {
30+
if (!array_is_list($condition)) {
31+
throw new DbException('Input argument $pkList must be list');
32+
}
33+
/** @var \Composite\Entity\Columns\AbstractColumn $pkColumn */
34+
$pkColumn = reset($pkColumns);
35+
$preparedPkValues = array_map(fn ($pk) => $pkColumn->uncast($pk), $condition);
36+
$query->andWhere($query->expr()->in($pkColumn->name, $preparedPkValues));
37+
} else {
38+
$expressions = [];
39+
foreach ($condition as $i => $pkArray) {
40+
if (!is_array($pkArray)) {
41+
throw new DbException('For tables with composite keys, input array must consist associative arrays');
42+
}
43+
$pkOrExpr = [];
44+
foreach ($pkArray as $pkName => $pkValue) {
45+
if (is_string($pkName) && isset($pkColumns[$pkName])) {
46+
$preparedPkValue = $pkColumns[$pkName]->cast($pkValue);
47+
$pkOrExpr[] = $query->expr()->eq($pkName, ':' . $pkName . $i);
48+
$query->setParameter($pkName . $i, $preparedPkValue);
49+
}
50+
}
51+
if ($pkOrExpr) {
52+
$expressions[] = $query->expr()->and(...$pkOrExpr);
53+
}
54+
}
55+
$query->where($query->expr()->or(...$expressions));
56+
}
57+
$this->queryBuilder = $query;
58+
}
59+
60+
public function getQueryBuilder(): QueryBuilder
61+
{
62+
return $this->queryBuilder;
63+
}
64+
}

src/Traits/OptimisticLock.php

+11-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,15 @@
44

55
trait OptimisticLock
66
{
7-
public int $version = 1;
7+
protected int $lock_version = 1;
8+
9+
public function getVersion(): int
10+
{
11+
return $this->lock_version;
12+
}
13+
14+
public function incrementVersion(): void
15+
{
16+
$this->lock_version++;
17+
}
818
}

0 commit comments

Comments
 (0)