State Machine Transitions

The state machine transitions are pieces of PHP code that move the state machine from one state to another. Typically, such a transition implementation executes a few SQL queries, maybe send an e-mail, or calls a remote REST API. The transitions are a business logic layer of the application, and it encapsulates the model layer (i.e., data access objects or ORM).

The only way to invoke a transition is through a transition decorator. A simple decorator we will use in this example is MethodTransitionsDecorator class, which calls a protected method to process a given transition.

The transition decorator uses TransitionGuard to decide whether a given transition can be invoked. The guard consults the state machine definition and evaluates access control rules. If everything is fine, it allows the transition. Having the transition implementation hidden in the protected methods ensures that there is no other way to access the business logic, and thus, we can rely on the guard.

The implementation of our Task state machine is a rather simple set of SQL queries (using Doctrine DBAL Query Builder). We extend the MethodTransitionsDecorator and provide a protected method for each transition:

<?php // src/StateMachine/TaskTransitions.php
declare(strict_types = 1);

namespace App\StateMachine;

use Doctrine\DBAL\Connection;
use Smalldb\StateMachine\Transition\MethodTransitionsDecorator;
use Smalldb\StateMachine\Transition\TransitionEvent;
use Smalldb\StateMachine\Transition\TransitionGuard;


class TaskTransitions extends MethodTransitionsDecorator
{
	private Connection $db;


	public function __construct(TransitionGuard $guard, Connection $db)
	{
		parent::__construct($guard);
		$this->db = $db;
	}


	protected function add(TransitionEvent $trEvent, Task $task, string $description)
	{
		$this->db->createQueryBuilder()->insert("Tasks")
			->values(["id" => ":id", "description" => ":desc"])
			->setParameter(":id", $task->getMachineId())
			->setParameter(":desc", $description)
			->execute();
		$trEvent->setNewId($task->getMachineId() ?? $this->db->lastInsertId());
	}


	protected function editDescription(TransitionEvent $trEvent, Task $task, string $newDescription)
	{
		$this->db->createQueryBuilder()->update("Tasks")
			->set("description", ":desc")
			->where("id = :id")
			->setParameter(":id", $task->getMachineId())
			->setParameter(":desc", $newDescription)
			->execute();
	}


	protected function markDone(TransitionEvent $trEvent, Task $task, \DateTimeImmutable $when)
	{
		$this->db->createQueryBuilder()->update("Tasks")
			->set("completedAt", ":when")
			->where("id = :id")
			->setParameter(":id", $task->getMachineId())
			->setParameter(":when", $when->format('Y-m-d H:i:s'))
			->execute();
	}


	protected function markIncomplete(TransitionEvent $trEvent, Task $task)
	{
		$this->db->createQueryBuilder()->update("Tasks")
			->set("completedAt", "NULL")
			->where("id = :id")
			->setParameter(":id", $task->getMachineId())
			->execute();
	}


	protected function delete(TransitionEvent $trEvent, Task $task)
	{
		$this->db->createQueryBuilder()->delete("Tasks")
			->where("id = :id")
			->setParameter(":id", $task->getMachineId())
			->execute();
	}

}

A difference from the abstract methods in the Task class is the two additional arguments in each method. The TransitionEvent $trEvent represents current transition invocation. It allows us to perform some advanced actions, for example, to notify the rest of the framework about the change of the state machine ID when we create a task. The second argument, Task $task, is a reference to the state machine on which we invoked the transition.

The TaskTransitions class constructor receives the TransitionGuard $guard and Connection $db from the Symfony DI container.

In case the MethodTransitionsDecorator would not fit our needs, we could implement a custom transitions decorator by implementing the TransitionDecorator interface.

We could also use, for example, Doctrine ORM instead of the plain SQL queries. The Smalldb framework does not enforce any particular way how to implement the transitions.