diff --git a/baseline.xml b/baseline.xml
index a453b09c4..e988b448e 100644
--- a/baseline.xml
+++ b/baseline.xml
@@ -226,6 +226,16 @@
+
+
+
+
+
+
+
+
+
+
command]]>
@@ -251,17 +261,12 @@
-
-
-
-
-
-
+
diff --git a/src/Attribute/HandledBy.php b/src/Attribute/HandledBy.php
deleted file mode 100644
index 983347653..000000000
--- a/src/Attribute/HandledBy.php
+++ /dev/null
@@ -1,18 +0,0 @@
- $aggregateClass */
- public function __construct(
- public readonly string $aggregateClass,
- ) {
- }
-}
diff --git a/src/CommandBus/AggregateHandler.php b/src/CommandBus/AggregateHandler.php
new file mode 100644
index 000000000..78f3890d0
--- /dev/null
+++ b/src/CommandBus/AggregateHandler.php
@@ -0,0 +1,16 @@
+ */
+ private array $createHandlers = [];
+
+ /** @var list */
+ private array $updateHandlers = [];
+
+ /** @param class-string $aggregateClass */
+ public function __construct(string $aggregateClass)
+ {
+ $typeResolver = TypeResolver::create();
+ $reflectionClass = new ReflectionClass($aggregateClass);
+
+ foreach ($reflectionClass->getMethods() as $reflectionMethod) {
+ $handleAttributes = $reflectionMethod->getAttributes(Handle::class);
+
+ if ($handleAttributes === []) {
+ continue;
+ }
+
+ $parameters = $reflectionMethod->getParameters();
+
+ if ($parameters === []) {
+ throw InvalidHandleMethod::noParameters(
+ $reflectionMethod->getDeclaringClass()->getName(),
+ $reflectionMethod->getName(),
+ );
+ }
+
+ $reflectionType = $parameters[0]->getType();
+
+ if ($reflectionType === null) {
+ throw InvalidHandleMethod::incompatibleType(
+ $reflectionMethod->getDeclaringClass()->getName(),
+ $reflectionMethod->getName(),
+ );
+ }
+
+ $type = $typeResolver->resolve($reflectionType);
+
+ if (!$type instanceof ObjectType) {
+ throw InvalidHandleMethod::incompatibleType(
+ $reflectionMethod->getDeclaringClass()->getName(),
+ $reflectionMethod->getName(),
+ );
+ }
+
+ $handle = $handleAttributes[0]->newInstance();
+ $commandClass = $handle->commandClass ?: $type->getClassName();
+
+ if (!class_exists($commandClass)) {
+ throw InvalidHandleMethod::incompatibleType(
+ $reflectionMethod->getDeclaringClass()->getName(),
+ $reflectionMethod->getName(),
+ );
+ }
+
+ if ($reflectionMethod->isStatic()) {
+ $this->createHandlers[] = new AggregateHandler(
+ $reflectionMethod->getName(),
+ $commandClass,
+ );
+ } else {
+ $this->updateHandlers[] = new AggregateHandler(
+ $reflectionMethod->getName(),
+ $commandClass,
+ );
+ }
+ }
+ }
+
+ /** @return iterable */
+ public function createHandlers(): iterable
+ {
+ return $this->createHandlers;
+ }
+
+ /** @return iterable */
+ public function updateHandlers(): iterable
+ {
+ return $this->updateHandlers;
+ }
+}
diff --git a/src/CommandBus/AggregateHandlerProvider.php b/src/CommandBus/AggregateHandlerProvider.php
index 2be72479c..82a26c69a 100644
--- a/src/CommandBus/AggregateHandlerProvider.php
+++ b/src/CommandBus/AggregateHandlerProvider.php
@@ -4,130 +4,60 @@
namespace Patchlevel\EventSourcing\CommandBus;
-use Patchlevel\EventSourcing\Aggregate\AggregateRoot;
-use Patchlevel\EventSourcing\Attribute\Handle;
-use Patchlevel\EventSourcing\Attribute\HandledBy;
use Patchlevel\EventSourcing\CommandBus\Handler\HandlerFactory;
-use ReflectionClass;
-use ReflectionMethod;
-use Symfony\Component\TypeInfo\Type\ObjectType;
-use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;
-
-use function array_key_exists;
-use function is_a;
+use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry;
final class AggregateHandlerProvider implements HandlerProvider
{
- /** @var array */
- private array $handlers = [];
+ private bool $initialized = false;
- private readonly TypeResolver $typeResolver;
+ /** @var array> */
+ private array $handlers = [];
public function __construct(
+ private readonly AggregateRootRegistry $aggregateRootRegistry,
private readonly HandlerFactory $handlerFactory,
) {
- $this->typeResolver = TypeResolver::create();
- }
-
- /**
- * @param class-string $commandClass
- *
- * @throws HandlerNotFound
- */
- public function handlerForCommand(string $commandClass): HandlerDescriptor
- {
- if (array_key_exists($commandClass, $this->handlers)) {
- return $this->handlers[$commandClass];
- }
-
- $aggregateClass = $this->aggregateClass($commandClass);
-
- $reflectionClass = new ReflectionClass($aggregateClass);
-
- foreach ($reflectionClass->getMethods() as $method) {
- if (!$this->canHandle($method, $commandClass)) {
- continue;
- }
-
- if ($method->isStatic()) {
- $this->handlers[$commandClass] = new HandlerDescriptor($this->handlerFactory->createHandler(
- $aggregateClass,
- $method->getName(),
- ));
- } else {
- $this->handlers[$commandClass] = new HandlerDescriptor($this->handlerFactory->updateHandler(
- $aggregateClass,
- $method->getName(),
- ));
- }
-
- return $this->handlers[$commandClass];
- }
-
- throw new HandlerNotFound($commandClass);
}
/**
* @param class-string $commandClass
*
- * @return class-string
+ * @return iterable
*/
- private function aggregateClass(string $commandClass): string
+ public function handlerForCommand(string $commandClass): iterable
{
- $reflectionClass = new ReflectionClass($commandClass);
- $attributes = $reflectionClass->getAttributes(HandledBy::class);
-
- if ($attributes === []) {
- throw new MissingHandledBy($commandClass);
+ if (!$this->initialized) {
+ $this->initialize();
}
- $handledBy = $attributes[0]->newInstance();
-
- return $handledBy->aggregateClass;
+ return $this->handlers[$commandClass] ?? [];
}
- private function canHandle(ReflectionMethod $reflectionMethod, string $commandClass): bool
+ private function initialize(): void
{
- $handleAttributes = $reflectionMethod->getAttributes(Handle::class);
-
- if ($handleAttributes === []) {
- return false;
- }
-
- $parameters = $reflectionMethod->getParameters();
-
- if ($parameters === []) {
- throw InvalidHandleMethod::noParameters(
- $reflectionMethod->getDeclaringClass()->getName(),
- $reflectionMethod->getName(),
- );
- }
-
- $reflectionType = $parameters[0]->getType();
-
- if ($reflectionType === null) {
- throw InvalidHandleMethod::incompatibleType(
- $reflectionMethod->getDeclaringClass()->getName(),
- $reflectionMethod->getName(),
- );
- }
-
- $type = $this->typeResolver->resolve($reflectionType);
-
- if (!$type instanceof ObjectType) {
- throw InvalidHandleMethod::incompatibleType(
- $reflectionMethod->getDeclaringClass()->getName(),
- $reflectionMethod->getName(),
- );
- }
-
- $handle = $handleAttributes[0]->newInstance();
- $handleClassName = $handle->commandClass ?: $type->getClassName();
+ foreach ($this->aggregateRootRegistry->aggregateClasses() as $aggregateClass) {
+ $aggregateHandlerFinder = new AggregateHandlerFinder($aggregateClass);
+
+ foreach ($aggregateHandlerFinder->createHandlers() as $handler) {
+ $this->handlers[$handler->commandClass][] = new HandlerDescriptor(
+ $this->handlerFactory->createHandler(
+ $aggregateClass,
+ $handler->method,
+ ),
+ );
+ }
- if ($handleClassName === $commandClass) {
- return true;
+ foreach ($aggregateHandlerFinder->updateHandlers() as $handler) {
+ $this->handlers[$handler->commandClass][] = new HandlerDescriptor(
+ $this->handlerFactory->updateHandler(
+ $aggregateClass,
+ $handler->method,
+ ),
+ );
+ }
}
- return is_a($commandClass, $handleClassName, true);
+ $this->initialized = true;
}
}
diff --git a/src/CommandBus/DefaultCommandBus.php b/src/CommandBus/DefaultCommandBus.php
index 410eafeb1..cf6302eaf 100644
--- a/src/CommandBus/DefaultCommandBus.php
+++ b/src/CommandBus/DefaultCommandBus.php
@@ -5,11 +5,15 @@
namespace Patchlevel\EventSourcing\CommandBus;
use Patchlevel\EventSourcing\CommandBus\Handler\DefaultHandlerFactory;
+use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry;
use Patchlevel\EventSourcing\Repository\RepositoryManager;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use function array_shift;
+use function count;
+use function is_array;
+use function iterator_to_array;
use function sprintf;
final class DefaultCommandBus implements CommandBus
@@ -48,9 +52,23 @@ public function dispatch(object $command): void
$this->logger?->debug('CommandBus: Start processing queue.');
while ($command = array_shift($this->queue)) {
- $handler = $this->handlerProvider->handlerForCommand($command::class);
+ $handlers = $this->handlerProvider->handlerForCommand($command::class);
- ($handler->callable())($command);
+ if (!is_array($handlers)) {
+ $handlers = iterator_to_array($handlers);
+ }
+
+ $count = count($handlers);
+
+ if ($count === 0) {
+ throw new HandlerNotFound($command::class);
+ }
+
+ if ($count > 1) {
+ throw new MultipleHandlersFound($command::class);
+ }
+
+ ($handlers[0]->callable())($command);
}
} finally {
$this->processing = false;
@@ -59,13 +77,15 @@ public function dispatch(object $command): void
}
}
- public static function createDefault(
+ public static function createForAggregateHandlers(
+ AggregateRootRegistry $aggregateRootRegistry,
RepositoryManager $repositoryManager,
ContainerInterface|null $container = null,
LoggerInterface|null $logger = null,
): self {
return new self(
new AggregateHandlerProvider(
+ $aggregateRootRegistry,
new DefaultHandlerFactory(
$repositoryManager,
$container,
diff --git a/src/CommandBus/HandlerNotFound.php b/src/CommandBus/HandlerNotFound.php
index f555fa1d8..476f4a17b 100644
--- a/src/CommandBus/HandlerNotFound.php
+++ b/src/CommandBus/HandlerNotFound.php
@@ -10,6 +10,7 @@
final class HandlerNotFound extends RuntimeException
{
+ /** @param class-string $commandClass */
public function __construct(string $commandClass)
{
parent::__construct(sprintf('Handler for command "%s" not found', $commandClass));
diff --git a/src/CommandBus/HandlerProvider.php b/src/CommandBus/HandlerProvider.php
index 6069b2eb1..f0b73e155 100644
--- a/src/CommandBus/HandlerProvider.php
+++ b/src/CommandBus/HandlerProvider.php
@@ -9,7 +9,7 @@ interface HandlerProvider
/**
* @param class-string $commandClass
*
- * @throws HandlerNotFound
+ * @return iterable
*/
- public function handlerForCommand(string $commandClass): HandlerDescriptor;
+ public function handlerForCommand(string $commandClass): iterable;
}
diff --git a/src/CommandBus/MissingHandledBy.php b/src/CommandBus/MissingHandledBy.php
deleted file mode 100644
index b41cd91af..000000000
--- a/src/CommandBus/MissingHandledBy.php
+++ /dev/null
@@ -1,17 +0,0 @@
- ProfileWithCommands::class]);
+
$manager = new DefaultRepositoryManager(
new AggregateRootRegistry(['profile_with_commands' => ProfileWithCommands::class]),
$store,
@@ -280,7 +282,8 @@ public function testCommandBus(): void
$engine,
);
- $commandBus = DefaultCommandBus::createDefault(
+ $commandBus = DefaultCommandBus::createForAggregateHandlers(
+ $aggregateRootRegistry,
$manager,
new ServiceLocator([
ClockInterface::class => new SystemClock(),
diff --git a/tests/Integration/BasicImplementation/Command/ChangeProfileName.php b/tests/Integration/BasicImplementation/Command/ChangeProfileName.php
index b490b3821..7061192a9 100644
--- a/tests/Integration/BasicImplementation/Command/ChangeProfileName.php
+++ b/tests/Integration/BasicImplementation/Command/ChangeProfileName.php
@@ -4,12 +4,9 @@
namespace Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Command;
-use Patchlevel\EventSourcing\Attribute\HandledBy;
use Patchlevel\EventSourcing\Attribute\Id;
use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\ProfileId;
-use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\ProfileWithCommands;
-#[HandledBy(ProfileWithCommands::class)]
final class ChangeProfileName
{
public function __construct(
diff --git a/tests/Integration/BasicImplementation/Command/CreateProfile.php b/tests/Integration/BasicImplementation/Command/CreateProfile.php
index 04f51fc90..774047cc2 100644
--- a/tests/Integration/BasicImplementation/Command/CreateProfile.php
+++ b/tests/Integration/BasicImplementation/Command/CreateProfile.php
@@ -4,11 +4,8 @@
namespace Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\Command;
-use Patchlevel\EventSourcing\Attribute\HandledBy;
use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\ProfileId;
-use Patchlevel\EventSourcing\Tests\Integration\BasicImplementation\ProfileWithCommands;
-#[HandledBy(ProfileWithCommands::class)]
final class CreateProfile
{
public function __construct(
diff --git a/tests/Unit/CommandBus/AggregateHandlerProviderTest.php b/tests/Unit/CommandBus/AggregateHandlerProviderTest.php
index 0f6c510cd..80a9e72c9 100644
--- a/tests/Unit/CommandBus/AggregateHandlerProviderTest.php
+++ b/tests/Unit/CommandBus/AggregateHandlerProviderTest.php
@@ -7,49 +7,58 @@
use Patchlevel\EventSourcing\CommandBus\AggregateHandlerProvider;
use Patchlevel\EventSourcing\CommandBus\Handler\HandlerFactory;
use Patchlevel\EventSourcing\CommandBus\InvalidHandleMethod;
-use Patchlevel\EventSourcing\CommandBus\MissingHandledBy;
+use Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry;
use Patchlevel\EventSourcing\Tests\Unit\Fixture\ChangeProfileName;
use Patchlevel\EventSourcing\Tests\Unit\Fixture\CreateProfile;
-use Patchlevel\EventSourcing\Tests\Unit\Fixture\NoParameterCommand;
-use Patchlevel\EventSourcing\Tests\Unit\Fixture\NoTypeCommand;
-use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileWithHandlers;
+use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileWithHandler;
+use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileWithNoParameterHandler;
+use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileWithNoTypeHandler;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
-use stdClass;
/** @covers \Patchlevel\EventSourcing\CommandBus\AggregateHandlerProvider */
final class AggregateHandlerProviderTest extends TestCase
{
use ProphecyTrait;
- public function testMissingHandledBy(): void
+ public function testNoParameters(): void
{
$handlerFactory = $this->prophesize(HandlerFactory::class);
- $provider = new AggregateHandlerProvider($handlerFactory->reveal());
+ $provider = new AggregateHandlerProvider(
+ new AggregateRootRegistry(['profile' => ProfileWithNoParameterHandler::class]),
+ $handlerFactory->reveal(),
+ );
- $this->expectException(MissingHandledBy::class);
+ $this->expectException(InvalidHandleMethod::class);
- $provider->handlerForCommand(stdClass::class);
+ $provider->handlerForCommand(ChangeProfileName::class);
}
- public function testNoParameters(): void
+ public function testNoType(): void
{
$handlerFactory = $this->prophesize(HandlerFactory::class);
- $provider = new AggregateHandlerProvider($handlerFactory->reveal());
+ $provider = new AggregateHandlerProvider(
+ new AggregateRootRegistry(['profile' => ProfileWithNoTypeHandler::class]),
+ $handlerFactory->reveal(),
+ );
$this->expectException(InvalidHandleMethod::class);
- $provider->handlerForCommand(NoParameterCommand::class);
+ $provider->handlerForCommand(ChangeProfileName::class);
}
- public function testNoType(): void
+ public function testEmpty(): void
{
$handlerFactory = $this->prophesize(HandlerFactory::class);
- $provider = new AggregateHandlerProvider($handlerFactory->reveal());
- $this->expectException(InvalidHandleMethod::class);
+ $provider = new AggregateHandlerProvider(
+ new AggregateRootRegistry([]),
+ $handlerFactory->reveal(),
+ );
+
+ $result = $provider->handlerForCommand(CreateProfile::class);
- $provider->handlerForCommand(NoTypeCommand::class);
+ self::assertCount(0, $result);
}
public function testGetCreateHandler(): void
@@ -58,15 +67,24 @@ public function testGetCreateHandler(): void
$handlerFactory = $this->prophesize(HandlerFactory::class);
$handlerFactory
- ->createHandler(ProfileWithHandlers::class, 'create')
+ ->createHandler(ProfileWithHandler::class, 'create')
->shouldBeCalled()
->willReturn($handler);
- $provider = new AggregateHandlerProvider($handlerFactory->reveal());
+ $handlerFactory
+ ->updateHandler(ProfileWithHandler::class, 'changeName')
+ ->shouldBeCalled()
+ ->willReturn($handler);
+
+ $provider = new AggregateHandlerProvider(
+ new AggregateRootRegistry(['profile' => ProfileWithHandler::class]),
+ $handlerFactory->reveal(),
+ );
$result = $provider->handlerForCommand(CreateProfile::class);
- self::assertSame($handler, $result->callable());
+ self::assertCount(1, $result);
+ self::assertSame($handler, $result[0]->callable());
}
public function testGetUpdateHandler(): void
@@ -74,15 +92,25 @@ public function testGetUpdateHandler(): void
$handler = static fn (ChangeProfileName $command): mixed => null;
$handlerFactory = $this->prophesize(HandlerFactory::class);
+
+ $handlerFactory
+ ->createHandler(ProfileWithHandler::class, 'create')
+ ->shouldBeCalled()
+ ->willReturn($handler);
+
$handlerFactory
- ->updateHandler(ProfileWithHandlers::class, 'changeName')
+ ->updateHandler(ProfileWithHandler::class, 'changeName')
->shouldBeCalled()
->willReturn($handler);
- $provider = new AggregateHandlerProvider($handlerFactory->reveal());
+ $provider = new AggregateHandlerProvider(
+ new AggregateRootRegistry(['profile' => ProfileWithHandler::class]),
+ $handlerFactory->reveal(),
+ );
$result = $provider->handlerForCommand(ChangeProfileName::class);
- self::assertSame($handler, $result->callable());
+ self::assertCount(1, $result);
+ self::assertSame($handler, $result[0]->callable());
}
}
diff --git a/tests/Unit/CommandBus/DefaultCommandBusTest.php b/tests/Unit/CommandBus/DefaultCommandBusTest.php
index 4c461825f..c39b50bcd 100644
--- a/tests/Unit/CommandBus/DefaultCommandBusTest.php
+++ b/tests/Unit/CommandBus/DefaultCommandBusTest.php
@@ -8,6 +8,7 @@
use Patchlevel\EventSourcing\CommandBus\HandlerDescriptor;
use Patchlevel\EventSourcing\CommandBus\HandlerNotFound;
use Patchlevel\EventSourcing\CommandBus\HandlerProvider;
+use Patchlevel\EventSourcing\CommandBus\MultipleHandlersFound;
use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait;
@@ -22,7 +23,7 @@ public function testHandlerNotFound(): void
};
$handlerProvider = $this->prophesize(HandlerProvider::class);
- $handlerProvider->handlerForCommand($command::class)->willThrow(new HandlerNotFound($command::class));
+ $handlerProvider->handlerForCommand($command::class)->willReturn([]);
$commandBus = new DefaultCommandBus($handlerProvider->reveal());
@@ -31,6 +32,24 @@ public function testHandlerNotFound(): void
$commandBus->dispatch($command);
}
+ public function testMultipleHandlersFound(): void
+ {
+ $command = new class {
+ };
+
+ $handlerProvider = $this->prophesize(HandlerProvider::class);
+ $handlerProvider->handlerForCommand($command::class)->willReturn([
+ new HandlerDescriptor(static fn () => null),
+ new HandlerDescriptor(static fn () => null),
+ ]);
+
+ $commandBus = new DefaultCommandBus($handlerProvider->reveal());
+
+ $this->expectException(MultipleHandlersFound::class);
+
+ $commandBus->dispatch($command);
+ }
+
public function testHandleSuccess(): void
{
$command = new class {
@@ -46,9 +65,9 @@ public function __invoke(object $command): void
};
$handlerProvider = $this->prophesize(HandlerProvider::class);
- $handlerProvider->handlerForCommand($command::class)->willReturn(
+ $handlerProvider->handlerForCommand($command::class)->willReturn([
new HandlerDescriptor($handler),
- );
+ ]);
$commandBus = new DefaultCommandBus($handlerProvider->reveal());
diff --git a/tests/Unit/CommandBus/HandlerDescriptorTest.php b/tests/Unit/CommandBus/HandlerDescriptorTest.php
index ac5ae94fc..c7174126f 100644
--- a/tests/Unit/CommandBus/HandlerDescriptorTest.php
+++ b/tests/Unit/CommandBus/HandlerDescriptorTest.php
@@ -5,7 +5,7 @@
namespace Patchlevel\EventSourcing\Tests\Unit\CommandBus;
use Patchlevel\EventSourcing\CommandBus\HandlerDescriptor;
-use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileWithHandlers;
+use Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileWithHandler;
use PHPUnit\Framework\Attributes\RequiresPhp;
use PHPUnit\Framework\TestCase;
@@ -14,20 +14,20 @@ final class HandlerDescriptorTest extends TestCase
{
public function testObjectMethod(): void
{
- $aggregate = ProfileWithHandlers::createEmpty();
+ $aggregate = ProfileWithHandler::createEmpty();
$descriptor = new HandlerDescriptor($aggregate->changeName(...));
self::assertEquals($aggregate->changeName(...), $descriptor->callable());
- self::assertEquals('Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileWithHandlers::changeName', $descriptor->name());
+ self::assertEquals('Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileWithHandler::changeName', $descriptor->name());
}
public function testStaticObjectMethod(): void
{
- $descriptor = new HandlerDescriptor([ProfileWithHandlers::class, 'create']);
+ $descriptor = new HandlerDescriptor([ProfileWithHandler::class, 'create']);
- self::assertEquals(ProfileWithHandlers::create(...), $descriptor->callable());
- self::assertEquals('Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileWithHandlers::create', $descriptor->name());
+ self::assertEquals(ProfileWithHandler::create(...), $descriptor->callable());
+ self::assertEquals('Patchlevel\EventSourcing\Tests\Unit\Fixture\ProfileWithHandler::create', $descriptor->name());
}
#[RequiresPhp('>= 8.2')]
diff --git a/tests/Unit/Fixture/AggregateWithoutHandleCommand.php b/tests/Unit/Fixture/AggregateWithoutHandleCommand.php
deleted file mode 100644
index f2258a557..000000000
--- a/tests/Unit/Fixture/AggregateWithoutHandleCommand.php
+++ /dev/null
@@ -1,12 +0,0 @@
-