1. 快速概览
1.1 目标
我想要实现的是创建/编辑用户工具。可编辑的字段是:
- 用户名(类型:文本)
- 普通密码(类型:密码)
- 电子邮件(类型:电子邮件)
- 组(类型:集合)
- avoRoles(类型:集合)
注意:最后一个属性没有命名为$roles,因为我的 User 类扩展了 FOSUserBundle 的 User 类并且覆盖角色带来了更多问题。为了避免它们,我只是决定将我的角色集合存储在$avoRoles下。
1.2 用户界面
我的模板由两部分组成:
- 用户表单
- 显示 $userRepository->findAllRolesExceptOwnedByUser($user); 的表格
注意:findAllRolesExceptOwnedByUser() 是一个自定义存储库函数,返回所有角色的子集(那些尚未分配给 $user 的角色)。
1.3 所需功能
1.3.1 添加角色:
当用户单击角色表中的“+”(添加)按钮时, jquery 从角色表中删除该行 并且 jquery 将新列表项添加到用户表单(avoRoles 列表)
1.3.2 移除角色:
当用户单击用户表单(avoRoles 列表)中的“x”(删除)按钮时, jquery 从用户表单( avoRoles列表)中删除该列表项, 并且 jquery 将新行添加到角色表
1.3.3 保存更改:
当用户单击“Zapisz”(保存)按钮 时,用户表单提交所有字段(用户名、密码、电子邮件、avoRoles、组) 并将avoRoles 保存为角色实体的 ArrayCollection(ManyToMany 关系) 并将组保存为角色实体 的 ArrayCollection(多对多关系)
注意:只有现有的角色和组可以分配给用户。如果由于任何原因未找到该表格,则不应验证该表格。
2.代码
在本节中,我将介绍/或简要描述此操作背后的代码。如果描述不够,您需要查看代码,请告诉我,我会粘贴它。我并不是一开始就将其全部粘贴,以避免向您发送不必要的代码垃圾邮件。
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 角色名称类型
这种形式应该呈现:
- 隐藏角色 ID
- 角色名称(只读)
- 隐藏模块(只读)
- 隐藏的描述(只读)
- 删除 (x) 按钮
模块和描述被呈现为隐藏字段,因为当管理员从用户中删除一个角色时,该角色应该由 jQuery 添加到角色表 - 并且该表具有模块和描述列。
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. 当前/已知问题
3.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="整数")
- @ORM\generatedValue(strategy="AUTO") */ protected $id;
3.2 案例2:在Role实体中为ID属性添加setter
我认为这是错误的,但我这样做只是为了确定。将此代码添加到角色实体后:
public function setId($id)
{
$this->id = $id;
return $this;
}
如果我创建新用户并添加角色,然后保存...会发生什么:
- 新用户已创建
- 新用户具有分配了所需 ID 的角色(耶!)
- 但是该角色的名称被空字符串覆盖(真糟糕!)
显然,这不是我想要的。我不想编辑/覆盖角色。我只想在他们和用户之间添加一个关系。
3.3 案例 3:Jeppe 建议的解决方法
当我第一次遇到这个问题时,我最终找到了一个解决方法,这与 Jeppe 建议的相同。今天(由于其他原因)我不得不重新制作我的表单/视图并且解决方法停止工作。
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;
}
不幸的是,这并没有改变任何东西。结果是 CASE1(没有 ID 设置器)或 CASE2(有 ID 设置器)。
3.4 案例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 中建议的解决方法代码确实改变了一些东西:
- 未创建用户和角色之间的关联
- .. 但是角色实体的名称被空字符串覆盖(如 3.2 中)
所以..它确实改变了一些东西,但方向错误。
4. 版本
4.1 Symfony2 v2.0.15
4.2 教义2 v2.1.7
4.3 FOSUserBundle 版本:6fb81861d84d460f1d070ceb8ec180aac841f7fa
五、总结
我尝试了许多不同的方法(以上只是最新的方法),经过数小时研究代码、谷歌搜索和寻找答案后,我无法得到这个工作。
任何帮助将不胜感激。如果您需要了解任何内容,我会发布您需要的任何代码部分。