From edd4d94d5cf878ed02d65ba6182c4f0432076944 Mon Sep 17 00:00:00 2001 From: Pavel Buchnev Date: Fri, 17 Feb 2023 11:40:15 +0400 Subject: [PATCH] Adds support for Tokenizer listeners (#10) --- .github/workflows/run-tests.yml | 2 +- .github/workflows/static-analysis.yml | 2 +- .github/workflows/update-changelog.yml | 28 ------ CHANGELOG.md | 7 -- README.md | 43 +++------ composer.json | 2 +- src/Bootloader/CqrsBootloader.php | 25 +++-- src/CqrsAttributesListener.php | 121 +++++++++++++++++++++++++ src/HandlersLocator.php | 100 ++------------------ src/HandlersRegistryInterface.php | 23 +++++ 10 files changed, 188 insertions(+), 165 deletions(-) delete mode 100644 .github/workflows/update-changelog.yml delete mode 100644 CHANGELOG.md create mode 100644 src/CqrsAttributesListener.php create mode 100644 src/HandlersRegistryInterface.php diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index bfc0adb..9c4810a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -13,7 +13,7 @@ jobs: fail-fast: true matrix: os: [ ubuntu-latest ] - php: [ 8.0, 8.1 ] + php: [ 8.1 ] stability: [ prefer-lowest, prefer-stable ] name: P${{ matrix.php }} - ${{ matrix.stability }} - ${{ matrix.os }} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 4d1f56c..0243843 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - php: [ 8.0 ] + php: [ 8.1 ] os: [ ubuntu-latest ] steps: diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml deleted file mode 100644 index b20f3b6..0000000 --- a/.github/workflows/update-changelog.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: "Update Changelog" - -on: - release: - types: [released] - -jobs: - update: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - ref: main - - - name: Update Changelog - uses: stefanzweifel/changelog-updater-action@v1 - with: - latest-version: ${{ github.event.release.name }} - release-notes: ${{ github.event.release.body }} - - - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v4 - with: - branch: main - commit_message: Update CHANGELOG - file_pattern: CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 9bbfc47..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,7 +0,0 @@ -# Changelog - -All notable changes to `Cqrs` will be documented in this file. - -## 1.0.0 - 202X-XX-XX - -- initial release diff --git a/README.md b/README.md index c180992..c27c825 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ [![PHP](https://img.shields.io/packagist/php-v/spiral-packages/cqrs.svg?style=flat-square)](https://packagist.org/packages/spiral-packages/cqrs) [![Latest Version on Packagist](https://img.shields.io/packagist/v/spiral-packages/cqrs.svg?style=flat-square)](https://packagist.org/packages/spiral-packages/cqrs) -[![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/spiral-packages/cqrs/run-tests?label=tests&style=flat-square)](https://github.com/spiral-packages/cqrs/actions?query=workflow%3Arun-tests+branch%3Amain) [![Total Downloads](https://img.shields.io/packagist/dt/spiral-packages/cqrs.svg?style=flat-square)](https://packagist.org/packages/spiral-packages/cqrs) +[![run-tests](https://github.com/spiral-packages/cqrs/actions/workflows/run-tests.yml/badge.svg)](https://github.com/spiral-packages/cqrs/actions/workflows/run-tests.yml) It's a lightweight messaging facade. It allows you to define the API of your model with the help of messages. @@ -68,9 +68,9 @@ class StoreUserHandler public function __construct( private EntityManagerInterface $entityManager ) { - + } - + #[\Spiral\Cqrs\Attribute\CommandHandler] public function __invoke(StoreUser $command) { @@ -93,17 +93,17 @@ class StoreUserHandler ```php use Ramsey\Uuid\Uuid; -class UserController +class UserController { public function store(UserStoreRequest $request, \Spiral\Cqrs\CommandBusInterface $bus) { $bus->dispatch(new StoreUser( - $uuid = Uuid::uuid4(), - $request->getUsername(), - $request->getPassword(), + $uuid = Uuid::uuid4(), + $request->getUsername(), + $request->getPassword(), new \DateTimeImmutable() )); - + return $uuid; } } @@ -142,7 +142,7 @@ class UsersQueries private UserRepository $users ) { } - + #[\Spiral\Cqrs\Attribute\QueryHandler] public function findAll(FindAllUsers $query): UserCollection { @@ -150,12 +150,12 @@ class UsersQueries if ($query->roles !== []) { $scope['roles'] = $query->roles } - + return new UserCollection( $this->users->findAll($scope) ); } - + #[\Spiral\Cqrs\Attribute\QueryHandler] public function findById(FindUserById $query): UserResource { @@ -171,7 +171,7 @@ class UsersQueries ```php use Ramsey\Uuid\Uuid; -class UserController +class UserController { public function index(UserFilters $filters, \Spiral\Cqrs\QueryBusInterface $bus) { @@ -179,7 +179,7 @@ class UserController new FindAllUsers($filters->roles()) )->toArray(); } - + public function show(string $uuid, \Spiral\Cqrs\QueryBusInterface $bus) { return $bus->ack( @@ -195,23 +195,6 @@ class UserController composer test ``` -## Changelog - -Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. - -## Contributing - -Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. - -## Security Vulnerabilities - -Please review [our security policy](../../security/policy) on how to report security vulnerabilities. - -## Credits - -- [butschster](https://github.com/spiral-packages) -- [All Contributors](../../contributors) - ## License The MIT License (MIT). Please see [License File](LICENSE) for more information. diff --git a/composer.json b/composer.json index 19e0323..9f141b0 100644 --- a/composer.json +++ b/composer.json @@ -22,9 +22,9 @@ "spiral/boot": "^3.0", "spiral/core": "^3.0", "spiral/config": "^3.0", + "spiral/attributes": "^3.0", "spiral/console": "^3.0", "spiral/tokenizer": "^3.0", - "spiral/attributes": "^2.8 || ^3.0", "symfony/messenger": "^6.0" }, "require-dev": { diff --git a/src/Bootloader/CqrsBootloader.php b/src/Bootloader/CqrsBootloader.php index 48b552b..591a458 100644 --- a/src/Bootloader/CqrsBootloader.php +++ b/src/Bootloader/CqrsBootloader.php @@ -4,14 +4,17 @@ namespace Spiral\Cqrs\Bootloader; -use Spiral\Attributes\AttributeReader; use Spiral\Boot\Bootloader\Bootloader; +use Spiral\Bootloader\Attributes\AttributesBootloader; use Spiral\Core\Container; use Spiral\Cqrs\CommandBus; use Spiral\Cqrs\CommandBusInterface; +use Spiral\Cqrs\CqrsAttributesListener; use Spiral\Cqrs\HandlersLocator; +use Spiral\Cqrs\HandlersRegistryInterface; use Spiral\Cqrs\QueryBus; use Spiral\Cqrs\QueryBusInterface; +use Spiral\Tokenizer\Bootloader\TokenizerListenerBootloader; use Spiral\Tokenizer\ClassesInterface; use Symfony\Component\Messenger\Handler\HandlersLocatorInterface; use Symfony\Component\Messenger\MessageBus; @@ -20,13 +23,26 @@ final class CqrsBootloader extends Bootloader { + protected const DEPENDENCIES = [ + AttributesBootloader::class, + ]; + protected const SINGLETONS = [ - HandlersLocatorInterface::class => [self::class, 'initHandlersLocator'], + HandlersLocator::class => [self::class, 'initHandlersLocator'], + HandlersLocatorInterface::class => HandlersLocator::class, + HandlersRegistryInterface::class => HandlersLocator::class, MessageBusInterface::class => [self::class, 'initMessageBus'], CommandBusInterface::class => CommandBus::class, QueryBusInterface::class => QueryBus::class, ]; + public function init( + TokenizerListenerBootloader $tokenizer, + CqrsAttributesListener $listener + ): void { + $tokenizer->addListener($listener); + } + public function initMessageBus(HandlersLocatorInterface $locator): MessageBusInterface { return new MessageBus([ @@ -37,13 +53,10 @@ public function initMessageBus(HandlersLocatorInterface $locator): MessageBusInt } public function initHandlersLocator( - Container $container, - ClassesInterface $classes + Container $container ): HandlersLocatorInterface { return new HandlersLocator( $container, - $classes, - new AttributeReader() ); } } diff --git a/src/CqrsAttributesListener.php b/src/CqrsAttributesListener.php new file mode 100644 index 0000000..10d9ada --- /dev/null +++ b/src/CqrsAttributesListener.php @@ -0,0 +1,121 @@ +getMethods() as $method) { + if ($this->reader->firstFunctionMetadata($method, CommandHandler::class)) { + $this->assertHandlerMethodIsPublic($method); + $this->processCommandHandler($method); + } + + if ($this->reader->firstFunctionMetadata($method, QueryHandler::class)) { + $this->assertHandlerMethodIsPublic($method); + $this->processQueryHandler($method); + } + } + } + + public function finalize(): void + { + foreach ($this->commandHandlers as $command => $handlers) { + foreach ($handlers as $handler) { + $this->registry->registerCommandHandler($command, $handler); + } + } + + foreach ($this->queryHandlers as $command => $handlers) { + foreach ($handlers as $handler) { + $this->registry->registerQueryHandler($command, $handler); + } + } + } + + private function processCommandHandler(ReflectionMethod $method): void + { + $this->assertHandlerMethodIsPublic($method); + + foreach ($this->getMethodParameters($method) as $parameter) { + if (\is_a($parameter->getName(), CommandInterface::class, true)) { + $this->commandHandlers[$parameter->getName()][] = [ + $method->getDeclaringClass()->getName(), + $method->getName(), + ]; + } + } + } + + private function processQueryHandler(ReflectionMethod $method) + { + $this->assertHandlerMethodIsPublic($method); + + foreach ($this->getMethodParameters($method) as $parameter) { + if (\is_a($parameter->getName(), QueryInterface::class, true)) { + $this->queryHandlers[$parameter->getName()][] = [ + $method->getDeclaringClass()->getName(), + $method->getName(), + ]; + } + } + } + + /** + * @throws InvalidHandlerException + */ + private function assertHandlerMethodIsPublic(ReflectionMethod $method): void + { + if (! $method->isPublic()) { + throw new InvalidHandlerException( + \sprintf( + 'Handler method %s:%s should be public.', + $method->getDeclaringClass()->getName(), + $method->getName() + ) + ); + } + } + + /** + * @return \Generator + */ + private function getMethodParameters(ReflectionMethod $method): \Generator + { + foreach ($method->getParameters() as $parameter) { + if ($parameter->getType() instanceof \ReflectionUnionType) { + foreach ($parameter->getType()->getTypes() as $type) { + yield $type; + } + } else { + yield $parameter->getType(); + } + } + } +} diff --git a/src/HandlersLocator.php b/src/HandlersLocator.php index 5786d2e..a4d01d5 100644 --- a/src/HandlersLocator.php +++ b/src/HandlersLocator.php @@ -4,41 +4,25 @@ namespace Spiral\Cqrs; -use Generator; -use ReflectionMethod; -use ReflectionNamedType; -use ReflectionType; -use Spiral\Attributes\ReaderInterface; use Spiral\Core\Container; -use Spiral\Cqrs\Attribute\CommandHandler; -use Spiral\Cqrs\Attribute\QueryHandler; use Spiral\Cqrs\Exception\HandlerTypeIsNotSupported; -use Spiral\Cqrs\Exception\InvalidHandlerException; -use Spiral\Tokenizer\ClassesInterface; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Handler\HandlerDescriptor; use Symfony\Component\Messenger\Handler\HandlersLocatorInterface; use Symfony\Component\Messenger\Stamp\ReceivedStamp; -final class HandlersLocator implements HandlersLocatorInterface +final class HandlersLocator implements HandlersLocatorInterface, HandlersRegistryInterface { private array $commandHandlers = []; private array $queryHandlers = []; - private bool $precessed = false; public function __construct( private readonly Container $container, - private readonly ClassesInterface $classes, - private readonly ReaderInterface $reader, ) { } public function getHandlers(Envelope $envelope): iterable { - if (! $this->precessed) { - $this->lookForHandlers(); - } - $seen = []; $handlers = match (true) { @@ -56,7 +40,7 @@ public function getHandlers(Envelope $envelope): iterable } $name = $handlerDescriptor->getName(); - if (in_array($name, $seen)) { + if (\in_array($name, $seen)) { continue; } @@ -70,11 +54,11 @@ public function getHandlers(Envelope $envelope): iterable /** @internal */ public static function listTypes(Envelope $envelope): array { - $class = get_class($envelope->getMessage()); + $class = \get_class($envelope->getMessage()); return [$class => $class] - + class_parents($class) - + class_implements($class) + + \class_parents($class) + + \class_implements($class) + ['*' => '*']; } @@ -103,79 +87,13 @@ private function buildHandlerDescriptor(array $handler): HandlerDescriptor ]); } - private function lookForHandlers() - { - foreach ($this->classes->getClasses() as $class) { - foreach ($class->getMethods() as $method) { - if ($this->reader->firstFunctionMetadata($method, CommandHandler::class)) { - $this->processCommandHandler($method); - } - - if ($this->reader->firstFunctionMetadata($method, QueryHandler::class)) { - $this->processQueryHandler($method); - } - } - } - } - - private function processCommandHandler(ReflectionMethod $method): void - { - $this->assertHandlerMethodIsPublic($method); - - foreach ($this->getMethodParameters($method) as $parameter) { - if (is_a($parameter->getName(), CommandInterface::class, true)) { - $this->commandHandlers[$parameter->getName()][] = [ - $method->getDeclaringClass()->getName(), - $method->getName(), - ]; - } - } - } - - private function processQueryHandler(ReflectionMethod $method) - { - $this->assertHandlerMethodIsPublic($method); - - foreach ($this->getMethodParameters($method) as $parameter) { - if (is_a($parameter->getName(), QueryInterface::class, true)) { - $this->queryHandlers[$parameter->getName()][] = [ - $method->getDeclaringClass()->getName(), - $method->getName(), - ]; - } - } - } - - /** - * @throws InvalidHandlerException - */ - private function assertHandlerMethodIsPublic(ReflectionMethod $method): void + public function registerCommandHandler(string $command, array $handler): void { - if (! $method->isPublic()) { - throw new InvalidHandlerException( - \sprintf( - 'Handler method %s:%s should be public.', - $method->getDeclaringClass()->getName(), - $method->getName() - ) - ); - } + $this->commandHandlers[$command][] = $handler; } - /** - * @param ReflectionMethod $method - * @return Generator - */ - private function getMethodParameters(ReflectionMethod $method): Generator + public function registerQueryHandler(string $query, array $handler): void { - foreach ($method->getParameters() as $parameter) { - if ($parameter->getType() instanceof \ReflectionUnionType) { - foreach ($parameter->getType()->getTypes() as $type) { - yield $type; - } - } else { - yield $parameter->getType(); - } - } + $this->queryHandlers[$query][] = $handler; } } diff --git a/src/HandlersRegistryInterface.php b/src/HandlersRegistryInterface.php new file mode 100644 index 0000000..60caaf1 --- /dev/null +++ b/src/HandlersRegistryInterface.php @@ -0,0 +1,23 @@ + $command + * @param THandler $handler + */ + public function registerCommandHandler(string $command, array $handler): void; + + /** + * @param class-string $query + * @param THandler $handler + */ + public function registerQueryHandler(string $query, array $handler): void; +}