我想要实现的是创建/编辑用户工具。可编辑的字段是:
注意:最后一个属性未命名为 $ roles ,因为我的用户类正在扩展FOSUserBundle的用户类,并且覆盖角色带来了更多问题。为了避免它们,我只是决定将我的角色集合存储在 $ avoRoles 下。
My template由两部分组成:
注意:findAllRolesExceptOwnedByUser()是一个自定义存储库函数,返回所有角色的子集(尚未分配给$ user的角色)。
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 User form (avoRoles list)
1.3.2删除角色:
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保存更改:
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)
注意:只有现有的角色和组可以分配给用户。如果由于任何原因找不到表格,表格不应该验证。
在本节中,我将介绍/或简要介绍此操作背后的代码。如果描述不够,你需要看到代码告诉我,我会粘贴它。我不是首先将它们全部粘贴,以避免使用不必要的代码向您发送垃圾邮件。
我的User类扩展了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;
}
}
My Role类扩展了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;
}
}
由于我在群组和角色方面遇到同样的问题,我在这里跳过它们。如果我开始工作,我知道我可以对团队做同样的事情。
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,
));
}
}
发布此信息不是必要的,因为它们工作得很好 - 它们会返回所有角色/组的子集(未分配给用户的组)。
的用户类型:
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;
}
}
这个表格应该呈现:
模块和描述呈现为隐藏字段,因为当Admin从用户删除角色时,该角色应该通过jQuery添加到角色表 - 此表包含模块和描述列。 < / p>
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');
}
}
以上配置返回错误:
Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"?
但不需要用于ID的setter。
即使我确实想要创建一个新角色,也应该自动生成它的ID:
/ **
我认为这是错的,但我这样做是为了确定。将此代码添加到Role实体后:
public function setId($id)
{
$this->id = $id;
return $this;
}
如果我创建新用户并添加角色,那么SAVE ......会发生什么:
显然,那不是我想要的。我不想编辑/覆盖角色。我只想在它们与用户之间添加关系。
当我第一次遇到这个问题时,我最终得到了一个解决方法,就像Jeppe建议的那样。今天(出于其他原因)我不得不重新制作表单/视图,并且解决方法已停止工作。
Case3 UserManagementController中的哪些更改 - &gt; 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设置器)。
添加cascade = {&#34; persist&#34;,&#34;删除&#34;}到映射。
/**
* @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中建议的解决方法代码确实改变了一些东西:
所以..它确实改变了一些东西但方向错误。
我尝试了很多不同的方法(上面只是最近的方法),花了几个小时研究代码,google和寻找答案,我无法让这个工作。
任何帮助将不胜感激。如果您需要了解任何内容,我会发布您需要的任何代码部分。
答案 0 :(得分:13)
我得出的结论是,Form组件有问题,无法找到一种简单的方法来解决它。但是,我提出了一个完全通用的稍微麻烦的解决方案;它没有任何关于实体/属性的硬编码知识,因此将修复它遇到的任何集合:
这不要求您对实体进行任何更改。
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's 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
// e.g. doctrine:
// $em = $this->getDoctrine()->getEntityManager();
// $em->remove($entity);
}
}
}
}
}
cleanupCollections()
方法
# 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...
}
}
答案 1 :(得分:10)
所以一年过去了,这个问题变得非常流行。 Symfony已经发生了变化,我的技能和知识也得到了改善,我目前解决这个问题的方法也是如此。
我为symfony2创建了一组表单扩展(请参阅github上的FormExtensionsBundle项目),它们包含用于处理One / Many ToMany 关系的表单类型。
在编写这些内容时,向控制器添加自定义代码以处理集合是不可接受的 - 表单扩展应该易于使用,开箱即用,让我们的开发人员更轻松,而不是更难。还..记得..干!
所以我不得不在其他地方移动添加/删除关联代码 - 正确的地方自然是一个EventListener:)
查看EventListener/CollectionUploadListener.php文件,看看我们现在如何处理这个问题。
PS。在这里复制代码是不必要的,最重要的是像这样的东西应该在EventListener中实际处理。
答案 2 :(得分:8)
Jeppe Marianger-Lam建议的解决方案解决方案目前是我所知道的唯一一个解决方案。
我将RoleNameType(出于其他原因)更改为:
问题是我的自定义类型标签将NAME属性呈现为
<span> role name </span>
由于它不是“只读”,因此FORM组件可能会在POST中获得NAME。
相反,只有ID被POST,因此FORM组件假定NAME为NULL。
这导致案例2(3.2) - &gt;创建关联,但用空字符串覆盖ROLE NAME。
这种解决方法非常简单。
在您的控制器中,在您验证表单之前,您必须获取已发布的实体identyficators并获取匹配的实体,然后将它们设置为您的对象。
// 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,
));
}
在updateUser函数中的变通方法代码下面:
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's 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;
}
为此,您的SETTER(在本例中为User.php实体)必须为:
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;
}
尽管如此,我认为这种解决方法正在完成
的工作$form->bindRequest($request);
应该做!这是我做错了,或者symfony的Collection表单类型不完整。
There are some major changes in Form component来自symfony 2.1,希望这将得到解决。
...请发布应该做的方式!我很高兴看到一个快速,简单和“干净”的解决方案。
Jeppe Marianger-Lam和用户友好(来自IRC上的#symfony2)。你一直非常乐于助人。干杯!
答案 3 :(得分:6)
这就是我之前所做的 - 我不知道这是否是'正确'的方式,但它确实有效。
当您从提交的表单中获得结果时(即,在if($form->isValid())
之前或之后),只需询问角色列表,然后将其全部从实体中删除(将列表另存为变量) 。使用此列表,只需循环遍历所有列表,向存储库询问与ID匹配的角色实体,并在persist
和flush
之前将这些实体添加到您的用户实体。
我刚刚浏览了Symfony2文档,因为我记得有关prototype
表单集合的内容,结果出现了:http://symfony.com/doc/current/cookbook/form/form_collections.html - 它提供了如何正确处理javascript添加和删除集合的示例表格中的类型。也许首先尝试这种方法,然后尝试我之后提到的,如果你不能让它工作:)
答案 4 :(得分:0)
您需要更多实体:
的 USER 强>
id_user(类型:整数)
用户名(类型:文字)
plainPassword(类型:密码)
电子邮件(类型:电子邮件)
的组强>
id_group(类型:整数)
descripcion(type:text)
的 AVOROLES 强>
id_avorole(类型:整数)
descripcion(type:text)
*的 USER_GROUP * 强>
id_user_group(类型:整数)
id_user(类型:整数)(这是用户实体上的id)
id_group(类型:整数)(这是组实体上的id)
*的 USER_AVOROLES * 强>
id_user_avorole(类型:整数)
id_user(类型:整数)(这是用户实体上的id)
id_avorole(类型:整数)(这是avorole实体上的id)
你可以举例如下:
用户:
id:3
用户名:john
plainPassword:johnpw
电子邮件:john@email.com
组:
id_group:5
描述:第5组
USER_GROUP:
id_user_group:1
id_user:3
id_group:5
*此用户可以在另一行中拥有多个组*