State Machine Definition

Smalldb state machine stands on three components: the definition, transition implementations, and a repository.

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):

Then there are additional annotations that connect the three components together:

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:

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:

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:

Screenshot: Task Machine in 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.