"Typed property must not be accessed before initialization" error in PHP 7.4+

Viewed 13167

I'm using PHP 7.4 and property type hints.

Let's say I have a class A, with a couple of private properties. When I use \SoapClient, Doctrine ORM, or any tool that instantiates a class bypassing the constructor and getting/setting properties directly using reflection, I face an error PHP Fatal error: Uncaught Error: Typed property A::$id must not be accessed before initialization in.

<?php

declare(strict_types=1);

class A
{
    private int $id;
    private string $name;

    public function __construct(int $id, string $name)
    {
        $this->id   = $id;
        $this->name = $name;
    }

    public function getId(): int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

$a = (new \ReflectionClass(A::class))->newInstanceWithoutConstructor();

var_dump($a->getId()); // Fatal error: Uncaught Error: Typed property A::$id must not be accessed before initialization in ...

I can mitigate this problem by declaring properties as nullable and setting a null value by default.

<?php

declare(strict_types=1);

class A
{
    private ?int $id      = null;
    private ?string $name = null;

    public function __construct(?int $id, ?string $name)
    {
        $this->id   = $id;
        $this->name = $name;
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): ?string
    {
        return $this->name;
    }
}

$a = (new \ReflectionClass(A::class))->newInstanceWithoutConstructor();

var_dump($a->getId()); // NULL
var_dump($a->getName()); // NULL

However, I don't like this workaround. The point of my class is to be domain compliant and encapsulate domain constraints within the class design. In this case, the property name shouldn't be nullable. Potentially I can declare the property name as an empty string, but it doesn't seem like a clean solution as well.

<?php

declare(strict_types=1);

class A
{
    private ?int $id     = null;
    private string $name = '';

    public function __construct(?int $id, string $name)
    {
        $this->id   = $id;
        $this->name = $name;
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getName(): string
    {
        return $this->name;
    }
}

$a = (new \ReflectionClass(A::class))->newInstanceWithoutConstructor();

var_dump($a->getId()); // NULL
var_dump($a->getName()); // ''

$idProperty = new \ReflectionProperty($a, 'id');
$idProperty->setAccessible(true);
if (null === $idProperty->getValue($a)) {
    $idProperty->setValue($a, 1001);
}

$nameProperty = new \ReflectionProperty($a, 'name');
$nameProperty->setAccessible(true);
if ('' === $nameProperty->getValue($a)) {
    $nameProperty->setValue($a, 'Name');
}

var_dump($a->getId()); // 1001
var_dump($a->getName()); // Name

My question is: is there a way to keep a proper class design and avoid facing the Typed property must not be accessed before initialization error? If no, what is the preferred approach to tackle this problem? (e.g. define all properties as nullable nulls or string properties as an empty string, etc.)

2 Answers

I think there's a misunderstanding here. Uninitialized typed properties have no state, which means they have no initial NULL. If you want a property to be NULL you have to specify it explicitly.

private ?string $name = NULL;

So your attempt to avoid this exception without setting these properties is just not right and makes no sense!. The purpose of typed properties is to avoid implicit initialization and always provide an explicit value that is clear and makes sense. And please don't just define all properties as nullable to make this exception disappear!! as this will defeat the whole point of typed properties and PHP 7.4 .

Let's simplify things and clarify how Reflection based ORM's such as Doctrine work. In particular, Doctrine only uses reflection when loading an object from the database. So id will always be set. Consider:

class Entity
{
    public int $id;

}
$entity = (new \ReflectionClass(Entity::class))->newInstanceWithoutConstructor();

$idProperty = new \ReflectionProperty($entity, 'id');
$idProperty->setValue($entity, 1001);

echo 'ID ' . $entity->id . "\n";

The above example works without the error message. You won't be using reflection to create a class without a constructor unless you plan on setting all the properties as well.

At this point, the only question is what you want a 'properly designed' class to do with:

$entity = new Entity();
echo 'ID ' . $entity->id . "\n";

If it is considered to be okay for id to be null before the object is persisted then you go the ?int route. If not then maybe consider using a guid instead of a sequence.

Related