Symfony - I get circular reference error in index of controller - symfony

Hi I'm having troubles getting a list of all "blog tag" entities in the index function of my controller.
I tried using normalizer groups but somehow I still get a circular reference error. I expect my controller to output a list of blog tags, by id and name.
This is my controller:
#[Route('/', name: 'api_blogtag_index', methods: ['GET'])]
#[IsGranted('IS_AUTHENTICATED')]
public function index(BlogtagRepository $blogtagRepository): Response
{
return $this->json([
'tags' => $blogtagRepository->findAll(),
Response::HTTP_OK, [], [
AbstractNormalizer::GROUPS => ['show_blogtag']
]
]);
}
And this is the blog tag entity class:
#[ORM\Entity(repositoryClass: BlogtagRepository::class)]
class Blogtag
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['show_blogtag'])]
private ?int $id = null;
#[ORM\Column(length: 255, unique: true)]
#[Groups(['show_blogtag'])]
private ?string $name = null;
#[ORM\ManyToMany(targetEntity: Blog::class, inversedBy: 'blogtags')]
private Collection $blogs;

Circular reference is an error that appears when an object refers to itself, directly.
This problem has already been solved, here you will find an answer of your problem.
You can learn more on official documentation

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 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

Symfony3 deserialize

I have this entity:
AppBundle\Entity\Ciudad
class Ciudad{
...
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\ComunidadAutonoma")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="id_ccaa", referencedColumnName="id")
* })
*/
private $ccaa;
....
public function getCcaa()
{
return $this->ccaa;
}
public function setCcaa(ComunidadAutonoma $ccaa)
{
$this->ccaa = $ccaa;
}
}
And the other entity is:
AppBundle\Entity\ComunidadAutonoma
class ComunidadAutonoma{
properties
getters
setters
}
In a controller, I get data from a form, and I´m triying to deserialize the data into a Ciudad entity, but is getting me allways the same error:
Expected argument of type "AppBundle\Entity\ComunidadAutonoma", "integer" given
In the form data I send to the action in the controller, the value of the comunidadautonoma is the id of the selected option in a combo:
{
parameters...
ccaa:7,
parameters...
}
In my controller I have this:
<?php
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use AppBundle\Entity\Ciudad;
class CiudadController extends Controller
{
public function procesarAction(Request $request)
{
$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$this->serializer = new Serializer($normalizers, $encoders);
$ciudad= $this->serializer->deserialize($parametros['parametros'], Ciudad::class, 'json');
}
}
Am I missing something?Do I need any special configuration to deserializer an entity with a relation?
You dont have to do anything if you properly configured a type. While creating a Form Type for your entity please add class name to your type like:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Ciudad::class,
]);
}
And please use english naming for your projects.
First of all, since you are sending form data to your controller you could use Form Type classes to leverage all the power of the Symfony Form Component that will all this job for you.
Answering your specific question (and assuming you cannot/don't want to use Symfony Form Component) this error is absolutely expected. As you can see in your setCcaa function declaration inside Ciudad class:
public function setCcaa(ComunidadAutonoma $ccaa)
Because of the type-hinting (ComunidadAutonoma $ccaa) setCcaa function expects an argument of type ComunidadAutonoma. Now when Symfony serializer tries to denormalize your json object it calls setCcaa function with argument the ccaa value provided in your json (in your example is 7) which happens to be an integer. So Symfony complains that you provide an integer instead of ComunidadAutonoma type.
In order to solve this problem you have to create and use your own normalizer so that you can transform this integer to the corresponding entity object from your database. Something like this:
class EntityNormalizer extends ObjectNormalizer
{
/**
* Entity manager
* #var EntityManagerInterface
*/
protected $em;
public function __construct(
EntityManagerInterface $em,
?ClassMetadataFactoryInterface $classMetadataFactory = null,
?NameConverterInterface $nameConverter = null,
?PropertyAccessorInterface $propertyAccessor = null,
?PropertyTypeExtractorInterface $propertyTypeExtractor = null
) {
parent::__construct($classMetadataFactory, $nameConverter, $propertyAccessor, $propertyTypeExtractor);
// Entity manager
$this->em = $em;
}
public function supportsDenormalization($data, $type, $format = null)
{
return strpos($type, 'App\\Entity\\') === 0 && (is_numeric($data) || is_string($data));
}
public function denormalize($data, $class, $format = null, array $context = [])
{
return $this->em->find($class, $data);
}
}
What this normalizer does is that it checks if your data type (in this case $ccaa) is type of an entity and if the data value provided (in this case 7) is an integer, it transforms this integer to the corresponding entity object from your database (if existing).
To get this normalizer working you should also register it in your services.yaml configuration, with the appropriate tags like this:
services:
App\Normalizer\EntityNormalizer:
public: false
autowire: true
autoconfigure: true
tags:
- { name: serializer.normalizer }
You could also set the normalizer's priority but since the default priority value is equal to 0 when Symfony's built-in normalizers' priority is by default negative, your normalizer will be used first.
You could check a fully explained example of this in this fine article.

Symfony3 - Serializing nested entities

So I have a couple doctrine entities, a Subscription and a Subscriber. There are many Subscriptions to a single subscriber (manyToOne). I wrote custom normalizers for both entities, but am having trouble getting the Subscriber to show up in the Subscription once it has been normalized to JSON.
The only way I've been able to get it to work is by passing the 'Subscriber' normalizer to the 'Subscription' normailizer. It seems like I should just be able to use the SerializerAwareNormalizer Trait, or something like that, to have Symfony recursively normalize my related entities.
services:
acme.marketing.api.normalizer.subscription:
class: acme\MarketingBundle\Normalizer\SubscriptionNormalizer
arguments: ['#acme.marketing.api.normalizer.subscriber']
public: false
tags:
- { name: serializer.normalizer }
acme.marketing.api.normalizer.subscriber:
class: acme\MarketingBundle\Normalizer\SubscriberNormalizer
public: false
tags:
- { name: serializer.normalizer }
and the normalizer...
<?php
namespace acme\MarketingBundle\Normalizer;
use acme\MarketingBundle\Entity\Subscription;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class SubscriptionNormalizer implements NormalizerInterface
{
private $subscriberNormalizer;
public function __construct($subscriberNormalizer)
{
$this->subscriberNormalizer = $subscriberNormalizer;
}
public function normalize($subscription, $format = null, array $context = [])
{
/* #var $subscription Subscription */
$subscriber = $subscription->getSubscriber();
return [
"id" => $subscription->getId(),
"subscriber" => $this->subscriberNormalizer->normalize($subscriber, $format)
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Subscription;
}
}
Is there a better way to accomplish this?
Spent a few hours on google and couldn't figure it out. Post on SO and 5 minutes later hit the right google link :(. Answer seems to be to implement NormalizerAwareInterface on the custom normalizer, and then use the NormalizerAwareTrait to get access to the normalizer for nested entities.
<?php
namespace acme\MarketingBundle\Normalizer;
use acme\MarketingBundle\Entity\Subscription;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class SubscriptionNormalizer implements NormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
public function normalize($subscription, $format = null, array $context = [])
{
return [
"id" => $subscription->getId(),
"subscriber" => $this->normalizer->normalize($subscription->getSubscriber())
];
}
public function supportsNormalization($data, $format = null)
{
return $data instanceof Subscription;
}
}

Symfony custom form weird property access errors

I've got this strange problem, here is example usage of my custom ThingType class.
->add('photos', 'namespace\Form\Type\ThingType', [
'required' => false,
])
if the field name is photos everything works as expected, but if I change my entity field to let's say photosi, run generate entities, and change the form field name, this error is thrown:
Neither the property "photosi" nor one of the methods
"addPhotosus()"/"removePhotosus()", "setPhotosi()", "photosi()",
"__set()" or "__call()" exist and have public access in class
"AppBundle\Entity\Product".
I guess the problem comes from Symfony trying to generate getter method name for my entity. Why is this addPhotosus method name generated? How can I solve this?
EDIT:
I'm using model transformer when showing the data to the user.
$builder->addModelTransformer(new CallbackTransformer(
function ($imagesAsText) {
if (!$imagesAsText) {
return null;
}
$newImages = [];
foreach($imagesAsText as $img) {
$newImages[] = $img->getID();
}
return implode(',', $newImages);
},
function ($textAsImages) use ($repo) {
$images = [];
foreach(explode(',', $textAsImages) as $imgID) {
$img = $repo->findOneById($imgID);
if ($img) {
$images[] = $img;
}
}
return $images;
}
));
The actual field is TextType::class with entity ids in it for example 1,10,32,51. The model transformer transforms this data to entities. Setting 'data_class' to my form type seems irrelevant, because the actual form type is a part of entity. I mean I have Product entity and Photo entity, photos is array of photo entity. So in my ThingType, what data_class should I use, photo or product?
Thanks
The fist parameter of the add method for a form, should be one of the mapped attributes of the data_class of the form, usually selected inside the form as
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Product'
));
}
That isn't related to the form name. So , you are trying to access to a "photosi" attribute inside your Product class.
Hope this help you.
Ok so for the first point you need to remember that Symfony is looking for setXX() and getXX()method in your entity for each entry of your form.
If you change your variable name you need to update the form :
->add('newName', XXType::class, [
'required' => false,
])
and you're entity by changing the variable
class Entity
{
/**
* #ORM\Column(type="string", length=255)
*/
private $newName;
public function getOldName(){
return $this->$oldName;
}
public function setOldName(oldName){
$this->oldName = $oldName;
return $this
}
}
then run the command
php bin/console make:entity --regenerate
and symfony will upload your entity by itself
class Entity
{
/**
* #ORM\Column(type="string", length=255)
* #SerializedName("title")
* #Groups({"calendar"})
*/
private $newName;
public function getOldName(){
return $this->$oldName;
}
public function setOldName($oldName){
$this->oldName = $oldName;
return $this
}
public function getNewName(){
return $this->newName;
}
public function setNewName($newName){
$this->newName = $newName;
return $this
}
note that the old get and set method are not deleted by the script
note as well that in your specific case of photosi, symfonyguess that the "i" is a plural mark and look for addPhotosus() methods
For the edit it looks very unclear and has nothing to do with the first question. Consider reading : doc on collectionType

Resources