You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 19:20:47