Skip to content
Permalink
Browse files

Merge pull request #372 from thephpleague/event-dispatcher

Implement event dispatching
  • Loading branch information...
colinodell committed Jun 4, 2019
2 parents 72e87ec + ad51a80 commit d3fffa356ff9444b4237efe83081069e6f31c39d
@@ -4,6 +4,10 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi

## [Unreleased][unreleased]

### Added

- Added event dispatcher functionality (#359, #372)

## [1.0.0-beta3] - 2019-05-27

### Changed
@@ -0,0 +1,63 @@
---
layout: default
title: Event Dispatcher
---

Event Dispatcher
================

This library includes basic event dispatcher functionality. This makes it possible to add hook points throughout the library and third-party extensions which other code can listen for and execute code. If you're familiar with [Symfony's EventDispatcher](https://symfony.com/doc/current/components/event_dispatcher.html) or [PSR-14](https://www.php-fig.org/psr/psr-14/) then this should be very familiar to you.

## Event Class

All events must extend from the `AbstractEvent` class:

```php
use League\CommonMark\Event\AbstractEvent;
class MyCustomEvent extends AbstractEvent {}
```

An event can have any number of methods on it which return useful information the listeners can use or modify.

## Registering Listeners

Listeners can be registered with the `Environment` using the `addEventListener()` method:

```php
public function addEventListener(string $eventClass, callable $listener, int $priority = 0)
```

The parameters for this method are:

1. The fully-qualified name of the event class you wish to observe
2. Any PHP callable to execute when that type of event is dispatched
3. An optional priority (defaults to `0`)

For example:

```php
// Telling the environment which method to call:
$customListener = new MyCustomListener();
$environment->addEventListener(MyCustomEvent::class, [$customListener, 'onDocumentParsed']);
// Or if MyCustomerListener has an __invoke() method:
$environment->addEventListener(MyCustomEvent::class, new MyCustomListener(), 10);
// Or use any other type of callable you wish!
$environment->addEventListener(MyCustomEvent::class, function (MyCustomEvent $event) {
// TODO: Stuff
}, 10);
```

## Dispatching Events

Events can be dispatched via the `$environment->dispatch()` method which takes a single argument - an instance of `AbstractEvent` to dispatch:

```php
$environment->dispatch(new MyCustomEvent());
```

Listeners will be called in order of priority (higher priorities will be called first). If multiple listeners have the same priority, they'll be called in the order in which they were registered. If you'd like your listener to prevent other subsequent events from running, simply call `$event->stopPropagation()`.

Listeners may call any method on the event to get more information about the event, make changes to event data, etc.
@@ -14,6 +14,7 @@ version:
'Overview': '/1.0/customization/overview/'
'Environment': '/1.0/customization/environment/'
'Extensions': '/1.0/customization/extensions/'
'Event Dispatcher': '/1.0/customization/event-dispatcher/'
'Cursor': '/1.0/customization/cursor/'
'Block Parsing': '/1.0/customization/block-parsing/'
'Inline Parsing': '/1.0/customization/inline-parsing/'
@@ -100,4 +100,15 @@ public function addBlockRenderer($blockClass, BlockRendererInterface $blockRende
* @return self
*/
public function addInlineRenderer(string $inlineClass, InlineRendererInterface $renderer, int $priority = 0): ConfigurableEnvironmentInterface;
/**
* Registers the given event listener
*
* @param string $eventClass Fully-qualified class name of the event this listener should respond to
* @param callable $listener Listener to be executed
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return self
*/
public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface;
}
@@ -18,6 +18,7 @@
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Event\AbstractEvent;
use League\CommonMark\Extension\CommonMarkCoreExtension;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Inline\Parser\InlineParserInterface;
@@ -78,6 +79,11 @@ final class Environment implements EnvironmentInterface, ConfigurableEnvironment
*/
private $inlineRenderersByClass = [];
/**
* @var array<string, PrioritizedList<callable>>
*/
private $listeners = [];
/**
* @var Configuration
*/
@@ -380,6 +386,40 @@ public function getInlineParserCharacterRegex(): string
return $this->inlineParserCharacterRegex;
}
/**
* {@inheritdoc}
*/
public function addEventListener(string $eventClass, callable $listener, int $priority = 0): ConfigurableEnvironmentInterface
{
$this->assertUninitialized('Failed to add event listener.');
if (!isset($this->listeners[$eventClass])) {
$this->listeners[$eventClass] = new PrioritizedList();
}
$this->listeners[$eventClass]->add($listener, $priority);
return $this;
}
/**
* {@inheritdoc}
*/
public function dispatch(AbstractEvent $event): void
{
$this->initializeExtensions();
$type = get_class($event);
foreach ($this->listeners[$type] ?? [] as $listener) {
if ($event->isPropagationStopped()) {
return;
}
$listener($event);
}
}
private function buildInlineParserCharacterRegex()
{
$chars = \array_unique(\array_merge(
@@ -14,6 +14,7 @@
use League\CommonMark\Block\Parser\BlockParserInterface;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
use League\CommonMark\Event\AbstractEvent;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
@@ -75,4 +76,11 @@ public function getInlineRenderersForClass(string $inlineClass): iterable;
* @return string
*/
public function getInlineParserCharacterRegex(): string;
/**
* Dispatches the given event to listeners
*
* @param AbstractEvent $event
*/
public function dispatch(AbstractEvent $event): void;
}
@@ -0,0 +1,49 @@
<?php
/**
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the Symfony EventDispatcher "Event" contract
* - (c) 2018-2019 Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Event;
/**
* Base class for classes containing event data.
*
* This class contains no event data. It is used by events that do not pass
* state information to an event handler when an event is raised.
*
* You can call the method stopPropagation() to abort the execution of
* further listeners in your event listener.
*/
abstract class AbstractEvent
{
private $propagationStopped = false;
/**
* Returns whether further event listeners should be triggered.
*/
final public function isPropagationStopped(): bool
{
return $this->propagationStopped;
}
/**
* Stops the propagation of the event to further event listeners.
*
* If multiple event listeners are connected to the same event, no
* further event listener will be triggered once any trigger calls
* stopPropagation().
*/
final public function stopPropagation(): void
{
$this->propagationStopped = true;
}
}
@@ -24,6 +24,7 @@
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Inline\Parser\InlineParserInterface;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
use League\CommonMark\Tests\Unit\Event\FakeEvent;
use League\CommonMark\Util\ConfigurationAwareInterface;
use PHPUnit\Framework\TestCase;
@@ -475,4 +476,53 @@ public function testInlineRendererPrioritization()
$this->assertSame($renderer1, $parsers[1]);
$this->assertSame($renderer3, $parsers[2]);
}
public function testEventDispatching()
{
$environment = new Environment();
$event = new FakeEvent();
$actualOrder = [];
$environment->addEventListener(FakeEvent::class, function (FakeEvent $e) use ($event, &$actualOrder) {
$this->assertSame($event, $e);
$actualOrder[] = 'a';
});
$environment->addEventListener(FakeEvent::class, function (FakeEvent $e) use ($event, &$actualOrder) {
$this->assertSame($event, $e);
$actualOrder[] = 'b';
$e->stopPropagation();
});
$environment->addEventListener(FakeEvent::class, function (FakeEvent $e) use ($event, &$actualOrder) {
$this->assertSame($event, $e);
$actualOrder[] = 'c';
}, 10);
$environment->addEventListener(FakeEvent::class, function (FakeEvent $e) use ($event, &$actualOrder) {
$this->fail('Propogation should have been stopped before here');
});
$environment->dispatch($event);
$this->assertCount(3, $actualOrder);
$this->assertEquals('c', $actualOrder[0]);
$this->assertEquals('a', $actualOrder[1]);
$this->assertEquals('b', $actualOrder[2]);
}
/**
* @expectedException \RuntimeException
*/
public function testAddEventListenerFailsAfterInitialization()
{
$environment = new Environment();
$event = $this->createMock(AbstractEvent::class);
$environment->dispatch($event);
$environment->addEventListener(AbstractEvent::class, function (AbstractEvent $e) {
});
}
}
@@ -0,0 +1,27 @@
<?php
/**
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Tests\Unit\Event;
use PHPUnit\Framework\TestCase;
final class AbstractEventTest extends TestCase
{
public function testStopPropagation()
{
$event = new FakeEvent();
$this->assertFalse($event->isPropagationStopped());
$event->stopPropagation();
$this->assertTrue($event->isPropagationStopped());
}
}
@@ -0,0 +1,18 @@
<?php
/**
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Tests\Unit\Event;
use League\CommonMark\Event\AbstractEvent;
final class FakeEvent extends AbstractEvent
{
}

0 comments on commit d3fffa3

Please sign in to comment.
You can’t perform that action at this time.