Skip to content

Commit 07803f4

Browse files
committed
Add Async Event Dispatcher
1 parent 1990de9 commit 07803f4

File tree

9 files changed

+262
-3
lines changed

9 files changed

+262
-3
lines changed

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
],
1515
"require": {
1616
"php": "^7.4|^8.0",
17-
"psr/event-dispatcher": "^1.0"
17+
"psr/event-dispatcher": "^1.0",
18+
"react/event-loop": "^1.2"
1819
},
1920
"require-dev": {
2021
"infection/infection": "^0.21.0",

src/AsyncEventDispatcher.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Antidot\Event;
6+
7+
use Psr\EventDispatcher\EventDispatcherInterface;
8+
use Psr\EventDispatcher\ListenerProviderInterface;
9+
use Psr\EventDispatcher\StoppableEventInterface;
10+
use React\EventLoop\Loop;
11+
use React\EventLoop\LoopInterface;
12+
13+
class AsyncEventDispatcher implements EventDispatcherInterface
14+
{
15+
private ListenerProviderInterface $listenerProvider;
16+
private LoopInterface $loop;
17+
18+
public function __construct(ListenerProviderInterface $listenerProvider, LoopInterface $loop)
19+
{
20+
$this->listenerProvider = $listenerProvider;
21+
$this->loop = $loop;
22+
}
23+
24+
/**
25+
* @param StoppableEventInterface|object $event
26+
* @return object
27+
*/
28+
public function dispatch(object $event): object
29+
{
30+
$this->loop->futureTick(function () use ($event) {
31+
/** @var callable[] $listeners */
32+
$listeners = $this->listenerProvider->getListenersForEvent($event);
33+
foreach ($listeners as $listener) {
34+
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
35+
return;
36+
}
37+
$listener($event);
38+
}
39+
});
40+
41+
42+
return $event;
43+
}
44+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Antidot\Event\Container;
6+
7+
use Antidot\Event\AsyncEventDispatcher;
8+
use Antidot\Event\EventDispatcher;
9+
use Antidot\Event\ListenerProvider;
10+
use Psr\Container\ContainerInterface;
11+
use Psr\EventDispatcher\EventDispatcherInterface;
12+
use Psr\EventDispatcher\ListenerProviderInterface;
13+
use React\EventLoop\Loop;
14+
use RuntimeException;
15+
use Throwable;
16+
17+
class AsyncEventDispatcherFactory
18+
{
19+
public function __invoke(ContainerInterface $container): EventDispatcherInterface
20+
{
21+
/** @var ListenerProviderInterface $listenerProvider */
22+
$listenerProvider = $container->get(ListenerProviderInterface::class);
23+
24+
return new AsyncEventDispatcher($listenerProvider, Loop::get());
25+
}
26+
}

src/Container/Config/ConfigProvider.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
namespace Antidot\Event\Container\Config;
66

77
use Antidot\Event\Container\EventDispatcherFactory;
8+
use Antidot\Event\Container\ListenerProviderFactory;
89
use Psr\EventDispatcher\EventDispatcherInterface;
10+
use Psr\EventDispatcher\ListenerProviderInterface;
911

1012
class ConfigProvider
1113
{
@@ -17,6 +19,7 @@ public function __invoke(): array
1719
return [
1820
'factories' => [
1921
EventDispatcherInterface::class => EventDispatcherFactory::class,
22+
ListenerProviderInterface::class => ListenerProviderFactory::class,
2023
],
2124
'app-events' => [
2225
// 'event-listeners' => [
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Antidot\Event\Container;
6+
7+
use Antidot\Event\ListenerProvider;
8+
use Psr\Container\ContainerInterface;
9+
use Psr\EventDispatcher\ListenerProviderInterface;
10+
11+
final class ListenerProviderFactory
12+
{
13+
public function __invoke(ContainerInterface $container): ListenerProviderInterface
14+
{
15+
/** @var array<string, mixed> $globalConfig */
16+
$globalConfig = $container->get('config');
17+
/** @var array<string, array> $config */
18+
$config = $globalConfig['app-events'];
19+
$listenerProvider = new ListenerProvider();
20+
/**
21+
* @var string $eventClass
22+
* @var ?array<string> $listeners
23+
*/
24+
foreach ($config['event-listeners'] ?? [] as $eventClass => $listeners) {
25+
foreach ($listeners ?? [] as $listenerId) {
26+
$listenerProvider->addListener(
27+
$eventClass,
28+
static function () use ($container, $listenerId): callable {
29+
/** @var callable $listener */
30+
$listener = $container->get($listenerId);
31+
32+
return $listener;
33+
}
34+
);
35+
}
36+
}
37+
38+
return $listenerProvider;
39+
}
40+
}

src/ListenerCollection.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use IteratorAggregate;
88

9+
use Traversable;
910
use function array_key_exists;
1011

1112
/**
@@ -47,9 +48,9 @@ public function has(string $eventClass): bool
4748
}
4849

4950
/**
50-
* @return \Generator<mixed>|\Traversable<mixed>
51+
* @return \Generator<mixed>|Traversable<mixed>
5152
*/
52-
public function getIterator()
53+
public function getIterator(): Traversable
5354
{
5455
yield from $this->listeners;
5556
}

test/AsyncEventDispatcherTest.php

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
namespace Antidot\Test\Event;
4+
5+
use Antidot\Event\AsyncEventDispatcher;
6+
use Antidot\Event\EventDispatcher;
7+
use Antidot\Event\ListenerInterface;
8+
use PHPUnit\Framework\TestCase;
9+
use Psr\EventDispatcher\ListenerProviderInterface;
10+
use Psr\EventDispatcher\StoppableEventInterface;
11+
use React\EventLoop\Loop;
12+
use StdClass;
13+
14+
class AsyncEventDispatcherTest extends TestCase
15+
{
16+
public function testItShouldDispatchEvents(): void
17+
{
18+
$event = $this->createMock(StoppableEventInterface::class);
19+
$listenerProvider = $this->createMock(ListenerProviderInterface::class);
20+
21+
$listenerProvider
22+
->expects($this->once())
23+
->method('getListenersForEvent')
24+
->with($event)
25+
->willReturn([
26+
$this->makeListener(1, $event),
27+
$this->makeListener(1, $event),
28+
]);
29+
30+
$event
31+
->expects($this->exactly(2))
32+
->method('isPropagationStopped');
33+
34+
$eventDispatcher = new AsyncEventDispatcher($listenerProvider, Loop::get());
35+
$eventDispatcher->dispatch($event);
36+
37+
Loop::run();
38+
}
39+
40+
public function testItShouldDispatchEventOfAnyTypeOfObject(): void
41+
{
42+
$event = $this->createMock(StdClass::class);
43+
$listenerProvider = $this->createMock(ListenerProviderInterface::class);
44+
45+
$listenerProvider
46+
->expects($this->once())
47+
->method('getListenersForEvent')
48+
->with($event)
49+
->willReturn([
50+
$this->makeListener(1, $event),
51+
$this->makeListener(1, $event),
52+
]);
53+
54+
$eventDispatcher = new AsyncEventDispatcher($listenerProvider, Loop::get());
55+
$eventDispatcher->dispatch($event);
56+
57+
Loop::run();
58+
}
59+
60+
public function testItShouldNotHandleAnyEventWhenPropagationIsStopped(): void
61+
{
62+
$event = $this->createMock(StoppableEventInterface::class);
63+
$listenerProvider = $this->createMock(ListenerProviderInterface::class);
64+
65+
$listenerProvider
66+
->expects($this->once())
67+
->method('getListenersForEvent')
68+
->with($event)
69+
->willReturn([
70+
$this->makeListener(1, $event),
71+
$this->makeListener(0, $event),
72+
]);
73+
74+
$event
75+
->expects($this->exactly(2))
76+
->method('isPropagationStopped')
77+
->willReturnOnConsecutiveCalls(
78+
false,
79+
true
80+
);
81+
82+
$eventDispatcher = new AsyncEventDispatcher($listenerProvider, Loop::get());
83+
$eventDispatcher->dispatch($event);
84+
85+
Loop::run();
86+
}
87+
88+
private function makeListener(int $callTimes, $event): ListenerInterface
89+
{
90+
$listener = $this->createMock(ListenerInterface::class);
91+
$listener
92+
->expects($this->exactly($callTimes))
93+
->method('__invoke')
94+
->with($event);
95+
96+
return $listener;
97+
}
98+
}

test/Container/Config/ConfigProviderTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
use Antidot\Event\Container\Config\ConfigProvider;
66
use Antidot\Event\Container\EventDispatcherFactory;
7+
use Antidot\Event\Container\ListenerProviderFactory;
78
use PHPUnit\Framework\TestCase;
89
use Psr\EventDispatcher\EventDispatcherInterface;
10+
use Psr\EventDispatcher\ListenerProviderInterface;
911

1012
class ConfigProviderTest extends TestCase
1113
{
@@ -15,6 +17,7 @@ public function testItShouldREturnDefaultConfig(): void
1517
$this->assertSame([
1618
'factories' => [
1719
EventDispatcherInterface::class => EventDispatcherFactory::class,
20+
ListenerProviderInterface::class => ListenerProviderFactory::class,
1821
],
1922
'app-events' => []
2023
], $configProvider());
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
namespace Antidot\Test\Event\Container;
4+
5+
use Antidot\Event\Container\Config\ConfigProvider;
6+
use Antidot\Event\Container\ListenerProviderFactory;
7+
use Antidot\Event\ListenerInterface;
8+
use AntidotTest\Event\Container\TestEvent;
9+
use PHPUnit\Framework\TestCase;
10+
use Psr\Container\ContainerInterface;
11+
12+
class ListenerProviderFactoryTest extends TestCase
13+
{
14+
public function testItShouldVConfigureListenerProviderFactory(): void
15+
{
16+
$config = new ConfigProvider();
17+
$config = array_merge($config->__invoke(), [
18+
'app-events' => [
19+
'event-listeners' => [
20+
TestEvent::class => [
21+
'Listener1',
22+
'Listener2',
23+
]
24+
]
25+
]
26+
]);
27+
$container = $this->createMock(ContainerInterface::class);
28+
$container
29+
->expects($this->exactly(3))
30+
->method('get')
31+
->withConsecutive(['config'], ['Listener1'], ['Listener2'])
32+
->willReturnOnConsecutiveCalls(
33+
$config,
34+
$this->createMock(ListenerInterface::class),
35+
$this->createMock(ListenerInterface::class)
36+
);
37+
38+
39+
$listenerProviderFactory = new ListenerProviderFactory();
40+
$listenerProvider = $listenerProviderFactory->__invoke($container);
41+
self::assertCount(2, $listenerProvider->getListenersForEvent(new TestEvent()));
42+
}
43+
}

0 commit comments

Comments
 (0)