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; class User extends BaseUser { const ROLE_DEFAULT = 'ROLE_USER'; const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN'; protected $id; protected $groups; protected $avoRoles; protected $createdAt; public function __construct() { parent::__construct(); $this->groups = new ArrayCollection(); $this->avoRoles = new ArrayCollection(); $this->createdAt = new \DateTime(); } public function getId() { return $this->id; } public function setAvoRoles($avoRoles) { $this->getAvoRoles()->clear(); foreach($avoRoles as $role) { $this->addAvoRole($role); } return $this; } public function addAvoRole(Role $avoRole) { if(!$this->getAvoRoles()->contains($avoRole)) { $this->getAvoRoles()->add($avoRole); } return $this; } public function getAvoRoles() { return $this->avoRoles; } public function setGroups($groups) { $this->getGroups()->clear(); foreach($groups as $group) { $this->addGroup($group); } return $this; } public function getGroups() { return $this->groups ?: $this->groups = new ArrayCollection(); } 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; class Role extends BaseRole { protected $id; protected $name; protected $module; protected $description; public function __construct() { } public function __toString() { return (string) $this->getName(); } public function getId() { return $this->id; } public function setName($name) { $name = strtoupper($name); $this->name = $name; return $this; } public function getName() { return $this->name; } public function setModule($module) { $this->module = $module; return $this; } public function getModule() { return $this->module; } public function setDescription($description) { $this->description = $description; return $this; } 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 { 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()) { $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');
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, ))
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.
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:
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.
protected $groups; protected $avoRoles;
And changing by_reference to false in FormType:
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
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.