State Machine Properties
The state machine properties represent data that the state machine stores. Usually, each property corresponds with a column in an assigned SQL table. However, some properties may be calculated (e.g., using a subselect rather than a column), or generated by the state machine reference (e.g., a relation to another state machine).
To define the properties, we will need two things: a DTO object to carry the data, and an interface that could the state machine reference implement.
For our Task state machine, we will use the TaskDataImmutable
object, which implements the TaskData
interface.
However, we will not write these on our own — we will use the CodeCooker component to generate them.
Code Cooker & DTO Generator
The CodeCooker is a code generator which scans for annotations in our classes
and then generates other classes according to registered recipes.
One of the recipes is a DtoRecipe
, which utilizes the DtoGenerator
class to generate boilerplate code for our DTO objects.
Note that this generated boilerplate is not specific to Smalldb state machines.
The input class for the DtoGenerator
will be the TaskProperties
class
— an abstract class with a few protected properties (member variables) only,
and this will be the only class we write by hand here:
<?php // src/StateMachine/TaskProperties.php
declare(strict_types = 1);
namespace App\StateMachine;
use Smalldb\CodeCooker\Annotation\GenerateDTO;
use Smalldb\StateMachine\SqlExtension\Annotation\SQL;
/**
* @GenerateDTO("TaskData")
* @SQL\Table("Tasks")
*/
abstract class TaskProperties
{
/**
* @SQL\Id
*/
protected int $id;
/**
* @SQL\Column
*/
protected string $description;
/**
* @SQL\Column
*/
protected ?\DateTimeImmutable $completedAt;
}
The @GenerateDTO("TaskData")
annotation triggers the DtoRecipe
to generate TaskData classes (in the current namespace).
The @SQL
annotations define a mapping between SQL table Tasks
and the properties of this DTO object.
These will be used by repository data source and query builder in the next chapter.
This TaskProperties
class becomes a common ancestor of the generated classes, which will implement various accessor methods.
The DtoGenerator
generates the following classes.
Generated DTO Classes
TaskData interface
TaskData
interface defines getters for each protected property of TaskProperties
class:
interface TaskData {
public function getId(): int;
public function getDescription(): string;
public function getComletedAt(): ?\DateTimeImmutable;
}
TaskDataImmutable class
TaskDataImmutable
class is an immutable DTO that implements the getters from TaskData
interface
and the with*
methods to create a modified clone.
There are also some helper methods to access a property by name (the get()
method),
or to construct the immutable object from various inputs.
The constructor semantics is inspired by C++ copy constructors.
The new object will contain data from the $source
.
Unlike the clone
operator, the copy constructor accepts any objects that implement TaskData
interface
and always produces TaskDataImmutable
.
class TaskDataImmutable extends TaskProperties implements TaskData {
public function __construct(?TaskData $source = null) {/*...*/}
public static function fromArray(?array $source, ?TaskData $sourceObj = null): ?self {/*...*/}
public static function fromIterable(?TaskData $sourceObj, iterable $source): self {/*...*/}
public function getId(): int {/*...*/}
public function getDescription(): string {/*...*/}
public function getCompletedAt(): ?DateTimeImmutable {/*...*/}
public static function get(TaskData $source, string $propertyName) {/*...*/}
public function withId(int $id): self {/*...*/}
public function withDescription(string $description): self {/*...*/}
public function withCompletedAt(?DateTimeImmutable $completedAt): self {/*...*/}
}
TaskDataMutable class
TaskDataMutable
class is a mutable variant of TaskDataImmutable
class.
Instead of the with*
methods, it has traditional setters that mutate the data in-place.
Both classes implement the TaskData
interface, and both extend the abstract TaskProperties
class.
They are mostly interchangeable except for the mutable/immutable semantics.
class TaskDataMutable extends TaskProperties implements TaskData {
public function __construct(?TaskData $source = null) {/*...*/}
public static function fromArray(?array $source, ?TaskData $sourceObj = null): ?self {/*...*/}
public static function fromIterable(?TaskData $sourceObj, iterable $source): self {/*...*/}
public function getId(): int {/*...*/}
public function getDescription(): string {/*...*/}
public function getCompletedAt(): ?DateTimeImmutable {/*...*/}
public static function get(TaskData $source, string $propertyName) {/*...*/}
public function setId(int $id): void {/*...*/}
public function setDescription(string $description): void {/*...*/}
public function setCompletedAt(?DateTimeImmutable $completedAt): void {/*...*/}
}
We can also use the mutable class as a builder to construct the immutable object:
$builder = new TaskDataMutable();
$builder->setDescription($description);
$builder->setCompletedAt(new DateTimeImmutable());
$immutableObject = new TaskDataImmutable($builder);
DTO Without a Generator
Smalldb generates implementations of abstract methods in the reference classes automatically.
The generated class uses ReferenceTrait
to implement most of the state machine API,
the rest of the abstract methods are either transitions or property getters.
A property getter will be implemented automatically when:
- it is an abstract method of the reference class (directly or via an interface),
- and it is implemented by the DTO object specified by
@WrapDto
annotation.
Therefore, if you want to use a custom hand-written DTO object, you need to create an interface wish the getters you want to use from the reference and pass it to the reference object.
/**
* @StateMachine("your-custom-machine")
* @WrapDto(YourCustomDTO::class)
*/
abstract class YourCustomMachine implements ReferenceInterface, YourCustomDtoGetters {
/* ... */
}
class YourCustomDto implements YourCustomDtoGetters {
public function getSomething() {/* ... */}
public function setSomething($value) {/* ... */}
}
interface YourCustomDtoGetters {
public function getSomething();
}
You may skip the generator, the mutable/immutable stuff, and keep it this simple. You may also inline the getters directly into the reference class; however, the use of the interface helps to keep the classes consistent.