I am trying to do a category relationship table, basically having categories that own other categories like children and parents when i try to POST a new category (e.g. “Child-category-1”) and assign it an owner it goes into an infinite loop with an error of :
The total number of joined relations has exceeded the specified maximum. Raise the limit if necessary with the \"api_platform.eager_loading.max_joins\" configuration key (https://api-platform.com/docs/core/performance/#eager-loading), or limit the maximum serialization depth using the \"enable_max_depth\" option of the Symfony serializer (https://symfony.com/doc/current/components/serializer.html#handling-serialization-depth).
A parent category looks like this:
{
"@context": "/contexts/Category",
"@id": "/categories/1",
"@type": "Category",
"name": "Parent1",
"ownedUsers": [
"/category_owners/1",
"/category_owners/2",
"/category_owners/3"
],
"owners": []
}
My payload would looks like this:
{
"name" : "child-category-4",
"owners":[
"/categories/1"
]
}
I have a ManyToOne with cascade:["persist"] from category entity to categoryOwner entity. however i can not pass this infinite loop.
What am i missing?
Here’s how Category and CategoryOwner entities look like:
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\CategoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity(repositoryClass: CategoryRepository::class)]
#[ApiResource(
normalizationContext: [
'groups' => ['category:read'],
],
denormalizationContext: [
'groups' => ['category:write'],
],
)]
class Category
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['category:write', 'category:read'])]
private ?string $name = null;
#[ORM\OneToMany(mappedBy: 'owner', targetEntity: CategoryOwner::class,cascade: ["persist"])]
#[Groups(['category:write', 'category:read'])]
private Collection $ownedUsers;
#[ORM\OneToMany(mappedBy: 'owned', targetEntity: CategoryOwner::class,cascade: ["persist"])]
#[Groups(['category:write', 'category:read'])]
private Collection $owners;
public function __construct()
{
$this->ownedUsers = new ArrayCollection();
$this->owners = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
/**
* @return Collection<int, CategoryOwner>
*/
public function getOwnedUsers(): Collection
{
return $this->ownedUsers;
}
public function addOwnedUser(CategoryOwner $ownedUser): static
{
if (!$this->ownedUsers->contains($ownedUser)) {
$this->ownedUsers->add($ownedUser);
$ownedUser->setOwner($this);
}
return $this;
}
public function removeOwnedUser(CategoryOwner $ownedUser): static
{
if ($this->ownedUsers->removeElement($ownedUser)) {
// set the owning side to null (unless already changed)
if ($ownedUser->getOwner() === $this) {
$ownedUser->setOwner(null);
}
}
return $this;
}
/**
* @return Collection<int, CategoryOwner>
*/
public function getOwners(): Collection
{
return $this->owners;
}
public function addOwner(CategoryOwner $owner): static
{
if (!$this->owners->contains($owner)) {
$this->owners->add($owner);
$owner->setOwned($this);
}
return $this;
}
public function removeOwner(CategoryOwner $owner): static
{
if ($this->owners->removeElement($owner)) {
// set the owning side to null (unless already changed)
if ($owner->getOwned() === $this) {
$owner->setOwned(null);
}
}
return $this;
}
}
===
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\CategoryOwnerRepository;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\Groups;
#[ORM\Entity(repositoryClass: CategoryOwnerRepository::class)]
#[ApiResource(
normalizationContext: [
'groups' => ['category:owner:read'],
],
denormalizationContext: [
'groups' => ['category:owner:write'],
],
)]
class CategoryOwner
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'ownedUsers')]
#[ORM\JoinColumn(nullable: false)]
#[Groups(['category:owner:read', 'category:owner:write', 'category:write'])]
private ?Category $owner = null;
#[ORM\ManyToOne(inversedBy: 'owners')]
#[ORM\JoinColumn(nullable: false)]
#[Groups(['category:owner:read', 'category:owner:write', 'category:write'])]
private ?Category $owned = null;
public function getId(): ?int
{
return $this->id;
}
public function getOwner(): ?Category
{
return $this->owner;
}
public function setOwner(?Category $owner): static
{
$this->owner = $owner;
return $this;
}
public function getOwned(): ?Category
{
return $this->owned;
}
public function setOwned(?Category $owned): static
{
$this->owned = $owned;
return $this;
}
}
I have used a transparent ManyToMany relation based on the doctrine docs thus i removed the unnecessary entity CategoryOwner.
Category entity would look like:
namespace App\Entity;
use ApiPlatform\Metadata\ApiResource;
use App\Repository\CategoryRepository;
use Doctrine\Common\Collections\ArrayCollection;
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(repositoryClass: CategoryRepository::class)]
#[ApiResource(
normalizationContext: [
'groups' => ['category:read'],
],
denormalizationContext: [
'groups' => ['category:write'],
],
)]
class Category
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['category:write', 'category:read'])]
private ?string $name = null;
#[ORM\ManyToMany(targetEntity: Category::class, inversedBy: "ownedCategories")]
#[Groups(['category:write', 'category:read'])]
#[ORM\JoinTable(name: 'category_owner')]
#[ORM\JoinColumn(name: 'owned_id', referencedColumnName: 'id', nullable: false)]
#[ORM\InverseJoinColumn(name: 'owner_id', referencedColumnName: 'id', nullable: false)]
#[Assert\NotEqualTo(value:"owner.id", message: 'A category cannot be its own owner.')]
private Collection $owners;
#[ORM\ManyToMany(targetEntity: Category::class, mappedBy: "owners")]
#[Groups(['category:write', 'category:read'])]
private Collection $ownedCategories;
public function __construct()
{
$this->ownedCategories = new ArrayCollection();
$this->owners = 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;
}
public function getOwners(): Collection
{
return $this->owners;
}
public function addOwner(Category $owner): self
{
if ($owner === $this) {
throw new \InvalidArgumentException("A category cannot be its own owner.");
}
if (!$this->owners->contains($owner)) {
$this->owners[] = $owner;
}
return $this;
}
public function removeOwner(Category $owner): self
{
$this->owners->removeElement($owner);
return $this;
}
public function getOwnedCategories(): Collection
{
return $this->ownedCategories;
}
public function addOwnedCategory(Category $ownedCategory): self
{
if ($ownedCategory === $this) {
throw new \InvalidArgumentException("A category cannot be owned by itself.");
}
if (!$this->ownedCategories->contains($ownedCategory)) {
$this->ownedCategories[] = $ownedCategory;
}
return $this;
}
public function removeOwnedCategory(Category $ownedCategory): self
{
$this->ownedCategories->removeElement($ownedCategory);
return $this;
}
}