1. Quick overview
1.1目标
我试图实现的是一个创建/编辑用户工具.可编辑字段包括:
- 用户名(类型:文本)
- 明文密码(类型:密码)
- 邮箱(类型:邮箱)
- 组(类型:集合)
- 角色(类型:集合)
Note: the last property is not named 100 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 101.
1.2用户界面
My template由两部分组成:
- 用户表单
- 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所需功能
1.3.1添加角色:
WHEN user clicks "+" (add) button in Roles table THEN jquery removes that row from Roles table AND jquery adds new list item to 用户表单 (avoRoles list)
1.3.2删除角色:
WHEN user clicks "x" (remove) button in 用户表单 (avoRoles list) THEN jquery removes that list item from 用户表单 (avoRoles list) AND jquery adds new row to Roles table
1.3.3保存更改:
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
在本节中,我将介绍/或简要描述此操作背后的代码.如果描述不够,你需要看到代码,告诉我,我会粘贴它.我并不是为了避免用不必要的代码向您发送垃圾邮件而将其全部粘贴.
2.1用户类
我的用户类扩展了FOSUserBundle用户类.
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角色类别
我的角色类扩展了symfony安全组件核心角色类.
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小组课
因为我对团队和角色都有同样的问题,所以我在这里跳过它们.如果我能让角色发挥作用,我知道我也能在团队中发挥同样的作用.
2.4控制器
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自定义存储库
无需发布此消息,因为它们工作正常——它们返回所有角色/组(未分配给用户的角色/组)的子集.
2.6用户类型
用户类型:
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's 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 RoleNameType
此表单应呈现:
- 隐藏角色ID
- 角色名称(只读)
- 隐藏模块(只读)
- 隐藏描述(只读)
- 移除(x)按钮
Module and description are rendered as hidden fields, becouse when Admin removes a role from a User, that role should be added by jQuery to Roles Table - and this table has 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 Problems
3.1情况1:如上所述的配置
上述配置返回错误:
Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"?
但是ID的设置器不应该是必需的.
- 首先,因为我不想创建一个新角色.我只想在现有角色和用户实体之间创建一种关系.
-
即使我确实想创建一个新角色,它的ID也应该是自动生成的:
/**
- @ORM\ID
- @ORM\Column(type="INTEGER")
- @ORM\generatedValue(strategy="AUTO")
3.2 case 2:为角色实体中的ID属性添加了setter
我认为这是错误的,但我这么做只是为了确认.将此代码添加到角色实体后:
public function setId($id)
{
$this->id = $id;
return $this;
}
如果我创建了新用户并添加了一个角色,然后保存...结果是:
- 新用户已创建
- 新用户具有分配了所需ID的角色(耶!)
- but that role's name is overwritten with empty string(糟糕!)
很明显,那不是我想要的.我不想编辑/覆盖角色.我只想在他们和用户之间添加一种关系.
3.3 case 3:Jeppe建议的解决方案
当我第一次遇到这个问题时,我最终找到了一个解决方法,这和杰普建议的一样.今天(由于其他原因),我不得不重新制作我的表单/视图,变通方法停止了工作.
What 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;
}
不幸的是,这并没有改变任何事情..结果是 case 1(没有ID设置器)或 case 2(有ID设置器).
3.4 case 4:按照用户友好型的建议
将cascade={"persist","remove"}添加到映射中.
/**
* @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;
并将FormType中的by_reference更改为false:
// ...
->add('avoRoles', 'collection_list', array(
'type' => new RoleNameType(),
'allow_add' => true,
'allow_delete' => true,
'by_reference' => false,
'error_bubbling' => false,
'prototype' => true,
));
// ...
保留3.3中建议的变通代码确实改变了一些事情:
- 用户和角色之间的关联度为not created
- ..但是角色实体的名称被空字符串覆盖(如3.2中所示)
所以.它确实改变了一些东西,但方向错了.
4. Versions
4.1 Symfony2 v2.0.15
4.2 Doctrine2 v2.1.7
4.3 FOSUserBundle version: 6fb81861d84d460f1d070ceb8ec180aac841f7fa
5. Summary
我try 过许多不同的方法(以上只是最新的方法),在花了数小时研究代码、搜索和寻找答案之后,我就是无法让它工作.
任何帮助都将不胜感激.如果您需要了解任何信息,我可以发布您需要的任何代码部分.