├── .gitattributes ├── .github └── workflows │ └── unit-tests.yml ├── .gitignore ├── .scrutinizer.yml ├── LICENSE.md ├── README.md ├── composer.json ├── config └── module.config.php ├── phpunit.xml.dist └── src ├── ConfigProvider.php ├── Controller └── Plugin │ └── TacticianCommandBusPlugin.php ├── Factory ├── CommandBusFactory.php ├── CommandHandlerMiddlewareFactory.php ├── Controller │ └── Plugin │ │ └── TacticianCommandBusPluginFactory.php ├── InMemoryLocatorFactory.php └── Plugin │ └── DoctrineTransactionFactory.php ├── Locator ├── ClassnameLaminasLocator.php ├── ClassnameLaminasLocatorFactory.php ├── LaminasLocator.php └── LaminasLocatorFactory.php └── Module.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /test export-ignore 2 | phpunit.xml export-ignore 3 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: "Unit tests" 2 | 3 | on: [ "pull_request", "push" ] 4 | 5 | jobs: 6 | unit-tests: 7 | name: "Unit tests" 8 | 9 | runs-on: ubuntu-20.04 10 | 11 | strategy: 12 | matrix: 13 | php-version: 14 | - "7.1" 15 | - "7.2" 16 | - "7.3" 17 | - "7.4" 18 | - "8.0" 19 | - "8.1" 20 | - "8.2" 21 | - "8.3" 22 | dependencies: 23 | - lowest 24 | - highest 25 | steps: 26 | - name: "Checkout" 27 | uses: "actions/checkout@v2" 28 | 29 | - name: "Caching dependencies" 30 | uses: "actions/cache@v2" 31 | with: 32 | path: | 33 | ~/.composer/cache 34 | vendor 35 | key: "php-${{ matrix.php-version }}" 36 | 37 | - name: "Installing dependencies lowest" 38 | run: "composer update --no-interaction --no-progress --prefer-lowest --prefer-stable" 39 | if: "matrix.dependencies == 'lowest'" 40 | 41 | - name: "Installing dependencies highest" 42 | run: "composer update --no-interaction --no-progress" 43 | if: matrix.dependencies != 'lowest' 44 | 45 | - name: "Unit tests" 46 | run: "vendor/bin/phpunit" 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /.idea 3 | composer.phar 4 | phpunit.xml 5 | composer.lock 6 | .phpunit.result.cache 7 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | excluded_paths: 3 | - vendor/* 4 | - config/* 5 | - test/* 6 | 7 | checks: 8 | php: 9 | remove_extra_empty_lines: true 10 | remove_php_closing_tag: true 11 | remove_trailing_whitespace: true 12 | fix_use_statements: 13 | remove_unused: true 14 | preserve_multiple: false 15 | preserve_blanklines: true 16 | order_alphabetically: true 17 | fix_php_opening_tag: true 18 | fix_linefeed: true 19 | fix_line_ending: true 20 | fix_identation_4spaces: true 21 | fix_doc_comments: true 22 | 23 | build: 24 | environment: 25 | variables: 26 | XDEBUG_MODE: coverage 27 | tests: 28 | override: 29 | - 30 | command: 'vendor/bin/phpunit --coverage-clover=clover.xml' 31 | coverage: 32 | file: 'clover.xml' 33 | format: 'php-clover' 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Michal Zukowski 2 | https://www.phpcontext.com 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tactician Laminas/Mezzio Module 2 | 3 | Wrapper module for easy use of the [Tactician](http://tactician.thephpleague.com/) Command Bus in your Laminas or Mezzio applications. 4 | 5 | [![Build Status](https://travis-ci.org/mikemix/TacticianModule.svg?branch=master)](https://travis-ci.org/mikemix/TacticianModule) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/mikemix/TacticianModule/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/mikemix/TacticianModule/?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/mikemix/TacticianModule/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/mikemix/TacticianModule/?branch=master) [![Latest Stable Version](https://poser.pugx.org/mikemix/tactician-module/v/stable)](https://packagist.org/packages/mikemix/tactician-module) [![Total Downloads](https://poser.pugx.org/mikemix/tactician-module/downloads)](https://packagist.org/packages/mikemix/tactician-module) [![License](https://poser.pugx.org/mikemix/tactician-module/license)](https://packagist.org/packages/mikemix/tactician-module) 6 | 7 | ## Installation 8 | 9 | Best install with Composer: 10 | 11 | ``` 12 | composer require mikemix/tactician-module 13 | ``` 14 | 15 | Register as Laminas Framework module inside your ```config/application.config.php``` file using `TacticianModule` name. 16 | 17 | You can also use this package as [Mezzio](https://docs.zendframework.com/zend-expressive/v3/features/modular-applications/) module by `TacticianModule\ConfigProvider` 18 | 19 | ## Using 20 | 21 | The module presents a __Controller Plugin__ called `tacticianCommandBus()` for easy use of dispatching commands. If no command object is passed to it, the CommandBus object will be returned. If you pass the command however, it will be passed over to the CommandBus and handled, and the output from the handler will be returned. 22 | 23 | You can type hint this plugin in your controller, for example: ```@method \League\Tactician\CommandBus|mixed tacticianCommandBus(object $command)```. 24 | 25 | ```php 26 | 27 | // Real life example. 28 | // Namespaces, imports, class properties skipped for brevity 29 | 30 | class LoginController extends AbstractActionController 31 | { 32 | public function indexAction() 33 | { 34 | if ($this->request->isPost()) { 35 | $this->form->setData($this->request->getPost()); 36 | 37 | if ($this->form->isValid()) { 38 | $command = new UserLoginCommand( 39 | $this->form->getLogin(), 40 | $this->form->getPassword() 41 | )); 42 | 43 | try { 44 | $this->tacticianCommandBus($command); 45 | return $this->redirect()->toRoute('home'); 46 | } catch (\Some\Kind\Of\Login\Failure $exception) { 47 | $this->flashMessenger()->addErrorMessage($exception->getMessage()); 48 | return $this->redirect()->refresh(); 49 | } 50 | } 51 | } 52 | 53 | $view = new ViewModel(); 54 | $view->setVariable('form', $this->form); 55 | $view->setTemplate('app/login/index'); 56 | 57 | return $view; 58 | } 59 | } 60 | 61 | final class UserLoginCommand 62 | { 63 | public function __construct($login, $password) 64 | { 65 | $this->login = $login; 66 | $this->password = $password; 67 | } 68 | } 69 | 70 | final class UserLoginHandler 71 | { 72 | // constructor skipped for brevity 73 | 74 | public function handle(UserLoginCommand $command) 75 | { 76 | $this->authenticationService->login($command->username, $command->password); 77 | } 78 | } 79 | ``` 80 | 81 | You can inject the `CommandBus` into yout service layer through a factory by simply requesting the `League\Tactician\CommandBus::class` from the __Container__. 82 | 83 | ## Configuring 84 | 85 | The module ships with a `LaminasLocator` and a `CommandHandlerMiddleware` and a `HandlerInflector` configured as default. If you wish to override the default locator or default command bus implementations, then simply use the `tactician` key in the merged config. 86 | 87 | ```php 88 | 'tactician' => [ 89 | 'default-extractor' => League\Tactician\Handler\CommandNameExtractor\ClassNameExtractor::class, 90 | 'default-locator' => TacticianModule\Locator\LaminasLocator::class, 91 | 'default-inflector' => League\Tactician\Handler\HandleInflector::class, 92 | 'handler-map' => [], 93 | 'middleware' => [ 94 | CommandHandlerMiddleware::class => 0, 95 | ], 96 | ], 97 | ``` 98 | 99 | `default-extractor`, `default-locator` and `default-inflector` are service manager keys to registered services. 100 | 101 | `LaminasLocator` expects handlers in the `handler-map` to be `commandName => serviceManagerKey` or `commandName => Handler_FQCN`, altough to prevent additional costly checks, use serviceManagerKey and make sure it is available as a Laminas Service. 102 | 103 | To add custom middleware to the middleware stack, add it to the `middleware` array as `ServiceName` => `priority` in which the middleware are supposed to be executed (higher the number, earlier it will execute). For example 104 | 105 | ```php 106 | // ... your module config 107 | 'tactician' => [ 108 | 'middleware' => [ 109 | YourAnotherMiddleware::class => 100, // execute early 110 | YourCustomMiddleware::class => 50, // execute last 111 | ], 112 | ], 113 | ``` 114 | 115 | ## Basic usage 116 | 117 | Basicly, all you probably will want to do, is to define the `handler-map` array in your module's configuration. For example: 118 | 119 | ```php 120 | // module.config.php file 121 | 122 | return [ 123 | // other keys 124 | 'tactician' => [ 125 | 'handler-map' => [ 126 | App\Command\SomeCommand::class => App\Handler\SomeCommandHandler::class, 127 | ], 128 | ], 129 | ]; 130 | ``` 131 | 132 | ## Plugins 133 | 134 | ### LockingMiddleware 135 | 136 | The [LockingMiddleware](http://tactician.thephpleague.com/plugins/locking-middleware/) can now be used out of the box. 137 | Simply add the `League\Tactician\Plugins\LockingMiddleware` FQCN to the TacticianModule's middleware configuration with 138 | appropriate priority. You probably want to execute it before the `CommandHandlerMiddleware`: 139 | 140 | ```php 141 | // module.config.php file 142 | 143 | return [ 144 | // other keys 145 | 'tactician' => [ 146 | 'middleware' => [ 147 | \League\Tactician\Plugins\LockingMiddleware::class => 500, 148 | ], 149 | ], 150 | ]; 151 | ``` 152 | 153 | ### TransactionMiddleware 154 | 155 | The [TransactionMiddleware](http://tactician.thephpleague.com/plugins/doctrine/) can now be used out of the box. 156 | Simply add the `League\Tactician\Doctrine\ORM\TransactionMiddleware` FQCN to the TacticianModule's middleware configuration with 157 | appropriate priority. You probably want to execute it before the `CommandHandlerMiddleware` and after the `LockingMiddleware`: 158 | 159 | ```php 160 | // module.config.php file 161 | 162 | return [ 163 | // other keys 164 | 'tactician' => [ 165 | 'middleware' => [ 166 | \League\Tactician\Doctrine\ORM\TransactionMiddleware::class => 250, 167 | ], 168 | ], 169 | ]; 170 | ``` 171 | 172 | ## Changing the Handler Locator 173 | 174 | ### ClassnameLaminasLocator 175 | 176 | This locator simply appends the word `Handler` to the command's FQCN so you don't have to define any handler map. For example, if you request command `App\Commands\LoginCommand`, locator will try to get `App\Command\LoginCommandHandler` from the Service Manager. 177 | 178 | Locator will work with FQCN's not registered in the Service Manager, altough to prevent additional costly checks, make sure the locator is registered as a invokable or factory. 179 | 180 | To change the locator from LaminasLocator to ClassnameLaminasLocator simply set it in the config: 181 | 182 | ```php 183 | // ... your module config 184 | 'tactician' => [ 185 | 'default-locator' => TacticianModule\Locator\ClassnameLaminasLocator::class, 186 | ], 187 | ``` 188 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mikemix/tactician-module", 3 | "description": "Laminas/Mezzio Module to use the League of Extraordinary Packages' Tactician library - flexible command bus implementation", 4 | "require": { 5 | "php": "^7.1 || ^8.0", 6 | "league/tactician": "^1.0", 7 | "psr/container": "^1.0 || ^2.0" 8 | }, 9 | "require-dev": { 10 | "league/tactician-doctrine": "^1.0", 11 | "phpunit/phpunit": "^7.5.20 || ^9.3.8", 12 | "doctrine/orm": "^2.5", 13 | "squizlabs/php_codesniffer": "^3.6", 14 | "laminas/laminas-mvc": "^3.1.1" 15 | }, 16 | "keywords": [ 17 | "laminas framework", 18 | "tactician", 19 | "command bus", 20 | "thephpleague" 21 | ], 22 | "license": "MIT", 23 | "authors": [ 24 | { 25 | "name": "Michal Zukowski", 26 | "homepage": "https://www.phpcontext.com" 27 | }, 28 | { 29 | "name": "Gary Hockin", 30 | "homepage": "http://hock.in", 31 | "role": "Trailblazer" 32 | }, 33 | { 34 | "name": "Witold Wasiczko", 35 | "homepage": "https://github.com/snapshotpl" 36 | } 37 | ], 38 | "autoload": { 39 | "psr-4": { 40 | "TacticianModule\\": "src/" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "TacticianModuleTest\\": "test/" 46 | } 47 | }, 48 | "config": { 49 | "allow-plugins": { 50 | "composer/package-versions-deprecated": true 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config/module.config.php: -------------------------------------------------------------------------------- 1 | [ 22 | 'invokables' => [ 23 | ClassNameExtractor::class => ClassNameExtractor::class, 24 | HandleInflector::class => HandleInflector::class, 25 | LockingMiddleware::class => LockingMiddleware::class, 26 | InvokeInflector::class => InvokeInflector::class 27 | ], 28 | 'factories' => [ 29 | CommandBus::class => CommandBusFactory::class, 30 | CommandHandlerMiddleware::class => CommandHandlerMiddlewareFactory::class, 31 | InMemoryLocator::class => InMemoryLocatorFactory::class, 32 | ClassnameLaminasLocator::class => ClassnameLaminasLocatorFactory::class, 33 | LaminasLocator::class => LaminasLocatorFactory::class, 34 | 'League\Tactician\Doctrine\ORM\TransactionMiddleware' => DoctrineTransactionFactory::class, 35 | ], 36 | ], 37 | 'controller_plugins' => [ 38 | 'factories' => [ 39 | 'tacticianCommandBus' => TacticianCommandBusPluginFactory::class, 40 | ], 41 | ], 42 | 'tactician' => [ 43 | 'default-extractor' => ClassNameExtractor::class, 44 | 'default-locator' => LaminasLocator::class, 45 | 'default-inflector' => HandleInflector::class, 46 | 'handler-map' => [], 47 | 'plugins' => [ 48 | 'League\Tactician\Doctrine\ORM\TransactionMiddleware' => 'Doctrine\ORM\EntityManager', 49 | ], 50 | 'middleware' => [ 51 | CommandHandlerMiddleware::class => 0, 52 | ], 53 | ], 54 | ]; 55 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | ./test 15 | 16 | 17 | 18 | ./src 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | config = (new Module())->getConfig(); 11 | } 12 | 13 | public function __invoke() 14 | { 15 | $config = $this->config; 16 | 17 | $config['dependencies'] = $config['service_manager']; 18 | 19 | return $config; 20 | } 21 | 22 | public function getDependencies() 23 | { 24 | return $this->config['service_manager']; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Controller/Plugin/TacticianCommandBusPlugin.php: -------------------------------------------------------------------------------- 1 | commandBus = $commandBus; 17 | } 18 | 19 | /** 20 | * @param object|null $command 21 | * @return CommandBus|mixed 22 | */ 23 | public function __invoke($command = null) 24 | { 25 | if ($command === null) { 26 | return $this->commandBus; 27 | } 28 | 29 | return $this->commandBus->handle($command); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Factory/CommandBusFactory.php: -------------------------------------------------------------------------------- 1 | get('config')['tactician']['middleware']; 17 | 18 | arsort($configMiddleware); 19 | 20 | $list = []; 21 | foreach (array_keys($configMiddleware) as $serviceName) { 22 | /** @var Middleware $middleware */ 23 | $list[] = $container->get($serviceName); 24 | } 25 | 26 | return new CommandBus($list); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Factory/CommandHandlerMiddlewareFactory.php: -------------------------------------------------------------------------------- 1 | get('config')['tactician']; 19 | 20 | /** @var CommandNameExtractor $extractor */ 21 | $extractor = $container->get($config['default-extractor']); 22 | 23 | /** @var HandlerLocator $locator */ 24 | $locator = $container->get($config['default-locator']); 25 | 26 | /** @var MethodNameInflector $inflector */ 27 | $inflector = $container->get($config['default-inflector']); 28 | 29 | return new CommandHandlerMiddleware($extractor, $locator, $inflector); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Factory/Controller/Plugin/TacticianCommandBusPluginFactory.php: -------------------------------------------------------------------------------- 1 | get(CommandBus::class); 17 | 18 | return new TacticianCommandBusPlugin($commandBus); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Factory/InMemoryLocatorFactory.php: -------------------------------------------------------------------------------- 1 | get('config')['tactician']['handler-map']; 16 | 17 | return new InMemoryLocator($handlerMap); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Factory/Plugin/DoctrineTransactionFactory.php: -------------------------------------------------------------------------------- 1 | get('config')['tactician']['plugins']; 16 | $ormKey = $config[TransactionMiddleware::class]; 17 | 18 | return new TransactionMiddleware($container->get($ormKey)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Locator/ClassnameLaminasLocator.php: -------------------------------------------------------------------------------- 1 | container = $container; 15 | } 16 | 17 | /** 18 | * Retrieves the handler for a specified command 19 | * 20 | * @param string $commandName 21 | * 22 | * @return mixed 23 | * 24 | * @throws MissingHandlerException 25 | */ 26 | public function getHandlerForCommand($commandName) 27 | { 28 | $handlerFQCN = $commandName . 'Handler'; 29 | 30 | if ($this->container->has($handlerFQCN)) { 31 | return $this->container->get($handlerFQCN); 32 | } 33 | 34 | if (class_exists($handlerFQCN)) { 35 | return new $handlerFQCN(); 36 | } 37 | 38 | throw MissingHandlerException::forCommand($commandName); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Locator/ClassnameLaminasLocatorFactory.php: -------------------------------------------------------------------------------- 1 | container = $container; 18 | } 19 | 20 | /** 21 | * Retrieves the handler for a specified command 22 | * 23 | * @param string $commandName 24 | * 25 | * @return mixed 26 | * 27 | * @throws MissingHandlerException 28 | */ 29 | public function getHandlerForCommand($commandName) 30 | { 31 | if (!$this->commandExists($commandName)) { 32 | throw MissingHandlerException::forCommand($commandName); 33 | } 34 | 35 | $serviceNameOrFQCN = $this->handlerMap[$commandName]; 36 | 37 | if ($this->container->has($serviceNameOrFQCN)) { 38 | return $this->container->get($serviceNameOrFQCN); 39 | } 40 | 41 | if (class_exists($serviceNameOrFQCN)) { 42 | return new $serviceNameOrFQCN(); 43 | } 44 | 45 | throw MissingHandlerException::forCommand($commandName); 46 | } 47 | 48 | /** 49 | * @param string $commandName 50 | * @return bool 51 | */ 52 | protected function commandExists($commandName) 53 | { 54 | if (!$this->handlerMap) { 55 | $this->handlerMap = $this->container->get('config')['tactician']['handler-map']; 56 | } 57 | 58 | return isset($this->handlerMap[$commandName]); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Locator/LaminasLocatorFactory.php: -------------------------------------------------------------------------------- 1 |