Symfony/Doctrine 中的一对多关系反转查询时出现了无限递归问题

5

背景

在一个简单的Symfony项目中,我创建了两个实体,ProductCategory,它们通过Doctrine Annotations的@ManyToOne@OneToMany关系相关联。一个类别可以有多个产品,一个产品与一个类别相关联。我手动插入了数据到Category表中。

当我使用Category实体仓库获取数据并使用var_dump(...)显示它时,会发生无限递归。当我返回这些数据的JSON响应时,它就是空的。它应该恰好检索到我手动插入的数据。

你有没有想过如何避免这个错误而不删除Category实体中的反向关系?

我尝试过的方法

  • 在关系的一端、另一端和两端都添加Doctrine注释fetch="LAZY"
  • 使用Doctrine将Category对象插入数据库,以查看数据库连接是否正常。是的。
  • 删除关系的反向端。它有效,但这不是我想要的。

代码片段

控制器

dummy/src/Controller/DefaultController.php

...

$entityManager = $this->getDoctrine()->getManager();
$repository = $entityManager->getRepository(Category::class);

// ===== PROBLEM HERE =====
//var_dump($repository->findOneByName('house'));
//return $this->json($repository->findOneByName('house'));

...

实体

dummy/src/Entity/Category.php

<?php

namespace App\Entity;

use App\Repository\CategoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=CategoryRepository::class)
 */
class Category
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(name="id", type="integer")
     */
    private $id;

    /**
     * @ORM\Column(name="name", type="string", length=255)
     */
    private $name;

    /**
     * @ORM\OneToMany(targetEntity=Product::class, mappedBy="category", fetch="LAZY")
     */
    private $products;

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

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

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    /**
     * @return Collection|Product[]
     */
    public function getProducts(): Collection
    {
        return $this->products;
    }

    public function addProduct(Product $product): self
    {
        if (!$this->products->contains($product)) {
            $this->products[] = $product;
            $product->setCategory($this);
        }

        return $this;
    }

    public function removeProduct(Product $product): self
    {
        if ($this->products->contains($product)) {
            $this->products->removeElement($product);
            // set the owning side to null (unless already changed)
            if ($product->getCategory() === $this) {
                $product->setCategory(null);
            }
        }

        return $this;
    }
}


dummy/src/Entity/Product.php

<?php

namespace App\Entity;

use App\Repository\ProductRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass=ProductRepository::class)
 */
class Product
{
    /**
     * @ORM\Id()
     * @ORM\GeneratedValue()
     * @ORM\Column(name="id", type="integer")
     */
    private $id;

    /**
     * @ORM\Column(name="name", type="string", length=255)
     */
    private $name;

    /**
     * @ORM\ManyToOne(targetEntity=Category::class, inversedBy="products", fetch="LAZY")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;

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

    public function getName(): ?string
    {
        return $this->name;
    }

    public function setName(string $name): self
    {
        $this->name = $name;

        return $this;
    }

    public function getCategory(): ?Category
    {
        return $this->category;
    }

    public function setCategory(?Category $category): self
    {
        $this->category = $category;

        return $this;
    }
}
1个回答

5
我假设你使用var_dump用于调试目的。在调试时,请使用dumpdd,它们来自于symfony/debug,并且默认情况下应已启用dev环境。 dumpdd应该能够及时终止无限递归。(许多Symfony / Doctrine对象/服务具有循环引用或仅有大量的引用对象)。dump将给定的php变量添加到分析器(分析器栏中的目标标记符号)或输出中。 dddump一样添加了给定的变量,但还结束了进程(因此是dump和die) - 在生产环境中,不要使用dump/dd/var_dump,而是正确地序列化数据。
其次,$this->json本质上是把json_encode打包到一个JsonResponse对象中的快捷方式(或者使用symfony/serializer代替)。另一方面,json_encode会序列化给定对象的公共属性除非对象实现了JsonSerializable(见下文)。由于几乎所有实体通常都将所有属性设置为私有,因此结果通常是一个空对象序列化。
有许多选项可供选择,但基本上您需要解决无限递归的问题。我认为标准选项如下:
- 使用Symfony序列化器,可以处理循环引用(导致无限递归/循环),从而将对象转换为安全数组。但是,结果可能仍然不符合您的要求... - 在实体上实现JsonSerializable,并小心避免递归添加子对象。 - 自己从对象中构建一个安全数组,以传递给$this->json(“手动方法”)。
在此上下文中,“安全”数组指仅包含字符串、数字和(嵌套)字符串和数字数组的数组,这基本上意味着失去了所有实际对象。
可能还有其他选项,但我发现这些是最方便的选项之一。我通常更喜欢JsonSerializable选项,但这是一种品味问题。以下是一个示例:
class Category implements \JsonSerializable { // <-- new implements!
   // ... your entity stuff
 
   public function jsonSerialize() {
       return [
           'id' => $this->id,
           'name' => $this->name,
           'products' => $this->products->map(function(Product $product) {
               return [
                   'id' => $product->getId(),
                   'name' => $product->getName(),
                   // purposefully excluding category here!
               ];
           })->toArray(),
       ];
   }
}

添加这段代码后,您的代码应该可以直接运行了。在开发时,您应该始终使用如上文所述的dump工具,并且所有的$this->json都可以正常工作。这就是为什么我通常更喜欢这个选项的原因。然而,有一个注意点:您只能通过这种方式拥有一个类别的JSON序列化方案。对于任何其他额外的方法,您必须使用其他选项...这几乎总是成立的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接