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.
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.
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.
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']
]
]
];
}
}
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.
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.
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 :
yii2-workflow is released under the BSD 3-Clause License. See the bundled LICENSE.md
for details.
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(*)
ChangeStatusAction
(from https://github.com/raoul2000/yii2-workflow/pull/24)WorkflowHelper::getStatusDropDownData()
Maximum function nesting level of '200' reached, aborting!
. Version 1.3.1 is okrelease 1.0.0
use
statement in SimpleWorkflowBehavior
StatusInterface
and not anymore Status
WorkflowFileSource->parseStatusId()
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.
WorkflowValidator->init()
(not needed)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
.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
StatusInterface->isInitialStatus()
to test if a status instance is the initial status of the workflow it belongs to.$includeCurrent
in WorkflowHelper::getNextStatusListData(...)
. When TRUE the current model status is added to the returned array.
When FALSE (default) only next statuses are included.$emptyStringAsNull
is removed. The behavior now applies empty() to detect unset status$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.
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 :
Depending on the value returned by getStartStatus(), getEndStatus() it is possible to identify the kind of transition that is being performed by the model :
In both cases, a call to getTransition() returns NULL : entering or leaving a workflow is not considered as a transition.
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)
);
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.
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)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.
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',
],
],
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();
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 :
raoul2000\workflow\source\php
has been renamed raoul2000\workflow\source\file
WorkflowPhpSource
has been renamed WorkflowFileSource
namespace
configuration setting has been removed from the source componentdefinitionLoader
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 :
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'
]
// ...
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
}
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.
raoul2000\workflow\helpers
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()
)
)
SimpleWorkflowBehavior::_createTransitionItems
to SimpleWorkflowBehavior::createTransitionItems
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.
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 :
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.
[
'draft' => ['ready', 'delivered'],
'ready' => ['draft', 'delivered'],
'delivered' => ['payed', 'archived'],
'payed' => ['archived'],
'archived' => []
]
The initialStatusId is the first status defined (here draft)
/^[a-zA-Z]+[[:alnum:]-]*$/
[
'initialStatusId' => 'A',
'status' => [
'A' => [
'transition' => 'A, B'
],
'B' => [
'transition' => ['A','C']
],
'C'
]
]
Comments