我似乎对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
状态。
所以,这太糟糕了。我想我有几个选择:
- 声明所有这些都是
null
可执行的,并初始化为null
(当然( - 将标量属性初始化为各自的默认值(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错误。