From 074e94346454456177f8339309289e2817a4baa0 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Mon, 13 Jan 2025 17:36:07 +0200 Subject: [PATCH 1/3] Issue #41: Added support for Signed-off-by: alexmerlin --- .laminas-ci.json | 6 ++++++ README.md | 8 +++----- composer.json | 19 ++++++++++--------- docs/book/index.md | 2 +- src/AbstractController.php | 7 +++++-- src/Plugin/PluginManager.php | 20 ++++++++++++++++++-- test/AbstractControllerTest.php | 4 ++-- 7 files changed, 45 insertions(+), 21 deletions(-) create mode 100644 .laminas-ci.json mode change 100644 => 120000 docs/book/index.md diff --git a/.laminas-ci.json b/.laminas-ci.json new file mode 100644 index 0000000..82cd446 --- /dev/null +++ b/.laminas-ci.json @@ -0,0 +1,6 @@ +{ + "ignore_php_platform_requirements": { + "8.4": true + }, + "backwardCompatibilityCheck": true +} diff --git a/README.md b/README.md index d647272..8bc5e41 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is DotKernel's controller package that can be use like middleware inside Do It provides base classes for action based controllers similar to Laminas controller component. It is more lightweight though, but supports controller plugins and event listeners ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-controller) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-controller/3.4.3) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-controller/3.6.0) [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-controller)](https://github.com/dotkernel/dot-controller/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-controller)](https://github.com/dotkernel/dot-controller/network) @@ -14,14 +14,12 @@ It provides base classes for action based controllers similar to Laminas control [![Build Static](https://github.com/dotkernel/dot-controller/actions/workflows/static-analysis.yml/badge.svg?branch=3.0)](https://github.com/dotkernel/dot-controller/actions/workflows/static-analysis.yml) [![codecov](https://codecov.io/gh/dotkernel/dot-controller/graph/badge.svg?token=VUBG5LM4CK)](https://codecov.io/gh/dotkernel/dot-controller) -[![SymfonyInsight](https://insight.symfony.com/projects/c4aac671-40d7-4590-b1fa-b3e46a1e3f43/big.svg)](https://insight.symfony.com/projects/c4aac671-40d7-4590-b1fa-b3e46a1e3f43) - ## Installation Install `dot-controller` by executing the following Composer command: -```bash -$ composer require dotkernel/dot-controller +```shell +composer require dotkernel/dot-controller ``` ## Usage diff --git a/composer.json b/composer.json index ebab335..1e3f44f 100644 --- a/composer.json +++ b/composer.json @@ -18,18 +18,18 @@ } ], "require": { - "php": "~8.1.0 || ~8.2.0 || ~8.3.0", - "psr/http-message": "^1.0 || ^2.0", - "laminas/laminas-servicemanager": "^3.11.2", - "dotkernel/dot-event": "^3.2.0", + "php": "~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", + "dotkernel/dot-event": "^4.0.0", + "laminas/laminas-servicemanager": "^4.0", + "mezzio/mezzio-helpers": "^5.8.0", "mezzio/mezzio-template": "^2.4.0", - "mezzio/mezzio-helpers": "^5.8.0" + "psr/http-message": "^1.0 || ^2.0" }, "require-dev": { + "laminas/laminas-coding-standard": "^3.0", + "laminas/laminas-diactoros": "^3.0", "phpunit/phpunit": "^10.2", - "vimeo/psalm": "^5.13", - "laminas/laminas-coding-standard": "^2.5", - "laminas/laminas-diactoros": "^3.0" + "vimeo/psalm": "^5.13" }, "autoload": { "psr-4": { @@ -44,7 +44,8 @@ "scripts": { "check": [ "@cs-check", - "@test" + "@test", + "@static-analysis" ], "cs-check": "phpcs", "cs-fix": "phpcbf", diff --git a/docs/book/index.md b/docs/book/index.md deleted file mode 100644 index ae42a26..0000000 --- a/docs/book/index.md +++ /dev/null @@ -1 +0,0 @@ -../../README.md diff --git a/docs/book/index.md b/docs/book/index.md new file mode 120000 index 0000000..fe84005 --- /dev/null +++ b/docs/book/index.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/src/AbstractController.php b/src/AbstractController.php index edad384..d1c810d 100644 --- a/src/AbstractController.php +++ b/src/AbstractController.php @@ -10,6 +10,7 @@ use Dot\Controller\Plugin\PluginManager; use Dot\Controller\Plugin\PluginManagerAwareInterface; use Laminas\EventManager\EventManagerAwareInterface; +use Psr\Container\ContainerExceptionInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; @@ -86,10 +87,12 @@ public function __call(string $method, array $params): mixed /** * Get plugin instance + * + * @throws ContainerExceptionInterface */ - public function plugin(string $name, array $options = []): PluginInterface|callable + public function plugin(string $name, array $options = []): PluginInterface|callable|null { - return $this->getPluginManager()->get($name, $options); + return $this->getPluginManager()?->build($name, $options); } public function getPluginManager(): ?PluginManager diff --git a/src/Plugin/PluginManager.php b/src/Plugin/PluginManager.php index a21ce63..32d1aff 100644 --- a/src/Plugin/PluginManager.php +++ b/src/Plugin/PluginManager.php @@ -5,6 +5,11 @@ namespace Dot\Controller\Plugin; use Laminas\ServiceManager\AbstractPluginManager; +use Laminas\ServiceManager\Exception\InvalidServiceException; + +use function gettype; +use function is_object; +use function sprintf; /** * @template InstanceType @@ -12,6 +17,17 @@ */ class PluginManager extends AbstractPluginManager { - /** @var string $instanceOf */ - protected $instanceOf = PluginInterface::class; + protected string $instanceOf = PluginInterface::class; + + public function validate(mixed $instance): void + { + if (! $instance instanceof $this->instanceOf) { + throw new InvalidServiceException(sprintf( + '%s can only create instances of %s; %s is invalid', + static::class, + $this->instanceOf, + is_object($instance) ? $instance::class : gettype($instance) + )); + } + } } diff --git a/test/AbstractControllerTest.php b/test/AbstractControllerTest.php index a1cff57..3498e42 100644 --- a/test/AbstractControllerTest.php +++ b/test/AbstractControllerTest.php @@ -83,7 +83,7 @@ public function testDebug(): void public function testCallPlugin(): void { $this->pluginManager->expects($this->once()) - ->method('get') + ->method('build') ->with('somePlugin') ->willReturn($this->plugin); @@ -111,7 +111,7 @@ public function testCallCallablePlugin(): void }; $this->pluginManager->expects($this->once()) - ->method('get') + ->method('build') ->with('callablePlugin') ->willReturn($mockCallablePlugin); From ced9967b08106b16d3c346e07012686405a3c7b1 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Mon, 13 Jan 2025 17:42:14 +0200 Subject: [PATCH 2/3] Updated version numbers in README.md Signed-off-by: alexmerlin --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8bc5e41..3c14bbd 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ This is DotKernel's controller package that can be use like middleware inside Do It provides base classes for action based controllers similar to Laminas controller component. It is more lightweight though, but supports controller plugins and event listeners ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-controller) -![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-controller/3.6.0) +![PHP from Packagist (specify version)](https://img.shields.io/packagist/php-v/dotkernel/dot-controller/4.0.0) [![GitHub issues](https://img.shields.io/github/issues/dotkernel/dot-controller)](https://github.com/dotkernel/dot-controller/issues) [![GitHub forks](https://img.shields.io/github/forks/dotkernel/dot-controller)](https://github.com/dotkernel/dot-controller/network) [![GitHub stars](https://img.shields.io/github/stars/dotkernel/dot-controller)](https://github.com/dotkernel/dot-controller/stargazers) -[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-controller)](https://github.com/dotkernel/dot-controller/blob/3.0/LICENSE.md) +[![GitHub license](https://img.shields.io/github/license/dotkernel/dot-controller)](https://github.com/dotkernel/dot-controller/blob/4.0/LICENSE.md) [![Build Static](https://github.com/dotkernel/dot-controller/actions/workflows/static-analysis.yml/badge.svg?branch=3.0)](https://github.com/dotkernel/dot-controller/actions/workflows/static-analysis.yml) [![codecov](https://codecov.io/gh/dotkernel/dot-controller/graph/badge.svg?token=VUBG5LM4CK)](https://codecov.io/gh/dotkernel/dot-controller) From e1a0b5631931cce9ae4135e6fc333075d35ac062 Mon Sep 17 00:00:00 2001 From: alexmerlin Date: Tue, 14 Jan 2025 08:08:28 +0200 Subject: [PATCH 3/3] Added v4 docs. Made v4 the default version in the docs. Signed-off-by: alexmerlin --- README.md | 12 +-- docs/book/v4/configuration.md | 5 + docs/book/v4/events.md | 178 ++++++++++++++++++++++++++++++++++ docs/book/v4/installation.md | 5 + docs/book/v4/overview.md | 4 + docs/book/v4/plugins.md | 98 +++++++++++++++++++ docs/book/v4/usage.md | 60 ++++++++++++ mkdocs.yml | 12 ++- 8 files changed, 366 insertions(+), 8 deletions(-) create mode 100644 docs/book/v4/configuration.md create mode 100644 docs/book/v4/events.md create mode 100644 docs/book/v4/installation.md create mode 100644 docs/book/v4/overview.md create mode 100644 docs/book/v4/plugins.md create mode 100644 docs/book/v4/usage.md diff --git a/README.md b/README.md index 3c14bbd..47774e2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # dot-controller -This is DotKernel's controller package that can be use like middleware inside DotKernel or Mezzio application. +This is Dotkernel's controller package that can be use like middleware inside Dotkernel or Mezzio application. It provides base classes for action based controllers similar to Laminas controller component. It is more lightweight though, but supports controller plugins and event listeners ![OSS Lifecycle](https://img.shields.io/osslifecycle/dotkernel/dot-controller) @@ -30,14 +30,14 @@ Middleware controllers act as a handler for multiple routes. Some conventions we - action parameter value is converted to a method name inside the controller. Underscore, dot and line characters are removed and the action name is converted to camel-case suffixed by the string `Action`. For example a route and action pair like `/user/forgot-password` will be converted to method `forgotPasswordAction`. - the default action value, if not present in the URI is `index`, so you should always define an `indexAction` within your controllers for displaying a default page or redirecting. -In order to create your action based controllers, you must extend the abstract class `DotKernel\DotController\AbstractActionController` +In order to create your action based controllers, you must extend the abstract class `Dot\Controller\AbstractActionController`. ### Example -Creating a UserController with default action and a register action. Will handle routes `/user` and `/user/register` +Creating a UserController with default action and a register action. Will handle routes `/user` and `/user/register`. ```php -use DotKernel\DotController\AbstractActionController; +use Dot\Controller\AbstractActionController; class UserController extends AbstractActionController { @@ -56,7 +56,7 @@ class UserController extends AbstractActionController Then register this controller as a routed middleware in file `RoutesDelegator.php` just like a regular middleware. ```php -//Example from a DotKernel RoutesDelegator +//Example from a RoutesDelegator $app->route( '/user[/{action}]', UserController::class, @@ -72,5 +72,5 @@ Use case: You have defined a controller inside some package, with default action - create your own controller, independent of the package's controller which adds more actions - Mezzio lets you define an array of middleware for a route, so you can register this controller before the package's controller -Now when a request for this route comes in, your controller will run first. DotKernel controllers are designed to ignore requests that cannot be matched to one of its methods, so if no action matches, it will call the next middleware, in our case, the second controller. +Now when a request for this route comes in, your controller will run first. Dotkernel controllers are designed to ignore requests that cannot be matched to one of its methods, so if no action matches, it will call the next middleware, in our case, the second controller. If this is the last controller, and action does not match here, it will go to the default 404 Not found page(handled by NotFoundDelegate) diff --git a/docs/book/v4/configuration.md b/docs/book/v4/configuration.md new file mode 100644 index 0000000..344644b --- /dev/null +++ b/docs/book/v4/configuration.md @@ -0,0 +1,5 @@ +# Configuration + +After installation, the package can be used immediately but if you want to use all features of the package, like plugins and events you need to register the `ConfigProvider` in your project by adding the below line to your configuration aggregator (usually: `config/config.php`): + + \Dot\Controller\ConfigProvider::class diff --git a/docs/book/v4/events.md b/docs/book/v4/events.md new file mode 100644 index 0000000..d889f3a --- /dev/null +++ b/docs/book/v4/events.md @@ -0,0 +1,178 @@ +# Events + +DotKernel's controller package supports events and those events can be of 2 types: global events (middleware-like) or manually dispatch events. + +## Getting started + +- Every event listener that is triggered from a controller +needs to implement `Dot\Controller\Event\ControllerEventListenerInterface` which actually extends `Laminas\EventManager\ListenerAggregateInterface`. +- You can add the trait `Dot\Controller\Event\ControllerEventListenerTrait` to override the method of the interface. +- Every event listener needs to be registered under the `['dot_controller']['event_listenenrs]` key in the ConfigProvider, and every key must be the class that you want to attach the events + +## Usage + +The events in a controller can be done in 2 different ways, a global way where an event is attached automatically to all the controllers action and works in the same way the middlewares works +or a manually dispatchable way, where you can define to which controller the events is attached, and you can trigger the event where you want. + +For our example we have a UserController with some methods in it + +```php +use DotKernel\DotController\AbstractActionController; + +class UserController extends AbstractActionController +{ + public function indexAction() + { + //... + } + + public function registerAction() + { + //... + } + + // post method for updating the user + public function updateAction() + { + + } +} +``` + +### Example 1 - Global way + +First we will create the event listener + +```php +use Dot\Controller\Event\ControllerEvent; +use Dot\Controller\Event\ControllerEventListenerInterface; +use Dot\Controller\Event\ControllerEventListenerTrait; + +// for the logger we assume you will use your own logger and inject it + +class UserUpdatedListener implements ControllerEventListenerInterface +{ + use ControllerEventListenerTrait; + + public function onBeforeDispatch(ControllerEvent $event): void + { + $this->logger->info('on before dispatch'); + } + + public function onAfterDispatch(ControllerEvent $event): void + { + $this->logger->info('on after dispatch'); + } + +} +``` + +We register the event listener in the configuration key + +```php +'dot_controller' => [ + 'event_listeners' => [ + AccountController::class => [ + UserUpdatedListener::class, + ] + ] +] +``` + +As you can assume, `onBeforeDispatch` is triggered right before the controller is dispatched, and `onAfterDispatch` right +after the controller is dispatched. + +With this it doesn't matter what action is accessed, the event it will run before and after the action. + +In addition, you can make use of the `event` variable to access information about the event. + +For example: + +```php +// UserUpdatedListener +public function onAfterDispatch(ControllerEvent $e): void + { + $method = $e->getTarget()->getRequest()->getMethod(); + $action = $e->getParams()['method']; + if ($method == 'POST' && $action == 'updateAction') { + $this->logger->info('this will trigger '); + + } + } +``` + +So every time the `updateAction` is accessed and the method is post, +right after the action is dispatched, we can log that the user was updated. + +We can use the `onBeforeDispatch` in the same way, to log right before the user is updated. + +### Example 2 - Manually triggered way + +```php +use Dot\Controller\Event\ControllerEvent; +use Dot\Controller\Event\ControllerEventListenerInterface; +use Dot\Controller\Event\ControllerEventListenerTrait; + +// for the logger we assume you will use your own logger and inject it + +class UserUpdatedListener implements ControllerEventListenerInterface +{ + use ControllerEventListenerTrait; + + public function attach(EventManagerInterface $events, $priority = 1): void + { + $this->listeners[] = $events->attach( + 'user.profile.update', + [$this, 'userProfileUpdated'], + $priority + ); + } + + public function userProfileUpdated(ControllerEvent $event): void + { + $this->logger->info('User profile updated'); + } + +} +``` + +The `attach` method is from the `ListenerAggregateInterface` which `ControllerEventListenerTrait` +already is overriding it so can be used in a global way with `onBeforeDispatch` and `onAfterDispatch` +methods, but we can make our custom event and bind it to our method. + +In this case we create attach an event called `user.profile.update` and bind it to the `userProfileUpdated` method. + +Next we need to register the event + +```php +'dot_controller' => [ + 'event_listeners' => [ + AccountController::class => [ + 'user.profile.update' => UserUpdatedListener::class + ] + ] +] +``` + +Now you can manually trigger the event from the controller using build in `dispatchEvent` method. + +```php +// UserController +// post method for updating the user +public function updateAction() +{ + // logic + $this->dispatchEvent('user.profile.update', ['user' => $user]); + +} +``` + +As you can see we attach the `user` key to the parameters, so we can actually access it. + +```php + public function userProfileUpdated(ControllerEvent $event): void + { + $user = $event->getParams()['user']; + $this->logger->info('User profile updated', $user->toArray()); + } +``` diff --git a/docs/book/v4/installation.md b/docs/book/v4/installation.md new file mode 100644 index 0000000..8c71aa1 --- /dev/null +++ b/docs/book/v4/installation.md @@ -0,0 +1,5 @@ +# Installation + +Install `dot-controller` by executing the following Composer command: + + composer require dotkernel/dot-controller diff --git a/docs/book/v4/overview.md b/docs/book/v4/overview.md new file mode 100644 index 0000000..1d154ec --- /dev/null +++ b/docs/book/v4/overview.md @@ -0,0 +1,4 @@ +# Overview + +`dot-controller` is DotKernel's controller package that can be use like middleware inside DotKernel or Mezzio application. +It provides base classes for action based controllers similar to Laminas controller component. It is more lightweight though, but supports controller plugins and event listeners diff --git a/docs/book/v4/plugins.md b/docs/book/v4/plugins.md new file mode 100644 index 0000000..bc94d69 --- /dev/null +++ b/docs/book/v4/plugins.md @@ -0,0 +1,98 @@ +# Plugins + +DotKernel's controller support plugins, much like controllers in a Laminas Applications. +The package comes packed with a few built in plugins, but you can extend controller functionality with your own plugins. + +## Usage + +Any controller plugins must implement `Dot\Controller\Plugin\PluginInterface`. +You need to create a factory in addition to the plugin and register it +under the `['dot_controller']['plugin_manager']['factories']` with the plugin name. + +Once registered, a plugin can be directly accessed in any controller, +by calling a method with the plugin's name (the service name or the key at which the plugin is registered inside the manager) + +Controller plugins offer the advantage of globally accessible functionality +in any controller without to manually inject dependencies. +Plugins should be used for functions that are common to any controller. +Do not clutter controller's code with unnecessary plugins. + +### Example + +First we create our desired plugin, for our example a string helper + +```php +class StringPlugin implements PluginInterface +{ + + // any method inside the plugin needs to be public if you want to access it from a controller + public function toUpper(string $string): string + { + return strtoupper($string); + } +} +``` + +We create a factory for the plugin + +```php +use Psr\Container\ContainerInterface; + +class StringPluginFactory +{ + + public function __invoke(ContainerInterface $container): StringPlugin + { + return new StringPlugin(); + } + +} +``` + +Register the factory under the `['dot_controller']['plugin_manager']['factories']` key. + +```php +'dot_controller' => [ + 'plugin_manager' => [ + 'factories' => [ + 'string' => StringPluginFactory::class + ] + ] +] +``` + +You don't need to register the plugin factory to a regular dependencies in a configuration +because `AbstractPluginManager` actually extends `ServiceManager` + +Access it in a controller. + +```php +//inside a controller +$this->string(); // will return the StringPlugin class, so you can call any public method from it +$this->string()->toUpper("test") // will return TEST +``` + +## Build-in plugins + +The package comes in with 2 default plugins ``template`` and `url`. You can use +them in the same way as our example above. + +- `url` - the plugin is an instance of `Mezzio\Helper\UrlHelper` + +```php + //in a controller action + /** @var UrlHelper $url */ + $url = $this->url(); + echo $url->generate('account', ['action' => 'foo', 'hash' => 'bar']) +``` + +- `template` - the plugin is an instance of `Mezzio\Template\TemplateRendererInterface` + +```php + // in a controller action + return new HtmlResponse( + $this->template->render('page::home', [ + 'foo' => 'bar' + ]) + ); +``` diff --git a/docs/book/v4/usage.md b/docs/book/v4/usage.md new file mode 100644 index 0000000..b5aeffa --- /dev/null +++ b/docs/book/v4/usage.md @@ -0,0 +1,60 @@ +# Usage + +Middleware controllers act as a handler for multiple routes. Some conventions were made: + +- register controllers in the routes array just like any mezzio middleware. The requirement is that you should define an `action` route parameter(possibly optional) anywhere inside the route(e.g `/user[/{action}]`) +- action parameter value is converted to a method name inside the controller. Underscore, dot and line characters are removed and the action name is converted to camel-case suffixed by the string `Action`. For example a route and action pair like `/user/forgot-password` will be converted to method `forgotPasswordAction`. +- the default action value, if not present in the URI is `index`, so you should always define an `indexAction` within your controllers for displaying a default page or redirecting. + +In order to create your action based controllers, you must extend the abstract class `DotKernel\DotController\AbstractActionController` + +## Example + +Creating a UserController with default action and a register action. Will handle routes `/user` and `/user/register` + +```php +use DotKernel\DotController\AbstractActionController; + +class UserController extends AbstractActionController +{ + public function indexAction() + { + //... + } + + public function registerAction() + { + //... + } +} +``` + +Then register this controller as a routed middleware in file `RoutesDelegator.php` just like a regular middleware. + +```php +//Example from a DotKernel RoutesDelegator +$app->route( + '/user[/{action}]', + UserController::class, + [RequestMethodInterface::METHOD_GET, RequestMethodInterface::METHOD_POST], + 'user' +); +``` + +### Multiple controllers for the same route + +Use case: You have defined a controller inside some package, with default actions. You want to add actions that fall into the same controller name(or route name more exactly). You want to do this without extending the controller provided by the package. In this case you can do the following + +- create your own controller, independent of the package's controller which adds more actions +- Mezzio lets you define an array of middleware for a route, so you can register this controller before the package's controller + +Now when a request for this route comes in, your controller will run first. DotKernel controllers are designed to ignore requests that cannot be matched to one of its methods, so if no action matches, it will call the next middleware, in our case, the second controller. +If this is the last controller, and action does not match here, it will go to the default 404 Not found page(handled by NotFoundDelegate) + +## Plugins + +- [Plugins](plugins.md) + +## Events + +- [Events](events.md) diff --git a/mkdocs.yml b/mkdocs.yml index f8ab813..80b3c25 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,11 +2,19 @@ docs_dir: docs/book site_dir: docs/html extra: project: Packages - current_version: v3 + current_version: v4 versions: + - v4 - v3 nav: - Home: index.md + - v4: + - Overview: v4/overview.md + - Installation: v4/installation.md + - Configuration: v4/configuration.md + - Usage: v4/usage.md + - Plugins: v4/plugins.md + - Events: v4/events.md - v3: - Overview: v3/overview.md - Installation: v3/installation.md @@ -15,7 +23,7 @@ nav: - Plugins: v3/plugins.md - Events: v3/events.md site_name: dot-controller -site_description: "DotKernel's controller package" +site_description: "Dotkernel's controller package" repo_url: "https://github.com/dotkernel/dot-controller" plugins: - search