Last active
June 23, 2026 13:32
-
-
Save bpolaszek/d72d74528867cf6a91a059223a6a4354 to your computer and use it in GitHub Desktop.
Object Hydration for test purposes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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