Skip to content

Commit

Permalink
Implemented Event Dispatcher
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianmiu committed Oct 9, 2023
1 parent 0d48277 commit e57e94b
Show file tree
Hide file tree
Showing 14 changed files with 349 additions and 17 deletions.
72 changes: 72 additions & 0 deletions docs/2_3_event_dispatcher.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
title: PSR-14 Event dispatcher implementation
---

# PSR-14 Event dispatcher implementation

The `Sirius\StackRunner` library comes with an implementation of the [PSR-14 Event Dispatcher](https://www.php-fig.org/psr/psr-14/).

```php
use Sirius\StackRunner\Invoker;
use Sirius\StackRunner\Event\Dispatcher;
use Sirius\StackRunner\Event\ListenerProvider;

$invoker = new Invoker($psr11Container);
$listenerProvider = new ListenerProvider();
$dispatcher = new Dispatcher($listenerProvider, $invoker);

// event name, listener, priority
$listenerProvider->subscribeTo(Event::class, 'some_callable', 0);
$listenerProvider->subscribeOnceTo(Event::class, 'some_callable', 0);

// if you use the Sirius\StackRunner\Event\ListenerProvider
// the same results as above can also be achieved with
$dispatcher->subscribeTo(Event::class, 'some_callable', 0);
$dispatcher->subscribeOnceTo(Event::class, 'some_callable', 0);
```

### Named events

If you want to identify the events by something other than the class name you can make the event classes implement the `HasEventname` interface

```php
use Sirius\StackRunner\Event\HasEventName;

class EventWithName implements HasEventName {
public function getEventName() : string{
return 'event_name';
}
}
```

and then you can do something like

```php
$listenerProvider->subscribeTo('event_name', 'some_callable');
// later on
$dispatcher->dispatch(new EventWithName());
```

### Stoppable events

If you want some events to be able to stop the execution of the rest of the callables in the stack you can add the `Stoppable` trait to your event classes

```php
use Sirius\StackRunner\Event\Stoppable;

class StoppableEvent {
use Stoppable;
}
```

and then you can do something like

```php
$listenerProvider->subscribeTo(StoppableEvent::class, function(object $event) {
$event->stopPropagation();
});
// the subsequent callables won't be executed
$listenerProvider->subscribeTo(StoppableEvent::class, 'some_callable');
```

[Next: Actions a la Wordpress](2_4_wordpress_actions.md)
2 changes: 1 addition & 1 deletion docs/2_3_middlewares.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ While this example is for HTTP middleware, it does not implement the [PSR-15 mid

The example above can easily handle both the single-pass and double-pass types of [HTTP middleware](https://www.php-fig.org/psr/psr-15/meta/)

[Next: Actions a la Wordpress](2_4_wordpress_actions.md)
[Next: Event dispatcher](2_3_event_dispatcher.md)
3 changes: 3 additions & 0 deletions docs/4_the_invoker.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,7 @@ This for when you want to use as an argument in a different position than the po

For an example, check the documentation for the ["with arguments" modifier](3_callable_modifiers.md)

## Extending the invoker


[Next: The simple stack runner](3_simple_runner.md)
3 changes: 3 additions & 0 deletions docs/couscous.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ menu:
middlewares:
text: "- Middlewares"
relativeUrl: 2_3_middlewares.html
event_dispatcher:
text: "- Event dispatcher"
relativeUrl: 2_3_event_dispatcher.html
actions:
text: "- Actions a la Wordpress"
relativeUrl: 2_4_wordpress_actions.html
Expand Down
33 changes: 17 additions & 16 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,28 +27,29 @@ In the case of events, an `event` object is passed through each callable in the

```php
use Sirius\StackRunner\Invoker;
use Sirius\StackRunner\PipelineRunner;
use Sirius\StackRunner\Processors\PipelineProcessor;
use Sirius\StackRunner\Stack;

$container = app(); // your application DI container
$invoker = new Invoker($container)
$runner = new PipelineRunner($invoker);

$stack = Stack::make([
'trim', // regular function
'Str::toUppercase', // static method
fn($value) => { // anonymous function
return $value . '!!!';
},
'Logger@info', // object retrieved from the container
])

$runner($stack, " hello world "); // returns `HELLO WORLD!!!`
```
$processor = new PipelineProcessor($invoker);

$stack = new Stack();
$stack->add('trim');
$stack->add('Str::toUppercase');
$stack->add(fn($value) => { // anonymous function
return $value . '!!!';
});
$stack->add('Logger@info');

The stack runners are very simple and it's easy to build your own
$processor->process($stack, " hello world "); // returns `HELLO WORLD!!!`
```

## Links
## Where to next?

- [documentation](https://sirius.ro/php/sirius/stack_runner/)
- [changelog](CHANGELOG.md)

## Todo
- [] Implement resumable pipelines
- [] Document other use cases
60 changes: 60 additions & 0 deletions src/Event/Dispatcher.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace Sirius\StackRunner\Event;

use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;
use Psr\EventDispatcher\StoppableEventInterface;
use Sirius\StackRunner\Invoker;
use Sirius\StackRunner\Stack;

class Dispatcher implements EventDispatcherInterface
{

public function __construct(public ListenerProviderInterface $registry, public Invoker $invoker)
{
}

public function dispatch(object $event): object
{
/** @var Stack $stack */
$stack = $this->registry->getListenersForEvent($event);

/** @var mixed $callable */
foreach ($stack as $callable) {
$this->invoker->invoke($callable, $event);
if ($event instanceof StoppableEventInterface &&
$event->isPropagationStopped()) {
break;
}
}

return $event;
}

public function subscribeTo(string $eventName, mixed $callable, int $priority = 0): void
{
if ( ! $this->registry instanceof ListenerSubscriber) {
throw new \LogicException(sprintf('Unable to subscribe listener because %s is not instace of %s',
get_class($this->registry),
ListenerSubscriber::class
));
}

$this->registry->subscribeTo($eventName, $callable, $priority);
}

public function subscribeOnceTo(string $eventName, mixed $callable, int $priority = 0): void
{
if ( ! $this->registry instanceof ListenerSubscriber) {
throw new \LogicException(sprintf('Unable to subscribe listener because %s is not instace of %s',
get_class($this->registry),
ListenerSubscriber::class
));
}

$this->registry->subscribeOnceTo($eventName, $callable, $priority);
}
}
10 changes: 10 additions & 0 deletions src/Event/HasEventName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Sirius\StackRunner\Event;

interface HasEventName
{
public function getEventName(): string;
}
50 changes: 50 additions & 0 deletions src/Event/ListenerProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

namespace Sirius\StackRunner\Event;

use Psr\EventDispatcher\ListenerProviderInterface;
use Sirius\StackRunner\Stack;
use function Sirius\StackRunner\once;

class ListenerProvider implements ListenerProviderInterface, ListenerSubscriber
{
/**
* @var array<string, Stack|iterable>
*/
protected array $registry = []; // @phpstan-ignore-line

/**
* @return iterable|Stack
*/
public function getListenersForEvent(object $event): iterable // @phpstan-ignore-line
{
$eventName = get_class($event);
if ($event instanceof HasEventName) {
$eventName = $event->getEventName();
}

return $this->registry[$eventName] ?? new Stack();
}

public function subscribeTo(string $eventName, mixed $callable, int $priority = 0): void
{
/** @var Stack $stack */
$stack = $this->registry[$eventName] ?? new Stack();

$stack->add($callable, $priority);

$this->registry[$eventName] = $stack;
}

public function subscribeOnceTo(string $eventName, mixed $callable, int $priority = 0): void
{
/** @var Stack $stack */
$stack = $this->registry[$eventName] ?? new Stack();

$stack->add(once($callable), $priority);

$this->registry[$eventName] = $stack;
}
}
12 changes: 12 additions & 0 deletions src/Event/ListenerSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Sirius\StackRunner\Event;

interface ListenerSubscriber
{
public function subscribeTo(string $eventName, mixed $callable, int $priority = 0): void;

public function subscribeOnceTo(string $eventName, mixed $callable, int $priority = 0): void;
}
20 changes: 20 additions & 0 deletions src/Event/Stoppable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Sirius\StackRunner\Event;

trait Stoppable
{
protected bool $propagationStopped = false;

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

public function stopPropagation(): void
{
$this->propagationStopped = true;
}
}
73 changes: 73 additions & 0 deletions tests/src/Event/DispatcherTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Sirius\StackRunner\Event;

use Sirius\StackRunner\TestCase;

require_once __DIR__ . '/EventWithName.php';
require_once __DIR__ . '/EventWithoutName.php';
require_once __DIR__ . '/StoppableEvent.php';

class DispatcherTest extends TestCase
{
public function test_subscribers_are_executed_in_order()
{
$dispatcher = new Dispatcher(new ListenerProvider(), $this->getInvoker());
$dispatcher->subscribeTo('event_with_name', function (object $event) {
static::$results[] = 'subscriber 1';
});
$dispatcher->subscribeTo('event_with_name', function (object $event) {
static::$results[] = 'subscriber 2';
});
$dispatcher->subscribeTo('event_with_name', function (object $event) {
static::$results[] = 'subscriber 3';
});

$dispatcher->dispatch(new EventWithName());

$this->assertSame([
'subscriber 1',
'subscriber 2',
'subscriber 3',
], static::$results);
}

public function test_stoppable_events()
{
$dispatcher = new Dispatcher(new ListenerProvider(), $this->getInvoker());
$dispatcher->subscribeTo(StoppableEvent::class, function (object $event) {
static::$results[] = 'subscriber 1';
});
$dispatcher->subscribeTo(StoppableEvent::class, function (object $event) {
static::$results[] = 'subscriber 2';
$event->stopPropagation();
});
$dispatcher->subscribeTo(StoppableEvent::class, function (object $event) {
static::$results[] = 'subscriber 3';
});

$dispatcher->dispatch(new StoppableEvent());

$this->assertSame([
'subscriber 1',
'subscriber 2',
], static::$results);
}

public function test_once_subscribers()
{
$dispatcher = new Dispatcher(new ListenerProvider(), $this->getInvoker());
$dispatcher->subscribeOnceTo(EventWithoutName::class, function (object $event) {
static::$results[] = 'once subscriber';
});

$dispatcher->dispatch(new EventWithoutName());
$dispatcher->dispatch(new EventWithoutName());
$dispatcher->dispatch(new EventWithoutName());
$dispatcher->dispatch(new EventWithoutName());

$this->assertSame([
'once subscriber',
], static::$results);
}
}
11 changes: 11 additions & 0 deletions tests/src/Event/EventWithName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Sirius\StackRunner\Event;

class EventWithName implements HasEventName {

public function getEventName(): string
{
return 'event_with_name';
}
}
7 changes: 7 additions & 0 deletions tests/src/Event/EventWithoutName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Sirius\StackRunner\Event;

class EventWithoutName
{
}
Loading

0 comments on commit e57e94b

Please sign in to comment.