[RFC] ReflectionHydrator should recognize Property Hooks

Good morning,

During my free time between the holidays, I worked a little with Laminas Hydrator under PHP 8.5. I noticed that the ReflectionHydrator class does not take property hooks into account.

Property hooks can be used since PHP 8.4 and offer a high-performance way to convert passed values into expected values.

Basic Example

class Developer 
{
    public string $name;
    public DateTimeImmutable $birthday {
        set (string|DateTimeImmutable $value) {
            if (is_string($value)) {
                $value = new DateTimeImmutable($value);
            }

            $this->birthday = $value;
        }
    };
}

In this example a property hook for setting the value for the property $birthday is used. It takes a string value or an instance of DateTimeImmutable. If the value is a string it will instantiate a new instance of DateTimeImmutable.

The Problem

The current version of the ReflectionHydrator does not recognize property hooks. It sets values via the Reflection API. The call of ReflectionProperty::setValue() does not trigger the expected property hook. This behaviour is pretty logic, because Reflection is a simple API, that sets the value directly on the property without recognizing any other related functionality. On the other hand setting a value without calling ReflectionProperty::setValue() triggers the property hook.

// calls the property hook
$developer = new Developer();
$developer->birthday = '1979-12-19';

// does not call the property hook
$reflector = new ReflectionProperty($developer, 'birthday');
$reflector->setValue($developer, '1979-12-19');

A possible solution

The Reflaction API can check a property for having property hooks. So you can check, if there is a set property hook and if the property itself is public. Public visibility is necessary, because only values for public properties can be set by assignment. ReflectionHydrator::hydrate() can be changed like …

public function hydrate(array $data, object $object)
{
    $reflProperties = self::getReflProperties($object);
    foreach ($data as $key => $value) {
        $name = $this->hydrateName($key, $data);
        if (isset($reflProperties[$name])) {
            if ($reflProperties[$name]->isPublic() === true && $reflProperties[$name]->hasHook(\PropertyHookType::Set)) {
                $object->$name = $this->hydrateValue($name, $value, $data);
            } else {
                $reflProperties[$name]->setValue($object, $this->hydrateValue($name, $value, $data));
            }
        }
    }
    return $object;
}

Property Hooks should be preferred, because they are more performant than hydrator strategies. Hydrator strategies have to be instantiated while property hooks are already there and can be called automatically.

Benefits

If ReflectionHydrator took property hooks into account, a performance advantage could be achieved, as strategies for BackedEnum or DateTime instances could be replaced by property hooks in the future, which can be executed much faster because no additional instances need to be created for strategies.

Request for comments

I haven’t tested my own approach sufficiently yet. Do you see any possible side effects? Am I perhaps overlooking something? What do you think?

1 Like

Good idea! :+1:

The method ReflectionProperty::hasHook needs PHP with version 8.4 and therefore it must be checked if this method exists. But I see no problem, so it can be added with a minor version.

Related Issue Report