Symfony2 实体集合 - 如何添加/删除与现有实体的关联? [英] Symfony2 collection of Entities - how to add/remove association with existing entities?

查看:32
本文介绍了Symfony2 实体集合 - 如何添加/删除与现有实体的关联?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

1.快速概览

1.1 目标

我想要实现的是创建/编辑用户工具.可编辑的字段是:

  • 用户名(类型:文本)
  • plainPassword(类型:密码)
  • 电子邮件(类型:电子邮件)
  • 组(类型:集合)
  • avoRoles(类型:集合)

注意:最后一个属性没有命名为 $roles 因为我的 User 类是扩展 FOSUserBundle 的 User 类,覆盖角色带来了更多问题.为了避免它们,我只是决定将我的角色集合存储在 $avoRoles 下.

1.2 用户界面

我的模板由两部分组成:

  1. 用户表单
  2. 显示 $userRepository->findAllRolesExceptOwnedByUser($user); 的表格;

注意:findAllRolesExceptOwnedByUser() 是一个自定义存储库函数,返回所有角色(尚未分配给 $user 的角色)的子集.

1.3 期望的功能

1.3.1 添加角色:

<前>WHEN 用户点击角色表中的+"(添加)按钮THEN jquery 从 Roles 表中删除该行AND jquery 将新列表项添加到用户表单(avoRoles 列表)

1.3.2 删除角色:

<前>WHEN 用户单击用户表单(avoRoles 列表)中的x"(删除)按钮THEN jquery 从用户表单(avoRoles 列表)中删除该列表项AND jquery 向 Roles 表添加新行

1.3.3 保存更改:

<前>WHEN 用户点击Zapisz"(保存)按钮THEN 用户表单提交所有字段(用户名、密码、电子邮件、avoRoles、组)AND 将 avoRoles 保存为 Role 实体的 ArrayCollection(多对多关系)AND 将组保存为角色实体的 ArrayCollection(多对多关系)

注意:只能将现有角色和组分配给用户.如果出于任何原因未找到它们,则不应验证表单.

<小时>

2.代码

在本节中,我将介绍/或简要描述此操作背后的代码.如果描述不够,您需要查看代码,请告诉我,我将粘贴它.我不是一开始就粘贴所有内容,以免向您发送不必要的代码.

2.1 用户类

我的用户类扩展了 FOSUserBundle 用户类.

命名空间 AvocodeUserBundleEntity;使用 FOSUserBundleEntityUser 作为 BaseUser;使用 DoctrineORMMapping 作为 ORM;使用 AvocodeCommonBundleCollectionsArrayCollection;使用 SymfonyComponentValidatorExecutionContext;/*** @ORMEntity(repositoryClass="AvocodeUserBundleRepositoryUserRepository")* @ORMTable(name="avo_user")*/类 User 扩展了 BaseUser{const ROLE_DEFAULT = 'ROLE_USER';const ROLE_SUPER_ADMIN = 'ROLE_SUPER_ADMIN';/*** @ORMId* @ORMColumn(type="整数")* @ORMgeneratedValue(strategy="AUTO")*/受保护的 $id;/*** @ORMManyToMany(targetEntity="Group")* @ORMJoinTable(name="avo_user_avo_group",* joinColumns={@ORMJoinColumn(name="user_id", referencedColumnName="id")},* inverseJoinColumns={@ORMJoinColumn(name="group_id",referencedColumnName="id")}* )*/受保护的 $groups;/*** @ORMManyToMany(targetEntity="Role")* @ORMJoinTable(name="avo_user_avo_role",* joinColumns={@ORMJoinColumn(name="user_id", referencedColumnName="id")},* inverseJoinColumns={@ORMJoinColumn(name="role_id", referencedColumnName="id")}* )*/受保护的 $avoRoles;/*** @ORMColumn(type="datetime", name="created_at")*/受保护的 $createdAt;/*** 用户类构造函数*/公共函数 __construct(){parent::__construct();$this->groups = new ArrayCollection();$this->avoRoles = new ArrayCollection();$this->createdAt = new DateTime();}/*** 获取身份证** @return 整数*/公共函数 getId(){返回 $this->id;}/*** 设置用户角色** @return 用户*/公共函数 setAvoRoles($avoRoles){$this->getAvoRoles()->clear();foreach($avoRoles 作为 $role) {$this->addAvoRole($role);}返回 $this;}/*** 添加 avoRole** @param 角色 $avoRole* @return 用户*/公共函数 addAvoRole(Role $avoRole){if(!$this->getAvoRoles()->contains($avoRole)) {$this->getAvoRoles()->add($avoRole);}返回 $this;}/*** 获取 avoRoles** @return 数组集合*/公共函数 getAvoRoles(){返回 $this->avoRoles;}/*** 设置用户组** @return 用户*/公共函数 setGroups($groups){$this->getGroups()->clear();foreach($groups 作为 $group) {$this->addGroup($group);}返回 $this;}/*** 获取授予用户的组.** @return 集合*/公共函数 getGroups(){返回 $this->groups ?: $this->groups = new ArrayCollection();}/*** 获取用户创建日期** @return 日期时间*/公共函数 getCreatedAt(){返回 $this->createdAt;}}

2.2 角色类

My Role 类扩展了 Symfony 安全组件核心角色类.

命名空间 AvocodeUserBundleEntity;使用 DoctrineORMMapping 作为 ORM;使用 AvocodeCommonBundleCollectionsArrayCollection;使用 SymfonyComponentSecurityCoreRoleRole 作为 BaseRole;/*** @ORMEntity(repositoryClass="AvocodeUserBundleRepositoryRoleRepository")* @ORMTable(name="avo_role")*/类 Role 扩展 BaseRole{/*** @ORMId* @ORMColumn(type="整数")* @ORMgeneratedValue(strategy="AUTO")*/受保护的 $id;/*** @ORMColumn(type="string", unique="TRUE", length=255)*/受保护的 $name;/*** @ORMColumn(type="string", length=255)*/受保护的 $module;/*** @ORMColumn(type="text")*/受保护的 $ 说明;/*** 角色类构造函数*/公共函数 __construct(){}/*** 返回角色名称.** @return 字符串*/公共函数 __toString(){返回(字符串)$this->getName();}/*** 获取身份证** @return 整数*/公共函数 getId(){返回 $this->id;}/*** 设置名称** @param 字符串 $name* @return 角色*/公共函数 setName($name){$name = strtoupper($name);$this->name = $name;返回 $this;}/*** 获取名称** @return 字符串*/公共函数 getName(){返回 $this->name;}/*** 设置模块** @param 字符串 $module* @return 角色*/公共函数 setModule($module){$this->module = $module;返回 $this;}/*** 获取模块** @return 字符串*/公共函数 getModule(){返回 $this->module;}/*** 设置说明** @param text $description* @return 角色*/公共函数 setDescription($description){$this->description = $description;返回 $this;}/*** 获取描述** @return 文本*/公共函数 getDescription(){返回 $this->description;}}

2.3 分组类

因为我在处理组和处理角色时遇到了同样的问题,所以我在这里跳过它们.如果我让角色发挥作用,我知道我可以对小组做同样的事情.

2.4 控制器

命名空间 AvocodeUserBundleController;使用 SymfonyBundleFrameworkBundleControllerController;使用 SymfonyComponentHttpFoundationRequest;使用 SymfonyComponentHttpFoundationRedirectResponse;使用 SymfonyComponentSecurityCoreSecurityContext;使用 JMSSecurityExtraBundleAnnotationSecure;使用 AvocodeUserBundleEntityUser;使用 AvocodeUserBundleFormTypeUserType;类 UserManagementController 扩展控制器{/*** 用户创建* @Secure(roles="ROLE_USER_ADMIN")*/公共函数 createAction(Request $request){$em = $this->getDoctrine()->getEntityManager();$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()));返回新的重定向响应($url);}}return $this->render('AvocodeUserBundle:UserManagement:create.html.twig', array('形式' =>$form->createView(),'用户' =>$用户,'角色' =>$角色,'组' =>$组,));}}

2.5 自定义存储库

没有必要发布此内容,因为它们工作正常 - 它们返回所有角色/组(未分配给用户的角色/组)的子集.

2.6 用户类型

用户类型:

命名空间 AvocodeUserBundleFormType;使用 SymfonyComponentFormAbstractType;使用 SymfonyComponentFormFormBuilder;类 UserType 扩展了 AbstractType{私人 $options;公共函数 __construct(array $options = null){$this->options = $options;}公共函数 buildForm(FormBuilder $builder, array $options){$builder->add('username', 'text');//密码字段应该只为 CREATE 操作呈现//相同的表单类型将用于 EDIT 操作//这就是为什么它是可选的if($this->options['password']){$builder->add('plainpassword', 'repeated', array('类型' =>'文本','选项' =>大批('属性' =>大批('自动完成' =>'离开'),),'first_name' =>'输入','second_name' =>'确认','invalid_message' =>'repeated.invalid.password',));}$builder->add('email', 'email', array('修剪' =>真的,))//collection_list 是自定义字段类型//扩展集合字段类型////唯一的变化是不同的表单名称//(和自定义 collection_list_widget)////简而言之:它是一个带有自定义 form_theme 的集合字段//->add('groups', 'collection_list', array('类型' =>新的 GroupNameType(),'allow_add' =>真的,'allow_delete' =>真的,'by_reference' =>真的,'error_bubbling' =>错误的,'原型' =>真的,))->add('avoRoles', 'collection_list', array('类型' =>新角色名称类型(),'allow_add' =>真的,'allow_delete' =>真的,'by_reference' =>真的,'error_bubbling' =>错误的,'原型' =>真的,));}公共函数 getName(){返回avo_user";}公共函数 getDefaultOptions(array $options){$options = 数组('data_class' =>'AvocodeUserBundleEntityUser',);//如果密码字段已呈现,则添加密码验证if($this->options['password'])$options['validation_groups'][] = '密码';返回 $options;}}

2.7 角色名称类型

这个表单应该呈现:

  • 隐藏的角色 ID
  • 角色名称(只读)
  • 隐藏模块(只读)
  • 隐藏描述(只读)
  • 删除 (x) 按钮

模块和描述被呈现为隐藏字段,因为当管理员从用户中删除角色时,该角色应该通过 jQuery 添加到角色表 - 该表具有模块和描述列.

命名空间 AvocodeUserBundleFormType;使用 SymfonyComponentFormAbstractType;使用 SymfonyComponentFormFormBuilder;类 RoleNameType 扩展 AbstractType{公共函数 buildForm(FormBuilder $builder, array $options){$builder->add('', '按钮', 数组('必需' =>错误的,))//自定义字段类型呈现x"按钮->add('id', '隐藏')->add('name', 'label', array('必需' =>错误的,))//自定义字段类型渲染&lt;span>item 而不是 &lt;input>物品->add('module', 'hidden', array('read_only' => true))->add('description', 'hidden', array('read_only' => true));}公共函数 getName(){//no_label 是一个自定义小部件,它在没有标签的情况下呈现 field_row返回 '​​no_label';}公共函数 getDefaultOptions(array $options){return array('data_class' => 'AvocodeUserBundleEntityRole');}}

<小时>

3.当前/已知问题

3.1 案例 1:如上引用的配置

以上配置返回错误:

属性id"在类AvocodeUserBundleEntityRole"中不是公开的.也许您应该创建方法setId()"?

但是不应该需要 ID 的 setter.

  1. 首先是因为我不想创建新角色.我只想在现有角色和用户实体之间创建关系.
  2. 即使我确实想创建一个新角色,它的 ID 也应该是自动生成的:

    /**

    • @ORMId
    • @ORMColumn(type="integer")
    • @ORMgeneratedValue(strategy="AUTO")*/受保护的 $id;

3.2 Case 2:为 Role 实体中的 ID 属性添加 setter

我认为这是错误的,但我这样做只是为了确定.将此代码添加到角色实体后:

公共函数 setId($id){$this->id = $id;返回 $this;}

如果我创建新用户并添加角色,然后保存...会发生什么:

  1. 新用户已创建
  2. 新用户拥有分配了所需 ID 的角色(是的!)
  3. 但是那个角色的名字被空字符串覆盖了(糟糕!)

显然,那不是我想要的.我不想编辑/覆盖角色.我只想在他们和用户之间添加一个关系.

3.3 案例 3:Jeppe 建议的解决方法

当我第一次遇到这个问题时,我最终找到了一个解决方法,与 Jeppe 建议的相同.今天(出于其他原因)我不得不重新制作我的表单/视图并且解决方法停止工作.

Case3 UserManagementController -> createAction 有什么变化:

//在 createAction 中//而不是 $user = new User$user = $this->updateUser($request, new User());//和下面的updateUser函数/*** 创建 mew iser 并设置其属性* 根据要求** @return User 返回配置的用户*/受保护的函数 updateUser($request, $user){if($request->getMethod() == 'POST'){$avo_user = $request->request->get('avo_user');/*** 为用户设置和添加/删除组*/$owned_groups = (array_key_exists('groups', $avo_user)) ?$avo_user['groups'] : array();foreach($owned_groups as $key => $group) {$owned_groups[$key] = $group['id'];}如果(计数($owned_groups)> 0){$em = $this->getDoctrine()->getEntityManager();$groups = $em->getRepository('AvocodeUserBundle:Group')->findById($owned_groups);$user->setGroups($groups);}/*** 为用户设置和添加/删除角色*/$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);}/*** 设置其他属性*/$user->setUsername($avo_user['username']);$user->setEmail($avo_user['email']);if($request->request->has('generate_password'))$user->setPlainPassword($user->generateRandomPassword());}返回 $user;}

不幸的是,这并没有改变任何东西.结果是 CASE1(没有 ID 设置器)或 CASE2(有 ID 设置器).

3.4 案例 4:按照用户友好的建议

将cascade={"persist", "remove"} 添加到映射中.

/*** @ORMManyToMany(targetEntity="Group", cascade={"persist", "remove"})* @ORMJoinTable(name="avo_user_avo_group",* joinColumns={@ORMJoinColumn(name="user_id", referencedColumnName="id")},* inverseJoinColumns={@ORMJoinColumn(name="group_id",referencedColumnName="id")}* )*/受保护的 $groups;/*** @ORMManyToMany(targetEntity="Role", cascade={"persist", "remove"})* @ORMJoinTable(name="avo_user_avo_role",* joinColumns={@ORMJoinColumn(name="user_id", referencedColumnName="id")},* inverseJoinColumns={@ORMJoinColumn(name="role_id", referencedColumnName="id")}* )*/受保护的 $avoRoles;

并在 FormType 中将 by_reference 更改为 false:

//...->add('avoRoles', 'collection_list', array('类型' =>新角色名称类型(),'allow_add' =>真的,'allow_delete' =>真的,'by_reference' =>错误的,'error_bubbling' =>错误的,'原型' =>真的,));//...

保留 3.3 中建议的解决方法代码确实改变了一些东西:

  1. 用户和角色之间的关联未创建
  2. ...但角色实体的名称被空字符串覆盖(如在 3.2 中)

所以..它确实改变了一些东西,但方向错误.

4.版本

4.1 Symfony2 v2.0.15

4.2 Doctrine2 v2.1.7

4.3 FOSUserBundle 版本:6fb81861d84d4780cf1d84d47807f1d

5.总结

我尝试了许多不同的方法(以上只是最新的方法),在花了几个小时研究代码、谷歌搜索并寻找答案后,我就是无法得到这个结果.

任何帮助将不胜感激.如果您需要了解任何信息,我会发布您需要的任何代码部分.

解决方案

一年过去了,这个问题已经很火了.自那以后 Symfony 发生了变化,我的技能和知识也有所提高,我目前解决这个问题的方法也是如此.

我为 symfony2 创建了一组表单扩展(参见 FormExtensionsBundle 在 github 上的项目)并且它们包括用于处理单/多 ToMany 关系的表单类型.

在编写这些时,将自定义代码添加到您的控制器以处理集合是不可接受的 - 表单扩展应该易于使用、开箱即用并使我们的开发人员的生活更轻松,而不是更难.还有..记住..干!

所以我不得不将添加/删除关联代码移到其他地方 - 而正确的地方自然是一个 EventListener :)

查看 EventListener/CollectionUploadListener.php文件以了解我们现在如何处理.

附注.复制这里的代码是不必要的,最重要的是这样的东西应该在EventListener中实际处理.

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:

  1. User form
  2. 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' => 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' => 'AvocodeUserBundleEntityUser',
        );

        // adding password validation if password field was rendered

        if($this->options['password'])
          $options['validation_groups'][] = 'password';

        return $options;
    }
}

2.7 RoleNameType

This form is supposed to render:

  • hidden Role ID
  • Role name (READ ONLY)
  • hidden module (READ ONLY)
  • hidden description (READ ONLY)
  • remove (x) button

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 AvocodeUserBundleFormType;

use SymfonyComponentFormAbstractType;
use SymfonyComponentFormFormBuilder;

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 &lt;span&gt; item instead of &lt;input&gt; 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' => 'AvocodeUserBundleEntityRole');
    }
}


3. Current/known Problems

3.1 Case 1: configuration as quoted above

The above configuration returns error:

Property "id" is not public in class "AvocodeUserBundleEntityRole". Maybe you should create the method "setId()"?

But setter for ID should not be required.

  1. First becouse I don't want to create a NEW role. I want just to create a relation between existing Role and User entities.
  2. Even if I did want to create a new Role, it's ID should be auto-generated:

    /**

    • @ORMId
    • @ORMColumn(type="integer")
    • @ORMgeneratedValue(strategy="AUTO") */ protected $id;

3.2 Case 2: added setter for ID property in Role entity

I think it's wrong, but I did it just to be sure. After adding this code to Role entity:

public function setId($id)
{
    $this->id = $id;
    return $this;
}

If I create new user and add a role, then SAVE... What happens is:

  1. New user is created
  2. New user has role with the desired ID assigned (yay!)
  3. but that role's name is overwritten with empty string (bummer!)

Obviously, thats not what I want. I don't want to edit/overwrite roles. I just want to add a relation between them and the User.

3.3 Case 3: Workaround suggested by Jeppe

When I first encountered this problem I ended up with a workaround, the same that Jeppe suggested. Today (for other reasons) I had to remake my form/view and the workaround stopped working.

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;
    }

Unfortunately this does not change anything.. the results are either CASE1 (with no ID setter) or CASE2 (with ID setter).

3.4 Case 4: as suggested by userfriendly

Adding cascade={"persist", "remove"} to mapping.

/**
 * @ORMManyToMany(targetEntity="Group", cascade={"persist", "remove"})
 * @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", cascade={"persist", "remove"})
 * @ORMJoinTable(name="avo_user_avo_role",
 *      joinColumns={@ORMJoinColumn(name="user_id", referencedColumnName="id")},
 *      inverseJoinColumns={@ORMJoinColumn(name="role_id", referencedColumnName="id")}
 * )
 */
protected $avoRoles;

And changeing by_reference to false in FormType:

// ...

                ->add('avoRoles', 'collection_list', array(
                        'type' => new RoleNameType(),
                        'allow_add' => true,
                        'allow_delete' => true,
                        'by_reference' => false,
                        'error_bubbling' => false,
                        'prototype' => true,
                     ));

// ...

And keeping workaround code suggested in 3.3 did change something:

  1. Association between user and role was not created
  2. .. but Role entity's name was overwritten by empty string (like in 3.2)

So.. it did change something but in the wrong direction.

4. Versions

4.1 Symfony2 v2.0.15

4.2 Doctrine2 v2.1.7

4.3 FOSUserBundle version: 6fb81861d84d460f1d070ceb8ec180aac841f7fa

5. Summary

I've tried many diffrent approaches (above are only the most recent ones) and after hours spent on studying code, google'ing and looking for the answer I just couldn't get this working.

Any help will be greatly appreciated. If you need to know anything I'll post whatever part of code you need.

解决方案

So a year has passed, and this question has become quite popular. Symfony has changed since, my skills and knowledge have also improved, and so has my current approach to this problem.

I've created a set of form extensions for symfony2 (see FormExtensionsBundle project on github) and they include a form type for handleing One/Many ToMany relationships.

While writing these, adding custom code to your controller to handle collections was unacceptable - the form extensions were supposed to be easy to use, work out-of-the-box and make life easier on us developers, not harder. Also.. remember.. DRY!

So I had to move the add/remove associations code somewhere else - and the right place to do it was naturally an EventListener :)

Have a look at the EventListener/CollectionUploadListener.php file to see how we handle this now.

PS. Copying the code here is unnecessary, the most important thing is that stuff like that should actually be handled in the EventListener.

这篇关于Symfony2 实体集合 - 如何添加/删除与现有实体的关联?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆