Over time the shape of your stored data drifts away from your classes: fields get renamed, split or merged. Upcasting reshapes the stored array on the fly while it is hydrated, so old payloads keep loading into your current classes without a migration of the underlying storage.
Register the UpcastExtension on the builder and pass it a list of upcasters.
Each upcaster receives the raw data array and returns a reshaped array.
use Patchlevel\Hydrator\CoreExtension;
use Patchlevel\Hydrator\Extension\Upcast\CallbackUpcaster;
use Patchlevel\Hydrator\Extension\Upcast\UpcastExtension;
use Patchlevel\Hydrator\StackHydratorBuilder;
$hydrator = (new StackHydratorBuilder())
->useExtension(new CoreExtension())
->useExtension(new UpcastExtension(
beforeTransform: [
CallbackUpcaster::forClass(
ProfileCreated::class,
static function (array $data): array {
$data['name'] = $data['firstName'] . ' ' . $data['lastName'];
unset($data['firstName'], $data['lastName']);
return $data;
},
),
],
))
->build();Upcasting only runs during hydration. Extraction always writes the current shape, so once an object has been re-extracted its stored payload is up to date.
An upcaster implements the Upcaster interface. It receives the
class metadata, the data array and the context, and returns the
reshaped data. Mark it with the #[UpcasterFor] attribute to restrict it to a
single class, so you do not have to check the metadata yourself:
use Patchlevel\Hydrator\Extension\Upcast\Attribute\UpcasterFor;
use Patchlevel\Hydrator\Extension\Upcast\Upcaster;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
#[UpcasterFor(ProfileCreated::class)]
final class RenameEmailUpcaster implements Upcaster
{
public function upcast(ClassMetadata $metadata, array $data, array $context): array
{
$data['email'] = $data['mail'];
unset($data['mail']);
return $data;
}
}#[UpcasterFor] is more than a convenience: the UpcastMiddleware reads it to
know it can be skipped entirely for
classes it does not target. Without it, the upcaster is called for every
class, so if you do check the metadata yourself, the middleware has no way of
knowing and always calls it.
For the common case of a single class and a closure, use the
CallbackUpcaster. It compares the class name for you and only invokes the
callback for a match, and the UpcastMiddleware recognizes it the same way it
recognizes #[UpcasterFor]. The callback receives the data and the context:
use Patchlevel\Hydrator\Extension\Upcast\CallbackUpcaster;
$upcaster = CallbackUpcaster::forClass(
ProfileCreated::class,
static function (array $data, array $context): array {
$data['email'] = $data['mail'];
unset($data['mail']);
return $data;
},
);The hydrator decodes the stored payload in stages: first it is read as raw
values, then normalizers decode each field, and finally the
object is built. The UpcastExtension can hook into two of these stages, and
you pass your upcasters to the matching argument.
| Argument | Runs | Works on |
|---|---|---|
beforeEncoding |
before the values are decoded | the raw stored values (strings, ints, ...) |
beforeTransform |
after decoding, right before the object is built | the decoded values (enums, dates, value objects, ...) |
Use beforeEncoding when you rename or restructure fields whose raw form is
enough, and beforeTransform when you need the already decoded values.
use Patchlevel\Hydrator\Extension\Upcast\UpcastExtension;
$extension = new UpcastExtension(
beforeEncoding: [$renameFieldUpcaster],
beforeTransform: [$mergeNameUpcaster],
);The cryptography extension decrypts values during the
decoding stage. A beforeEncoding upcaster therefore still sees the encrypted
values, while a beforeTransform upcaster sees the decrypted ones. Pick the
stage that matches the data you need.