Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/vendor/
vendor/
.phpunit.result.cache
7 changes: 6 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@
},
"require-dev": {
"phpunit/phpunit": "^10.1@dev",
"squizlabs/php_codesniffer": "4.0.x-dev"
"squizlabs/php_codesniffer": "*"
},
"config": {
"platform": {
"php": "8.1.0"
}
},
"scripts": {
"phpcbf": "phpcbf --standard=psr12 --severity=4 --tab-width=4 src tests",
Expand Down
736 changes: 434 additions & 302 deletions composer.lock

Large diffs are not rendered by default.

37 changes: 37 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,43 @@ public function run()
}
```

### Attribute-based registration

You can also map handlers to their commands/queries with PHP attributes instead of manual arrays:

```php
use Bow\CQRS\Attribute\CommandHandler;
use Bow\CQRS\Attribute\QueryHandler;
use Bow\CQRS\Command\CommandHandlerInterface;
use Bow\CQRS\Command\CommandInterface;
use Bow\CQRS\Query\QueryHandlerInterface;
use Bow\CQRS\Query\QueryInterface;

#[CommandHandler(CreateUserCommand::class)]
class CreateUserCommandHandler implements CommandHandlerInterface
{
public function process(CommandInterface $command): mixed
{
// create the user...
}
}

#[QueryHandler(FetchUserQuery::class)]
class FetchUserQueryHandler implements QueryHandlerInterface
{
public function process(QueryInterface $query): mixed
{
// fetch and return the user...
}
}

// Register handlers once (attributes map them to the right message)
CQRSRegistration::handlers([
CreateUserCommandHandler::class,
FetchUserQueryHandler::class,
]);
```

Execute the command in the controller:

```php
Expand Down
21 changes: 21 additions & 0 deletions src/Attribute/CommandHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Bow\CQRS\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class CommandHandler
{
public function __construct(
private readonly string $commandClass
) {
}

public function getCommandClass(): string
{
return $this->commandClass;
}
}
21 changes: 21 additions & 0 deletions src/Attribute/QueryHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

declare(strict_types=1);

namespace Bow\CQRS\Attribute;

use Attribute;

#[Attribute(Attribute::TARGET_CLASS)]
class QueryHandler
{
public function __construct(
private readonly string $queryClass
) {
}

public function getQueryClass(): string
{
return $this->queryClass;
}
}
2 changes: 1 addition & 1 deletion src/Command/CommandBus.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class CommandBus
*/
public function execute(CommandInterface $command): mixed
{
$handler = Registration::getHandler($command);
$handler = Registration::getCommandHandler($command);

return $handler->process($command);
}
Expand Down
2 changes: 2 additions & 0 deletions src/Command/CommandHandlerTransactionService.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
<?php

declare(strict_types=1);

namespace Bow\CQRS\Command;

use Bow\CQRS\Command\CommandInterface;
Expand Down
2 changes: 1 addition & 1 deletion src/Query/QueryBus.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class QueryBus
*/
public function execute(QueryInterface $query): mixed
{
$handler = Registration::getHandler($query);
$handler = Registration::getQueryHandler($query);

return $handler->process($query);
}
Expand Down
138 changes: 126 additions & 12 deletions src/Registration.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

namespace Bow\CQRS;

use Bow\CQRS\Attribute\CommandHandler as CommandHandlerAttribute;
use Bow\CQRS\Attribute\QueryHandler as QueryHandlerAttribute;
use Bow\CQRS\CQRSException;
use Bow\CQRS\Query\QueryInterface;
use Bow\CQRS\Command\CommandInterface;
use Bow\CQRS\Query\QueryHandlerInterface;
use Bow\CQRS\Command\CommandHandlerInterface;
use ReflectionClass;

final class Registration
{
Expand All @@ -34,7 +37,7 @@ final class Registration
*/
public static function queries(array $queries): void
{
static::$queries = $queries;
static::$queries = [...static::$queries, ...$queries];
}

/**
Expand All @@ -45,7 +48,66 @@ public static function queries(array $queries): void
*/
public static function commands(array $commands): void
{
static::$commands = $commands;
static::$commands = [...static::$commands, ...$commands];
}

/**
* Register handlers declared with attributes.
*
* @param array $handlers
* @return void
*/
public static function handlers(array $handlers): void
{
foreach ($handlers as $handler) {
static::registerHandler($handler);
}
}

/**
* Register a handler declared with attributes.
*
* @param string $handler
* @return void
*/
private static function registerHandler(string $handler): void
{
$reflection = new ReflectionClass($handler);

$commandAttributes = $reflection->getAttributes(CommandHandlerAttribute::class);
$queryAttributes = $reflection->getAttributes(QueryHandlerAttribute::class);

if (count($commandAttributes) === 0 && count($queryAttributes) === 0) {
throw new CQRSException(
sprintf(
"The handler %s has no #[CommandHandler] or #[QueryHandler] attribute",
$handler
)
);
}

foreach ($commandAttributes as $attribute) {
$commandHandler = $attribute->newInstance();
static::$commands[$commandHandler->getCommandClass()] = $handler;
}

foreach ($queryAttributes as $attribute) {
$queryHandler = $attribute->newInstance();
static::$queries[$queryHandler->getQueryClass()] = $handler;
}
}

/**
* Reset the registered commands and queries.
*
* Mostly useful in tests to isolate state between cases.
*
* @return void
*/
public static function reset(): void
{
static::$commands = [];
static::$queries = [];
}

/**
Expand All @@ -55,26 +117,78 @@ public static function commands(array $commands): void
* @return QueryHandlerInterface|CommandHandlerInterface
*/
public static function getHandler(
QueryInterface|CommandInterface $action
QueryInterface|CommandInterface $cq
): QueryHandlerInterface|CommandHandlerInterface {
$action_class = get_class($action);
if ($cq instanceof CommandInterface) {
return static::getCommandHandler($cq);
}

if ($action instanceof QueryInterface) {
$handler = static::$queries[$action_class] ?? null;
} else {
$handler = static::$commands[$action_class] ?? null;
return static::getQueryHandler($cq);
}

/**
* Get the command handler for the given command instance.
*
* @param CommandInterface $command
* @return CommandHandlerInterface
*/
public static function getCommandHandler(
CommandInterface $command
): CommandHandlerInterface {
$command_class = get_class($command);

$handler = static::$commands[$command_class] ?? null;

if (!is_null($handler)) {
return static::makeHandler($handler);
}

throw new CQRSException(
sprintf(
"The command %s handler is not found on the CQ register",
$command_class
)
);
}

/**
* Get the query handler for the given query instance.
*
* @param QueryInterface $query
* @return QueryHandlerInterface
*/
public static function getQueryHandler(
QueryInterface $query
): QueryHandlerInterface {
$query_class = get_class($query);

$handler = static::$queries[$query_class] ?? null;

if (!is_null($handler)) {
return app($handler);
return static::makeHandler($handler);
}

throw new CQRSException(
sprintf(
"The %s %s:class handler is not found on the CQ register",
$action instanceof QueryInterface ? 'query' : 'command',
$action_class
"The query %s handler is not found on the CQ register",
$query_class
)
);
}

/**
* Resolve handler from container or instantiate directly.
*
* @param string $handler
* @return QueryHandlerInterface|CommandHandlerInterface
*/
private static function makeHandler(
string $handler
): QueryHandlerInterface|CommandHandlerInterface {
if (function_exists('app')) {
return app($handler);
}

return new $handler();
}
}
38 changes: 32 additions & 6 deletions tests/CQRSTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,21 @@
use Bow\CQRS\Registration as CQRSRegistration;
use Bow\Tests\CQRS\Queries\FetchPetQueryHandler;
use Bow\Tests\CQRS\Commands\CreatePetCommandHandler;
use Bow\Tests\CQRS\Commands\DummyCreateUserCommand;
use Bow\Tests\CQRS\Commands\DummyCreateUserCommandHandler;
use Bow\Tests\CQRS\Fixtures\PetFinder;
use Bow\Tests\CQRS\Queries\InlineFetchAllQuery;
use Bow\Tests\CQRS\Queries\InlineFetchAllQueryHandler;

class CQRSTest extends TestCase
{
public static function setUpBeforeClass(): void
{
PetFinder::clear();

CQRSRegistration::commands([
CreatePetCommand::class => CreatePetCommandHandler::class
]);

CQRSRegistration::queries([
FetchPetQuery::class => FetchPetQueryHandler::class
CQRSRegistration::handlers([
CreatePetCommandHandler::class,
FetchPetQueryHandler::class
]);
}

Expand Down Expand Up @@ -60,4 +61,29 @@ public function test_query_bus()

$this->assertEquals($pet->name, 'Pascal');
}

public function test_attribute_based_command_handler_registration_and_execution()
{
CQRSRegistration::handlers([
DummyCreateUserCommandHandler::class
]);

$command_bus = new CommandBus();

$result = $command_bus->execute(new DummyCreateUserCommand('Alex'));

$this->assertSame('created Alex', $result);
}

public function test_manual_query_registration_merges_and_resolves()
{
CQRSRegistration::queries([
InlineFetchAllQuery::class => InlineFetchAllQueryHandler::class
]);

$handler = CQRSRegistration::getQueryHandler(new InlineFetchAllQuery());

$this->assertInstanceOf(InlineFetchAllQueryHandler::class, $handler);
$this->assertSame('ok', $handler->process(new InlineFetchAllQuery()));
}
}
2 changes: 2 additions & 0 deletions tests/Commands/CreatePetCommandHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@

namespace Bow\Tests\CQRS\Commands;

use Bow\CQRS\Attribute\CommandHandler;
use Bow\Tests\CQRS\Fixtures\PetFinder;
use Bow\CQRS\Command\CommandInterface;
use Bow\CQRS\Command\CommandHandlerInterface;

#[CommandHandler(CreatePetCommand::class)]
class CreatePetCommandHandler implements CommandHandlerInterface
{
public function process(CommandInterface $command): int
Expand Down
12 changes: 12 additions & 0 deletions tests/Commands/DummyCreateUserCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace Bow\Tests\CQRS\Commands;

use Bow\CQRS\Command\CommandInterface;

class DummyCreateUserCommand implements CommandInterface
{
public function __construct(public string $name)
{
}
}
Loading
Loading