Finite¶
Use with Symfony¶
Installation¶
$ composer require yohang/finite
Register the bundle¶
Register the bundle in your AppKernel:
<?php
// app/AppKernel.php
public function registerBundles()
{
$bundles = array(
// ...
new\Finite\Bundle\FiniteBundle\FiniteFiniteBundle(),
// ...
);
}
Defining your stateful class¶
As we want to track the state of an object (or the multiples states, but this example will focus on
object with single state-graph), create your class if it doesn’t already exists. This class has simply
to implements Finite\StatefulInterface
.
This part is covered in Define your object.
Configuration¶
finite_finite:
document_workflow:
class: MyDocument # You class FQCN
states:
draft: { type: initial, properties: { visible: false } }
proposed: { type: normal, properties: { visible: false } }
accepted: { type: final, properties: { visible: true } }
refused: { type: final, properties: { visible: false } }
transitions:
propose: { from: draft, to: proposed }
accept: { from: proposed, to: accepted }
refuse: { from: proposed, to: refused }
At this point, your graph is ready and you can start using your workflow on your object.
Controller / Service usage¶
Finite define several services into the Symfony DIC. The easier to use is finite.context
.
Example¶
<?php
$context = $this->get('finite.context');
$context->getState($document); // return "draft", or… the current state if different
$context->getProperties($document); // array:1 [ 'visible' => false ]
$context->getTransitions($document); // array:2 [ 0 => "propose", 1 => "refuse" ]
$context->hasProperty($document, 'visible'); // true
$context->getFactory(); // Return an instance of FiniteFactory, used to instantiate the state machine
$context->getStateMachine($document); // Returns a initialized StateMachine instance for $document
// Throw a 404 if document isn't visible
if (!$this->get('finite.context')->getProperties($document)['visible']) {
throw $this->createNotFoundException(
sprintf('The document "%s" is not in a visible state.', $document->getName())
);
}
Twig usage¶
Although the Twig Extension is not Symfony-specific at all, when using the Symfony Bundle, Finite functions are automatically accessible in your templates.
{{ dump(finite_state(document)) }} {# "draft" #}
{{ dump(finite_transitions(document)) }} {# array:2 [ 0 => "propose", 1 => "refuse" ] #}
{{ dump(finite_properties(document)) }} {# array:1 [ 'visible' => false ] #}
{{ dump(finite_has(document, 'visible')) }} {# true #}
{{ dump(finite_can(document, 'accept')) }} {# true #}
{# Display reachable transitions #}
{% for transition in finite_transitions(document) %}
<a href="{{ path('document_apply_transition', {transition: transition}) }}">
{{ transition }}
</a>
{% endfor %}
{# Display an action if available #}
{% if finite_can(document, 'accept') %}
<button type="submit" name="accept">
Accept this document
</button>
{% endif %}
Example¶
Using callbacks¶
The state machine is built around a a very flexible and powerful events / callbacks system. Events dispatched with the EventDispatcher and works as the Symfony kernel events.
Events¶
- finite.set_initial_state:
- This event is fired when initializing a state machine with an object which does not have a defined state. It allows you to manage the default initial state of your object.
- finite.initialize:
- Fired when the StateMachine is initialized for an object (event if the current object state is known)
- finite.test_transition:
- Fired when testing if a transition can be applied, when you call
StateMachine#can
orStateMachine#apply
. This event is an instance ofFinite\Event\TransitionEvent
and can be rejected, which leads to a non-appliable transition. This is one of the most useful event, as it allows you to introduce business code for allowing / rejecting transitions - finite.test_transition.[transition_name]:
- Same as
finite.test_transition
but with the concerned transition in the event name. - finite.test_transition.[graph].[transition_name]:
- Same as
finite.test_transition
but with the concerned graph and transition in the event name. - finite.pre_transition:
- Fired before applying a transition. You can use it to prepare your object for a transition.
- finite.pre_transition.[transition_name]:
- Same as
finite.pre_transition
but with the concerned transition in the event name. - finite.pre_transition.[graph].[transition_name]:
- Same as
finite.pre_transition
but with the concerned graph and transition in the event name. - finite.post_transition:
- Fired after applying a transition. You can use it to execute the business code you have to execute when a transition is applied.
- finite.post_transition.[post_transition]:
- Same as
finite.post_transition
but with the concerned transition in the event name. - finite.post_transition.[graph].[transition_name]:
- Same as
finite.post_transition
but with the concerned graph and transition in the event name.
Callbacks¶
Callbacks are a simplified mechanism allowing you to plug your domain services on the finite events. You can see it as a way to listen to events without defining a listener class that just redirects the events to your services.
Using YAML configuration¶
finite_finite:
document_workflow:
class: MyDocument
states:
# ...
transitions:
# ...
callbacks:
before:
# Will call the `sendPublicationMail` method of `@app.mailer.document` service
# When the `accept` transition is applied
send_publication_mail:
disabled: false # default value
on: accept
do: [ @app.mailer.document, 'sendPublicationMail' ]
# Will call the `sendNotAnymoreProposedEmail` method of `@app.mailer.document` service
# When any transition from the `proposed` state is applied.
# This condition can be negated by prefixing a `-` before the state name
# And the same exists for the destination transitions (with `to: `)
send_publication_mail:
disabled: false # default value
from: ['proposed']
do: [ @app.mailer.document, 'sendNotAnymoreProposedEmail' ]
Configuration reference¶
finite_finite:
# Prototype
name: # internal name of your graph, not used
class: ~ # Required, FQCN of your class
graph: default # Name of your graph, keep default if using a single graph
property_path: finiteState # The property of your class used to store the state
states:
# Prototype
name: # Required, Name of your state
type: normal # State type, in "initial", "normal", "final"
properties: # Properties array.
# Prototype
name: ~
transitions:
# Prototype
name: # Required, Name of your transition
from: [] # Required, states the transition can come from
to: ~ # Required, state where the transition go
properties: # Properties array.
# Prototype
name: ~
callbacks:
before: # Pre-transition callbacks
# Prototype
name:
do: ~ # Required. The callback.
on: ~ # On which transition to trigger the callback. Default null
from: ~ # From which states are we triggering the callback. Default null
to: ~ # To which states are we triggering the callback. Default null
disabled: false
after: # Post-transition callbacks
# Prototype
name:
on: ~
do: ~
from: ~
to: ~
disabled: false
Basic graph¶
Goal¶
In this example, we’ll see a basic Document workflow, following this graph :
Reject
|-----------------|
Transitions | |
v Propose | Accept
States Draft ----------> Proposed ----------> Accepted
Properties * Deletable * Printable
* Editable
Implement the document class¶
<?php
class Document implements Finite\StatefulInterface
{
private $state;
public function getFiniteState()
{
return $this->state;
}
public function setFiniteState($state)
{
$this->state = $state;
}
}
Configure your graph¶
<?php
$loader = new Finite\Loader\ArrayLoader([
'class' => 'Document',
'states' => [
'draft' => [
'type' => Finite\State\StateInterface::TYPE_INITIAL,
'properties' => ['deletable' => true, 'editable' => true],
],
'proposed' => [
'type' => Finite\State\StateInterface::TYPE_NORMAL,
'properties' => [],
],
'accepted' => [
'type' => Finite\State\StateInterface::TYPE_FINAL,
'properties' => ['printable' => true],
]
],
'transitions' => [
'propose' => ['from' => ['draft'], 'to' => 'proposed'],
'accept' => ['from' => ['proposed'], 'to' => 'accepted'],
'reject' => ['from' => ['proposed'], 'to' => 'draft'],
],
]);
$document = new Document;
$stateMachine = new Finite\StateMachine\StateMachine($document);
$loader->load($stateMachine);
$stateMachine->initialize();
At this point, your Workflow / State graph is fully accessible to the state machine, and you can start to work with your workflow.
Working with workflow¶
Current state¶
<?php
// Get the name of the current state
$stateMachine->getCurrentState()->getName();
// string(5) "draft"
// List the currently accessible properties, and their values
$stateMachine->getCurrentState()->getProperties();
// array(2) {
// 'deletable' => bool(true)
// 'editable' => bool(true)
// }
// Checks if "deletable" property is defined
$stateMachine->getCurrentState()->has('deletable');
// bool(true)
// Checks if "printable" property is defined
$stateMachine->getCurrentState()->has('printable');
// bool(false)
Available transitions¶
<?php
// Retrieve available transitions
var_dump($stateMachine->getCurrentState()->getTransitions());
// array(1) {
// [0] => string(7) "propose"
// }
// Check if we can apply the "propose" transition
var_dump($stateMachine->getCurrentState()->can('propose'));
// bool(true)
// Check if we can apply the "accept" transition
var_dump($stateMachine->getCurrentState()->can('accept'));
// bool(false)
Apply transition¶
<?php
// Trying to apply a not accessible transition
try {
$stateMachine->apply('accept');
} catch (\Finite\Exception\StateException $e) {
echo $e->getMessage();
}
// The "accept" transition can not be applied to the "draft" state.
// Applying a transition
$stateMachine->apply('propose');
$stateMachine->getCurrentState()->getName();
// string(7) "proposed"
$document->getFiniteState();
// string(7) "proposed"
Events / Callbacks¶
Overview¶
Finite use the Symfony EventDispatcher component to notify each actions done by the State Machine.
You can use the event system directly with callbacks in your configuration, or by attaching listeners to the event dispatcher.
Implement your document class and define your workflow¶
See Basic graph.
Use callbacks¶
Callbacks can be defined directly in your State Machine configuration. The can be called before or after the transition apply, and their definition use the following pattern :
<?php
$definition = [
'from' => [], // a string or an array of string that represent the initial states that trigger the callback. Empty for All.
'to' => [], // a string or an array of string that represent the target states that trigger the callback. Empty for All.
'on' => [], // a string or an array of string that represent the transition names that trigger the callback. Empty for All.
'do' => function($object, Finite\Event\TransitionEvent $e) {
// The callback
}
];
from and to parameters can be any state names. Prefix by - to process an exclusion. By default, callbacks matchs all the events.
Example :¶
<?php
[
'from' => ['all', '-proposed'],
'do' => function($object, Finite\Event\TransitionEvent $e) {
// callback code
}
];
Will match any transition that don’t begin on the proposed state.
Full example :¶
<?php
$loader = new Finite\Loader\ArrayLoader([
'class' => 'Document',
'states' => [
'draft' => [
'type' => Finite\State\StateInterface::TYPE_INITIAL,
'properties' => ['deletable' => true, 'editable' => true],
],
'proposed' => [
'type' => Finite\State\StateInterface::TYPE_NORMAL,
'properties' => [],
],
'accepted' => [
'type' => Finite\State\StateInterface::TYPE_FINAL,
'properties' => ['printable' => true],
]
],
'transitions' => [
'propose' => ['from' => ['draft'], 'to' => 'proposed', 'properties' => ['foo' => 'bar']],
'accept' => ['from' => ['proposed'], 'to' => 'accepted'],
'reject' => ['from' => ['proposed'], 'to' => 'draft'],
],
'callbacks' => [
'before' => [
[
'from' => '-proposed',
'do' => function(\Finite\Event\TransitionEvent $e) {
echo 'Applying transition '.$e->getTransition()->getName(), "\n";
if ($e->has('foo')) {
echo "Parameter \"foo\" is defined\n";
}
}
],
[
'from' => 'proposed',
'do' => function() {
echo 'Applying transition from proposed state', "\n";
}
]
],
'after' => [
[
'to' => ['accepted'], 'do' => [$document, 'display']
]
]
]
]);
$stateMachine->apply('propose');
// => "Applying transition propose"
// => "Parameter "foo" is defined"
$stateMachine->apply('reject');
// => "Applying transition from proposed state"
$stateMachine->apply('propose');
// => "Applying transition propose"
// => "Parameter "foo" is defined"
$stateMachine->apply('accept');
// => "Applying transition from proposed state"
// => "Hello, I'm a document and I'm currently at the accepted state."
Use event dispatcher¶
If you prefer, you can use directly the event dispatcher.
Here is the available events :
finite.initialize => Dispatched at State Machine initialization
finite.test_transition => Dispatched when testing if a transition can be applied
finite.pre_transition => Dispatched before a transition
finite.post_transition => Dispatched after a transition
finite.test_transition.{transitionName} => Dispatched when testing if a specific transition can be applied
finite.pre_transition.{transitionName} => Dispatched before a specific transition
finite.post_transition.{transitionName} => Dispatched after a specific transition
finite.test_transition.{graph}.{transitionName} => Dispatched when testing if a specific transition in a specific graph can be applied
finite.pre_transition.{graph}.{transitionName} => Dispatched before a specific transition in a specific graph
finite.post_transition.{graph}.{transitionName} => Dispatched after a specific transition in a specific graph
Example :¶
<?php
$stateMachine->getDispatcher()->addListener('finite.pre_transition', function(\Finite\Event\TransitionEvent $e) {
echo 'This is a pre transition', "\n";
});
$stateMachine->apply('propose');
// => "This is a pre transition"
Example testing transitions:¶
<?php
$stateMachine->getDispatcher()->addListener('finite.test_transition', function(\Finite\Event\TransitionEvent $e) {
$e->reject();
});
try {
$stateMachine->apply('propose');
}
catch (Finite\StateMachine\Exception\StateException $e) {
echo 'The transition did not apply', "\n";
}
// => "The transition did not apply"
Transitions properties¶
As the second argument, StateMachine#apply and StateMachine#test will accept an array of properties to be passed to the dispatched event, and accessible by the listeners.
Default properties can be defined with your state graph.
$stateManager->apply('some_event', array('something' => $value));
In your listeners you just have to call `$event->getProperties()`
to access the passed data.
<?php
namespace My\AwesomeBundle\EventListener;
use Finite\Event\TransitionEvent;
class TransitionListener
{
/**
* @param TransitionEvent $event
*/
public function someEvent(TransitionEvent $event)
{
$entity = $event->getStateMachine()->getObject();
$params = $event->getProperties();
$entity->setSomething($params['something']);
}
}
Default properties¶
'transitions' => array(
'finish' => array(
'from' => array('middle'),
'to' => 'end',
'properties' => array('foo' => 'bar'),
'configure_properties' => function (OptionsResolver $resolver) {
$resolver->setRequired('baz');
}
)
)
A PHP Finite State Machine¶
Finite is a state machine library that gives you ability to manage the state of a PHP object through a graph of states and transitions.
Overview¶
Define your workflow / state graph¶
<?php
$document = new MyDocument;
$stateMachine = new Finite\StateMachine\StateMachine;
$loader = new Finite\Loader\ArrayLoader([
'class' => 'MyDocument',
'states' => [
'draft' => ['type' => 'initial', 'properties' => []],
'proposed' => ['type' => 'normal', 'properties' => []],
'accepted' => ['type' => 'final', 'properties' => []],
'refused' => ['type' => 'final', 'properties' => []],
],
'transitions' => [
'propose' => ['from' => ['draft'], 'to' => 'proposed'],
'accept' => ['from' => ['proposed'], 'to' => 'accepted'],
'refuse' => ['from' => ['proposed'], 'to' => 'refused'],
]
]);
$loader->load($stateMachine);
$stateMachine->setObject($document);
$stateMachine->initialize();
Define your object¶
<?php
class MyDocument implements Finite\StatefulInterface
{
private $state;
public function getFiniteState()
{
return $this->state;
}
public function setFiniteState($state)
{
$this->state = $state;
}
}
Work with states & transitions¶
<?php
echo $stateMachine->getCurrentState();
// => "draft"
var_dump($stateMachine->can('accept'));
// => bool(false)
var_dump($stateMachine->can('propose'));
// => bool(true)
$stateMachine->apply('propose');
echo $stateMachine->getCurrentState();
// => "proposed"
Contribute¶
Contributions are welcome !
Finite follows PSR-2 code, and accept pull-requests on the GitHub repository.
If you’re a beginner, you will find some guidelines about code contributions at Symfony.