Skip to content

Instantly share code, notes, and snippets.

@bpolaszek
Last active June 23, 2026 13:32
Show Gist options
  • Select an option

  • Save bpolaszek/d72d74528867cf6a91a059223a6a4354 to your computer and use it in GitHub Desktop.

Select an option

Save bpolaszek/d72d74528867cf6a91a059223a6a4354 to your computer and use it in GitHub Desktop.
Object Hydration for test purposes
<?php
/**
* Hydrates an object — or instantiates a class and hydrates it — from an associative array.
*
* When a class-string is given, promoted constructor properties present in $props are
* forwarded to the constructor (the only way to set readonly properties); the remaining
* props are assigned after instantiation.
*
* @template T of object
*
* @param class-string<T>|T $object the instance to hydrate, or a class-string to instantiate
* @param array<string, mixed> $props property name => value
*
* @return T
*
* @throws LogicException if the class is unknown/not instantiable, or a prop is unknown,
* static, non-promoted readonly, or a non-writable virtual property
* @throws TypeError if a value is incompatible with the property type
* @throws ReflectionException
*/
function hydrate(object|string $object, array $props): object
{
if (is_string($object) && !class_exists($object)) {
throw new LogicException(sprintf('Class %s does not exist', $object));
}
$refl = new ReflectionClass($object);
if (is_string($object)) {
if (!$refl->isInstantiable()) {
throw new LogicException(sprintf('Class %s is not instantiable (abstract, interface or enum)', $refl->getName()));
}
// Route promoted constructor params through the constructor (the only way to set readonly props).
$constructorArgs = [];
foreach ($refl->getConstructor()?->getParameters() ?? [] as $param) {
if ($param->isPromoted() && array_key_exists($param->getName(), $props)) {
$constructorArgs[$param->getName()] = $props[$param->getName()];
unset($props[$param->getName()]); // don't re-hydrate it afterwards
}
}
$object = $refl->newInstance(...$constructorArgs); // named arguments spread
}
// Hydrate the remaining props on the (now guaranteed) instance.
foreach ($props as $key => $value) {
if (!$refl->hasProperty($key)) {
throw new LogicException(sprintf('Property %s does not exist on %s', $key, $refl->getName()));
}
$prop = $refl->getProperty($key);
if ($prop->isStatic()) {
throw new LogicException(sprintf('Property %s is static on %s and cannot be hydrated', $key, $refl->getName()));
}
if ($prop->isReadOnly()) {
throw new LogicException(sprintf('Property %s is read-only on %s and can only be set via the constructor', $key, $refl->getName()));
}
// A virtual property (PHP 8.4 hooks) with no set hook has no backing store to write to.
if ($prop->isVirtual() && !$prop->hasHook(PropertyHookType::Set)) {
throw new LogicException(sprintf('Property %s on %s is a virtual property without a set hook and cannot be hydrated', $key, $refl->getName()));
}
$prop->setValue($object, $value);
}
return $object;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment