Symfony2 object collection - how to add / remove links to existing objects?

1. Brief Description

1.1 Purpose

What I'm trying to achieve is to create / modify a custom tool. Editable fields:

  • username (type: text)
  • plainPassword (type: password)
  • email (enter: email)
  • groups (type: collection)
  • avoRoles (type: collection)

Note: the last property is not named $ role , because my User class extends the FOSUserBundle user class and overwrites roles, causing more problems. To avoid them, I just decided to keep my collection of roles under $ avoRoles .

1.2 User Interface

My template consists of two sections:

  • User form
  • Table showing $ userRepository-> findAllRolesExceptOwnedByUser ($ user);

Note: findAllRolesExceptOwnedByUser () is a user repository function that returns a subset of all roles (which are not yet assigned to $ user).

1.3 Desired Functionality

1.3.1 Add role:

     WHEN user clicks "+" (add) button in Roles table  
     THEN jquery removes that row from Roles table  
     AND jquery adds new list item to User form (avoRoles list)

1.3.2 Removing roles:

     WHEN user clicks "x" (remove) button in User form (avoRoles list)  
     THEN jquery removes that list item from User form (avoRoles list)  
     AND jquery adds new row to Roles table

1.3.3 Save changes:

     WHEN user clicks "Zapisz" (save) button  
     THEN user form submits all fields (username, password, email, avoRoles, groups)  
     AND saves avoiding Roles as an ArrayCollection of Role entities (ManyToMany relation)  
     AND saves groups as an ArrayCollection of Role entities (ManyToMany relation)  

Note. Only ONLY existing roles and groups can be assigned to the user. If for any reason they are not found, the form should not be checked.




2. Code

In this section, I present / or briefly describe the code for this action. If the description is not enough and you need to see the code, just tell me and I will paste it. I am not going to use all this in the first place so as not to send you unnecessary code.

2.1 User Class

The My User class extends the FOSUserBundle user class.

namespace Avocode\UserBundle\Entity; use FOS\UserBundle\Entity\User as BaseUser; use Doctrine\ORM\Mapping as ORM; use Avocode\CommonBundle\Collections\ArrayCollection; use Symfony\Component\Validator\ExecutionContext; /** * @ORM\Entity(repositoryClass="Avocode\UserBundle\Repository\UserRepository") * @ORM\Table(name="avo_user") */ class User extends BaseUser { const ROLE_DEFAULT = 'ROLE_USER'; const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN'; /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\generatedValue(strategy="AUTO") */ protected $id; /** * @ORM\ManyToMany(targetEntity="Group") * @ORM\JoinTable(name="avo_user_avo_group", * joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}, * inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id")} * ) */ protected $groups; /** * @ORM\ManyToMany(targetEntity="Role") * @ORM\JoinTable(name="avo_user_avo_role", * joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}, * inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")} * ) */ protected $avoRoles; /** * @ORM\Column(type="datetime", name="created_at") */ protected $createdAt; /** * User class constructor */ public function __construct() { parent::__construct(); $this->groups = new ArrayCollection(); $this->avoRoles = new ArrayCollection(); $this->createdAt = new \DateTime(); } /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set user roles * * @return User */ public function setAvoRoles($avoRoles) { $this->getAvoRoles()->clear(); foreach($avoRoles as $role) { $this->addAvoRole($role); } return $this; } /** * Add avoRole * * @param Role $avoRole * @return User */ public function addAvoRole(Role $avoRole) { if(!$this->getAvoRoles()->contains($avoRole)) { $this->getAvoRoles()->add($avoRole); } return $this; } /** * Get avoRoles * * @return ArrayCollection */ public function getAvoRoles() { return $this->avoRoles; } /** * Set user groups * * @return User */ public function setGroups($groups) { $this->getGroups()->clear(); foreach($groups as $group) { $this->addGroup($group); } return $this; } /** * Get groups granted to the user. * * @return Collection */ public function getGroups() { return $this->groups ?: $this->groups = new ArrayCollection(); } /** * Get user creation date * * @return DateTime */ public function getCreatedAt() { return $this->createdAt; } } 

2.2 Role Class

The My Role class extends the Symfony Security Component Core role class.

 namespace Avocode\UserBundle\Entity; use Doctrine\ORM\Mapping as ORM; use Avocode\CommonBundle\Collections\ArrayCollection; use Symfony\Component\Security\Core\Role\Role as BaseRole; /** * @ORM\Entity(repositoryClass="Avocode\UserBundle\Repository\RoleRepository") * @ORM\Table(name="avo_role") */ class Role extends BaseRole { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\generatedValue(strategy="AUTO") */ protected $id; /** * @ORM\Column(type="string", unique="TRUE", length=255) */ protected $name; /** * @ORM\Column(type="string", length=255) */ protected $module; /** * @ORM\Column(type="text") */ protected $description; /** * Role class constructor */ public function __construct() { } /** * Returns role name. * * @return string */ public function __toString() { return (string) $this->getName(); } /** * Get id * * @return integer */ public function getId() { return $this->id; } /** * Set name * * @param string $name * @return Role */ public function setName($name) { $name = strtoupper($name); $this->name = $name; return $this; } /** * Get name * * @return string */ public function getName() { return $this->name; } /** * Set module * * @param string $module * @return Role */ public function setModule($module) { $this->module = $module; return $this; } /** * Get module * * @return string */ public function getModule() { return $this->module; } /** * Set description * * @param text $description * @return Role */ public function setDescription($description) { $this->description = $description; return $this; } /** * Get description * * @return text */ public function getDescription() { return $this->description; } } 

2.3 Class of groups

Since I have the same problem with groups as with roles, I skip them here. If I get roles, I know that I can do the same with groups.

2.4 controller

 namespace Avocode\UserBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\Security\Core\SecurityContext; use JMS\SecurityExtraBundle\Annotation\Secure; use Avocode\UserBundle\Entity\User; use Avocode\UserBundle\Form\Type\UserType; class UserManagementController extends Controller { /** * User create * @Secure(roles="ROLE_USER_ADMIN") */ public function createAction(Request $request) { $em = $this->getDoctrine()->getEntityManager(); $user = new User(); $form = $this->createForm(new UserType(array('password' => true)), $user); $roles = $em->getRepository('AvocodeUserBundle:User') ->findAllRolesExceptOwned($user); $groups = $em->getRepository('AvocodeUserBundle:User') ->findAllGroupsExceptOwned($user); if($request->getMethod() == 'POST' && $request->request->has('save')) { $form->bindRequest($request); if($form->isValid()) { /* Persist, flush and redirect */ $em->persist($user); $em->flush(); $this->setFlash('avocode_user_success', 'user.flash.user_created'); $url = $this->container->get('router')->generate('avocode_user_show', array('id' => $user->getId())); return new RedirectResponse($url); } } return $this->render('AvocodeUserBundle:UserManagement:create.html.twig', array( 'form' => $form->createView(), 'user' => $user, 'roles' => $roles, 'groups' => $groups, )); } } 

2.5 User Repositories

You should not publish this, as they work very well - they return a subset of all Roles / groups (not assigned to the user).

2.6 UserType

UserType:

 namespace Avocode\UserBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilder; class UserType extends AbstractType { private $options; public function __construct(array $options = null) { $this->options = $options; } public function buildForm(FormBuilder $builder, array $options) { $builder->add('username', 'text'); // password field should be rendered only for CREATE action // the same form type will be used for EDIT action // thats why its optional if($this->options['password']) { $builder->add('plainpassword', 'repeated', array( 'type' => 'text', 'options' => array( 'attr' => array( 'autocomplete' => 'off' ), ), 'first_name' => 'input', 'second_name' => 'confirm', 'invalid_message' => 'repeated.invalid.password', )); } $builder->add('email', 'email', array( 'trim' => true, )) // collection_list is a custom field type // extending collection field type // // the only change is diffrent form name // (and a custom collection_list_widget) // // in short: it a collection field with custom form_theme // ->add('groups', 'collection_list', array( 'type' => new GroupNameType(), 'allow_add' => true, 'allow_delete' => true, 'by_reference' => true, 'error_bubbling' => false, 'prototype' => true, )) ->add('avoRoles', 'collection_list', array( 'type' => new RoleNameType(), 'allow_add' => true, 'allow_delete' => true, 'by_reference' => true, 'error_bubbling' => false, 'prototype' => true, )); } public function getName() { return 'avo_user'; } public function getDefaultOptions(array $options){ $options = array( 'data_class' => 'Avocode\UserBundle\Entity\User', ); // adding password validation if password field was rendered if($this->options['password']) $options['validation_groups'][] = 'password'; return $options; } } 

2.7 Type RoleNameType

This form should display:

  • hidden role id
  • Role Name (READ ONLY)
  • hidden module (ONLY READ)
  • hidden description (ONLY READ) Button
  • remove (x)

The module and description are displayed as hidden fields, because when the administrator deletes the user role, this role must be added by the jQuery command to the role table, and this table has the "Module" and "Description" columns.

 namespace Avocode\UserBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilder; class RoleNameType extends AbstractType { public function buildForm(FormBuilder $builder, array $options) { $builder ->add('', 'button', array( 'required' => false, )) // custom field type rendering the "x" button ->add('id', 'hidden') ->add('name', 'label', array( 'required' => false, )) // custom field type rendering <span> item instead of <input> item ->add('module', 'hidden', array('read_only' => true)) ->add('description', 'hidden', array('read_only' => true)) ; } public function getName() { // no_label is a custom widget that renders field_row without the label return 'no_label'; } public function getDefaultOptions(array $options){ return array('data_class' => 'Avocode\UserBundle\Entity\Role'); } } 



3. Current / known issues

3.1 Case 1: configuration above

The above configuration returns an error:

 Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"? 

But the set parameter for ID is not required.

  • At first I do not want to create a new role. I want to simply create a relationship between existing Role and User objects.
  • Even if I wanted to create a new role, its identifier should be generated automatically:

    / **

    • @ORM \ Id
    • @ORM \ Column (type = "integer")
    • @ORM \ GeneratedValue (strategy = "AUTO") * / protected $ id;

3.2 Case 2: Added setter for the ID property in the Role object

I think this is wrong, but I did it to be sure. After adding this code to the Role object:

 public function setId($id) { $this->id = $id; return $this; } 

If I create a new user and add a role, then SAVE ... What happens:

  • New user created.
  • The new user has a role with an assigned identifier (yay!)
  • , but the name of this role is overwritten with an empty string (bummer!)

Obviously, this is not what I want. I do not want to edit / rewrite roles. I just want to add a connection between them and the user.

3.3 Case 3: Workaround proposed by Jeppe

When I first encountered this problem, I got a workaround, the same thing that Geppe suggested. Today (for other reasons) I had to redo my form / presentation, and the workaround stopped working.

What are the changes in Case3 UserManagementController -> createAction:

  // in createAction // instead of $user = new User $user = $this->updateUser($request, new User()); //and below updateUser function /** * Creates mew iser and sets its properties * based on request * * @return User Returns configured user */ protected function updateUser($request, $user) { if($request->getMethod() == 'POST') { $avo_user = $request->request->get('avo_user'); /** * Setting and adding/removeing groups for user */ $owned_groups = (array_key_exists('groups', $avo_user)) ? $avo_user['groups'] : array(); foreach($owned_groups as $key => $group) { $owned_groups[$key] = $group['id']; } if(count($owned_groups) > 0) { $em = $this->getDoctrine()->getEntityManager(); $groups = $em->getRepository('AvocodeUserBundle:Group')->findById($owned_groups); $user->setGroups($groups); } /** * Setting and adding/removeing roles for user */ $owned_roles = (array_key_exists('avoRoles', $avo_user)) ? $avo_user['avoRoles'] : array(); foreach($owned_roles as $key => $role) { $owned_roles[$key] = $role['id']; } if(count($owned_roles) > 0) { $em = $this->getDoctrine()->getEntityManager(); $roles = $em->getRepository('AvocodeUserBundle:Role')->findById($owned_roles); $user->setAvoRoles($roles); } /** * Setting other properties */ $user->setUsername($avo_user['username']); $user->setEmail($avo_user['email']); if($request->request->has('generate_password')) $user->setPlainPassword($user->generateRandomPassword()); } return $user; } 

Unfortunately, this does not change anything. The results are either CASE1 (without an identifier) ​​or CASE2 (with an ID installer).

3.4 Case 4: as suggested by the user

Adding a cascade = {"persist", "remove"} to the display.

 /** * @ORM\ManyToMany(targetEntity="Group", cascade={"persist", "remove"}) * @ORM\JoinTable(name="avo_user_avo_group", * joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}, * inverseJoinColumns={@ORM\JoinColumn(name="group_id", referencedColumnName="id")} * ) */ protected $groups; /** * @ORM\ManyToMany(targetEntity="Role", cascade={"persist", "remove"}) * @ORM\JoinTable(name="avo_user_avo_role", * joinColumns={@ORM\JoinColumn(name="user_id", referencedColumnName="id")}, * inverseJoinColumns={@ORM\JoinColumn(name="role_id", referencedColumnName="id")} * ) */ protected $avoRoles; 

And changing by_reference to false in FormType:

 // ... ->add('avoRoles', 'collection_list', array( 'type' => new RoleNameType(), 'allow_add' => true, 'allow_delete' => true, 'by_reference' => false, 'error_bubbling' => false, 'prototype' => true, )); // ... 

And while supporting the workaround proposed in 3.3, something changed:

  • An association between a user and a role has not been created
  • .. but the name of the role entity was overwritten with an empty string (as in 3.2)

So, he changed something, but in the wrong direction.

4. Versions

4.1 Symfony2 v2.0.15

4.2 Doctrine2 v2.1.7

4.3 FOSUserBundle Version: 6fb81861d84d460f1d070ceb8ec180aac841f7fa

5. Summary

I tried many different approaches (only the most recent ones are above), and after several hours spent learning the code, google'ing and finding the answer, I just couldn't get it to work.

Any help would be greatly appreciated. If you need to know something, I will send you any part of the code.

+65
collections php forms symfony entity
Jun 18 '12 at 19:40
source share
5 answers

So, a year has passed, and this question has become quite popular. Symfony has changed since then, my skills and knowledge have also improved, and therefore my current approach to this problem.

I created a set of form extensions for symfony2 (see the FormExtensionsBundle project on github), and they include the form type for handling One / Many ToMany .

When writing this data, adding custom code to the controller for processing collections was unacceptable - the form extension should have been easy to use, ready to work and make life easier for us developers, and not more difficult. Also .. remember .. DRY!

So I had to move the add / remove code elsewhere - and the EventListener was naturally the right place for this :)

Take a look at EventListener / CollectionUploadListener.php to see how we do it now.

PS. Copying the code here is not necessary, the most important thing is that in fact this should be handled in the EventListener.

+9
Jun 13 '14 at 10:17
source share

I came to the same conclusion that something is wrong with the Form component and cannot find a simple way to fix it. However, I came up with a slightly less cumbersome workaround solution that is completely general; it does not have hard-coded knowledge of entities / attributes, therefore, it corrects any collection that it encounters:

Simplified general workaround

This does not require any changes to your essence.

 use Doctrine\Common\Collections\Collection; use Symfony\Component\Form\Form; # In your controller. Or possibly defined within a service if used in many controllers /** * Ensure that any removed items collections actually get removed * * @param \Symfony\Component\Form\Form $form */ protected function cleanupCollections(Form $form) { $children = $form->getChildren(); foreach ($children as $childForm) { $data = $childForm->getData(); if ($data instanceof Collection) { // Get the child form objects and compare the data of each child against the object current collection $proxies = $childForm->getChildren(); foreach ($proxies as $proxy) { $entity = $proxy->getData(); if (!$data->contains($entity)) { // Entity has been removed from the collection // DELETE THE ENTITY HERE // eg doctrine: // $em = $this->getDoctrine()->getEntityManager(); // $em->remove($entity); } } } } } 

Call the new cleanupCollections() method before saving

 # in your controller action... if($request->getMethod() == 'POST') { $form->bindRequest($request); if($form->isValid()) { // 'Clean' all collections within the form before persisting $this->cleanupCollections($form); $em->persist($user); $em->flush(); // further actions. return response... } } 
+12
Oct 10
source share

1. Workaround

The workaround proposed by Jeppe Marianger-Lam is currently the only one that I know of.

1.1 Why did this stop working in my case?

I changed my RoleNameType (for other reasons) to:

  • ID (hidden)
  • name (custom type - label)
  • module and description (hidden, read-only)

The problem was that my custom label type displayed the NAME property as

     <span> role name </span>

And since it was not read-only, the FORM component was supposed to get NAME in POST.

Instead, only the ID was POSTED, and thus the FORM component, presumably NAME, is NULL.

This leads to the creation of the CASE 2 (3.2) β†’ association, but overwrites the ROLE NAME with an empty string.

2. So, what does this turn to?

2.1 Controller

This workaround is very simple.

In your controller, before the VALIDATE form, you must get the published identifiers of the entities and get the corresponding objects, and then install them in your object.

 // example action public function createAction(Request $request) { $em = $this->getDoctrine()->getEntityManager(); // the workaround code is in updateUser function $user = $this->updateUser($request, new User()); $form = $this->createForm(new UserType(), $user); if($request->getMethod() == 'POST') { $form->bindRequest($request); if($form->isValid()) { /* Persist, flush and redirect */ $em->persist($user); $em->flush(); $this->setFlash('avocode_user_success', 'user.flash.user_created'); $url = $this->container->get('router')->generate('avocode_user_show', array('id' => $user->getId())); return new RedirectResponse($url); } } return $this->render('AvocodeUserBundle:UserManagement:create.html.twig', array( 'form' => $form->createView(), 'user' => $user, )); } 

And under the workaround in the updateUser function:

 protected function updateUser($request, $user) { if($request->getMethod() == 'POST') { // getting POSTed values $avo_user = $request->request->get('avo_user'); // if no roles are posted, then $owned_roles should be an empty array (to avoid errors) $owned_roles = (array_key_exists('avoRoles', $avo_user)) ? $avo_user['avoRoles'] : array(); // foreach posted ROLE, get it ID foreach($owned_roles as $key => $role) { $owned_roles[$key] = $role['id']; } // FIND all roles with matching ID's if(count($owned_roles) > 0) { $em = $this->getDoctrine()->getEntityManager(); $roles = $em->getRepository('AvocodeUserBundle:Role')->findById($owned_roles); // and create association $user->setAvoRoles($roles); } return $user; } 

For this to work your SETTER (in this case, in the User.php object) should be:

 public function setAvoRoles($avoRoles) { // first - clearing all associations // this way if entity was not found in POST // then association will be removed $this->getAvoRoles()->clear(); // adding association only for POSTed entities foreach($avoRoles as $role) { $this->addAvoRole($role); } return $this; } 

3. Final thoughts

However, I think this workaround does work that

 $form->bindRequest($request); 

must do! Either I'm doing something wrong, or the form type of the symfony collection is not complete.

There are significant changes to the Form component in Symform 2.1 , hopefully this will be fixed.

PS. If he does something wrong ...

... please write how this should be done! I would be happy to see a quick, simple and clean solution.

PS2. Special thanks to:

Jeppe Marianger-Lam and a user friendly (from # symfony2 on IRC). You have helped a lot. Hurrah!

+8
Jun 19 2018-12-12T00: 00Z
source share

This is what I did before - I don’t know whether to do it right, but it works.

When you get the results from the submitted form (that is, just before and immediately after if($form->isValid()) ), just ask for the list of roles, then remove them all from the entity (saving the list as a variable). In this list, just skip them all, ask the repository for the role object that matches the identifiers, and add them to your custom object before persist and flush .

I just looked through the Symfony2 documentation because I remembered something about prototype for form collections, and it turned out: http://symfony.com/doc/current/cookbook/form/form_collections.html - It has examples of how handle javascript correctly to add and remove collection types in forms. Perhaps try this approach first, and then try what I mentioned above if you cannot make it work :)

+6
Jun 18 2018-12-18T00:
source share

You need a few more objects:
USER
id_user (type: integer)
username (type: text)
plainPassword (type: password)
email (enter: email)




GROUPS
id_group (type: integer)
descripcion (type: text)




AVOROLES
id_avorole (type: integer)
descripcion (type: text)




* user_group *
id_user_group (type: integer)
id_user (type: integer) (this is a user id)

id_group (type: integer) (this is the identifier of the group object)




* USER_AVOROLES *
id_user_avorole (type: integer)
id_user (type: integer) (this is a user id)

id_avorole (type: integer) (this is the identifier of an avorole object)




You may have, for example, something like this:
User:
id: 3
username: john
plainPassword: johnpw
email: john@email.com


groups:
id_group: 5
descripcion: group 5


user_group:
id_user_group: 1
id_user: 3
id_group: 5
* this user can have many groups, so on a different line *

0
Nov 21 '13 at 13:26
source share



All Articles