1. Quick overview
1.1 Goal
What I'm trying to achieve is a create/edit user tool. Editable fields are:
- username (type: text)
- plainPassword (type: password)
- email (type: email)
- groups (type: collection)
- avoRoles (type: collection)
Note: the last property is not named $roles becouse my User class is extending FOSUserBundle's User class and overwriting roles brought more problems. To avoid them I simply decided to store my collection of roles under $avoRoles.
1.2 User Interface
My template consists of 2 sections:
- User form
- Table displaying $userRepository->findAllRolesExceptOwnedByUser($user);
Note: findAllRolesExceptOwnedByUser() is a custom repository function, returns a subset of all roles (those 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 Remove 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 avoRoles as an ArrayCollection of Role entities (ManyToMany relation)
AND saves groups as an ArrayCollection of Role entities (ManyToMany relation)
Note: ONLY existing Roles and Groups can be assigned to User. If for any reason they are not found the form should not validate.
2. Code
In this section I present/or shortly describe code behind this action. If description is not enough and you need to see the code just tell me and I'll paste it. I'm not pasteing it all in the first place to avoid spamming you with unnecessary code.
2.1 User class
My User class extends FOSUserBundle user class.
namespace AvocodeUserBundleEntity;
use FOSUserBundleEntityUser as BaseUser;
use DoctrineORMMapping as ORM;
use AvocodeCommonBundleCollectionsArrayCollection;
use SymfonyComponentValidatorExecutionContext;
/**
* @ORMEntity(repositoryClass="AvocodeUserBundleRepositoryUserRepository")
* @ORMTable(name="avo_user")
*/
class User extends BaseUser
{
const ROLE_DEFAULT = 'ROLE_USER';
const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';
/**
* @ORMId
* @ORMColumn(type="integer")
* @ORMgeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORMManyToMany(targetEntity="Group")
* @ORMJoinTable(name="avo_user_avo_group",
* joinColumns={@ORMJoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@ORMJoinColumn(name="group_id", referencedColumnName="id")}
* )
*/
protected $groups;
/**
* @ORMManyToMany(targetEntity="Role")
* @ORMJoinTable(name="avo_user_avo_role",
* joinColumns={@ORMJoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={@ORMJoinColumn(name="role_id", referencedColumnName="id")}
* )
*/
protected $avoRoles;
/**
* @ORMColumn(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
My Role class extends Symfony Security Component Core Role class.
namespace AvocodeUserBundleEntity;
use DoctrineORMMapping as ORM;
use AvocodeCommonBundleCollectionsArrayCollection;
use SymfonyComponentSecurityCoreRoleRole as BaseRole;
/**
* @ORMEntity(repositoryClass="AvocodeUserBundleRepositoryRoleRepository")
* @ORMTable(name="avo_role")
*/
class Role extends BaseRole
{
/**
* @ORMId
* @ORMColumn(type="integer")
* @ORMgeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORMColumn(type="string", unique="TRUE", length=255)
*/
protected $name;
/**
* @ORMColumn(type="string", length=255)
*/
protected $module;
/**
* @ORMColumn(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 Groups class
Since I've got the same problem with groups as with roles, I'm skipping them here. If I get roles working I know I can do the same with groups.
2.4 Controller
namespace AvocodeUserBundleController;
use SymfonyBundleFrameworkBundleControllerController;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationRedirectResponse;
use SymfonyComponentSecurityCoreSecurityContext;
use JMSSecurityExtraBundleAnnotationSecure;
use AvocodeUserBundleEntityUser;
use AvocodeUserBundleFormTypeUserType;
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 Custom repositories
It is not neccesary to post this since they work just fine - they return a subset of all Roles/Groups (those not assigned to user).
2.6 UserType
UserType:
namespace AvocodeUserBundleFormType;
use SymfonyComponentFormAbstractType;
use SymfonyComponentFormFormBuilder;
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' => tr