Custom type¶
The User name is typed with a string. This means that it can be empty. Let's introduce the Name type to make sure it's never empty:
Name.php
final readonly class Name
{
    private function __construct(
        private string $value,
    ) {}
    public static function of(string $value): self
    {
        return match ($value) {
            '' => throw new \LogicException('The name cannot be empty'),
            default => new self($value),
        };
    }
    public function toString(): string
    {
        return $this->value;
    }
}
We can now refactor the User like this:
User.php
use Formal\ORM\Id;
final readonly class User
{
    /**
     * @param Id<self> $id
     */
    private function __construct(
        private Id $id,
        private Name $name,
    ) {}
    public static function new(Name $name): self
    {
        return new self(Id::new(self::class), $name);
    }
    public function name(): Name
    {
        return $this->name;
    }
    public function rename(Name $name): self
    {
        return new self($this->id, $name);
    }
}
And just like that the User can't have an empty name.
But for Formal to properly store this Name we need to tell it how to convert the object to a primitive value and vice-versa.
NameType.php
use Formal\ORM\Definition\Type;
/**
 * @psalm-immutable
 * @implements Type<Name>
 */
final class NameType implements Type
{
    public function normalize(mixed $value): null|string|int|float|bool
    {
        return $value->toString();
    }
    public function denormalize(null|string|int|float|bool $value): mixed
    {
        if (!\is_string($value)) {
            throw new \LogicException("'$value' is not a string");
        }
        return Name::of($value);
    }
}
@implements Type<Name> allows Psalm to know that the $value argument of normalize is always a Name (despite it's mixed type), and the return type of denormalize must also be a Name.
And lastly tell the ORM about this type converter:
use Formal\ORM\{
    Manager,
    Definition\Aggregates,
    Definition\Types,
    Definition\Type\Support,
}
$orm = Manager::of(
    /* any adapter (1) */,
    Aggregates::of(
        Types::of(
            Support::class(Name::class, new NameType),
        ),
    ),
);
- See the Adapters chapter to see all the adapters you can use.
 
With this you can also use the ?Name type on a property.
Formal handles the null case for you!