Symfony form post - add a new self-referenced ArrayCollection item returns empty entity - php

Situation
I have an entity Item that can has a collection of Items (preconditions) with the same class Item. I used this documentation for making a form with the collection type. I want to make almost the same like in the documentation. But my situation is:
My collecting items are self-referenced to the main item
I want to add already existing items from an existing list, not creating new ones
With prototyping, i'm adding a new choosed collection item with JS with the same HTML structure like the default repopulation from symfony's form does. But naturally with the replaced choosed relation ID.
Persisting the relations is working, but only with a manual hack, and with missing datas into the $form object.
Problems
When i add an relation, with js prototyping and post the data, a new entity with the correct Item class is added in the ArrayCollection. This new entity returns with the posted ID as key as ArrayCollection item, but an empty entity itself as value. (New one is with key 1 below)
#collection: ArrayCollection {#829 ▼
-elements: array:2 [▼
5 => Item {#834 ▼
#id: 5
-title: "foo"
+preconditions: PersistentCollection {#836 ▶}
}
1 => Item {#890 ▼
#id: null
-title: null
+preconditions: ArrayCollection {#886 ▶}
}
]
}
Problem 1: This results on persisting that an unknown new entity want to be inserted (Because entity with ID null doesn't exists in the DB), and then with invalid null datas. It must not be inserted because its actually already existing.
Problem 2: Before persist i must modify the ArrayCollection property by hand, invalid entities i have to refill. So the entries are valid for relation persisting.
Problem 3: When i modify the ArrayCollection this way, i need this datas to be accessable also in twig. And an update into $form will result an Exception that i can’t modify the $form after it was submitted. Then i saw here the suggestion of an event listener, but even with an event listener i cant’ update the $form because either $form is not prepared or is too late to update.
Questions
Is following the right way to perform my goal, or does exist a better variant?
How can the new entities in the ArrayCollection be already be filled correctly after a post?
Code
Controller:
public function newesiAction($id, Request $request)
{
$em = $this->getDoctrine()->getManager();
// For repopulating an existing entity
$item = 0 < $id ? $em->getRepository('AppBundle:Item')->find($id) : new Item();
$form = $this->createForm(new ItemType($this->container), $item);
$form->add('submit', 'submit', array('label' => 'speichern'));
$form->handleRequest($request);
/* After handleRequest, the posted (also new) precondition entities are given. But not as expected. So we update the array collection.
* Not as expected meant: New relation entities becomes new entities with NULL as id. So entities with NULL as id wantet to be INSERTED, what is not our goal.
*/
foreach($item->preconditions as $precondition_entity_id => $precondition_entity) {
$precondition_entity_inner_id = $precondition_entity->getId();
if(null === $precondition_entity_inner_id) {
$item->preconditions[$precondition_entity_id] = $em->getRepository('AppBundle:Item')->find($precondition_entity_id);
}
}
if ($form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($item);
$em->flush();
}
return $this->render('AppBundle:Item:edit.html.twig', array(
'form' => $form->createView(),
));
}
ItemType:
<?php
// src/AppBundle/Form/Type/ItemType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\Form\FormEvent;
class ItemType extends AbstractType
{
public function __construct($container) {
$this->container = $container;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title');
$builder->add('preconditions', 'collection', array(
'type' => new PreconditionType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
));
$builder->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) {
// $em = $this->container->get('doctrine')->getManager();
// $form = $event->getForm();
// // Replace the new related entity that have NULL in its ID, with an fully loaded entity
// $entity = $event->getData();
// foreach($entity->preconditions as $precondition_entity_id_posted => $possible_empty_entity) {
// $precondition_entity_id_loaded = $possible_empty_entity->getId();
// if(null === $precondition_entity_id_loaded) {
// $entity->preconditions[$precondition_entity_id_posted] = $em->getRepository('AppBundle:Item')->find($precondition_entity_id_posted);
// }
// }
// $form->get('preconditions')->setData($entity->preconditions);
});
$builder->add('available_items', 'entity', array(
'class' => 'AppBundle\Entity\Item',
'multiple' => true,
'expanded' => false,
'mapped' => false
));
$builder->add('add_item_precondition', 'button', array(
'attr' => array(
'onclick' => "addPreconditionForm('" . $this->getName() . "_available_items')"
)
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Item',
));
}
public function getName()
{
return 'item';
}
}
?>
Precondition type:
<?php
// src/AppBundle/Form/Type/PreconditionType.php
namespace AppBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class PreconditionType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('relation', 'hidden', array(
'mapped' => false,
'required' => false
));
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Item'
));
}
public function getName()
{
return 'precondition';
}
}
?>

I had the possibility to discuss this with an employee of sensiolabs. Its officially not possible to automate this with the built-in tools, you must loop the collection entries and re-get the incomplete entities.
So the answers to my questions:
Yes, it is the correct way go get the missing entities
No, its not possible to get those automatically

Related

is it possible to assign a value to a field added with EventListener symfony in $builder symfony?

I would like to know if it is possible to automatically assign values ​​to added fields of type:
datetime
entity
Thanks for your help
public function buildForm(FormBuilderInterface $builder, array $options)
{
$user = $options['user']; // entity User
$player = $options['player']; // entity Player
$today = new DateTime('now');
$builder
->add('fieldA')
->add('fieldB')
->add('fieldC');
$builder
->addEventListener(FormEvents::PRE_SUBMIT, function (FormEvent $event) use ($user, $player, $today) {
$form = $event->getForm();
$datas = $event->getData();
$form->add('today');
$form->add('user');
$form->add('player');
//dd($form); ok = 3 fields added
$datas['dateDuJour'] = $today;
$datas['user'] = $user;
$datas['player'] = $player;
//dd($datas); ok = 3 assigned values
$form->setData($datas);
question 1 : how to insert the data in the form
question 2 : pb from entity (object) to string
//dd($form, $datas);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => Evaluation::class,
'user' => null,
'player' => null
]);
}
}
I thought about inserting the 3 fields with the type = hidden and using Data Transformer
I do not know what is the best practice?
if you have a concrete example
From what i can see, you have some form and you want to plug 3 data to the form on submit.
Depending on your database configuration, you can do 3 different way:
The best one is to use the mapping
Your evaluation have those 3 fields:
date
user
player
Then just add them to the original builder as hidden field whith default value what you have:
$builder->add('token', HiddenType::class, [
'data' => $today,
])->add('user', HiddenType::class, [
'data' => $user,
])->add('player', HiddenType::class, [
'data' => $player,
]);
As they are hidden, the security check will not autorise users to change those value plus thoise fields will be hiddent
It require those three fields exist in your entity
Second one is to use unmapped hidden field. Same a previous, but add 'mapped'` => false . Then you in your controller, you will have the value and use them as needed.
The third one is to not use them in your form (my favorite) but in your controller
public function addEvaluation(Request $request, EvaluationManager $evaluationManager): Response
{
$evaluation = new Evaluation();
$form = $this->createForm(EvaluationType::class, $evaluation);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$evaluation->setTime(new DateTime('now'))
->setUser($user)
->setPlayer($player);
$evaluationManager->save($evaluation);
return $this->redirectToRoute('evaluation_add');
}
return $this->render('/evaluation_add.twig', [
'form' => $form,
]);
}

Symfony: ManyToOne Form Issue

I have two entities called, Ticket & TicketUpdate.
Each Ticket can have many TicketUpdates, but every TicketUpdate can only have 1 Ticket.
Next I have a form which shows the current Ticket, but also allows me to add 1 TicketUpdate & change attributes of Ticket.
This is my controller:
//TicketController.php
...
/**
* #Route("/ticket/{id}", name="app_ticket")
*/
public function ticket(Request $request, Ticket $ticket)
{
$ticketUpdate = new TicketUpdate();
$ticketUpdate->setTicket($ticket);
$form = $this->createForm(TicketUpdateType::class, $ticketUpdate); //custom form type
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($ticketUpdate);
$em->persist($ticket);
$em->flush();
}
return $this->render('ticket/view.html.twig', ['ticket' => $ticket, 'form' => $form->createView()]);
}
...
TicketUpdateType:
//TicketUpdateType.php
...
class TicketUpdateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('text', TextareaType::class, ['label' => 'update', 'required' => false, 'attr' => ['class' => 'textarea-sm'])
->add('ticket', TicketType::class, ['label' => false, 'by_reference' => false]) //custom Type for Tickets
->add('submit', SubmitType::class, ['label' => 'save']);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => TicketUpdate::class
]);
}
}
...
However, this solution does not work for me. Symfony always wants to create a new Ticket entry, instead of changing the old one.
Is there any way to fix this?
May you know, a magic with symfony forms, with you can get an Entity (Ticket), like in your example, I dont know... but this will working:
/**
* #Route("/ticket/{ticketId}", name="app_ticket", requirements={"ticketId"="\d+"})
*/
public function ticket(Request $request, int $ticketId = 0)
{
$em = $this->getDoctrine()->getManager();
$ticket = $em->getRepository(Ticket::class)
->findOneBy([
'id' => $ticketId
]);
if ($ticket instanceof Ticket === false)
{
die('Ticket dont exist with the requested ID.'); #Just return here some error message
}
$ticketUpdate = new TicketUpdate();
//Because your setTicket() setter inside your TicketUpdate Entity
//sure have nullable typehinted argument (?Ticket $ticket)
//if this is a valid doctrine relationship
//(but if i'm wrong, please show the touched parts of your TicketUpdate entity)
$ticketUpdate->setTicket($ticket); #<-- here is an "old" Ticket $ticket
$form = $this->createForm(TicketUpdateType::class, $ticketUpdate); //custom form type
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid())
{
$em->persist($ticketUpdate);
//I'm working a lot with doctrine relationships
//In many time necessary using a setter in the both Entity
$ticket->addTicketUpdate($ticketUpdate); #You know this setter! Change, if my tip wrong
$em->persist($ticket);
$em->flush();
}
return $this->render('ticket/view.html.twig', [
//Now, this is an instance of the Ticket
//not an int ID!
//so if you need the ID, you can get in twig, like:
//{{ ticket.id }}
'ticket' => $ticket, #Or: $ticket->getId()
'form' => $form->createView()
]);
}
The requirements inside the #route means, the method will running only on pages, where the {ticketId} is numeric.
Update: I changed by_reference to true and removed every logic in my TicketType, which seemed to cause the issue.
Atleast for now I got it running. Here is my controller:
//TicketController.php
...
/**
* #Route("/ticket/{id}", name="app_ticket")
*/
public function ticket(Request $request, Ticket $ticket)
{
$ticketUpdate = new TicketUpdate();
$ticketUpdate->setTicket($ticket);
$form = $this->createForm(TicketUpdateType::class, $ticketUpdate); //custom form type
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->persist($ticketUpdate);
//$em->persist($ticket); -> removed, will be automatically updated by symfony
$em->flush();
}
return $this->render('ticket/view.html.twig', ['ticket' => $ticket, 'form' => $form->createView()]);
}
...

Symfony2 - Get entity in presubmit/submit form event

As the title says, I need to get the entity which is going to be deleted in a form event. I'm using Symfony 2.7. I'm able to get entity on POST_SUBMIT event if it gets created/edited, but I'm not able to get it on PRE_SUBMIT, SUBMIT and also POST_SUBMIT before it gets deleted.
What I tried so far (I wrote results of variables in comments)
public static function getSubscribedEvents()
{
return array(
FormEvents::POST_SUBMIT => 'onPostSubmit',
FormEvents::PRE_SUBMIT => 'onPreSubmit',
FormEvents::SUBMIT => 'onSubmit'
);
}
public function onPreSubmit(FormEvent $event)
{
dump($event->getForm()->getData()); // <-- null
dump($event->getData()); // <-- array:1 ["submit" => ""]
}
public function onSubmit(FormEvent $event)
{
dump($event->getForm()->getData()); // <-- null
dump($event->getData()); // <-- array:0 []
}
public function onPostSubmit(FormEvent $event)
{
dump($event->getForm()->getData()); // <-- array:0 []
dump($event->getData()); // <-- array:0 []
}
This is basically how deletion starts. I'll not put the whole functions used by it because I don't think is needed:
public function deleteConfirmAction(Request $request, $id)
{
$form = $this->createDeleteForm($id);
$form->handleRequest($request);
$entity = $coreService->getArea($id);
if ($form->isValid()) {
$coreService->deleteEntity($entity); // will remove also relationships
$coreService->persistChanges(); // basically a Doctrine flush()
$this->addFlash('success', GlobalHelper::get()->getCore()->translate('flash.entity.delete.success'));
return $this->redirect($this->generateUrl('index')));
}
}
private function createDeleteForm($id)
{
return $this->createFormBuilder()
->setAction($this->generateUrl('area_delete_confirm', array('id' => $id)))
->setMethod('POST')
->add('submit', 'submit', array('label' => 'entity.delete', 'attr' => ['class' => 'btn button-primary-right']))
->getForm();
}
Any idea?
You don't pass your entity to the form, you can do this by specifying first argument for createFormBuilder call:
$this->createFormBuilder(['entity' => $entity]);
and then you should be able to retrieve entity in pre submit event listener.

Symfony, how to use form event to validate dynamic client-side form

I'm using the select2 plugin with ajax to have a dynamic field on my form, but when i submit the it return me an error "This value is not valid", which is normal cause i use the ChoiceType with an empty array() in the choices options on creation. According to this part of the symfony doc, the form event is my savior, so trying to use it but it look like something wrong with my code and can't really see what.
So My Question Is :
HOW to pass the choices possibility to the field, for the form to be valid.
My form Type
class ArticleType extends AbstractType
{
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//My other field
//My functions to add the field with the possible choices
$formModifier = function (FormInterface $form, $imageValue) use ($options) {
if ($imageValue !== null) {
$listImages = $this->getChoiceValue($imageValue, $options);
if (!$listImages) {
$form->get('image')->addError(new FormError(
'Nous n\'avons pas pu trouver l\'image, veuiller choisir une autre'
));
}
} else {
$listImages = array();
}
//die(var_dump($listImages)); //Array of Image
$form->add('image', ChoiceType::class, array(
'attr' => array(
'id' => 'image'),
'expanded' => false,
'multiple' => false,
'choices' => $listImages));
};
$formModifierSubmit = function (FormInterface $form, $imageValue) use ($options) {
if ($imageValue !== null) {
$listImages = $this->getChoiceValue($imageValue, $options);
if (!$listImages) {
$form->get('image')->addError(new FormError(
'Nous n\'avons pas pu trouver l\'image, veuiller choisir une autre'
));
}
} else {
$form->get('image')->addError(new FormError(
'Veuillez choisir une image s.v.p.'
));
}
//die(var_dump($listImages)); //Array of Image object
$config = $form->get('image')->getConfig();
$opts = $config->getOptions();
$chcs = array('choices' => $listImages);
//die(var_dump($chcs)); //output an array with a 'choices' keys with array value
array_replace($opts, $chcs); //not work
//array_merge($opts, $chcs); //not work
//die(var_dump($opts)); //replacements/merge are not made
};
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
function (FormEvent $event) use ($formModifier) {
// this would be the entity Article
$data = $event->getData();
$formModifier($event->getForm(), $data->getImage());
}
);
//$builder->get('image')->addEventListener( //give error cause the field image don't exist
$builder->addEventListener(
FormEvents::PRE_SUBMIT,
function (FormEvent $event) use ($formModifierSubmit) {
$imageVal = $event->getData();
//die(var_dump($imageVal)); //return all the submitted data field in an array
//But when change this event to Submit it return the Article model populated by the submitted data, EXCEPT the image field which have null as value
$formModifierSubmit($event->getForm(), $imageVal['image']);
}
);
}
public function getChoiceValue($imageValue, $options)
{
$listImages = $options['em']->getRepository('AlmotivAppBundle:Image')->findBy(array(
'id' => $imageValue
));
return $listImages; //array of Image object
}
[...]
}
For Info
My image field is not depending on any other field like the doc example, so i need to populate the choices options on PRE_SUBMIT event to give the possible choice.
And also image have a ManyToOne relation in my Article entity
class Article implements HighlightableModelInterface
{
//some properties
/**
* #ORM\ManyToOne(targetEntity="Image\Entity\Path", cascade={"persist"})
* #Assert\Valid()
*/
private $image;
}
If i'm in the bad way let me know cause i'm out of idea now, i try much thing, like
array_replace with the options in the configuration of the field but didn't wrong.
make an ajax request to the url of the form action url : $form.attr('action'), i think it will load the choices option with the possible of <option> but my select is still returned with none <option>.
and much more (can't remmenber).
And also i'm using the v3.1 of the framework with the v4.0.3 of the select2 plugin, if need more info just ask and thx for reading and trying help.
Edit
Just add some info to be more clear
You making things way too complicated. In your documentation example they add eventListener for already existing form field ('sport') and you are adding it to only later added field which does not exist (your 'image' field and 'position' field from the documentation example).
You should use EntityType and if you need (which I'm not if sure you are) filter your images using query_builder option, for validation add constraints (example with controller).
class ArticleType extends AbstractType {
/**
* {#inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// $builder
// My other field
$imageFieldFunction = $this->getImageFieldFunction();
$builder->addEventListener(FormEvents::PRE_SET_DATA, $imageFieldFunction);
$builder->addEventListener(FormEvents::PRE_SUBMIT, $imageFieldFunction);
}
private function getImageFieldFunction()
{
return function(FormEvent $event) {
$form = $event->getForm();
$data = $event->getData();
//when your data_class is Article
$image = $data->getImage();//depending on your Article class
/*if you are using data_class => null
$image = $data['image'];
*/
$imageId = $image ? $image->getId() : 0;
$builder->add('image', EntityType::class , array(
'class' => 'AlmotivAppBundle:Image',
'attr' => array(
'id' => 'image'
) ,
'expanded' => false,
'multiple' => false,
'constraints' => new NotBlank(),
'query_builder' => function (EntityRepository $er) use ($imageId) {
return $er->createQueryBuilder('i')
->where('i.id = :image_id')
->setParameter('image_id', $imageId);
}
));
}
}
}

Symfony2 ModelTransformer reverseTransform is never called

In Symfony 2.8 I've got Movie entity with actors field, which is ArrayCollection of entity Actor (ManyToMany) and I wanted the field to be ajax-loaded Select2.
When I don't use Ajax, the form is:
->add('actors', EntityType::class, array(
'class' => Actor::class,
'label' => "Actors of the work",
'multiple' => true,
'attr' => array(
'class' => "select2-select",
),
))
It works, and this is what profiler displays after form submit: http://i.imgur.com/54iXbZy.png
Actors' amount grown up and I wanted to load them with Ajax autocompleter on Select2. I changed form to ChoiceType:
->add('actors', ChoiceType::class, array(
'multiple' => true,
'attr' => array(
'class' => "select2-ajax",
'data-entity' => "actor",
),
))
//...
$builder->get('actors')
->addModelTransformer(new ActorToNumberModelTransformer($this->manager));
I made DataTransformer:
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Persistence\ObjectManager;
use CompanyName\Common\CommonBundle\Entity\Actor;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class ActorToNumberModelTransformer implements DataTransformerInterface
{
private $manager;
public function __construct(ObjectManager $objectManager)
{
$this->manager = $objectManager;
}
public function transform($actors)
{
if(null === $actors)
return array();
$actorIds = array();
$actorsArray = $actors->toArray();
foreach($actorsArray as $actor)
$actorIds[] = $actor->getId();
return $actorIds;
}
public function reverseTransform($actorIds)
{
if($actorIds === null)
return new ArrayCollection();
$actors = new ArrayCollection();
$actorIdArray = $actorIds->toArray();
foreach($actorIdArray as $actorId)
{
$actor = $this->manager->getRepository('CommonBundle:Actor')->find($actorId);
if(null === $actor)
throw new TransformationFailedException(sprintf('An actor with id "%s" does not exist!', $actorId));
$actors->add($actor);
}
return $actors;
}
}
And registered form:
common.form.type.movie:
class: CompanyName\Common\CommonBundle\Form\Type\MovieType
arguments: ["#doctrine.orm.entity_manager"]
tags:
- { name: form.type }
But seems like the reverseTransform() is never called. I even put die() at the beginning of it - nothing happened. This is, what profiler displays after form submit: http://i.imgur.com/qkjLLot.png
I tried to add also ViewTransformer (code here: pastebin -> 52LizvhF - I don't want to paste more and I can't post more than 2 links), with the same result, except that reverseTransform() is being called and returns what it should return.
I know that this is an old question, but I was having a very similar problem. It turned out that I had to explicitly set the compound option to false.
That is to say, for the third parameter to the add() method, you need to add 'compound => false'.

Categories