Skip to content

Commit 8b9c57f

Browse files
Maxime Rainvillemaxime-rainville
Maxime Rainville
authored andcommitted
Implement error logging and better testing set up
1 parent 38c064f commit 8b9c57f

File tree

7 files changed

+193
-11
lines changed

7 files changed

+193
-11
lines changed

README.md

+35-5
Original file line numberDiff line numberDiff line change
@@ -236,22 +236,45 @@ return `null` if the DataObject has been deleted.
236236
`DataObjectEvent::getObject(true) will attempt to retrieve the exact version of the DataObject that fired the event,
237237
assuming it was versioned.
238238
239-
## Testing Your Events
239+
## Handling Errors in Event Listeners
240240
241-
### Writing Event Tests
241+
Exceptions thrown by an event listener will not stop the execution of follow events. By default, those exceptions will be sent to `EventService::handleError()` who will logged them to the default Silverstripe CMS logger.
242+
243+
You can provide your own error handler with Injector.
244+
245+
```yml
246+
---
247+
Name: custom-event-service
248+
After:
249+
- '#event-service'
250+
---
251+
SilverStripe\Core\Injector\Injector:
252+
ArchiPro\EventDispatcher\AsyncEventDispatcher:
253+
errorhandler: [MyCustomEventHandler, handleError]
254+
```
255+
256+
## Testing your Events
242257

243258
When testing your event listeners, you'll need to:
244259
1. Dispatch your events
245260
2. Run the event loop
246261
3. Assert the expected outcomes
247262

263+
You can also use the `TestEventService` to test your events. The `TestEventService` will replace the default `EventService` and log any exceptions thrown by listeners.
264+
265+
You need to require the `colinodell/psr-testlogger` package in your dev dependencies to use the `TestEventService`.
266+
267+
```
268+
composer require colinodell/psr-testlogger --dev
269+
```
270+
248271
Here's an example test:
249272
250273
```php
251274
use Revolt\EventLoop;
252275
use SilverStripe\Dev\SapphireTest;
253276
use SilverStripe\Core\Injector\Injector;
254-
use ArchiPro\Silverstripe\EventDispatcher\Service\EventService;
277+
use ArchiPro\Silverstripe\EventDispatcher\Service\TestEventService;
255278
256279
class MyEventTest extends SapphireTest
257280
{
@@ -260,8 +283,9 @@ class MyEventTest extends SapphireTest
260283
// Create your test event
261284
$event = new MyCustomEvent('test message');
262285
263-
// Get the event service
264-
$service = Injector::inst()->get(EventService::class);
286+
// Get the Test Event Service ... this will replace the default EventService with a TestEventService
287+
// with an implementation that will log errors to help with debugging.
288+
$service = TestEventService::bootstrap();
265289
266290
// Add your test listener ... or if you have already
267291
$wasCalled = false;
@@ -281,6 +305,12 @@ class MyEventTest extends SapphireTest
281305
MyCustomEventListener::wasCalled(),
282306
'Assert some side effect of the event being handled'
283307
);
308+
309+
$this->assertCount(
310+
0,
311+
$service->getTestLogger()->records,
312+
'No errors were logged'
313+
);
284314
}
285315
}
286316
```

_config/events.yml

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
---
2-
Name: events
2+
Name: event-service
33
After:
44
- '#coreservices'
55
---
66
SilverStripe\Core\Injector\Injector:
77
# Define the listener provider
88
ArchiPro\EventDispatcher\ListenerProvider:
99
class: ArchiPro\EventDispatcher\ListenerProvider
10-
10+
1111
# Default event dispatcher
1212
ArchiPro\EventDispatcher\AsyncEventDispatcher:
1313
class: ArchiPro\EventDispatcher\AsyncEventDispatcher
1414
constructor:
1515
listenerProvider: '%$ArchiPro\EventDispatcher\ListenerProvider'
16+
errorhandler: [ArchiPro\Silverstripe\EventDispatcher\Service\EventService, handleError]
1617
Psr\EventDispatcher\EventDispatcherInterface:
1718
alias: '%$ArchiPro\EventDispatcher\AsyncEventDispatcher'
1819

1920
# Bootstrap the event service
2021
ArchiPro\Silverstripe\EventDispatcher\Service\EventService:
21-
constructor:
22+
constructor:
2223
dispatcher: '%$ArchiPro\EventDispatcher\AsyncEventDispatcher'
23-
listenerProvider: '%$ArchiPro\EventDispatcher\ListenerProvider'
24+
listenerProvider: '%$ArchiPro\EventDispatcher\ListenerProvider'

composer.json

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
"silverstripe/versioned": "^1.13 || ^2.0",
1010
"psr/event-dispatcher": "^1.0",
1111
"psr/event-dispatcher-implementation": "^1.0",
12-
"archipro/revolt-event-dispatcher": "^0.0.0"
12+
"archipro/revolt-event-dispatcher": "^0.1.0",
13+
"psr/log": "^1 || ^2 || ^3"
1314
},
1415
"require-dev": {
1516
"phpunit/phpunit": "^9.5",
1617
"squizlabs/php_codesniffer": "^3.0",
1718
"friendsofphp/php-cs-fixer": "^3.0",
18-
"phpstan/phpstan": "^1.10"
19+
"phpstan/phpstan": "^1.10",
20+
"colinodell/psr-testlogger": "^1.0"
1921
},
2022
"autoload": {
2123
"psr-4": {
@@ -43,5 +45,8 @@
4345
"composer/installers": true,
4446
"silverstripe/vendor-plugin": true
4547
}
48+
},
49+
"suggest": {
50+
"colinodell/psr-testlogger": "To use the TestEventService, you must require the 'colinodell/psr-testlogger' package in your dev dependencies."
4651
}
4752
}

src/Service/EventService.php

+17
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
use ArchiPro\EventDispatcher\AsyncEventDispatcher;
77
use ArchiPro\EventDispatcher\ListenerProvider;
88
use ArchiPro\Silverstripe\EventDispatcher\Contract\ListenerLoaderInterface;
9+
use Psr\Log\LoggerInterface;
910
use SilverStripe\Core\Config\Configurable;
1011
use SilverStripe\Core\Injector\Injectable;
1112
use SilverStripe\Core\Injector\Injector;
13+
use Throwable;
1214

1315
/**
1416
* Core service class for handling event dispatching in Silverstripe.
@@ -148,4 +150,19 @@ public function disableDispatch(): void
148150
{
149151
$this->suppressDispatch = true;
150152
}
153+
154+
/**
155+
* Handle an error that occurred during event dispatching by logging them
156+
* with the default Silverstripe CMS error handler logger.
157+
*
158+
* @internal This method is wired to the AsyncEventDispatcher with the Injector
159+
*
160+
* @see _config/events.yml
161+
*/
162+
public static function handleError(Throwable $error): void
163+
{
164+
Injector::inst()
165+
->get(LoggerInterface::class)
166+
->error($error->getMessage(), ['exception' => $error]);
167+
}
151168
}

src/Service/TestEventService.php

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace ArchiPro\Silverstripe\EventDispatcher\Service;
4+
5+
use ArchiPro\EventDispatcher\AsyncEventDispatcher;
6+
use ArchiPro\EventDispatcher\ListenerProvider;
7+
use Closure;
8+
use ColinODell\PsrTestLogger\TestLogger;
9+
use SilverStripe\Core\Injector\Injector;
10+
use Throwable;
11+
12+
/**
13+
* Extension of the AsyncEventDispatcher for testing purposes.
14+
*
15+
* This service will throw exceptions when errors occur to make it easier to debug issues.
16+
*/
17+
class TestEventService extends EventService
18+
{
19+
private TestLogger $logger;
20+
21+
public function __construct()
22+
{
23+
if (!class_exists(TestLogger::class)) {
24+
throw new \Exception(
25+
'To use the TestEventService, you must require the "colinodell/psr-testlogger" ' .
26+
'package in your dev dependencies.'
27+
);
28+
}
29+
30+
$this->logger = new TestLogger();
31+
32+
$listenerProvider = Injector::inst()->get(ListenerProvider::class);
33+
$dispatcher = new AsyncEventDispatcher(
34+
$listenerProvider,
35+
Closure::fromCallable([$this, 'recordError'])
36+
);
37+
parent::__construct($dispatcher, $listenerProvider);
38+
}
39+
40+
/**
41+
* Bootstrap the TestEventService. Will replace the default EventService with a TestEventService.
42+
*/
43+
public static function bootstrap(): self
44+
{
45+
$service = new self();
46+
Injector::inst()->registerService($service, AsyncEventDispatcher::class);
47+
return $service;
48+
}
49+
50+
/**
51+
* Catch errors and store them for later inspection.
52+
*/
53+
private function recordError(Throwable $message): void
54+
{
55+
$this->logger->error($message->getMessage(), ['exception' => $message]);
56+
}
57+
58+
/**
59+
* Test logger where exception thrown by listeners are logged.
60+
*/
61+
public function getTestLogger(): TestLogger
62+
{
63+
return $this->logger;
64+
}
65+
}

tests/php/Service/EventServiceTest.php

+18
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
use ArchiPro\Silverstripe\EventDispatcher\Service\EventService;
66
use ArchiPro\Silverstripe\EventDispatcher\Tests\TestListenerLoader;
7+
use ColinODell\PsrTestLogger\TestLogger;
8+
use Psr\Log\LoggerInterface;
79
use Revolt\EventLoop;
810
use SilverStripe\Core\Injector\Injector;
911
use SilverStripe\Dev\SapphireTest;
@@ -114,4 +116,20 @@ public function testEventDispatchWithDisabledDispatch(): void
114116
// Assert listener was called
115117
$this->assertTrue($result->handled, 'Event listener should have been called when dispatch is re-enabled');
116118
}
119+
120+
public function testHandleError(): void
121+
{
122+
// Arrange
123+
$testLogger = new TestLogger();
124+
Injector::inst()->registerService($testLogger, LoggerInterface::class);
125+
$ex = new \Exception('Test error');
126+
127+
// Act
128+
EventService::handleError($ex);
129+
130+
// Assert
131+
$this->assertCount(1, $testLogger->records, 'Error should be to default error logger');
132+
$this->assertEquals('Test error', $testLogger->records[0]['message'], 'Error message should be "Test error"');
133+
$this->assertEquals($ex, $testLogger->records[0]['context']['exception'], 'Error should be the same exception');
134+
}
117135
}
+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
namespace ArchiPro\Silverstripe\EventDispatcher\Tests\Service;
4+
5+
use ArchiPro\Silverstripe\EventDispatcher\Service\TestEventService;
6+
use Exception;
7+
use Revolt\EventLoop;
8+
use SilverStripe\Dev\SapphireTest;
9+
10+
class TestEventServiceTest extends SapphireTest
11+
{
12+
private TestEventService $service;
13+
14+
protected function setUp(): void
15+
{
16+
parent::setUp();
17+
$this->service = TestEventService::bootstrap();
18+
}
19+
20+
public function testGetTestLogger(): void
21+
{
22+
// Create test event
23+
$event = new class () {};
24+
25+
// Add test listener
26+
$this->service->addListener(get_class($event), function ($event) {
27+
throw new Exception('Test exception');
28+
});
29+
30+
$this->assertFalse(
31+
$this->service->getTestLogger()->hasErrorRecords(),
32+
'No exceptions have been thrown yet'
33+
);
34+
35+
// Dispatch event
36+
$this->service->dispatch($event);
37+
38+
EventLoop::run();
39+
40+
$this->assertCount(
41+
1,
42+
$this->service->getTestLogger()->records,
43+
'Running the event loop will cause an error to be logged'
44+
);
45+
}
46+
}

0 commit comments

Comments
 (0)