Symfony 5 form handle request is executing the same query twice - php

So I'm starting a new Symfony 5 application, and having some trouble in a editing controller, imho, symfony is executing a dumb query.
So, below is the code of my controller and where is executing duplicate query and the queries. If anyone could help me out, would be very thankfull, If more code is needed, please let me know.
The repeated query, just occurs when submiting the request, not getting it. So just in the POST method.
EDIT: So... I found out, something that is causing, in formtype 'by_reference'=> false, but I want to use it. So I could solve it in a ugly way, doing:
$product = $this->productRepository->find($id);
$product->getImages()->initialize();
It looks like, if I pre load entity, it works fine, maybe because of something that by reference does, not take from entity cache, like the example in docs:
From symfony docs: https://symfony.com/doc/current/reference/forms/types/collection.html
// If you set by_reference to false, submitting looks like this:
$article->setTitle('...');
$author = clone $article->getAuthor();
$author->setName('...');
$author->setEmail('...');
$article->setAuthor($author);
class ProductController extends AbstractController
{
public function editProduct(Request $request, int $id)
{
// Here it executes the query for getting the product, its ok
$product = $this->productRepository->find($id);
// Here it executes the query of the images, for handling the forms
$form = $this->createForm(ProductType::class, $product);
// Here is the problem, it executes the same query AGAIN!
$form->handleRequest($request);
}
}
So, I don't know why when I call $form->handleRequest(), It executes a query, that already runned, bellow is some usefull code, that maybe I mess up.
Query runned twice:
SELECT t0.id AS id_1, t0.image AS image_2, t0.product_id AS product_id_3 FROM product_image t0 WHERE t0.product_id = ?
class ProductType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('images', CollectionType::class, [
'label' => false,
'entry_type' => ImageType::class,
'entry_options' => [
'label' => false,
],
'prototype' => true,
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'required' => false,
'attr' => ['class' => 'row text-center text-lg-left'],
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Product::class,
]);
}
class: App\Entity\Catalog\Product
/**
* #ORM\Entity(repositoryClass=ProductRepository::class)
*/
class Product
{
/**
* #ORM\OneToMany(targetEntity="ProductImage", mappedBy="product", cascade={"persist"}, orphanRemoval=true)
*/
private $images;
/**
* #return ProductImage[]|ArrayCollection
*/
public function getImages()
{
return $this->images;
}
/**
* #param ProductImage $productImage
* #return Product
*/
public function addImage(ProductImage $productImage): self
{
$productImage->setProduct($this);
$this->images->add($productImage);
return $this;
}
/**
* #param ProductImage $productImage
* #return $this
*/
public function removeImage(ProductImage $productImage): self
{
$productImage->setProduct(null);
$this->images->removeElement($productImage);
return $this;
}
}
class: App\Entity\Catalog\ProductImage
/**
* #ORM\Entity(repositoryClass=ProductImageRepository::class)
*/
class ProductImage
{
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="images")
* #ORM\JoinColumn(name="product_id", onDelete="CASCADE")
*/
private $product;
}

Related

Doctrine Array type field not being updated

My question is more or less the same as this one:
How to force Doctrine to update array type fields?
but for some reason the solution didn't work for me, so there must be a detail I am missing and I would be happy if someone could point it out to me.
The context is a Symfony 5.2 app with Doctrine ^2.7 being used.
Entity-Class-Excerpt:
class MyEntity {
// some properties
/**
* #var string[]
* #Groups("read")
* #ORM\Column(type="array")
* #Assert\Valid
*/
protected array $abbreviations = [];
public function getAbbreviations(): ?array
{
return $this->abbreviations;
}
//this is pretty much the same set-function as in the question I referenced
public function setAbbreviations(array $abbreviations)
{
if (!empty($abbreviations) && $abbreviations === $this->abbreviations) {
reset($abbreviations);
$abbreviations[key($abbreviations)] = clone current($abbreviations);
}
$this->abbreviations = $abbreviations;
}
public function addAbbreviation(LocalizableStringEmbeddable $abbreviation): self
{
foreach ($this->abbreviations as $existingAbbreviation) {
if ($abbreviation->equals($abbreviation)) {
return $this;
}
}
$this->abbreviations[] = $abbreviation;
return $this;
}
public function removeAbbreviation(LocalizableStringEmbeddable $abbreviation): self
{
foreach ($this->abbreviations as $i => $existingAbbreviation) {
if ($abbreviation->equals($existingAbbreviation)) {
array_splice($this->abbreviations, $i, 1);
return $this;
}
}
return $this;
}
}
But none of these methods are ever being called (I also tried removing add-/removeAbbreviation only leaving get/set in place).
LocalizableStringEmbeddable being an Embeddable like this:
* #ORM\Embeddable
*/
class LocalizableStringEmbeddable
{
/**
* #var string|null
* #Groups("read")
* #ORM\Column(type="string", nullable=false)
*/
private ?string $text;
/**
* #var string|null
* #Groups("read")
* #ORM\Column(type="string", nullable=true)
* #Assert\Language
*/
private ?string $language;
//getters/setters/equals/#Assert\Callback
}
By using dd(...) I can furthermore say that in my controller on submit
$myEntity = $form->getData();
yields a correctly filled MyEntity with an updated array but my subsequent call to
$entityManager->persist($myEntity);
$entityManager->flush();
doesn't change the database.
What am I missing?
EDIT: I was asked to give information about the Type I use. It is a custom one that is based on this class. So technically at the base of things I am using a collection type.
abstract class AbstractLocalizableUnicodeStringArrayType extends AbstractType implements DataMapperInterface
{
abstract public function getDataClassName(): string;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('items', CollectionType::class, [
'entry_type' => LocalizedStringEmbeddableType::class,
'entry_options' => [
'data_class' => $this->getDataClassName(),
'attr' => ['class' => 'usa-item-list--item-box'],
],
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
])
;
$builder->setDataMapper($this);
}
public function mapDataToForms($viewData, iterable $forms): void
{
$forms = iterator_to_array($forms);
if (null === $viewData) {
return;
}
if (!is_array($viewData)) {
throw new UnexpectedTypeException($viewData, "array");
}
$forms['items']->setData($viewData);
}
public function mapFormsToData(iterable $forms, &$viewData): void
{
$forms = iterator_to_array($forms);
$viewData = $forms['items']->getData();
}
}
As I found out this can be solved (in an unelegant way in my opinion but until something better comes around I want to state this) by combining two answers from another post on SO:
https://stackoverflow.com/a/13231876/6294605
https://stackoverflow.com/a/59898632/6294605
This means combining a preflush-hook in your entity class with a "fake-change" of the array in question.
Remark that doing the fake-change in a setter/adder/remover didn't work for me as those are not being called when editing an existing entity. In this case only setters of the changed objects inside the array will be called thus making Doctrine not recognize there was a change to the array itself as no deep-check seems to be made.
Another thing that was not stated in the other thread I wanna point out:
don't forget to annotate your entity class with
#ORM\HasLifecycleCallbacks
or else your preflush-hook will not be executed.

Testing Form symfony with construct

The problem was too old version of PHPunit
/////
Now I have other problem.I have that test:
class TodoTypeTest extends TypeTestCase
{
private $em;
protected function setUp()
{
$this->em = $this->createMock(EntityManager::class);
parent::setUp();
}
protected function getExtensions()
{
return array(
new PreloadedExtension([
new TodoType($this->em)
], [])
);
}
public function testTodoType()
{
$task = new Todo();
$form = $this->factory->create(TodoType::class, $task, ['locale' => 'en']);
}
}
I get this problem:
Error: Call to a member function getPrioritysInUserLocaleToForm() on null
Problem is here in TodoType class:
class TodoType extends AbstractType
{
/**
* #var EntityManagerInterface
*/
private $em;
/**
* TodoType constructor.
*
* #param EntityManagerInterface $em
*/
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('title', Type\TextType::class)
->add('content', Type\TextareaType::class)
->add('priority', Type\ChoiceType::class, [ 'choices' => $this->addChoicesInUserLocale($options['locale']) ])
->add('dueDate', Type\DateTimeType::class, [
'widget' => 'single_text',
'attr' => ['class' => 'js-datepicker'],
'html5' => false,
]);
}
/**
* Configure defaults options
*
* #param OptionsResolver $resolver
*/
public function configureOptions( OptionsResolver $resolver )
{
$resolver->setDefaults( [
'locale' => 'en',
] );
}
/**
* Method adds array with choices to ChoiceType in builder
*
* #param string $locale User's locale
*
* #return array All priority in user _locale formatted as array e.g. ['1' => 'low', ...]
*/
private function addChoicesInUserLocale(string $locale): array
{
return $this->em->getRepository('AppBundle:Priority')
->getPrioritysInUserLocaleToForm($locale);
}
}
I do not know why it is not working :/
As the error message tells you, you are trying to call a method on null in this part of the code:
return $this->em->getRepository('AppBundle:Priority')
->getPrioritysInUserLocaleToForm($locale);
That message tells you, that getRepository() returns null.
This happens because the EntityManager you use is a mock, which will always return null for all methods unless specified otherwise. You can fix this by making it return an EntityRepository instead. You can do this in your test method:
public function testTodoType()
{
$repoMock = $this->createMock(PriorityRepository::class);
$repoMock->expects($this->any())
->method('getPrioritysInUserLocaleToForm')
->with('en')
->willReturn([]); // Whatever you want it to return
$this->em->expects($this->any())
->method('getRepository')
->with('AppBundle:Priority')
->willReturn($repoMock);
$task = new Todo();
$form = $this->factory->create(TodoType::class, $task, ['locale' => 'en']);
}
This will make the EntityManager return your Repository-mock which then in turn returns whatever values you want. The expects() calls are assertions and since your test does not have any yet, you might want to check that the repository method is called using $this->atLeastOnce() instead of $this->any(). In any case for the test to be useful you have to make assertions at some point.

Symfony entity field : manyToMany with multiple = false - field not populated correctly

I am using symfony2 with doctrine 2.
I have a many to many relationship between two entities :
/**
* #ORM\ManyToMany(targetEntity="\AppBundle\Entity\Social\PostCategory", inversedBy="posts")
* #ORM\JoinTable(
* name="post_postcategory",
* joinColumns={#ORM\JoinColumn(name="postId", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="postCategoryId", referencedColumnName="id", onDelete="CASCADE")}
* )
*/
private $postCategories;
Now I want to let the user only select one category. For this I use the option 'multiple' => false in my form.
My form:
->add('postCategories', 'entity', array(
'label'=> 'Catégorie',
'required' => true,
'empty_data' => false,
'empty_value' => 'Sélectionnez une catégorie',
'class' => 'AppBundle\Entity\Social\PostCategory',
'multiple' => false,
'by_reference' => false,
'query_builder' => $queryBuilder,
'position' => array('before' => 'name'),
'attr' => array(
'data-toggle'=>"tooltip",
'data-placement'=>"top",
'title'=>"Choisissez la catégorie dans laquelle publier le feedback",
)))
This first gave me errors when saving and I had to change the setter as following :
/**
* #param \AppBundle\Entity\Social\PostCategory $postCategories
*
* #return Post
*/
public function setPostCategories($postCategories)
{
if (is_array($postCategories) || $postCategories instanceof Collection)
{
/** #var PostCategory $postCategory */
foreach ($postCategories as $postCategory)
{
$this->addPostCategory($postCategory);
}
}
else
{
$this->addPostCategory($postCategories);
}
return $this;
}
/**
* Add postCategory
*
* #param \AppBundle\Entity\Social\PostCategory $postCategory
*
* #return Post
*/
public function addPostCategory(\AppBundle\Entity\Social\PostCategory $postCategory)
{
$postCategory->addPost($this);
$this->postCategories[] = $postCategory;
return $this;
}
/**
* Remove postCategory
*
* #param \AppBundle\Entity\Social\PostCategory $postCategory
*/
public function removePostCategory(\AppBundle\Entity\Social\PostCategory $postCategory)
{
$this->postCategories->removeElement($postCategory);
}
/**
* Get postCategories
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getPostCategories()
{
return $this->postCategories;
}
/**
* Constructor
* #param null $user
*/
public function __construct($user = null)
{
$this->postCategories = new \Doctrine\Common\Collections\ArrayCollection();
}
Now, when editing a post, I also have an issue because it uses a getter which ouputs a collection, not a single entity, and my category field is not filled correctly.
/**
* Get postCategories
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getPostCategories()
{
return $this->postCategories;
}
It's working if I set 'multiple' => true but I don't want this, I want the user to only select one category and I don't want to only constraint this with asserts.
Of course there are cases when I want to let the user select many fields so I want to keep the manyToMany relationship.
What can I do ?
If you want to set the multiple option to false when adding to a ManyToMany collection, you can use a "fake" property on the entity by creating a couple of new getters and setters, and updating your form-building code.
(Interestingly, I saw this problem on my project only after upgrading to Symfony 2.7, which is what forced me to devise this solution.)
Here's an example using your entities. The example assumes you want validation (as that's slightly complicated, so makes this answer hopefully more useful to others!)
Add the following to your Post class:
public function setSingleCategory(PostCategory $category = null)
{
// When binding invalid data, this may be null
// But it'll be caught later by the constraint set up in the form builder
// So that's okay!
if (!$category) {
return;
}
$this->postCategories->add($category);
}
// Which one should it use for pre-filling the form's default data?
// That's defined by this getter. I think you probably just want the first?
public function getSingleCategory()
{
return $this->postCategories->first();
}
And now change this line in your form:
->add('postCategories', 'entity', array(
to be
->add('singleCategory', 'entity', array(
'constraints' => [
new NotNull(),
],
i.e. we've changed the field it references, and also added some inline validation - you can't set up validation via annotations as there is no property called singleCategory on your class, only some methods using that phrase.
You can setup you form type to not to use PostCategory by reference (set by_reference option to false)
This will force symfony forms to use addPostCategory and removePostCategory instead of setPostCategories.
UPD
1) You are mixing working with plain array and ArrayCollection. Choose one strategy. Getter will always output an ArrayCollection, because it should do so. If you want to force it to be plain array add ->toArray() method to getter
2) Also I understand that choice with multiple=false return an entity, while multiple=true return array independend of mapped relation (*toMany, or *toOne). So just try to remove setter from class and use only adder and remover if you want similar behavior on different cases.
/** #var ArrayCollection|PostCategory[] */
private $postCategories;
public function __construct()
{
$this->postCategories = new ArrayCollection();
}
public function addPostCategory(PostCategory $postCategory)
{
if (!$this->postCategories->contains($postCategory) {
$postCategory->addPost($this);
$this->postCategories->add($postCategory);
}
}
public function removePostCategory(PostCategory $postCategory)
{
if ($this->postCategories->contains($postCategory) {
$postCategory->removePost($this);
$this->postCategories->add($postCategory);
}
}
/**
* #return ArrayCollection|PostCategory[]
*/
public function getPostCategories()
{
return $this->postCategories;
}
In my case, the reason was that Doctrine does not have relation One-To-Many, Unidirectional with Join Table. In Documentations example is show haw we can do this caind of relation by ManyToMany (adding flag unique=true on second column).
This way is ok but Form component mixes himself.
Solution is to change geters and seters in entity class... even those generated automatically.
Here is my case (I hope someone will need it). Assumption: classic One-To-Many relation, Unidirectional with Join Table
Entity class:
/**
* #ORM\ManyToMany(targetEntity="B2B\AdminBundle\Entity\DictionaryValues")
* #ORM\JoinTable(
* name="users_responsibility",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="responsibility_id", referencedColumnName="id", unique=true, onDelete="CASCADE")}
* )
*/
private $responsibility;
/**
* Constructor
*/
public function __construct()
{
$this->responsibility = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add responsibility
*
* #param \B2B\AdminBundle\Entity\DictionaryValues $responsibility
*
* #return User
*/
public function setResponsibility(\B2B\AdminBundle\Entity\DictionaryValues $responsibility = null)
{
if(count($this->responsibility) > 0){
foreach($this->responsibility as $item){
$this->removeResponsibility($item);
}
}
$this->responsibility[] = $responsibility;
return $this;
}
/**
* Remove responsibility
*
* #param \B2B\AdminBundle\Entity\DictionaryValues $responsibility
*/
public function removeResponsibility(\B2B\AdminBundle\Entity\DictionaryValues $responsibility)
{
$this->responsibility->removeElement($responsibility);
}
/**
* Get responsibility
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getResponsibility()
{
return $this->responsibility->first();
}
Form:
->add('responsibility', EntityType::class,
array(
'required' => false,
'label' => 'Obszar odpowiedzialności:',
'class' => DictionaryValues::class,
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('n')
->where('n.parent = 2')
->orderBy('n.id', 'ASC');
},
'choice_label' => 'value',
'placeholder' => 'Wybierz',
'multiple' => false,
'constraints' => array(
new NotBlank()
)
)
)
I know its a pretty old question, but the problem is still valid today.
Using a simple inline data transformer did the trick for me.
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$builder->add('profileTypes', EntityType::class, [
'multiple' => false,
'expanded' => true,
'class' => ProfileType::class,
]);
// data transformer so profileTypes does work with multiple => false
$builder->get('profileTypes')
->addModelTransformer(new CallbackTransformer(
// return first item from collection
fn ($data) => $data instanceof Collection && $data->count() ? $data->first() : $data,
// convert single ProfileType into collection
fn ($data) => $data && $data instanceof ProfileType ? new ArrayCollection([$data]) : $data
));
}
PS: Array functions are available in PHP 7.4 and above.

Symfony2 parameter with embedded forms

EDIT : I find the real problem!
I am trying to give a paramEter to a sub-form from the controller. Without this parameter, the form is working perfectly.
I want to show in the select list only users which are not already present in the relation. I have this query_builder:
'query_builder' => function(UserRepository $er) use($options) {
return $er->getFormateursAvailable($options['categ']);
},
And the method:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => 'Intranet\FormationBundle\Entity\CategorieFormateur', 'categ' => false));
//$resolver->setDefaults(array('data_class' => 'Intranet\FormationBundle\Entity\CategorieFormateur'));
}
For the collection form, so, I have to put this option in the form:
'options' => $options,
But I don't know if it is true, and I have to define the method:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('categ' => false));
}
(The form is working without this method if there is no parameter.)
And calling:
$form = $this->createForm(new GererFormateurCategorieType(), $categorie, array('categ' => $categorie));
And then, I have this error:
Neither the property "user" nor one of the methods "getUser()", "isUser()", "hasUser()", "__get()" exist and have public access in class "Intranet\FormationBundle\Entity\Categorie".
The relation:
Categorie has this property:
/**
* #ORM\OneToMany(targetEntity="Intranet\FormationBundle\Entity\CategorieFormateur", mappedBy="categorie", cascade={"persist", "remove"}, orphanRemoval=true)
**/
private $formateurs;
With addFormateur, removeFormateur and getFormateurs
CategorieFormateur :
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Intranet\FormationBundle\Entity\Categorie", inversedBy="formateurs")
* #ORM\JoinColumn(name="categorie_id", referencedColumnName="id")
*/
private $categorie;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Intranet\UserBundle\Entity\User")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
with setters and getters for each properties.
According to the error message, you have to add the setFormateurs() method in your Categorie entity:
public function setFormateurs($formateurs)
{
$this->formateurs = $formateurs;
return $this;
}
I found an alternative method, wich is not the true answer :
In the repository, I use
$request = Request::createFromGlobals();
And I explode the $requestPathInfo() to get the last parameter and I use it in the query...
But the existing users in the relation are not loaded altough they not appers in the list.
I hope somebody knows how to do that properly !

Symfony2 Form FormType Itself collection

I'm trying to create a symfony 2 form 'PersonType', which has a PersonType collection field that should map a given person's children.
And I'm getting this error,
{"message":"unable to save order","code":400,"errors":["This form should not contain extra fields."]}
Here is my Person entity,
class Person
{
private $id;
/**
* #ORM\OneToMany(targetEntity="Person", mappedBy="parent", cascade={"persist"})
*/
private $children;
/**
* #ORM\ManyToOne(targetEntity="Person", inversedBy="children")
* #ORM\JoinColumn(name="orderitem_id", referencedColumnName="id", nullable=true)
*/
private $parent;
}
And my Type,
class PersonType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id')
->add('children', 'collection', array(
'type' => new PersonType()
))
;
}
UPDATE :
I've seen that the problem was because the option :
'allow_add' => true,
'by_reference' => false
wasn't in the Type, I've deleted it because when i insert them, the form don't appear and the page crash with no error.
I'm very confused because with this error, people can't have children :/
Does anyone already faced the same problem? (A formType nested over itself)
ACUTALLY :
I've duplicate my personType to PersonchildrenType to insert this last in the first...
I was having the same problem except that the error message was :
FatalErrorException: Error: Maximum function nesting level of 'MAX' reached, aborting!
Which is normal because "PersonType" is trying to build a form with a new "PersonType" field that is also trying to build a form with a new "PersonType" field and so on...
So the only way I managed to solve this problem, for the moment, is to proceed in two different steps :
Create the parent
Create a child and "link" it to the parent
You can simply do this in your controller
public function addAction(Person $parent=null){
$person = new Person();
$person->setParent($parent);
$request = $this->getRequest();
$form = $this->createForm(new PersonType(), $person);
if($this->getRequest()->getMethod() == 'POST'){
$form->bind($request);
if ($form->isValid()) {
// some code here
return $this->redirect($this->generateUrl('path_to_person_add', array(
'id' => $person->getId()
); //this redirect allows you to directly add a child to the new created person
}
}
//some code here
return $this->render('YourBundle::yourform.html.twig', array(
'form' => $form->createView()
));
}
I hope this can help you to solve your problem.
Tell me if you don't understand something or if I'm completly wrong ;)
Try to register your form as a service, like described here: http://symfony.com/doc/current/book/forms.html#defining-your-forms-as-services, and modify your form like this:
class PersonType extends AbstractType
{
public function getName()
{
return 'person_form';
}
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id')
->add('children', 'collection', array(
'type' => 'person_form',
'allow_add' => true,
'by_reference' => false
))
;
}
}

Categories