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!