如何使用Symfony的序列化程序正确地填充DTO的ArrayCollection属性?

hl0ma9xz  于 2022-11-16  发布在  其他
关注(0)|答案(2)|浏览(138)

我有一个DTO类,如下所示:

class ParamsDto
{
    #[Assert\Type(ArrayCollection::class)]
    #[Assert\All([
        new Assert\Type('digit'),
        new Assert\Positive(),
    ])]
    private ?ArrayCollection $tagIds = null;

    public function getTagIds(): ?ArrayCollection
    {
        return $this->tagIds;
    }

    public function setTagIds(?ArrayCollection $tagIds): self
    {
        $this->tagIds = $tagIds;

        return $this;
    }
}

给定一个对URL(如https://domain.php?tag-ids[]=2)的请求,我希望将tag-ids请求参数解析为该DTO的tagIds属性。
第一步,我创建了一个名称转换器,这样我就可以在tag-idstagIds之间进行转换,所以我的序列化器示例化如下所示:

$nameConverter = new EducationEntrySearchParamNameConverter();
$serializer = new Serializer([
    new ArrayDenormalizer(),
    new ObjectNormalizer(null, $nameConverter, null, new ReflectionExtractor()),
], [new JsonEncoder()]);

$params = $serializer->denormalize($requestParams, ParamsDto::class);

其中$params显示为:

^ App\DataTransferObject\ParamsDto {#749
  -tagIds: Doctrine\Common\Collections\ArrayCollection {#753
    -elements: []
  }
}

因此,它已示例化,但为空。
很可能是因为我的请求中没有包含elements键。如果我做一些预处理,比如:

$requestParams = [];
foreach ($request->query->all() as $key => $value) {
    if (in_array($key, ['tag-ids'])) {
        $requestParams[$key] = ['elements' => $value];
    } else {
        $requestParams[$key] = $value;
    }
}
$params = $serializer->denormalize($requestParams, ParamsDto::class);

然后我得到正确的输出:

^ App\DataTransferObject\ParamsDto {#749
  -tagIds: Doctrine\Common\Collections\ArrayCollection {#757
    -elements: array:1 [
      0 => "2"
    ]
  }
}

如何才能让序列化程序将请求转换为DTO,而不必执行此预处理?
L.E:不需要使用自定义名称转换器,我现在使用SerializedName

yyhrrdl8

yyhrrdl81#

简短回答:我强烈建议用一个标准的php内置array来代替这个集合--至少在setter上是这样!--我打赌这会有帮助。
道长回答:
我的猜测是,由于您的DTO不是一个实体,序列化程序不会使用一些与理论相关的东西,这些东西会告诉它将数组转换为集合。
恕我直言,集合是对象内部的解决方案,不应该直接暴露给外部,与外部世界的接口是数组。可能非常固执己见。
从非原则序列化程序的Angular 来看,集合是一个对象。对象具有属性。要将数组Map到对象上,需要使用数组的键,并使用反射将它们与对象的属性相匹配:直接使用属性,如果它是公共的,则使用加法器/移除器,否则使用setter/getter。(按IIRC顺序)-这就是为什么将elements作为键添加到带有id数组的输入中“工作”的原因。
使用数组作为setter/getter对中的接口将极大地帮助property accessor只设置数组。如果你想要/需要的话,你仍然可以在内部将该属性作为ArrayCollection。
这只是一种猜测:在setter中添加docblock类型提示/** @param array<int> $tagIds */甚至可能触发从"2"2的转换。用php类型提示代替addTagId + removeTagId + getTagIds也可能起作用。

axr492tv

axr492tv2#

我会加上这一点,以帮助其他人挣扎与同样的问题,但我能够想出它后,@雅库米的答案,这也是为什么我要奖励他们的赏金。
因此,经过一些研究后,似乎确实最简单的方法是使用array作为类型提示。这样,事情就可以开箱即用,而不需要做任何其他事情,但您不会使用集合,因此:

#[Assert\Type('array')]
    #[Assert\All([
        new Assert\Type('numeric'),
        new Assert\Positive(),
    ])]
    #[SerializedName('tag-ids')]
    private ?array $tagIds = null;

    public function getTagIds(): ?array
    {
        return $this->tagIds;
    }

    public function setTagIds(?array $tagIds): self
    {
        $this->tagIds = $tagIds;

        return $this;
    }

但是,如果您希望继续使用集合,则需要做一些额外的工作:

#[Assert\Type(ArrayCollection::class)]
    #[Assert\All([
        new Assert\Type('numeric'),
        new Assert\Positive(),
    ])]
    #[SerializedName('tag-ids')]
    private ?ArrayCollection $tagIds = null;
    
    public function getTagIds(): ?ArrayCollection
    {
        return $this->tagIds;
    }

    public function setTagIds(array|ArrayCollection|null $tagIds): self
    {
        if (null !== $tagIds) {
            $tagIds = $tagIds instanceof ArrayCollection ? $tagIds : new ArrayCollection($tagIds);
        }
        $this->tagIds = $tagIds;

        return $this;
    }

    // Make sure our numeric values are treated as integers not strings
    public function addTagId(int $tagId): void
    {
        if (null === $this->getTagIds()) {
            $this->setTagIds(new ArrayCollection());
        }

        $this->getTagIds()?->add($tagId);
    }

    public function removeTagId(int $tagId): void
    {
        $this->getTagIds()?->remove($tagId);
    }

因此,除了setter和getter之外,还需要添加加法器和移除器。
这将反序列化为整数的ArrayCollection。
另外,如果您的对象属性是DTO的集合,而不仅仅是整数,并且您还希望对这些属性进行反规范化,则可以执行以下操作:

#[Assert\Type(TagCollection::class)]
    #[Assert\All([
        new Assert\Type(TagDto::class),
    ])]
    #[Assert\Valid]
    private ?TagCollection $tags = null;

    private PropertyAccessorInterface $propertyAccessor;

    public function __construct()
    {
        $this->propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
            ->disableExceptionOnInvalidIndex()
            ->disableExceptionOnInvalidPropertyPath()
            ->getPropertyAccessor()
        ;
    }

    public function getTags(): ?TagCollection
    {
        return $this->tags;
    }

    public function setTags(array|TagCollection|null $tags): self
    {
        if (null !== $tags) {
            $tags = $tags instanceof TagCollection ? $tags : new TagCollection($tags);
        }
        $this->tags = $tags;

        return $this;
    }

    public function addTag(array $tag): void
    {
        if (null === $this->getTags()) {
            $this->setTags(new TagCollection());
        }

        $tagDto = new TagDto();
        foreach ($tag as $key => $value) {
            $this->propertyAccessor->setValue($tagDto, $key, $value);
        }

        $this->getTags()?->add($tagDto);
    }

    public function removeTag(TagDto $tag): void
    {
        $this->getTags()?->remove($tag);
    }

这会将tags属性反规范化为ArrayCollection示例(TagCollection扩展了它),在这种情况下,集合中的每一项都将是TagDto。

相关问题