Hydration
Hydration turns a database row into an entity, and extraction turns an entity back into the values that the database receives. Both steps run through one object, the entity hydrator, which the ORM holds. The casts are applied there, so every read and every write follows the same rules.
Most applications never replace the entity hydrator. Knowing the pipeline helps when a rule must apply to every entity, or when a value must leave the ORM in a different form.
The Pipeline
When a row is hydrated into an entity, the work happens in two layers:
- The entity hydrator checks each column of the entity. For every column it runs the casts of the property, such as
#[Cast]and#[CastNullable]. - The driver's hydrator writes the converted values into the object. The default driver hydrator is
SimpleHydrator.
Extraction runs the same layers in the other direction:
- The entity hydrator runs the casts of each column in the extract direction.
- A value that is not
nulland not a string is converted to a string, when theextractForceStringifyoption is enabled. It is enabled by default.
When the ORM hydrates a row, it first emits a HydrateEvent with the row data. A listener on the ORM can change the data before the entity hydrator receives it:
use Windwalker\Database\Event\HydrateEvent;
$orm->on(
HydrateEvent::class,
function (HydrateEvent $event) {
// Change $event->item before the entity is filled
}
);Reading the Entity Hydrator
getEntityHydrator() returns the hydrator in use. The first call creates the default entity hydrator, which wraps the driver hydrator:
$hydrator = $orm->getEntityHydrator();Replacing the Entity Hydrator
setEntityHydrator() replaces the hydrator for the whole ORM instance. Pass null to restore the default, which is created again on the next call.
A replacement must implement FieldHydratorInterface. The usual way is to decorate the default entity hydrator, so that the casts stay in place:
use Windwalker\Database\Hydrator\FieldHydratorInterface;
use Windwalker\ORM\Hydrator\EntityHydrator;
class TrimmingHydrator implements FieldHydratorInterface
{
public function __construct(
protected FieldHydratorInterface $inner,
) {
}
public function hydrate(array $data, object $object): object
{
foreach ($data as $key => $value) {
if (is_string($value)) {
$data[$key] = trim($value);
}
}
return $this->inner->hydrate($data, $object);
}
public function extract(object $object): array
{
return $this->inner->extract($object);
}
public function extractField(object $object, string $field): mixed
{
return $this->inner->extractField($object, $field);
}
}
$orm->setEntityHydrator(
new TrimmingHydrator(
new EntityHydrator($db->getHydrator(), $orm),
),
);WARNING
A replacement that does not pass the data to an EntityHydrator skips the casts of every entity. The entities then receive raw values with no error. Always keep an EntityHydrator inside the chain.
Keeping Types on Extract
The setting is the public property extractForceStringify of the Windwalker\ORM\Hydrator\EntityHydrator class. Its default is true, and nothing in the framework or in the application configuration changes it. The ORM has no configuration key for it, so it is changed in code.
By default, extract() converts the values to strings, so that the statement receives text. Turn the option off to keep the PHP types. The ORM creates its entity hydrator on the first call to getEntityHydrator(), so the usual way is to replace the hydrator before the entities are used:
use Windwalker\ORM\Hydrator\EntityHydrator;
$hydrator = new EntityHydrator($db->getHydrator(), $orm);
$hydrator->extractForceStringify = false;
$orm->setEntityHydrator($hydrator);The default instance can also be changed in place. Call getEntityHydrator() first, and set the property on the object it returns. The method is typed as FieldHydratorInterface, which has no such property, so this works only while the hydrator is the default EntityHydrator:
$orm->getEntityHydrator()->extractForceStringify = false;INFO
The database driver must accept the PHP types in its statements. Keep the default unless a driver or a column type needs the typed values.
Related Topics
- Database Hydrators: the driver hydrator that the entity hydrator wraps, and how to write one.
- Casting: the casts that run in this pipeline.