State Machine Definition
Smalldb state machine stands on three components: the definition, transition implementations, and a repository.
-
The definition tells what can happen in each state. It also provides additional metadata via definition extensions, for example, icons and labels for the actions so that we can generate a menu.
-
The transition implementations are the PHP code that moves the state machine from one state to another — we typically let them execute SQL queries and send e-mails.
-
The repository provides persistence, and a way of browsing the state machine space via methods that return a reference to a state machine, or a collection of such references.
The state machine references are like pointers to state machines. They are not the state machines, nor their representation; they provide the means of communication with the referred state machine, because the Smalldb state machine is an abstract construct independent of application runtime. For a more detailed explanation, please take a look at the published papers, especially the “RESTful State Machines and SQL Database”.
To create a state machine definition, we create an abstract class with several annotations.
The most important annotations are the following (from the Smalldb\StateMachine\Annotation
namespace):
-
@StateMachine($name)
, a class annotation, marks the given class as the state machine definition. Smalldb looks for these annotations when searching the definitions. -
@State($stateName)
, a class constant annotation, defines a state of the state machine. The value of the annotated constant is the name of the state unless the optional$stateName
is provided. -
@Transition($sourceStateName, {$targetStateNames,...})
, a method annotation, defines a transition of the state machine. The method name is the name of the transition, which goes from the source state to one of the target states. Note that the Smalldb state machines are nondeterministic, and thus, there may be multiple target states of the transition (therefore, the second argument is an array). Transition parameters (passed when invoking the transition) are defined by the method signature.
Then there are additional annotations that connect the three components together:
-
@UseRepository($repositoryClassName)
tells Smalldb which repository can provide references to this state machine. -
@UseTransitions($transitionsClassName)
tells Smalldb which class implements the state machine transitions. -
@UseReference($referenceClassName)
tells Smalldb which class to use as a state machine reference. This annotation is available only for completeness because the definition class is typically a reference class.
These six annotations form the core of the state machine definition. All other annotations you may encounter come from extensions. The extensions are nothing more than immutable objects attached to various pieces of the state machine definition. Smalldb includes several extensions, and third-party libraries may provide additional extensions. In this application, we will use the Style extension, DTO extension, and SQL extension.
So, let’s create App\StateMachine\Task
class:
<?php // src/StateMachine/Task.php
declare(strict_types = 1);
namespace App\StateMachine;
use App\StateMachine\TaskData\TaskData;
use App\StateMachine\TaskData\TaskDataImmutable;
use Smalldb\StateMachine\Annotation\StateMachine;
use Smalldb\StateMachine\Annotation\State;
use Smalldb\StateMachine\Annotation\Transition;
use Smalldb\StateMachine\Annotation\UseRepository;
use Smalldb\StateMachine\Annotation\UseTransitions;
use Smalldb\StateMachine\DtoExtension\Annotation\WrapDTO;
use Smalldb\StateMachine\StyleExtension\Annotation\Color;
use Smalldb\StateMachine\ReferenceInterface;
/**
* @StateMachine("task")
* @UseRepository(TaskRepository::class)
* @UseTransitions(TaskTransitions::class)
* @WrapDTO(TaskDataImmutable::class)
*/
abstract class Task implements ReferenceInterface, TaskData
{
/**
* @State
* @Color("#ea8")
*/
const TODO = "Todo";
/**
* @State
* @Color("#4c0")
*/
const DONE = "Done";
/**
* State function: Map TaskData to state machine state.
* The "Not Exists" state is detected automatically.
*/
public function getState(): string
{
return $this->getCompletedAt() === null ? self::TODO : self::DONE;
}
/**
* @Transition("", {"Todo"})
*/
abstract function add(string $description);
/**
* @Transition("Todo", {"Todo"})
*/
abstract function editDescription(string $newDescription);
/**
* @Transition("Todo", {"Done"})
*/
abstract function markDone(\DateTimeImmutable $when);
/**
* @Transition("Done", {"Todo"})
*/
abstract function markIncomplete();
/**
* @Transition("Todo", {""})
* @Transition("Done", {""})
*/
abstract function delete();
}
As we can see, this definition has three states:
""
, the “Not Exists” state, defined inReferenceInterface
."Todo"
."Done"
.
After the state definitions, we have a state function getState()
that checks the data stored within state machine properties (we will define them later)
and returns the state machine’s current state.
Finally, the state machine defines five actions with six transitions — the delete
action has two transitions:
add
editDescription
markDone
markIncomplete
delete
The TaskData
interface with @WrapDTO
annotation defines how the state machine’s properties are handled.
We will return to these in the later chapters.
If we comment out this interface (as we will create it later),
we should see the state machine definition in the Symfony profiler:
Smalldb provides us with a nice state diagram of our new state machine.
We can also see the effect of the @Color
annotation on the states.
Similarly, we can colorize the transitions as well.
If you are wondering why there are four nodes in the state diagram when the state machine has only three states, it is because both dots represent the “Not Exists” state. It is more convenient to split the “Not Exists” state into two nodes, because the graph then better captures real-world workflows that have a beginning and an end.
Now we need to implement transitions, define properties, and create a repository.
State Machine Definition API
Smalldb provides API to read the state machine definitions at runtime. The definitions are immutable objects compiled into the DI container and available via Smalldb entry point or via reference objects:
use Smalldb\StateMachine\Smalldb;
use App\StateMachine\Task;
$smalldb = $container->get(Smalldb::class);
$taskDefinition = $smalldb->getDefinition(Task::class);
$doneState = $taskDefinition->getState(Task::DONE);
var_dump($doneState->getName()); // "Done"
$markDoneTransition = $taskDefinition->getTransition("markDone", Task::DONE);
var_dump($markDoneTransition->getSourceState()->getName()); // Also "Done".
The state machine definition and all its components are extensible. Such an extension is also an immutable object, and it provides some additional metadata. The extensions are identified by their respective class names.
One of the extensions is StyleExtension
.
It enables us to assign some visual attributes to the definition:
/** @var StyleExtension $doneStyle */
if (($doneStyle = $doneState->findExtension(StyleExtension::class)) !== null) {
var_dump($doneStyle->getColor()); // "#4c0" (See the @Color annotation in the definition.)
}
Each extension is optional. Therefore, we must check for its presence before using it.
The findExtension()
method returns null
when the extension is missing.
We can also check for the presence of an extension using hasExtension()
method,
and then retrieve it using getExtension()
method, which will throw an exception when the extension is missing.
You may explore each state machine definition and its extensions using the Symfony Profiler. The extensions and the data they carry are listed under the mandatory attributes of each definition component.
Smalldb provides several extensions, including Access Control Extension, BPMN Extension, GraphML Extension, and SQL Extension. These extensions are replacable and optional. However, some extensions, like Sources Extension, are used by the Smalldb framework itself.
Applications and libraries are expected to introduce their own extensions, and because the extensions are identified using fully-qualified class names, we do not have to worry about conflicts between the libraries.