Watch out for this when upgrading to 7.4

Viewing 6 posts - 1 through 6 (of 6 total)
  • Author
    Posts
  • #1481
    tehnologie
    Participant

    Hello everyone,

    I want to share a bugfix I had to perform as I updated my code to PHP `7.4` features. There’s a place in the code where I do hydration: I populate an instance of a class with data from a similarly-structured `stdClass` object returned by an API (one level deep only).

    This is the code that bugged out once I added types to the class:

    function hydrate(string $className, stdClass $remote)
    {
    $object = new $className();
    foreach ($object as $field => $_) {
    $object->$field = $remote->$field;
    }
    return $object;
    }

    The loop works fine in valid `7.3` code, but once you add types to fields, the loop doesn’t loop at all.

    PHP.Watch documents the cause, it has to do with typed fields being in an “uninitialized” state by default, instead of being simply `null`:

    >Because this value is not the same as `null`, you cannot use `$foo->name === null` to check if the property is uninitialized. With PHP 7.4 typed properties, class properties have an uninitialized state. This simply means that the property is not initialized yet. This is not the same as `null`.

    >https://php.watch/versions/7.4/typed-properties

    Accessing an uninitialized variable produces the message `Typed property … must not be accessed before initialization …`, but you might be confused because according to your code that worked fine just a minute ago the property should have been initialized.

    What I did to fix it was to loop on `get_class_vars($className)`, instead of on the instance:

    foreach (get_class_vars($className) as $field => $_) {
    $object->$field = $remote->$field;
    }

    Maybe this helps someone who’s upgrading to `7.4` in anticipation of the upcoming `8.0` release.

    #1482
    jojoxy
    Guest

    Defining your object properties as nullable with a default value of `null` should work in your case, for example:

    `public ?int $myProperty = null;`

    #1483
    Flibbertygibbety22
    Guest

    Thanks for sharing.

    As a side-note, I have a bit of code that does something similar but in a completely different way. I had to do this on 1000s of objects, and performance was critical. Accessing and writing dynamic properties is relatively slow, so I looked for a different approach to the one you mentioned.

    I ended up in copying an approach used by Doctrine ORM: serialising the object to a string, then in that string replacing the original class name with the new one, and finally unserialising from the new string.

    function castToDifferentClass(object $original, string $newClass): object
    {
    $originalClass = get_class($original);
    return unserialize(str_replace(sprintf(‘%d:”%s”:’, strlen($originalClass), $originalClass), sprintf(‘%d:”%s”:’, strlen($newClass), $newClass), serialize($original)));
    }

    It’s quite a hack that has other downsides – it ignores the constructor on the new class, and there’s a potential security issue with nested objects. So you might still default to the simpler approach you originally mentioned, but I thought I’d leave this here in case anyone stumbles on it.

    #1484
    Bogdanuu
    Guest

    That’s the expected behavior. You are accessing an uninitialized property by iterating with that foreach with that $_ variable.

    That hydrate function should work with 7.4 as well, but you changed your class to use a new feature that was not properly handled in your implementation.

    #1485
    SerdanKK
    Guest

    You could also just, like… I’unno… initialize your fields?

    #1486
    djxfade
    Guest

    /r/LolPHP

Viewing 6 posts - 1 through 6 (of 6 total)
  • You must be logged in to reply to this topic.