Stars: 106
Forks: 11
Pull Requests: 2
Issues: 6
Watchers: 4
Last Updated: 2023-06-29 14:27:25
State Machine library for PHP
License: GNU Lesser General Public License v3.0
Languages: PHP
It is State Machine library written on PHP aimed to business process.
This library has only a simple external dependency, it is a minimalist (yet complete) library with only 3 classes.
Since this library is PHP native, then it could run in Laravel, Symfony and any other frameworks.
A State Machine (also called Automata) is a procedural execution of a job based in states. Every job must have a single state at the same time, such as "INITIATED","PENDING","IN PROCESS" and so on, and the job changes of state (transition) according to some logic or condition. Such conditions could be a field, a time or a custom function.
The target of this library is to ease the process to create a state machine for business.
We need to create a process to deliver Chinese food at home. Is it easy?. Well, let's find it out.
Fields are values used for out State Machine. In this example, I am not including other values that it could be useful (such as money, customer name, address and such) because they are not part or used by the state machine.
It must include all the possible situation. The real world is not as easy as: sell and money.
Example/BuyMilk.php (buy milk)
Example/Car.php (car parking)
<?php
use eftec\statemachineone\StateMachineOne;
include "vendor/autoload.php";
define("STATE_PICK",1);
define("STATE_CANCEL",2);
define("STATE_TRANSPORT",3);
define("STATE_ABORTTRANSPORT",4);
define("STATE_TODELIVER",5);
define("STATE_HELP",6);
define("STATE_DELIVERED",7);
define("STATE_ABORTED",8);
$smachine=new StateMachineOne();
$smachine->setDebug(true);
$smachine->tableJobs="chopsuey_jobs";
$smachine->tableJobLogs="chopsuey_logs";
$smachine->setDefaultInitState(STATE_PICK);
$smachine->setAutoGarbage(false); // we don't want to delete automatically a stopped job.
$smachine->setStates([STATE_PICK=>'Pick order'
,STATE_CANCEL=>'Cancel order'
,STATE_TRANSPORT=>'Transport order'
,STATE_ABORTTRANSPORT=>'Abort the delivery'
,STATE_TODELIVER=>'Pending to deliver'
,STATE_HELP=>'Request assistance'
,STATE_DELIVERED=>'Delivered'
,STATE_ABORTED=>'Aborted']);
$smachine->fieldDefault=[
'customerpresent'=>-1
,'addressnotfound'=>-1
,'signeddeliver'=>-1
,'abort'=>-1
,'instock'=>-1
,'picked'=>-1
,'message'=>''];
$smachine->setDB('mysql','localhost',"root","abc.123","statemachinedb");
$smachine->createDbTable(false); // you don't need to create this table every time.
$smachine->loadDBAllJob(); // we load all jobs, including finished ones.
// business rules
$smachine->addTransition(STATE_PICK,STATE_PICK
,'when instock = 0 set message="without stock"','stay'); // it stays in the same state
$smachine->addTransition(STATE_PICK,STATE_CANCEL
,'when instock = 0 set abort = 1','stop'); // ends the process
$smachine->addTransition(STATE_PICK,STATE_TRANSPORT
,'when instock = 1','change'); // changes transition
$smachine->addTransition(STATE_TRANSPORT,STATE_ABORTTRANSPORT
,'when abort = 1','stop'); // ends the process
$smachine->addTransition(STATE_TRANSPORT,STATE_DELIVERED
,'when addressnotfound = 0 and customerpresent = 1 and signeddeliver = 1 timeout 3600','stop'); // 1 hour max.
$smachine->addTransition(STATE_TRANSPORT,STATE_HELP
,'when addressnotfound = 1 or customerpresent = 0 timeout 3600','change'); // 1 hour max
$smachine->addTransition(STATE_HELP,STATE_ABORTED
,'when wait timeout 900','change'); // it waits 15 minutes max.
$smachine->addTransition(STATE_HELP,STATE_DELIVERED
,'when addressnotfound = 0 and customerpresent = 1 and signeddeliver = 1','change');
$msg=$smachine->fetchUI();
$smachine->checkAllJobs();
$smachine->viewUI(null,$msg); // null means it takes the current job
Let's say the next transition
$smachine->addTransition(STATE_PICK,STATE_CANCEL
,'when instock = 0 set abort = 1','stop');
The transition is written as follows:
"_when_ var1 = var2 and var3 = var4 or var4 = var5"
"_set_ var1 = var2 , var3 = var4"
"_timeout_ var1"
"_fulltimeout_ var2"
It adds a condition so where the transition must be done. For example:
$smachine->addTransition(STATE_ONE,STATE_TWO,'when field = 0');
In this case, the state changes from STATE_ONE to STATE_TWO when field is zero.
It could also exist multiple initial states
$smachine->addTransition([STATE_ONE,STATE_EXTRA],STATE_TWO,'when field = 0');
// its the same than to add multiples transitions:
// $smachine->addTransition(STATE_ONE,STATE_TWO,'when field = 0');
// $smachine->addTransition(STATE_EXTRA,STATE_TWO,'when field = 0');
Types of transition:
"when field=0" // it happens when the field is zero.
"when $var='hi'" // it happens when the global variable is 'hi'
"when fn()=44" // the transition is triggered when the function fn() returns 44
"when always" // its always true. It is the same than "when 1=1". The transition is always executed
It compares a constant. The binary operator for comparison are
"when field contain 'text'"
Values of the field could be as the next ones:
"when field = field2" // when field (of the job) is equals to field2
"set id=_idjob"
"set ts=_time"
"set currentstate=_state0"
"set nextstate=_state1"
"set transition_happened=_result"
$v1=20;
$smachine->addTransition(S1,S2,"when $v1=1"); // WRONG: the variable v1 is evaluated when it is defined, i.e equals to write "when 20=1"
$smachine->addTransition(S1,S2,"when \$v1=1"); // RIGHT: the variable v1 is evaluated when the transition is checked
$smachine->addTransition(S1,S2,'when $v1=1'); // RIGHT:(') the variable v1 is evaluated when the transition is checked
"when field = 777" // when field is equals to 777
"AAA", 'aaa' = it is a literal
when field = 'hello' // when field is equals to the text hello
function() = it is a global function. Every function must have the parameter $job.
"when field = somefunc()" // function somefunc(Job $job) {...}
"when field = null() "
"when field = true()" // when field is equals to true
"when field = true()" // when field is equals to false
on() it is the on value (1)
off() it is the off value (0)
undef() it is the undefined value (-1)
flip() indicates that the value will be flipped (1=>0 and 0=>1). Example (x=1) x = flip(), now (x=0). If the value is not zero, then it's flipped to zero.
"set field=flip()" // it is only valid for set.
$smachine=new StateMachineOne();
function someFunction($job) {
// we could do something here we should return a value
return 0;
}
$var=222;
$smachine->fieldDefault=[
'field2'=>0
,'field3'=>0
,'field4'=>0];
$smachine->addTransition(STATE_ONE,STATE_TWO
,'when field2 = 2 and field3 > someFunction() and field4=$var');
We could add one or many change of variables when a transition is done.
$smachine->addTransition(STATE_ONE,STATE_TWO,'when field = 0 set field=1');
In this case, when we are in "STATE_ONE" and field=0, then we change to state "STATE_TWO", and we assign the field=1
Examples:
"set field = 0 , field2 = 3"
It sets a field of the job.
"set 0 = 20" // is invalid
"set myfunc() = 20"
Where the function (global) must be defined as myfunc(Job $job,$input) {}
"set field=20"
"set $variable=20"
"set field = 0"
It sets the field to the value 0
"set field + 1"
It increases the value of field by 1 (field=field+1)
"set field - 1"
It decreases the value of field by 1 (field=field-1)
It works similar than "set" but it is executed when the transition is not done.
$smachine->addTransition(STATE_ONE,STATE_TWO,'when field = 0 set field2="done" else field2="not done"');
In this case, it sets the field2="not done" until the transition is done.
Note: This operation is called every time the expression is evaluated. So it could be evaluated many times.
$smachine->addTransition(STATE_ONE,STATE_TWO,'when field = 0 timeout 3600');
The state chances from STATE_ONE to STATE_TWO when the field is zero, or has elapsed 3600 seconds elapsed in the STATE_ONE.
It sets the timeout between the time of current state and the current time. If a timeout happens, then the transition is executed.
"timeout 3600" // 1 hour timeout
"timeout field" // timeout by field, it is calculated each time.
$smachine->addTransition(STATE_ONE,STATE_TWO,'when field = 0 fulltimeout 3600');
The state chances from STATE_ONE to STATE_TWO when the field is zero, or has elapsed (since the start of the job) 3600 seconds.
It sets the timeout between the time of initial state and the current time. If a timeout happens, then the transition is executed.
"fulltimeout 3600" // 1 hour timeout
"fulltimeout field" // timeout by field, the field is evaluated each time.
Let's say we have a blueprint to build the house.
The job is the action to build the house and the blueprint are the transitions.
So, the job is an operative part of our work-flow.
A job keeps values, including the current state, and it has a lifecycle, while the workflow (the transitions) doesn't keep any single value. It is possible to create a short life job that works in a single web thread. However, if we need to keep the values, then we could use a database or a file system (flat file).
There are several ways to create a job. One of them is using the GUI and other is via code.
Creating a job via code
// $smachine is our state machine with transitions, events and such.
$job=$smachine->createJob(['value1'=>'hi','value2'=>'world']); // we create a job with the values value1 and value2
// or we could create the job the with the default values
$job=$smachine->createJob();
$smachine->checkJob($job); // then we executed the job.
var_dump($job->state); // the id of the current state.
var_dump($job->fields); // And we could see the result of the job
$smachine->checkJob($job); // then we executed the job.
$smachine->checkAllJob(); // We execute all jobs stored in our state machine.
$smachine->getLastJob(); // we load the last job (if we already has a job into a memory);
$smachine->getJob($idJob); // we get a job from the queue.
$smachine->getJob($idJob); // we get a job from the queue.
$smachine->getJobQueue(); // we get all the jobs from the queue
$smachine->loadDbJob($idJob); // we read a job with the id $idJob and we store into the queue
$smachine->loadDbAllJob(); // We load all jobs (including inactive jobs) from the database and we store into the queue
$smachine->loadDBActiveJobs(); // We load all active jobs from the database and we store into the queue.
$smachine->loadDbJob($idJob); // we read a job with the id $idJob
$smachine->loadDbAllJob(); // We load all jobs (including inactive jobs) from the database.
$smachine->loadDBActiveJobs(); // We load all active jobs from the database.
$smachine->saveDBAllJob(); // we save all the jobs in memory.
$smachine->saveDBJob($job); // we save a specific job.
$smachine->saveDBJobLog($job,$texto); // we save a log of a job.
$smachine->deleteJobDB($job); // we delete a specific job.
This library has a build-in GUI for testing.
StateMachineOne It is the main class.
Job It is the model class for the job
Transition It is the model class for the transitions.
It is possible to cache all the configurations.
$stateMachine=new StateMachineOne();
// configuration goes here.
file_put_contents("mycode.php", "<?php\n".$stateMachine->cacheMachine("FumMachineCache"));
$stateMachine=new StateMachineOne();
include 'mycode.php';
FumMachineCache($stateMachine);
Commonly, the log format could be of the type info or error. Flag could show a different type.
state,,changed,,Parked,,1,,Idling,,2,,0,,change
state = verb of the operation.
changed = the operation executed. It could be changed,stop,continue
Parked = the first state (before the change)
1 = the number of the first state (before the change)
Idling = the state it changed.
2 = the number of the state it changed.
0 = the transaction that triggered the change.
change = the description of the change.
state,,transition,,text,,1,,message // when a check of the job fails
savejob,,message // when to save a job fails.
changestate,,idjob,,idstate,newstate // when the change of a state fails
Dual license (LGPL 3.0 and Commercial). See LICENSE file.
2.23.3 2023-06-29
2.23.2 2023-06-29
2.23.1 23-03-11
2.23 22-09-11
2.22 22-09-11
2.21.1 2022-09-03
2.21 2022-09-03
2.20 2022-08-26
2.18 2022-06-28
2.17 2021-10-01
2.17 2021-09-26
2.16 2021-09-26
2.15 2021-09-18
2.14 2021-09-17
2.13 2021-07-03
2.12 2021-01-16
2.11 2020-10-16
2.10.1 2020-10-15
2.10 2020-10-15
saveDbJob(): Update in the database: The library doesn't update fields that aren't changed. For this,
it creates a backup variable every time a job is loaded, and it compares the backup with the job to save.
2.9.2 2020-09-29 saveDbJob() updated the primary key field. Now, it skips to update it.
2.9.1 2020-09-22 cacheMachine() now works correctly.
2.9 2020-09-20 The flags are visualized differently. Also, the serialization of text_job now use serialize instead of JSON. Previous jobs must be flushed, you can flush with $stateMachine->createDbTable(false);
2.8 2020-09-15 added the field $fieldUI to specify visual components.
2.7 2020-08-11 a small update of dependencies.
2.6 2020-04-23
2.5 2020-04-13
2.4 2019-12-26
2.3 2019-12-26
2.2 2019-10-22
2.1 2019-08-28
2.0 2019-08-24
1.12 2019-08-16
1.11 2019-08-04 Some fixes.
1.10 2019-08-04
1.9 2019-08-03
1.8 2019-07-29 Some cleanups and methods setPdoOne() and getPdoOne();
1.7 2019-06-16 Lots of changes.
1.6 2018-12-26 Now MiniLang is a separate dependency.
1.5 2018-12-23 Xmas update (btw porca miseria).
1.4 2018-12-12
1.3 2018-12-11
1.2 2018-12-09 Updated dependency
1.1 2018-12-09 Some corrections.
1.0 2018-12-08 First (non beta) version.