Symfony PHP中区分null与undefined:GraphQL更新字段异常处理
问题描述
现有Street实体,字段信息如下:
- id: 1
- name: 街道名称
- 门牌号: 1
- 门牌号后缀(可为空): 'A'
对应的GraphQL mutation如下:
mutation updateStreet ($input: UpdateStreet!) { updateStreet(input: $input) { id name houseNumber houseNumberAddition } }
输入参数为:
{ "input": { "id": 1, "name": "修改后的名称", } }
问题:未传入的可为空字段houseNumberAddition被自动设为null,无法区分用户是主动传入null还是根本没传该字段,需要在Symfony PHP中实现这个区分逻辑。当前使用的Input类代码如下:
#[GQL\Input('UpdatePartner')] final class UpdatePartner { #[GQL\Field(type: 'Guid!')] #[Assert\NotBlank(message: 'Partner guid is required')] #[Assert\Uuid(message: 'Partner guid is not a valid UUID')] public Uuid $guid; #[GQL\Field(type: 'String')] public ?string $street; #[GQL\Field(type: 'String')] #[ Assert\Length( max: 255, maxMessage: 'House number exceeds limit of 255 characters' ) ] public ?string $houseNumber; #[GQL\Field(type: 'String')] #[ Assert\Length( max: 255, maxMessage: 'House number addition exceeds limit of 255 characters' ) ] public ?string $houseNumberAddition; public function guid(): Guid { return Guid::fromString($this->guid->toRfc4122()); } }
解决方案
PHP本身无法直接区分“未赋值的null”和“主动赋值的null”,这里提供两种可行的实现方式:
方式一:自定义Optional类(推荐)
通过一个包裹类来标记字段的传入状态,逻辑清晰且不依赖底层赋值机制。
1. 编写Optional类
final class Optional { private function __construct( private readonly mixed $value, private readonly bool $isPresent ) {} // 标记字段被主动传入(包括传入null) public static function present(mixed $value): self { return new self($value, true); } // 标记字段未被传入 public static function absent(): self { return new self(null, false); } // 获取字段值,未传入时可指定默认值 public function get(mixed $default = null): mixed { return $this->isPresent ? $this->value : $default; } // 判断字段是否被主动传入 public function isPresent(): bool { return $this->isPresent; } // 判断字段是否被主动传入且值不为null public function isNotEmpty(): bool { return $this->isPresent && $this->value !== null; } }
2. 修改Input类
将原来的可为空字段类型替换为Optional,并在构造方法中初始化所有可选字段为“未传入”状态:
#[GQL\Input('UpdatePartner')] final class UpdatePartner { #[GQL\Field(type: 'Guid!')] #[Assert\NotBlank(message: 'Partner guid is required')] #[Assert\Uuid(message: 'Partner guid is not a valid UUID')] public Uuid $guid; #[GQL\Field(type: 'String')] public Optional $street; #[GQL\Field(type: 'String')] #[ Assert\Length( max: 255, maxMessage: '门牌号长度不能超过255字符' ) ] public Optional $houseNumber; #[GQL\Field(type: 'String')] #[ Assert\Length( max: 255, maxMessage: '门牌号后缀长度不能超过255字符' ) ] public Optional $houseNumberAddition; public function __construct() { $this->street = Optional::absent(); $this->houseNumber = Optional::absent(); $this->houseNumberAddition = Optional::absent(); } public function guid(): Guid { return Guid::fromString($this->guid->toRfc4122()); } }
3. 在Resolver中处理逻辑
通过isPresent()方法判断字段状态,区分两种场景:
public function updatePartner(UpdatePartner $input): Partner { $partner = $this->partnerRepository->findOneBy(['guid' => $input->guid()]); // 只有用户主动传入时才更新字段(包括传入null) if ($input->houseNumberAddition->isPresent()) { $partner->setHouseNumberAddition($input->houseNumberAddition->get()); } else { // 用户未传入该字段,保持实体原有值不变 } // 其他字段同理处理... $this->entityManager->persist($partner); $this->entityManager->flush(); return $partner; }
方式二:追踪属性赋值状态
如果不想引入额外类,可以通过记录被赋值的字段来判断,但这种方式依赖GraphQL库的赋值方式(需要库使用__set方法赋值)。
修改Input类
#[GQL\Input('UpdatePartner')] final class UpdatePartner { #[GQL\Field(type: 'Guid!')] #[Assert\NotBlank(message: 'Partner guid is required')] #[Assert\Uuid(message: 'Partner guid is not a valid UUID')] public Uuid $guid; #[GQL\Field(type: 'String')] public ?string $street = null; #[GQL\Field(type: 'String')] #[ Assert\Length( max: 255, maxMessage: '门牌号长度不能超过255字符' ) ] public ?string $houseNumber = null; #[GQL\Field(type: 'String')] #[ Assert\Length( max: 255, maxMessage: '门牌号后缀长度不能超过255字符' ) ] public ?string $houseNumberAddition = null; private array $setFields = []; // 重写__set,记录被赋值的字段 public function __set(string $name, mixed $value): void { $this->$name = $value; $this->setFields[] = $name; } // 判断字段是否被主动赋值 public function hasBeenSet(string $name): bool { return in_array($name, $this->setFields, true); } public function guid(): Guid { return Guid::fromString($this->guid->toRfc4122()); } }
在Resolver中使用
public function updatePartner(UpdatePartner $input): Partner { $partner = $this->partnerRepository->findOneBy(['guid' => $input->guid()]); if ($input->hasBeenSet('houseNumberAddition')) { // 用户主动传入了该字段(不管值是null还是其他) $partner->setHouseNumberAddition($input->houseNumberAddition); } else { // 未传入,保留原有值 } // 其他字段处理... $this->entityManager->persist($partner); $this->entityManager->flush(); return $partner; }
注意:如果你的GraphQL库直接给public属性赋值(不经过__set),这种方式会失效,建议优先使用第一种方式。
内容的提问来源于stack exchange,提问作者Marinescu
相关产品推荐
相关产品推荐

