StateMachine is a simple state machine with annotations for PHP, inspired by AASM known as a Ruby state machine.
StateMachine is ... ?
- using some features of Doctrine. doctrine/annotations and doctrine/inflector only.
- Doctrine is known as ORM, but StateMachine is not related to the database.
- StateMachine is anything available if the PHP class.
- composed of Doctrine's custom annotations and traits.
- very easily available. Describe the annotations and
use StateMachineTrait
to what you want to manage the state, only this. - provides
@StateMachine
,@State
,Event
, and@Transition
annotations.
As an example, you want to manage the states of Job class.
Job has `sleeping`, `running`, and `cleaning` states.
Transition to be forgiven,
* from `sleeping` to `running`
* from `running` to `cleaning`
* from `sleeping` or `cleaning` to `cleaning`
Job's initial state is `sleeping`.
This case, you can use StateMachine, only this.
use StateMachine\Annotations as SM;
use StateMachine\Traits\StateMachineTrait;
/**
* Job
*
* @SM\StateMachine(
* property="status",
* states={
* @SM\State(name="sleeping"),
* @SM\State(name="running"),
* @SM\State(name="cleaning")
* },
* events={
* @SM\Event(
* name="run",
* transitions={
* @SM\Transition(from="sleeping", to="running")
* }
* ),
* @SM\Event(
* name="clean",
* transitions={
* @SM\Transition(from="running", to="cleaning")
* }
* ),
* @SM\Event(
* name="sleep",
* transitions={
* @SM\Transition(from={"running", "cleaning"}, to="sleeping")
* }
* )
* }
* )
*/
class Job
{
use StateMachineTrait;
/**
* @var string
*/
private $status = 'sleeping';
/**
* Get status
*
* @return string
*/
public function getStatus()
{
return $this->status;
}
}
So simple! So easy!
StateMachine vs. Other state machine for PHP ...
- using annotations.
- using trait.
- no configuration files.
- no state object.
- no state machine class, only trait.
- no state machine factory.
- when state changed, is immediately reflected.
StateMachine works with PHP 5.4.0 or later.
{
"require": {
"gomachan46/state-machine": "~1.0"
}
}
Adding a state machine is as simple as:
use StateMachine\Annotations as SM;
use StateMachine\Traits\StateMachineTrait;
/**
* state machine annotations ...
*/
class ClassName
{
use StateMachineTrait;
private $status = 'initial status';
...
and start defining states and events together with their transitions.
## use StateMachine;
use StateMachine\Annotations as SM;
use StateMachine\Traits\StateMachineTrait;
/**
* Job
*
* @SM\StateMachine(
* property="status", // write you want to manage states property name
* states={ // all states write here
* @SM\State(name="sleeping"), // isSleeping() is available
* @SM\State(name="running"), // isRunning() is available
* @SM\State(name="cleaning") // isCleaning() is available
* },
* events={ // all events write here
* @SM\Event(
* name="run", // run() and canRun() are available
* transitions={
* @SM\Transition(from="sleeping", to="running")
* }
* ),
* @SM\Event(
* name="clean", clean() and canClean() are available
* transitions={
* @SM\Transition(from="running", to="cleaning")
* }
* ),
* @SM\Event(
* name="sleep", // sleep() and canSleep() are available
* transitions={
* @SM\Transition(from={"running", "cleaning"}, to="sleeping") // "from" can be set multiple state
* }
* )
* }
* )
*/
class Job
{
use StateMachineTrait; // Do not forget it!
/**
* @var string
*/
private $status = 'sleeping'; // write initial state.
/**
* Get status
*
* @return string
*/
public function getStatus()
{
return $this->status;
}
/**
* StateMachine added methods setStatus() automatically.
* Please be careful if override setStatus() method.
* I recommend that you do not override. (if you manage the state appropriately)
*/
// public function setStatus()
// {
// }
}
@SM\StateMachine
property
target property name to manage the states (e.g. property="status")
this name is used to likesetStatus()
states
events
@SM\State
- name state name (e.g. name="sleeping")
@SM\Event
- name
event name (e.g. name="run")
this name is used to likerun() canRun()
- transitions
- name
event name (e.g. name="run")
@SM\Transition
- from
transition from ... (e.g. from="sleeping")
you can set array or string. - to transition to ... (e.g. to="running")
- from
transition from ... (e.g. from="sleeping")
StateMachine provides some methods.
$job = new Job();
$job->isSleeping(); // true
$job->canRun(); // true
$job->run();
$job->isRunning(); // true
$job->isSleeping(); // false
$job->canRun(); // false
$job->getSleeping(); // 'sleeping'
$job->getRunning(); // 'running'
$job->getCleaning(); // 'cleaning'
$job->run(); // raises StateMachine\Exceptions\InvalidTransitionException
If you do not like exceptions and prefer a simple true
or false
as response, you can use whinyTransitions
option.
/**
* @SM\StateMachine(
* ...,
* whinyTransitions=true
* )
**/
job.isRunning() # => true
job.canRun() # => false
job.run # => false
StateMachine support direct assign.
$job = new Job();
$job->getStatus(); // 'sleeping'
$job->setStatus('running'); // return $job
$job->getStatus(); // 'running'
$job->setStatus('sleeping'); // raises StateMachine\Exceptions\NoDirectAssignmentException
If you do not want to forgive direct assign, you can use noDirectAssignment
option.
/**
* @SM\StateMachine(
* ...,
* noDirectAssignment=true
* )
**/
Only this!
$job = new Job();
$job->getStatus(); // 'sleeping'
$job->setStatus('running'); // raises StateMachine\Exceptions\NoDirectAssignmentException
You can set callback method when ...
- before event
- before exit old state
- exit old state
- after transition
- before enter new state
- enter new state
- update state
- after exit old state
- after enter new state
- after event
<?php
namespace StateMachine\Tests\Entity;
use StateMachine\Annotations as SM;
use StateMachine\Traits\StateMachineTrait;
/**
* CallbackJob
*
* @SM\StateMachine(
* property="status",
* states={
* @SM\State(
* name="sleeping",
* beforeExit="beforeExitSleeping",
* exit="exitSleeping",
* afterExit="afterExitSleeping"
* ),
* @SM\State(
* name="running",
* beforeEnter="beforeEnterRunning",
* enter="enterRunning",
* afterEnter="afterEnterRunning"
* ),
* @SM\State(name="cleaning")
* },
* events={
* @SM\Event(
* name="run",
* transitions={
* @SM\Transition(
* from="sleeping",
* to="running",
* after="afterTransition"
* )
* },
* before="beforeRunEvent",
* after="afterRunEvent"
* ),
* @SM\Event(
* name="clean",
* transitions={
* @SM\Transition(from="running", to="cleaning")
* }
* ),
* @SM\Event(
* name="sleep",
* transitions={
* @SM\Transition(from={"running", "cleaning"}, to="sleeping")
* }
* )
* }
* )
*/
class CallbackJob
{
use StateMachineTrait;
/**
* @var string
*/
private $status = 'sleeping';
/**
*
*/
private function beforeEnterRunning()
{
...
}
/**
*
*/
private function enterRunning()
{
...
}
/**
*
*/
private function afterEnterRunning()
{
...
}
/**
*
*/
private function beforeExitSleeping()
{
...
}
/**
*
*/
private function exitSleeping()
{
...
}
/**
*
*/
private function afterExitSleeping()
{
...
}
/**
*
*/
private function beforeRunEvent()
{
...
}
/**
*
*/
private function afterRunEvent()
{
...
}
/**
*
*/
private function afterTransition()
{
...
}
}
In this case,
$job = new CallbackJob();
$job->run();
/**
* run methods in this order.
*
* beforeRunEvent()
* beforeExitSleeping()
* exitSleeping()
* afterTransition()
* beforeEnterRunning()
* enterRunning()
* setStatus('running')
* afterExitSleeping()
* afterEnterRunning()
* afterRunEvent()
*/
You can run methods with args, too.
For example,
public function beforeRunEvent()
{
$args = func_get_args();
echo join(', ', $args);
}
$job->run('foo', 'bar'); // echo 'foo, bar';
In future, I am going to implement like AASM provides services:
- Guards
- Inspection
- and more ...
- create an issue on GitHub
- send us a tweet @gomachan46
- send mail [email protected]
Feel free!
- Please fork it!
- Create your feature branch:
git checkout -b my-new-feature
- Commit your changes:
git commit -am 'Add some feature'
- Push to the branch:
git push origin my-new-feature
- Submit a pull request :)
I'm looking forward to your contributions!
Thank you for the great product.