registry = new ErrorHandlerRegistry(); $this->manager = new ErrorHandlerManager($this->registry); }); it('executes handlers in priority order', function () { $executionOrder = []; $highPriorityHandler = new class ($executionOrder) implements ErrorHandlerInterface { public function __construct(private array &$executionOrder) {} public function canHandle(\Throwable $exception): bool { return true; } public function handle(\Throwable $exception): HandlerResult { $this->executionOrder[] = 'high'; return HandlerResult::create( handled: true, message: 'High priority handler' ); } public function getName(): string { return 'high_priority'; } public function getPriority(): ErrorHandlerPriority { return ErrorHandlerPriority::HIGH; } }; $lowPriorityHandler = new class ($executionOrder) implements ErrorHandlerInterface { public function __construct(private array &$executionOrder) {} public function canHandle(\Throwable $exception): bool { return true; } public function handle(\Throwable $exception): HandlerResult { $this->executionOrder[] = 'low'; return HandlerResult::create( handled: true, message: 'Low priority handler' ); } public function getName(): string { return 'low_priority'; } public function getPriority(): ErrorHandlerPriority { return ErrorHandlerPriority::LOW; } }; $this->manager = $this->manager->register($lowPriorityHandler, $highPriorityHandler); $exception = new \Exception('Test'); $this->manager->handle($exception); expect($executionOrder)->toBe(['high', 'low']); }); it('stops propagation when handler marks as final', function () { $called = []; $finalHandler = new class ($called) implements ErrorHandlerInterface { public function __construct(private array &$called) {} public function canHandle(\Throwable $exception): bool { return true; } public function handle(\Throwable $exception): HandlerResult { $this->called[] = 'final'; return HandlerResult::create( handled: true, message: 'Final handler', isFinal: true ); } public function getName(): string { return 'final_handler'; } public function getPriority(): ErrorHandlerPriority { return ErrorHandlerPriority::HIGH; } }; $afterHandler = new class ($called) implements ErrorHandlerInterface { public function __construct(private array &$called) {} public function canHandle(\Throwable $exception): bool { return true; } public function handle(\Throwable $exception): HandlerResult { $this->called[] = 'after'; return HandlerResult::create( handled: true, message: 'After handler' ); } public function getName(): string { return 'after_handler'; } public function getPriority(): ErrorHandlerPriority { return ErrorHandlerPriority::LOW; } }; $this->manager = $this->manager->register($finalHandler, $afterHandler); $exception = new \Exception('Test'); $result = $this->manager->handle($exception); expect($called)->toBe(['final']); expect($result->handled)->toBeTrue(); }); it('skips handlers that cannot handle exception', function () { $specificHandler = new class implements ErrorHandlerInterface { public function canHandle(\Throwable $exception): bool { return $exception instanceof \InvalidArgumentException; } public function handle(\Throwable $exception): HandlerResult { return HandlerResult::create( handled: true, message: 'Specific handler' ); } public function getName(): string { return 'specific'; } public function getPriority(): ErrorHandlerPriority { return ErrorHandlerPriority::HIGH; } }; $this->manager = $this->manager->register($specificHandler); $exception = new \RuntimeException('Test'); $result = $this->manager->handle($exception); expect($result->handled)->toBeFalse(); expect($result->results)->toBeEmpty(); }); it('continues chain even if handler throws exception', function () { $called = []; $failingHandler = new class ($called) implements ErrorHandlerInterface { public function __construct(private array &$called) {} public function canHandle(\Throwable $exception): bool { return true; } public function handle(\Throwable $exception): HandlerResult { $this->called[] = 'failing'; throw new \RuntimeException('Handler failed'); } public function getName(): string { return 'failing'; } public function getPriority(): ErrorHandlerPriority { return ErrorHandlerPriority::HIGH; } }; $workingHandler = new class ($called) implements ErrorHandlerInterface { public function __construct(private array &$called) {} public function canHandle(\Throwable $exception): bool { return true; } public function handle(\Throwable $exception): HandlerResult { $this->called[] = 'working'; return HandlerResult::create( handled: true, message: 'Working handler' ); } public function getName(): string { return 'working'; } public function getPriority(): ErrorHandlerPriority { return ErrorHandlerPriority::LOW; } }; $this->manager = $this->manager->register($failingHandler, $workingHandler); $exception = new \Exception('Test'); $result = $this->manager->handle($exception); expect($called)->toBe(['failing', 'working']); expect($result->handled)->toBeTrue(); }); it('aggregates results from multiple handlers', function () { $handler1 = new class implements ErrorHandlerInterface { public function canHandle(\Throwable $exception): bool { return true; } public function handle(\Throwable $exception): HandlerResult { return HandlerResult::create( handled: true, message: 'Handler 1', data: ['from' => 'handler1'] ); } public function getName(): string { return 'handler1'; } public function getPriority(): ErrorHandlerPriority { return ErrorHandlerPriority::HIGH; } }; $handler2 = new class implements ErrorHandlerInterface { public function canHandle(\Throwable $exception): bool { return true; } public function handle(\Throwable $exception): HandlerResult { return HandlerResult::create( handled: true, message: 'Handler 2', data: ['from' => 'handler2'] ); } public function getName(): string { return 'handler2'; } public function getPriority(): ErrorHandlerPriority { return ErrorHandlerPriority::LOW; } }; $this->manager = $this->manager->register($handler1, $handler2); $exception = new \Exception('Test'); $result = $this->manager->handle($exception); expect($result->results)->toHaveCount(2); expect($result->getMessages())->toBe(['Handler 1', 'Handler 2']); $combinedData = $result->getCombinedData(); expect($combinedData)->toHaveKey('from'); }); });