Embed Form and EntityType multiple - symfony

I have embed form, inside embed form there is an EntityType with multiple=> true.
When i am saving to databse i get this: Doctrine\Common\Collections\ArrayCollection#000000...
When i change EntityType to CHoiceType with multiple=> true everything seems to be correct.
/**
* Set emotions
*
* #param simple_array $emotions
*
* #return Movies
*/
public function setEmotions($emotions) {
$this->emotions = $emotions;
return $this;
}
FormType
->add('emotions', EntityType::class, array(
'class' => 'MovieBundle:Emotions',
'multiple' => true,
'choice_label' => 'emotion',
))
I have no idea where the problem is. Is it something with embed form, or i need data transformer?

Whene you set multiple to true symfony set your attribute value into an array, if you set multiple to false this attribute will be set like it is so the difference between them is :
multiple: true gonna call a setter in your class this setter should have an array argument like setEmotions(ArrayCollection $emotions), and it will be persisted one by one using a loop.
multiple : false gonna call a setter in your class with an argument of type Emotion like setEmotions(Emotion $emotion) and it will be persisted only the object that he get from getEmotions() in this case he will persist only the DoctrineArrayCollection not the objects wrapped in it and this is the reason of your exception

Related

Symfony 5 / Easy Admin 3 - FormBuilder added field not displaying appropiate input

I am building a form using Easy Admin's FormBuilder. My goal is to have an AssociationField which represents a OneToMany relationship, for example, to assign multiple products to a shop. Additionally, I only want some filtered products to be listed, so I overrode the createEditFormBuilder method in the CrudController, I used this question as reference, and this is the code for the overridden function :
public function createEditFormBuilder(EntityDto $entityDto, KeyValueStore $formOptions, AdminContext $context): FormBuilderInterface
{
$formBuilder = parent::createEditFormBuilder($entityDto, $formOptions, $context);
$filteredProducts = $context->getEntity()->getInstance()->getFilteredProducts();
$formBuilder->add('products', EntityType::class, ['class' => 'App\Entity\Product', 'choices' => $filteredProducts, 'multiple' => true]);
return $formBuilder;
}
I expected an Association field as the ones configured in the configureFields() function, however, the displayed field doesn't allow text search or autocomplete features, plus has incorrect height.
Expected:
Actual:
I tried to change the second argument in the $formBuilder->Add() function, but all specific EasyAdmin types threw errors.
UPDATE: I also tried using EasyAdmin's CrudFormType instead of EntityType, which doesn't support the 'choice' parameter. Still, the result was the same.
There is setQueryBuilder on the field, you can use it for filtering entities like this
<?php
// ...
public function configureFields(string $pageName): iterable
{
// ...
yield new AssociationField::new('products')->setQueryBuilder(function($queryBuilder) {
$queryBuilder
->andWhere('entity.id IN (1,2,3)')
;
})
;
// ...
}

ChoiceType multiple attribute according to entity property (how to choose between returning a collection of entities or one entity)

I'm working on a quiz project with the Symfony framework (version 4.4) and Doctrine as ORM.
There is a ManyToOne relation between the Answer and the Question entities, as for the QuizQuestion and Answer entities. I use the QuizQuestion entity to make the link between a quiz, a question, and the selected answer(s).
I use a EntityType "QuizQuestionType" with the multiple attribute set to true to collect answers, and it works as expected :
$builder
->add('answers', EntityType::class, [
'class' => Answer::class,
'choices' => $this->fillAnswers($quizQuestion),
'expanded' => true,
'multiple' => true,
]);
The thing is, I want to be able to setup question as multiple or single choice. If I set the EntityType multiple attibute to false, I got the error :
Entity of type "Doctrine\ORM\PersistentCollection" passed to the
choice field must be managed. Maybe you forget to persist it in the
entity manager?
I could use two answers entities with a OneToMany and a OneToOne relations, but it seems a really poor design to me.
I wonder how it can be done, ideally with a property in the Question entity that indicates if it is a multiple or unique choice question. That will allow me to simply declare it in the backend (because technically, a multiple choice question may have only one good answer, so I can't calculate it by the number of answers).
Do you have any idea on how I can achieve this ?
Here is the conceptual data model :
CDM
The answer entity : https://pastebin.com/kiRTHnvL
The QuizQuestion entity : https://pastebin.com/wL3v9fwT
Thank you for your help,
EDIT 01/08/2020
As suggested by #victor-vasiloi, I added an event listener to the form type so I can setup the correct extensions. I was not able to add the transformer though. I found the solution here and created an extension to use a data transformer from the event listener :
QuizQuestionType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event) use ($builder){
$quizQuestion = $event->getData();
$form = $event->getForm();
if ($quizQuestion->getQuestion()->getIsMultiple()){
$form->add('answers', EntityType::class, [
'class' => Answer::class,
'choices' => $this->fillAnswers($quizQuestion),
'expanded' => true,
'multiple' => true,
]);
} else {
$form->add('answers', EntityType::class, [
'class' => Answer::class,
'choices' => $this->fillAnswers($quizQuestion),
'expanded' => true,
'multiple' => false,
'model_transformer' => new CollectionToAnswerTransformer(),
]);
}
})
;
}
ModelTransformerExtension
class ModelTransformerExtension extends AbstractTypeExtension
{
public static function getExtendedTypes(): iterable
{
// return FormType::class to modify (nearly) every field in the system
return [FormType::class];
}
public function buildForm(FormBuilderInterface $builder, array $options) {
parent::buildForm($builder, $options);
if (isset($options['model_transformer'])) {
$builder->addModelTransformer($options['model_transformer']);
}
}
public function configureOptions(OptionsResolver $resolver) {
parent::configureOptions($resolver);
$resolver->setDefaults(array('model_transformer' => null));
}
}
Now the form could be loaded. When submitting though (in a case of a unique answer with radio buttons), a CollectionToArrayTranformer was giving the following error :
Expected argument of type "App\Entity\Answer", "array" given at
property path "answers".
I tried a custom CollectionToAnswerTransformer, that looks like this :
class CollectionToAnswerTransformer implements DataTransformerInterface
{
/**
* #param mixed $collection
* #return mixed|string
*/
public function transform($collection)
{
if (null === $collection){
return '';
}
else
{
foreach ($collection as $answer){
return $answer;
}
}
}
/**
* #param mixed $answer
* #return ArrayCollection|mixed
*/
public function reverseTransform($answer)
{
$collection = new ArrayCollection();
$collection->add($answer);
return $collection;
}
}
But with no better results. I get the error :
Expected argument of type "App\Entity\Answer", "instance of
Doctrine\Common\Collections\ArrayCollection" given at property path
"answers".
It looks like an issue with the reverse transformer method, but if I change it to return an entity, I got the opposite error :
Could not determine access type for property "answers" in class
"App\Entity\QuizQuestion": The property "answers" in class
"App\Entity\QuizQuestion" can be defined with the methods
"addAnswer()", "removeAnswer()" but the new value must be an array or
an instance of \Traversable, "App\Entity\Answer" given...
I think I'm almost at it, but I don't know if my transformer is the way to go or if it is easier than that...
To setup questions with single choice you could use a radio button, and checkboxes for multiple choices.
Radio button is expanded "true" and multiple "false".
Checkbox is expanded "true" and multiple "true".
Code example that display checkboxes:
$builder
->add('filter', EntityType::class, array(
'class' => 'FilterBundle:Filter',
'multiple' => true,
'expanded' => true,
'required' => true
));
Source: https://symfony.com/doc/current/reference/forms/types/choice.html#select-tag-checkboxes-or-radio-buttons
And if you want to define it for each question before displaying, there could be a field on your question entity (for example a boolean "multiple").
You can dynamically set the multiple option based on the given Question using a form event listener on the Symfony\Component\Form\FormEvents::PRE_SET_DATA event, here's where you can learn more about dynamically modifying the form and form events.
Using the same logic, when the multiple option is set to true, you can add the Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer model transformer to the answers field like this $builder->get('answers')->addModelTransformer(new CollectionToArrayTransformer()); which will ensure the transformation between the Doctrine Collection and the choices array (including single choice).

Symfony Form - sorting data by changing sort option in dropdown list

I'm sorry for the title but I don't know how to define it good. I'd like my app to sort data (in Doctrine) depending on the sort option which user select it in form dropdown list.
The data I mentioned above are stored inside my Doctrine Entity which I called it Flashcards and the Flashcards Entity contains properties which them must be sorted by option that user select in dropdown. Flashcards Entity looks like the following (I gave only a few properties for simplicity):
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Words", inversedBy="flashcards")
* #ORM\JoinColumn(nullable=false)
*/
private $words;
/**
* #ORM\Column(type="datetimetz")
*/
private $creation_date;
Now the controller's code for form is usual, like the Symfony Doc say:
// code for form inside FlashcardController
$flashcard = new Flashcards();
$form = $this->createForm(FlashcardType::class, $flashcard);
And inside FlashcardController render() method I call createView() method on $form object.
The form code is placed inside FlashcardType class and contains code for mentioned above dropdown list and its options. It looks like this (for simplicity I gave only the methods):
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('sortBy', ChoiceType::class, [
'label' => 'Sort by',
'choices' => [
'Date increase' => 1,
'Date decrease' => 2,
'Word alphabetically' => 3,
'Word not alphabetically' => 4
]
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Flashcards::class
]);
}
So as you see the dropdown options include sorting by: date increase, date decrease, word alphabetically, word not alphabetically. User choose one of them in view. So manipulating data by user does NOT change value of this data represented by properties. Changing sort option in dropdown should change Doctrine query to sort by user-changed value. I have no idea how to achieve this in Symfony. Could you give me plese some tips how to make it?
Thank you in advance for answers!
If I understood correctly you have a few options:
First: wire javascript / jquery to select input and submit the form when user changes order(it should reload the form applying the filter, you can set any data user used in the 'previous' page using RequestStack to get the form fields from request / query depending on the method)
Second: get all the nodes you display record and try to manipulate the order based on the select value every time user changes the value
Tell me if this is what you meant else correct me and I'll try to provide the answer

Add custom validator on unmapped field, but with context of whole form submission?

tl;dr:
I need a custom validation constraint to run on an unmapped form field
I need the ID of the object the form is manipulating to eliminate it from consideration doing my validation constraint
Attaching the validation to the form itself or the unmapped field doesn't give me enough context to run my validation query
I have an unmapped field on my Person entity form that I need to run a validation on. I've followed this great article on how to do this, but my use case is slightly different and not entirely covered by the article.
I am making my own Unique Constraint that needs to run a custom query to determine the uniqueness. To run the query, I need access to the field value that was submitted, as well as the original Person object (so I can get it's ID if it's an update operation). Without also having the that Person object I won't be able to eliminate it from consideration during the uniqueness query.
If I apply the validator on the PersonType class like so:
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver
->setDefaults(array(
'data_class' => 'CS\AcmeBundle\Entity\Person',
'constraints' => array(
new MyUniqueValidator(),
)
))
;
}
Then the validator gets passed the entire Person object to perform the validation on. This doesn't help me, because the submitted form data is not persisted to the Person object (it's an unmapped field that I handle after $form->isValid() is called in the controller).
If I apply the validator to the unmapped field directly instead:
$builder
->add('myUnmappedField', 'text', array(
'mapped' => false,
'constraints' => array(
new MyUniqueValidator(),
)
),
))
Then the object I get passed to the validator is just the standalone form text, and nothing else. I don't have the ID Person object (if it was an update operation) to perform by uniqueness query.
Hopefully I've explained this properly. Do I have any options to do this sort of validation gracefully?
You say you have unmapped field. Would it help, if you make it mapped to the Person entity? Just make a new property in the Person class with getter and setter methods, but not to the ORM, since you don't want it persisted.
If you do not want to polute your Person class, you can also make another composite class, which will hold your currently unmapped field and a Person object (you will then make it mapped). Ofcourse you will then set data_class to match the new object's namespace.
Both above solutions should work with the upper code you have there. Please let me know it it helped.
You can achieve this by using a callback constraint with a closure.
To access your Person entity, you will need to add the field via an event listener.
$builder->addEventListener(FormEvents::POST_SET_DATA, function (FormEvent $event) {
$form = $event->getForm();
$person = $event->getData();
$form->add('myUnmappedField', TextType::class, [
'mapped' => false,
'constraints' => [
new Symfony\Component\Validator\Constraints\Callback([
'callback' => function ($value, ExecutionContextInterface $context) use ($person) {
// Here you can use $person->getId()
// $value is the value of the unmapped field
}
])
],
]);
});

Choice Multiple = true, create new Entries

My Entity
/**
* Set friend
*
* #param \Frontend\ChancesBundle\Entity\UserFriends $friend
* #return ChanceRequest
*/
public function setFriend(\Frontend\ChancesBundle\Entity\UserFriends $friend = null)
{
$this->friend = $friend;
return $this;
}
My Action
$task = new ChanceRequest();
$form = $this->createFormBuilder($task)
->add('friend', 'choice', array(
'required' => true,
'expanded' => true,
'choices' => $fb_friends,
'multiple' => true,
'mapped' => true
))
->getForm();
Because setFriend is expecting a scalar, I cannot validate this or save it to db. It is an array from how many friends the user want to send a message to somebody. How can I change it?
I have seen here a post:
Symfony2 Choice : Expected argument of type "scalar", "array" given
but this don't work that I put an array in front of \Frontend or $friend. I guess because of the related table.
What do I have to do in Entity to get it work?
If friends could be found in your database (for example it is a User entity), you should declare ManyToMany relationship for these two tables. If not, make friend property to be a doctrine array type. if friends is not a valid entity class, all the rest you have is to make a custom validator and datatransformer. Otherwise you have nothing left to do. All this information you can find in the official symfony and doctrine documentation.
How to create a Custom Validation Constraint
How to use Data Transformers
Association Mapping
Working with Associations

Resources