diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 0000000..26c5802 --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,11 @@ +name: "Continuous Integration" + +on: + pull_request: + push: + branches: + tags: + +jobs: + ci: + uses: laminas/workflow-continuous-integration/.github/workflows/continuous-integration.yml@1.x diff --git a/.github/workflows/cs-tests.yml b/.github/workflows/cs-tests.yml deleted file mode 100644 index e8bbade..0000000 --- a/.github/workflows/cs-tests.yml +++ /dev/null @@ -1,47 +0,0 @@ -on: - - push - -name: Run phpcs checks - -jobs: - mutation: - name: PHP ${{ matrix.php }}-${{ matrix.os }} - - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: - - ubuntu-latest - - php: - - "8.1" - - "8.2" - - "8.3" - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: "${{ matrix.php }}" - tools: composer:v2, cs2pr - coverage: none - - - name: Determine composer cache directory - run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - - - name: Cache dependencies installed with composer - uses: actions/cache@v3 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: | - php${{ matrix.php }}-composer- - - name: Install dependencies with composer - run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - - - name: Run phpcs checks - run: vendor/bin/phpcs diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml new file mode 100644 index 0000000..1a7aa24 --- /dev/null +++ b/.github/workflows/docs-build.yml @@ -0,0 +1,16 @@ +name: docs-build + +on: + release: + types: [published] + workflow_dispatch: + +jobs: + build-deploy: + runs-on: ubuntu-latest + steps: + - name: Build Docs + uses: dotkernel/documentation-theme/github-actions/docs@main + env: + DEPLOY_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml deleted file mode 100644 index 6f7452d..0000000 --- a/.github/workflows/static-analysis.yml +++ /dev/null @@ -1,47 +0,0 @@ -on: - - push - -name: Run static analysis - -jobs: - mutation: - name: PHP ${{ matrix.php }}-${{ matrix.os }} - - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: - - ubuntu-latest - - php: - - "8.1" - - "8.2" - - "8.3" - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: "${{ matrix.php }}" - tools: composer:v2, cs2pr - coverage: none - - - name: Determine composer cache directory - run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - - - name: Cache dependencies installed with composer - uses: actions/cache@v3 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: | - php${{ matrix.php }}-composer- - - name: Install dependencies with composer - run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - - - name: Run static analysis - run: vendor/bin/psalm --no-cache --output-format=github --show-info=false --threads=4 diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml deleted file mode 100644 index 7f5f333..0000000 --- a/.github/workflows/unit-tests.yml +++ /dev/null @@ -1,48 +0,0 @@ -on: - - push - -name: Run PHPUnit tests - -jobs: - mutation: - name: PHP ${{ matrix.php }}-${{ matrix.os }} - - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: - - ubuntu-latest - - php: - - "8.1" - - "8.2" - - "8.3" - - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Install PHP - uses: shivammathur/setup-php@v2 - with: - php-version: "${{ matrix.php }}" - tools: composer:v2, cs2pr - coverage: none - - - name: Determine composer cache directory - run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - - - name: Cache dependencies installed with composer - uses: actions/cache@v3 - with: - path: ${{ env.COMPOSER_CACHE_DIR }} - key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: | - php${{ matrix.php }}-composer- - - - name: Install dependencies with composer - run: composer install --prefer-dist --no-interaction --no-progress --optimize-autoloader --ansi - - - name: Run PHPUnit tests - run: vendor/bin/phpunit --colors=always diff --git a/README.md b/README.md index 3c18126..e91e35c 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ DotKernel event component extending and customizing [laminas-eventmanager](https [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-event)](https://github.com/dotkernel/dot-event/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-event)](https://github.com/dotkernel/dot-event/network) [![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-event)](https://github.com/dotkernel/dot-event/stargazers) -[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-event)](https://github.com/dotkernel/dot-event/blob/3.0/LICENSE.md) +[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-event)](https://github.com/dotkernel/dot-event/blob/4.0/LICENSE.md) -[![Build Static](https://github.com/dotkernel/dot-event/actions/workflows/static-analysis.yml/badge.svg?branch=3.0)](https://github.com/dotkernel/dot-event/actions/workflows/static-analysis.yml) +[![Build Static](https://github.com/dotkernel/dot-event/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0)](https://github.com/dotkernel/dot-event/actions/workflows/continuous-integration.yml) [![codecov](https://codecov.io/gh/dotkernel/dot-event/graph/badge.svg?token=C00YQLVZ7Y)](https://codecov.io/gh/dotkernel/dot-event) [![SymfonyInsight](https://insight.symfony.com/projects/5c4a19db-4114-4f92-a838-ea72ec4b9a5a/big.svg)](https://insight.symfony.com/projects/5c4a19db-4114-4f92-a838-ea72ec4b9a5a) diff --git a/docs/book/index.md b/docs/book/index.md new file mode 100644 index 0000000..e69de29 diff --git a/docs/book/v4/configuration.md b/docs/book/v4/configuration.md new file mode 100644 index 0000000..65fd521 --- /dev/null +++ b/docs/book/v4/configuration.md @@ -0,0 +1,6 @@ +# Configuration + +After installation, you need to register the `ConfigProvider` in your project +by adding the below line to your configuration aggregator (usually: `config/config.php`) + + \Dot\Event\ConfigProvider diff --git a/docs/book/v4/instalation.md b/docs/book/v4/instalation.md new file mode 100644 index 0000000..73fbaa9 --- /dev/null +++ b/docs/book/v4/instalation.md @@ -0,0 +1,6 @@ +# Instalation +Install `dot-event` by executing the following composer command + +```bash +composer require dotkernel/dot-event +``` diff --git a/docs/book/v4/overview.md b/docs/book/v4/overview.md new file mode 100644 index 0000000..b7bd01d --- /dev/null +++ b/docs/book/v4/overview.md @@ -0,0 +1,3 @@ +# Overview +Dotkernel's event manager is extending [laminas-eventmanager](https://docs.laminas.dev/laminas-eventmanager/) +so it can be easier to implement events and listeners. diff --git a/docs/book/v4/usage.md b/docs/book/v4/usage.md new file mode 100644 index 0000000..94f0c24 --- /dev/null +++ b/docs/book/v4/usage.md @@ -0,0 +1,203 @@ +# Usage + +This tutorial explores various examples of dot-event usages. + +To start using events you will need the following things: + +- An **Event** +- One or more **listeners** that will be registered on the application config ( ``ConfigProvider`` ) +- An instance of **EventManager** from the **container** so you can trigger the events + +The listeners needs to be registered in the `ConfigProvider`, under the +`['dot-event']` key + +## Example + +The below example will implement an event for update users. + +**Event** + +Every event needs to extends `Dot\Event\Event` + +```php +class UserEvent extends Event +{ + public const EVENTS_PRE_UPDATE = 'pre.update.user'; + public const EVENTS_POST_UPDATE = 'post.update.user'; + + public function __construct($name = null, $target = null, $params = []) + { + parent::__construct($name, $target, $params); + } + +} +``` + +**Listeners** + +We use the concept of listener aggregates because with this approach in a single class +we can listen to multiple events. If you pay attention, in the above event we have 2 events +`pre.update.user` and `post.update.user` + +Every listener needs to extend `Dot\Event\ControllerEventListenerInterface`. The interface +defines two methods `attach()` and `detach()` + +```php +class UserEventListener implements DotEventListenerInterface +{ + use ListenerAggregateTrait; + + #[Dot\AnnotatedServices\Attribute\Inject(Logger::class)] + public function __construct(private Logger $logger) + { + } + + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->listeners[] = $events->attach(UserUpdatedEvent::EVENTS_PRE_UPDATE, [$this, 'onUserPreUpdated']); + $this->listeners[] = $events->attach(UserUpdatedEvent::EVENTS_POST_UPDATE, [$this, 'onUserPostUpdate']); + } + + public function onUserPreUpdated(UserEvent $event) + { + $this->logger->info('The pre update event is triggered'); + + //...more logic + + } + + public function onUserPostUpdate(UserEvent $event) + { + //...more logic + + $this->logger->info('The post update event is triggered'); + } + +} +``` + +> **NOTE** +> +> The trait `Laminas\EventManager\ListenerAggregateTrait` can be used to help implementing +> `DotEventListenerInterface`. It defines the `$listeners` property, and `detach()` logic + +We register the listener in the `ConfigProvider` + +```php +'dot-event' => [ + UserEventListener::class +] +``` + +**EventManager** + +Every event can be triggered from an `Laminas\EventManager\EventManager` instance loaded +from the container + +```php + +class MyService +{ + #[Dot\AnnotatedServices\Attribute\Inject(EventManagerInterface::class)] + public function __construct(private EventManagerInterface $eventManager) + { + + } + + public function update() + { + $this->eventManager->triggerEvent(new UserEvent(UserEvent::EVENTS_PRE_UPDATE, params: ['user' => $user])); + + // ... logic of update + + $this->eventManager->triggerEvent(new UserEvent(UserEvent::EVENTS_POST_UPDATE, params: ['user' => $user])); + } +} + +``` + +> **NOTE** +> +> To inject classes from the container we use `Dot\AnnotatedServices\Attribute\Inject` from +> `dot-annotated-services` package, but you can use your own logic to get things from +> the container + +## Keep all in order + +You can attach multiple listeners to the same event but with different logic. + +All listeners are executed in the order in which they are attached. However, you can provide a priority value +and you can influence the order of the execution + +- Higher priority values execute earlier. +- Lower (negative) priority values execute later. + +```php +class UserEventListener implements DotEventListenerInterface +{ + use ListenerAggregateTrait; + + #[Dot\AnnotatedServices\Attribute\Inject(Logger::class)] + public function __construct(private Logger $logger) + { + } + + public function attach(EventManagerInterface $events, $priority = 1) + { + $this->listeners[] = $events->attach(UserUpdatedEvent::EVENTS_POST_UPDATE, [$this, 'onUserPostUpdateSecond'], 1); // will run second + $this->listeners[] = $events->attach(UserUpdatedEvent::EVENTS_POST_UPDATE, [$this, 'onUserPostUpdateFirst'], 2); // will run first + } + + // ... callbacks + +} +``` + +As you can notice, we attach the same event name to listeners, so once we trigger the event both +callback will run one after another. But because we provide priority, the second attach will run first +because has a higher priority. + +## Short-circuiting the execution + +Sometimes you have more listeners to an event, and you may want to stop the execution of the event if something is wrong +in one of the listeners. + +```php + + public function onUserPostUpdateFirst(UserEvent $event) + { + $event->stopPropagation(); + } + + public function onUserPostUpdateSecond(UserEvent $event) + { + + // this will not execute + + } +``` + +## Returns + +You can return whatever you want in a listener callback. All events trigger returns an instance of +`Laminas\EventManager\ResponseCollection` so you can have information if the event has stopped, or if event +returned some expected object. + +```php +class MyService +{ + #[Dot\AnnotatedServices\Attribute\Inject(EventManagerInterface::class)] + public function __construct(private EventManagerInterface $eventManager) + { + + } + + public function update() + { + /** @var Laminas\EventManager\ResponseCollection $result */ + $result = $this->eventManager->triggerEvent(new UserEvent(UserEvent::EVENTS_POST_UPDATE, params: ['user' => $user])); + $result->stopped(); // true or false if propagation is stopped + $result->first(); // what last listener has returned (on multiple listeners it uses LIFO mode) + } +} +``` diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..7f08164 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,19 @@ +docs_dir: docs/book +site_dir: docs/html +extra: + project: Packages + current_version: v4 + versions: + - v4 +nav: + - Home: index.md + - v4: + - Overview: v4/overview.md + - Installation: v4/installation.md + - Configuration: v4/configuration.md + - Usage: v4/usage.md +site_name: dot-event +site_description: "DotKernel's event manager package" +repo_url: "https://github.com/dotkernel/dot-event" +plugins: + - search diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index efee958..c66b9fb 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -5,10 +5,8 @@ namespace Dot\Event; use Dot\Event\Factory\EventManagerFactory; +use Laminas\EventManager\EventManager; use Laminas\EventManager\EventManagerInterface; -use Laminas\EventManager\SharedEventManager; -use Laminas\EventManager\SharedEventManagerInterface; -use Laminas\ServiceManager\Factory\InvokableFactory; class ConfigProvider { @@ -16,6 +14,7 @@ public function __invoke(): array { return [ 'dependencies' => $this->getDependencyConfig(), + 'dot-event' => [], ]; } @@ -23,14 +22,10 @@ public function getDependencyConfig(): array { return [ 'factories' => [ - SharedEventManager::class => InvokableFactory::class, - EventManagerInterface::class => EventManagerFactory::class, + EventManager::class => EventManagerFactory::class, ], 'aliases' => [ - SharedEventManagerInterface::class => SharedEventManager::class, - ], - 'shared' => [ - EventManagerInterface::class => false, + EventManagerInterface::class => EventManager::class, ], ]; } diff --git a/src/DotEventListenerInterface.php b/src/DotEventListenerInterface.php new file mode 100644 index 0000000..4a90990 --- /dev/null +++ b/src/DotEventListenerInterface.php @@ -0,0 +1,11 @@ +has(SharedEventManagerInterface::class) - ? $container->get(SharedEventManagerInterface::class) - : null; + $eventManager = new EventManager(); + $config = $container->get('config')['dot-event'] ?? []; + + foreach ($config as $listenerAggregate) { + $listener = $this->getListener($container, $listenerAggregate); + $listener->attach($eventManager); + } + + return $eventManager; + } + + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + private function getListener( + ContainerInterface $container, + string $listenerName + ): DotEventListenerInterface { + $listener = $listenerName; + if ($container->has($listener)) { + $listener = $container->get($listener); + } + + if (is_string($listener) && class_exists($listener)) { + $listener = new $listener(); + } + + if (! $listener instanceof DotEventListenerInterface) { + throw new RuntimeException(sprintf( + 'Event listener must be an instance of %s, but %s was provided', + DotEventListenerInterface::class, + is_object($listener) ? $listener::class : gettype($listener) + )); + } - return new EventManager($sharedEventManager); + return $listener; } } diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php index 6d1dfb5..16657b5 100644 --- a/test/ConfigProviderTest.php +++ b/test/ConfigProviderTest.php @@ -5,9 +5,8 @@ namespace DotTest\Event; use Dot\Event\ConfigProvider; +use Laminas\EventManager\EventManager; use Laminas\EventManager\EventManagerInterface; -use Laminas\EventManager\SharedEventManager; -use Laminas\EventManager\SharedEventManagerInterface; use PHPUnit\Framework\TestCase; class ConfigProviderTest extends TestCase @@ -22,24 +21,18 @@ protected function setup(): void public function testHasDependencies(): void { $this->assertArrayHasKey('dependencies', $this->config); + $this->assertArrayHasKey('dot-event', $this->config); } public function testDependenciesHasFactories(): void { $this->assertArrayHasKey('factories', $this->config['dependencies']); - $this->assertArrayHasKey(SharedEventManager::class, $this->config['dependencies']['factories']); - $this->assertArrayHasKey(EventManagerInterface::class, $this->config['dependencies']['factories']); + $this->assertArrayHasKey(EventManager::class, $this->config['dependencies']['factories']); } public function testDependenciesHasAliases(): void { $this->assertArrayHasKey('aliases', $this->config['dependencies']); - $this->assertArrayHasKey(SharedEventManagerInterface::class, $this->config['dependencies']['aliases']); - } - - public function testDependenciesHasShared(): void - { - $this->assertArrayHasKey('shared', $this->config['dependencies']); - $this->assertArrayHasKey(EventManagerInterface::class, $this->config['dependencies']['shared']); + $this->assertArrayHasKey(EventManagerInterface::class, $this->config['dependencies']['aliases']); } } diff --git a/test/Factory/EventManagerFactoryTest.php b/test/Factory/EventManagerFactoryTest.php index 190bf97..adfe55f 100644 --- a/test/Factory/EventManagerFactoryTest.php +++ b/test/Factory/EventManagerFactoryTest.php @@ -6,17 +6,17 @@ use Dot\Event\Factory\EventManagerFactory; use Laminas\EventManager\EventManager; -use Laminas\EventManager\SharedEventManagerInterface; use PHPUnit\Framework\MockObject\Exception; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; +use Psr\Container\NotFoundExceptionInterface; class EventManagerFactoryTest extends TestCase { private EventManagerFactory $eventManagerFactory; private ContainerInterface|MockObject $container; - private SharedEventManagerInterface|MockObject $sharedEventManager; /** * @throws Exception @@ -24,36 +24,23 @@ class EventManagerFactoryTest extends TestCase public function setUp(): void { $this->eventManagerFactory = new EventManagerFactory(); - $this->sharedEventManager = $this->createMock(SharedEventManagerInterface::class); $this->container = $this->createMock(ContainerInterface::class); } - public function testInvokeWithSharedEventManager(): void + /** + * @throws ContainerExceptionInterface + * @throws NotFoundExceptionInterface + */ + public function testInvokeEventManager(): void { - $container = $this->container; - $sharedEventManager = $this->sharedEventManager; - $container->expects($this->once()) - ->method('has') - ->with(SharedEventManagerInterface::class) - ->willReturn(true); + $container = $this->container; + $container->expects($this->once()) ->method('get') - ->with(SharedEventManagerInterface::class) - ->willReturn($sharedEventManager); + ->with('config') + ->willReturn([]); $factory = new EventManagerFactory(); - $eventManager = $factory->__invoke($container); - $this->assertInstanceOf(EventManager::class, $eventManager); - } - - public function testInvokeWithoutSharedEventManager(): void - { - $container = $this->container; - $container->expects($this->once()) - ->method('has') - ->with(SharedEventManagerInterface::class) - ->willReturn(false); - $factory = $this->eventManagerFactory; - $eventManager = $factory->__invoke($container); + $eventManager = $factory($container); $this->assertInstanceOf(EventManager::class, $eventManager); } }