├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── Module.php ├── components └── WorkflowDbSource.php ├── controllers ├── DefaultController.php └── StatusController.php ├── messages └── en-US │ └── workflow.php ├── migrations ├── m160815_081611_sw_status.php ├── m160815_081612_sw_transition.php ├── m160815_081613_sw_workflow.php ├── m160815_223711_sw_metadata.php └── m160815_223712_relations.php ├── models ├── Metadata.php ├── Status.php ├── Transition.php ├── Workflow.php └── form │ └── StatusForm.php └── views ├── default ├── _form.php ├── create.php ├── index.php ├── update.php └── view.php ├── layouts └── main.php └── status ├── _form-metadata.php ├── _form.php ├── create.php └── update.php /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/cornernote/yii2-returnurl). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer). 11 | 12 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 13 | 14 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 15 | 16 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 17 | 18 | - **Create feature branches** - Don't ask us to pull from your master branch. 19 | 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | 22 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 23 | 24 | 25 | ## Running Tests 26 | 27 | ``` bash 28 | $ phpunit 29 | ``` 30 | 31 | 32 | **Happy coding**! 33 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The BSD License (BSD) 2 | 3 | Copyright (c) 2013-2015, Mr PHP 4 | 5 | > Redistribution and use in source and binary forms, with or without modification, 6 | > are permitted provided that the following conditions are met: 7 | > 8 | > Redistributions of source code must retain the above copyright notice, this 9 | > list of conditions and the following disclaimer. 10 | > 11 | > Redistributions in binary form must reproduce the above copyright notice, this 12 | > list of conditions and the following disclaimer in the documentation and/or 13 | > other materials provided with the distribution. 14 | > 15 | > Neither the name of Mr PHP. nor the names of its 16 | > contributors may be used to endorse or promote products derived from 17 | > this software without specific prior written permission. 18 | > 19 | >THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 20 | >ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 21 | >WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | >DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 23 | >ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 24 | >(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | >LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 26 | >ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 | >(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 28 | >SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yii2 Workflow Manager 2 | 3 | [![Latest Version](https://img.shields.io/github/tag/cornernote/yii2-workflow-manager.svg?style=flat-square&label=release)](https://github.com/cornernote/yii2-workflow-manager/tags) 4 | [![Software License](https://img.shields.io/badge/license-BSD-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | [![Build Status](https://img.shields.io/travis/cornernote/yii2-workflow-manager/master.svg?style=flat-square)](https://travis-ci.org/cornernote/yii2-workflow-manager) 6 | [![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/cornernote/yii2-workflow-manager.svg?style=flat-square)](https://scrutinizer-ci.com/g/cornernote/yii2-workflow-manager/code-structure) 7 | [![Quality Score](https://img.shields.io/scrutinizer/g/cornernote/yii2-workflow-manager.svg?style=flat-square)](https://scrutinizer-ci.com/g/cornernote/yii2-workflow-manager) 8 | [![Total Downloads](https://img.shields.io/packagist/dt/cornernote/yii2-workflow-manager.svg?style=flat-square)](https://packagist.org/packages/cornernote/yii2-workflow-manager) 9 | 10 | Workflow Manager for Yii2. Extends [Yii2-Workflow](https://github.com/raoul2000/yii2-workflow/) to provide an interface to manage workflows. 11 | 12 | ![screenshot](https://cloud.githubusercontent.com/assets/51875/17660161/a351c124-6316-11e6-8e2b-28340fe6dc8d.png) 13 | 14 | 15 | ## Features 16 | 17 | * Create and manage workflows, statuses and transitions using a simple interface. 18 | * Manage metadata for each status to allow additional data such as colors and icons. 19 | * Displays the workflow transitions using [Yii2 Workflow View](https://github.com/raoul2000/yii2-workflow-view) 20 | 21 | 22 | ## Installation 23 | 24 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 25 | 26 | Either run 27 | 28 | ``` 29 | $ composer require cornernote/yii2-workflow-manager "*" 30 | ``` 31 | 32 | or add 33 | 34 | ``` 35 | "cornernote/yii2-workflow-manager": "*" 36 | ``` 37 | 38 | to the `require` section of your `composer.json` file. 39 | 40 | 41 | ## Migrations 42 | 43 | ``` 44 | $ php yii migrate --migrationPath=@cornernote/workflow/manager/migrations 45 | ``` 46 | 47 | 48 | ## Configuration 49 | 50 | ```php 51 | $config = [ 52 | 'components' => [ 53 | 'workflowSource' => [ 54 | 'class' => 'cornernote\workflow\manager\components\WorkflowDbSource', 55 | ], 56 | ], 57 | 'modules' => [ 58 | 'workflow' => [ 59 | 'class' => 'cornernote\workflow\manager\Module', 60 | ], 61 | ], 62 | ]; 63 | ``` 64 | 65 | 66 | ## Usage 67 | 68 | Simply visit `?r=workflow` within your application to start managing workflows. 69 | 70 | Once you have defined a workflow, you can attach it to a model as follows: 71 | 72 | ```php 73 | class Post extends \yii\db\ActiveRecord 74 | { 75 | public function behaviors() 76 | { 77 | return [ 78 | [ 79 | 'class' => \raoul2000\workflow\base\SimpleWorkflowBehavior::className(), 80 | 'defaultWorkflowId' => 'post', 81 | 'propagateErrorsToModel' => true, 82 | ], 83 | ]; 84 | } 85 | } 86 | ``` 87 | 88 | 89 | ## License 90 | 91 | - Author: Brett O'Donnell 92 | - Source Code: https://github.com/cornernote/yii2-workflow-manager 93 | - Copyright © 2016 Mr PHP 94 | - License: BSD-3-Clause https://raw.github.com/cornernote/yii2-workflow-manager/master/LICENSE 95 | 96 | 97 | ## Links 98 | 99 | - [Yii2 Extension](http://www.yiiframework.com/extension/yii2-workflow-manager) 100 | - [Composer Package](https://packagist.org/packages/cornernote/yii2-workflow-manager) 101 | - [MrPHP](http://mrphp.com.au) 102 | 103 | 104 | [![Mr PHP](https://raw.github.com/cornernote/mrphp-assets/master/img/code-banner.png)](http://mrphp.com.au) 105 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cornernote/yii2-workflow-manager", 3 | "description": "Workflow Manager for Yii2.", 4 | "keywords": ["yii2", "workflow"], 5 | "type": "yii2-extension", 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Brett O'Donnell", 10 | "email": "cornernote@gmail.com", 11 | "homepage": "http://mrphp.com.au/" 12 | } 13 | ], 14 | "require": { 15 | "yiisoft/yii2": "*", 16 | "yiisoft/yii2-jui": "~2.0.0", 17 | "raoul2000/yii2-workflow": "@dev", 18 | "raoul2000/yii2-workflow-view": "@dev" 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "4.*", 22 | "scrutinizer/ocular": "~1.1" 23 | }, 24 | "autoload": { 25 | "psr-4": {"cornernote\\workflow\\manager\\": "src"} 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | i18n->translations['workflow'])) { 26 | Yii::$app->i18n->translations['workflow'] = [ 27 | 'class' => 'yii\i18n\PhpMessageSource', 28 | 'sourceLanguage' => 'en-US', 29 | 'basePath' => '@cornernote/workflow/manager/messages' 30 | ]; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/WorkflowDbSource.php: -------------------------------------------------------------------------------- 1 | 'raoul2000\workflow\base\Workflow', 116 | self::TYPE_STATUS => 'raoul2000\workflow\base\Status', 117 | self::TYPE_TRANSITION => 'raoul2000\workflow\base\Transition' 118 | ]; 119 | 120 | /** 121 | * @var bool[] 122 | */ 123 | private $_allStatusLoaded = []; 124 | 125 | /** 126 | * Constructor method. 127 | * 128 | * @param array $config 129 | * @throws InvalidConfigException 130 | */ 131 | public function __construct($config = []) 132 | { 133 | if (array_key_exists('classMap', $config)) { 134 | if (is_array($config['classMap']) && count($config['classMap']) != 0) { 135 | $this->_classMap = array_merge($this->_classMap, $config['classMap']); 136 | unset($config['classMap']); 137 | // classmap validation 138 | foreach ([self::TYPE_STATUS, self::TYPE_TRANSITION, self::TYPE_WORKFLOW] as $type) { 139 | $className = $this->getClassMapByType($type); 140 | if (empty($className)) { 141 | throw new InvalidConfigException("Invalid class map value : missing class for type " . $type); 142 | } 143 | } 144 | } else { 145 | throw new InvalidConfigException("Invalid property type : 'classMap' must be a non-empty array"); 146 | } 147 | } 148 | 149 | parent::__construct($config); 150 | } 151 | 152 | /** 153 | * @param mixed $id 154 | * @param null $model 155 | * @return Status|StatusInterface 156 | * @throws WorkflowException 157 | */ 158 | public function getStatus($id, $model = null) 159 | { 160 | list($wId, $stId) = $this->parseStatusId($id, $model); 161 | $canonicalStId = $wId . self::SEPARATOR_STATUS_NAME . $stId; 162 | if (!isset($this->_s[$wId])) { 163 | $this->_s[$wId] = []; 164 | } 165 | if (!array_key_exists($canonicalStId, $this->_s[$wId])) { 166 | $statusModel = \cornernote\workflow\manager\models\Status::findOne([ 167 | 'workflow_id' => $wId, 168 | 'id' => $stId 169 | ]); 170 | if ($statusModel == null) { 171 | throw new WorkflowException('No status found with id ' . $id); 172 | } 173 | $this->_s[$wId][$canonicalStId] = Yii::createObject([ 174 | 'class' => $this->getClassMapByType(self::TYPE_STATUS), 175 | 'id' => $canonicalStId, 176 | 'workflowId' => $statusModel->workflow_id, 177 | 'label' => $statusModel->label ? $statusModel->label : Inflector::camel2words($stId, true), 178 | 'source' => $this, 179 | 'metadata' => ArrayHelper::map($statusModel->metadatas, 'key', 'value'), 180 | ]); 181 | } 182 | return $this->_s[$wId][$canonicalStId]; 183 | } 184 | 185 | /** 186 | * @param string $workflowId 187 | * @return Status[]|StatusInterface[] 188 | */ 189 | public function getAllStatuses($workflowId) 190 | { 191 | if (empty($this->_allStatusLoaded[$workflowId])) { 192 | $this->_s[$workflowId] = []; 193 | /** @var \cornernote\workflow\manager\models\Status[] $statusModels */ 194 | $statusModels = \cornernote\workflow\manager\models\Status::find() 195 | ->where(['workflow_id' => $workflowId]) 196 | //->andWhere(['NOT IN', 'id', array_keys($this->_s[$workflowId])]) // removed to fix sort order 197 | ->orderBy(['sort_order' => SORT_ASC]) 198 | ->all(); 199 | foreach ($statusModels as $statusModel) { 200 | $canonicalStId = $workflowId . self::SEPARATOR_STATUS_NAME . $statusModel->id; 201 | $this->_s[$workflowId][$canonicalStId] = Yii::createObject([ 202 | 'class' => $this->getClassMapByType(self::TYPE_STATUS), 203 | 'id' => $canonicalStId, 204 | 'workflowId' => $workflowId, 205 | 'label' => $statusModel->label ? $statusModel->label : Inflector::camel2words($statusModel->id, true), 206 | 'source' => $this, 207 | 'metadata' => ArrayHelper::map($statusModel->metadatas, 'key', 'value'), 208 | ]); 209 | } 210 | $this->_allStatusLoaded[$workflowId] = true; 211 | } 212 | return $this->_s[$workflowId]; 213 | } 214 | 215 | /** 216 | * @param mixed $statusId 217 | * @param null $model 218 | * @return Transition|TransitionInterface[] 219 | * @throws WorkflowException 220 | */ 221 | public function getTransitions($statusId, $model = null) 222 | { 223 | list($wId, $stId) = $this->parseStatusId($statusId, $model); 224 | $statusId = $wId . self::SEPARATOR_STATUS_NAME . $stId; 225 | if (!isset($this->_t[$wId])) { 226 | $this->_t[$wId] = []; 227 | } 228 | if (!array_key_exists($statusId, $this->_t[$wId])) { 229 | $transitions = []; 230 | $transitionModels = \cornernote\workflow\manager\models\Transition::find() 231 | ->andWhere([ 232 | '{{%sw_transition}}.workflow_id' => $wId, 233 | '{{%sw_transition}}.start_status_id' => $stId, 234 | ]) 235 | ->leftJoin('{{%sw_status}}', '{{%sw_status}}.id = {{%sw_transition}}.end_status_id AND {{%sw_status}}.workflow_id = :workflow_id', [ 236 | ':workflow_id' => $wId, 237 | ]) 238 | ->orderBy(['{{%sw_status}}.sort_order' => SORT_ASC]) 239 | ->all(); 240 | foreach ($transitionModels as $transition) { 241 | $endId = $wId . self::SEPARATOR_STATUS_NAME . $transition->end_status_id; 242 | $transitions[] = Yii::createObject([ 243 | 'class' => $this->getClassMapByType(self::TYPE_TRANSITION), 244 | 'start' => $this->getStatus($statusId), 245 | 'end' => $this->getStatus($endId), 246 | 'source' => $this 247 | ]); 248 | } 249 | $this->_t[$wId][$statusId] = $transitions; 250 | } 251 | return $this->_t[$wId][$statusId]; 252 | } 253 | 254 | /** 255 | * @param string $startId 256 | * @param string $endId 257 | * @param null $defaultWorkflowId 258 | * @return null|TransitionInterface 259 | */ 260 | public function getTransition($startId, $endId, $defaultWorkflowId = null) 261 | { 262 | $tr = $this->getTransitions($startId, $defaultWorkflowId); 263 | if (count($tr) > 0) { 264 | foreach ($tr as $aTransition) { 265 | if ($aTransition->getEndStatus()->getId() == $endId) { 266 | return $aTransition; 267 | } 268 | } 269 | } 270 | return null; 271 | } 272 | 273 | /** 274 | * @param mixed $id 275 | * @return Workflow|WorkflowInterface 276 | * @throws WorkflowException 277 | */ 278 | public function getWorkflow($id) 279 | { 280 | if (!array_key_exists($id, $this->_w)) { 281 | $workflow = null; 282 | $def = $this->getWorkflowDefinition($id); 283 | if ($def != null) { 284 | unset($def[self::KEY_NODES]); 285 | $def['id'] = $id; 286 | if (isset($def[Workflow::PARAM_INITIAL_STATUS_ID])) { 287 | $ids = $this->parseStatusId($def[Workflow::PARAM_INITIAL_STATUS_ID], $id); 288 | $def[Workflow::PARAM_INITIAL_STATUS_ID] = implode(self::SEPARATOR_STATUS_NAME, $ids); 289 | } else { 290 | throw new WorkflowException('failed to load Workflow ' . $id . ' : missing initial status id'); 291 | } 292 | $def['class'] = $this->getClassMapByType(self::TYPE_WORKFLOW); 293 | $def['source'] = $this; 294 | $workflow = Yii::createObject($def); 295 | } 296 | $this->_w[$id] = $workflow; 297 | } 298 | return $this->_w[$id]; 299 | } 300 | 301 | /** 302 | * Loads definition for the workflow whose id is passed as argument. 303 | * 304 | * The workflow Id passed as argument is used to create the class name of the object 305 | * that holds the workflow definition. 306 | * 307 | * @param string $id 308 | * @return mixed 309 | * @throws WorkflowException the definition could not be loaded 310 | */ 311 | public function getWorkflowDefinition($id) 312 | { 313 | if (!$this->isValidWorkflowId($id)) { 314 | throw new WorkflowException('Invalid workflow Id : ' . VarDumper::dumpAsString($id)); 315 | } 316 | if (!isset($this->_workflowDef[$id])) { 317 | if ($this->getDefinitionCache() != null) { 318 | $cache = $this->getDefinitionCache(); 319 | $key = $cache->buildKey('yii2-workflow-def-' . $id); 320 | if ($cache->exists($key)) { 321 | $this->_workflowDef[$id] = $cache->get($key); 322 | } else { 323 | $this->_workflowDef[$id] = $this->loadDefinition($id); 324 | $cache->set($key, $this->_workflowDef[$id]); 325 | } 326 | } else { 327 | $this->_workflowDef[$id] = $this->loadDefinition($id); 328 | } 329 | } 330 | return $this->_workflowDef[$id]; 331 | } 332 | 333 | /** 334 | * Loads the definition oa a workflow. 335 | * 336 | * @param string $id 337 | * @return \cornernote\workflow\manager\models\Workflow 338 | * @throws WorkflowException 339 | * @internal param IWorkflowSource $source 340 | */ 341 | public function loadDefinition($id) 342 | { 343 | $workflowModel = \cornernote\workflow\manager\models\Workflow::findOne(['id' => $id]); 344 | if (!$workflowModel) { 345 | return null; 346 | //throw new WorkflowException('No workflow found with id ' . $id); 347 | } 348 | return [ 349 | 'class' => 'raoul2000\workflow\base\Workflow', 350 | 'id' => $workflowModel->id, 351 | Workflow::PARAM_INITIAL_STATUS_ID => $workflowModel->id . self::SEPARATOR_STATUS_NAME . $workflowModel->initial_status_id, 352 | 'source' => $this 353 | ]; 354 | } 355 | 356 | /** 357 | * Return the workflow definition cache component used by this workflow source or NULL if no cache is used. 358 | * @return null|Cache 359 | * @throws InvalidConfigException 360 | */ 361 | public function getDefinitionCache() 362 | { 363 | if (!isset($this->definitionCache)) { 364 | return null; 365 | } 366 | if (!isset($this->_dc)) { 367 | if (is_string($this->definitionCache)) { 368 | $this->_dc = Yii::$app->get($this->definitionCache); 369 | } elseif (is_array($this->definitionCache)) { 370 | $this->_dc = Yii::createObject($this->definitionCache); 371 | } elseif (is_object($this->definitionCache)) { 372 | $this->_dc = $this->definitionCache; 373 | } else { 374 | throw new InvalidConfigException('invalid "definitionCache" attribute : string or object expected'); 375 | } 376 | if (!$this->_dc instanceof Cache) { 377 | throw new InvalidConfigException('the workflow definition cache must implement the yii\caching\Cache interface'); 378 | } 379 | } 380 | return $this->_dc; 381 | } 382 | 383 | /** 384 | * Returns the class map array for this Workflow source instance. 385 | * 386 | * @return string[] 387 | */ 388 | public function getClassMap() 389 | { 390 | return $this->_classMap; 391 | } 392 | 393 | /** 394 | * Returns the class name that implement the type passed as argument. 395 | * There are 3 built-in types that must have a class name : 396 | * 397 | * - self::TYPE_WORKFLOW 398 | * - self::TYPE_STATUS 399 | * - self::TYPE_TRANSITION 400 | * 401 | * The constructor ensure that if a class map is provided, it include class names for these 3 types. Failure to do so 402 | * will result in an exception being thrown by the constructor. 403 | * 404 | * @param string $type Type name 405 | * @return string | null the class name or NULL if no class name is found forthis type. 406 | */ 407 | public function getClassMapByType($type) 408 | { 409 | return array_key_exists($type, $this->_classMap) ? $this->_classMap[$type] : null; 410 | } 411 | 412 | /** 413 | * @param string $val canonical id (e.g. myWorkflow/myStatus) 414 | * @param BaseActiveRecord|SimpleWorkflowBehavior $helper 415 | * @return array 416 | * @throws WorkflowException 417 | */ 418 | public function parseStatusId($val, $helper = null) 419 | { 420 | if (empty($val) || !is_string($val)) { 421 | throw new WorkflowException('Not a valid status id : a non-empty string is expected - status = ' . VarDumper::dumpAsString($val)); 422 | } 423 | $tokens = array_map('trim', explode(self::SEPARATOR_STATUS_NAME, $val)); 424 | $tokenCount = count($tokens); 425 | if ($tokenCount == 1) { 426 | $tokens[1] = $tokens[0]; 427 | $tokens[0] = null; 428 | if (!empty($helper)) { 429 | if (is_string($helper)) { 430 | $tokens[0] = $helper; 431 | } elseif ($helper instanceof BaseActiveRecord) { 432 | $tokens[0] = $helper->hasWorkflowStatus() 433 | ? $helper->getWorkflowStatus()->getWorkflowId() 434 | : $helper->getDefaultWorkflowId(); 435 | } 436 | } 437 | if ($tokens[0] === null) { 438 | throw new WorkflowException('Not a valid status id format: failed to get workflow id - status = ' . VarDumper::dumpAsString($val)); 439 | } 440 | } elseif ($tokenCount != 2) { 441 | throw new WorkflowException('Not a valid status id format: ' . VarDumper::dumpAsString($val)); 442 | } 443 | 444 | if (!$this->isValidWorkflowId($tokens[0])) { 445 | throw new WorkflowException('Not a valid status id : incorrect workflow id format in ' . VarDumper::dumpAsString($val)); 446 | } elseif (!$this->isValidStatusLocalId($tokens[1])) { 447 | throw new WorkflowException('Not a valid status id : incorrect status local id format in ' . VarDumper::dumpAsString($val)); 448 | } 449 | return $tokens; 450 | } 451 | 452 | /** 453 | * Checks if the string passed as argument can be used as a workflow ID. 454 | * 455 | * A workflow ID is a string that matches self::PATTERN_ID. 456 | * 457 | * @param string $val 458 | * @return boolean TRUE if the $val can be used as workflow id, FALSE otherwise 459 | */ 460 | public function isValidWorkflowId($val) 461 | { 462 | return is_string($val) && preg_match(self::PATTERN_ID, $val) != 0; 463 | } 464 | 465 | /** 466 | * Checks if the string passed as argument can be used as a status local ID. 467 | * 468 | * @param string $val 469 | * @return boolean 470 | */ 471 | public function isValidStatusLocalId($val) 472 | { 473 | return is_string($val) && preg_match(self::PATTERN_ID, $val) != 0; 474 | } 475 | 476 | } 477 | -------------------------------------------------------------------------------- /src/controllers/DefaultController.php: -------------------------------------------------------------------------------- 1 | render('index'); 25 | } 26 | 27 | /** 28 | * Displays a single Workflow model. 29 | * @param string $id 30 | * @return \yii\web\Response 31 | */ 32 | public function actionView($id) 33 | { 34 | $model = $this->findModel($id); 35 | 36 | // save transitions 37 | if (isset($_POST['Status'])) { 38 | foreach ($_POST['Status'] as $start_status_id => $statuses) { 39 | foreach ($statuses as $end_status_id => $checked) { 40 | $transition = Transition::findOne(['workflow_id' => $model->id, 'start_status_id' => $start_status_id, 'end_status_id' => $end_status_id]); 41 | if ($checked) { 42 | if (!$transition) { 43 | $transition = new Transition(); 44 | $transition->workflow_id = $model->id; 45 | $transition->start_status_id = $start_status_id; 46 | $transition->end_status_id = $end_status_id; 47 | $transition->save(); 48 | } 49 | } else { 50 | if ($transition) { 51 | $transition->delete(); 52 | } 53 | } 54 | } 55 | } 56 | return $this->redirect(['view', 'id' => $model->id]); 57 | } 58 | 59 | return $this->render('view', [ 60 | 'model' => $model, 61 | ]); 62 | } 63 | 64 | /** 65 | * Creates a new Workflow model. 66 | * If creation is successful, the browser will be redirected to the 'view' page. 67 | * @return \yii\web\Response 68 | */ 69 | public function actionCreate() 70 | { 71 | $model = new Workflow; 72 | if ($model->load($_POST) && $model->save()) { 73 | return $this->redirect(['view', 'id' => $model->id]); 74 | } 75 | return $this->render('create', ['model' => $model]); 76 | } 77 | 78 | /** 79 | * Updates an existing Workflow model. 80 | * If update is successful, the browser will be redirected to the 'view' page. 81 | * @param string $id 82 | * @return \yii\web\Response 83 | */ 84 | public function actionUpdate($id) 85 | { 86 | $model = $this->findModel($id); 87 | 88 | if ($model->load($_POST) && $model->save()) { 89 | return $this->redirect(['view', 'id' => $model->id]); 90 | } 91 | return $this->render('update', [ 92 | 'model' => $model, 93 | ]); 94 | } 95 | 96 | /** 97 | * Updates the Initial Status of a Workflow model. 98 | * @param string $id 99 | * @param int $status_id 100 | * @return \yii\web\Response 101 | */ 102 | public function actionInitial($id, $status_id) 103 | { 104 | $model = $this->findModel($id); 105 | $model->initial_status_id = $status_id; 106 | $model->save(false, ['initial_status_id']); 107 | return $this->redirect(['view', 'id' => $model->id]); 108 | } 109 | 110 | /** 111 | * Sets the sort order of Status models. 112 | * @param $id 113 | * @throws HttpException 114 | */ 115 | public function actionSort($id) 116 | { 117 | $model = $this->findModel($id); 118 | if (Yii::$app->request->post('Status')) { 119 | foreach (Yii::$app->request->post('Status') as $k => $id) { 120 | $status = Status::findOne(['id' => $id, 'workflow_id' => $model->id]); 121 | if ($status) { 122 | $status->sort_order = $k; 123 | $status->save(false); 124 | } 125 | } 126 | } 127 | } 128 | 129 | /** 130 | * Deletes an existing Workflow model. 131 | * If deletion is successful, the browser will be redirected to the 'index' page. 132 | * @param string $id 133 | * @return \yii\web\Response 134 | */ 135 | public function actionDelete($id) 136 | { 137 | $this->findModel($id)->delete(); 138 | return $this->redirect(['index']); 139 | } 140 | 141 | /** 142 | * Finds the Workflow model based on its primary key value. 143 | * If the model is not found, a 404 HTTP exception will be thrown. 144 | * @param string $id 145 | * @return Workflow the loaded model 146 | * @throws HttpException if the model cannot be found 147 | */ 148 | protected function findModel($id) 149 | { 150 | if (($model = Workflow::findOne($id)) !== null) { 151 | return $model; 152 | } else { 153 | throw new HttpException(404, Yii::t('workflow', 'The requested page does not exist.')); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/controllers/StatusController.php: -------------------------------------------------------------------------------- 1 | status = new Status(); 27 | $model->status->loadDefaultValues(); 28 | $model->setAttributes(Yii::$app->request->post()); 29 | $model->status->workflow_id = $workflow_id; 30 | 31 | if (Yii::$app->request->post() && $model->save()) { 32 | Yii::$app->getSession()->setFlash('success', 'Status has been created.'); 33 | return $this->redirect(['default/view', 'id' => $model->status->workflow_id]); 34 | } 35 | return $this->render('create', ['model' => $model]); 36 | } 37 | 38 | /** 39 | * Updates an existing Status model. 40 | * If update is successful, the browser will be redirected to the 'view' page. 41 | * @param string $id 42 | * @param string $workflow_id 43 | * @return \yii\web\Response 44 | */ 45 | public function actionUpdate($id, $workflow_id) 46 | { 47 | $model = new StatusForm(); 48 | $model->status = $this->findModel($id, $workflow_id); 49 | $model->setAttributes(Yii::$app->request->post()); 50 | 51 | if (Yii::$app->request->post() && $model->save()) { 52 | Yii::$app->getSession()->setFlash('success', 'Status has been updated.'); 53 | return $this->redirect(['default/view', 'id' => $model->status->workflow_id]); 54 | } 55 | return $this->render('update', ['model' => $model]); 56 | } 57 | 58 | /** 59 | * Deletes an existing Status model. 60 | * If deletion is successful, the browser will be redirected to the 'index' page. 61 | * @param string $id 62 | * @param string $workflow_id 63 | * @return \yii\web\Response 64 | */ 65 | public function actionDelete($id, $workflow_id) 66 | { 67 | $model = $this->findModel($id, $workflow_id); 68 | if ($model->workflow->initial_status_id != $model->id) { 69 | $model->delete(); 70 | } 71 | return $this->redirect(['default/view', 'id' => $model->workflow_id]); 72 | } 73 | 74 | /** 75 | * Finds the Status model based on its primary key value. 76 | * If the model is not found, a 404 HTTP exception will be thrown. 77 | * @param string $id 78 | * @param string $workflow_id 79 | * @return Status the loaded model 80 | * @throws HttpException if the model cannot be found 81 | */ 82 | protected function findModel($id, $workflow_id) 83 | { 84 | if (($model = Status::findOne(['id' => $id, 'workflow_id' => $workflow_id])) !== null) { 85 | return $model; 86 | } else { 87 | throw new HttpException(404, Yii::t('workflow', 'The requested page does not exist.')); 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /src/messages/en-US/workflow.php: -------------------------------------------------------------------------------- 1 | 'Workflow', 5 | ]; 6 | -------------------------------------------------------------------------------- /src/migrations/m160815_081611_sw_status.php: -------------------------------------------------------------------------------- 1 | createTable('{{%sw_status}}', [ 11 | 'id' => $this->string(32)->notNull(), 12 | 'workflow_id' => $this->string(32)->notNull(), 13 | 'label' => $this->string(64)->null()->defaultValue(null), 14 | 'sort_order' => $this->integer(11)->null()->defaultValue(null), 15 | 'PRIMARY KEY (id, workflow_id)', 16 | ], 'ENGINE=InnoDB'); 17 | $this->createIndex('workflow_id', '{{%sw_status}}', 'workflow_id'); 18 | } 19 | 20 | public function safeDown() 21 | { 22 | $this->dropIndex('workflow_id', '{{%sw_status}}'); 23 | $this->dropTable('{{%sw_status}}'); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/migrations/m160815_081612_sw_transition.php: -------------------------------------------------------------------------------- 1 | createTable('{{%sw_transition}}', [ 11 | 'workflow_id' => $this->string(32)->notNull(), 12 | 'start_status_id' => $this->string(32)->notNull(), 13 | 'end_status_id' => $this->string(32)->notNull(), 14 | 'PRIMARY KEY (workflow_id, start_status_id, end_status_id)', 15 | ], 'ENGINE=InnoDB'); 16 | $this->createIndex('workflow_start_status_id', '{{%sw_transition}}', ['workflow_id', 'start_status_id']); 17 | $this->createIndex('workflow_end_status_id', '{{%sw_transition}}', ['workflow_id', 'end_status_id']); 18 | } 19 | 20 | public function safeDown() 21 | { 22 | $this->dropIndex('workflow_start_status_id', '{{%sw_transition}}'); 23 | $this->dropIndex('workflow_end_status_id', '{{%sw_transition}}'); 24 | $this->dropTable('{{%sw_transition}}'); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/migrations/m160815_081613_sw_workflow.php: -------------------------------------------------------------------------------- 1 | createTable('{{%sw_workflow}}', [ 11 | 'id' => $this->string(32)->notNull(), 12 | 'initial_status_id' => $this->string(32)->null()->defaultValue(null), 13 | 'PRIMARY KEY (id)', 14 | ], 'ENGINE=InnoDB'); 15 | $this->createIndex('initial_status_id', '{{%sw_workflow}}', 'initial_status_id'); 16 | } 17 | 18 | public function safeDown() 19 | { 20 | $this->dropIndex('initial_status_id', '{{%sw_workflow}}'); 21 | $this->dropTable('{{%sw_workflow}}'); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/migrations/m160815_223711_sw_metadata.php: -------------------------------------------------------------------------------- 1 | createTable('{{%sw_metadata}}', [ 12 | 'workflow_id' => $this->string(32)->notNull(), 13 | 'status_id' => $this->string(32)->notNull(), 14 | 'key' => $this->string(64)->notNull(), 15 | 'value' => $this->string(255)->null()->defaultValue(null), 16 | ], 'ENGINE=InnoDB'); 17 | $this->createIndex('workflow_status_id', '{{%sw_metadata}}', ['workflow_id', 'status_id', 'key'], true); 18 | } 19 | 20 | public function safeDown() 21 | { 22 | $this->dropIndex('workflow_status_id', '{{%sw_metadata}}'); 23 | $this->dropTable('{{%sw_metadata}}'); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/migrations/m160815_223712_relations.php: -------------------------------------------------------------------------------- 1 | addForeignKey('fk_sw_status_workflow_id', '{{%sw_status}}', 'workflow_id', 'sw_workflow', 'id'); 12 | //$this->addForeignKey('fk_sw_transition_workflow_id', '{{%sw_transition}}', 'workflow_id', 'sw_workflow', 'id'); 13 | //$this->addForeignKey('fk_sw_transition_start_status_id', '{{%sw_transition}}', 'start_status_id', 'sw_status', 'id'); 14 | //$this->addForeignKey('fk_sw_transition_end_status_id', '{{%sw_transition}}', 'end_status_id', 'sw_status', 'id'); 15 | //$this->addForeignKey('fk_sw_workflow_initial_status_id', '{{%sw_workflow}}', 'initial_status_id', 'sw_status', 'id'); 16 | //$this->addForeignKey('fk_sw_metadata_status_id', '{{%sw_metadata}}', 'status_id', 'sw_status', 'id'); 17 | //$this->addForeignKey('fk_sw_metadata_workflow_id', '{{%sw_metadata}}', 'workflow_id', 'sw_workflow', 'id'); 18 | } 19 | 20 | public function safeDown() 21 | { 22 | //$this->dropForeignKey('fk_sw_status_workflow_id', '{{%sw_status}}'); 23 | //$this->dropForeignKey('fk_sw_transition_workflow_id', '{{%sw_transition}}'); 24 | //$this->dropForeignKey('fk_sw_transition_start_status_id', '{{%sw_transition}}'); 25 | //$this->dropForeignKey('fk_sw_transition_end_status_id', '{{%sw_transition}}'); 26 | //$this->dropForeignKey('fk_sw_workflow_initial_status_id', '{{%sw_workflow}}'); 27 | //$this->dropForeignKey('fk_sw_metadata_status_id', '{{%sw_metadata}}'); 28 | //$this->dropForeignKey('fk_sw_metadata_workflow_id', '{{%sw_metadata}}'); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/models/Metadata.php: -------------------------------------------------------------------------------- 1 | 32], 37 | [['key'], 'string', 'max' => 64], 38 | [['value'], 'string', 'max' => 255] 39 | ]; 40 | } 41 | 42 | /** 43 | * @inheritdoc 44 | */ 45 | public function attributeLabels() 46 | { 47 | return [ 48 | 'workflow_id' => Yii::t('app', 'Workflow'), 49 | 'status_id' => Yii::t('app', 'Status'), 50 | 'key' => Yii::t('app', 'Key'), 51 | 'value' => Yii::t('app', 'Value'), 52 | ]; 53 | } 54 | 55 | /** 56 | * @return \yii\db\ActiveQuery 57 | */ 58 | public function getStatus() 59 | { 60 | return $this->hasOne(Status::className(), ['id' => 'status_id'])->andWhere(['workflow_id' => $this->workflow_id]); 61 | } 62 | 63 | /** 64 | * @return \yii\db\ActiveQuery 65 | */ 66 | public function getWorkflow() 67 | { 68 | return $this->hasOne(Workflow::className(), ['id' => 'workflow_id']); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/models/Status.php: -------------------------------------------------------------------------------- 1 | 32], 43 | [['label'], 'string', 'max' => 64], 44 | [['workflow_id'], 'exist', 'skipOnError' => true, 'targetClass' => Workflow::className(), 'targetAttribute' => ['workflow_id' => 'id']] 45 | ]; 46 | } 47 | 48 | /** 49 | * @inheritdoc 50 | */ 51 | public function attributeLabels() 52 | { 53 | return [ 54 | 'id' => Yii::t('app', 'ID'), 55 | 'workflow_id' => Yii::t('app', 'Workflow'), 56 | 'label' => Yii::t('app', 'Label'), 57 | ]; 58 | } 59 | 60 | /** 61 | * @return string 62 | */ 63 | public function getName() 64 | { 65 | return $this->label ? $this->label : Inflector::camel2words($this->id); 66 | } 67 | 68 | /** 69 | * @return \yii\db\ActiveQuery 70 | */ 71 | public function getWorkflow() 72 | { 73 | return $this->hasOne(Workflow::className(), ['id' => 'workflow_id']); 74 | } 75 | 76 | /** 77 | * @return \yii\db\ActiveQuery 78 | */ 79 | public function getStartTransitions() 80 | { 81 | return $this->hasMany(Transition::className(), ['start_status_id' => 'id']) 82 | ->andWhere(['{{%sw_transition}}.workflow_id' => $this->workflow_id]) 83 | ->leftJoin('{{%sw_status}}', '{{%sw_status}}.id = {{%sw_transition}}.end_status_id AND {{%sw_status}}.workflow_id = :workflow_id', [ 84 | ':workflow_id' => $this->workflow_id, 85 | ]) 86 | ->orderBy(['{{%sw_status}}.sort_order' => SORT_ASC]); 87 | } 88 | 89 | /** 90 | * @return \yii\db\ActiveQuery 91 | */ 92 | public function getEndTransitions() 93 | { 94 | return $this->hasMany(Transition::className(), ['end_status_id' => 'id']) 95 | ->andWhere(['{{%sw_transition}}.workflow_id' => $this->workflow_id]) 96 | ->leftJoin('{{%sw_status}}', '{{%sw_status}}.id = {{%sw_transition}}.start_status_id AND {{%sw_status}}.workflow_id = :workflow_id', [ 97 | ':workflow_id' => $this->workflow_id, 98 | ]) 99 | ->orderBy(['{{%sw_status}}.sort_order' => SORT_ASC]); 100 | } 101 | 102 | /** 103 | * @return \yii\db\ActiveQuery 104 | */ 105 | public function getMetadatas() 106 | { 107 | return $this->hasMany(Metadata::className(), ['status_id' => 'id'])->andWhere(['workflow_id' => $this->workflow_id]); 108 | } 109 | 110 | /** 111 | * @inheritdoc 112 | */ 113 | public function beforeSave($insert) 114 | { 115 | if ($insert) { 116 | if ($this->sort_order === null) { 117 | $lowest = static::find()->andWhere(['workflow_id' => $this->workflow_id])->orderBy(['sort_order' => SORT_DESC])->one(); 118 | $this->sort_order = $lowest ? $lowest->sort_order + 1 : 1; 119 | } 120 | } 121 | if (!$insert && $this->id != $this->oldAttributes['id']) { 122 | $id = $this->id; 123 | $this->id = $this->oldAttributes['id']; 124 | if ($this->workflow->initial_status_id == $this->id) { 125 | $this->workflow->initial_status_id = $id; 126 | $this->workflow->save(false, ['initial_status_id']); 127 | } 128 | foreach ($this->startTransitions as $startTransition) { 129 | $startTransition->start_status_id = $id; 130 | $startTransition->save(false, ['start_status_id']); 131 | } 132 | foreach ($this->endTransitions as $endTransition) { 133 | $endTransition->end_status_id = $id; 134 | $endTransition->save(false, ['end_status_id']); 135 | } 136 | foreach ($this->metadatas as $metadata) { 137 | $metadata->delete(); 138 | } 139 | $this->id = $id; 140 | } 141 | return parent::beforeSave($insert); 142 | } 143 | 144 | /** 145 | * @inheritdoc 146 | */ 147 | public function afterSave($insert, $changedAttributes) 148 | { 149 | if ($this->workflow && !$this->workflow->initial_status_id) { 150 | $this->workflow->initial_status_id = $this->id; 151 | $this->workflow->save(false, ['initial_status_id']); 152 | } 153 | parent::afterSave($insert, $changedAttributes); 154 | } 155 | 156 | /** 157 | * @inheritdoc 158 | */ 159 | public function beforeDelete() 160 | { 161 | foreach ($this->metadatas as $metadata) { 162 | $metadata->delete(); 163 | } 164 | foreach ($this->startTransitions as $startTransition) { 165 | $startTransition->delete(); 166 | } 167 | foreach ($this->endTransitions as $endTransition) { 168 | $endTransition->delete(); 169 | } 170 | return parent::beforeDelete(); 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /src/models/Transition.php: -------------------------------------------------------------------------------- 1 | 32], 39 | [['start_status_id'], 'exist', 'skipOnError' => true, 'targetClass' => Status::className(), 'targetAttribute' => ['start_status_id' => 'id']], 40 | [['end_status_id'], 'exist', 'skipOnError' => true, 'targetClass' => Status::className(), 'targetAttribute' => ['end_status_id' => 'id']] 41 | ]; 42 | } 43 | 44 | /** 45 | * @inheritdoc 46 | */ 47 | public function attributeLabels() 48 | { 49 | return [ 50 | 'start_status_id' => Yii::t('app', 'Start Status'), 51 | 'end_status_id' => Yii::t('app', 'End Status'), 52 | ]; 53 | } 54 | 55 | /** 56 | * @return string 57 | */ 58 | public function getStartName() 59 | { 60 | return $this->startStatus->getName(); 61 | } 62 | 63 | /** 64 | * @return string 65 | */ 66 | public function getEndName() 67 | { 68 | return $this->endStatus->getName(); 69 | } 70 | 71 | /** 72 | * @return \yii\db\ActiveQuery 73 | */ 74 | public function getEndStatus() 75 | { 76 | return $this->hasOne(Status::className(), ['id' => 'end_status_id'])->andWhere(['workflow_id' => $this->workflow_id]); 77 | } 78 | 79 | /** 80 | * @return \yii\db\ActiveQuery 81 | */ 82 | public function getStartStatus() 83 | { 84 | return $this->hasOne(Status::className(), ['id' => 'start_status_id'])->andWhere(['workflow_id' => $this->workflow_id]); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/models/Workflow.php: -------------------------------------------------------------------------------- 1 | 32], 37 | [['initial_status_id'], 'exist', 'skipOnError' => true, 'targetClass' => Status::className(), 'targetAttribute' => ['initial_status_id' => 'id']] 38 | ]; 39 | } 40 | 41 | /** 42 | * @inheritdoc 43 | */ 44 | public function attributeLabels() 45 | { 46 | return [ 47 | 'id' => Yii::t('app', 'ID'), 48 | 'initial_status_id' => Yii::t('app', 'Initial Status'), 49 | ]; 50 | } 51 | 52 | /** 53 | * @return \yii\db\ActiveQuery 54 | */ 55 | public function getStatuses() 56 | { 57 | return $this->hasMany(Status::className(), ['workflow_id' => 'id'])->orderBy(['sort_order' => SORT_ASC]); 58 | } 59 | 60 | /** 61 | * @return \yii\db\ActiveQuery 62 | */ 63 | public function getInitialStatus() 64 | { 65 | return $this->hasOne(Status::className(), ['id' => 'initial_status_id']); 66 | } 67 | 68 | /** 69 | * @return \yii\db\ActiveQuery 70 | */ 71 | public function getTransitions() 72 | { 73 | return $this->hasMany(Transition::className(), ['workflow_id' => 'id']); 74 | } 75 | 76 | /** 77 | * @return \yii\db\ActiveQuery 78 | */ 79 | public function getMetadatas() 80 | { 81 | return $this->hasMany(Metadata::className(), ['workflow_id' => 'id']); 82 | } 83 | 84 | /** 85 | * @inheritdoc 86 | */ 87 | public function beforeSave($insert) 88 | { 89 | if (!$insert && $this->id != $this->oldAttributes['id']) { 90 | $id = $this->id; 91 | $this->id = $this->oldAttributes['id']; 92 | foreach ($this->statuses as $status) { 93 | $status->workflow_id = $id; 94 | $status->save(false, ['workflow_id']); 95 | } 96 | foreach ($this->transitions as $transition) { 97 | $transition->workflow_id = $id; 98 | $transition->save(false, ['workflow_id']); 99 | } 100 | foreach ($this->metadatas as $metadata) { 101 | $metadata->workflow_id = $id; 102 | $metadata->save(false, ['workflow_id']); 103 | } 104 | $this->id = $id; 105 | } 106 | return parent::beforeSave($insert); 107 | } 108 | 109 | /** 110 | * @inheritdoc 111 | */ 112 | public function beforeDelete() 113 | { 114 | $this->initial_status_id = null; 115 | $this->save(false, ['initial_status_id']); 116 | foreach ($this->statuses as $status) { 117 | $status->delete(); 118 | } 119 | return parent::beforeDelete(); 120 | } 121 | 122 | /** 123 | * @return string 124 | */ 125 | public function getColor() 126 | { 127 | $string = $this->id; 128 | $darker = 1.3; 129 | $rgb = substr(dechex(crc32(str_repeat($string, 10) . md5($string))), 0, 6); 130 | list($R16, $G16, $B16) = str_split($rgb, 2); 131 | $R = sprintf("%02X", floor(hexdec($R16) / $darker)); 132 | $G = sprintf("%02X", floor(hexdec($G16) / $darker)); 133 | $B = sprintf("%02X", floor(hexdec($B16) / $darker)); 134 | return '#' . $R . $G . $B; 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/models/form/StatusForm.php: -------------------------------------------------------------------------------- 1 | getAllModels())) { 48 | $this->addError(null); // add an empty error to prevent saving 49 | } 50 | parent::afterValidate(); 51 | } 52 | 53 | /** 54 | * @return bool 55 | * @throws \yii\db\Exception 56 | */ 57 | public function save() 58 | { 59 | if (!$this->validate()) { 60 | return false; 61 | } 62 | $transaction = Yii::$app->db->beginTransaction(); 63 | if (!$this->status->save()) { 64 | $transaction->rollBack(); 65 | return false; 66 | } 67 | if (!$this->saveMetadatas()) { 68 | $transaction->rollBack(); 69 | return false; 70 | } 71 | $transaction->commit(); 72 | return true; 73 | } 74 | 75 | /** 76 | * @return bool 77 | * @throws \Exception 78 | */ 79 | public function saveMetadatas() 80 | { 81 | $keep = []; 82 | foreach ($this->metadatas as $metadata) { 83 | $metadata->status_id = $this->status->id; 84 | $metadata->workflow_id = $this->status->workflow_id; 85 | if (!$metadata->save(false)) { 86 | return false; 87 | } 88 | $keep[] = $metadata->key; 89 | } 90 | $query = Metadata::find()->andWhere(['status_id' => $this->status->id, 'workflow_id' => $this->status->workflow_id]); 91 | if ($keep) { 92 | $query->andWhere(['not in', 'key', $keep]); 93 | } 94 | foreach ($query->all() as $metadata) { 95 | $metadata->delete(); 96 | } 97 | return true; 98 | } 99 | 100 | /** 101 | * @return Status 102 | */ 103 | public function getStatus() 104 | { 105 | return $this->_status; 106 | } 107 | 108 | /** 109 | * @param Status|array $status 110 | */ 111 | public function setStatus($status) 112 | { 113 | if ($status instanceof Status) { 114 | $this->_status = $status; 115 | } else if (is_array($status)) { 116 | $this->_status->setAttributes($status); 117 | } 118 | } 119 | 120 | /** 121 | * @return Metadata[] 122 | */ 123 | public function getMetadatas() 124 | { 125 | if ($this->_metadatas === null) { 126 | $this->_metadatas = $this->status->isNewRecord ? [] : $this->status->metadatas; 127 | } 128 | return $this->_metadatas; 129 | } 130 | 131 | /** 132 | * @param $key 133 | * @return Metadata 134 | */ 135 | private function getMetadata($key) 136 | { 137 | $metadata = $key && strpos($key, 'new') === false ? Metadata::findOne(['key' => $key, 'status_id' => $this->status->id, 'workflow_id' => $this->status->workflow_id]) : false; 138 | if (!$metadata) { 139 | $metadata = new Metadata(); 140 | $metadata->loadDefaultValues(); 141 | } 142 | return $metadata; 143 | } 144 | 145 | /** 146 | * @param Metadata[]|array $metadatas 147 | */ 148 | public function setMetadatas($metadatas) 149 | { 150 | unset($metadatas['__id__']); // remove the hidden "new Metadata" row 151 | $this->_metadatas = []; 152 | foreach ($metadatas as $key => $metadata) { 153 | if (is_array($metadata)) { 154 | $this->_metadatas[$key] = $this->getMetadata($key); 155 | $this->_metadatas[$key]->setAttributes($metadata); 156 | } elseif ($metadata instanceof Metadata) { 157 | $this->_metadatas[$metadata->id] = $metadata; 158 | } 159 | } 160 | } 161 | 162 | /** 163 | * @param ActiveForm $form 164 | * @return mixed 165 | */ 166 | public function errorSummary($form) 167 | { 168 | $errorLists = []; 169 | foreach ($this->getAllModels() as $id => $model) { 170 | $errorList = $form->errorSummary($model, [ 171 | 'header' => '

Please fix the following errors for ' . $id . '

', 172 | ]); 173 | $errorList = str_replace('
  • ', '', $errorList); // remove the empty error 174 | $errorLists[] = $errorList; 175 | } 176 | return implode('', $errorLists); 177 | } 178 | 179 | /** 180 | * @return ActiveRecord[] 181 | */ 182 | private function getAllModels() 183 | { 184 | $models = [ 185 | //'form' => $this, 186 | 'status' => $this->status, 187 | ]; 188 | foreach ($this->metadatas as $id => $metadata) { 189 | $models['Metadata.' . $id] = $this->metadatas[$id]; 190 | } 191 | return $models; 192 | } 193 | } -------------------------------------------------------------------------------- /src/views/default/_form.php: -------------------------------------------------------------------------------- 1 | 11 | 12 |
    13 | 14 | 'Workflow', 16 | 'enableClientValidation' => false, 17 | 'errorSummaryCssClass' => 'error-summary alert alert-error' 18 | ]) ?> 19 | 20 | errorSummary($model); ?> 21 | 22 | field($model, 'id')->textInput(['maxlength' => true]) ?> 23 | 24 | ' . ($model->isNewRecord ? Yii::t('workflow', 'Create') : Yii::t('workflow', 'Save')), [ 25 | 'id' => 'save-' . $model->formName(), 26 | 'class' => 'btn btn-success' 27 | ]) ?> 28 | 'btn btn-default']) ?> 29 | 30 | 31 | 32 |
    33 | 34 | -------------------------------------------------------------------------------- /src/views/default/create.php: -------------------------------------------------------------------------------- 1 | title = Yii::t('workflow', 'Create'); 10 | $this->params['breadcrumbs'][] = ['label' => Yii::t('workflow', 'Workflow'), 'url' => ['index']]; 11 | $this->params['breadcrumbs'][] = $this->title; 12 | ?> 13 |
    14 | 15 |

    16 | title) ?> 17 |

    18 | 19 | render('_form', [ 20 | 'model' => $model, 21 | ]); ?> 22 | 23 |
    24 | -------------------------------------------------------------------------------- /src/views/default/index.php: -------------------------------------------------------------------------------- 1 | title = Yii::t('workflow', 'Workflow'); 11 | $this->params['breadcrumbs'][] = $this->title; 12 | ?> 13 |
    14 | 15 |

    16 | title) ?> 17 |

    18 | 19 | ' ' . Yii::t('workflow', 'Create'), 23 | 'url' => ['create'], 24 | 'encode' => false, 25 | ], 26 | ]; 27 | foreach (Workflow::find()->orderBy(['id' => SORT_ASC])->all() as $workflow) { 28 | /** @var Workflow $workflow */ 29 | $items[] = [ 30 | 'label' => $workflow->id, 31 | 'url' => ['view', 'id' => $workflow->id], 32 | 'linkOptions' => ['style' => 'color:#fff;background:' . $workflow->getColor()], 33 | ]; 34 | } 35 | echo Nav::widget([ 36 | 'items' => $items, 37 | 'options' => ['class' => 'nav-pills'], 38 | ]); 39 | ?> 40 | 41 |
    42 | -------------------------------------------------------------------------------- /src/views/default/update.php: -------------------------------------------------------------------------------- 1 | title = $model->id; 10 | $this->params['breadcrumbs'][] = ['label' => Yii::t('workflow', 'Workflow'), 'url' => ['index']]; 11 | $this->params['breadcrumbs'][] = ['label' => $model->id, 'url' => ['view', 'id' => $model->id]]; 12 | $this->params['breadcrumbs'][] = Yii::t('workflow', 'Update'); 13 | ?> 14 |
    15 | 16 |

    17 | title) ?> 18 |

    19 | 20 | render('_form', [ 21 | 'model' => $model, 22 | ]); ?> 23 | 24 |
    25 | -------------------------------------------------------------------------------- /src/views/default/view.php: -------------------------------------------------------------------------------- 1 | title = $model->id; 18 | $this->params['breadcrumbs'][] = ['label' => Yii::t('workflow', 'Workflow'), 'url' => ['index']]; 19 | $this->params['breadcrumbs'][] = $this->title; 20 | ?> 21 |
    22 | 23 |

    24 | title) ?> 25 |
    26 | ' . Yii::t('workflow', 'Update'), ['update', 'id' => $model->id], ['class' => 'btn btn-info']) ?> 27 | ' . Yii::t('workflow', 'Delete'), ['delete', 'id' => $model->id], [ 28 | 'class' => 'btn btn-danger', 29 | 'data-confirm' => Yii::t('workflow', 'Are you sure?'), 30 | 'data-method' => 'post', 31 | ]) ?> 32 |
    33 |

    34 | 35 |
    36 |
    37 | statuses as $status) { 40 | $actions = []; 41 | $actions[] = ''; 42 | if ($model->initial_status_id != $status->id) { 43 | $actions[] = Html::a('', ['initial', 'id' => $model->id, 'status_id' => $status->id], ['title' => Yii::t('workflow', 'Set Initial')]); 44 | } 45 | $actions[] = Html::a('', ['status/update', 'id' => $status->id, 'workflow_id' => $status->workflow_id], ['title' => Yii::t('workflow', 'Update')]); 46 | $actions[] = Html::a('', ['status/delete', 'id' => $status->id, 'workflow_id' => $status->workflow_id], [ 47 | 'title' => Yii::t('workflow', 'Delete'), 48 | 'data-confirm' => Yii::t('workflow', 'Are you sure?'), 49 | 'data-method' => 'post', 50 | ]); 51 | $transitions = $status->startTransitions ? '
       ' . implode(', ', ArrayHelper::map($status->startTransitions, 'end_status_id', 'endName')) . '' : ''; 52 | $metadatas = $status->metadatas ? '
       ' . Json::encode(ArrayHelper::map($status->metadatas, 'key', 'value')) . '' : ''; 53 | $sortables[] = [ 54 | 'content' => '
    ' . implode(' ', $actions) . '
    ' . $status->name . $transitions . $metadatas, 55 | 'options' => [ 56 | 'id' => 'Status_' . $status->id, 57 | 'class' => 'list-group-item', 58 | ], 59 | ]; 60 | } 61 | echo DetailView::widget([ 62 | 'model' => $model, 63 | 'attributes' => [ 64 | [ 65 | 'attribute' => 'id', 66 | 'value' => Html::tag('span', $model->id, ['class' => 'label label-default', 'style' => 'color:#fff;background:' . $model->getColor()]), 67 | 'format' => 'raw', 68 | ], 69 | [ 70 | 'attribute' => 'color', 71 | 'format' => 'raw', 72 | ], 73 | [ 74 | 'attribute' => 'initial_status_id', 75 | 'value' => $model->initial_status_id, 76 | ], 77 | [ 78 | 'label' => Yii::t('workflow', 'Status') . '
    ' . Html::a(Yii::t('workflow', 'Create Status'), ['status/create', 'workflow_id' => $model->id], ['class' => 'btn btn-success btn-xs']), 79 | 'value' => Sortable::widget([ 80 | 'items' => $sortables, 81 | 'options' => [ 82 | 'class' => 'list-group', 83 | 'style' => 'margin-bottom:0;', 84 | ], 85 | 'clientOptions' => [ 86 | 'axis' => 'y', 87 | 'update' => new JsExpression("function(event, ui){ 88 | $.ajax({ 89 | type: 'POST', 90 | url: '" . Url::to(['sort', 'id' => $model->id]) . "', 91 | data: $(event.target).sortable('serialize') + '&_csrf=" . Yii::$app->request->getCsrfToken() . "', 92 | success: function() { 93 | location.reload(); 94 | } 95 | }); 96 | }"), 97 | ], 98 | ]), 99 | 'format' => 'raw', 100 | ], 101 | ], 102 | ]); 103 | ?> 104 |
    105 |
    106 | statuses) { 108 | echo WorkflowViewWidget::widget([ 109 | 'workflow' => Yii::$app->workflowSource->getWorkflow($model->id), 110 | 'containerId' => 'workflowView' 111 | ]); 112 | echo '
    '; 113 | } 114 | ?> 115 |
    116 |
    117 | 118 | statuses) { ?> 119 | 120 |

    121 | 122 | 123 | 124 | 125 | 126 | 127 | statuses as $endStatus) { ?> 128 | 131 | 132 | 133 | statuses as $k => $startStatus) { ?> 134 | 135 | 136 | 137 | 138 | 139 | statuses as $endStatus) { ?> 140 | 151 | 152 | 153 | 154 |
    129 | name ?> 130 |
    name ?> 141 | 0]; 143 | if ($startStatus->id == $endStatus->id) { 144 | unset($options['uncheck']); 145 | $options['disabled'] = true; 146 | } 147 | $transition = Transition::findOne(['workflow_id' => $model->id, 'start_status_id' => $startStatus->id, 'end_status_id' => $endStatus->id]); 148 | echo Html::checkbox('Status[' . $startStatus->id . '][' . $endStatus->id . ']', $transition ? true : false, $options); 149 | ?> 150 |
    155 | 'btn btn-success']) ?> 156 | 157 | 158 | 159 |
    160 | -------------------------------------------------------------------------------- /src/views/layouts/main.php: -------------------------------------------------------------------------------- 1 | 13 | beginPage() ?> 14 | 15 | 16 | 17 | 18 | 19 | 20 | <?= Html::encode($this->title) ?> 21 | registerCss('body{padding-top: 60px; padding-bottom: 60px;}'); ?> 22 | head() ?> 23 | 24 | 25 | beginBody() ?> 26 | 27 | Yii::t('workflow', 'Workflow'), 30 | 'brandUrl' => ['default/index'], 31 | 'options' => ['class' => 'navbar-default navbar-fixed-top navbar-fluid'], 32 | 'innerContainerOptions' => ['class' => 'container-fluid'], 33 | ]); 34 | $items = []; 35 | foreach (Workflow::find()->orderBy(['id' => SORT_ASC])->all() as $workflow) { 36 | /** @var Workflow $workflow */ 37 | $items[] = [ 38 | 'label' => $workflow->id, 39 | 'url' => ['default/view', 'id' => $workflow->id], 40 | ]; 41 | } 42 | echo Nav::widget([ 43 | 'items' => $items, 44 | 'options' => ['class' => 'navbar-nav'], 45 | ]); 46 | echo Nav::widget([ 47 | 'items' => [ 48 | ['label' => Yii::$app->name, 'url' => Yii::$app->getHomeUrl()], 49 | ], 50 | 'options' => ['class' => 'navbar-nav navbar-right'], 51 | ]); 52 | NavBar::end(); 53 | ?> 54 | 55 |
    56 | params['breadcrumbs'])) { ?> 57 | 62 | 63 | 64 | 65 |
    66 | 67 | endBody() ?> 68 | 69 | 70 | endPage() ?> 71 | -------------------------------------------------------------------------------- /src/views/status/_form-metadata.php: -------------------------------------------------------------------------------- 1 | 14 | 15 | 16 | field($metadata, 'key')->textInput([ 17 | 'id' => "Metadatas_{$key}_key", 18 | 'name' => "Metadatas[$key][key]", 19 | ])->label(false) ?> 20 | 21 | 22 | field($metadata, 'value')->textInput([ 23 | 'id' => "Metadatas_{$key}_value", 24 | 'name' => "Metadatas[$key][value]", 25 | ])->label(false) ?> 26 | 27 | 28 | ', 'javascript:void(0);', [ 29 | 'class' => 'status-remove-metadata-button btn btn-default btn-xs', 30 | 'title' => Yii::t('workflow', 'Remove {key}', ['key' => $key]), 31 | ]) ?> 32 | -------------------------------------------------------------------------------- /src/views/status/_form.php: -------------------------------------------------------------------------------- 1 | 13 | 14 |
    15 | 16 | 'Status', 18 | 'enableClientValidation' => false, 19 | 'errorSummaryCssClass' => 'error-summary alert alert-error' 20 | ]) ?> 21 | 22 | errorSummary($form); ?> 23 | 24 |
    25 | 26 | field($model->status, 'id')->textInput(['maxlength' => true]) ?> 27 | field($model->status, 'label')->textInput(['maxlength' => true]) ?> 28 |
    29 | 30 |
    31 | 32 | ' . Yii::t('workflow', 'New Metadata'), 'javascript:void(0);', [ 35 | 'id' => 'status-new-metadata-button', 36 | 'class' => 'pull-right btn btn-default btn-xs' 37 | ]) 38 | ?> 39 | 40 | loadDefaultValues(); 44 | echo ''; 45 | echo ''; 46 | echo ''; 47 | echo ''; 48 | echo ''; 49 | echo ''; 50 | echo ''; 51 | echo ''; 52 | echo ''; 53 | // existing metadatas fields 54 | foreach ($model->metadatas as $key => $_metadata) { 55 | echo ''; 56 | echo $this->render('_form-metadata', [ 57 | 'key' => $_metadata->isNewRecord ? (strpos($key, 'new') !== false ? $key : 'new' . $key) : $_metadata->key, 58 | 'form' => $form, 59 | 'metadata' => $_metadata, 60 | ]); 61 | echo ''; 62 | } 63 | // new metadata fields 64 | echo ''; 65 | echo $this->render('_form-metadata', [ 66 | 'key' => '__id__', 67 | 'form' => $form, 68 | 'metadata' => $metadata, 69 | ]); 70 | echo ''; 71 | echo ''; 72 | echo '
    ' . $metadata->getAttributeLabel('key') . '' . $metadata->getAttributeLabel('value') . ' 
    '; 73 | ?> 74 | 75 | 76 | 89 | registerJs(str_replace([''], '', ob_get_clean())); ?> 90 | 91 |
    92 | 93 | ' . ($model->status->isNewRecord ? Yii::t('workflow', 'Create') : Yii::t('workflow', 'Save')), [ 94 | 'id' => 'save-' . $model->formName(), 95 | 'class' => 'btn btn-success' 96 | ]) ?> 97 | $model->status->workflow_id], ['class' => 'btn btn-default']) ?> 98 | 99 | 100 | 101 |
    102 | 103 | -------------------------------------------------------------------------------- /src/views/status/create.php: -------------------------------------------------------------------------------- 1 | title = Yii::t('workflow', 'Create Status'); 10 | $this->params['breadcrumbs'][] = ['label' => Yii::t('workflow', 'Workflow'), 'url' => ['default/index']]; 11 | $this->params['breadcrumbs'][] = ['label' => $model->status->workflow->id, 'url' => ['default/view', 'id' => $model->status->workflow->id]]; 12 | $this->params['breadcrumbs'][] = $this->title; 13 | ?> 14 |
    15 | 16 |

    17 | title) ?> 18 |

    19 | 20 | render('_form', [ 21 | 'model' => $model, 22 | ]); ?> 23 | 24 |
    25 | -------------------------------------------------------------------------------- /src/views/status/update.php: -------------------------------------------------------------------------------- 1 | title = $model->status->id; 10 | $this->params['breadcrumbs'][] = ['label' => Yii::t('workflow', 'Workflow'), 'url' => ['default/index']]; 11 | $this->params['breadcrumbs'][] = ['label' => $model->status->workflow->id, 'url' => ['default/view', 'id' => $model->status->workflow->id]]; 12 | $this->params['breadcrumbs'][] = $model->status->id; 13 | ?> 14 |
    15 | 16 |

    17 | title) ?> 18 |

    19 | 20 | render('_form', [ 21 | 'model' => $model, 22 | ]); ?> 23 | 24 |
    25 | --------------------------------------------------------------------------------