container = new DefaultContainer(); $this->container->instance(MethodInvoker::class, new MethodInvoker( $this->container, new SimpleReflectionService() )); // Mock Logger $logger = $this->createMock(Logger::class); $this->container->instance(Logger::class, $logger); // Mock Queue $queue = $this->createMock(Queue::class); $this->container->instance(Queue::class, $queue); // Mock PerformanceCollectorInterface (optional - wird nur verwendet wenn verfügbar) // Wir registrieren es nicht, damit die Middleware es als optional behandelt // Mock ExecutionContext $executionContext = new ExecutionContext(ContextType::TEST); $this->container->instance(ExecutionContext::class, $executionContext); // Reset execution tracking $this->beforeExecuted = new \ArrayObject(); $this->afterExecuted = new \ArrayObject(); $this->onErrorExecuted = new \ArrayObject(); } public function testBeforeExecuteAttributeIsExecuted(): void { $command = new TestCommand('test'); $handler = new TestHandlerWithBeforeAttribute($this->beforeExecuted); $this->setupCommandBus($handler::class, 'handle', TestCommand::class); $this->commandBus->dispatch($command); $this->assertContains('before', (array)$this->beforeExecuted, 'BeforeExecute should have been executed'); } public function testAfterExecuteAttributeIsExecuted(): void { $command = new TestCommand('test'); $handler = new TestHandlerWithAfterAttribute($this->afterExecuted); $this->setupCommandBus($handler::class, 'handle', TestCommand::class); $result = $this->commandBus->dispatch($command); $this->assertContains('after', (array)$this->afterExecuted); $this->assertEquals('result', $result); } public function testOnErrorAttributeIsExecuted(): void { $command = new TestCommand('test'); $handler = new TestHandlerWithOnErrorAttribute($this->onErrorExecuted); $this->setupCommandBus($handler::class, 'handle', TestCommand::class); try { $this->commandBus->dispatch($command); $this->fail('Expected exception was not thrown'); } catch (\RuntimeException $e) { $this->assertEquals('Test error', $e->getMessage()); } $this->assertContains('error', (array)$this->onErrorExecuted); } private function setupCommandBus(string $handlerClass, string $handlerMethod, string $commandClass): void { // Erstelle Handler-Descriptor $handlerDescriptor = new CommandHandlerDescriptor( class: $handlerClass, method: $handlerMethod, command: $commandClass ); $handlersCollection = new CommandHandlersCollection($handlerDescriptor); $this->container->instance(CommandHandlersCollection::class, $handlersCollection); // Erstelle DiscoveryRegistry mit Handler-Attribut $discoveryRegistry = new DiscoveryRegistry( attributes: new AttributeRegistry() ); // Füge CommandHandler-Attribut hinzu $commandHandlerAttribute = new DiscoveredAttribute( className: \App\Framework\Core\ValueObjects\ClassName::create($handlerClass), attributeClass: CommandHandler::class, target: AttributeTarget::METHOD, methodName: \App\Framework\Core\ValueObjects\MethodName::create($handlerMethod), arguments: [], additionalData: [ 'class' => $handlerClass, 'method' => $handlerMethod, 'command' => $commandClass, ] ); $discoveryRegistry->attributes->add(CommandHandler::class, $commandHandlerAttribute); // Entdecke BeforeExecute, AfterExecute, OnError Attribute für den Handler $reflectionClass = new \ReflectionClass($handlerClass); $reflectionMethod = $reflectionClass->getMethod($handlerMethod); foreach ($reflectionMethod->getAttributes(BeforeExecute::class) as $attribute) { $beforeAttribute = new DiscoveredAttribute( className: \App\Framework\Core\ValueObjects\ClassName::create($handlerClass), attributeClass: BeforeExecute::class, target: AttributeTarget::METHOD, methodName: \App\Framework\Core\ValueObjects\MethodName::create($handlerMethod), arguments: $attribute->getArguments(), additionalData: [] ); $discoveryRegistry->attributes->add(BeforeExecute::class, $beforeAttribute); } foreach ($reflectionMethod->getAttributes(AfterExecute::class) as $attribute) { $afterAttribute = new DiscoveredAttribute( className: \App\Framework\Core\ValueObjects\ClassName::create($handlerClass), attributeClass: AfterExecute::class, target: AttributeTarget::METHOD, methodName: \App\Framework\Core\ValueObjects\MethodName::create($handlerMethod), arguments: $attribute->getArguments(), additionalData: [] ); $discoveryRegistry->attributes->add(AfterExecute::class, $afterAttribute); } foreach ($reflectionMethod->getAttributes(OnError::class) as $attribute) { $onErrorAttribute = new DiscoveredAttribute( className: \App\Framework\Core\ValueObjects\ClassName::create($handlerClass), attributeClass: OnError::class, target: AttributeTarget::METHOD, methodName: \App\Framework\Core\ValueObjects\MethodName::create($handlerMethod), arguments: $attribute->getArguments(), additionalData: [] ); $discoveryRegistry->attributes->add(OnError::class, $onErrorAttribute); } $this->container->instance(\App\Framework\Discovery\Results\DiscoveryRegistry::class, $discoveryRegistry); // Initialisiere Attribute Execution System $callbackExecutor = new CallbackExecutor($this->container); $this->container->instance(CallbackExecutor::class, $callbackExecutor); $attributeRunner = new AttributeRunner( discoveryRegistry: $discoveryRegistry, container: $this->container, callbackExecutor: $callbackExecutor ); $this->container->instance(AttributeRunner::class, $attributeRunner); // Registriere Test-Handler im Container $handlerInstance = new $handlerClass(...$this->getHandlerDependencies($handlerClass)); $this->container->instance($handlerClass, $handlerInstance); // Registriere Test-Attribute-Handler mit korrekten Referenzen // WICHTIG: Wir müssen die ArrayObjects direkt übergeben, nicht über Closures if (str_contains($handlerClass, 'Before')) { $this->container->instance(TestBeforeHandler::class, new TestBeforeHandler($this->beforeExecuted)); } if (str_contains($handlerClass, 'After')) { $this->container->instance(TestAfterHandler::class, new TestAfterHandler($this->afterExecuted)); } if (str_contains($handlerClass, 'OnError')) { $this->container->instance(TestOnErrorHandler::class, new TestOnErrorHandler($this->onErrorExecuted)); } $handlerAttributeExecutor = new HandlerAttributeExecutor( discoveryRegistry: $discoveryRegistry, container: $this->container, attributeRunner: $attributeRunner ); $this->container->instance(HandlerAttributeExecutor::class, $handlerAttributeExecutor); // Erstelle CommandBus $executionContext = $this->container->get(ExecutionContext::class); $queue = $this->container->get(Queue::class); $logger = $this->container->get(Logger::class); $this->commandBus = new DefaultCommandBus( commandHandlers: $handlersCollection, container: $this->container, executionContext: $executionContext, queue: $queue, logger: $logger ); } /** * @return array */ private function getHandlerDependencies(string $handlerClass): array { if (str_contains($handlerClass, 'Before')) { return [$this->beforeExecuted]; } if (str_contains($handlerClass, 'After')) { return [$this->afterExecuted]; } if (str_contains($handlerClass, 'OnError')) { return [$this->onErrorExecuted]; } return []; } } // Test Command final readonly class TestCommand { public function __construct( public string $data ) { } } // Test Handler mit BeforeExecute Attribute final class TestHandlerWithBeforeAttribute { public function __construct( private \ArrayObject $beforeExecuted ) {} #[BeforeExecute(TestBeforeHandler::class)] #[CommandHandler] public function handle(TestCommand $command): void { // Handler logic } } // Test Handler mit AfterExecute Attribute final class TestHandlerWithAfterAttribute { public function __construct( private \ArrayObject $afterExecuted ) {} #[AfterExecute(TestAfterHandler::class)] #[CommandHandler] public function handle(TestCommand $command): string { return 'result'; } } // Test Handler mit OnError Attribute final class TestHandlerWithOnErrorAttribute { public function __construct( private \ArrayObject $onErrorExecuted ) {} #[OnError(TestOnErrorHandler::class)] #[CommandHandler] public function handle(TestCommand $command): void { throw new \RuntimeException('Test error'); } } // Test Handler-Klassen für Attribute final class TestBeforeHandler { public function __construct( private \ArrayObject $beforeExecuted ) {} public function __invoke($ctx): void { $this->beforeExecuted[] = 'before'; } } final class TestAfterHandler { public function __construct( private \ArrayObject $afterExecuted ) {} public function __invoke($ctx): void { $this->afterExecuted[] = 'after'; } } final class TestOnErrorHandler { public function __construct( private \ArrayObject $onErrorExecuted ) {} public function __invoke($ctx): void { $this->onErrorExecuted[] = 'error'; } }