Casting Data
Read models utilises an extended AttributeCaster
system to convert scalar types to objects. Several casters are provided including a DoctrineTypeCaster
. Using the DoctrineTypeCaster
allows directly casting to any known (registered) DBAL type. Add the doctrine type to the casts array for the attribute:
class User extends Model
{
protected $casts = [
'created_at' => 'datetime',
'updated_at' => 'datetime',
'uuid' => 'uuid',
'website_url' => 'url',
'location' => 'resource:geometry',
];
}
Certain DBAL types expect a resource to work with (e.g. Creof GeoSpatial Postgres types). Prefix the type with resource:
and the string will be converted to a resource and passed through.
Note: types requiring converting to a resource may require explicitly registering by calling either:
Manager::instance()->caster()->add($doctrineCaster, ['resource:...'])
or,Manager::instance()->caster()->extend('geometry', ['resource:...'])
Embeddables / Casting to Value Objects
Just like Doctrine ORM, you can embed and hydrate value-objects into the read models. These can be the exact same VOs used in the main domain (this is safe, VOs are immutable). Like with Doctrine, these are mapped as types against the attribute name you want the resulting VO to be loaded to:
class UserContact extends Model
{
protected $casts = [
'contact' => Contact::class, // or 'contact' if that alias was registered
];
}
The Contact class has the following signature:
class Contact
{
public function __construct($name, ?PhoneNumber $phone, ?EmailAddress $email)
{
}
}
To cast this a custom AttributeCaster
is needed. All casters must implement the interface to be registered in the Managers master caster system. For the above Contact
VO, the caster could be:
use Somnambulist\Components\ReadModels\Contracts\AttributeCaster;
class ContactCaster implements AttributeCaster
{
public function types(): array
{
return ['contact', Contact::class];
}
public function supports(string $type): bool
{
return in_array($type, $this->types());
}
public function cast(array &$attributes, string $attribute, string $type): void
{
$attributes['contact'] = new Contact(
$attributes['name'],
$attributes['contact_phone'] ? new PhoneNumber($attributes['contact_phone']) : null,
$attributes['contact_email'] ? new EmailAddress($attributes['contact_email']) : null,
);
unset($attributes['name'], $attributes['contact_phone'], $attributes['contact_email']);
}
}
When the Contact
is created, the name
, contact_phone
and contact_email
attributes are used to build the value-object. This is then set to the key contact
. As the attributes array is passed by reference, the original attributes can be removed (unset()
), though this will mean other casters will not be able to access them.
It is recommended to guard against empty attribute arrays, or where the attributes only contain partial results.
The types()
method is used to pre-register the names that the caster will respond to. Any number of casters can be registered, however the type name must be unique. If you require variations use either class names, or prefixed names to distinguish.
The result is something like:
...
#attributes: array:3 [
"id" => 1
"user_id" => 1
"contact" => Contact^ {#54
-name: "A Contact"
-phone: Somnambulist\Domain\Entities\Types\PhoneNumber^ {#138
-value: "+12345678901"
}
-email: Somnambulist\Domain\Entities\Types\Identity\EmailAddress^ {#139
-value: "a.contact@example.com"
}
}
}
...
Built-in Casters
The following built-in casters are available:
AreaCaster
- cast to anArea
value-object (requires: somnambulist/domain)CoordinateCaster
- cast to aCoordinate
value-object (requires: somnambulist/domain)DistanceCaster
- cast to anArea
value-object (requires: somnambulist/domain)DoctrineTypeCaster
- cast to any register Doctrine typeExternalIdentityCaster
- cast to anExternalIdentity
value-object (requires: somnambulist/domain)MoneyCaster
- cast to aMoney
value-object (requires: somnambulist/domain)
The built-in casters can be registered in a DI container for re-use.