From e372d0ea3640a0f4ebfcfdeb9898dacfad992e54 Mon Sep 17 00:00:00 2001 From: bidi Date: Fri, 30 Oct 2026 18:31:32 +0200 Subject: [PATCH 1/3] update composer and workflows, code refactor, created docs for v5 Signed-off-by: bidi --- .github/workflows/codecov.yml | 8 +- .github/workflows/static-analysis.yml | 8 +- .gitignore | 2 + CHANGELOG.md | 233 ++++++++++++++++++++--- README.md | 2 +- SECURITY.md | 4 +- composer.json | 7 +- docs/book/v5/configuration.md | 13 ++ docs/book/v5/installation.md | 14 ++ docs/book/v5/overview.md | 19 ++ docs/book/v5/usage.md | 201 +++++++++++++++++++ mkdocs.yml | 8 +- phpstan.neon | 2 +- phpunit.xml | 3 +- src/ConfigProvider.php | 6 + src/Exception/ExceptionInterface.php | 11 ++ src/Exception/RuntimeException.php | 9 + src/Factory/EventManagerFactory.php | 39 ++-- test/Asset/TestListener.php | 33 ++++ test/ConfigProviderTest.php | 25 ++- test/EventTest.php | 9 +- test/Exception/RuntimeExceptionTest.php | 25 +++ test/Factory/EventManagerFactoryTest.php | 139 +++++++++++++- 23 files changed, 747 insertions(+), 73 deletions(-) create mode 100644 docs/book/v5/configuration.md create mode 100644 docs/book/v5/installation.md create mode 100644 docs/book/v5/overview.md create mode 100644 docs/book/v5/usage.md create mode 100644 src/Exception/ExceptionInterface.php create mode 100644 src/Exception/RuntimeException.php create mode 100644 test/Asset/TestListener.php create mode 100644 test/Exception/RuntimeExceptionTest.php diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index c0fbc53..be82fcc 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -15,14 +15,14 @@ jobs: - ubuntu-latest php: - - "8.2" - "8.3" - "8.4" - "8.5" + - "8.6" steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -36,7 +36,7 @@ jobs: run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - name: Cache dependencies installed with composer - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: ${{ env.COMPOSER_CACHE_DIR }} key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} @@ -50,6 +50,6 @@ jobs: run: vendor/bin/phpunit --colors=always --coverage-clover clover.xml - name: Send code coverage report to Codecov.io - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 9976515..0087b3d 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -4,7 +4,7 @@ on: name: Run PHPStan checks jobs: - mutation: + static-analysis: name: PHPStan ${{ matrix.php }}-${{ matrix.os }} runs-on: ${{ matrix.os }} @@ -15,10 +15,10 @@ jobs: - ubuntu-latest php: - - "8.2" - "8.3" - "8.4" - "8.5" + - "8.6" steps: - name: Checkout @@ -28,9 +28,9 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: "${{ matrix.php }}" - coverage: pcov + coverage: none ini-values: assert.exception=1, zend.assertions=1, error_reporting=-1, log_errors_max_len=0, display_errors=On - tools: composer:v2, cs2pr + tools: composer:v2 - name: Determine composer cache directory run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV diff --git a/.gitignore b/.gitignore index d3096f4..790c13a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ clover.xml coveralls-upload.json .phpunit.result.cache .phpcs-cache +.phpunit.cache +docs/html/ # Created by .ignore support plugin (hsz.mobi) ### JetBrains template diff --git a/CHANGELOG.md b/CHANGELOG.md index 7891944..d772732 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,102 +1,277 @@ +# Changelog + +## 5.0.0 - 2026-11-02 + +### Changed + +- Minimum supported PHP version is now 8.3; PHP 8.6 is allowed. +- `Dot\Event\Factory\EventManagerFactory` now throws `Dot\Event\Exception\RuntimeException` (which extends `\RuntimeException`) when a configured listener does not implement `Dot\Event\DotEventListenerInterface`. +- Entries under the `dot-event` config key may now also be listener instances. + +### Added + +- `Dot\Event\Exception\ExceptionInterface` and `Dot\Event\Exception\RuntimeException`. + +### Deprecated + +- Instantiating a listener from its class name when the container has no service with that name. This now emits an `E_USER_DEPRECATED` notice and will be removed in the next major version: register your listeners in the container. + +### Removed + +- Support for PHP 8.2. +- The `psr/http-message` requirement, which the package never used. Projects that rely on it must require it directly. + +### Fixed + +- A `TypeError` is no longer thrown when a `dot-event` config entry is not a string; the listener validation error is thrown for invalid entries instead. + +## 4.4.0 - 2025-10-31 + +### Added + +- Issue #36: Support for PHP 8.4 and 8.5 (#38). + +### Removed + +- Issue #36: Support for PHP 8.1 (#38). + +## 4.3.0 - 2025-03-04 + +### Changed + +- Issue #32: Replaced Psalm with PHPStan (#33). + +## 4.2.1 - 2025-01-28 + +### Changed + +- Issue #30: Bumped Psalm to 6.x (#31). + +## 4.2.0 - 2025-01-22 + +### Added + +- Issue #26: Support for PHP 8.4 (#29). + +## 4.1.0 - 2025-01-13 + +### Added + +- Issue #27: Support for `laminas/laminas-servicemanager` 4.x (#28). + +### Changed + +- Updated `README.md` (#25). + +## 4.0.3 - 2024-06-28 + +### Changed + +- Issue #22: Marked the package as a wrapper on top of laminas-eventmanager (#23). +- Updated `overview.md` (#24). + +## 4.0.2 - 2024-05-03 + +No changes since 4.0.1. + +## 4.0.1 - 2024-05-03 + +### Fixed + +- Renamed `docs/book/v4/instalation.md` to `installation.md` and fixed `configuration.md` (#21). + +## 4.0.0 - 2024-04-25 + +### Added + +- `Dot\Event\DotEventListenerInterface`. +- Documentation for version 4, and `SECURITY.md`. + +### Changed + +- Refactored the package: `EventManagerFactory` now attaches the listeners listed under the `dot-event` config key (#20). +- CI now uses the shared Laminas continuous integration workflow. + +## 3.5.0 - 2025-10-31 + +### Added + +- Issue #35: Support for PHP 8.4 and 8.5 (#37). + +### Removed + +- Issue #35: Support for PHP 8.1 (#37). + +## 3.4.2 - 2023-11-29 + +### Added + +- Support for PHP 8.3 (#17). + +## 3.4.1 - 2023-08-24 + +### Added + +- Codecov integration and badge (#13, #15). + +### Changed + +- Allowed `psr/http-message` `^1.0 || ^2.0` (#13). + +## 3.4.0 - 2023-07-21 + +### Changed + +- Code quality improvements, enhancements and tests (#11). + +## 3.3.0 - 2023-03-29 + +### Removed + +- `laminas/laminas-dependency-plugin` from `composer.json` (#8). + +## 3.2.0 - 2022-04-01 + +### Changed + +- Updated dependency versions. +- Updated tags in `README.md`. + ## 3.1.0 - 2020-11-02 ### Changed -* Composer 2 compatibility. -* Update composer.json to require version 2 of laminas/laminas-dependency-plugin to be Composer 2 compatible. + +- Composer 2 compatibility. +- Update composer.json to require version 2 of laminas/laminas-dependency-plugin to be Composer 2 compatible. ### Added -* Nothing + +- Nothing ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing + +- Nothing + +## 3.0.1 - 2020-04-23 + +### Changed + +- License, `.gitignore` and composer keywords. + +## 3.0.0 - 2020-04-10 + +### Changed + +- Dependencies update. ## 2.0.0 - 2020-01-30 ### Changed -* Laminas and Mezzio migration. + +- Laminas and Mezzio migration. ### Added -* Nothing + +- Nothing ### Deprecated -* Nothing + +- Nothing ### Removed -* Support + +- Support ### Fixed -* Nothing +- Nothing ## 0.2.1 - 2018-05-04 ### Changed -* dependency update + +- dependency update ### Added -* Nothing + +- Nothing ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing +- Nothing ## 0.2.0 - 2017-03-15 ### Changed -* Updated factories to typehint against PSR11 container + +- Updated factories to typehint against PSR11 container ### Added -* Nothing + +- Nothing ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing +- Nothing ## 0.1.1 - 2017-03-11 ### Added -* Updated php file headers doc blocks and package description + +- Updated php file headers doc blocks and package description ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing +- Nothing ## 0.1.0 - 2017-03-07 Initial tagged release ### Added -* Everything + +- Everything ### Deprecated -* Nothing + +- Nothing ### Removed -* Nothing + +- Nothing ### Fixed -* Nothing + +- Nothing diff --git a/README.md b/README.md index 8ae8117..de9f8a8 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Documentation is available at: https://docs.dotkernel.org/dot-event/. ## Badges ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-event) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-event/4.4.0) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-event) [![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) diff --git a/SECURITY.md b/SECURITY.md index 5b585ca..bf35f32 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,6 +4,7 @@ | Version | Supported | PHP Version | |---------|--------------------|-----------------------------------------------------------------------------------------------------------| +| 5.x | :white_check_mark: | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-event/5.0.0) | | 4.x | :white_check_mark: | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-event/4.4.0) | | 3.x | :white_check_mark: | ![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-event/3.5.0) | | <= 2.x | :x: | | @@ -26,7 +27,6 @@ release prior to any public exposure; this helps protect the project's users and provides them with a chance to upgrade and/or update to protect their applications. - ## Policy If we verify a reported security vulnerability, our policy is: @@ -35,4 +35,4 @@ If we verify a reported security vulnerability, our policy is: release branch. - After patching the release branches, we will immediately issue new security - fix releases for each patched release branch. \ No newline at end of file + fix releases for each patched release branch. diff --git a/composer.json b/composer.json index e9a7de1..f97a76a 100644 --- a/composer.json +++ b/composer.json @@ -17,16 +17,15 @@ } ], "require": { - "php": "~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0 || ~8.6.0", "laminas/laminas-eventmanager": "^3.4", - "laminas/laminas-servicemanager": "^3.10 || ^4.0", - "psr/http-message": "^1.0 || ^2.0" + "laminas/laminas-servicemanager": "^3.10 || ^4.0" }, "require-dev": { "laminas/laminas-coding-standard": "^3.0", "phpstan/phpstan": "^2.1", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.2" + "phpunit/phpunit": "^11.5 || ^12.0" }, "autoload": { "psr-4": { diff --git a/docs/book/v5/configuration.md b/docs/book/v5/configuration.md new file mode 100644 index 0000000..02d7d5e --- /dev/null +++ b/docs/book/v5/configuration.md @@ -0,0 +1,13 @@ +# 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`): + +```php +Dot\Event\ConfigProvider::class, +``` + +## What the ConfigProvider registers + +- `Laminas\EventManager\EventManager`, built by `Dot\Event\Factory\EventManagerFactory`. +- `Laminas\EventManager\EventManagerInterface`, an alias of the above, so you can inject either one. Both resolve to the same shared instance. +- An empty `dot-event` key, where your application lists its event listeners (see [Usage](usage.md)). diff --git a/docs/book/v5/installation.md b/docs/book/v5/installation.md new file mode 100644 index 0000000..49deb36 --- /dev/null +++ b/docs/book/v5/installation.md @@ -0,0 +1,14 @@ +# Installation + +## Requirements + +- PHP 8.3, 8.4, 8.5 or 8.6 +- A PSR-11 container that exposes the merged application configuration as a `config` service. Mezzio and Dotkernel applications provide this out of the box. + +## Install + +Install `dot-event` by executing the following composer command: + +```shell +composer require dotkernel/dot-event +``` diff --git a/docs/book/v5/overview.md b/docs/book/v5/overview.md new file mode 100644 index 0000000..a2228c0 --- /dev/null +++ b/docs/book/v5/overview.md @@ -0,0 +1,19 @@ +# Overview + +Dotkernel's event manager package. + +> dot-event is a wrapper on top of [laminas-eventmanager](https://github.com/laminas/laminas-eventmanager) + +## Badges + +![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-event) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-event) + +[![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/5.0/LICENSE.md) + +[![Build Static](https://github.com/dotkernel/dot-event/actions/workflows/continuous-integration.yml/badge.svg?branch=5.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) +[![PHPStan](https://github.com/dotkernel/dot-event/actions/workflows/static-analysis.yml/badge.svg?branch=5.0)](https://github.com/dotkernel/dot-event/actions/workflows/static-analysis.yml) diff --git a/docs/book/v5/usage.md b/docs/book/v5/usage.md new file mode 100644 index 0000000..0f2e287 --- /dev/null +++ b/docs/book/v5/usage.md @@ -0,0 +1,201 @@ +# 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 in the application config +- An instance of **EventManager** from the **container** so you can trigger the events + +The listeners need to be registered in your application's config (for example in your own module's `ConfigProvider`), under the `dot-event` key. + +## Example + +The below example will implement an event for update users. + +Events should extend `Dot\Event\Event`: + +```php +use Dot\Event\Event; + +class UserEvent extends Event +{ + public const EVENTS_PRE_UPDATE = 'pre.update.user'; + public const EVENTS_POST_UPDATE = 'post.update.user'; +} +``` + +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 two events `pre.update.user` and `post.update.user`. + +Every listener needs to implement `Dot\Event\DotEventListenerInterface`. +The interface extends `Laminas\EventManager\ListenerAggregateInterface`, so it requires two methods: `attach()` and `detach()`. + +In the examples below, `User` and `Logger` stand for your own classes. + +```php +use Dot\Event\DotEventListenerInterface; +use Laminas\EventManager\EventManagerInterface; +use Laminas\EventManager\ListenerAggregateTrait; + +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): void + { + $this->listeners[] = $events->attach(UserEvent::EVENTS_PRE_UPDATE, [$this, 'onUserPreUpdated']); + $this->listeners[] = $events->attach(UserEvent::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'); + } + +} +``` + +> 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 application config, under the `dot-event` key: + +```php +'dot-event' => [ + UserEventListener::class +] +``` + +Each entry is resolved in the following order: + +1. If the entry is already an instance of `Dot\Event\DotEventListenerInterface`, it is used as is. +2. If the entry is a string and the container has a service with that name, the service is used. This is the recommended way, and the only way to give a listener constructor dependencies. +3. Otherwise, if the entry is the name of an existing class, the class is instantiated without arguments. + > This fallback is **deprecated** and emits an `E_USER_DEPRECATED` notice. It will be removed in the next major version, so register your listeners in the container. +4. Otherwise, a `Dot\Event\Exception\RuntimeException` is thrown. + +In every case, the resolved listener must implement `Dot\Event\DotEventListenerInterface`, otherwise the same exception is thrown. This includes entries that are not strings or listener instances, such as integers or arrays. + +> Listener instances are convenient in tests or small scripts. In an application that caches its configuration, register listeners by class name and let the container create them, because objects cannot be stored in a cached config file. + +> Listeners are created and attached when the `EventManager` is first resolved from the container, not when an event is triggered. Do not inject `EventManagerInterface` into a listener's constructor, since that creates a circular dependency. + +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(User $user) + { + $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])); + } +} + +``` + +> 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): void + { + $this->listeners[] = $events->attach(UserEvent::EVENTS_POST_UPDATE, [$this, 'onUserPostUpdateSecond'], 1); // will run second + $this->listeners[] = $events->attach(UserEvent::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 callbacks will run one after another. +But because we provide priority, the second attachment will run first because it 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 with 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 return 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(User $user) + { + /** @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 the first executed listener has returned + $result->last(); // what the last executed listener has returned + } +} +``` + +## Exceptions + +Invalid listener configuration throws `Dot\Event\Exception\RuntimeException`. It extends `\RuntimeException` and implements `Dot\Event\Exception\ExceptionInterface`, so you can catch any of the three. diff --git a/mkdocs.yml b/mkdocs.yml index 70eac50..6605fcf 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,11 +2,17 @@ docs_dir: docs/book site_dir: docs/html extra: project: Packages - current_version: v4 + current_version: v5 versions: + - v5 - v4 nav: - Home: index.md + - v5: + - Overview: v5/overview.md + - Installation: v5/installation.md + - Configuration: v5/configuration.md + - Usage: v5/usage.md - v4: - Overview: v4/overview.md - Installation: v4/installation.md diff --git a/phpstan.neon b/phpstan.neon index 349be25..08d6acd 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,7 @@ includes: - vendor/phpstan/phpstan-phpunit/extension.neon parameters: - level: 5 + level: 8 paths: - src - test diff --git a/phpunit.xml b/phpunit.xml index c2ec941..8fe4ecf 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,11 +1,10 @@ - + ./test - ./src diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index c66b9fb..c0ae545 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -10,6 +10,9 @@ class ConfigProvider { + /** + * @return array + */ public function __invoke(): array { return [ @@ -18,6 +21,9 @@ public function __invoke(): array ]; } + /** + * @return array> + */ public function getDependencyConfig(): array { return [ diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php new file mode 100644 index 0000000..ef4941c --- /dev/null +++ b/src/Exception/ExceptionInterface.php @@ -0,0 +1,11 @@ +has($listener)) { - $listener = $container->get($listener); - } - - if (is_string($listener) && class_exists($listener)) { - $listener = new $listener(); + private function getListener(ContainerInterface $container, mixed $listener): DotEventListenerInterface + { + if (is_string($listener)) { + if ($container->has($listener)) { + $listener = $container->get($listener); + } elseif (class_exists($listener)) { + trigger_error( + sprintf( + 'Instantiating event listener "%s" without a container entry is deprecated;' + . ' register it in the container. This fallback will be removed in the next major version.', + $listener + ), + E_USER_DEPRECATED + ); + $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) + get_debug_type($listener) )); } diff --git a/test/Asset/TestListener.php b/test/Asset/TestListener.php new file mode 100644 index 0000000..968d229 --- /dev/null +++ b/test/Asset/TestListener.php @@ -0,0 +1,33 @@ +listeners[] = $events->attach(self::EVENT_NAME, [$this, 'onTestEvent'], $priority); + } + + /** + * @param EventInterface> $event + */ + public function onTestEvent(EventInterface $event): void + { + $event->setParam('handled', true); + } +} diff --git a/test/ConfigProviderTest.php b/test/ConfigProviderTest.php index 16657b5..e61fd0b 100644 --- a/test/ConfigProviderTest.php +++ b/test/ConfigProviderTest.php @@ -5,15 +5,18 @@ namespace DotTest\Event; use Dot\Event\ConfigProvider; +use Dot\Event\Factory\EventManagerFactory; use Laminas\EventManager\EventManager; use Laminas\EventManager\EventManagerInterface; +use Laminas\ServiceManager\ServiceManager; use PHPUnit\Framework\TestCase; class ConfigProviderTest extends TestCase { + /** @var array */ protected array $config; - protected function setup(): void + protected function setUp(): void { $this->config = (new ConfigProvider())(); } @@ -22,17 +25,37 @@ public function testHasDependencies(): void { $this->assertArrayHasKey('dependencies', $this->config); $this->assertArrayHasKey('dot-event', $this->config); + $this->assertSame([], $this->config['dot-event']); } public function testDependenciesHasFactories(): void { $this->assertArrayHasKey('factories', $this->config['dependencies']); $this->assertArrayHasKey(EventManager::class, $this->config['dependencies']['factories']); + $this->assertSame( + EventManagerFactory::class, + $this->config['dependencies']['factories'][EventManager::class] + ); } public function testDependenciesHasAliases(): void { $this->assertArrayHasKey('aliases', $this->config['dependencies']); $this->assertArrayHasKey(EventManagerInterface::class, $this->config['dependencies']['aliases']); + $this->assertSame( + EventManager::class, + $this->config['dependencies']['aliases'][EventManagerInterface::class] + ); + } + + public function testEventManagerResolvesFromServiceManager(): void + { + $container = new ServiceManager($this->config['dependencies']); + $container->setService('config', ['dot-event' => []]); + + $eventManager = $container->get(EventManagerInterface::class); + + $this->assertInstanceOf(EventManager::class, $eventManager); + $this->assertSame($eventManager, $container->get(EventManager::class)); } } diff --git a/test/EventTest.php b/test/EventTest.php index 6f687d7..e09cdd1 100644 --- a/test/EventTest.php +++ b/test/EventTest.php @@ -11,8 +11,13 @@ class EventTest extends TestCase { public function testEventCreation(): void { - $event = new Event('testEvent', $this); - $this->assertSame(Event::class, $event::class); + $params = ['user' => 'dotkernel']; + $event = new Event('testEvent', $this, $params); + + $this->assertSame('testEvent', $event->getName()); + $this->assertSame($this, $event->getTarget()); + $this->assertSame($params, $event->getParams()); + $this->assertSame('dotkernel', $event->getParam('user')); } public function testEventPropagation(): void diff --git a/test/Exception/RuntimeExceptionTest.php b/test/Exception/RuntimeExceptionTest.php new file mode 100644 index 0000000..9ca3a31 --- /dev/null +++ b/test/Exception/RuntimeExceptionTest.php @@ -0,0 +1,25 @@ +assertSame(\RuntimeException::class, get_parent_class(RuntimeException::class)); + } + + public function testImplementsPackageExceptionInterface(): void + { + $this->assertContains(ExceptionInterface::class, class_implements(RuntimeException::class)); + } +} diff --git a/test/Factory/EventManagerFactoryTest.php b/test/Factory/EventManagerFactoryTest.php index 4d8a5f5..597d62a 100644 --- a/test/Factory/EventManagerFactoryTest.php +++ b/test/Factory/EventManagerFactoryTest.php @@ -4,24 +4,36 @@ namespace DotTest\Event\Factory; +use Dot\Event\DotEventListenerInterface; +use Dot\Event\Event; +use Dot\Event\Exception\RuntimeException; use Dot\Event\Factory\EventManagerFactory; +use DotTest\Event\Asset\TestListener; use Laminas\EventManager\EventManager; +use PHPUnit\Framework\Attributes\DataProvider; 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; +use stdClass; + +use function restore_error_handler; +use function set_error_handler; +use function sprintf; + +use const E_USER_DEPRECATED; class EventManagerFactoryTest extends TestCase { private EventManagerFactory $eventManagerFactory; - private ContainerInterface|MockObject $container; + private ContainerInterface&MockObject $container; /** * @throws Exception */ - public function setUp(): void + protected function setUp(): void { $this->eventManagerFactory = new EventManagerFactory(); $this->container = $this->createMock(ContainerInterface::class); @@ -33,14 +45,127 @@ public function setUp(): void */ public function testInvokeEventManager(): void { - $container = $this->container; - - $container->expects($this->once()) + $this->container->expects($this->once()) ->method('get') ->with('config') ->willReturn([]); - $factory = $this->eventManagerFactory; - $eventManager = $factory($container); + + $eventManager = ($this->eventManagerFactory)($this->container); + $this->assertSame(EventManager::class, $eventManager::class); } + + public function testMissingDotEventConfigKey(): void + { + $this->container->method('get') + ->with('config') + ->willReturn(['other' => [TestListener::class]]); + + $eventManager = ($this->eventManagerFactory)($this->container); + $event = new Event(TestListener::EVENT_NAME); + $eventManager->triggerEvent($event); + + $this->assertNull($event->getParam('handled')); + } + + public function testListenerResolvedFromContainerIsAttached(): void + { + $this->container->method('has') + ->with(TestListener::class) + ->willReturn(true); + $this->container->method('get') + ->willReturnMap([ + ['config', ['dot-event' => [TestListener::class]]], + [TestListener::class, new TestListener()], + ]); + + $eventManager = ($this->eventManagerFactory)($this->container); + $event = new Event(TestListener::EVENT_NAME); + $eventManager->triggerEvent($event); + + $this->assertTrue($event->getParam('handled')); + } + + public function testListenerInstanceInConfigIsAttached(): void + { + $this->container->method('get') + ->with('config') + ->willReturn(['dot-event' => [new TestListener()]]); + + $eventManager = ($this->eventManagerFactory)($this->container); + $event = new Event(TestListener::EVENT_NAME); + $eventManager->triggerEvent($event); + + $this->assertTrue($event->getParam('handled')); + } + + public function testListenerInstantiatedFromClassNameIsDeprecated(): void + { + $this->container->method('has') + ->with(TestListener::class) + ->willReturn(false); + $this->container->method('get') + ->with('config') + ->willReturn(['dot-event' => [TestListener::class]]); + + $deprecations = []; + set_error_handler( + static function (int $errno, string $errstr) use (&$deprecations): bool { + $deprecations[] = $errstr; + + return true; + }, + E_USER_DEPRECATED + ); + + try { + $eventManager = ($this->eventManagerFactory)($this->container); + } finally { + restore_error_handler(); + } + + $this->assertCount(1, $deprecations); + $this->assertStringContainsString(TestListener::class, $deprecations[0]); + $this->assertStringContainsString('deprecated', $deprecations[0]); + + $event = new Event(TestListener::EVENT_NAME); + $eventManager->triggerEvent($event); + + $this->assertTrue($event->getParam('handled')); + } + + #[DataProvider('invalidListenerProvider')] + public function testInvalidListenerThrows(mixed $listener, string $provided): void + { + $this->container->method('get') + ->with('config') + ->willReturn(['dot-event' => [$listener]]); + + try { + ($this->eventManagerFactory)($this->container); + $this->fail('Expected an exception for an invalid listener'); + } catch (RuntimeException $exception) { + $this->assertSame( + sprintf( + 'Event listener must be an instance of %s, but %s was provided', + DotEventListenerInterface::class, + $provided + ), + $exception->getMessage() + ); + } + } + + /** + * @return array + */ + public static function invalidListenerProvider(): array + { + return [ + 'unknown class name' => ['Not\\A\\Listener', 'string'], + 'integer' => [123, 'int'], + 'array' => [[], 'array'], + 'object' => [new stdClass(), 'stdClass'], + ]; + } } From 3b1603b8170cb1eeae03f351b9c0ae90aa8bb341 Mon Sep 17 00:00:00 2001 From: bidi Date: Mon, 2 Nov 2026 13:12:17 +0200 Subject: [PATCH 2/3] updated based on review Signed-off-by: bidi --- .github/workflows/codecov.yml | 6 +++--- .github/workflows/static-analysis.yml | 4 ++-- .laminas-ci.json | 3 +++ README.md | 2 +- composer.json | 2 +- docs/book/v5/overview.md | 2 +- phpstan.neon | 2 +- 7 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index be82fcc..9652006 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -36,7 +36,7 @@ jobs: run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - name: Cache dependencies installed with composer - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ${{ env.COMPOSER_CACHE_DIR }} key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} @@ -50,6 +50,6 @@ jobs: run: vendor/bin/phpunit --colors=always --coverage-clover clover.xml - name: Send code coverage report to Codecov.io - uses: codecov/codecov-action@v5 + uses: codecov/codecov-action@v7 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 0087b3d..e716528 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v7 - name: Install PHP uses: shivammathur/setup-php@v2 @@ -36,7 +36,7 @@ jobs: run: echo "COMPOSER_CACHE_DIR=$(composer config cache-dir)" >> $GITHUB_ENV - name: Cache dependencies installed with composer - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ${{ env.COMPOSER_CACHE_DIR }} key: php${{ matrix.php }}-composer-${{ hashFiles('**/composer.json') }} diff --git a/.laminas-ci.json b/.laminas-ci.json index 29216fa..e827682 100644 --- a/.laminas-ci.json +++ b/.laminas-ci.json @@ -1,3 +1,6 @@ { + "ignore_php_platform_requirements": { + "8.6": true + }, "backwardCompatibilityCheck": true } diff --git a/README.md b/README.md index de9f8a8..bde235e 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Documentation is available at: https://docs.dotkernel.org/dot-event/. ## Badges ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-event) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-event) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-event/5.0.0) [![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) diff --git a/composer.json b/composer.json index f97a76a..0ac5be4 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "laminas/laminas-coding-standard": "^3.0", "phpstan/phpstan": "^2.1", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^11.5 || ^12.0" + "phpunit/phpunit": "^10.2 || ^11.5 || ^12.0" }, "autoload": { "psr-4": { diff --git a/docs/book/v5/overview.md b/docs/book/v5/overview.md index a2228c0..287ed1b 100644 --- a/docs/book/v5/overview.md +++ b/docs/book/v5/overview.md @@ -7,7 +7,7 @@ Dotkernel's event manager package. ## Badges ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-event) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-event) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-event/5.0.0) [![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) diff --git a/phpstan.neon b/phpstan.neon index 08d6acd..349be25 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,7 @@ includes: - vendor/phpstan/phpstan-phpunit/extension.neon parameters: - level: 8 + level: 5 paths: - src - test From 85031ddad0bfa256252ccb41165f7aedd158c48f Mon Sep 17 00:00:00 2001 From: bidi Date: Mon, 2 Nov 2026 13:34:25 +0200 Subject: [PATCH 3/3] updatd composer, phpstan config Signed-off-by: bidi --- composer.json | 2 +- phpstan.neon | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 0ac5be4..721f761 100644 --- a/composer.json +++ b/composer.json @@ -25,7 +25,7 @@ "laminas/laminas-coding-standard": "^3.0", "phpstan/phpstan": "^2.1", "phpstan/phpstan-phpunit": "^2.0", - "phpunit/phpunit": "^10.2 || ^11.5 || ^12.0" + "phpunit/phpunit": "^12.0" }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon index 349be25..08d6acd 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,7 @@ includes: - vendor/phpstan/phpstan-phpunit/extension.neon parameters: - level: 5 + level: 8 paths: - src - test