Skip to content

Commit

Permalink
Adds support for Tokenizer listeners (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
butschster committed Feb 17, 2023
1 parent ffab9c2 commit edd4d94
Show file tree
Hide file tree
Showing 10 changed files with 188 additions and 165 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
strategy:
fail-fast: false
matrix:
php: [ 8.0 ]
php: [ 8.1 ]
os: [ ubuntu-latest ]

steps:
Expand Down
28 changes: 0 additions & 28 deletions .github/workflows/update-changelog.yml

This file was deleted.

7 changes: 0 additions & 7 deletions CHANGELOG.md

This file was deleted.

43 changes: 13 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -68,9 +68,9 @@ class StoreUserHandler
public function __construct(
private EntityManagerInterface $entityManager
) {

}

#[\Spiral\Cqrs\Attribute\CommandHandler]
public function __invoke(StoreUser $command)
{
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -142,20 +142,20 @@ class UsersQueries
private UserRepository $users
) {
}

#[\Spiral\Cqrs\Attribute\QueryHandler]
public function findAll(FindAllUsers $query): UserCollection
{
$scope = [];
if ($query->roles !== []) {
$scope['roles'] = $query->roles
}

return new UserCollection(
$this->users->findAll($scope)
);
}

#[\Spiral\Cqrs\Attribute\QueryHandler]
public function findById(FindUserById $query): UserResource
{
Expand All @@ -171,15 +171,15 @@ class UsersQueries
```php
use Ramsey\Uuid\Uuid;

class UserController
class UserController
{
public function index(UserFilters $filters, \Spiral\Cqrs\QueryBusInterface $bus)
{
return $bus->ack(
new FindAllUsers($filters->roles())
)->toArray();
}

public function show(string $uuid, \Spiral\Cqrs\QueryBusInterface $bus)
{
return $bus->ack(
Expand All @@ -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.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
25 changes: 19 additions & 6 deletions src/Bootloader/CqrsBootloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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([
Expand All @@ -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()
);
}
}
121 changes: 121 additions & 0 deletions src/CqrsAttributesListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
<?php

declare(strict_types=1);

namespace Spiral\Cqrs;

use ReflectionMethod;
use Spiral\Attributes\ReaderInterface;
use Spiral\Cqrs\Attribute\CommandHandler;
use Spiral\Cqrs\Attribute\QueryHandler;
use Spiral\Cqrs\Exception\InvalidHandlerException;
use Spiral\Tokenizer\TokenizationListenerInterface;
use Spiral\Tokenizer\Attribute\TargetAttribute;

/**
* @psalm-suppress InvalidAttribute
* @psalm-suppress UndefinedAttributeClass
*/
#[TargetAttribute(CommandHandler::class)]
#[TargetAttribute(QueryHandler::class)]
final class CqrsAttributesListener implements TokenizationListenerInterface
{
private array $commandHandlers = [];
private array $queryHandlers = [];

public function __construct(
private readonly ReaderInterface $reader,
private readonly HandlersRegistryInterface $registry,
) {
}

public function listen(\ReflectionClass $class): void
{
foreach ($class->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<int, \ReflectionNamedType|\ReflectionType|null>
*/
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();
}
}
}
}
Loading

0 comments on commit edd4d94

Please sign in to comment.