├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src ├── CommandBus.php ├── console │ └── BackgroundBusController.php ├── exceptions │ └── MissingHandlerException.php ├── interfaces │ ├── BackgroundCommand.php │ ├── CommandBusInterface.php │ ├── Handler.php │ ├── HandlerLocator.php │ ├── Middleware.php │ ├── QueuedCommand.php │ └── SelfHandlingCommand.php ├── locators │ ├── ChainedLocator.php │ └── ClassNameLocator.php └── middlewares │ ├── BackgroundCommandMiddleware.php │ ├── BackgroundCommandTrait.php │ ├── LoggingMiddleware.php │ ├── QueuedCommandMiddleware.php │ ├── QueuedCommandTrait.php │ └── QueuedJobWrapper.php └── tests ├── BackgroundMiddlewareTest.php ├── CommandBusTest.php ├── QueuedMiddlewareTest.php ├── README.md ├── TestCase.php ├── bootstrap.php ├── config.php ├── data ├── BackgroundTestCommand.php ├── QueuedTestCommand.php ├── TestCommand.php ├── TestHandler.php ├── TestHandlerCommand.php └── TestMiddleware.php ├── files └── .gitignore ├── runtime └── .gitignore └── yii.php /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Composer template 3 | composer.phar 4 | composer.lock 5 | vendor/ 6 | 7 | phpunit.phar 8 | phpunit.xml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: php 3 | group: edge 4 | dist: trusty 5 | php: 6 | - 5.6 7 | - 7.2 8 | 9 | before_install: 10 | - composer config -g github-oauth.github.com $GITHUB_TOKEN 11 | 12 | install: 13 | - travis_retry composer self-update && composer --version 14 | - export PATH="$HOME/.composer/vendor/bin:$PATH" 15 | - travis_retry composer install --no-interaction 16 | 17 | script: 18 | - vendor/bin/phpunit --verbose $PHPUNIT_FLAGS 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Eugene Terentev 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of test-removal nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yii2 Command Bus 2 | 3 | Command Bus for Yii2 4 | 5 | [![Build Status](https://travis-ci.org/trntv/yii2-command-bus.svg?branch=master)](https://travis-ci.org/trntv/yii2-command-bus) 6 | 7 | 8 | ## What is Command Bus? 9 | > The idea of a command bus is that you create command objects that represent what you want your application to do. 10 | > Then, you toss it into the bus and the bus makes sure that the command object gets to where it needs to go. 11 | > So, the command goes in -> the bus hands it off to a handler -> and then the handler actually does the job. The command essentially represents a method call to your service layer. 12 | 13 | [Shawn McCool ©](http://shawnmc.cool/command-bus) 14 | 15 | ## Installation 16 | 17 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 18 | 19 | Either run 20 | 21 | ``` 22 | php composer.phar require --prefer-dist trntv/yii2-command-bus 23 | ``` 24 | 25 | or add 26 | ``` 27 | "trntv/yii2-command-bus": "^1.0" 28 | ``` 29 | to your composer.json file 30 | 31 | ## Setting Up 32 | 33 | ### 1. Command Bus Service 34 | After the installation, first step is to set the command bus component. 35 | 36 | ```php 37 | return [ 38 | // ... 39 | 'components' => [ 40 | 'commandBus' => [ 41 | 'class' => 'trntv\bus\CommandBus' 42 | ] 43 | ], 44 | ]; 45 | ``` 46 | 47 | ### 2. Background commands support (optional) 48 | Install required package: 49 | ``` 50 | php composer.phar require symfony/process:^3.0 51 | ``` 52 | 53 | For the background commands worker, add a controller and command bus middleware in your config 54 | 55 | ```php 56 | 'controllerMap' => [ 57 | 'background-bus' => [ 58 | 'class' => 'trntv\bus\console\BackgroundBusController', 59 | ] 60 | ], 61 | 62 | 'components' => [ 63 | 'commandBus' =>[ 64 | ... 65 | 'middlewares' => [ 66 | [ 67 | 'class' => '\trntv\bus\middlewares\BackgroundCommandMiddleware', 68 | 'backgroundHandlerPath' => '@console/yii', 69 | 'backgroundHandlerRoute' => 'background-bus/handle', 70 | ] 71 | ] 72 | ... 73 | ] 74 | ], 75 | ``` 76 | 77 | Create background command 78 | ```php 79 | class ReportCommand extends Object implements BackgroundCommand, SelfHandlingCommand 80 | { 81 | use BackgroundCommandTrait; 82 | 83 | public $someImportantData; 84 | 85 | public function handle($command) { 86 | // do what you need 87 | } 88 | } 89 | ``` 90 | And run it asynchronously! 91 | ```php 92 | Yii::$app->commandBus->handle(new ReportCommand([ 93 | 'async' => true, 94 | 'someImportantData' => [ // data // ] 95 | ])) 96 | ``` 97 | 98 | ### 3. Queued commands support (optional) 99 | #### 3.1 Install required package: 100 | 101 | ``` 102 | php composer.phar require yiisoft/yii2-queue 103 | ``` 104 | #### 3.2 Configure extensions 105 | If you need commands to be run in queue, you need to setup middleware and yii2-queue extensions. 106 | 107 | ```php 108 | 'components' => [ 109 | 'queue' => [ 110 | // queue config 111 | ], 112 | 113 | 'commandBus' =>[ 114 | ... 115 | 'middlewares' => [ 116 | [ 117 | 'class' => '\trntv\bus\middlewares\QueuedCommandMiddleware', 118 | // 'delay' => 3, // You can set default delay for all commands here 119 | ] 120 | ] 121 | ... 122 | ] 123 | ] 124 | ``` 125 | More information about yii2-queue config can be found [here](https://github.com/yiisoft/yii2-queue/blob/master/docs/guide/usage.md) 126 | #### 3.4 Run queue worker 127 | ``yii queue/listen`` 128 | More information [here](https://github.com/yiisoft/yii2-queue/blob/master/docs/guide/worker.md#worker-starting-control) 129 | #### 3.5 Create and run command 130 | ```php 131 | class HeavyComputationsCommand extends Object implements QueuedCommand, SelfHandlingCommand 132 | { 133 | use QueuedCommandTrait; 134 | 135 | public function handle() { 136 | // do work here 137 | } 138 | } 139 | 140 | $command = new HeavyComputationsCommand(); 141 | Yii::$app->commandBus->handle($command) 142 | ``` 143 | 144 | ### 4. Handlers 145 | Handlers are objects that will handle command execution 146 | There are two possible ways to execute command: 147 | #### 4.1 External handler 148 | ```php 149 | return [ 150 | // ... 151 | 'components' => [ 152 | 'commandBus' => [ 153 | 'class' => 'trntv\bus\CommandBus', 154 | 'locator' => [ 155 | 'class' => 'trntv\bus\locators\ClassNameLocator', 156 | 'handlers' => [ 157 | 'app\commands\SomeCommand' => 'app\handlers\SomeHandler' 158 | ] 159 | ] 160 | ] 161 | ], 162 | ]; 163 | 164 | // or 165 | $handler = new SomeHandler; 166 | Yii::$app->commandBus->locator->addHandler($handler, 'app\commands\SomeCommand'); 167 | // or 168 | Yii::$app->commandBus->locator->addHandler('app\handlers\SomeHandler', 'app\commands\SomeCommand'); 169 | ``` 170 | #### 4.1 Self-handling command 171 | ```php 172 | class SomeCommand implements SelfHandlingCommand 173 | { 174 | public function handle($command) { 175 | // do what you need 176 | } 177 | } 178 | 179 | $command = Yii::$app->commandBus->handle($command); 180 | ``` 181 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trntv/yii2-command-bus", 3 | "description": "Yii2 Command Bus extension", 4 | "keywords": [ 5 | "yii2", 6 | "extension", 7 | "command bus", 8 | "queue" 9 | ], 10 | "type": "yii2-extension", 11 | "license": "BSD-3-Clause", 12 | "support": { 13 | "issues": "https://github.com/trntv/yii2-command-bus/issues", 14 | "source": "https://github.com/trntv/yii2-command-bus" 15 | }, 16 | "minimum-stability": "stable", 17 | "authors": [ 18 | { 19 | "name": "Eugene Terentev", 20 | "email": "eugene@terentev.net" 21 | } 22 | ], 23 | "require": { 24 | "yiisoft/yii2": "^2.0", 25 | "yiisoft/yii2-queue": "^2.0" 26 | }, 27 | "suggest": { 28 | "symfony/process": "^3.0", 29 | "yiisoft/yii2-queue": "^2.0" 30 | }, 31 | "require-dev": { 32 | "yiisoft/yii2-dev": "^2.0", 33 | "phpunit/phpunit": "^4.8", 34 | "predis/predis": "^1.0", 35 | "symfony/process": "^3.0" 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "trntv\\bus\\": "src/" 40 | } 41 | }, 42 | "repositories": [ 43 | { 44 | "type": "composer", 45 | "url": "https://asset-packagist.org" 46 | } 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/CommandBus.php: -------------------------------------------------------------------------------- 1 | locator) { 35 | $this->locator = Instance::ensure($this->locator, HandlerLocator::class); 36 | } 37 | parent::init(); 38 | } 39 | 40 | /** 41 | * @param $command 42 | * @return mixed 43 | * @throws InvalidConfigException 44 | * @throws MissingHandlerException 45 | */ 46 | public function handle($command) 47 | { 48 | $chain = $this->createMiddlewareChain($command, $this->middlewares); 49 | return $chain($command); 50 | } 51 | 52 | /** 53 | * @param $command 54 | * @param array $middlewareList 55 | * @return \Closure 56 | * @throws InvalidConfigException 57 | * @throws MissingHandlerException 58 | */ 59 | protected function createMiddlewareChain($command, array $middlewareList) { 60 | 61 | $lastCallable = $this->createHandlerCallable($command); 62 | 63 | while ($middleware = array_pop($middlewareList)) { 64 | if (!$middleware instanceof Middleware) { 65 | throw new InvalidConfigException; 66 | } 67 | $lastCallable = function ($command) use ($middleware, $lastCallable) { 68 | return $middleware->execute($command, $lastCallable); 69 | }; 70 | } 71 | return $lastCallable; 72 | } 73 | 74 | /** 75 | * @param $command 76 | * @return \Closure 77 | * @throws MissingHandlerException 78 | */ 79 | protected function createHandlerCallable($command) 80 | { 81 | // Built-in self-handling locator 82 | if ($command instanceof SelfHandlingCommand) { 83 | $handler = $command; 84 | } else { 85 | $handler = $this->locator->locate($command, $this); 86 | } 87 | 88 | if (!$handler) { 89 | throw new MissingHandlerException('Handler not found'); 90 | } 91 | 92 | $handlerMiddleware = function ($command) use ($handler) { 93 | return $handler->handle($command); 94 | }; 95 | 96 | return $handlerMiddleware; 97 | } 98 | 99 | /** 100 | * @return array 101 | */ 102 | public function getMiddlewares() 103 | { 104 | return $this->middlewares; 105 | } 106 | 107 | /** 108 | * @param array $middlewares 109 | * @throws InvalidConfigException 110 | */ 111 | public function setMiddlewares($middlewares) 112 | { 113 | foreach ($middlewares as $k => $middleware) { 114 | $this->middlewares[$k] = Instance::ensure($middleware, Middleware::class); 115 | } 116 | } 117 | 118 | /** 119 | * @param Middleware $middleware 120 | */ 121 | public function addMiddleware(Middleware $middleware) 122 | { 123 | $this->middlewares[] = $middleware; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/console/BackgroundBusController.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class BackgroundBusController extends Controller 16 | { 17 | /** 18 | * @var mixed|CommandBus 19 | */ 20 | public $commandBus = 'commandBus'; 21 | 22 | /** 23 | * @param \yii\base\Action $action 24 | * @return bool 25 | * @throws \yii\base\InvalidConfigException 26 | */ 27 | public function beforeAction($action) 28 | { 29 | $this->commandBus = Instance::ensure($this->commandBus, CommandBus::class); 30 | return parent::beforeAction($action); 31 | } 32 | 33 | /** 34 | * @param string $command serialized command object 35 | * @return string 36 | */ 37 | public function actionHandle($command) 38 | { 39 | try { 40 | $command = unserialize(base64_decode($command)); 41 | $command->setRunningInBackground(true); 42 | $this->commandBus->handle($command); 43 | } catch (\Exception $e) { 44 | Console::error($e->getMessage()); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/exceptions/MissingHandlerException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface BackgroundCommand 11 | { 12 | /** 13 | * @return bool 14 | */ 15 | public function isAsync(); 16 | 17 | /** 18 | * @return bool 19 | */ 20 | public function isRunningInBackground(); 21 | } 22 | -------------------------------------------------------------------------------- /src/interfaces/CommandBusInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface Handler 11 | { 12 | /** 13 | * @param $command 14 | * @return mixed 15 | */ 16 | public function handle($command); 17 | } 18 | -------------------------------------------------------------------------------- /src/interfaces/HandlerLocator.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface HandlerLocator 11 | { 12 | /** 13 | * @param $command 14 | * @return mixed 15 | */ 16 | public function locate($command); 17 | } -------------------------------------------------------------------------------- /src/interfaces/Middleware.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface Middleware 11 | { 12 | /** 13 | * @param $command 14 | * @param callable $next 15 | * 16 | * @return mixed 17 | */ 18 | public function execute($command, callable $next); 19 | } 20 | -------------------------------------------------------------------------------- /src/interfaces/QueuedCommand.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface QueuedCommand 11 | { 12 | /** 13 | * @return bool 14 | */ 15 | public function isRunningInQueue(); 16 | 17 | /** 18 | * @return int 19 | */ 20 | public function getDelay(); 21 | } 22 | -------------------------------------------------------------------------------- /src/interfaces/SelfHandlingCommand.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface SelfHandlingCommand extends Handler 11 | { 12 | 13 | } -------------------------------------------------------------------------------- /src/locators/ChainedLocator.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ChainedLocator extends BaseObject implements HandlerLocator 15 | { 16 | /** 17 | * @var array|HandlerLocator[] 18 | */ 19 | public $locators = []; 20 | 21 | /** 22 | * @throws \yii\base\InvalidConfigException 23 | */ 24 | public function init() 25 | { 26 | foreach ($this->locators as $k => $config) { 27 | $this->locators[$k] = Instance::ensure($config, HandlerLocator::class); 28 | } 29 | parent::init(); 30 | } 31 | 32 | /** 33 | * @param $command 34 | * @return mixed 35 | */ 36 | public function locate($command) 37 | { 38 | foreach ($this->locators as $locator) { 39 | /** @var HandlerLocator $locator */ 40 | $handler = $locator->locate($command); 41 | if ($handler) { 42 | return $handler; 43 | } 44 | } 45 | 46 | return false; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/locators/ClassNameLocator.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ClassNameLocator extends BaseObject implements HandlerLocator 16 | { 17 | /** 18 | * @var 19 | */ 20 | public $handlers; 21 | 22 | /** 23 | * @param $command 24 | * @return mixed 25 | * @throws \yii\base\InvalidConfigException 26 | * @internal param CommandBus $commandBus 27 | */ 28 | public function locate($command) 29 | { 30 | $className = get_class($command); 31 | 32 | if (array_key_exists($className, $this->handlers)) { 33 | return Instance::ensure($this->handlers[$className], Handler::class); 34 | } 35 | 36 | return false; 37 | } 38 | 39 | /** 40 | * @param Handler $handler 41 | * @param $className 42 | */ 43 | public function addHandler(Handler $handler, $className) 44 | { 45 | $this->handlers[$className] = $handler; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/middlewares/BackgroundCommandMiddleware.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class BackgroundCommandMiddleware extends BaseObject implements Middleware 17 | { 18 | /** 19 | * @var string Path to php executable 20 | */ 21 | public $backgroundHandlerBinary; 22 | /** 23 | * @var string Path to cli script 24 | */ 25 | public $backgroundHandlerPath; 26 | /** 27 | * @var string console route 28 | */ 29 | public $backgroundHandlerRoute; 30 | /** 31 | * @var int|float|null process timeout 32 | */ 33 | public $backgroundProcessTimeout = 60; 34 | /** 35 | * @var int|float|null process idle timeout 36 | */ 37 | public $backgroundProcessIdleTimeout; 38 | /** 39 | * @var array Arguments that will be passed to script 40 | */ 41 | public $backgroundHandlerArguments = []; 42 | /** 43 | * @var array Arguments that will be passed to binary 44 | * 45 | * ```php 46 | * 'backgroundHandlerPath' => '/path/to/script.php', 47 | * 'backgroundHandlerArguments' => ['--foo bar'] 48 | * 'backgroundHandlerBinaryArguments' => ['--define memory_limit=1G'] 49 | * ``` 50 | * will generate 51 | * ``` 52 | * php --define memory_limit=1G /path/to/script.php --foo bar 53 | * ``` 54 | */ 55 | public $backgroundHandlerBinaryArguments = []; 56 | 57 | /** 58 | * @param $command 59 | * @param callable $next 60 | * 61 | * @return string 62 | */ 63 | public function execute($command, callable $next) 64 | { 65 | 66 | if ($command instanceof BackgroundCommand && !$command->isRunningInBackground()) { 67 | return $this->runProcess($command); 68 | } 69 | 70 | return $next($command); 71 | } 72 | 73 | /** 74 | * @param BackgroundCommand $command 75 | * @return string 76 | */ 77 | protected function runProcess(BackgroundCommand $command) 78 | { 79 | $binary = $this->getBackgroundHandlerBinary(); 80 | $path = $this->getBackgroundHandlerPath(); 81 | $route = $this->getBackgroundHandlerRoute(); 82 | $arguments = implode(' ', $this->getBackgroundHandlerArguments($command)); 83 | $binaryArguments = implode(' ', $this->backgroundHandlerBinaryArguments); 84 | 85 | $process = new Process("{$binary} {$binaryArguments} {$path} {$route} {$arguments}"); 86 | $process->setTimeout($this->backgroundProcessTimeout); 87 | $process->setIdleTimeout($this->backgroundProcessIdleTimeout); 88 | if ($command->isAsync()) { 89 | $process->start(); 90 | } else { 91 | $process->run(); 92 | } 93 | 94 | return $process; 95 | } 96 | 97 | /** 98 | * @return bool|string 99 | */ 100 | public function getBackgroundHandlerBinary() 101 | { 102 | $binary = $this->backgroundHandlerBinary ?: PHP_BINARY; 103 | return Yii::getAlias($binary); 104 | } 105 | 106 | /** 107 | * @return bool|string 108 | */ 109 | public function getBackgroundHandlerPath() 110 | { 111 | return Yii::getAlias($this->backgroundHandlerPath); 112 | } 113 | 114 | /** 115 | * @return mixed 116 | */ 117 | public function getBackgroundHandlerRoute() 118 | { 119 | return $this->backgroundHandlerRoute; 120 | } 121 | 122 | /** 123 | * @param $command 124 | * @return array 125 | */ 126 | private function getBackgroundHandlerArguments($command) 127 | { 128 | $arguments = $this->backgroundHandlerArguments; 129 | $command = base64_encode(serialize($command)); 130 | array_unshift($arguments, $command); 131 | return $arguments; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/middlewares/BackgroundCommandTrait.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | trait BackgroundCommandTrait 11 | { 12 | /** 13 | * @var bool 14 | */ 15 | protected $async = false; 16 | /** 17 | * @var bool 18 | */ 19 | protected $runningInBackground = false; 20 | 21 | /** 22 | * @return bool 23 | */ 24 | public function isAsync() 25 | { 26 | return $this->async; 27 | } 28 | /** 29 | * @param boolean $runningInBackground 30 | */ 31 | public function setRunningInBackground($runningInBackground = true) 32 | { 33 | $this->runningInBackground = $runningInBackground; 34 | } 35 | 36 | /** 37 | * @param boolean $async 38 | */ 39 | public function setAsync($async = true) 40 | { 41 | $this->async = $async; 42 | } 43 | 44 | /** 45 | * @return bool 46 | */ 47 | public function isRunningInBackground() 48 | { 49 | return $this->runningInBackground; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/middlewares/LoggingMiddleware.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class LoggingMiddleware extends BaseObject implements Middleware 16 | { 17 | /** 18 | * @var integer log message level 19 | */ 20 | public $level; 21 | /** 22 | * @var string log message category 23 | */ 24 | public $category = 'command-bus'; 25 | /** 26 | * @var string|array|callable|Logger Logger configuration 27 | */ 28 | public $logger; 29 | /** 30 | * @var bool 31 | */ 32 | public $forceFlush = false; 33 | 34 | /** 35 | * @return void 36 | * @throws \yii\base\InvalidConfigException 37 | */ 38 | public function init() 39 | { 40 | if ($this->logger === null) { 41 | $this->logger = Yii::getLogger(); 42 | } else { 43 | $this->logger = Yii::createObject($this->logger); 44 | } 45 | if (!$this->level) { 46 | $this->level = Logger::LEVEL_INFO; 47 | } 48 | } 49 | 50 | /** 51 | * @param $command 52 | * @param callable $next 53 | * 54 | * @return mixed 55 | */ 56 | public function execute($command, callable $next) 57 | { 58 | $class = get_class($command); 59 | $this->logger->log("Command execution started: {$class}", $this->level, $this->category); 60 | if ($this->forceFlush) { 61 | $this->logger->flush($final = false); 62 | } 63 | $result = $next($command); 64 | $this->logger->log("Command execution ended: {$class}", $this->level, $this->category); 65 | if ($this->forceFlush) { 66 | $this->logger->flush($final = false); 67 | } 68 | return $result; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/middlewares/QueuedCommandMiddleware.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class QueuedCommandMiddleware extends BaseObject implements Middleware 17 | { 18 | /** 19 | * @var mixed|Queue 20 | */ 21 | public $queue = 'queue'; 22 | 23 | /** 24 | * @var int Default delay for all commands 25 | */ 26 | public $delay = 0; 27 | 28 | /** 29 | * @param string|array Command bus component name or configuration array 30 | * compatible with Yii's createObject() method 31 | */ 32 | public $commandBus; 33 | 34 | /** 35 | * @throws \yii\base\InvalidConfigException 36 | */ 37 | public function init() 38 | { 39 | $this->queue = Instance::ensure($this->queue, Queue::class); 40 | } 41 | 42 | /** 43 | * @param $command 44 | * @param callable $next 45 | * 46 | * @return string 47 | */ 48 | public function execute($command, callable $next) 49 | { 50 | if ($command instanceof QueuedCommand && !$command->isRunningInQueue()) 51 | { 52 | $delay = $command->getDelay() !== null ?: $this->delay; 53 | 54 | $command->setRunningInQueue(true); 55 | $job = new QueuedJobWrapper([ 56 | 'command' => $command, 57 | 'commandBus' => $this->commandBus 58 | ]); 59 | 60 | return $this->queue->delay($delay)->push($job); 61 | } 62 | 63 | return $next($command); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/middlewares/QueuedCommandTrait.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | trait QueuedCommandTrait 12 | { 13 | /** 14 | * @var int 15 | */ 16 | protected $delay; 17 | 18 | /** 19 | * @var bool 20 | */ 21 | protected $runningInQueue = false; 22 | 23 | /** 24 | * @return boolean 25 | */ 26 | public function isRunningInQueue() 27 | { 28 | return $this->runningInQueue; 29 | } 30 | 31 | /** 32 | * @param boolean $runningInQueue 33 | */ 34 | public function setRunningInQueue($runningInQueue = true) 35 | { 36 | $this->runningInQueue = $runningInQueue; 37 | } 38 | 39 | /** 40 | * @return null|int 41 | */ 42 | public function getDelay() 43 | { 44 | return $this->delay; 45 | } 46 | 47 | /** 48 | * @param int|null $delay 49 | * 50 | * @return void 51 | */ 52 | public function setDelay($delay) 53 | { 54 | $this->delay = $delay; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/middlewares/QueuedJobWrapper.php: -------------------------------------------------------------------------------- 1 | commandBus ?: CommandBus::class, CommandBus::class); 33 | $commandBus->handle($this->command); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/BackgroundMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace trntv\bus\tests; 7 | 8 | use Symfony\Component\Process\Exception\ProcessTimedOutException; 9 | use Symfony\Component\Process\Process; 10 | use trntv\bus\tests\data\BackgroundTestCommand; 11 | 12 | class BackgroundMiddlewareTest extends TestCase 13 | { 14 | public function testBackgroundCommand() 15 | { 16 | /** @var $process Process */ 17 | $process = $this->commandBus->handle(new BackgroundTestCommand()); 18 | $this->assertInstanceOf(Process::class, $process); 19 | $this->assertTrue($process->isSuccessful()); 20 | $this->assertEquals('test ok', $process->getOutput()); 21 | } 22 | 23 | public function testBackgroundProcessArguments() 24 | { 25 | /** @var $process Process */ 26 | $process = $this->commandBus->handle(new BackgroundTestCommand()); 27 | $this->assertContains('-d foo=bar', $process->getCommandLine()); 28 | $this->assertContains('--interactive=0', $process->getCommandLine()); 29 | } 30 | 31 | public function testBackgroundAsyncCommand() 32 | { 33 | $command = new BackgroundTestCommand([ 34 | 'async' => true 35 | ]); 36 | for ($i = 0; $i < 10; $i++) { 37 | /** @var $process Process */ 38 | $process = $this->commandBus->handle($command); 39 | $this->assertInstanceOf(Process::class, $process); 40 | while ($process->isRunning()) { 41 | // waiting for process to finish 42 | } 43 | $this->assertTrue($process->isSuccessful()); 44 | $output = $process->getOutput(); 45 | $this->assertEquals('test ok', $output); 46 | $this->assertNotEquals('test is not ok', $output); 47 | } 48 | } 49 | 50 | public function testBackgroundCommandTimeout() { 51 | $commandWithTimeout = new BackgroundTestCommand([ 52 | 'async' => true, 53 | 'sleep' => 6 54 | ]); 55 | /** @var $process Process */ 56 | $process = $this->commandBus->handle($commandWithTimeout); 57 | $this->assertInstanceOf(Process::class, $process); 58 | try { 59 | while ($process->isRunning()) { 60 | $process->checkTimeout(); 61 | } 62 | $this->fail('Timeout not working'); 63 | } catch (ProcessTimedOutException $e) {} 64 | } 65 | 66 | public function tearDown() 67 | { 68 | parent::tearDown(); 69 | @unlink(__DIR__ . '/files/test-file'); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/CommandBusTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace trntv\bus\tests; 7 | 8 | use trntv\bus\tests\data\TestCommand; 9 | use trntv\bus\tests\data\TestHandler; 10 | use trntv\bus\tests\data\TestHandlerCommand; 11 | use trntv\bus\tests\data\TestMiddleware; 12 | 13 | class CommandBusTest extends TestCase 14 | { 15 | public function testCommand() 16 | { 17 | $result = $this->commandBus->handle(new TestCommand()); 18 | $this->assertEquals('test ok', $result); 19 | } 20 | 21 | public function testMiddleware() 22 | { 23 | \Yii::getLogger()->flush(); 24 | $this->commandBus->addMiddleware(new TestMiddleware()); 25 | $result = $this->commandBus->handle(new TestCommand()); 26 | $this->assertEquals('test ok', $result); 27 | $this->assertNotFalse(array_search('middleware test 1 ok', \Yii::$app->getLog()->logger->messages[1])); 28 | $this->assertNotFalse(array_search('middleware test 2 ok', \Yii::$app->getLog()->logger->messages[2])); 29 | } 30 | 31 | public function testHandler() 32 | { 33 | $this->commandBus->locator->addHandler(new TestHandler(), TestHandlerCommand::className()); 34 | $result = $this->commandBus->handle(new TestHandlerCommand([ 35 | 'param' => 'test ok' 36 | ])); 37 | $this->assertEquals('test ok', $result); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/QueuedMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace trntv\bus\tests; 7 | 8 | use trntv\bus\tests\data\QueuedTestCommand; 9 | use yii\helpers\FileHelper; 10 | 11 | class QueuedMiddlewareTest extends TestCase 12 | { 13 | public function testQueuedCommand() 14 | { 15 | $id = $this->commandBus->handle(new QueuedTestCommand()); 16 | $this->assertInternalType('integer', $id); 17 | 18 | /** @var \yii\queue\file\Queue $queue */ 19 | $queue = \Yii::$app->get('queue'); 20 | $queue->run(false); 21 | 22 | $this->assertStringEqualsFile(\Yii::getAlias('@runtime/test.lock'), QueuedTestCommand::class); 23 | } 24 | 25 | public function testQueuedCommandGetDelay() 26 | { 27 | $command = new QueuedTestCommand(); 28 | $command->setDelay(10); 29 | $this->assertEquals(10, $command->getDelay()); 30 | $command->setDelay(5); 31 | $this->assertEquals(5, $command->getDelay()); 32 | } 33 | 34 | public function tearDown() 35 | { 36 | @\unlink(\Yii::getAlias('@runtime/test.lock')); 37 | FileHelper::removeDirectory(\Yii::getAlias('@runtime/queue')); 38 | FileHelper::createDirectory(\Yii::getAlias('@runtime/queue')); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | ### Testing 2 | 1. Install composer requirements 3 | 2. Run tests 4 | ``` 5 | ./vendor/bin/phpunit 6 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class TestCase extends \PHPUnit_Framework_TestCase 14 | { 15 | /** 16 | * @var CommandBus 17 | */ 18 | public $commandBus; 19 | 20 | /** 21 | * Populates Yii::$app with a new application 22 | * The application will be destroyed on tearDown() automatically. 23 | * @param array $config The application configuration, if needed 24 | * @param string $appClass name of the application class to create 25 | */ 26 | protected function mockApplication($config = [], $appClass = '\yii\console\Application') 27 | { 28 | $config = ArrayHelper::merge(require(__DIR__ . '/config.php'), $config); 29 | new $appClass(ArrayHelper::merge([ 30 | 'id' => 'testapp', 31 | 'basePath' => __DIR__, 32 | 'vendorPath' => $this->getVendorPath(), 33 | ], $config)); 34 | } 35 | 36 | /** 37 | * @return string 38 | */ 39 | protected function getVendorPath() 40 | { 41 | $vendor = dirname(dirname(__DIR__)) . '/vendor'; 42 | if (!is_dir($vendor)) { 43 | $vendor = dirname(dirname(dirname(dirname(__DIR__)))); 44 | } 45 | return $vendor; 46 | } 47 | 48 | /** 49 | * @throws \yii\base\InvalidConfigException 50 | */ 51 | protected function setUp() 52 | { 53 | parent::setUp(); 54 | $this->mockApplication(); 55 | $this->commandBus = \Yii::$app->get('commandBus'); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | return [ 6 | 7 | 'id' => 'testapp', 8 | 'basePath' => __DIR__, 9 | 'vendorPath' => dirname(__DIR__) . '/vendor', 10 | 11 | 'controllerMap' => [ 12 | 'background-bus' => 'trntv\bus\console\BackgroundBusController', 13 | 'queue-bus' => 'trntv\bus\console\QueueBusController', 14 | ], 15 | 'components' => [ 16 | 'commandBus' => [ 17 | 'class' => 'trntv\bus\CommandBus', 18 | 'locator' => 'trntv\bus\locators\ClassNameLocator', 19 | 'middlewares' => [ 20 | [ 21 | 'class' => '\trntv\bus\middlewares\BackgroundCommandMiddleware', 22 | 'backgroundHandlerPath' => __DIR__ . '/yii.php', 23 | 'backgroundHandlerRoute' => 'background-bus/handle', 24 | 'backgroundHandlerArguments' => ['--interactive=0'], 25 | 'backgroundHandlerBinaryArguments' => ['-d foo=bar'], 26 | 'backgroundProcessTimeout' => 5 27 | ], 28 | [ 29 | 'class' => '\trntv\bus\middlewares\QueuedCommandMiddleware' 30 | ], 31 | [ 32 | 'class' => '\trntv\bus\middlewares\LoggingMiddleware', 33 | 'level' => 1, 34 | ], 35 | ], 36 | ], 37 | 'queue' => [ 38 | 'class' => '\yii\queue\file\Queue', 39 | 'path' => '@runtime/queue' 40 | ], 41 | ] 42 | ]; 43 | -------------------------------------------------------------------------------- /tests/data/BackgroundTestCommand.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class BackgroundTestCommand extends BaseObject implements BackgroundCommand, SelfHandlingCommand 16 | { 17 | use BackgroundCommandTrait; 18 | 19 | public $sleep = 1; 20 | 21 | public function handle($command) 22 | { 23 | sleep($this->sleep); 24 | echo 'test ok'; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/data/QueuedTestCommand.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class QueuedTestCommand extends BaseObject implements SelfHandlingCommand, QueuedCommand 16 | { 17 | use QueuedCommandTrait; 18 | 19 | public function handle($command) 20 | { 21 | \file_put_contents(\Yii::getAlias('@runtime/test.lock'), __CLASS__); 22 | return true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/data/TestCommand.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class TestCommand extends BaseObject implements SelfHandlingCommand 14 | { 15 | public function handle($command) 16 | { 17 | return 'test ok'; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/data/TestHandler.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class TestHandler extends BaseObject implements Handler 14 | { 15 | /** 16 | * @param $command 17 | * @return mixed 18 | */ 19 | public function handle($command) 20 | { 21 | return $command->param; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/data/TestHandlerCommand.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class TestHandlerCommand extends BaseObject 13 | { 14 | public $param; 15 | } 16 | -------------------------------------------------------------------------------- /tests/data/TestMiddleware.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class TestMiddleware extends BaseObject implements Middleware 14 | { 15 | 16 | public function execute($command, callable $next) 17 | { 18 | \Yii::info('middleware test 1 ok'); 19 | $result = $next($command); 20 | \Yii::info('middleware test 2 ok'); 21 | 22 | return $result; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/files/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/runtime/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/yii.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); 15 | exit($exitCode); --------------------------------------------------------------------------------