The StackHydrator is assembled from small building blocks: middlewares that
wrap the hydration process, guessers that resolve normalizers and
metadata enrichers that add information to the class metadata. An extension
bundles such building blocks so they can be registered with a single call.
Extensions are registered on the StackHydratorBuilder with useExtension.
The CoreExtension provides the default behaviour and should (almost) always
be there.
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Extension\Lifecycle\LifecycleExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;
$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new LifecycleExtension())
->build();The library ships with five extensions out of the box:
| Extension | Purpose |
|---|---|
CoreExtension |
The default behaviour, the TransformMiddleware and the BuiltInGuesser. |
LifecycleExtension |
Lifecycle hooks, run code before and after the extract and hydrate process. |
CryptographyExtension |
Cryptography, encrypt and decrypt sensitive data with crypto-shredding. |
UpcastExtension |
Upcasting, reshape outdated stored data while it is hydrated. |
GroupsExtension |
Groups, extract and hydrate only the properties of selected groups. |
A middleware wraps the hydration and extraction process, similar to HTTP
middlewares. It can modify the incoming data, the outgoing array or the object
itself, and then delegates to the next middleware on the stack. The innermost
middleware is the TransformMiddleware, which does the actual property mapping.
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Middleware\Middleware;
use Patchlevel\Hydrator\Middleware\Stack;
final class RemoveNullValuesMiddleware implements Middleware
{
public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object
{
return $stack->next()->hydrate($metadata, $data, $context, $stack);
}
public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array
{
$data = $stack->next()->extract($metadata, $object, $context, $stack);
return array_filter($data, static fn (mixed $value) => $value !== null);
}
}Middlewares are added with a priority, higher priorities run first (outermost).
The TransformMiddleware from the CoreExtension has priority -64, so it
always runs last.
$builder->addMiddleware(new RemoveNullValuesMiddleware(), 0);A middleware usually only has something to do for a few classes. Instead of
walking through the whole stack every time, a middleware can implement
SkippableMiddleware and tell the hydrator that it is not needed for a class.
The decision is made once per class and then reused, so it must only depend on
the metadata.
The skip method returns a Skip case: Skip::None to always run,
Skip::Hydrate or Skip::Extract to be left out in one direction only and
Skip::Both to be left out completely.
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Middleware\Skip;
use Patchlevel\Hydrator\Middleware\SkippableMiddleware;
use Patchlevel\Hydrator\Middleware\Stack;
final class RemoveNullValuesMiddleware implements SkippableMiddleware
{
public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object
{
return $stack->next()->hydrate($metadata, $data, $context, $stack);
}
public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array
{
$data = $stack->next()->extract($metadata, $object, $context, $stack);
return array_filter($data, static fn (mixed $value) => $value !== null);
}
public function skip(ClassMetadata $metadata): Skip
{
// the middleware only does something while extracting
return Skip::Hydrate;
}
}The built-in middlewares use this as well: the CryptographyMiddleware is left
out for classes without sensitive data, the LifecycleMiddleware only runs in
the directions the class has hooks for, and the UpcastMiddleware is left out
while extracting, since upcasting only ever happens while hydrating. If every
upcaster is a CallbackUpcaster or carries an
#[UpcasterFor] attribute, it is also left
out while hydrating classes none of them target.
At least one middleware has to run. If every middleware skips a class, an
AllMiddlewaresSkipped exception is thrown.
A metadata enricher runs once per class when the metadata is created. It can
inspect the class and attach extra information to ClassMetadata::$extras,
which a middleware can later read. This keeps expensive reflection out of the
hot path.
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Metadata\MetadataEnricher;
final class AuditMetadataEnricher implements MetadataEnricher
{
public function enrich(ClassMetadata $classMetadata): void
{
$attributes = $classMetadata->reflection->getAttributes(Audited::class);
if ($attributes === []) {
return;
}
$classMetadata->extras[Audited::class] = true;
}
}$builder->addMetadataEnricher(new AuditMetadataEnricher());Metadata enrichers also accept a priority. Since the metadata (including the
extras) can be cached, everything you store in extras must be
serializable.
An extension implements the Extension interface and configures the builder.
This is the way to package a middleware together with its metadata enricher.
use Patchlevel\Hydrator\Extension;
use Patchlevel\Hydrator\StackHydratorBuilder;
final class AuditExtension implements Extension
{
public function configure(StackHydratorBuilder $builder): void
{
$builder->addMetadataEnricher(new AuditMetadataEnricher());
$builder->addMiddleware(new AuditMiddleware());
}
}