diff --git a/config/app.php b/config/app.php index b64baec6..5a380499 100644 --- a/config/app.php +++ b/config/app.php @@ -5,12 +5,12 @@ return [ // Service providers 'providers' => [ - // Application bootstrap \Engelsystem\Logger\LoggerServiceProvider::class, \Engelsystem\Exceptions\ExceptionsServiceProvider::class, \Engelsystem\Config\ConfigServiceProvider::class, \Engelsystem\Helpers\ConfigureEnvironmentServiceProvider::class, + \Engelsystem\Events\EventsServiceProvider::class, // Request handling \Engelsystem\Http\UrlGeneratorServiceProvider::class, @@ -55,4 +55,15 @@ return [ // Handle request \Engelsystem\Middleware\RequestHandler::class, ], + + // Event handlers + 'event-handlers' => [ + // 'event' => [ + // a list of + // 'Class@method' or 'Class' (which uses @handle), + // ['Class', 'method'], + // callable like [$instance, 'method] or 'function' + // or $function + // ] + ], ]; diff --git a/src/Config/ConfigServiceProvider.php b/src/Config/ConfigServiceProvider.php index aff7918d..02f1274b 100644 --- a/src/Config/ConfigServiceProvider.php +++ b/src/Config/ConfigServiceProvider.php @@ -11,7 +11,7 @@ use Illuminate\Database\QueryException; class ConfigServiceProvider extends ServiceProvider { /** @var array */ - protected $configFiles = ['config.default.php', 'config.php']; + protected $configFiles = ['app.php', 'config.default.php', 'config.php']; /** @var EventConfig */ protected $eventConfig; diff --git a/src/Events/EventDispatcher.php b/src/Events/EventDispatcher.php new file mode 100644 index 00000000..f6cc1915 --- /dev/null +++ b/src/Events/EventDispatcher.php @@ -0,0 +1,86 @@ +listeners[$event][] = $listener; + } + } + + /** + * @param string $event + */ + public function forget($event): void + { + unset($this->listeners[$event]); + } + + /** + * @param string|object $event + * @param array|mixed $payload + * @param bool $halt + * + * @return array|mixed|null + */ + public function fire($event, $payload = [], $halt = false) + { + return $this->dispatch($event, $payload, $halt); + } + + /** + * @param string|object $event + * @param array|mixed $payload + * @param bool $halt Stop on first non-null return + * + * @return array|null|mixed + */ + public function dispatch($event, $payload = [], $halt = false) + { + if (is_object($event)) { + $payload = $event; + $event = get_class($event); + } + + $listeners = []; + if (isset($this->listeners[$event])) { + $listeners = $this->listeners[$event]; + } + + $responses = []; + foreach ($listeners as $listener) { + if (!is_callable($listener) && is_string($listener) && !Str::contains($listener, '@')) { + $listener = $listener . '@handle'; + } + + $response = app()->call($listener, ['event' => $event] + Arr::wrap($payload)); + + // Return the events response + if ($halt && !is_null($response)) { + return $response; + } + + // Stop further event propagation + if ($response === false) { + break; + } + + $responses[] = $response; + } + + return $halt ? null : $responses; + } +} diff --git a/src/Events/EventsServiceProvider.php b/src/Events/EventsServiceProvider.php new file mode 100644 index 00000000..50005ded --- /dev/null +++ b/src/Events/EventsServiceProvider.php @@ -0,0 +1,34 @@ +app->make(EventDispatcher::class); + + $this->app->instance(EventDispatcher::class, $dispatcher); + $this->app->instance('events.dispatcher', $dispatcher); + + $this->registerEvents($dispatcher); + } + + /** + * @param EventDispatcher $dispatcher + */ + protected function registerEvents(EventDispatcher $dispatcher) + { + /** @var Config $config */ + $config = $this->app->get('config'); + + foreach ($config->get('event-handlers', []) as $event => $handlers) { + foreach ((array)$handlers as $handler) { + $dispatcher->listen($event, $handler); + } + } + } +} diff --git a/tests/Unit/Config/ConfigServiceProviderTest.php b/tests/Unit/Config/ConfigServiceProviderTest.php index 8895f66d..d7c060d0 100644 --- a/tests/Unit/Config/ConfigServiceProviderTest.php +++ b/tests/Unit/Config/ConfigServiceProviderTest.php @@ -27,11 +27,11 @@ class ConfigServiceProviderTest extends ServiceProviderTest /** @var Config|MockObject $config */ list($app, $config) = $this->getConfiguredApp(__DIR__ . '/../../../config'); - $this->setExpects($config, 'set', null, null, $this->exactly(2)); - $config->expects($this->exactly(3)) + $this->setExpects($config, 'set', null, null, $this->exactly(3)); + $config->expects($this->exactly(4)) ->method('get') ->with(null) - ->willReturnOnConsecutiveCalls([], [], ['lor' => 'em']); + ->willReturnOnConsecutiveCalls([], [], [], ['lor' => 'em']); $configFile = __DIR__ . '/../../../config/config.php'; $configExists = file_exists($configFile); diff --git a/tests/Unit/Events/EventDispatcherTest.php b/tests/Unit/Events/EventDispatcherTest.php new file mode 100644 index 00000000..a62b4bd9 --- /dev/null +++ b/tests/Unit/Events/EventDispatcherTest.php @@ -0,0 +1,161 @@ +listen('foo', [$this, 'eventHandler']); + $event->listen(['foo', 'bar'], [$this, 'eventHandler']); + + $event->fire('foo'); + $event->fire('bar', 'Test!'); + + $this->assertEquals( + ['foo' => ['count' => 2, ['foo'], ['foo']], 'bar' => ['count' => 1, ['bar', 'Test!']]], + $this->firedEvents + ); + } + + /** + * @covers \Engelsystem\Events\EventDispatcher::forget + */ + public function testForget(): void + { + $event = new EventDispatcher(); + $event->forget('not-existing-event'); + + $event->listen('test', [$this, 'eventHandler']); + $event->forget('test'); + + $event->fire('test'); + + $this->assertEquals([], $this->firedEvents); + } + + /** + * @covers \Engelsystem\Events\EventDispatcher::dispatch + */ + public function testDispatchNotExistingEvent(): void + { + $event = new EventDispatcher(); + $response = $event->fire('not-existing-event'); + + $this->assertEquals([], $response); + } + + /** + * @covers \Engelsystem\Events\EventDispatcher::dispatch + */ + public function testDispatchObject(): void + { + $event = new EventDispatcher(); + $event->listen(static::class, [$this, 'eventHandler']); + $event->fire($this); + + $this->assertEquals([static::class => ['count' => 1, [static::class, $this]]], $this->firedEvents); + } + + /** + * @covers \Engelsystem\Events\EventDispatcher::dispatch + */ + public function testDispatchHalt(): void + { + $event = new EventDispatcher(); + $event->listen('test', [$this, 'returnNull']); + $event->listen('test', [$this, 'returnData']); + $event->listen('test', [$this, 'eventHandler']); + $response = $event->dispatch('test', [], true); + + $this->assertEquals(['example' => 'data'], $response); + $this->assertEquals([], $this->firedEvents); + + $event = new EventDispatcher(); + $response = $event->dispatch('test', [], true); + $this->assertNull($response); + } + + /** + * @covers \Engelsystem\Events\EventDispatcher::dispatch + */ + public function testDispatchStopPropagation(): void + { + $event = new EventDispatcher(); + $event->listen('test', [$this, 'returnNull']); + $event->listen('test', [$this, 'returnFalse']); + $event->listen('test', [$this, 'eventHandler']); + $response = $event->dispatch('test'); + + $this->assertEquals([null], $response); + $this->assertEquals([], $this->firedEvents); + } + + /** + * @covers \Engelsystem\Events\EventDispatcher::dispatch + */ + public function testDispatchFallbackHandleMethod(): void + { + $event = new EventDispatcher(); + $event->listen('test', EventDispatcherTest::class); + $response = $event->dispatch('test', [], true); + + $this->assertEquals(['default' => 'handler'], $response); + } + + /** + * @param string $event + */ + public function eventHandler(string $event): void + { + if (!isset($this->firedEvents[$event])) { + $this->firedEvents[$event] = ['count' => 0]; + } + + $this->firedEvents[$event]['count']++; + $this->firedEvents[$event][] = func_get_args(); + } + + /** + * @return null + */ + public function returnNull() + { + return null; + } + + /** + * @return bool + */ + public function returnFalse(): bool + { + return false; + } + + /** + * @return array + */ + public function returnData(): array + { + return ['example' => 'data']; + } + + /** + * @return array + */ + public function handle(): array + { + return ['default' => 'handler']; + } +} diff --git a/tests/Unit/Events/EventsServiceProviderTest.php b/tests/Unit/Events/EventsServiceProviderTest.php new file mode 100644 index 00000000..a91abc87 --- /dev/null +++ b/tests/Unit/Events/EventsServiceProviderTest.php @@ -0,0 +1,41 @@ +createMock(EventDispatcher::class); + $this->app->instance(EventDispatcher::class, $dispatcher); + $dispatcher->expects($this->exactly(3)) + ->method('listen') + ->withConsecutive( + ['test.event', 'someFunction'], + ['another.event', 'Foo\Bar@baz'], + ['another.event', [$this, 'someMethod']] + ); + + $config = new Config([ + 'event-handlers' => [ + 'test.event' => 'someFunction', + 'another.event' => ['Foo\Bar@baz', [$this, 'someMethod']] + ] + ]); + $this->app->instance('config', $config); + + /** @var EventsServiceProvider $provider */ + $provider = $this->app->make(EventsServiceProvider::class); + + $provider->register(); + } +}