Skip to content

Commit

Permalink
[EventDispatcher] Adding a few new things and working on docs (#202)
Browse files Browse the repository at this point in the history
## Description



## Checklist
- [x] Updated CHANGELOG files
- [x] Updated Documentation
- [x] Unit Tests Created
- [x] php-cs-fixer
  • Loading branch information
JoshuaEstes authored Dec 27, 2023
1 parent b24d817 commit 9cd723e
Show file tree
Hide file tree
Showing 11 changed files with 259 additions and 14 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ To get the diff between two versions, go to https://github.com/SonsOfPHP/sonsofp
* [PR #182](https://github.com/SonsOfPHP/sonsofphp/pull/182) [Container] New Component (PSR-11)
* [PR #187](https://github.com/SonsOfPHP/sonsofphp/pull/187) [HttpHandler] New Component (PSR-15) and Contract
* [PR #190](https://github.com/SonsOfPHP/sonsofphp/pull/190) [Mailer] New Component and Contract
* [PR #202](https://github.com/SonsOfPHP/sonsofphp/pull/202) [EventDispatcher] Added Stoppable Event Code

## [0.3.8]

Expand Down
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ test-cookie: phpunit
test-cqrs: PHPUNIT_TESTSUITE=cqrs
test-cqrs: phpunit

test-event-dispatcher: PHPUNIT_TESTSUITE=event-dispatcher
test-event-dispatcher: phpunit

test-http-factory: PHPUNIT_TESTSUITE=http-factory
test-http-factory: phpunit

Expand Down Expand Up @@ -125,8 +128,8 @@ coverage-cookie: coverage
coverage-cqrs: PHPUNIT_TESTSUITE=cqrs
coverage-cqrs: coverage

coverage-event-dispatcher:
XDEBUG_MODE=coverage $(PHP) -dxdebug.mode=coverage $(PHPUNIT) --testsuite event-dispatcher --coverage-html $(COVERAGE_DIR)
coverage-event-dispatcher: PHPUNIT_TESTSUITE=event-dispatcher
coverage-event-dispatcher: coverage

coverage-event-sourcing:
XDEBUG_MODE=coverage $(PHP) -dxdebug.mode=coverage $(PHPUNIT) --testsuite event-sourcing --coverage-html $(COVERAGE_DIR)
Expand Down
100 changes: 100 additions & 0 deletions docs/components/event-dispatcher/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,109 @@ Must implement `EventSubscriberInterface`.
### Listener Priorities

```php
<?php

$dispatcher->addListener('event.name', function () {}, $priority);
```

The priority will default to `0`. Lower numbers are higher priority. Higher
numbers will be handled later. For example, a listener with a priority of `-1`
will be handled before a listener of priority `1`.


### Stoppable Events

If your code extends the `AbstractStoppableEvent` and within your listener or
subscriber code, you execute `$this->stopPropagation();` and it will return the
event and no more listeners or subscribers will handle that event.

```php
<?php

use SonsOfPHP\Component\EventDispatcher\AbstractStoppableEvent;

class OrderCreated extends AbstractStoppableEvent
{
// ...
}

class OrderListener
{
public function __invoke(OrderCreated $event): void
{
// ...
$event->stopPropagation();
// ...
}
}
```

You can also create Stoppable Events by using the `StoppableEventInterface` and
`StoppableEventTrait`.

```php
<?php

use SonsOfPHP\Component\EventDispatcher\StoppableEventTrait;
use Psr\EventDispatcher\StoppableEventInterface;

class OrderCreated implements StoppableEventInterface
{
use StoppableEventTrait;

// ...
}
```

## Creating an Event Listener

```php
<?php

use Psr\EventDispatcher\EventDispatcherInterface;

class OrderListener
{
public function __invoke(OrderCreated $event, string $eventName, EventDispatcherInterface $dispatcher): void
{
// ...
}
}
```

The dispatcher will always invoke the Listener with those three arguments in
that order. If you do not need to know the event name or if you do not need the
event dispatcher, you can ignore those two arguments.

## Creating an Event Subscriber

Subscribers allow you to "subscribe" to multiple events.

```php
<?php

use OrderCreated;
use OrderUpdated;
use OrderDeleted;
use SonsOfPHP\Component\EventDispatcher\EventSubscriberInterface;

class OrderSubscriber implements EventSubscriberInterface
{
// ...

public static function getSubscribedEvents()
{
// Can return like this:
yield OrderCreated::class => 'onCreated';
yield OrderUpdated::class => ['onUpdated', 100];
yield OrderDeleted::class => [['onDeleted', 100], ['doFirstOnDeleted', -100]];

// OR like this
return [
OrderCreated::class => 'onCreated',
OrderUpdated::class => ['onUpdated', 100],
OrderDeleted::class => [['onDeleted', 100], ['doFirstOnDeleted', -100]],
];
}
}
```
15 changes: 15 additions & 0 deletions src/SonsOfPHP/Component/EventDispatcher/AbstractStoppableEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

namespace SonsOfPHP\Component\EventDispatcher;

use Psr\EventDispatcher\StoppableEventInterface;

/**
* @author Joshua Estes <joshua@sonsofphp.com>
*/
abstract class AbstractStoppableEvent implements StoppableEventInterface
{
use StoppableEventTrait;
}
15 changes: 13 additions & 2 deletions src/SonsOfPHP/Component/EventDispatcher/EventDispatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ public function __construct(
) {}

/**
* @return object
* {@inheritdoc}
*
* @param string|null $eventName
* Is the event name is null, is will use the event's classname as the Event Name
*/
public function dispatch(object $event, string $eventName = null): object
{
Expand All @@ -35,11 +38,19 @@ public function dispatch(object $event, string $eventName = null): object
return $event;
}

public function addListener(string $eventName, callable|array $listener, int $priority = 0): void
/**
*/
public function addListener(string|object $eventName, callable|array $listener, int $priority = 0): void
{
if (is_object($eventName)) {
$eventName = $eventName::class;
}

$this->provider->add($eventName, $listener, $priority);
}

/**
*/
public function addSubscriber(EventSubscriberInterface $subscriber): void
{
$this->provider->addSubscriber($subscriber);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
interface EventSubscriberInterface
{
/**
* Returns an iterable with one or more of the following
* - ['eventName' => 'methodName'], ...
* - ['eventName' => ['methodName', $priority], ...], ...
* - ['eventName' => [['methodName', $priority], ['methodName']], ...], ...
*/
public static function getSubscribedEvents();
}
10 changes: 0 additions & 10 deletions src/SonsOfPHP/Component/EventDispatcher/ListenerInterface.php

This file was deleted.

11 changes: 11 additions & 0 deletions src/SonsOfPHP/Component/EventDispatcher/ListenerProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,24 @@ class ListenerProvider implements ListenerProviderInterface
private array $listeners = [];
private array $sorted = [];

/**
* {@inheritdoc}
*/
public function getListenersForEvent(object $event): iterable
{
return $this->getListenersForEventName($event::class);
}

/**
*/
public function add(string $eventName, callable|array $listener, int $priority = 0): void
{
$this->listeners[$eventName][$priority][] = $listener;
unset($this->sorted[$eventName]);
}

/**
*/
public function addSubscriber(EventSubscriberInterface $subscriber): void
{
foreach ($subscriber::getSubscribedEvents() as $eventName => $params) {
Expand All @@ -48,6 +55,8 @@ public function addSubscriber(EventSubscriberInterface $subscriber): void
}
}

/**
*/
public function getListenersForEventName(string $eventName): iterable
{
if (!\array_key_exists($eventName, $this->listeners)) {
Expand All @@ -61,6 +70,8 @@ public function getListenersForEventName(string $eventName): iterable
return $this->sorted[$eventName];
}

/**
*/
private function sortListeners(string $eventName): void
{
ksort($this->listeners[$eventName]);
Expand Down
26 changes: 26 additions & 0 deletions src/SonsOfPHP/Component/EventDispatcher/StoppableEventTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace SonsOfPHP\Component\EventDispatcher;

/**
* @author Joshua Estes <joshua@sonsofphp.com>
*/
trait StoppableEventTrait
{
private bool $isStopped = false;

public function isPropagationStopped(): bool
{
return $this->isStopped;
}

/**
* Makes `isPropagationStopped` return true
*/
public function stopPropagation(): void
{
$this->isStopped = true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

namespace SonsOfPHP\Component\EventDispatcher\Tests;

use PHPUnit\Framework\TestCase;
use Psr\EventDispatcher\StoppableEventInterface;
use SonsOfPHP\Component\EventDispatcher\AbstractStoppableEvent;

/**
* @coversDefaultClass \SonsOfPHP\Component\EventDispatcher\AbstractStoppableEvent
*
* @uses \SonsOfPHP\Component\EventDispatcher\AbstractStoppableEvent
*/
final class AbstractStoppableEventTest extends TestCase
{
/**
* @coversNothing
*/
public function testItHasTheCorrectInterface(): void
{
$event = new class () extends AbstractStoppableEvent {};

$this->assertInstanceOf(StoppableEventInterface::class, $event);
}

/**
* @covers ::isPropagationStopped
* @covers ::stopPropagation
*/
public function testItCanStopPropagation(): void
{
$event = new class () extends AbstractStoppableEvent {};
$this->assertFalse($event->isPropagationStopped());

$event->stopPropagation();
$this->assertTrue($event->isPropagationStopped());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use PHPUnit\Framework\TestCase;
use Psr\EventDispatcher\EventDispatcherInterface;
use SonsOfPHP\Component\EventDispatcher\AbstractStoppableEvent;
use SonsOfPHP\Component\EventDispatcher\EventDispatcher;
use SonsOfPHP\Component\EventDispatcher\EventSubscriberInterface;
use SonsOfPHP\Component\EventDispatcher\ListenerProvider;
Expand All @@ -15,6 +16,7 @@
*
* @uses \SonsOfPHP\Component\EventDispatcher\EventDispatcher
* @uses \SonsOfPHP\Component\EventDispatcher\ListenerProvider
* @uses \SonsOfPHP\Component\EventDispatcher\StoppableEventTrait
*/
final class EventDispatcherTest extends TestCase
{
Expand All @@ -28,6 +30,35 @@ public function testItHasTheCorrectInterface(): void
$this->assertInstanceOf(EventDispatcherInterface::class, $dispatcher); // @phpstan-ignore-line
}

/**
* @covers ::dispatch
*/
public function testDispatch(): void
{
$dispatcher = new EventDispatcher();
$dispatcher->addListener('stdClass', function ($event): void {});

$event = new \stdClass();
$this->assertSame($event, $dispatcher->dispatch($event));
}

/**
* @covers ::dispatch
*/
public function testDispatchWithStoppedEvent(): void
{
$event = new class () extends AbstractStoppableEvent {};

$dispatcher = new EventDispatcher();
$dispatcher->addListener($event, function ($event): void {
throw new \RuntimeException('This should never run');
});

$event->stopPropagation();

$this->assertSame($event, $dispatcher->dispatch($event));
}

/**
* @covers ::dispatch
*/
Expand All @@ -52,6 +83,19 @@ public function testItCanAddEventListener(): void
$dispatcher->addListener('stdClass', function (): void {});
}

/**
* @covers ::addListener
*/
public function testAddListenerWithObject(): void
{
$provider = $this->createMock(ListenerProvider::class);
$provider->expects($this->once())->method('add')->with($this->identicalTo('stdClass'));

$dispatcher = new EventDispatcher($provider);

$dispatcher->addListener(new \stdClass(), function (): void {});
}

/**
* @covers ::addSubscriber
*/
Expand Down

0 comments on commit 9cd723e

Please sign in to comment.