Skip to content

Commit

Permalink
Up to PHP:^8.1
Browse files Browse the repository at this point in the history
  • Loading branch information
sbooker committed Jun 30, 2024
1 parent 8473b70 commit a468a51
Show file tree
Hide file tree
Showing 12 changed files with 166 additions and 114 deletions.
13 changes: 10 additions & 3 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,18 @@ You need to make sure that firstly do all checks are and throws exceptions.

And only then mutates state of entities.

## Example usage with Symfony and Doctrine (while bundle not written)
## Example usage with Symfony and Doctrine (without bundle)

> #### Important!
> Build-in realization persistence layer of command bus require doctrine/dbal:^3.8 and
> database supports SELECT FOR UPDATE SKIP LOCKED (MySQL 8.0+, PostgreSQL 9.5+)
Install suggest libraries:
```bash
composer require sbooker/doctrine-enumerable-type sbooker/doctrine-transaction-handler ramsey/uuid-doctrine
composer require sbooker/doctrine-enumerable-type \
sbooker/doctrine-transaction-handler \
ramsey/uuid-doctrine \
doctrine/dbal:^3.8
```

To use default configuration configure as bellow:
Expand All @@ -46,7 +53,7 @@ doctrine:

services:
Sbooker\CommandBus\CommandBus:
class: Sbooker\CommandBus\PersistentCommandCommandBus
class: Sbooker\CommandBus\PersistentCommandBus
arguments:
- '@Sbooker\CommandBus\Normalizer'
- '@Sbooker\TransactionManager\TransactionManager'
Expand Down
5 changes: 4 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
],
"minimum-stability": "dev",
"prefer-stable": true,
"version": "1.0.0",
"require": {
"php": "^7.4 || ^8.0",
"ramsey/uuid": "^4.0",
Expand All @@ -21,7 +22,8 @@
},
"require-dev": {
"doctrine/cache": "^1.12",
"doctrine/orm": "^2.9",
"doctrine/orm": "^2.9 || ^3.0",
"doctrine/dbal": "^3.8 || ^4.0",
"phpunit/phpunit": "^9.0",
"psr/container": "1.0.0",
"ramsey/uuid-doctrine": "^1.6",
Expand All @@ -30,6 +32,7 @@
},
"suggest": {
"doctrine/orm": "If you want use DB persistence with Doctrine",
"doctrine/dbal": "^3.8",
"psr/container": "If you want use container registry implementation",
"ramsey/uuid-doctrine": "",
"sbooker/doctrine-enumerable-type": "",
Expand Down
161 changes: 113 additions & 48 deletions src/Infrastructure/Persistence/DoctrineRepository.php
Original file line number Diff line number Diff line change
@@ -1,85 +1,150 @@
<?php

declare(strict_types=1);

namespace Sbooker\CommandBus\Infrastructure\Persistence;

use Doctrine\DBAL\LockMode;
use Doctrine\DBAL\Types\Types;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Query\Expression\ExpressionBuilder;
use Doctrine\DBAL\Query\ForUpdate\ConflictResolutionMode;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Persistence\Mapping\MappingException;
use Ramsey\Uuid\UuidInterface;
use Sbooker\CommandBus\Command;
use Sbooker\CommandBus\ReadStorage;
use Sbooker\CommandBus\Status;
use Sbooker\CommandBus\WriteStorage;

class DoctrineRepository extends EntityRepository implements WriteStorage, ReadStorage
final class DoctrineRepository extends EntityRepository implements WriteStorage, ReadStorage
{
public function get(UuidInterface $id): ?Command
{
return $this->find($id);
}

/**
* @throws \Doctrine\ORM\NonUniqueResultException
* @throws \Doctrine\ORM\TransactionRequiredException
* @throws ORMException
* @throws \ReflectionException
* @throws MappingException
*/
public function getAndLock(array $names, UuidInterface $id): ?Command
{
return
$this->createQueryBuilderWithNamesCondition('t', $names)
->andWhere('t.id = :id')
->setParameter('id', $id)
->getQuery()
->setLockMode(LockMode::PESSIMISTIC_WRITE)
->getOneOrNullResult()
;
$alias = 'c';
$qb = $this->createDbalQueryBuilderWithCommonExpression($alias, $names);
$sql = $qb
->andWhere('c.id = :id')
->getSQL();

return $this->findCommandBySQL($alias, $sql, ['id' => $id->toString()]);
}

/**
* @throws \Doctrine\ORM\NonUniqueResultException
* @throws ORMException
* @throws \ReflectionException
* @throws MappingException|DBALException
*/
public function get(UuidInterface $id): ?Command
public function getFirstToProcessAndLock(array $names): ?Command
{
return $this->find($id);
$alias = 'c';
$qb = $this->createDbalQueryBuilderWithCommonExpression($alias, $names);

$sql = $qb
->andWhere(
$this->buildInExpr(
$qb->expr(),
'c.status',
[
Status::created()->getRawValue(),
Status::pending()->getRawValue()
]
)
)
->andWhere('c.next_attempt_at < :now')
->orderBy('c.next_attempt_at', 'ASC')
->setMaxResults(1)
->getSQL()
;

return $this->findCommandBySQL($alias, $sql, [
'now' => (new \DateTimeImmutable())->format($this->getPlatform()->getDateTimeTzFormatString()),
]);
}

/**
* @throws \Doctrine\ORM\NonUniqueResultException
* @throws \Doctrine\ORM\TransactionRequiredException
* @throws \ReflectionException
* @throws MappingException
*/
public function getFirstToProcessAndLock(array $names): ?Command
private function createDbalQueryBuilderWithCommonExpression(string $alias, array $names): QueryBuilder
{
$builder = $this->createQueryBuilderWithNamesCondition('t', $names);
$expr = $builder->expr();
$qb = $this->getConnection()->createQueryBuilder();
$qb
->select("$alias.*")
->from($this->getTableName(), $alias)
->forUpdate(ConflictResolutionMode::SKIP_LOCKED)
;

if ([] !== $names) {
$qb->andWhere($this->buildInExpr($qb->expr(), "$alias.name", $names));
}

return $qb;
}

private function buildInExpr(ExpressionBuilder $expr, string $field, array $values): string
{
if ([] === $values) {
throw new \InvalidArgumentException("Parameter values must not be empty");
}

return
$builder
->andWhere(
$expr->in(
"t.workflow.status",
[
Status::created()->getRawValue(),
Status::pending()->getRawValue()
]
)
$expr->in(
$field,
array_map(
fn(string $name) => $expr->literal($name, ParameterType::STRING),
$values
)
->andWhere('t.attemptCounter.nextAttemptAt < :now')
->orderBy('t.attemptCounter.nextAttemptAt', 'ASC')
->setParameter('now', new \DateTimeImmutable(), Types::DATETIMETZ_IMMUTABLE)
->setMaxResults(1)
->getQuery()
->setLockMode(LockMode::PESSIMISTIC_WRITE)
->getOneOrNullResult();
);
}

private function createQueryBuilderWithNamesCondition(string $alias, array $names): QueryBuilder
/**
* @throws NonUniqueResultException
*/
private function findCommandBySQL(string $tableAlias, string $sql, array $parameters): ?Command
{
$builder = $this->createQueryBuilder($alias);
$rsm = new ResultSetMappingBuilder($this->getEntityManager());
$rsm->addRootEntityFromClassMetadata(Command::class, $tableAlias);

if ([] === $names) {
return $builder;
}
return
$this->getEntityManager()
->createNativeQuery($sql, $rsm)
->setParameters($parameters)
->getOneOrNullResult();
}

$expr = $builder->expr();
$builder->andWhere($expr->in("$alias.normalizedCommand.name", $names));
/**
* @throws MappingException
* @throws \ReflectionException
*/
private function getTableName(): string
{
return $this->getEntityManager()->getMetadataFactory()->getMetadataFor(Command::class)->getTableName();
}

return $builder;
/**
* @throws DBALException
*/
private function getPlatform(): AbstractPlatform
{
return $this->getConnection()->getDatabasePlatform();
}

private function getConnection(): Connection
{
return $this->getEntityManager()->getConnection();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
namespace Sbooker\CommandBus;

use Ramsey\Uuid\UuidInterface;
use Sbooker\CommandBus\CommandBus;
use Sbooker\TransactionManager\TransactionManager;

final class PersistentCommandCommandBus implements CommandBus
final class PersistentCommandBus implements CommandBus
{
private Normalizer $normalizer;

Expand Down
9 changes: 0 additions & 9 deletions tests/Infrastructure/Persistence/EntityManagerBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
final class EntityManagerBuilder
{
public const PGSQL12 = 'pgsql12';
public const MYSQL5 = 'mysql5';
public const MYSQL8 = 'mysql8';

private static ?EntityManagerBuilder $me = null;
Expand Down Expand Up @@ -80,14 +79,6 @@ private static function getParams(string $db): array
'server_version' => '12',
];
break;
case self::MYSQL5:
$params = [
'driver' => 'pdo_mysql',
'host' => self::MYSQL5,
'port' => 3306,
'server_version' => '5',
];
break;
case self::MYSQL8:
$params = [
'driver' => 'pdo_mysql',
Expand Down
29 changes: 20 additions & 9 deletions tests/Infrastructure/Persistence/Makefile
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
all: up install test down
dc := docker compose -p cb -f ./build/docker-compose.yaml
dcup := $(dc) up --build -d

up:
docker-compose -p cb -f ./build/docker-compose.yaml up --build -d

install:
docker-compose -p cb -f ./build/docker-compose.yaml exec app composer install
all:
up-db
up-php VER=8.3
update
test
down

up-db:
$(dcup) mysql8 && \
$(dcup) pgsql12
up-php:
$(dc) build --build-arg PHP_VER=$(VER) && \
$(dc) up -d app
update:
$(dc) exec app composer update
test:
docker-compose -p cb -f ./build/docker-compose.yaml exec app ./vendor/bin/phpunit ./tests/Infrastructure/Persistence

$(dc) exec app ./vendor/bin/phpunit ./tests/Infrastructure/Persistence
down:
docker-compose -p cb -f ./build/docker-compose.yaml down --rmi all
$(dc) down
down-all:
$(dc) down --rmi all
3 changes: 2 additions & 1 deletion tests/Infrastructure/Persistence/NestedTransactionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ private function createCommandHandler(string $commandName, WriteStorage $command
$this->createRegistry($commandName, $endpoint),
$this->createDenormalizer($commandName),
$commandRepository,
$this->getTransactionManager()
$this->getTransactionManager(),
[$commandName]
);
}

Expand Down
1 change: 0 additions & 1 deletion tests/Infrastructure/Persistence/PersistenceTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ public function dbs(): array
{
return [
[ EntityManagerBuilder::PGSQL12 ],
[ EntityManagerBuilder::MYSQL5 ],
[ EntityManagerBuilder::MYSQL8 ],
];
}
Expand Down
14 changes: 10 additions & 4 deletions tests/Infrastructure/Persistence/build/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
FROM php:8.1-fpm
ARG PHP_VER=8.2
FROM php:${PHP_VER}-fpm

RUN apt-get update && apt-get install -y --no-install-recommends \
unzip \
Expand All @@ -11,11 +12,16 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
&& pecl install APCu xdebug \
&& docker-php-ext-enable apcu xdebug \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& useradd --create-home --uid 1000 --user-group --system app
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

COPY php.ini /usr/local/etc/php

COPY --chown=app:app id_rsa /home/app/.ssh/id_rsa
RUN chmod 600 /home/app/.ssh/id_rsa
RUN touch /home/app/.ssh/known_hosts

WORKDIR /opt/app

USER app



Loading

0 comments on commit a468a51

Please sign in to comment.