PHP未初始化对象属性



我似乎对PHP的Typed属性和uninitialized状态有点误解。

假设一个类似REST的服务获得以下JSON对象:

{
    "firstName": "some name",
    "lastName": "some last name",
    "groupId": 0,
    "dateOfBirth": "2000-01-01"
}

我更喜欢DTO看起来像这样:

class Person {
    private string $firstName;
    private string $lastName;
    private int $groupId;
    private DateTime $dateOfBirth;
    // All the getters/setters, cannot have __construct due to serializer limitation
}

但是,由于这些消息属性中的任何一个都可能被省略(错误地,不是有效的情况(,我的反序列化将使一些字段处于unintialized状态。

所以,这太糟糕了。我想我有几个选择:

  1. 声明所有这些都是null可执行的,并初始化为null(当然(
  2. 将标量属性初始化为各自的默认值(0、"等(,并将对象属性初始化为null(声明它们为null(

假设我选择了选项#2:

class Person {
    private string $firstName = '';
    private string $lastName = '';
    private int $groupId = 0;
    private ?DateTime $dateOfBirth = null;
    // The rest
}

在代码的另一部分,我有这样的东西:

function doSomethingWithDate(DateTime $dateTime): string{
    return ...; // does not really matter
}
...
doSomethingWithDate($person->getDateOfBirth());
...

我的IDE发出警告:

Expected parameter of type 'DateTime', 'DateTime|null' provided 

很明显,getter为什么说";嘿,我可以为null";,但是该方法说";不,不"除此之外。

但是,我该怎么做呢;说服";这是一个有效的场景吗?

我应该有一套单独的DTO吗?一套用于不安全状态,另一套用于保险箱?似乎不太可能。。。

你会如何处理";问题";?

更新

尽管我的问题听起来很模糊和奇怪(我知道确实是:D(,但我想详细说明一下。

  • 我的2个内部系统通过内部Redis流进行通信
  • 一个是遗留代码(基于php56(,另一个是基于php81
  • 由于沟通是内部的,我完全忽略了验证方面
  • 在测试过程中,遗留系统错误地发送了一个格式错误的对象(不包含任何属性(
  • 另一边的反序列化程序完成了任务,但得到了完全未初始化的对象,我的Redis消费者开始在试图处理它时旋转
  • 在达到failed尝试次数后,它试图将其推送到failed队列,但由于未初始化属性的序列化,它无法
  • 我的Redis消费者得到了"卡住";就在这条信息上

(此答案特定于Symfony Serializer组件和PHP 8.1+。(

确保传入数据符合特定的合同当然是一件好事。我也喜欢我的属性类型尽可能严格,我也讨厌PhpStorm对我大喊大叫

问题

想象一下,我们会有这样的DTO:

class Dto1 {
private string $foo;
public function setFoo(string $foo): void { $this->foo = $foo; }
public function getFoo(): string { return $this->foo; }
}

你可能会这样反序列化它:

$dto = $serializer->deserialize($json, Dto1::class, 'json', [
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
]);

(将ALLOW_EXTRA_ATTRIBUTES设置为false可确保遗留系统不会偷偷进入额外属性(

正如您所注意到的,当JSON缺少$foo属性时,我们现在将遇到一个问题:

$json = '{}';
$dto = $serializer->deserialize($json, Dto1::class, 'json', [
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
]);
var_dump($dto->foo); // Oops! Uninitialized property access

不幸的是,似乎没有办法让Symfony序列化程序在反序列化后检查未初始化的属性。这给我们留下了解决这个问题的另外两个选择。

解决方案1

确保在反序列化后初始化所有属性。这可能需要编写一个看起来有点像这样的函数:

function ensureInitialized(object $o): void {
// There are probably more robust ways to do this, 
// this is just an example.
$reflectionClass = new ReflectionClass($o);
foreach ($reflectionClass->getProperties() as $reflectionProperty) {
if (!$reflectionProperty->isInitialized($o)) {
throw new RuntimeException('Uninitialized properties!');
}
}
}

我们可以使用这个函数来确保反序列化的DTO是有效的:

$json = '{}';
$dto = $serializer->deserialize($json, Dto1::class, 'json', [
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
]);
ensureInitialized($dto); // <-- throws exception

然而,我宁愿避免检查每个反序列化的DTO。我更喜欢下一个解决方案。

解决方案2

由于您提到您使用的是PHP8.1,我们可以为DTO使用构造函数属性提升和只读属性。

class Dto2 {
public function __construct(
public readonly string $foo,
) {}
}

我们仍然可以像正常情况一样反序列化JSON:

$json = '{"foo": "bar"}';
$dto = $serializer->deserialize($json, Dto2::class, 'json', [
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
]);
var_dump($dto->foo); // string(3) "bar"

但如果遗留系统再次试图欺骗我们:

$json = '{}';
$dto = $serializer->deserialize($json, Dto2::class, 'json', [
AbstractNormalizer::ALLOW_EXTRA_ATTRIBUTES => false,
]);
// ^ Will throw: Uncaught SymfonyComponentSerializerExceptionMissingConstructorArgumentsException: Cannot create an instance of "B" from serialized data because its constructor requires parameter "foo" to be present

现在,您可以简单地捕获此异常,并根据需要返回一个4XX错误。

最新更新