raoul2000/yii2-workflow A simple workflow engine for Yii2

workflow

yii2-workflow

Build Latest Stable Version Total Downloads License

Installation

The preferred way to install this extension is through composer.

Either run

php composer.phar require --prefer-dist raoul2000/yii2-workflow "*"

or add

"raoul2000/yii2-workflow": "*"

to the require section of your composer.json file.

Quick Start

Configuration

For this "Quick start Guide" we will be using default configuration settings, but remember that yii2-workflow is designed to be highly flexible so to adapt to a lot of execution contexts... well at least that was my goal.

Create A Workflow

A workflow is defined as a PHP class that implements the \raoul2000\workflow\source\file\IWorkflowDefinitionProvider interface. which declares the getDefinition() method. This method must return an array representing the workflow definition.

Let's define a very simple workflow that will be used to manage posts in a basic blog system.

workflow1.png

Here is the PHP class that implements the definition for our workflow :

@app/models/PostWorkflow.php

namespace app\models;

class PostWorkflow implements \raoul2000\workflow\source\file\IWorkflowDefinitionProvider
{
    public function getDefinition() {
        return [
            'initialStatusId' => 'draft',
            'status' => [
                'draft' => [
                    'transition' => ['publish','deleted']
                ],
                'publish' => [
                    'transition' => ['draft','deleted']
                ],
                'deleted' => [
                    'transition' => ['draft']
                ]
            ]
        ];
    }
}

Attach To The Model

Now let's have a look to our Post model: we store the status of a post in a column named status of type STRING(40).

The last step is to associate the workflow definition with posts models. To do so we must declare the SimpleWorkflowBehavior behavior in the Post model class and let the default configuration settings do the rest.

@app/models/Post.php

namespace app\models;
/**
 * @property integer $id
 * @property string $title
 * @property string $body
 * @property string $status column used to store the status of the post
 */
class Post extends \yii\db\ActiveRecord
{
    public function behaviors()
    {
        return [
            \raoul2000\workflow\base\SimpleWorkflowBehavior::className()
        ];
    }
    // ...

That's it ! We are ready to play with SimpleWorkflowBehavior.

Use It !

Now that we are all setup, we can use the SimpleWorkflowBehavior methods to set/get the status of our posts : the SimpleWorkflowBehavior will take care that the post doesn't reach a status where it is not supposed to go to, depending on the workflow definition that we have provided.

$post = new Post();
$post->status = 'draft';
$post->save();
echo 'post status is : '. $post->workflowStatus->label;

This will print the following message :

post status is : Draft

If you do the same thing but instead of draft set the status to publish and try to save it, the following exception is thrown :

Not an initial status : PostWorkflow/publish ("PostWorkflow/draft" expected)

That's because in your workflow definition the initial status is set to draft and not publish.

Ok, one more example for the fun ! This time we are not going to perform the transition when the Post is saved (like we did in the previous example), but immediately, by invoking the sendToStatus method. Our Post is going to try to reach status publish passing through deleted which is strictly forbidden by the workflow. Will it be successful in this risky attempt to break workflow rules ?

$post = new Post();
$post->sendToStatus('draft');
$post->sendToStatus('deleted');
$post->sendToStatus('publish'); // danger zone !

Game Over ! There is no transition between deleted and publish, and that's what SimpleWorkflow tries to explain to our fearless post object.

Workflow Exception – raoul2000\workflow\base\WorkflowException
No transition found between status PostWorkflow/deleted and PostWorkflow/publish

Yes, that's severe, but there was many ways to avoid this exception like for instance by first validating that the transition was possible.

What's Next ?

This is just one way of using the SimpleWorkflowBehavior but there's much more and hopefully enough to assist you in workflow management inside your Yii2 web app.

You will find additional information there :

You may also be interested in the following projects developed around yii2-workflow :

License

yii2-workflow is released under the BSD 3-Clause License. See the bundled LICENSE.md for details.

Yii2

Changelog

version 1.2.0

  • upgrade to Yii 2.0.13 dependency : if you are using a version of Yii 2.0.13 or greater you must use this version (or greater).

about Events : Since Yii v2.0.14 it is possible to specify event name as a wildcard pattern

$component->on('event.group.*', function ($event) {
    Yii::trace($event->name . ' is triggered.');
});

This may cause a problem if you are using the ExtendedEventSequence model because it is making use of the wildcard characters to name events (for example : beforeEnterWorkflow(*)). Consequently if you set an event handler for beforeEnterWorkflow(MY_WORKFLOW) event, the handler will be invoked twice : once for event \ beforeEnterWorkflow(MY_WORKFLOW) and once for event beforeEnterWorkflow(*)

version 1.1.0

version 1.0.2

version 1.0.1

  • Ci : update components versions
  • Ci : remove scrutinizer hook
  • Ci : remove php version prior to 5.6
  • add unit tests for WorkflowHelper::getStatusDropDownData()
  • refactor User guide and Class Reference documentation

version 1.0.0

  • fix CI failure : deep-copy 1.4.0 caused error Maximum function nesting level of '200' reached, aborting!. Version 1.3.1 is ok
  • add composer.lock
  • fix doc blocks

release 1.0.0

version 0.0.20

  • remove unnecessary use statement in SimpleWorkflowBehavior
  • test against StatusInterface and not anymore Status
  • update doc
  • minor change in the WorkflowFileSource->parseStatusId()
  • fix : restore model errors after getNextStatuses()

important notice Because Yii2 ActiveRecord implementation does not fire any event when the method refresh() is called, the attached SimpleWorkflowBehavior has no way to know that the status attribute may have change and that the actual model status maintained internally by the behavior, needs to be updated. A feature request has been created but until it gets accepted and implemented you must call initStatus() after each call to refresh(). For example :

$post->refresh();
$post->initStatus();

Note that calling initStatus() is usually never done by the developer but by the behavior itself when specific ActiveRecords event are fired.

  • remove custom validation error message in WorkflowValidator->init() (not needed)
  • when a model changes status, all WorkflowEvent events fired now include the start status, the end status and the transitions. This change has been applied to all Event sequences in raoul2000\workflow\events.

version 0.0.19

  • fix GraphmlLoader to not only process ShapeNode elements, and use node label in yEd as Status Id.

version 0.0.18

  • add method WorkflowInterface->getAllStatuses() allowing to get a list of all statuses belonging to a workflow, using the workflow instance.

You can now write :

// assuming our $post has a status let's get a list of all statuses in the current workflow
$allStatuses = $post->getWorkflow()->getAllStatuses();

This feature extends the one implemented in version 0.0.11

version 0.0.17

  • add method StatusInterface->isInitialStatus() to test if a status instance is the initial status of the workflow it belongs to.
  • add argument $includeCurrent in WorkflowHelper::getNextStatusListData(...). When TRUE the current model status is added to the returned array. When FALSE (default) only next statuses are included.
  • rollback previous change : $emptyStringAsNull is removed. The behavior now applies empty() to detect unset status

version 0.0.16

  • add the configuration setting $emptyStringAsNull to SimpleWorkflowBehavior.

When TRUE, the status attribute is considered as null if it contains an empty string If set to FALSE this is developer responsability to nullify the status attribute value, because an empty string is considered as an invalid status id.

version 0.0.15

  • add default Event

The Default event is not related to the configured event sequence, it's a built-in event, fired by the SimpleWorkflowBehavior, in its before and after form, whenever a model changes status. Like any other event, it is an instance of the WorkflowEvent with a name set to :

  • SimpleWorkflowBehavior::EVENT_BEFORE_CHANGE_STATUS
  • SimpleWorkflowBehavior::EVENT_AFTER_CHANGE_STATUS

Depending on the value returned by getStartStatus(), getEndStatus() it is possible to identify the kind of transition that is being performed by the model :

  • getStartStatus() == null : the model is entering into the workflow through the initial status ( returned by getEndStatus() )
  • getEndStatus() == null : the model is leaving the workflow from status returned by getStartStatus().

In both cases, a call to getTransition() returns NULL : entering or leaving a workflow is not considered as a transition.

version 0.0.14

  • improve leave workflow management

The action to delete the owner model is now considered as leaving the workflow : the leave workflow event sequence is fired. Previously, the only way for a model to leave a workflow was by assigning NULL to the status attribute and saving the model (or by calling sendToStatus(null));

version 0.0.13

  • the SimpleWorkflowBehavior can now be safely attached to any object that inherits from \yii\base\Object.

warning : The SimpleWorkflowBehavior has been first designed to be attached to an ActiveRecord instance and thus integrates in the life cycle of such objects. By installing event handlers on various ActiveRecord events, it automatically handles status persistence. If the behavior is attached to another type of object, the developer must understand and (possibly) implement all the features that otherwise would be available by default.

version 0.0.12

  • add status conversion map setter to the class raoul2000\workflow\base\StatusIdConverter. The map is still required by the constructor but it can be updated at runtime using the setMap() method. (see dynamic maps for status conversion issue)
  • Both status converter and status accessor components can now be configured as an object instance.

For example, assuming that variable $myConverter contains a reference to an object instance that implements raoul2000\workflow\base\StatusIdConverter, you can now write :

class Post extends \yii\db\ActiveRecord
{
    public function behaviors()
    {
        return [
            [
                'class' => \raoul2000\workflow\base\SimpleWorkflowBehavior::className(),
                'statusConverter' => $myConverter
            ]
        ];
    }
}

In the previous version it was only possible to initialize the statusConverter parameter to a string, representing the id of a component registered in Yii::$app.

This also applies to statusAccessor parameter.

  • lazy component initialization for status converter and status accessor. If configured, these component are actually initialized (assigned) and validated when accessed for the first time.
  • add cache to WorkflowFileSource component. If set, the definitionCache parameter defines the cache object to use.

To initialize the WorkflowFileSource component to use a cache :

$config = [
    'components' => [
        'workflowSource' => [
          'class' => 'raoul2000\workflow\source\file\WorkflowFileSource',
          'definitionCache' => [
                'class' => 'yii\caching\FileCache',
          ],
        ],

version 0.0.11

  • update unit tests
  • check interface implemented instead of class
  • remove StatusInterface.addTransition() method
  • add workflow source component to WorkflowBaseObject constructor. Update Status and WorkflowInterface to enable accessing workflow items from Status or Workflow instances.

For example it is now possible to do the following :

// let's get a status instance from our Post model
$status = $post->getWorkflowStatus();

// get an array containing out going Transitions objects
$status->getTransitions();

// get the parent workflow
$status->getWorkflow();

// get the initial status of the parent workflow
$status->getWorkflow()->getInitialStatus();

version 0.0.10

Massive refactoring of the workflow source component architecture to allow loading workflow definition from virtually any file (and not only PHP class).

WARNING : this modification may break back compatibility so pay attention to the following major changes :

  • namespace raoul2000\workflow\source\php has been renamed raoul2000\workflow\source\file
  • class WorkflowPhpSource has been renamed WorkflowFileSource
  • the namespace configuration setting has been removed from the source component
  • add configuration attribute definitionLoader to WorkflowFileSource
  • IWorkflowDefinitionProvider has been moved to namespace raoul2000\workflow\source\file

The WorkflowFileSource component is dedicated to load workflow definition from any file, for this reason, it relies on a WorkflowDefinitionLoader component that is used to :

  1. locate the file containing the workflow definition
  2. load the workflow definition
  3. convert it into a PHP array having the structure expected by the WorkflowFileSource component.

By Default, the WorkflowFileSource component uses a PhpClassLoader instance, maintaining this way the default feature that allows a workflow definition to be retrieved from a PHP class. The default namespace remains app\models and if you want to change it, you must explicitely declare the Workflow source component with appropriate settings (like required in the previous versions).

In the example below, we declare a Workflow Source component, using the PhpClassLoader and with a customized namespace attribute (in our example, workflow definition classes are located in app\models\workflows).

$config = [
    // ....
    'components' => [
        'workflowSource' => [
          'class' => '\raoul2000\workflow\source\file\WorkflowFileSource',
          'definitionLoader' => [
              'class' => 'raoul2000\workflow\source\file\PhpClassLoader',
              'namespace' => 'app\models\workflows'
          ]
   // ...

version 0.0.9

  • add propagateErrorsToModel configuration setting to the SimpleWorkflowBehavior

If TRUE, all errors that may be registred on an invalidated 'before' event, are assigned to the status attribute of the owner model (allowing to display them to the user).

Example :

/**
 * This is the model class for table "Post".
 *
 * @property integer $id
 * @property string $name
 * @property string $status
 */
class Post extends \yii\db\ActiveRecord
{
    public function init()
    {
        $this->on(
            WorkflowEvent::beforeEnterStatus('Post/to-publish'),
            function ($event) {
                // test if the model can enter in status 'publish'
                // ...
                if( $error ) {
                    $event->invalidate('the post can\'t be published');
                }
            }
        );
    }

    public function behaviors()
    {
        return [
            [
                'class' => \raoul2000\workflow\base\SimpleWorkflowBehavior::className(),
                'propagateErrorsToModel' => true
            ]
        ];
    }
}

$post = Post::findOne(1);
$post->status = 'Post/to-publish';
if( $post->save() == false ) {
    echo 'error : '.$item->getFirstError('status');  // the post can\'t be published
}
  • add stopOnFirstInvalidEvent configuration setting to the SimpleWorkflowBehavior

if TRUE, all "before" events are fired event if one of them is invalidated by an attached handler. When FALSE, the first invalidated event interrupts the event sequence.

version 0.0.8

  • WARNING : relocate WorkflowHelper, now in namespace raoul2000\workflow\helpers
  • add helper function getAllStatusListData()

getAllStatusListData() returns an associative array containing all statuses that belong to a workflow. The array returned is suitable to be used as list data value in (for instance) a dropdown list control.

Usage example : assuming model Post has a SimpleWorkflowBehavior the following code displays a dropdown list containing all statuses defined in $post current the workflow :

echo Html::dropDownList(
        'status',
        null,
        WorkflowHelper::getAllStatusListData(
            $post->getWorkflow()->getId(),
            $post->getWorkflowSource()
        )
)

version 0.0.7

  • update doc
  • rename SimpleWorkflowBehavior::_createTransitionItems to SimpleWorkflowBehavior::createTransitionItems
  • add autoInsert feature.

this feature is not enabled

The autoInsert feature allows to automatically insert a model into a workflow when the model is created and only if there is no previous status set.

Example :

class Post extends \yii\db\ActiveRecord
{
    public function behaviors()
    {
        return [
            [
                'class' => \raoul2000\workflow\base\SimpleWorkflowBehavior::className(),
                'autoInsert' => true,
                'defaultWorkflowId' => 'MyWorkflow'
            ]
        ];
    }
}

$post = new Post();
echo $post->getWorkflowStatus()->getId();   // output : MyWorkflow/new

Note that no event is fired when a model is auto-inserted into a workflow. If autoInsert is a string, it must be the ID of the workflow where the model will be automatically inserted to. If autoInsert is a TRUE boolean, the model is inserted into its default workflow.

version 0.0.6

  • add support for multi workflow : more than one workflow can be attached to a model.

The first declared SimpleWorkflow behavior handles the main workflow and all other SimpleWorkflow behavior handle secondary workflows. All SimpleWorkflow behavior related to secondary workflow must provide configuration settings for :

  • statusAttribute
  • defaultWorkflowId

Moreover, SimpleWorkflow behavior related to secondary workflows must NOT be declared as anonymous : a behavior name is required.

Example : Item08Workflow1 is the primary workflow, Item08Workflow2 is the secondary workflow

class Post extends \yii\db\ActiveRecord
{
    public function behaviors()
    {
        return [
            // The main workflow is ALWAYS declared first
            [
                'class' => \raoul2000\workflow\base\SimpleWorkflowBehavior::className(),
            ],          
            // the secondary workflow : note that it is declared as 'w2'
            'w1' => [
                'class' => \raoul2000\workflow\base\SimpleWorkflowBehavior::className(),
                'statusAttribute' => 'status_ex',
                'defaultWorkflowId' => 'SecondaryWorkflow'
            ]
        ];
    }
}

To access SimpleWorkflow methods related to the main workflow, you can use the usual way. To access SimpleWorkflow methods related to the secondary workflow, you must use the behavior name.

In both case you can also use direct attribute assignement.

Example

$p = new Post();
// direct attribute assignement
$p->status = 'PostWorkflow/draft';
$p->status_ex = 'SecondaryWorkflow/ready';
// both transitions are committed now
$p->save();

// SimpleWorkflowBehavior methods
$o = new Post();
// applied on the main workflow only
$o->enterWorkflow();     
// applied on the secondary workflow
$p->getBehavior('w1')->enterWorkflow();

See unit test tests\unit\workflow\behavior\MultiWorkflowTest for more example.

version 0.0.5

  • add MinimalArrayParser for workflow definition PHP arrays provided as for instance :
[
    'draft'     => ['ready', 'delivered'],
    'ready'     => ['draft', 'delivered'],
    'delivered' => ['payed', 'archived'],
    'payed'     => ['archived'],
    'archived'  => []
]

The initialStatusId is the first status defined (here draft)

version 0.0.4

  • add tests for raoul2000\workflow\source\php\DefaultArrayParser
  • minor fix in DefaultArrayParser

version 0.0.3

  • externalize array parser to normalize workflow php array definition

version 0.0.2

  • change regex for status and workflow ID pattern: now /^[a-zA-Z]+[[:alnum:]-]*$/
  • Improve WorkflowPhpSource parsing of a workflow defined as a PHP array.
[
    'initialStatusId' => 'A',
    'status' => [
        'A' => [
            'transition' => 'A,  B'
        ],
        'B' => [
            'transition' => ['A','C']
        ],
        'C'
    ]
]

version 0.0.1

  • Initial import

Statistics

Downloads
GitHub Stars
GitHub Forks

Releases

Comments



1.2.0 is the latest of 12 releases



BSD-3-Clause license
Stats
168 github stars & 48 github forks
37 downloads in the last day
974 downloads in the last 30 days
55687 total downloads