Modify multiple entities with one form (bulk edit) - symfony

I want to be able to submit a form that modifies multiple entities. Let's say I've got a BulkeditFormType to change the 'active' (Boolean) or 'organisation' (EntityType with App\Entity\Organisation) fields on multiple users at once.
This is my current solution (semi-pseudo-code):
public function bulkedit(Request $request)
{
$form = $this->createForm(BulkeditFormType::class, null, [
//..options
]);
if ($request->isXmlHttpRequest()) {
// fields '_token' and empty values are unset, not shown in this example
$formData = $request->request->get($form->getName());
$entityManager = $this->getDoctrine()->getManager();
$repo = $entityManager->getRepository(App\Entity\User::class);
$entities = $repo->findBy([
'id' => [1,2,3,4,5]
]);
foreach ($entities as $entity) {
$form = $this->createForm(BulkeditFormType::class, $entity, [
//..options
]);
$clearMissing = false;
$form->submit($formData, $clearMissing);
$entityManager->persist($entity);
}
$entityManager->flush();
}
return $this->render('#User/User/bulkedit.html.twig', [
'form' => $form->createView()
]);
}
Note that I've tried to include only relevant parts of my code, so please consider this as pseudo-code, just to get an idea of my current implementation.
While this solution works, it creates a Form object for every entity. Editing 100 users will lead to a large amount of memory usage and many useless database queries.
How can I modify my code in such a way that only one form will be generated which can be re-used by all entities? I've tried to use $form->setData(), but I feel like there must be a better way. A CollectionType will create multiple subforms instead of multiple forms, so in this case it doesn't make a big difference.

Related

How to persist a collection of forms embedded in a collection of Forms...?

For my Poll Application i created a FormType called CampaignType which holds a CollectionType named blocks which in turn holds a CollectionType named lines, which holds a CollectionType named fields, which holds a CollectionType named pollResults.
In my next code example you can see my code that renders the View to fill a campaign(poll).
public function fillAction(Request $request, $id)
{
$campaign = $this->getDoctrine()->getRepository(Campaign::class)->find($id);
$entityManager = $this->getDoctrine()->getManager();
foreach ($campaign->getBlocks() AS $block){
foreach ($block->getLines() AS $line){
foreach ($line->getFields() AS $field){
$pollResult = new PollResult();
$pollResult->setCampaign($campaign);
$pollResult->setField($field);
$pollResult->setUser($this->getUser());
$entityManager->persist($pollResult);
$field->getPollResults()->add($pollResult);
}
}
}
$form = $this->createForm(CampaignType::class, $campaign);
$form->handleRequest($request);
if($form->isSubmitted() && $form->isValid()){
var_dump("true");
//$entityManager->persist($campaign);
$entityManager->flush();
return $this->redirectToRoute("grappt_poll_campaignShow", ['id' => $id]);
}
return $this->render('GrapptPollBundle:Campaigns:fill.html.twig', [
'campaign' => $campaign,
'form' => $form->createView()
]);
}
The only thing that must be persisted in the database are the PollResults.
Every PollResult has an entry for the campaign_id and the field_id it belongs to, the user_id who filled out the campaign and the value the user chose (and of course its own id, which gets generated automatically).
My Problem is that i don't know how to do that.
Where do i have to call $entityManager->persist($pollResult);.
Right now i put it directly under the initialization-stuff.
Do i have to put it into the if($form->isSubmitted() && $form->isValid())-query and loop through every pollResult?
Do i have to call $entityManager->persist($campaign); although nothing changes there?
Furthermore i wonder if i have to add something for the value-entry of each PollResult?
Thanks in advance for every answer
lxg
What will $form->isValid() return ?
It will depend on the validation constraints of you master form. If your validation constraints are in the annotations of your entity, in your master entity you should have the #Assert\Valid() annotation which will be sure that the nested form is valid :
class Campaign
{
/**
* #ORM\OneToMany(…)
* #Assert\Valid() // <- this line here
*/
private $blocks;
...
If you prefer to put your validation constraints in your CampaignType, you can put it in the options :
public function buildForm (FormBuilderInterface $builder, array $options)
{
$builder
->add('blocks', CollectionType::class,[
'entry_type' => BlockType::class,
'constraints' => array(new Valid()) // <- this line here
...
So, where should you put the persist()?
The best is to have Symfony's form validation (->isValid()) before any persistance, for security and data sanity (don't persist before ensuring csrf protection for instance). If you may add a lot of data (like persisting thousands of entities after one form submission), you can look into Doctrine's batch processing and bulk inserts : https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/batch-processing.html
Should you also persist the Campaign object ?
It depends on the cascade persistence rules you have in your entity.
You can find all the rules to fine-tune the cascade here : https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/working-with-associations.html#transitive-persistence-cascade-operations

I can't create multiple object of an entity at once

I'm learning Symfony and I have a situation where I need, after a form, to create multiple object of an entity A.
All these objects are quite identical but just the value of one field change which is a relation OneToMany to an entity B. In the form this relation is selected with checkbox (that are EntityType related to B), and so I want to create one object A for each checked boxes.
My issue is that the First's form allows me to create only one object of First type at a time.
It is a project given by my teachers, and I mustn't create a ManyToMany relation.
Can somebody help me?
Here is the code in the Controller:
$A = new A();
$form = $this->createForm(AType::class, $A);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($A);
$entityManager->flush();
}
And here is the code in the FormType:
$builder
->add('anotherField')
->add('idB', EntityType::class, [
'label' => 'B',
'class' => B::class,
'expanded' => true,
'multiple' => true
])
;
If I understand you correctly, with one form submission you wish to save multiple entities to the database. I would clone all the form data into a new object, make changes and then persist all and flush like so:
if ($form->isSubmitted() && $form->isValid()) {
$original = $form->getData();
$copy = clone $original;
// here you can make changes to your copy.
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($original);
$entityManager->persist($copy);
$entityManager->flush();
}

Entity persist finds new relationship that existed before

Hi I have an action which adds people to a group.
In order to increase the usability the form for it removes the people that are already in the group.
My controller action looks like this:
public function addAction(UserGroup $userGroup)
{
$tempGroup = new UserGroup();
foreach ($userGroup->getUsers() as $user) {
$tempGroup->addUser($user);
}
$form = $this->container->get('form.factory')->create(new UserGroupQuickType(), $tempGroup);
$request = $this->container->get('request');
if ('POST' === $request->getMethod()) {
$form->submit($request);
if ($form->isValid()) {
$group = $form->getData();
/** #var $myUserManager UserManager */
$myUserManager = $this->container->get('strego_user.user_manager');
/** #var $em EntityManager */
$em = $this->container->get('em');
foreach ($group->getUsers() as $toInvite) {
$userGroup->addUser($toInvite);
}
$em->persist($userGroup);
$em->flush($userGroup);
}
}
return array(
'form' => $form->createView(),
'userGroup' => $userGroup
);
}
This code throws an exception:
A new entity was found through the relationship 'Strego\UserBundle\Entity\UserGroup#users'
that was not configured to cascade persist operations for entity: Degi.
To solve this issue: Either explicitly call EntityManager#persist() on
this unknown entity or configure cascade persist this
association in the mapping for example #ManyToOne(..,cascade={"persist"}).
The new found relation with was already there before. Meaning the user "Degi" was already in the group and is not a new entity.
I can avoid this error if I'll leave out the persist but then I'll get an exception: Entity has to be managed or scheduled for removal for single computation.
This is caused by the fact that my "usergroup" entity has the whole time a entity status of 3 (= detached)
I have used the same logic (with temp group etc.) for an entity that has 1 to 1 relationship to my usergroup and from there I can easily add people even to the group.
But not with this action, which is logically doing the exact same thing.
UPDATE:
My previous update was leading in the wrong direction. But here in comparison the (almost) same controller that works:
public function addAction(BetRound $betRound)
{
$userGroup = new UserGroup();
foreach ($betRound->getUsers() as $user) {
$userGroup->addUser($user);
}
$form = $this->createForm(new UserGroupQuickType(), $userGroup);
$request = $this->getRequest();
if ('POST' === $request->getMethod()) {
$form->submit($request);
if ($form->isValid()) {
/** #var $betRoundManager BetRoundManager */
$betRoundManager = $this->container->get('strego_tipp.betround_manager');
/** #var $myUserManager UserManager */
$myUserManager = $this->container->get('strego_user.user_manager');
$group = $form->getData();
foreach ($group->getUsers() as $toInvite) {
if (!$betRound->getUserGroup()->hasUser($toInvite)) {
$betRound->getUserGroup()->addUser($toInvite);
}
}
$this->getDoctrine()->getManager()->flush();
return $this->redirect($this->generateUrl('betround_show', array('id' => $betRound->getId())));
}
}
return array(
'form' => $form->createView(),
'betRound' => $betRound
);
}
This is expected behavior since your have relationship to, not really a new entity but unmanaged object.
While iterating you could try merging $toInvite. Don't worry, if objects you acquired via form have an appropriate identifier (#Id) value set they would be just reloaded from databases instead of marked for insertion. Newly added objects, on the other hand, will be marked.
So, before trying anything, ensure that each of old $toInvite have an ID set.
foreach ($group->getUsers() as $toInvite) {
$em->merge($toInvite);
$userGroup->addUser($toInvite);
}
// safe to do now, all of the added users are either reloaded or marked for insertion
$em->perist($usetGroup);
$em->flush($userGroup);
Hope this helps.
This is happening because $userGroup is already an entity which already exists and $toInvite is a new entity and you are trying to flush $userGroup. To make this code working you need to specify cascade={"persist"} in your entity file (Annotations or yaml whichever you prefer).
Other solution is to do the reverse and persist $toInvite
foreach ($group->getUsers() as $toInvite) {
$toInvite->setUserGroup($userGroup);
$em->persist($toInvite);
}
$em->flush();
I think the first option would be a better choice
After several attempts I found out that this issue were actually 2 issues:
1. I had a listener that updated some rows on persist even if it was not necessary.
2. The paramconverter somehow detaches the entities that are coming in. Which means that my UserGroup and all my users in it are detached. As soon as they are added again to the Usergroup, those Entities seem to be "new" for the EM. Therefore I needed to merge all users AND fix my listener that updates the "createdBy", which otherwise would have also needed to been merged.

Symfony2 confirm form submission

I need to be able to do an operation on form data before it gets persisted to the database. The problem is, the operation can be risky and I need the user's consent each time.
I'd like to do this through a confirmation form (with a little message explaining what's going on), not with a Javascript confirm window.
How can I achieve this functionality?
Here is an example of a controller action method that handles the form:
<?php
public function indexAction(Request $request)
{
...
$form = $this->createFormBuilder($myEntity)
->add('someField', 'integer', array('required' => true))
// Lots and lots of fields
->getForm();
if ($request->isMethod('POST')) {
$form->bind($request);
if ($form->isValid()) {
// CUSTOM VALIDATION HERE
// If invalid, must display a new confirmation form to ask user
// if it's alright to do a somewhat risky operation that would
// validate the form data.
// Else, persist the data.
$db = $this->getDoctrine()->getManager();
$db->persist($myEntity);
$db->flush();
return $this->redirect($this->generateUrl('my_path_to_success_page'));
}
}
return $this->render('MyBundle:Preferences:index.html.twig', array(
'form' => $form->createView(),
'errors' => $form->getErrors(),
));
}
You might want to look into CraueFormFlowBundle, which allows you to create multi-step forms.

Symfony2 Functional test: Passing form data directly

I am using phpunit to run functional tests but I am having a problem with a few of the forms. The problem is that phpunit is not aware of JS, and I have a form with a dynamically populated select box that needs jQuery.
So I need to pass the form data directly. The 'book' gives the following example:
// Directly submit a form (but using the Crawler is easier!)
$client->request('POST', '/submit', array('name' => 'Fabien'));
When I used this example the controller didn't receive any of the form data. Intially I saw that passing the array key 'name' wasn't correct in my situation as I needed the form name which was 'timesheet' in my code. So I tried something like:
$client->request('POST', '/timesheet/create', array('timesheet[project]' => '100'));
But this still didn't work. In the controller I tried to understand what was happening and what if anything was being received:
$postData = $request->request->get('timesheet');
$project = $postData['project'];
This didn't work and $project remained empty. However if I used the following code I got the value:
$project = $request->request->get('timesheet[project]');
But clearly that's not what I want. Atleast though I can see that there is some POST data. My last attempt was to try the following in the test method:
$this->crawler = $this->client->request('POST', '/timesheet/create/', array('timesheet' => array(project => '100'));
So I am trying to pass a 'timesheet' array as the first element of the request parameter array. But with this I get the error:
Symfony\Component\Form\Exception\UnexpectedTypeException: Expected argument of type "array", "string" given (uncaught exception) at /mnt/hgfs/pmt/src/vendor/symfony/src/Symfony/Component/Form/Form.php line 489
I would be very happy if someone can expand on what's in the 'book' about how I am supposed to get this working.
Form bind in controller:
if ($request->getMethod() == 'POST') {
$form->bindRequest($request);
if ($form->isValid()) {
$postData = $request->request->get('timesheet');
$project = $postData['project'];
$timesheetmanager = $this->get('wlp_pmt.timesheet_db_access');
$timesheetmanager->editTimesheet($timesheet);
return $this->redirect($this->generateUrl('timesheet_list'));
}
}
If you are wanting to know how to inject arrays of POST data using the test client...
In your test method, do something like
$crawler = $client->request('POST', '/foo', array(
'animal_sounds' => array(
'cow' => 'moo',
'duck' => 'quack'
)
); // This would encode to '/foo?animal_sounds%5Bcow%5D=moo&animal_sounds%5Bduck%5D=quack'
$this->assertTrue( ... );
In the controller, you would access your params like this:
$data = $request->request->get('animal_sounds');
$cowNoise = $data['cow'];
$duckNoise = $data['duck'];
Or you could just use the forms API if the test method was injecting valid form data...
do you have a $request parameter in your action?
that was the reason why my request->get() was empty:
//WRONG
public function projectAction()
{
$request = Request::createFromGlobals();
$project = $request->request->get('timesheet[project]');
//$project will be empty
}
//CORRECT
public function projectAction(Request $request)
{
$project = $request->request->get('timesheet[project]');
//$project is not empty
}
see
How do I create a functional test which includes a POST to a page with parameters?
Try to use $form->bind($clientData) instead of $form->bindRequest($request).

Resources