Symfony2: List Users with Option to Deactivate Each User - symfony

In my application's admin panel, I am showing a list of users who are currently marked as "Active" in the database.
<ul class="list-group">
{% for activeuser in activeusers %}
<li class="list-group-item">
{{ activeuser.username|e }}
<input type="checkbox" name="my-checkbox" class="ckbx" checked>
</li>
{% endfor %}
</ul>
As you can see, each active user list item has a placeholder checkbox for now which is checked when the user is, you guessed it, active.
I would like to be able to simply uncheck the checkbox, and then run an AJAX call to update the database to mark the user as inactive. My first instinct was to create a form for each user object in my controller, but it seems like that would get incredibly messy. Also, I can't simply pass in a
'form' => $form->createView()
from my controller as there presumably has to be one form for each user. Any of the documentation I have read on the subject doesn't seem to provide any help for this particular problem.
UPDATE
I created a function within my controller to create a generic user update form:
/**
* Creates a form to create a User entity.
*
* #param User $entity The entity
*
* #return \Symfony\Component\Form\Form The form
*/
public function createUserForm(User $entity){
$form = $this->createForm(new UserType(), $entity, array(
'action' => $this->generateUrl('user_update', array('id' => $entity->getId())),
'method' => 'PUT',
));
return $form;
}
The form is generated by the UserType class
class UserType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('isActive', 'checkbox');
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\User'
));
}
/**
* #return string
*/
public function getName()
{
return 'appbundle_user';
}
}
Now inside of the main action (called dashboardAction), I get a list of users and for each of user, generate a form for it. Don't forget to run createView() on each form generation!
public function dashboardAction()
{
$userService = new UserService($this->getDoctrine()->getManager());
$activeUsers = $userService->listUsers('active');
$inactiveUsers = $userService->listUsers('inactive');
$formify_activeUsers = array();
foreach($activeUsers as $activeUser){
$formify_activeUsers[] = $this->createUserForm($activeUser)->createView();
};
return $this->render('AppBundle:Admin:dashboard.html.twig',
array('page_title' => 'Administration Panel',
'activeusers' => $formify_activeUsers,
)
);
}
Then the twig code looks like this:
<ul class="list-group">
{% for activeuser in activeusers %}
<li class="list-group-item">
{{ form_start(activeuser) }}
{{ form_widget(activeuser) }}
{{ form_end(activeuser) }}
</li>
{% endfor %}
</ul>

If what you really want is to activate/desactivate an user why put the overhead of the forms in this situation.
You could simply create an action:
/**
* #Route("/admin/isactive", name="isactive")
* #Method("POST")
*/
public function deactivateUserAction($id){
$em = $this->getDoctrine();
$user= $em
->getRepository('AppBundle\Entity\User')
->find($id);
if (!$user) {
throw $this->createNotFoundException(
'No userfound for id '.$id
);
}
$user->setActive(false);
$em->getManager()->flush($user);
return new JsonResponse();
}
On your view:
<ul class="list-group">
{% for activeUser in activeusers %}
<li class="list-group-item">
<input class="user-status" type="checkbox" value="{{activeUser.id}}">{{activeUser.name}}
</li>
{% endfor %}
</ul>
Attach a on click event into your checkboxs.
$('.user-status).on('click', function(e){
$.post("{{ path('isactive') }}", {userId: $(this).val()})
.done(function(data){
alert("Data loaded: " + data);
});
});

Related

Iterate or count ArrayCollection from twig

symfony 2.8
Ok, I have a Category entity which looks like this:
class Category
{
// ... //
/**
* #ORM\OneToMany(targetEntity="Classified", mappedBy="category")
*/
private $classified;
// ... //
public function __construct()
{
$this->classified = new ArrayCollection();
}
}
and the Classified entity looks like this:
Class Classified
{
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="classified")
*/
private $category;
}
In my result.html.twig I called to render the controller like this:
{{ render(controller('ClassifiedBundle:Category:displayCategoryPanelList'))}}
which basically calls Category Controller
class CategoryController extends Controller
{
public function displayCategoryPanelListAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$categories = $em->getRepository('ClassifiedBundle:Category')
->findAllOrderedByName();
return $this->render('ClassifiedBundle:Front/Widgets:category-panel-list.html.twig',
array(
'categories' => $categories
));
}
}
which in turn calls the CategoryRepository for query:
class CategoryRepository extends EntityRepository
{
public function findAllOrderedByName()
{
$query = $this->createQueryBuilder('c')
->select('c, cc')
->leftJoin('c.classified', 'cc')
->orderBy('c.name', 'ASC')
->getQuery()
->getResult();
return $query;
}
}
which in the end is rendered to category-panel-list.html.twig:
<ul>
{% for c in categories %}
<li>
<a href="#">
<i class="fa {{ c.icon }}"></i>
{{ c.name }} <span class="category-counter">{# COUNT SHOULD GO HERE#}</span>
</a>
</li>
{% endfor %}
</ul>
Now, all these work fine apart from the counting classified arraycollection part. I have tried {{ c.classified|length }} but this gives a fatal error.
The dump looks good and I can see the entry for the classified collection, but just don't know how to count them. Some categories have empty classified collection.
{{ c.classified|length }} is applied to an array.
Your object c.classified is a PersistCollection, not an array.
You should use {{ c.classified.count }}
You could try just using the doctrine method
{{ c.classified.count }}

Symfony2 registration form - extra created fields with jQuery are not persisted in DB only value from last input

I want to persist into database email field all dynamically created emails input values, after submitting the form. Now the problem is that - only last value is saved, from emails field. I don't have idea how i can fix it.
Registration controller:
class RegistrationFormType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('first_name')
->add('last_name')
->add('is_company', CheckboxType::class, array(
'label' => 'Is company?',
'required' => true,
))
->add('emails', TextType::class)
->add('add' , ButtonType::class, array('attr'=>array('class'=>'add-email')))
->add('department', EntityType::class, array(
'class' => 'UserBundle:Department',
'choice_label' => 'name',
));
}
public function getParent()
{
return 'FOS\UserBundle\Form\Type\RegistrationFormType';
}
public function getBlockPrefix()
{
return 'custom_user_registration';
}
public function getName()
{
return $this->getBlockPrefix();
}
}
jQuery funcion to add / remove form field:
$(document).ready(function() {
var max_fields = 10; //maximum input boxes allowed
var wrapper = $("div"); //Fields wrapper
var add_button = $(".add-email"); //Add button ID
var x = 1; //initlal text box count
$(add_button).click(function(e){ //on add input button click
e.preventDefault();
console.log('Clicking Add Button');
if(x < max_fields){ //max input box allowed
x++; //text box increment
$('.add-email').parent("div").prev().append('<div><input type="text" id="fos_user_registration_form_emails" name="fos_user_registration_form[emails]" required="required">Remove</div>');
}
});
$(wrapper).on("click",".remove_field", function(e){ //user click on remove text
e.preventDefault(); $(this).parent('div').remove(); x--;
})
});
User Entity:
/**
* #ORM\Column(type="text",nullable=true)
*/
protected $emails;
/**
* #return mixed
*/
public function getEmails()
{
return $this->emails;
}
/**
* #param mixed $emails
*/
public function setEmails($emails)
{
$this->emails = $emails;
}
Registration controller:
class RegistrationController extends BaseController
{
// public function registerAction(Request $request)
// {
//
// $response = parent::registerAction( $request );
//
// return $response;
// }
public function registerAction(Request $request)
{
$form = $this->container->get('fos_user.registration.form');
$formHandler = $this->container->get('fos_user.registration.form.handler');
$confirmationEnabled = $this->container->getParameter('fos_user.registration.confirmation.enabled');
$process = $formHandler->process($confirmationEnabled);
if ($process) {
$user = $form->getData();
$this->container->get('logger')->info(
sprintf('New user registration: %s', $user)
);
if ($confirmationEnabled) {
$this->container->get('session')->set('fos_user_send_confirmation_email/email', $user->getEmail());
$route = 'fos_user_registration_check_email';
} else {
$this->authenticateUser($user);
$route = 'fos_user_registration_confirmed';
}
$this->setFlash('fos_user_success', 'registration.flash.user_created');
$url = $this->container->get('router')->generate($route);
return new RedirectResponse($url);
}
return $this->container->get('templating')->renderResponse('FOSUserBundle:Registration:register.html.'.$this->getEngine(), array(
'form' => $form->createView(),
));
}
}
register.html.twig (overriden from FOS User Bundle):
{% extends "FOSUserBundle::layout.html.twig" %}
{% block fos_user_content %}
{% include "FOSUserBundle:Registration:register_content.html.twig" %}
{% endblock fos_user_content %}
It sounds like the fields being rendered/cloned by the js aren't having their names updated, and those that come later are overriding earlier fields with the same names.
For example, if I have a form rendered something like:
<!-- pseudo markup; yours will likely differ -->
<div class="form_row">
<input type="email" name="fos_user[email]" />
</div>
<button class="add_email" onclick="javascript:[...];">Add email</button>
And I click that .add-email button, giving me:
<div class="form_row">
<input type="email" name="fos_user[email]" />
</div>
<div class="form_row">
<input type="email" name="fos_user[email]" />
<button onclick="javascript:[...];">Remove</button>
</div>
<button class="add_email" onclick="javascript:[...];">Add email</button>
If I were to fill in both <input> fields, and submit the form, I'd essentially have have two values for the fos_user[email] input field. The browser takes the last one as the canonical value, ignoring all others with the same name. When the request goes out, it sends only one value field name.
To verify this hypothesis, you should examine the request data from the "network" panel in your browser's developer tools. Here's how to do that in Google Chrome: link.
Pic of google chrome dev tools examining an HTTP request. Where the user has selected remotedebugging.png, you might look for the main POST request to /register, or whatever your FOSUserBundle's register action is.
If this turns out to be the case - then the error lies with the way that your javascript is cloning the fields. You'll need to be certain that the fields have a tailing [n] on each of their name attributes. (Where n is a number.) The best way to to that is to use a collection.
But hold up... a collection!?. It looks like you've not primed your user model to hold a collection of emails.
<?php
namespace AppBundle\Entity\User;
use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
class User extends BaseUser
{
...
/**
* #ORM\Column(type="text",nullable=true)
*/
protected $emails;
You've named it in the plural, but it appears just like a normal string as far as Doctrine or your ORM are concerned.
The Fix
Change your User's $emails field to a simple_array type, which stores comma-delimited values in the db column. The example below uses a doctrine annotation, to do this. (Here's the doctrine reference for simple_array.) Other array-like options: "array" (stored w/ php serialization) "json" (stored as a json object)
Don't discard the $email field, (note the singular name) as I believe FOSUserBundle requires it when extending their User model class. Be sure to update your getters and setters (as in example below), and ensure the $emails property of your user is an empty ArrayCollection object on instantiation. Run doctrine:schema:update from the symfony console after doing this.
<?php
// src/AppBundle/Entities/User.php
namespace AppBundle\Entity\User;
use FOS\UserBundle\Entity\User as BaseUser;
use Doctrine\ORM\Mapping as ORM;
class User extends BaseUser
{
...
/**
* #ORM\Column(type="text", nullable=true)
*/
protected $email;
/**
* #ORM\Column(type="simple_array")
*/
protected $emails;
...
/**
* Instantiate the User
*/
public function __construct()
{
parent::__construct();
$this->emails = new \Doctrine\Common\Collections\ArrayCollection();
...
}
/**
* Add an email to the collection of emails
*
* #param string $email The email to add.
*
* #return User
*/
public function addEmail($email)
{
$this->emails[] = $email;
return $this;
}
/**
* Remove an email from the collection of emails
*
* #param string $email The email to disassociate from this user.
*
* #return User
*/
public function removeEmail($email)
{
$this->email->removeElement($email);
return $this;
}
/**
* Get all emails in colletion
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getEmails()
{
return $this->emails;
}
Extend your registration form further so it includes the collection field.
<?php
// src/AppBundle/Form/UserRegistrationType.php
namespace AppBundle\Form;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
class RegistrationFormType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
$builder->add(
...
'emails',
CollectionType::class,
[
'entry_type' => EmailType::class,
'allow_add' => true,
'allow_delete' => true
],
...
)
...
}
In my example, I'm extending the form outside the controller. When I extend the reg. form in FOSUserBundle, I let the bundle know in app/config.yml
// app/config.yml
...
fos_user:
registration:
form:
type: AppBundle\Form\RegistrationFormType
...
The allow_add key on the options array tells twig to render the emails field's container with a data-prototype attribute, which will include a __name__ placeholder string. That placeholder will help you number your form fields more accurately, and avoid the duplicate name trap we fell into above.
Render the form a little differently. Pull from Symfony's recommended javascript for CollectionType. From http://symfony.com/doc/current/reference/forms/types/collection.html:
{# '/project/base/app/Resources/FOSUserBundle/views/Registration/register_content.html.twig' #}
{{ form_start(form) }}
{# ... #}
{# store the prototype on the data-prototype attribute #}
<ul id="email-fields-list"
data-prototype="{{ form_widget(form.emails.vars.prototype)|e }}">
{% for emailField in form.emails %}
<li>
{{ form_errors(emailField) }}
{{ form_widget(emailField) }}
</li>
{% endfor %}
</ul>
Add another email
{# ... #}
{{ form_end(form) }}
<script type="text/javascript">
// keep track of how many email fields have been rendered
var emailCount = '{{ form.emails|length }}';
jQuery(document).ready(function() {
jQuery('#add-another-email').click(function(e) {
e.preventDefault();
var emailList = jQuery('#email-fields-list');
// grab the prototype template
var newWidget = emailList.attr('data-prototype');
// replace the "__name__" used in the id and name of the prototype
// with a number that's unique to your emails
// end name attribute looks like name="contact[emails][2]"
newWidget = newWidget.replace(/__name__/g, emailCount);
emailCount++;
// create a new list element and add it to the list
var newLi = jQuery('<li></li>').html(newWidget);
newLi.appendTo(emailList);
});
})
</script>
Notice how the script replaces a prototype's __name__ placeholder with that email's position in the form? That will render as something like:
<div class="form_row">
<input type="email" name="fos_user[emails][0]" />
</div>
<div class="form_row">
<input type="email" name="fos_user[emails][1]" />
<button onclick="javascript:[...];">Remove</button>
</div>
<button class="add_email" onclick="javascript:[...];">Add email</button>
... which will force the browser to treat each field individually.
If you have 10 inputs with name fos_user_registration_form[emails], the last one overrides the first nines. But if you change the name of e-mail field, Symfony does not recognize the field and send an error.
Solution for you: CollectionType Field. It allows you to create multiple fields for one attribute or delete existing ones. Also by default it generates prototype - a HTML template to insert new input into DOM.

Double validation errors in custom form type

I'm implementing a custom form type that provides an autocomplete field to select a location (country,city or spot). The form type creates two fields, one text field for the autocomplete search input and one hidden field to hold the selected id of the selected location.
When typing into the text field, a server call is made and results are displayed via jquery autocomplete. If a location is selected, the id of the selected location is written to the hidden field whereas the name of the location is displayed in the text field. On the server, I use a client transformer to lookup the entity of the id passed by the hidden field. The text field is ignored.
My model class defines a location field with a property to write back the location entity annotated with a NotNull validation constraint.
Everything works perfectly fine so far but if I do not select a location, the validation message "This value should not be null." is displayed two times.
The bundle is public and can be found in my github repo. The relevant classes are the LocationFieldType and the LocationDataTransformer and the form theme.
And now for how I'm integrating the form type into my project. I added the whole code, sorry for the mass;)
In the model, I define the property as following:
class JourneyCreate
{
/**
* #Assert\NotNull()
* #Assert\Choice(choices = {"offer", "request"})
*/
public $type;
/**
* #Assert\NotNull()
* #Assert\Date()
*/
public $date;
/**
* #Assert\NotNull()
* #Assert\Time()
*/
public $time;
/**
* #Assert\NotNull()
*
*/
public $start;
/**
* #Assert\NotNull()
*
*/
public $destination;
public function buildJourney(User $owner)
{
switch($this->type)
{
case 'offer':
$journey = new JourneyOffer();
break;
case 'request':
$journey = new JourneyRequest();
break;
default:
throw new \InvalidArgumentException('Invalid journey type');
}
$journey->setDate($this->date);
$journey->setTime($this->time);
$journey->addStation(new JourneyStation($this->start));
$journey->addStation(new JourneyStation($this->destination));
$journey->setOwner($owner);
return $journey;
}
}
And in the main form I add the field as following:
class JourneyCreateType extends BaseType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('type','choice', array(
'choices' => array(
'offer' => 'Driver',
'request' => 'Passanger',
),
'empty_value'=>'',
'multiple' => false,
'expanded' => true,
))
->add('date','date',array(
'widget' => 'single_text',
'format' => $this->getDateFormat(\IntlDateFormatter::TRADITIONAL),
))
->add('time','time',array(
'widget' => 'single_text',
))
->add('start','room13_geo_location')
->add('destination','room13_geo_location')
;
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'Acme\DemoBundle\Form\Model\JourneyCreate',
));
}
public function getName()
{
return 'journey_create';
}
}
And the controller code:
/**
* #Route("/create/{type}", defaults={"type" = null})
* #Template()
*/
public function createAction($type=null)
{
if($type !== null && !in_array($type,array('request','offer')))
{
throw new NotFoundHttpException();
}
$journeyCreate = new JourneyCreate();
$journeyCreate->type = $type;
$form = $this->createForm(new JourneyCreateType(),$journeyCreate);
if($this->isPost())
{
$form->bind($this->getRequest());
if($form->isValid())
{
$journeyCreate = $form->getData();
$journey = $journeyCreate->buildJourney($this->getCurrentUser());
$this->persistAndFlush($journey);
return $this->redirect($this->generateUrl('acme_demo_journey_edit',array('id'=>$journey->getId())));
}
}
return array(
'form' => $form->createView(),
);
}
And finaly the template code to display the form:
{% block page_body %}
<form class="form-horizontal" action="{{ path('acme_demo_journey_create') }}" method="post" novalidate>
{{form_widget(form)}}
<div class="form-actions">
<button class="btn btn-primary" type="submit">{{'form.submit'|trans}}</button>
{{'form.cancel'|trans}}
</div>
</form>
{% endblock %}
I'm having the theory that this could be because I use two form fields but don't know how to fix this. Any suggestions about how to solve this more elegant are welcome.
As complicated as this question might look, the answer is as simple as removing the {{form_errors(form)}} from the widget template block. Because the *form_row* block looks like:
{% block form_row %}
{% spaceless %}
<div class="form_row">
{{ form_label(form) }}
{{ form_errors(form) }}
{{ form_widget(form) }}
</div>
{% endspaceless %}
{% endblock form_row %}
The error was simply outputted two times.

How to submit multiple forms of same type with one button in symfony2

I have the todolist where i display three forms of task type
$task1 = new Task();
$form1 = $this->createForm(new MyForm('f1'), $task1);
$task2 = new Task('fo');
$form2 = $this->createForm(new MyForm('f2'), $task2);
$task3 = new Task();
$form3 = $this->createForm(new MyForm('f3'), $task3);
Now the problem is i have one submit button only . How can i persist these three tasks within one controller. and user can add more forms dynamically as well.
so what the way to solve this
Create a Form Model class — like TaskList — that holds a collection of Tasks. Then create TaskListType that holds a collection of TaskTypes. This way you'll have one form with as many tasks as you want.
For the sake of completeness find below a complete example.
You should create a new Model that represents the desired form. The point is that you probably don't want to affect Doctrine (eg. see doctrine:schema:update command). It might try to create a table for an entity that doesn't really exist. To avoid that, just put your model class under the Model folder (\src\Acme\Bundle\DemoBundle\Model\TaskList.php).
Assume that the following is your TaskType form class:
<?php
namespace Acme\Bundle\DemoBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TaskType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('id', null, array('read_only' => true))
->add('name');
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'Acme\Bundle\DemoBundle\Entity\Task'
)
);
}
/**
* #return string
*/
public function getName()
{
return 'acme_demo_task';
}
}
This should be your TaskList model class:
<?php
namespace Acme\Bundle\DemoBundle\Model;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Class TaskList
* #package Acme\Bundle\DemoBundle\Model
*
* #ORM\Entity()
*/
class TaskList
{
/**
* #var \Doctrine\Common\Collections\ArrayCollection
* #ORM\ManyToMany(targetEntity="\Acme\Bundle\DemoBundle\Entity\Task")
*/
private $tasks;
public function __construct()
{
$this->tasks = new ArrayCollection();
}
/**
* #param \Acme\Bundle\DemoBundle\Entity\Task $task
* #return $this
*/
public function addTask(\Acme\Bundle\DemoBundle\Entity\Task $task)
{
$this->tasks[] = $task;
return $this;
}
/**
* #param \Acme\Bundle\DemoBundle\Entity\Task $task
* #return $this
*/
public function removeTask(\Acme\Bundle\DemoBundle\Entity\Task $task)
{
$this->tasks->remove($task);
return $this;
}
/**
* #return ArrayCollection
*/
public function getTasks()
{
return $this->tasks;
}
/**
* #param \Doctrine\Common\Collections\Collection $tasks
* #return $this
*/
public function setTasks(\Doctrine\Common\Collections\Collection $tasks)
{
$this->tasks = $tasks;
return $this;
}
/**
* #param \Knp\Component\Pager\Pagination\PaginationInterface $pagination
* #return $this
*/
public function setFromPagination(\Knp\Component\Pager\Pagination\PaginationInterface $pagination)
{
foreach ($pagination as $task) {
$this->addTask($task);
}
return $this;
}
}
And find below the TaskListType class:
<?php
namespace Acme\Bundle\DemoBundle\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TaskListType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'tasks',
'collection',
array(
'type' => new \Acme\Bundle\DemoBundle\Form\TaskType(),
)
)
->add('save', 'submit');
}
/**
* #param OptionsResolverInterface $resolver
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(
array(
'data_class' => 'Acme\Bundle\DemoBundle\Model\TaskList'
)
);
}
/**
* #return string
*/
public function getName()
{
return 'acme_demo_task_list';
}
}
And your services.yml (optional):
services:
acme.demo.form.type.task_list:
class: Acme\Bundle\DemoBundle\Form\TaskListType
tags:
- { name: form.type, alias: acme_demo_task_list }
And a sample controller:
public function indexAction($page)
{
ini_set('xdebug.max_nesting_level', 300); // this might be useful with deeply nested forms
$search = $this->getRequest()->get(
'search',
array(
'name' => '',
'date' => '',
'lang' => $this->container->getParameter('acme_core.default_lang')
)
);
/**
* #var \Doctrine\ORM\EntityManager $em
*/
$em = $this->getDoctrine()->getManager();
$paginator = $this->get('knp_paginator');
$pagination = $paginator->paginate(
$em->getRepository('AcmeDemoBundle:Task')->getQueryFilteringByLangNameAndDate(
$search['lang'],
$search['name'],
$search['date'] != '' ? new \DateTime($search['date']) : null
),
$page,
$this->getRequest()->get('elementsPerPage', 10)
);
$taskList = new TaskList();
$taskList->setFromPagination($pagination);
$form = $this->createForm('acme_demo_task_list', $taskList); // "acme_demo_task_list" has been defined in the services.yml file
$form->handleRequest($this->getRequest());
if ($form->isValid()) {
foreach ($form->getData() as $task) {
$em->merge($task);
}
$em->flush();
}
return $this->render(
'AcmeDemoBundle:Task:index.html.twig',
array(
'search' => $search,
'pagination' => $pagination,
'form' => $form->createView()
)
);
}
I hope this helps!
We followed the exapmle shown by 'Francesco Casula' and it worked perfectly.
For orur purposes we didn't need pagination, so this is how we filled our collection (in the controller):
$entities = $em->getRepository('AcmeBundle:Stock')->findAll();
$stockList = new StockList(); // This is our model, what Francesco called 'TaskList'
foreach ($entities as $entity) {
$stockList->addStock($entity);
}
// StockListType() is what Francesco called TaskListType
$form = $this->createForm(new StockListType(), $stockList, array(
'action' => $this->generateUrl('stock_take_update'),
'method' => 'POST',
'attr' => array('class' => 'form-horizontal'),
));
For those who needs to customise the output of the form collections, we managed to access the subform by iterating on {% for form in forms.children.stocks %}. 'Stocks' being the name of the field in our form builder (in StockListType):
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add(
'stocks', 'collection', array(
'type' => new StockType(),
)
)
->add('submit', 'submit')
;
}
This is what we ended up using in our twig view:
{{ form_start(forms) }}
<table class="table table-stripped table-hover">
<thead>
<th>#</th>
<th>Name</th>
<th>Code</th>
<th>Location</th>
<th>Total</th>
<th>Updated Toal</th>
</thead>
<tbody>
{% set counter = 1 %}
{% for form in forms.children.stocks %}
<tr>
<div class="hidden">
{{ form_widget(form.name) }}
{{ form_widget(form.code) }}
{{ form_widget(form.location) }}
{{ form_widget(form.available) }}
{{ form_widget(form.assigned) }}
{{ form_widget(form.minLevel) }}
{{ form_widget(form.type) }}
{{ form_widget(form.colourCode) }}
</div>
<td>{{ counter }}</td>
<td>
{% if form.vars.data.name is defined %}
{{ form.vars.data.name }}
{% endif %}
</td>
<td>
{% if form.vars.data.code is defined %}
{{ form.vars.data.code }}
{% endif %}
</td>
<td>
{% if form.vars.data.location is defined %}
{{ form.vars.data.location }}
{% endif %}
</td>
<td>
{% if form.vars.data.total is defined %}
{{ form.vars.data.total }}
{% endif %}
</td>
<td>{{ form_widget(form.total) }}</td>
</tr>
{% set counter = counter + 1 %}
{% endfor %}
</tbody>
</table>
{{ form_widget(forms.submit) }}
{{ form_end(forms) }}

New entities added using Symfony2 many-to-one embedded forms not being saved

This is related to my earlier question about embedded forms. As advised, I switched to a twig template and now everything is displaying as expected and the link to add a new empty form is working correctly. The problem is that when I try to save a new record, it doesn't work (although edits to existing entities are saved).
There are several points where I may have gone wrong, so I'm going to ask questions as I go along.
Here's some background:
A study can have many participants (i.e. a Study entity has a OneToMany relationship with the entity Participant). In the database, each Participant record has the foreign key link from the column "study" to the "study_id" column of a record in the Study table, making it the owning side of the relation. The annotation in the classes should reflect this relationship.
Study class:
namespace MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* CRUK\MyBundle\Entity\Study
*
* #ORM\Table(name="study")
* #ORM\Entity
*/
class Study
{
/**
* #var integer $id
*
* #ORM\Column(name="study_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string $studyName
*
* #ORM\Column(name="study_name", type="string", length=50, nullable=false)
*/
private $studyName;
/*
* #ORM\OneToMany(targetEntity="Participant", mappedBy="study", cascade={"persist"})
*
* #var ArrayCollection $participants
*/
protected $participants;
public function __construct()
{
$this->participants = new ArrayCollection();
}
public function setParticipants(ArrayCollection $participants)
{
foreach($participants as $participant) {
$participant->setStudy($this);
}
$this->participants = $participants;
}
/**
* #return ArrayCollection A Doctrine ArrayCollection
*/
public function getParticipants()
{
return $this->participants;
}
}
My Participant class:
namespace MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* CRUK\SampleTrackingBundle\Entity\Participant
*
* #ORM\Table(name="participant")
* #ORM\Entity
*/
class Participant
{
/**
* #var integer $id
*
* #ORM\Column(name="participant_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
...
/**
* #var study
*
* #ORM\ManyToOne(targetEntity="Study", inversedBy="participants")
* #ORM\JoinColumn(name="study", referencedColumnName="study_id")
*
*/
private $study;
//setters and getters...
}
First of all, are these annotations correct? (I'm pretty sure I got the whole owning/inverse many-to-one/one-to many relationship straight in my head, but I could be mistaken)
My controller:
Class StudyController extends Controller
{
...
public function addParticipantsAction($id)
{
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('MyBundle:Study')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Study id='.$id);
}
$participantArray = $em->getRepository('MyBundle:Participant')->findByStudy($id);
//this is supposed to return a Doctrine ArrayCollection, but for some reason, returns an array
// This needs to be converted to an ArrayCollection
$participants = new ArrayCollection();
foreach ($participantArray as $participant) {
$participants->add($participant);
}
$entity->setParticipants($participants);
$form = $this->createForm(new StudyType(), $entity);
$request = $this->getRequest();
if ('POST' === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
$em->persist($entity);
$em->flush();
}
}
return $this->render('MyBundle:Study:addParticipants.html.twig', array(
'form' => $form->createView(),
'entity' => $entity
));
}
...
}
At this point I have to ask why it is neccessary to explicitly fetch the collection of participants and use it to set the collection on the study entity? Before I added that code, $entity->getParticipants() would return null (even when I know there were several participants with the foreign key set for the study). I have two other tables in a many-to-many relationship where the collections seem to come up automatically just by having the correct annotations in the entity classes. Is this a difference between a many-to-many mapping vs. a many-to-one, or have I messed up the annotation somehow?
I'm not sure if the rest of the code will help, but here's some more:
My study form class:
class StudyType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('studyName', null, array('label'=> 'Study Name:'))
->add('participants', 'collection', array(
'type'=> new ParticipantType(),
'allow_add'=>true,
'by_reference'=>false
));
}
public function getName()
{
return 'study';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'MyBundle\Entity\Study',
);
}
}
My embedded form class:
class ParticipantType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('participantId','text', array('label'=>'Participant ID'))
));
}
public function getName()
{
return 'participant';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'MyBundle\Entity\Participant',
);
}
}
My template:
{% extends 'MyBundle::base.html.twig' %}
{% block body%}
<form action="{{ path('study_addparticipants', { 'id': entity.id }) }}" method="POST" {{ form_enctype(form) }}>
<!-- renders global errors -->
{{ form_errors(form) }}
<h2>Study</h2>
{{ form_label(form.studyName) }}
{{ form_errors(form.studyName) }}
{{ form_widget(form.studyName) }}
<h3>Participants in this study</h3>
<ul class="participants" data-prototype="{{ form_widget(form.participants.get('prototype')) | e }}">
{% for participant in form.participants %}
<li>{{form_row(participant) }}</li>
{% endfor %}
</ul>
{{ form_rest(form) }}
<button type="submit">Save Changes</button>
</form>
{% endblock%}
{% block javascripts %}
{# parent block includes jQuery #}
{{ parent() }}
<script type='text/javascript'>
jQuery(document).ready(function() {
// keep track of how many participant fields have been rendered
var collectionHolder = $('ul.participants');
var $addLink = $('Add new Participant');
var $newLinkLi = $('<li></li>'). append($addLink);
collectionHolder.append($newLinkLi);
$addLink.on('click', function(e) {
e.preventDefault();
addParticipantForm(collectionHolder, $newLinkLi);
});
});
function addParticipantForm(collectionHolder, $newLinkLi) {
// Get the data-prototype we explained earlier
var prototype = collectionHolder.attr('data-prototype');
// Replace '$$name$$' in the prototype's HTML to
// instead be a number based on the current collection's length.
var newForm = prototype.replace(/\$\$name\$\$/g, collectionHolder.children().length);
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
}
</script>
{% endblock %}
So, the form displays correctly and when I click the "Add new participant" link, an empty participant form is appended. Changes to the Study and exisitng participant records are saved. There are no errors, but any new participants are not saved.
I have read many similar questions to this one and as far as I know, incorporated everything that should make this work. I've obviously missed something, so would appreciate any suggestions on how to put this right.
Many Thanks.
Thanks to #Luke for the advice. I have solved the problem by looping through the pariticpants collection of my study object and saving each one individually in my controller. The new contoller code:
...
if ('POST' === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
$em->persist($entity);
foreach($entity->getParticipants() as $participant) {
$em->persist($participant);
}
// flush once to commit all entities
$em->flush();
}
}
...

Resources