Can you please simplify (or help me refactor) my logic - symfony

I am aggregating data in json to convert it into symfony object.
A category (otology) has a type (swimming pool, restaurant). This category has a description that is translated into several languages.
I would like to be able to simply do getXXX('fr'), to have this description in French, getXXX('en') for English.
With a repository I think I can do it, but maybe I complicate it:
My class Otolongy:
class Ontology
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $idOntology = null;
#[ORM\OneToMany(mappedBy: 'ontology', targetEntity: Label::class, cascade: ['persist', 'remove'], fetch:'EAGER')]
private Collection $descriptions;
my class label (for fields description):
#[ORM\Entity(repositoryClass: LabelRepository::class)]
class Label
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: Types::TEXT)]
private ?string $label = null;
#[ORM\ManyToOne(inversedBy: 'descriptions')]
private ?Ontology $ontology = null;
#[ORM\ManyToOne(inversedBy: 'labels', fetch:'EAGER')]
private ?Lang $lang = null;
my class lang:
#[ORM\Entity(repositoryClass: LangRepository::class)]
class Lang
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 10)]
private ?string $labelLang = null;
#[ORM\OneToMany(mappedBy: 'lang', targetEntity: Label::class)]
private Collection $labels;
You can see that for my tests I had to put fetch:'EAGER' I'm not convinced that this is the right way to do it... well after all put 1 eager or have to make a 'complex' request in my controller it should be equivalent...
I have already done this system on java and I had made a map key, so I used this command ontology.description.get(lang) in the template, in order to use the local as an argument and java gave me the description in the good language.
if i can avoid loading the ontology in eager i think the display of twig will go faster? Namely that I must also check that the translation in the visitor's local exists, otherwise I must display by default (English or French)
So my goal is to use the visitor's locale to know which language (language of the ontology in the database) that twig should display. Is there maybe a simpler way to do it? Maybe I'm totally wrong?!

Related

A circular reference has been detected when serializing the object of class "App\Entity\Orders" (configured limit: 1)

I am encountering problems regarding the circular references of the entities.
I am creating an API where one route will have the purpose of providing in detail, all entities in one single call. So in the OrdersRepository, i have a function that provide to give all the infos.
I have tried, as read in other questions made on stack overflow (and as suggest in the doc), to use the annotations #[Ignore] and #[Groups(['group_name'])], but I was not able to make it work. Probably I did not understand the concept of this solution well.
Even the "MaxDepth" solution didn't work.
Basically, I have a repository function that provides the result of 4 entities: Order, Address, OrderProducts and Product. And they are structured in this way:
Order
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne]
private ?Carrier $carrier = null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?Address $address_delivery = null;
#[ORM\ManyToOne]
private ?Address $address_invoice = null;
#[ORM\Column]
private ?int $invoice = null;
#[ORM\OneToMany(mappedBy: 'order_related', targetEntity: OrderProducts::class)]
private Collection $orderProducts;
#[ORM\OneToMany(mappedBy: 'order_related', targetEntity: OrderDetail::class)]
private Collection $orderDetails;
OrderDetail
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(type: Types::DECIMAL, precision: 20, scale: 6, nullable: true)]
private ?string $weight = null;
#[ORM\Column(type: Types::DECIMAL, precision: 20, scale: 6, nullable: true)]
private ?string $height = null;
#[ORM\Column(type: Types::DECIMAL, precision: 20, scale: 6, nullable: true)]
private ?string $depth = null;
#[ORM\Column(type: Types::DECIMAL, precision: 20, scale: 6, nullable: true)]
private ?string $length = null;
#[ORM\ManyToOne(inversedBy: 'orderDetails')]
private ?Orders $order_related = null;
OrderProducts
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'orderProducts')]
#[ORM\JoinColumn(nullable: false)]
private ?Orders $order_related = null;
#[ORM\Column(type: Types::DECIMAL, precision: 20, scale: 6)]
private ?string $price= null;
#[ORM\ManyToOne]
#[ORM\JoinColumn(nullable: false)]
private ?Product $product = null;
Product
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(length: 255)]
private ?string $description_short = null;
#[ORM\Column(length: 255)]
private ?string $description = null;
#[ORM\Column(length: 255)]
private ?string $reference = null;
OrderController
#[Route('v1/api/all', name: 'dashboard', methods: 'GET')]
public function all_orders_info(CustomSerializer $serializer): Response
{
$orders = $this->ordersRepository->findAll();
$responseContent = $serializer->serialize($orders, 'json');
return new JsonResponse(data: $responseContent, status: Response::HTTP_OK, json: true);
}
Service CustomSerializer.php
class CustomSerializer implements SerializerInterface
{
private SerializerInterface $serializer;
public function __construct()
{
$this->serializer = new Serializer(
normalizers : [new ObjectNormalizer()],
encoders : [new JsonEncoder()],
);
}
public function serialize(mixed $data, string $format, array $context = []): string
{
return $this->serializer->serialize($data, $format, $context);
}
public function deserialize(mixed $data, string $type, string $format, array $context = []): mixed
{
return $this->serializer->deserialize($data, $type, $format, $context);
}
}
The problem is that I get the classic error "A circular reference has been detected when serializing the object of class "App\Entity\Orders" (configured limit: 1).". I inserted the Ignore and Groups annotations within the "Orders" entity, above the two properties "orderProducts" and "orderDetails" of Orders entity, but nothing.
Where i'm wrong?
None of these attributes would would work if you initialize the serializer component manually like that (your CustomSerializer), a little more is needed. Luckily there is an alternative;
It is recommended when within the Symfony framework to just inject the SerializerInterface service.
// src/Controller/DefaultController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Serializer\SerializerInterface;
class DefaultController extends AbstractController
{
public function index(SerializerInterface $serializer)
{
// keep reading for usage examples
}
}
As described here https://symfony.com/doc/current/serializer.html.
This comes with a few encoders and a lot of normalizers already configured to work with other aspects of the framework.
Sadly when you search through google the standalone component documentation (https://symfony.com/doc/current/components/serializer.html) comes up first. Which does explain some good core principles in how its internals work.
But within the framework most explained configuration things are already done and you can use it right away.
You could then start playing with your Ignore/Groups and MaxDepth things to get it working.
Another minor note you can also change some settings for the injected version in https://symfony.com/doc/current/reference/configuration/framework.html#serializer
these settings used to be in framework.yaml as a comment not sure if they are still there or if it got its own file now when you install the package though flex.
For those who are using the standalone component and still want attributes you can read up on how to set up that at https://symfony.com/doc/current/components/serializer.html#attributes-groups

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.

Symfony ArrayCollection dans form symfony

I'm trying somehow to attach a partner's modules to my PartnerModule entity, but I can't because it has to create as many relationships as there are associated modules.
A partner has several modules and a module belongs to several partners.
Hence this intermediate table: PartnerModule. I understand that this is definitely a problem in my entities but I can't manage to fix it. Here are my entities and my form. I specify that when I put "multiple" => false it works well.
enter image description here
My error :
Expected argument of type "?App\Entity\Module", "Doctrine\Common\Collections\ArrayCollection" given at property path "modules".
Entity Module :
#[ORM\Entity(repositoryClass: ModuleRepository::class)]
class Module
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 250)]
private ?string $nom = null;
#[ORM\Column]
private ?bool $statut = null;
#[ORM\OneToMany(mappedBy: 'Modules', targetEntity: PartenaireModule::class)]
private Collection $partenaireModules;
public function __construct()
{
$this->partenaireModules = new ArrayCollection();
}
Entity Partenaire Module
#[ORM\Entity(repositoryClass: PartenaireModuleRepository::class)]
class PartenaireModule
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column]
private ?bool $is_active = true;
#[ORM\ManyToOne(inversedBy: 'partenaireModules')]
private ?Partenaire $Partenaires = null;
#[ORM\ManyToOne(inversedBy: 'partenaireModules')]
private ?Module $Modules = null;
This is how my data should be mapped:
-> module_id, partenaire_id, is_active, id
Any help or clue to help me will be welcome :)
Thanks

API Platform: Apply a filter to multiple resources

I'm trying to apply SearchFilter to multiple resources.
Let's imagine I've got two resources: Post and Article (just for the illustration):
#[ApiRsource]
class Post
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $postDescription = null;
...
}
#[ApiRsource]
class Article
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $articleDescription = null;
...
}
I want to add a unique endpoint /search?q=key that will return Post and Article resources with a postDescription/articleDescription containing key.
Is there a way to do this without resorting to inheritance?

How to filter inherited Doctrine objects?

Each Product is "owned" by a given Tenant (i.e. user) and requires a color which could be either a standard Color available to all tenants or a proprietary TenantOwnedColor which was created by a given tenant and only available to that tenant.
#[ORM\Entity]
class Product implements BelongsToTenantInterface
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 180)]
private string $name;
#[ORM\ManyToOne(targetEntity: Color::class)]
#[ORM\JoinColumn(nullable: false)]
private ?Color $color;
#[ORM\ManyToOne(targetEntity: Tenant::class)]
#[ORM\JoinColumn(nullable: false)]
private ?Tenant $tenant;
}
#[ORM\Entity]
#[ORM\InheritanceType(value: 'JOINED')]
#[ORM\DiscriminatorColumn(name: 'type', type: 'string')]
#[ORM\DiscriminatorMap(value: ['open' => Color::class, 'proprietary' => TenantOwnedColor::class])]
class Color
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private int $id;
#[ORM\Column(type: 'string', length: 180)]
private string $name;
#[ORM\Column(type: 'string', length: 255)]
private string $colorCode;
}
#[ORM\Entity]
class TenantOwnedColor extends Color implements BelongsToTenantInterface
{
#[ORM\ManyToOne(targetEntity: Tenant::class)]
#[ORM\JoinColumn(nullable: false)]
private ?Tenant $tenant;
}
In order to filter all entities that implement BelongsToTenantInterface and limit them to the Tenant that the logged on user belongs to, a listener adds a doctrine filter.
namespace App\EventListener;
use Doctrine\ORM\EntityManager;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTAuthenticatedEvent;
use App\Entity\MultiTenenacy\BelongsToTenantInterface;
final class AuthenticatedTenantEntityListener
{
public function __construct(private EntityManager $entityManager)
{
}
public function onJWTAuthenticated(JWTAuthenticatedEvent $jwtAuthenticatedEvent): void
{
$user = $jwtAuthenticatedEvent->getToken()->getUser();
if (!$user instanceof BelongsToTenantInterface) {
return;
}
$this->entityManager
->getFilters()
->enable('tenant_filter')
->setParameter('tenantId', $user->getTenant()->getId());
}
}
namespace App\Doctrine;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Query\Filter\SQLFilter;
use App\Entity\MultiTenenacy\BelongsToTenantInterface;
final class TenantFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $classMetadata, $targetTableAlias): string
{
if ($classMetadata->getReflectionClass()->implementsInterface(BelongsToTenantInterface::class)) {
return sprintf('%s.tenant_id = %s', $targetTableAlias, $this->getParameter('tenantId'));
}
return '';
}
}
My approach works for Product but not for TenantOwnedColor. When troubleshooting, I discovered that TenantFilter::addFilterConstraint() is being passed the parent class (i.e. Color) metadata which doesn't implement BelongsToTenantInterface and thus I now know why it isn't filtering.
I also found the following in Doctrine's documentation so evidently it is by design:
In the case of joined or single table inheritance, you always get
passed the ClassMetadata of the inheritance root. This is necessary to
avoid edge cases that would break the SQL when applying the filters.
Are there other ways to implement this in order to overcome this shortcoming?
It seems that this topic has been brought up by the community some times now. There does not seem to be an official workaround, due to innestability provoked by those famous edge cases, although some people have made their changes/hacks/workarounds to the problem so it is not impossible.
Links that might help, with some workarounds mentioned in them, I hope you find them useful enough, sorry that I cannot be of more help:
https://github.com/doctrine/orm/issues/7504#issuecomment-568569307
https://github.com/doctrine/orm/issues/6329
https://github.com/doctrine/orm/issues/6329#issuecomment-538854316
https://www.doctrine-project.org/projects/doctrine-orm/en/2.11/reference/php-mapping.html#classmetadata-api

Resources