1.快速概述

1.1目标

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

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

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

    1.2用户界面

    My template包含2个部分:
  • 用户格式
  • 表显示$ userRepository-> findAllRolesExceptOwnedByUser($ user);

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

    1.3所需功能

    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 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 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 Controller

    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 RoleNameType

    这种形式应该呈现:
  • 隐藏的角色ID
  • 角色名称(只读)
  • 隐藏模块(只读)
  • 隐藏的描述(只读)
  • 删除(x)按钮

  • 模块和描述显示为隐藏字段,因为当Admin从用户删除角色时,该角色应由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情况1:如上所述的配置

    上面的配置返回错误:

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

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

    /**
  • @ORM\Id
  • @ORM\Column(type =“integer”)
  • @ORM\generationValue(strategy =“AUTO”)
    */
    protected $ id;

  • 3.2情况2:在角色实体中为ID属性添加了 setter

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

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

    如果我创建新用户并添加角色,则保存...发生的情况是:
  • 创建新用户
  • 新用户具有分配了所需ID的角色(是!)
  • ,但是该角色的名称被空字符串(糟糕!)覆盖。

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

    3.3情况3:Jeppe建议的解决方法

    当我第一次遇到此问题时,我得到了一种解决方法,与Jeppe建议的方法相同。今天(由于其他原因),我不得不重新制作表单/ View ,并且变通办法停止了工作。

    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 setter )或CASE2(带有ID setter )。

    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版本2.0.15

    4.2教义2 v2.1.7

    4.3 FOSUserBundle版本:6fb81861d84d460f1d070ceb8ec180aac841f7fa

    5.总结

    我尝试了许多不同的方法(以上只是最新的方法),并且花了几个小时研究代码,谷歌搜索并寻找答案后,我才无法正常工作。

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

    最佳答案

    我得出一个相同的结论,即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...
        }
    }
    

    关于php - Symfony2实体集合-如何添加/删除与现有实体的关联?,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/11089861/

    10-11 02:48
    查看更多