diff --git a/composer.json b/composer.json index e0aac8b..7ccaa42 100644 --- a/composer.json +++ b/composer.json @@ -12,14 +12,12 @@ "php": "^8.1", "psr/event-dispatcher": "^1.0", "revolt/event-loop": "^1.0", - "amphp/amp": "^3.0", - "psr/log": "^1.1 || ^2.0 || ^3.0" + "amphp/amp": "^3.0" }, "require-dev": { "phpunit/phpunit": "^10.0", "phpstan/phpstan": "^1.10", - "friendsofphp/php-cs-fixer": "^3.14", - "colinodell/psr-testlogger": "^1.3" + "friendsofphp/php-cs-fixer": "^3.14" }, "autoload": { "psr-4": { diff --git a/examples/basic-usage.php b/examples/basic-usage.php index b831368..a03453f 100644 --- a/examples/basic-usage.php +++ b/examples/basic-usage.php @@ -62,15 +62,9 @@ function () use ($event) { EventLoop::run(); // Set up logging for your dispatcher - all errors will be logged to PSR logger -$logger = new ColinODell\PsrTestLogger\TestLogger(); $dispatcher = new AsyncEventDispatcher( $listenerProvider, - $logger -); - -// Let errors bubble up. Useful for unit testing and debugging. -$dispatcher = new AsyncEventDispatcher( - $listenerProvider, - $logger, - AsyncEventDispatcher::THROW_ON_ERROR + function (Throwable $exception) { + error_log($exception->getMessage()); + } ); diff --git a/src/AsyncEventDispatcher.php b/src/AsyncEventDispatcher.php index ac2d628..6727ee7 100644 --- a/src/AsyncEventDispatcher.php +++ b/src/AsyncEventDispatcher.php @@ -19,7 +19,6 @@ use Psr\EventDispatcher\EventDispatcherInterface; use Psr\EventDispatcher\ListenerProviderInterface; use Psr\EventDispatcher\StoppableEventInterface; -use Psr\Log\LoggerInterface; use Throwable; /** @@ -30,16 +29,22 @@ */ class AsyncEventDispatcher implements EventDispatcherInterface { - public const THROW_ON_ERROR = 0b0001; + /** @var Closure(Throwable): (void) */ + private Closure $errorHandler; /** * @param ListenerProviderInterface $listenerProvider The provider of event listeners + * @param Closure(Throwable): (void) $errorHandler The handler for errors thrown by listeners */ public function __construct( private readonly ListenerProviderInterface $listenerProvider, - private readonly ?LoggerInterface $logger = null, - private readonly int $options = 0b0000 + ?Closure $errorHandler = null ) { + if ($errorHandler === null) { + $this->errorHandler = function (Throwable $exception): void {}; + } else { + $this->errorHandler = $errorHandler; + } } /** @@ -95,7 +100,7 @@ private function dispatchStoppableEvent( // that doesn't mean we want to block other listeners outside this loop. $future = async(function () use ($event, $listener) { $listener($event); - })->catch(Closure::fromCallable([$this, 'errorHandler'])); + })->catch($this->errorHandler); $future->await($cancellation); @@ -130,34 +135,13 @@ private function dispatchNonStoppableEvent( foreach ($listeners as $listener) { $futures[] = async(function () use ($event, $listener) { $listener($event); - })->catch(Closure::fromCallable([$this, 'errorHandler'])); + })->catch($this->errorHandler); } - // Wait for all listeners to complete - if ($this->options & self::THROW_ON_ERROR) { - // Let the errors bubble up - await($futures, $cancellation); - } else { - // Carry on despite errors - awaitAll($futures, $cancellation); - } + // Wait for all listeners to complete. This will carry on despite errors. + awaitAll($futures, $cancellation); return $event; }); } - - /** - * Handler for errors thrown by listeners. - * Based on the settings provided to the constructor, this method can log the errors and/or rethrow them. - */ - protected function errorHandler(Throwable $exception): void - { - if ($this->logger) { - $this->logger->error('Error dispatching event', ['exception' => $exception]); - } - - if (($this->options & self::THROW_ON_ERROR) === self::THROW_ON_ERROR) { - throw $exception; - } - } } diff --git a/tests/AsyncEventDispatcherTest.php b/tests/AsyncEventDispatcherTest.php index cada4cd..19ad611 100644 --- a/tests/AsyncEventDispatcherTest.php +++ b/tests/AsyncEventDispatcherTest.php @@ -13,9 +13,9 @@ use ArchiPro\EventDispatcher\Event\AbstractStoppableEvent; use ArchiPro\EventDispatcher\ListenerProvider; use ArchiPro\EventDispatcher\Tests\Fixture\TestEvent; -use ColinODell\PsrTestLogger\TestLogger; use Exception; use PHPUnit\Framework\TestCase; +use Throwable; /** * Test cases for AsyncEventDispatcher. @@ -31,7 +31,9 @@ class AsyncEventDispatcherTest extends TestCase { private ListenerProvider $listenerProvider; private AsyncEventDispatcher $dispatcher; - private TestLogger $logger; + + /** @var array */ + private array $errors = []; /** * Sets up the test environment before each test. @@ -39,10 +41,11 @@ class AsyncEventDispatcherTest extends TestCase protected function setUp(): void { $this->listenerProvider = new ListenerProvider(); - $this->logger = new TestLogger(); $this->dispatcher = new AsyncEventDispatcher( $this->listenerProvider, - $this->logger + function (Throwable $exception) { + $this->errors[] = $exception; + } ); } @@ -79,7 +82,7 @@ public function testDispatchEventToMultipleListeners(): void $this->assertContains('listener1: test data', $results); $this->assertContains('listener2: test data', $results); - $this->assertCount(0, $this->logger->records, 'No errors are logged'); + $this->assertCount(0, $this->errors, 'No errors are logged'); } /** @@ -104,7 +107,7 @@ public function testSynchronousStoppableEvent(): void $this->assertCount(1, $results); $this->assertEquals(['listener1'], $results); - $this->assertCount(0, $this->logger->records, 'No errors are logged'); + $this->assertCount(0, $this->errors, 'No errors are logged'); } /** @@ -116,7 +119,7 @@ public function testNoListenersForEvent(): void $dispatchedEvent = $this->dispatcher->dispatch($event); $this->assertSame($event, $dispatchedEvent->await()); - $this->assertCount(0, $this->logger->records, 'No errors are logged'); + $this->assertCount(0, $this->errors, 'No errors are logged'); } /** @@ -172,11 +175,7 @@ public function testDispatchesFailureInOneListenerDoesNotAffectOthers(): void 'The second listener should have been called despite the failure of the first listener' ); - $this->assertCount( - 2, - $this->logger->records, - 'Errors are logged to the logger' - ); + $this->assertCount(2, $this->errors, 'Errors are caught for both listeners'); } public function testCancellationOfStoppableEvent(): void @@ -197,7 +196,7 @@ public function testCancellationOfStoppableEvent(): void $this->dispatcher->dispatch($event, $cancellation)->await(); - $this->assertCount(0, $this->logger->records, 'No errors are logged'); + $this->assertCount(0, $this->errors, 'No errors are caught'); } public function testCancellationOfNonStoppableEvent(): void @@ -218,25 +217,6 @@ public function testCancellationOfNonStoppableEvent(): void $this->dispatcher->dispatch($event, $cancellation)->await(); - $this->assertCount(0, $this->logger->records, 'No errors are logged'); - } - - public function testThrowsErrors(): void - { - $this->dispatcher = new AsyncEventDispatcher( - $this->listenerProvider, - $this->logger, - AsyncEventDispatcher::THROW_ON_ERROR - ); - - $event = new class () {}; - $this->listenerProvider->addListener(get_class($event), function ($event) { - throw new Exception('This exception will bubble up because we set the THROW_ON_ERROR option'); - }); - - $this->expectException(Exception::class); - $this->expectExceptionMessage('This exception will bubble up because we set the THROW_ON_ERROR option'); - - $this->dispatcher->dispatch($event)->await(); + $this->assertCount(0, $this->errors, 'No errors are caught'); } }