Типизированное свойство не должно быть доступно до ошибки инициализации в PHP 7.4+

Я использую PHP 7.4 и подсказки типа свойства.

Скажем, у меня есть класс А с парой частных владений. Когда я использую \SoapClient, Doctrine ORM или любой другой инструмент, который создает экземпляр класса в обход конструктора и получает/устанавливает свойства напрямую с помощью отражения, я сталкиваюсь с ошибкой 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 ...

Я могу смягчить эту проблему, объявив свойства как обнуляемые и установив нулевое значение по умолчанию.

<?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

Однако мне не нравится этот обходной путь. Смысл моего класса в том, чтобы быть совместимым с предметной областью и инкапсулировать ограничения предметной области в структуру класса. В этом случае свойство name не должно принимать значение NULL. Потенциально я могу объявить свойство name пустой строкой, но это тоже не похоже на чистое решение.

<?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

Мой вопрос: есть ли способ сохранить правильный дизайн класса и избежать ошибки Typed property must not be accessed before initialization? Если нет, то какой предпочтительный подход к решению этой проблемы? (например, определить все свойства как пустые значения NULL или строковые свойства как пустую строку и т. д.)


person Mikhail Prosalov    schedule 28.12.2020    source источник
comment
Вы получаете к нему доступ до того, как он будет объявлен. Итак, самое простое — объявить со значением по умолчанию, например private int $id = 0;. Если вы этого не сделаете, начальное значение равно NULL.   -  person Markus Zeller    schedule 28.12.2020
comment
Но это сделало бы состояние класса недействительным с точки зрения домена, поскольку 0 не является правильным идентификатором. Выглядит очень халтурно.   -  person Mikhail Prosalov    schedule 28.12.2020
comment
Я думаю, что иметь int, который может быть NULL, гораздо более хакерский и нарушает безопасность типов, даже это возможно, на мой вкус.   -  person Markus Zeller    schedule 28.12.2020
comment
Да, это очень далеко от идеала. Мне не нравится делать его либо нулевым, либо 0 по умолчанию. Я надеюсь, что есть чистое решение проблемы.   -  person Mikhail Prosalov    schedule 28.12.2020
comment
Когда вы обращаетесь к getId(): int, вы ожидаете int, но когда это NULL, конечно, это неправильно. Итак, вам нужно вернуть int и проверить себя, как return (int)$this->id;. Затем NULL будет преобразован в 0. Или измените подпись на getId(): ?int.   -  person Markus Zeller    schedule 28.12.2020
comment
$id по умолчанию уже имеет значение null. Просто когда вы объявляете, что getId() возвращает целое число, php 4.4 поверит вам на слово и скажет: упс, null не является целым числом. Я думаю, что вы связываете себя узлами, пытаясь использовать getId() для объекта, который вы считаете недопустимым. Кстати, здесь виновато не отражение. Он использует getId().   -  person Cerad    schedule 28.12.2020
comment
Если я использую \ReflectionProperty вместо геттеров для получения значения, это вызывает ту же ошибку.   -  person Mikhail Prosalov    schedule 28.12.2020


Ответы (2)


Я думаю, что здесь есть недоразумение. Неинициализированные типизированные свойства не имеют состояния, что означает, что у них нет начального NULL. Если вы хотите, чтобы свойство было NULL, вы должны указать его явно.

private ?string $name = NULL;

Таким образом, ваша попытка избежать этого исключения без установки этих свойств просто неверна и не имеет смысла!. Цель типизированных свойств — избежать неявной инициализации и всегда предоставлять явное значение, которое понятно и имеет смысл. И, пожалуйста, не определяйте все свойства как обнуляемые, чтобы это исключение исчезло!! так как это уничтожит весь смысл типизированных свойств и PHP 7.4.

person Rain    schedule 29.12.2020
comment
Спасибо за Ваш ответ. Было бы лучше определить свойство int как ноль, а свойство string как пустую строку? Нуль действительно кажется немного странным, так как мой класс не должен принимать нуль для идентификатора или имени. - person Mikhail Prosalov; 29.12.2020
comment
@MikhailProsalov Да, определенно лучше инициализировать свойства значениями того же объявленного типа. И целью разработки PHP-команды было заставить разработчиков всегда обеспечивать явную инициализацию. - person Rain; 29.12.2020
comment
Но что нам делать с полями отношений, допускающими значение NULL? Если мы установим значение null в качестве значения по умолчанию, то ленивая загрузка не будет работать, потому что доктрина по странным причинам не заполняет ее прокси-сущностным объектом. - person keeborg; 09.07.2021

Давайте упростим и проясним, как работают ORM на основе Reflection, такие как Doctrine. В частности, Doctrine использует отражение только при загрузке объекта из базы данных. Таким образом, идентификатор всегда будет установлен. Рассмотреть возможность:

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";

Приведенный выше пример работает без сообщения об ошибке. Вы не будете использовать отражение для создания класса без конструктора, если только не планируете также устанавливать все свойства.

На данный момент единственный вопрос заключается в том, что вы хотите, чтобы «правильно спроектированный» класс делал:

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

Если считается, что id может иметь значение null до того, как объект будет сохранен, вы идете по маршруту ?int. Если нет, то, возможно, рассмотрите возможность использования guid вместо последовательности.

person Cerad    schedule 28.12.2020