Symfony2实体集合 – 如何添加/删除与现有实体的关联?
1.快速浏览
1.1目标
我想要实现的是一个创build/编辑用户工具。 可编辑的字段是:
- 用户名(types:文本)
- plainPassword(types:密码)
- 电子邮件(types:电邮)
- 组(types:集合)
- avoRoles(types:集合)
注意:最后一个属性没有命名为$ roles,因为我的User类正在扩展FOSUserBundle的User类,覆盖angular色带来了更多的问题。 为了避免它们,我决定将我的angular色集合存储在$ avoRoles下 。
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的设置不应该被要求。
- 首先,我不想创build一个新的angular色。 我只想创build现有的angular色和用户实体之间的关系。
-
即使我想创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色,然后保存…会发生什么事情是:
- 新用户被创build
- 新用户具有指定所需ID的angular色(耶!)
- 但是这个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中提出的解决方法代码确实改变了一些事情:
- 用户和angular色之间的关联没有创build
- ..但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色实体,并在persist
和flush
之前将这些实体添加到您的用户实体中。
我只是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
*这个用户可以在另一行有很多组*