Skip to content

Commit

Permalink
refactor handler provider
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidBadura committed Dec 20, 2024
1 parent ad0eb7c commit d2ce86a
Show file tree
Hide file tree
Showing 24 changed files with 325 additions and 238 deletions.
17 changes: 11 additions & 6 deletions baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,16 @@
<code><![CDATA[$row['payload']]]></code>
</MixedArgument>
</file>
<file src="tests/Unit/CommandBus/AggregateHandlerProviderTest.php">
<InvalidArrayAccess>
<code><![CDATA[$result[0]]]></code>
<code><![CDATA[$result[0]]]></code>
</InvalidArrayAccess>
<MixedMethodCall>
<code><![CDATA[callable]]></code>
<code><![CDATA[callable]]></code>
</MixedMethodCall>
</file>
<file src="tests/Unit/CommandBus/DefaultCommandBusTest.php">
<InvalidPropertyFetch>
<code><![CDATA[$handler->command]]></code>
Expand All @@ -251,17 +261,12 @@
<code><![CDATA[$value]]></code>
</MixedArgumentTypeCoercion>
</file>
<file src="tests/Unit/Fixture/ProfileWithBrokenApplyIntersection.php">
<ReservedWord>
<code><![CDATA[ProfileCreated&ProfileVisited $event]]></code>
</ReservedWord>
</file>
<file src="tests/Unit/Fixture/ProfileWithBrokenApplyNoType.php">
<MissingParamType>
<code><![CDATA[$event]]></code>
</MissingParamType>
</file>
<file src="tests/Unit/Fixture/ProfileWithHandlers.php">
<file src="tests/Unit/Fixture/ProfileWithNoTypeHandler.php">
<MissingParamType>
<code><![CDATA[$command]]></code>
</MissingParamType>
Expand Down
18 changes: 0 additions & 18 deletions src/Attribute/HandledBy.php

This file was deleted.

16 changes: 16 additions & 0 deletions src/CommandBus/AggregateHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\CommandBus;

/** @internal */
final class AggregateHandler
{
/** @param class-string $commandClass */
public function __construct(
public readonly string $method,
public readonly string $commandClass,
) {
}
}
99 changes: 99 additions & 0 deletions src/CommandBus/AggregateHandlerFinder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace Patchlevel\EventSourcing\CommandBus;

use Patchlevel\EventSourcing\Aggregate\AggregateRoot;
use Patchlevel\EventSourcing\Attribute\Handle;
use ReflectionClass;
use Symfony\Component\TypeInfo\Type\ObjectType;
use Symfony\Component\TypeInfo\TypeResolver\TypeResolver;

use function class_exists;

/** @internal */
final class AggregateHandlerFinder
{
/** @var list<AggregateHandler> */
private array $createHandlers = [];

/** @var list<AggregateHandler> */
private array $updateHandlers = [];

/** @param class-string<AggregateRoot> $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<AggregateHandler> */
public function createHandlers(): iterable
{
return $this->createHandlers;
}

/** @return iterable<AggregateHandler> */
public function updateHandlers(): iterable
{
return $this->updateHandlers;
}
}
132 changes: 31 additions & 101 deletions src/CommandBus/AggregateHandlerProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<class-string, HandlerDescriptor> */
private array $handlers = [];
private bool $initialized = false;

private readonly TypeResolver $typeResolver;
/** @var array<class-string, list<HandlerDescriptor>> */
private array $handlers = [];

public function __construct(
private readonly AggregateRootRegistry $aggregateRootRegistry,

Check failure on line 18 in src/CommandBus/AggregateHandlerProvider.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Deptrac (locked, 8.3, ubuntu-latest)

Patchlevel\EventSourcing\CommandBus\AggregateHandlerProvider must not depend on Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry (CommandBus on MetadataAggregate)
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<AggregateRoot>
* @return iterable<HandlerDescriptor>
*/
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;

Check warning on line 61 in src/CommandBus/AggregateHandlerProvider.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "TrueValue": --- Original +++ New @@ @@ $this->handlers[$handler->commandClass][] = new HandlerDescriptor($this->handlerFactory->updateHandler($aggregateClass, $handler->method)); } } - $this->initialized = true; + $this->initialized = false; } }

Check warning on line 61 in src/CommandBus/AggregateHandlerProvider.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "TrueValue": --- Original +++ New @@ @@ $this->handlers[$handler->commandClass][] = new HandlerDescriptor($this->handlerFactory->updateHandler($aggregateClass, $handler->method)); } } - $this->initialized = true; + $this->initialized = false; } }
}
}
26 changes: 23 additions & 3 deletions src/CommandBus/DefaultCommandBus.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Check warning on line 74 in src/CommandBus/DefaultCommandBus.php

View workflow job for this annotation

GitHub Actions / Mutation tests on diff (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "FalseValue": --- Original +++ New @@ @@ $handlers[0]->callable()($command); } } finally { - $this->processing = false; + $this->processing = true; $this->logger?->debug('CommandBus: Finished processing queue.'); } }

Check warning on line 74 in src/CommandBus/DefaultCommandBus.php

View workflow job for this annotation

GitHub Actions / Mutation tests (locked, 8.3, ubuntu-latest)

Escaped Mutant for Mutator "FalseValue": --- Original +++ New @@ @@ $handlers[0]->callable()($command); } } finally { - $this->processing = false; + $this->processing = true; $this->logger?->debug('CommandBus: Finished processing queue.'); } }
Expand All @@ -59,13 +77,15 @@ public function dispatch(object $command): void
}
}

public static function createDefault(
public static function createForAggregateHandlers(
AggregateRootRegistry $aggregateRootRegistry,

Check failure on line 81 in src/CommandBus/DefaultCommandBus.php

View workflow job for this annotation

GitHub Actions / Static Analysis by Deptrac (locked, 8.3, ubuntu-latest)

Patchlevel\EventSourcing\CommandBus\DefaultCommandBus must not depend on Patchlevel\EventSourcing\Metadata\AggregateRoot\AggregateRootRegistry (CommandBus on MetadataAggregate)
RepositoryManager $repositoryManager,
ContainerInterface|null $container = null,
LoggerInterface|null $logger = null,
): self {
return new self(
new AggregateHandlerProvider(
$aggregateRootRegistry,
new DefaultHandlerFactory(
$repositoryManager,
$container,
Expand Down
Loading

0 comments on commit d2ce86a

Please sign in to comment.