"Cache key "App:Category__CLASSMETADATA__" contains reserved characters "{}()/\#:"." - symfony

I'm getting this error which I think I get when trying to access any Entity Repository.
An exception has been thrown during the rendering of a template
("Cache key "App:Category__CLASSMETADATA__" contains reserved
characters "{}()/#:".").
I loaded the commit when the code last worked and reloaded but I'm still getting this error.
I have tried to find an answer in previous posts but those don't work for me because for example I don't have #Assert anywhere in the code.
I tried going through the composer.json file and update all libraries to latest version, then removing vendor folder and composer update because that worked for someone. But it hasn't worked for me.
I'm using Symfony 5.4 and PHP 8.0.
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(type: 'integer')]
private $id;
#[ORM\Column(type: 'string', length: 255)]
private $name;
#[ORM\OneToMany(mappedBy: 'category', targetEntity: Product::class, orphanRemoval: true)]
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<int, 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->removeElement($product)) {
// set the owning side to null (unless already changed)
if ($product->getCategory() === $this) {
$product->setCategory(null);
}
}
return $this;
}
}
I'll be happy to share any information you think might be useful.
Thank you very much in advance.
Edit: I have just downloaded the commit in a different folder and rerun everything but still getting same error message.

Try to replace "App:Category" with "App\Entity\Category" when you call in controllers, forms and repositories.
We expirienced the same issue and the replace worked for us.

Related

How to eliminate this Column not found error?

Originally, the entity Gut had a field reaction that contained a string. The options for reaction were hard-wired in a template. By adding an entity Reaction and changing the Gut form's reaction to an EntityType I'm now plagued with the error message
SQLSTATE[42S22]: Column not found: 1054 Unknown column 't0.reaction' in 'field list'
even though I've rewritten the Gut & Reaction entities. I've probably lost sight of the forest for the trees. What's wrong with the following?
MySQL table gut: reaction column replaced by reaction_id; reaction_id correctly created; foreign key created manually.
Error occurs with this controller method:
#[Route('/', name: 'app_gut_index', methods: ['GET'])]
public function index(GutRepository $gutRepository): Response
{
$guts = $gutRepository->findBy([], ['happened' => 'DESC']); // error thrown here
return $this->render('gut/index.html.twig', [
'guts' => $guts,
]);
}
Gut entity:
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[ORM\ManyToOne(targetEntity: Reaction::class)]
#[ORM\JoinColumn(name: 'reaction_id', referencedColumnName: 'id')]
protected $reaction;
#[ORM\Column(length: 255, nullable: true)]
private ?string $description = null;
#[ORM\Column(name: "datetime")]
private ?\DateTime $happened = null;
public function getId(): ?int
{
return $this->id;
}
public function getReaction(): ?Reaction
{
return $this->reaction;
}
public function setReaction(?Reaction $reaction): self
{
$this->reaction = $reaction;
return $this;
}
...
}
Reaction entity:
use App\Entity\Gut;
use App\Repository\ReactionRepository;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
#[ORM\Entity(repositoryClass: ReactionRepository::class)]
class Reaction
{
public function __construct()
{
$this->guts = new ArrayCollection();
}
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 45)]
private ?string $reaction = null;
public function getId(): ?int
{
return $this->id;
}
public function getReaction(): ?string
{
return $this->reaction;
}
public function setReaction(string $reaction): self
{
$this->reaction = $reaction;
return $this;
}
#[ORM\OneToMany(targetEntity: Gut::class, mappedBy: 'reaction')]
private $guts;
/**
* #return Collection|Product[]
*/
public function getGuts(): Collection
{
return $this->guts;
}
public function addGut($gut): self
{
$this->guts[] = $gut;
return $this;
}
public function __toString()
{
return $this->getReaction();
}
}
Your $reaction property should not have both ORM\Column and ORM\JoinColumn annotations at the same time.
Because of this Doctrine thinks it's a regular column so it's looking for a database field based on the variable name: $reaction -> gut.reaction.
Remove #[ORM\Column(length: 255)] then make sure that you have gut.reaction_id in your database and now it should work.
As a little side note I don't think you need name: 'reaction_id', referencedColumnName: 'id' in ORM\JoinColumn because that's how Doctrine will name them automatically anyway
Just couldn't let go. I eventually found a path to get the Gut and Reaction entities to play nicely together. What I did:
cloned the project
manually deleted reaction property from Gut entity; created & executed a migration
in MySQL, added back in a reaction column
used make:entity Gut to add a reaction property as ManyToOne on Reaction; made a migration
used MySQL to populate the reaction_id column from the database of the cloned project.
(Probably missed a step in here somewhere, but) gut->getReaction(),etc,
now behave as expected - in a ManyToOne relationship.

How does Doctrine determine the order to commit (and how to troubleshoot it)

I am adding fixtures using theofidry/AliceBundle which in turn uses nelmio/alice.
One of my entities is Vendor and another is VendorUser and VendorUser holds the PK of Vendor and thus Vendor must be committed before VendorUser. Even though I don't think it matters, I took steps to ensure they are persisted in the correct order and have verified that Vendor was persisted before VendorUser. But then when flushed, I get a not-null constraint error stating the vendor is not set in vendorUser.
I look into it and see that UnitOfWork::commit() calls UnitOfWork::getCommitOrder() which also shows that VendorUser is scheduled to be committed before Vendor. Going down the rabbit hole, I see that UnitOfWork::getCommitOrder() calls UnitOfWork::getCommitOrderCalculator() which returns a new CommitOrderCalculator, which has the following comment in its source code:
CommitOrderCalculator implements topological sorting, which is an
ordering algorithm for directed graphs (DG) and/or directed acyclic
graphs (DAG) by using a depth-first searching (DFS) to traverse the
graph built in memory. This algorithm have a linear running time
based on nodes (V) and dependency between the nodes (E), resulting in
a computational complexity of O(V + E).
I've tried to figure out how this class works but am not making much progress. Are there any tools to analyze what it is doing? How does Doctrine determine the order to commit records? How can I gleam information about why it is doing it the way it is so I may troubleshoot? I expect that Vendor has some not-null property which isn't yet set, however, for the life of me I can't figure it out.
EDIT It appears to either be a Doctrine bug or probably more likely my schema is not supported. I believe the cause is two entities have references to each other (users have an organization, all entities except for Tenant must belong to a Tenant, and all entities have a createBy and updateBy blameable field) and Doctrine doesn't know which one to do first. There is a correct order, but it is based on business logic and not the database schema, so Doctrine alone can't figure it out. Is there a way to instruct Doctrine the order to commit? Below is a test script which manually creates the entities without using fixtures or blamable. As seen by the output from UnitOfWork's commit order, it will clearly fail as concrete classes are committed before their inherited parents. Any recommendations?
private function testSomething()
{
$manager = $this->managerRegistry->getManager();
$rootUser = $manager->getRepository(TestTenantUser::class)->find(new NilUlid);
$tenant = new TestTenant;
$vendor = new TestVendor;
$tenant->addVendor($vendor);
$tenantUser = new TestTenantUser;
$vendorUser = new TestVendorUser;
$tenant->addUser($tenantUser);
$vendor->addUser($vendorUser);
// Simulating being set by blamable listner.
$tenant->setCreateBy($rootUser)->setUpdateBy($rootUser);
$vendor->setCreateBy($rootUser)->setUpdateBy($rootUser);
$tenantUser->setCreateBy($rootUser)->setUpdateBy($rootUser);
$vendorUser->setCreateBy($rootUser)->setUpdateBy($rootUser);
// Technically only need to persist tenant since others will cascade persist, but just to be double sure.
$manager->persist($tenant);
$manager->persist($vendor);
$manager->persist($tenantUser);
$manager->persist($vendorUser);
printf('%-30s with ID %s has organization %s'.PHP_EOL, get_class($tenantUser), $tenantUser->getId()->toRfc4122(), $tenantUser->getOrganization()->getId()->toRfc4122());
printf('%-30s with ID %s has organization %s'.PHP_EOL, get_class($vendorUser), $vendorUser->getId()->toRfc4122(), $vendorUser->getOrganization()->getId()->toRfc4122());
try {
$manager->flush();
}
catch(\Exception $e) {
exit($e->getMessage());
}
}
class UnitOfWork implements PropertyChangedListener
{
public function commit($entity = null)
{
...
$commitOrder = $this->getCommitOrder();
echo(PHP_EOL.'UnitOfWork::commit() $this->getCommitOrder(): '.PHP_EOL);
foreach($commitOrder as $class){
echo($class->getName().PHP_EOL);
}
...
}
}
App\Entity\Test\TestTenantUser with ID 0182f48a-5778-0cb2-d24d-7840c74773b1 has organization 0182f48a-5778-0cb2-d24d-7840c74773b0
App\Entity\Test\TestVendorUser with ID 0182f48a-5779-dc20-c1d4-9ec596c1ee6d has organization 0182f48a-5778-0cb2-d24d-7840c74773b2
UnitOfWork::commit() $this->getCommitOrder():
App\Entity\Test\TestTenant
App\Entity\Test\TestVendorUser
App\Entity\Test\TestAbstractUser
App\Entity\Test\TestAbstractOrganization
App\Entity\Test\TestTenantUser
App\Entity\Test\TestVendor
An exception occurred while executing a query: SQLSTATE[23502]: Not null violation: 7 ERROR: null value in column "organization_id" of relation "test_abstract_user" violates not-null constraint
DETAIL: Failing row contains (0182f48a-5779-dc20-c1d4-9ec596c1ee6d, null, 00000000-0000-0000-0000-000000000000, 00000000-0000-0000-0000-000000000000, 2022-08-31 08:34:43, 2022-08-31 08:34:43, vendor).
<?php
declare(strict_types=1);
namespace App\Entity\Test;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class TestTenant extends TestAbstractOrganization
{
#[ORM\OneToMany(targetEntity: TestVendor::class, mappedBy: 'tenant', cascade: ['persist', 'remove'])]
private Collection $vendors;
public function __construct()
{
parent::__construct();
$this->vendors = new ArrayCollection();
}
public function addUser(TestAbstractUser $user): self
{
$user->setTenant($this);
return parent::addUser($user);
}
public function getVendors(): Collection
{
return $this->vendors;
}
public function addVendor(TestVendor $vendor): self
{
if (!$this->vendors->contains($vendor)) {
$this->vendors[] = $vendor;
$vendor->setTenant($this);
}
return $this;
}
public function removeVendor(TestVendor $vendor): self
{
if (!$this->vendors->removeElement($vendor)) {
return $this;
}
if ($vendor->getTenant() !== $this) {
return $this;
}
$vendor->setTenant(null);
return $this;
}
}
<?php
declare(strict_types=1);
namespace App\Entity\Test;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\AssociationOverrides([new ORM\AssociationOverride(name: 'tenant', joinTable: new ORM\JoinTable(name: 'tenant'), inversedBy: 'vendors')])]
class TestVendor extends TestAbstractOrganization
{
use TestBelongsToTenantTrait;
public function addUser(TestAbstractUser $user): self
{
$user->setTenant($this->getTenant());
return parent::addUser($user);
}
}
<?php
declare(strict_types=1);
namespace App\Entity\Test;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\InheritanceType(value: 'JOINED')]
#[ORM\DiscriminatorColumn(name: 'discriminator', type: 'string')]
#[ORM\DiscriminatorMap(value: ['tenant' => TestTenant::class, 'vendor' => TestVendor::class])]
abstract class TestAbstractOrganization extends TestAbstractEntity
{
#[ORM\OneToMany(mappedBy: 'organization', targetEntity: TestAbstractUser::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
protected Collection $users;
public function __construct()
{
$this->users = new ArrayCollection();
parent::__construct();
}
public function getUsers(): Collection
{
return $this->users;
}
public function addUser(TestAbstractUser $user): self
{
if (!$this->users->contains($user)) {
$this->users[] = $user;
}
$user->setOrganization($this);
return $this;
}
public function removeUser(TestAbstractUser $user): self
{
if ($this->users->removeElement($user)) {
if ($user->getOrganization() === $this) {
$user->setOrganization(null);
}
}
return $this;
}
}
<?php
declare(strict_types=1);
namespace App\Entity\Test;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class TestTenantUser extends TestAbstractUser
{
use TestBelongsToTenantTrait;
}
<?php
declare(strict_types=1);
namespace App\Entity\Test;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class TestVendorUser extends TestAbstractUser
{
use TestBelongsToTenantTrait;
}
<?php
declare(strict_types=1);
namespace App\Entity\Test;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\InheritanceType(value: 'JOINED')]
#[ORM\DiscriminatorColumn(name: 'discriminator', type: 'string')]
#[ORM\DiscriminatorMap(value: ['tenant' => TestTenantUser::class, 'vendor' => TestVendorUser::class])]
abstract class TestAbstractUser extends TestAbstractEntity
{
#[ORM\ManyToOne(inversedBy: 'users')]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
protected ?TestAbstractOrganization $organization=null;
public function getOrganization(): TestAbstractOrganization
{
return $this->organization;
}
public function setOrganization(TestAbstractOrganization $organization): self
{
$this->organization = $organization;
return $this;
}
}
<?php
declare(strict_types=1);
namespace App\Entity\Test;
use Doctrine\ORM\Mapping as ORM;
use DateTime;
use Gedmo\Mapping\Annotation as Gedmo;
use Symfony\Component\Uid\Ulid;
use App\Doctrine\IdGenerator\UlidGenerator;
abstract class TestAbstractEntity
{
#[ORM\Id]
#[ORM\Column(type: 'ulid', unique: true)]
#[ORM\GeneratedValue(strategy: 'CUSTOM')]
#[ORM\CustomIdGenerator(class: UlidGenerator::class)]
protected ?Ulid $id = null;
#[ORM\Column(type: 'datetime')]
//#[Gedmo\Timestampable(on: 'create')]
protected ?DateTime $createAt = null;
#[ORM\ManyToOne(targetEntity: TestAbstractUser::class)]
#[ORM\JoinColumn(nullable: false)]
//#[Gedmo\Blameable(on: 'create')]
protected ?TestAbstractUser $createBy = null;
#[ORM\Column(type: 'datetime')]
//#[Gedmo\Timestampable(on: 'update')]
protected ?DateTime $updateAt = null;
#[ORM\ManyToOne(targetEntity: TestAbstractUser::class)]
#[ORM\JoinColumn(nullable: false)]
//#[Gedmo\Blameable(on: 'update')]
protected ?TestAbstractUser $updateBy = null;
public function __construct()
{
$this->createAt = new DateTime();
$this->updateAt = new DateTime();
}
public function getId(): ?Ulid
{
return $this->id;
}
public function getCreateAt(): ?DateTime
{
return $this->createAt;
}
public function getCreateBy(): ?TestAbstractUser
{
return $this->createBy;
}
// Shouldn't need this? Maybe only by HelpDesk?
public function setCreateAt(DateTime $date): self
{
$this->createAt = $date;
return $this;
}
public function setCreateBy(TestAbstractUser $user): self
{
$this->createBy = $user;
return $this;
}
public function getUpdateAt(): ?DateTime
{
return $this->updateAt;
}
public function getUpdateBy(): ?TestAbstractUser
{
return $this->updateBy;
}
// Shouldn't need this? Maybe only by HelpDesk?
public function setUpdateAt(DateTime $date): self
{
$this->updateAt = $date;
return $this;
}
public function setUpdateBy(TestAbstractUser $user): self
{
$this->updateBy = $user;
return $this;
}
}
<?php
declare(strict_types=1);
namespace App\Entity\Test;
use Doctrine\ORM\Mapping as ORM;
trait TestBelongsToTenantTrait
{
#[ORM\ManyToOne(targetEntity: TestTenant::class)]
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
protected ?TestTenant $tenant=null;
public function getTenant(): ?TestTenant
{
return $this->tenant;
}
public function setTenant(TestTenant $tenant): self
{
$this->tenant = $tenant;
return $this;
}
}

Circular reference for Repository in Symfony 5

I try to follow this tutorial : https://www.thinktocode.com/2018/03/05/repository-pattern-symfony/.
It's suppose to help structure your Repository.
But when i get to this point :
final class ProductRepository
{
/**
* #var EntityManagerInterface
*/
private $entityManager;
/**
* #var ObjectRepository
*/
private $objectRepository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
$this->objectRepository = $this->entityManager->getRepository(Product::class);
}
public function find(int $productId): Product
{
$product = $this->objectRepository->find($productId);
return $product;
}
public function findOneByTitle(string $title): Product
{
$product = $this->objectRepository
->findOneBy(['title' => $title]);
return $product;
}
public function save(Product $product): void
{
$this->entityManager->persist($product);
$this->entityManager->flush();
}
}
And testing my Repository with this test case :
<?php
namespace App\Tests\Repository;
use App\Repository\ProductRepository;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class ProductRepository_KernelTest extends KernelTestCase
{
private ?ProductRepository $_productRepository;
protected function setUp(): void
{
$kernel = self::bootKernel();
$this->_productRepository = self::$container->get(ProductRepository::class);
}
public function test_findAllProductNatByLabelForLabelEmptyReturnTenProduct()
{
dump($this->_productRepository->findAllProductsByLabel('AACIFEMINE'));
die();
}
}
It loop endlessly.
I think it's due to this code :
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
$this->objectRepository = $this->entityManager->getRepository(Product::class); // <-----
}
As it call the ProductRepository constructor inside of this same constructor... So i guess it's why that loop
So I don't know. Is this tutorial just wrong or not up to date ?
https://www.thinktocode.com/2018/03/05/repository-pattern-symfony/#comment-4155200782
Maciej,
You are correct that in these example we are using 2 repositories. The
object repository from doctrine inside our own custom repository. This
allows use to be decoupled from doctrine's repository and still change
this in the future. This means to not set your custom repository as
the default repository in your entity.
You can get rid of inject the object repository, and in so only be
using 1 repository by implementing a BaseRepository class in which you
create the basic findBy, findOneBy, createQueryBuilder yourself. Take
a look at the EntityRepository in Doctrine/ORM. This might be a good
follow up topic to go over in a future article to create a better
solution then I suggested in here.

Symfony/Doctrine Infinite recursion while fetching from database with inverted side of a ManyToOne relationship

Context
In a simple Symfony project, I've created two entities, Product and Category, which are related by a #ManyToOne and a #OneToMany relationship with Doctrine Annotations. One category can have multiple products and one product relates to one category. I've manually inserted data in the Category table.
When I fetch data using Category entity repository and I display it with a var_dump(...), an infinite recursion happens. When I return a JSON response with these data, it is just empty. It should retrieve exactly the data I inserted manually.
Do you have any idea of how to avoid this error without removing the inverse side relationship in the Category entity?
What I've tried
Adding the Doctrine Annotation fetch="LAZY" in one side, the other side and both side of the relationship.
Inserting Category object in the database using Doctrine to see if the database connection is working. Yes it is.
Removing the inverse side of the relationship. It worked but it's not what I want.
Code snippet
Controller
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'));
...
Entities
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;
}
}
I assume you use var_dump for debugging purposes. For debugging purposes use dump or dd which is from symfony/debug and should already be enabled on dev by default. Both dump and dd should abort the infinite recursion in time. (Lots of symfony/doctrine objects/services have circular references or just a lot of referenced objects.) dump adds the given php var(s) to either the profiler (target mark symbol in the profiler bar) or to the output. dd adds the given var(s) like dump but also ends the process (so dump and die). - On production never use dump/dd/var_dump, but properly serialize your data.
Secondly, $this->json is essentially a shortcut for packing json_encode into a JsonResponse object (or use the symfony/serializer instead). json_encode on the other hand serializes public properties of the object(s) given unless the object(s) implement JsonSerializable (see below). Since almost all entities usually have all their properties private, the result is usually an empty object(s) serialization.
There are a multitude of options to choose from, but essentially you need to solve the problem of infinite recursion. The imho standard options are:
using the symfony serializer which can handle circular references (which cause the infinite recursion/loop) and thus turning the object into a safe array. However, the results may still not be to your liking...
implementing JsonSerializable on your entity and carefully avoid recursively adding the child-objects.
building a safe array yourself from the object, to pass to $this->json ("the manual approach").
A safe array in this context is one, that contains only strings, numbers and (nested) arrays of strings and numbers, which essentially means, losing all actual objects.
There are probably other options, but I find these the most convenient ones. I usually prefer the JsonSerializable option, but it's a matter of taste. One example for this would be:
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(),
];
}
}
After adding this your code should just work. For dev, you always should use dump as mentioned and all $this->json will just work. That's why I usually prefer this option. However, the caveat: You only can have one json serialization scheme for categories this way. For any additional ways, you would have to use other options then ... which is almost always true anyway.

How to use Symfony EasyAdmin with entities having a constructor?

Does EasyAdmin support entity classes with constructor arguments for properties that are meant to be not nullable? EasyAdmin instantiates the entity class even if you click the "Add " button, right? Unfortunatelly this results in an "Too few arguments to function __construct()" error. Do you have a solution for this problem?
I tend to use the constructor for entity properties that are not nullable. Unfortunatelly EasyAdmin throws errors like this one when I click on the e.g. Add FiscalYear button to create a new entity object (FiscalYear in my example):
Too few arguments to function App\Entity\FiscalYear::__construct(), 0 passed in /myProject/vendor/easycorp/easyadmin-bundle/src/Controller/AdminControllerTrait.php on line 618 and exactly 2 expected
How can I prevent these errors? As you can see in the following entity class the two constructor arguments represent the data that is meant to be submitted via the form:
<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\FiscalYearRepository")
*/
class FiscalYear
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private ?int $id = null;
/**
* #ORM\Column(type="integer")
*/
private int $title;
/**
* #ORM\Column(type="boolean", options={"default": 0})
*/
private bool $completed = false;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Company", inversedBy="fiscalYears")
* #ORM\JoinColumn(nullable=false)
*/
private Company $company;
public function __construct(int $title, Company $company)
{
$this->title = $title;
$this->company = $company;
}
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): int
{
return $this->title;
}
public function setTitle(int $title): void
{
$this->title = $title;
}
public function getCompleted(): bool
{
return $this->completed;
}
public function setCompleted(bool $completed): void
{
$this->completed = $completed;
}
public function getCompany(): Company
{
return $this->company;
}
public function setCompany(Company $company): void
{
$this->company = $company;
}
}
Is there a possibility to let EasyAdmin show the "create a new entity object" form without instantiating the entity class?
No, EasyAdmin doesn't natively support constructor with argument.
To avoid this problem, you have three solution.
solution1: Override EasyAdminController
The documentation explains this method.
// src/Controller/AdminController.php
namespace App\Controller;
use EasyCorp\Bundle\EasyAdminBundle\Controller\EasyAdminController;
class FiscalYearController extends EasyAdminController
{
public function createNewFiscalYearEntity()
{
//your own logic here to retrieve title and company
return new FiscalYear($title, $company);
}
}
Depending you business model, it could be very difficult to retrieve title and company
solution2: Respect the entity pattern and help your business model with a factory pattern
Your entities should respect the entity pattern and their constructor should be edited to remove arguments.
To replace your constructor in your business model, create a factory.
class FiscalYearFactory
{
public static function create(int $title, Company $company): FiscalYear
{
$fiscalYear = new FiscalYear();
$fiscalYear->setCompany($company);
$fiscalYear->setTitle($title);
return $fiscalYear;
}
}
in your model, you have to do some updates:
//Comment code like this in your business model
$fiscalYear = new FiscalYear(2020,$company);
//Replace it, by this code:
$fiscalYear = FiscalYearFactory::create(2020,$company);
Solution3 Accept null values in your constructor.
I do NOT like this solution. Your properties shall be edited too to accept null values, your getters shall be edited to return null value. This is a solution, but I discourage you to use it.
<?php
declare(strict_types=1);
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\FiscalYearRepository")
*/
class FiscalYear
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private ?int $id = null;
/**
* #ORM\Column(type="integer")
*/
private ?int $title;
/**
* #ORM\Column(type="boolean", options={"default": 0})
*/
private bool $completed = false;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Company", inversedBy="fiscalYears")
* #ORM\JoinColumn(nullable=false)
*/
private Company $company;
public function __construct(?int $title = null, ?Company $company = null)
{
$this->title = $title;
$this->company = $company;
}
public function getId(): ?int
{
return $this->id;
}
public function getTitle(): ?int
{
return $this->title;
}
You should use the first solution which is a better practice

Resources