Symfony2实体集合 – 如何添加/删除与现有实体的关联?

1.快速浏览

1.1目标

我想要实现的是一个创build/编辑用户工具。 可编辑的字段是:

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

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

1.2用户界面

我的模板由两部分组成:

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

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

1.3所需的function

1.3.1添加angular色:


     用户单击angular色表中的“+”(添加)button时  
     然后 jquery从Roles表中删除该行  
      jQuery的增加新的列表项到用户窗体(avoRoles列表)

1.3.2删除angular色:


     用户单击“用户”窗体中的“x”(删除)button(avoRoles列表)  
     然后 jquery从用户窗体(avoRoles列表)中删除该列表项  
      jquery添加新行到angular色表

1.3.3保存更改:


     用户点击“Zapisz”(保存)button  
     THEN用户表单提交所有字段(用户名,密码,电子邮件,avoRoles,组)  
    并将avoRoles作为angular色实体的ArrayCollection保存(ManyToMany关系)  
    并将组保存为angular色实体的ArrayCollection(ManyToMany关系)  

注意:只能将现有的angular色和组分配给用户。 如果由于任何原因,他们没有find表格不应该validation。


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.2angular色类

我的angular色类扩展了Symfony安全组件核心angular色类。

 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组类

因为我和组里的angular色都有同样的问题,所以我在这里跳过了。 如果我得到angular色的工作,我知道我可以在团队中做同样的事情。

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自定义存储库

因为他们工作得很好,所以发布这个不是必须的 – 他们返回所有angular色/组的子集(那些没有分配给用户的)。

2.6用户types

用户types:

 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

这种forms应该是:

  • 隐藏的angular色ID
  • angular色名称(只读)
  • 隐藏模块(只读)
  • 隐藏描述(只读)
  • 删除(x)button

模块和描述呈现为隐藏字段,因为当pipe理员从用户中删除一个angular色时,该angular色应该由jQuery添加到angular色表中 – 并且此表具有“模块”和“描述”列。

 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情况1:上面引用的configuration

上面的configuration返回错误:

 Property "id" is not public in class "Avocode\UserBundle\Entity\Role". Maybe you should create the method "setId()"? 

但是ID的设置不应该被要求。

  1. 首先,我不想创build一个新的angular色。 我只想创build现有的angular色和用户实体之间的关系。
  2. 即使我想创build一个新的angular色,它的ID应该是自动生成的:

    / **

    • @ORM \标识
    • @ORM \柱(types= “整数”)
    • @ORM \ generatedValue(strategy =“AUTO”)* / protected $ id;

3.2案例2:添加angular色实体中ID属性的setter

我认为这是错误的,但我确实做了。 将此代码添加到angular色实体后:

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

如果我创build新用户并添加一个angular色,然后保存…会发生什么事情是:

  1. 新用户被创build
  2. 新用户具有指定所需ID的angular色(耶!)
  3. 但是这个angular色的名字被空string (无赖!) 覆盖

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

3.3案例3:由Jeppebuild议的解决方法

当我第一次遇到这个问题时,我结束了一个解决方法,就像Jeppe所build议的一样。 今天(由于其他原因)我不得不重做我的表单/视图和解决方法停止工作。

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:按用户友好build议

将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 中将参数更改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中提出的解决方法代码确实改变了一些事情:

  1. 用户和angular色之间的关联没有创build
  2. ..但angular色实体的名称被空string覆盖(如3.2)

所以..它确实改变了一些东西,但方向不对。

4.版本

4.1 Symfony2 v2.0.15

4.2 Doctrine2 v2.1.7

4.3 FOSUserBundle版本: 6fb81861d84d460f1d070ceb8ec180aac841f7fa

5.总结

我已经尝试了许多不同的方法(以上只是最近的),花了数小时学习代码,谷歌和寻找答案,我无法得到这个工作。

任何帮助将不胜感激。 如果你需要知道任何东西,我会发布你需要的代码的任何部分。

我已经得出了相同的结论,即表单组件出了问题,并且看不到一个简单的方法来修复它。 不过,我想出了一个稍微不太麻烦的解决方法,它是完全通用的。 它没有任何关于实体/属性的硬编码知识,因此将修复它遇到的任何集合:

更简单,通用的解决方法

这并不要求您对您的实体进行任何更改。

 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 // eg 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... } } 

所以一年过去了,这个问题变得很stream行。 Symfony已经改变了,我的技能和知识也有所改善,我目前对这个问题的解决方法也是如此。

我为symfony2创build了一套表单扩展(请参阅github上的FormExtensionsBundle项目),它们包含处理One / Many ToMany关系的表单types。

在编写这些代码时,向控制器添加自定义代码来处理集合是不可接受的 – 表单扩展本应该易于使用,即开即用,让开发人员的工作更轻松,而不是更难。 还记得..干!

所以我不得不在其他地方添加/删除关联代码 – 正确的地方做它自然是一个EventListener 🙂

看看EventListener / CollectionUploadListener.php文件,看看我们现在如何处理。

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

1.解决方法

Jeppe Marianger-Lambuild议的解决scheme目前是我所知道的唯一一个。

1.1为什么在我的情况下停止工作?

我改变了我的RoleNameType(由于其他原因)为:

  • ID(隐藏)
  • 名称(自定义types – 标签)
  • 模块和描述(隐藏,只读)

问题是我的自定义types标签呈现NAME属性为


     <span>angular色名称</ span>

而且由于它不是“只读”,所以FORM组件有望在POST中获得NAME。

相反,只有ID被张贴,因此FORM组件被假定为NAME。

这导致CASE 2(3.2) – >创build关联,但用空string覆盖ROLE NAME。

那么,这个解决方法有什么可行的呢?

2.1控制器

这个解决方法非常简单。

在你的控制器中,在你确认表单之前,你必须获取已经发布的实体标识符并获得匹配的实体,然后将它们设置为你的对象。

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

3.最后的想法

不过,我认为这个解决方法正在做这个工作

 $form->bindRequest($request); 

应该做! 这是要么我做错了,或者symfony的收集表单types不完整。

在symfony 2.1中有一些Form组件的主要变化 ,希望这个将会被修复。

PS。 如果是我做错了什么

…请张贴它应该做的方式! 我很高兴看到一个快速,简单和“干净”的解决scheme。

PS2。 特别感谢:

Jeppe Marianger-Lam和用户友好(来自IRC#symfony2)。 你一直很有帮助。 干杯!

这就是我以前所做的 – 我不知道这是否是“正确”的方式,但是它是有效的。

当你从提交的表单(即if($form->isValid()) )之前或之后得到结果时,只需询问angular色列表,然后将其全部从实体中删除(将列表保存为variables)。 有了这个列表,只需循环遍历它们,就可以向存储库询问与ID相匹配的angular色实体,并在persistflush之前将这些实体添加到您的用户实体中。

我只是search了Symfony2文档,因为我记得有关表单集合的prototype东西,并且出现了: http : //symfony.com/doc/current/cookbook/form/form_collections.html – 它有如何正确处理的例子用JavaScript添加和删除表单中的集合types。 也许首先尝试这种方法,然后尝试我之后提到的,如果你不能得到它的工作:)

你需要更多的实体:
用户
id_user(types:整型)
用户名(types:文本)
plainPassword(types:密码)
电子邮件(types:电邮)



id_group(types:整数)
descripcion(types:文本)


AVOROLES
id_avorole(types:整型)
descripcion(types:文本)


* USER_GROUP *
id_user_group(types:整数)
id_user(types:整型)(这是用户实体上的id)
id_group(types:整数)(这是组实体上的ID)


* USER_AVOROLES *
id_user_avorole(types:整数)
id_user(types:整型)(这是用户实体上的id)
id_avorole(type:integer)(这是avorole实体上的id)


你可以有这样的例子:
用户:
ID:3
用户名:john
plainPassword:johnpw
电子邮件:john@email.com

组:
id_group:5
descripcion:第5组

USER_GROUP:
id_user_group:1
id_user:3
id_group:5
*这个用户可以在另一行有很多组*