Skip to content

Commit

Permalink
Use an error handlecr instead of logger
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxime Rainville committed Dec 12, 2024
1 parent f37fee7 commit b22bff1
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 75 deletions.
6 changes: 2 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
12 changes: 3 additions & 9 deletions examples/basic-usage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
);
42 changes: 13 additions & 29 deletions src/AsyncEventDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;
use Psr\EventDispatcher\StoppableEventInterface;
use Psr\Log\LoggerInterface;
use Throwable;

/**
Expand All @@ -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;
}
}

/**
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;
}
}
}
46 changes: 13 additions & 33 deletions tests/AsyncEventDispatcherTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -31,18 +31,21 @@ class AsyncEventDispatcherTest extends TestCase
{
private ListenerProvider $listenerProvider;
private AsyncEventDispatcher $dispatcher;
private TestLogger $logger;

/** @var array<Throwable> */
private array $errors = [];

/**
* Sets up the test environment before each test.
*/
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;
}
);
}

Expand Down Expand Up @@ -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');
}

/**
Expand All @@ -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');
}

/**
Expand All @@ -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');
}

/**
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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');
}
}

0 comments on commit b22bff1

Please sign in to comment.