Symfony2 Forms - Cannot add/remove entities from a collection - symfony

I have a form for a "Person" entity that has a collection of "Nicknames".
I have implemented this following the guide in the cookbook (http://symfony.com/doc/current/cookbook/form/form_collections.html)
When I create a new "Person" I can add "Nicknames" as expected successfully.
Using a similar form I have an edit page. On the edit page I have set up the front end in the same way as the new page.
On the edit page, I can edit the existing nicknames and the other simple fields successfully. However, when I submit a new "Nickname" (or try to remove one), it does not work.
I have var_dump()'ed the request object before it is binded to the "Person" and I have var_dump()ed the "Person" object after binding. The request does have the "Nicknames" collection passed correctly. However, they are not being binded to the Person at all, despite the fact that everything is the same as the new page. Further, I can still edit the existing Nicknames so it is working on that front.
I cannot for the life of me figure out why Symfony isn't creating these new Nicknames or adding them to the Person as it should. I have looked around and searched but everything just confirms that it should be working - but it isn't.
Heres the entry for the Nicknames on the Person form type:
->add('nicknames', 'collection', array(
'type' => new NicknameType(),
'allow_add' => true,
'allow_delete' => true,
'prototype' => true,
'by_reference' => false,
'required' => false
))
Nickname type only has one field - "nickname" and its a text input.
Any help? Thanks for reading.
EDIT:
Here is the update action (changed Person to Player):
public function updateAction($id, Request $request)
{
$em = $this->getDoctrine()->getManager();
$player = $em->getRepository('SCDBAppBundle:Player')->find($id);
if (!$player) {
throw $this->createNotFoundException('Unable to find Player.');
}
$editForm = $this->createForm(new EditPlayerType(), $player);
$deleteForm = $this->createGenericIDForm($id);
$editForm->bindRequest($request);
if ($editForm->isValid()) {
$em->persist($player);
$em->flush();
return $this->redirect($this->generateUrl('admin_player_edit', array('id' => $id)));
}
return array(
'player' => $player,
'edit_form' => $editForm->createView(),
'delete_form' => $deleteForm->createView(),
);
}
I realize that to actually persist removing nicknames I need to change the code to persist. I'm not even worried about persistence right now, because the main issue is that after I bind the request to the Player form, the $player object does not contain the updated Nicknames collection.

If you are not using Symfony master but 2.0.x, beware, because the "collection" type is buggy there. If you are using Symfony master however, make sure to include a recent version of Doctrine (2.2.3 or 2.1.7) because there two bugs have been fixed that affect the correct working of the "collection" type.

Maybe try to use NicknameType::class instead of new NicknameType().
And I suggest to add cascade: [ persist ] conf attribute to your entity.

You have to get the data from the request with:
$objPerson = $editForm->getData();
$em->persist($objPerson);
$em->flush();
Otherwise you update the old object without changes from the form.
Short: You have only updated the object which was loaded from the database before.

Related

How it is possible to show a choice field (checkboxes) with the selected value

I use an Symfony2 form with a choice field based on 4 checkboxes. Now i have the Problem, that i can't reload data in this choice field.That means, when in the database the value 9 is stored, will the checkbox "unknown if "lang test" performed" not be selected, when i load the entity in this form.
->add('fu5LangTest', 'choice', array(
'choices' =>array(
'poor compliance'=>0,
'negative'=>1,
'positive (recognizes at least 1 image reliably)'=>2,
'unknown if "lang test" performed'=>9),
'choice_value' =>function (DataEFu5 $entity = null) {
return $entity ? $entity->getFu5LangTest() : '';
},
'expanded'=>true))
I tried a lot of things with the choice_value Attribut, but i think i'm using it wrong. Whats the right way? i didn't found some useful Information. What i have to do, that the correct checkbox will be selected after an reload?
The storing process is working fine.
Thanks for an feedback
The form is not the right place to do domain Logic.
Anyway, the data has the current fu5LangTest value. You should use
->add('fu5LangTest', 'choice', array(
'choices' =>array(
'poor compliance'=>0,
'negative'=>1,
'positive (recognizes at least 1 image reliably)'=>2,
'unknown if "lang test" performed'=>9
),
// ...
Form creation in Controller:
$object = new DataEFu5(); //or get it by your repository
//this condition will be better in a service as it is business logic ;)
if(!in_array($object->getFu5LangTest, [0,1,2])) {
$object->setFu5LangTest(9);
}
$form = $this->createForm(ObjectType, $object);

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
}
])
],
]);
});

Symfony2: KnpPaginator only show the first page with POST form

I'm using this bundle in an application. The controller is the typical that shows a search form, take the response and process it (an example):
public function indexAction()
{
$request = $this->getRequest();
$example = new Example();
$form = $this->createForm(new ExampleFindType(), $example, array(
'action' => $this->generateUrl('example_find'),
'method' => 'POST',
));
$form->handleRequest($request);
if ($form->isValid())
{
$em = $this->getDoctrine()->getManager();
$examples = $em->getRepository('ApplicationExampleBundle:Example')
->find_by_fields($example);
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$examples,
$this->get('request')->query->get('p', 1),
20
);
return $this->render('ApplicationExampleBundle:Default:searchResults.html.twig',
array('pagination' => $pagination));
}
return $this->render('ApplicationExampleBundle:Default:index.html.twig',
array('form' => $form->createView(),
));
}
When I perform the search I see the results list and the paginator correctly. The problem appears when I press the link to the next page. The link id generated well, with the URL ending with "?p=2" but it seems that the form POST data is not resent because it sent me to the search form page ($form->isValid() is false).
If I change the form method from POST to GET and pass the parameters in the URL:
$form = $this->createForm(new ExampleFindType(), $example, array(
'action' => $this->generateUrl('example_find'),
'method' => 'GET',
));
the paginator works perfect.
Am I doing something wrong? Is possible to use a POST form?
I've searched an answer but all the KnpPagintor controller examples I've seen don't generate the query with forms, and this question hasn't helped me.
Thanks.
You shouldn't use POST method to get data.
Otherwise, if you need to use the POST method then you need the data in the session. However it's difficult to build a nice user experience while it just makes more sense to use a GET method.
You can find an extensive documentation about HTTP on MDN.
A GET method should be used when you request data.
A POST method should be used when you save data (like saving a comment into a database) or other data manipulation.
Google uses a GET on their own search page.
https://www.google.com/#q=symfony&start=10
q is what I searched for and start is the paginator value. They probably use an offset instead of a page number to avoid calculating the offset (faster and less expensive).

Setting a default entity value

I'm trying to set an entity form type to have a default value, i.e the user that is currently logged in.
My code is as follows:
EventType.php
->add('forUser', 'entity', array(
'label' => 'Staff',
'class' => 'BMUserBundle:User'
'property' => 'fullName',
'empty_value' => 'Select Staff'.
'query_builder' => function(EntityRepository $er) {
return $er->findAllStaff();
}
))
This works fine and returns all th staff members in the drop down.
I have also tried to do the following:
$form = $this->createForm(new EventType());
$form->get('forUser')->setData("USER_ENTITY") -> entity of logged in user
This doesn't seem to affect the form and it just lists the users with no default selected.
Am I approaching this correctly?
Thanks
When you instanciate your entity type in the controller, you first have to create a Event object with the user set for the "forUser" relation. Then you just have to pass as 2nd argument your Event object and it will automatically populate the type with.
Here is a small code example to show you how to do :
/src/Your/AppBundle/Controller/YourController.php
// get the authenticated user
$user = $this->get('security.context')->getToken()->getUser();
$event = new Event();
$event->setForUser($user);
$form = $this->createForm(new EventType(), $event);
But as forgottenbas said, you have to verify that your EventType has well the data_class option with your Event Classname as value. You'll get more information about it here.
If you let me tell you an off topic advice, you shouldn't use Event as ClassName because the event concept is well too present in Symfony2 development so it'll only obfuscate the code.
Good luck for your project, I hope it'll help you.

Validate a Collection Field Type in Symfony 2 with allowExtraFields=true

I'm trying to validate a collection form field:
$builder->add(
'autor',
'collection',
array(
'type' => 'text',
'options' => array('required' => false),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'error_bubbling' => false
)
);
I use JavaScript, as suggested in the Cookbook, to dynamically add more text fields to the collection. My problem is, that I don't know, how to validate these fields. The collection validator lets me validate specific fields of a collection by their name but not simply every field of it. How can I manage that?
Even cooler would be, if I could check, if at least one of the fields is notBlank instead of forcing it to every field.
Best regards
You can use the "constraints" option defined in form Field Type that are available on all fields.(http://symfony.com/doc/master/reference/forms/types/form.html#constraints).
In your case, you can add your constraint like this:
$builder->add('autor', 'collection', array(
'constraints' => new NotBlank()),
));
(in this case dont forget to include the constraints provided by the Validation component:
use Symfony\Component\Validator\Constraints\NotBlank; ...)
i didnt test but i think with this every input will be validate againts the constraint you assigned to the field, and as you have the option "error_bubbling" as false, an error message should be attached to the invalid element.
--EDIT--
Since you even use the 2.0 version of Symfony i think this solution solves your problem, however I strongly advise you to update to the 2.3 version.
You can create a Form Event Subscriber(http://symfony.com/doc/2.0/cookbook/form/dynamic_form_modification.html) that will be listening the POST_BIND event.(note that Post Bind event is Deprecated since version 2.3 and will be removed in 3.0);
In your subscriber class, you will validate each of your submited authors as you want and add an error to the form if something is wrong.
Your postBind method could be something like this:
public function postBind(DataEvent $event)
{
$data = $event->getData();
$form = $event->getForm();
if (null === $data) {
return;
}
// get the submited values for author
// author is an array
$author = $form['autor']->getData();
// now iterate over the authors and validate what you want
// if you find any error, you can add a error to the form like this:
$form->addError(new FormError('your error message'));
// now as the form have errors it wont pass on the isValid() method
// on your controller. However i think this error wont appear
// next to your invalid author input but as a form error, but with
// this you can unsure that non of the fields will be blank for example.
}
you can check the Symfony2 Form Component API if you have any doubt about a core method.
http://api.symfony.com/2.0/index.html

Resources