对多关系的意外不可迭代值

Unexpected non-iterable value for to-many relation

当我运行 phpunit时,

$this->assertResponseIsSuccessful();

显示错误:

'hydra:description' => 'Unexpected non-iterable value for to-many relation'

Failed asserting that the Response is successful.
HTTP/1.1 400 Bad Request
Cache-Control:          no-cache, private
Content-Type:           application/ld+json; charset=utf-8
Date:                   Fri, 09 Jul 2021 13:06:48 GMT
Link:                   <http://example.com/docs.jsonld>; rel="http://www.w3.org/ns/hydra/core#apiDocumentation"
X-Content-Type-Options: nosniff
X-Frame-Options:        deny
X-Robots-Tag:           noindex

我认为这个问题与实体中的空值有关。

    /**
     * @return Collection|null<int, Falta>
     */
    public function getFalta(): ?Collection
    {
        return $this->falta;
    }

有道理吗?以及如何解决这个问题?

我的完整实体:

<?php

declare(strict_types=1);

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiFilter;
use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Doctrine\Orm\Filter\SearchFilter;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 * @ORM\Table(schema="db_automacao_sti", name="tb_pessoa")
 *
 * @ApiResource(
 *     normalizationContext={"groups"={"pessoa:read"}},
 *     denormalizationContext={"groups"={"pessoa:write"}},
 *     collectionOperations={
 *         "get"
 *     },
 *     itemOperations={
 *         "get"
 *     }
 * )
 * @ApiFilter(SearchFilter::class, properties={"ordemServicoContratoItemPessoa.itemContrato.contrato": "exact","ordemServicoContratoItemPessoa.itemContrato": "exact"})
 */
class Pessoa
{
    /**
     * @Groups({"pessoa:read"})
     *
     * @ORM\Id
     * @ORM\Column(name="pk_pessoa", type="integer")
     * @ORM\GeneratedValue(strategy="SEQUENCE")
     * @ORM\SequenceGenerator(sequenceName="sq_pessoa", initialValue=1, allocationSize=100)
     */
    private ?int $id = null;

    /**
     * @Groups({"pessoa:read"})
     *
     * @ORM\Column(name="dh_criado_em", type="datetime")
     */
    private ?\DateTimeInterface $criadoEm = null;

    /**
     * @Groups({"pessoa:read"})
     *
     * @ORM\Column(name="dh_atualizado_em", type="datetime")
     */
    private ?\DateTimeInterface $atualizadoEm = null;

    /**
     * @Assert\NotBlank
     * @Assert\Type("\DateTimeInterface")
     *
     * @Groups({"pessoa:read", "pessoa:write"})
     *
     * @ORM\Column(name="dh_contratado_em", type="datetime")
     */
    private ?\DateTimeInterface $contratadoEm = null;

    /**
     * @Assert\NotBlank
     * @Assert\Length(max=255)
     *
     * @Groups({"pessoa:read", "pessoa:write"})
     *
     * @ORM\Column(name="no_pessoa", type="string", length=255)
     */
    private ?string $nome = null;

    /**
     * @Assert\NotBlank
     * @Assert\Length(11)
     *
     * @Groups({"pessoa:read", "pessoa:write"})
     *
     * @ORM\Column(name="nu_cpf", type="string", length=11)
     */
    private ?string $cpf = null;

    /**
     * @Assert\NotBlank
     * @Assert\Length(max=255)
     * @Assert\Email
     *
     * @Groups({"pessoa:read", "pessoa:write"})
     *
     * @ORM\Column(name="ds_email", type="string", length=255)
     */
    private ?string $email = null;

    /**
     * @Assert\NotBlank
     * @Assert\Length(min=10, max=11)
     * @Assert\Type("digit")
     *
     * @Groups({"pessoa:read", "pessoa:write"})
     *
     * @ORM\Column(name="nu_telefone", type="string", length=11)
     */
    private ?string $numeroTelefone = null;

    /**
     * @var Collection<int, OrdemServicoContratoItemPessoa> Coleção de ordemServicoContratoItemPessoa
     * @ORM\OneToMany(targetEntity="OrdemServicoContratoItemPessoa", mappedBy="pessoa")
     */
    private $ordemServicoContratoItemPessoa;

    /**
     * @var Collection<int, Falta> Coleção de falta
     * @ORM\OneToMany(targetEntity="Falta", mappedBy="pessoa")
     * @Groups({"item:read"})
     */
    private $falta;

    /**
     * @ORM\PrePersist
     * @ORM\PreUpdate
     */
    public function onPrePersistOrUpdate(): void
    {
        $this->setAtualizadoEm(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));

        if (null === $this->getCriadoEm()) {
            $this->setCriadoEm(new \DateTimeImmutable('now', new \DateTimeZone('UTC')));
        }
    }

    public function getId(): ?int
    {
        return $this->id;
    }

    public function getCriadoEm(): ?\DateTimeInterface
    {
        return $this->criadoEm;
    }

    public function setCriadoEm(\DateTimeInterface $criadoEm): self
    {
        $this->criadoEm = $criadoEm;

        return $this;
    }

    public function getAtualizadoEm(): ?\DateTimeInterface
    {
        return $this->atualizadoEm;
    }

    public function setAtualizadoEm(\DateTimeInterface $atualizadoEm): self
    {
        $this->atualizadoEm = $atualizadoEm;

        return $this;
    }

    public function getContratadoEm(): ?\DateTimeInterface
    {
        return $this->contratadoEm;
    }

    public function setContratadoEm(\DateTimeInterface $contratadoEm): self
    {
        $this->contratadoEm = $contratadoEm;

        return $this;
    }

    public function getNome(): ?string
    {
        return $this->nome;
    }

    public function setNome(string $nome): self
    {
        $this->nome = $nome;

        return $this;
    }

    public function getCpf(): ?string
    {
        return $this->cpf;
    }

    public function setCpf(string $cpf): self
    {
        $this->cpf = $cpf;

        return $this;
    }

    public function getEmail(): ?string
    {
        return $this->email;
    }

    public function setEmail(string $email): self
    {
        $this->email = $email;

        return $this;
    }

    public function getNumeroTelefone(): ?string
    {
        return $this->numeroTelefone;
    }

    public function setNumeroTelefone(string $numeroTelefone): self
    {
        $this->numeroTelefone = $numeroTelefone;

        return $this;
    }

    /**
     * @return Collection<int, OrdemServicoContratoItemPessoa>
     */
    public function getOrdemServicoContratoItemPessoa(): Collection
    {
        return $this->ordemServicoContratoItemPessoa;
    }

    /**
     * @return Collection|null<int, Falta>
     */
    public function getFalta(): ?Collection
    {
        return $this->falta;
    }
}

您遇到错误,因为 Collection (ArrayCollection) 未初始化。

向您的实体添加构造函数并将每个集合值设置为 new ArrayCollection();

它应该看起来像:

use Doctrine\Common\Collections\ArrayCollection;

public function __construct()
{
    $this->ordemServicoContratoItemPessoa = new ArrayCollection();
    $this->falta = new ArrayCollection();
}

It makes sense?

是的,我会说您已经找到导致错误的罪魁祸首。

And how fix this?

它需要变成任何可迭代的,null 是不够的(它通常不被视为可迭代的,尽管如果它允许一对多关系,这在一对多关系中可能是有问题的 none, 不处理 null 的情况似乎是一种疏忽,但这是一个纯粹的理论讨论)。

实际上给定你的 getter:

    /**
     * @return Collection|null<int, Falta>
     */
    public function getFalta(): ?Collection
    {
        return $this->falta;
    }

您可以通过使 return 类型为非空来使用该错误使其更加明显:

    /**
     * @return Collection<int, Falta>
     */
    public function getFalta(): Collection
    {
        return $this->falta;
    }

(顺便说一句。null<XXX> 无论如何都是错误的 - null 不是任何其他类型,该类型模板不存在。)

现在PHP会直接高亮非法访问。您不需要额外的断言(因为目前它是在您 运行 测试时完成的)。

TypeError: ...

听起来适得其反?不,首先,您在生产代码中使用的 return-type-hint 是固定的。它在其 public 界面(记录此类内容的最佳位置之一)中准确显示并记录了代码的工作类型。


In some languages this called "testing with the compiler", that is to make errors or flaws in the code visible early (we embrace errors). As in PHP there is no compiler, Unit-Tests are important so all code is at least executed once and then the type constraints in the runtime are applied. So to say "testing with the interpreter".


这也应该使具体修复更加明显:如果在那个地方和时间 $this->falta 为空,则需要一个空集合:

    /**
     * @return Collection<int, Falta>
     */
    public function getFalta(): Collection
    {
        return $this->falta ?? new ArrayCollection();
    }

默认的CollectionDoctrine\Common\Collections\ArrayCollection是空的,所以可以很好的表示没有数据(之前null).

( Dylan Kas 也指出了这个基本的 Collection,它很容易在手边获得 )

并且您已经“解决”了类型问题。

这个修复有多好还需要问问自己,目前只解决了类型问题。但是类型问题可能暗示了初始化(或其他)问题。

过早解决问题可能会掩盖实际问题 (!)。这又是为什么我们接受错误并挑起它们,直到我们理解我们在这里所做的事情。然后一瞥之下,事情突然变得清晰起来,修复很容易,我们可以丢弃以前编写的代码(或者,这 可以 发生)。

我无法告诉您目前您是否在测试中遇到错误(或者仅在 return 类型的第一个修复建议中遇到错误 - 然后在生产中)这实际上是由于事实上 $this->falta 必须已经是非空的。如果是这样,让它失败并修复实际原因。

这就是测试到底应该给你什么。