ObjectHydrator
Object Hydration library to create Command and Query objects.
Install / Use
/learn @EventSaucePHP/ObjectHydratorREADME
Object Hydrator (and Serializer)
Installation
composer require eventsauce/object-hydrator
Skip to usage documentation.
About
This library allows magic-less conversion from serialized data to object and back. Unlike other object mappers, this library does not rely on magic reflection to set private properties. It hydrates and serializes objects as if you would do it by hand. The hydration mechanism inspects the constructor and figures out which keys need to map to which properties. The serialization mechanism inspects all public properties and getter-methods, converts the values from objects to plain data-structures. Unlike "magic" hydration mechanisms, that are able to grab private properties, this way to map objects opens the door to object mapping without reflection. You get all the convenience with none of the guilt (or performance hits).
This is a utility that converts structured request data (for example: decoded JSON) into a complex object structure. The intended use of this utility is to receive request data and convert this into Command or Query object. The library is designed to follow a convention and does not validate input.
When and why would you use this?
That's a good question, so let's dig in. Initially, this library was created to map plain data (like JSON request bodies) to strict object structures. The use of object (DTOs, Query and Command objects) is a great way to create expressive code that is easy to understand. Objects can be trusted to correctly represent concepts in your domain. The downside of using these objects is that they can be tedious to use. Construction and serialization becomes repetitive and writing the same code over and over is boring. This library aims to remove the boring parts of object hydration and serialization.
This library was built with two specific use-cases in mind:
- Construction of DTOs, Query-object, and Command-objects.
- Serialization and hydration of Event-objects.
Object hydration and serialization can be achieved at zero expense, due to an ahead-of-time resolving steps using code generation.
Quick links:
- Installation
- About
- Design goals
- Usage
- Custom mapping key
- Mapping from multiple keys
- Property casting
- Casting to scalar values
- Casting to a list of scalar values
- Casting to a list of objects
- Casting to DateTimeImmutable objects
- Casting to Uuid objects (ramsey/uuid)
- Using multiple casters per property
- Creating your own property casters
- Static constructors
- Key formatters
- Maximizing performance
Design goals
This package was created with a couple design goals in mind. They are the following:
- Object creation should not be too magical (use no reflection for instantiation)
- There should not be a hard runtime requirement on reflection
- Constructed objects should be valid from construction
- Construction through (static) named constructors should be supported
Usage
This library supports hydration and serialization of objects.
Hydration usage
By default, input is mapped by property name, and types need to match. By default, keys are mapped from snake_case input
to camelCase properties. If you want to keep the key names you can use the KeyFormatterWithoutConversion.
use EventSauce\ObjectHydrator\ObjectMapperUsingReflection;
$mapper = new ObjectMapperUsingReflection();
class ExampleCommand
{
public function __construct(
public readonly string $name,
public readonly int $birthYear,
) {}
}
$command = $mapper->hydrateObject(
ExampleCommand::class,
[
'name' => 'de Jonge',
'birth_year' => 1987
],
);
$command->name === 'de Jonge';
$command->birthYear === 1987;
Complex objects are automagically resolved.
class ChildObject
{
public function __construct(
public readonly string $value,
) {}
}
class ParentObject
{
public function __construct(
public readonly string $value,
public readonly ChildObject $child,
) {}
}
$command = $mapper->hydrateObject(
ParentObject::class,
[
'value' => 'parent value',
'child' => [
'value' => 'child value',
]
],
);
A simple doc-comment ensures that arrays of objects are automagically converted.
class ChildObject
{
public function __construct(
public readonly string $value,
) {}
}
class ParentObject
{
/**
* @param ChildObject[] $list
*/
public function __construct(
public readonly array $list,
) {}
}
$object = $mapper->hydrateObject(ParentObject::class, [
'list' => [
['value' => 'one'],
['value' => 'two'],
],
]);
$object->list[0]->value === 'one';
$object->list[1]->value === 'two';
The library supports the following formats:
@param Type[] $name@param array<Type> $name@param array<string, Type> $name@param array<int, Type> $name@param list<Type> $name
Custom mapping key
use EventSauce\ObjectHydrator\MapFrom;
class ExampleCommand
{
public function __construct(
public readonly string $name,
#[MapFrom('year')]
public readonly int $birthYear,
) {}
}
Mapping from multiple keys
You can pass an array to capture input from multiple input keys. This is useful when multiple values represent a singular code concept. The array allows you to rename keys as well, further decoupling the input from the constructed object graph.
use EventSauce\ObjectHydrator\MapFrom;
class BirthDate
{
public function __construct(
public int $year,
public int $month,
public int $day
){}
}
class ExampleCommand
{
public function __construct(
public readonly string $name,
#[MapFrom(['year_of_birth' => 'year', 'month', 'day'])]
public readonly BirthDate $birthDate,
) {}
}
$mapper->hydrateObject(ExampleCommand::class, [
'name' => 'Frank',
'year_of_birth' => 1987,
'month' => 11,
'day' => 24,
]);
Property casting
When the input type and property types are not compatible, values can be cast to specific scalar types.
Casting to scalar values
use EventSauce\ObjectHydrator\PropertyCasters\CastToType;
class ExampleCommand
{
public function __construct(
#[CastToType('integer')]
public readonly int $number,
) {}
}
$command = $mapper->hydrateObject(
ExampleCommand::class,
[
'number' => '1234',
],
);
Casting to a list of scalar values
use EventSauce\ObjectHydrator\PropertyCasters\CastListToType;
class ExampleCommand
{
public function __construct(
#[CastListToType('integer')]
public readonly array $numbers,
) {}
}
$command = $mapper->hydrateObject(
ExampleCommand::class,
[
'numbers' => ['1234', '2345'],
],
);
Casting to a list of objects
use EventSauce\ObjectHydrator\PropertyCasters\CastListToType;
class Member
{
public function __construct(
public readonly string $name,
) {}
}
class ExampleCommand
{
public function __construct(
#[CastListToType(Member::class)]
public readonly array $members,
) {}
}
$command = $mapper->hydrateObject(
ExampleCommand::class,
[
'members' => [
['name' => 'Frank'],
['name' => 'Renske'],
],
],
);
Casting to DateTimeImmutable objects
use EventSauce\ObjectHydrator\PropertyCasters\CastToDateTimeImmutable;
class ExampleCommand
{
public function __construct(
#[CastToDateTimeImmutable('!Y-m-d')]
public readonly DateTimeImmutable $birthDate,
) {}
}
$command = $mapper->hydrateObject(
ExampleCommand::class,
[
'birthDate' => '1987-11-24',
],
);
Casting to Uuid objects (ramsey/uuid)
use EventSauce\ObjectHydrator\PropertyCasters\CastToUuid;
use Ramsey\Uuid\UuidInterface;
class ExampleCommand
{
public function __construct(
#[CastToUuid]
public readonly UuidInterface $id,
) {}
}
$command = $mapper->hydrateObject(
ExampleCommand::class,
[
'id' => '9f960d77-7c9b-4bfd-9fc4-62d141efc7e5',
],
);
Using multiple casters per property
Create rich compositions of casting by using multiple casters.
use EventSauce\ObjectHydrator\PropertyCasters\CastToArrayWithKey;
use EventSauce\ObjectHydrator\PropertyCasters\CastToType;
use EventSauce\ObjectHydrator\MapFrom;
use Ramsey\Uuid\UuidInterface;
class ExampleCommand
{
public function __construct(
#[CastToType('string')]
#[CastToArrayWithKey('nested')]
#[MapFrom('number')]
public readonly array $stringNumbers,
) {}
}
$command = $mapper->hydrateObject(
ExampleCommand::class,
[
'number' => [1234],
],
);
$command->stringNumbers === ['nested' => [1234]];
Creating your own property casters
You can create your own property caster to handle complex cases that cannot follow the default conventions. Common cases for casters are union types or [intersection](https://wiki.php
