1. Quick overview

1.1目标

我试图实现的是一个创建/编辑用户工具.可编辑字段包括:

  • 用户名(类型:文本)
  • 明文密码(类型:密码)
  • 邮箱(类型:邮箱)
  • 组(类型:集合)
  • 角色(类型:集合)

Note: the last property is not named 100 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 101.

1.2用户界面

My template由两部分组成:

  1. 用户表单
  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所需功能

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 用户表单 (avoRoles list)

1.3.2删除角色:


    WHEN user clicks "x" (remove) button in  用户表单 (avoRoles list)  
    THEN jquery removes that list item from 用户表单 (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)  

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

在本节中,我将介绍/或简要描述此操作背后的代码.如果描述不够,你需要看到代码,告诉我,我会粘贴它.我并不是为了避免用不必要的代码向您发送垃圾邮件而将其全部粘贴.

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.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控制器

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)按钮

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 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. Current/known Problems

3.1情况1:如上所述的配置

上述配置返回错误:

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

但是ID的设置器不应该是必需的.

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

    /**

    • @ORM\ID
    • @ORM\Column(type="INTEGER")
    • @ORM\generatedValue(strategy="AUTO")

3.2 case 2:为角色实体中的ID属性添加了setter

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

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

如果我创建了新用户并添加了一个角色,然后保存...结果是:

  1. 新用户已创建
  2. 新用户具有分配了所需ID的角色(耶!)
  3. but that role's name is overwritten with empty string(糟糕!)

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

3.3 case 3:Jeppe建议的解决方案

当我第一次遇到这个问题时,我最终找到了一个解决方法,这和杰普建议的一样.今天(由于其他原因),我不得不重新制作我的表单/视图,变通方法停止了工作.

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

不幸的是,这并没有改变任何事情..结果是 case 1(没有ID设置器)或 case 2(有ID设置器).

3.4 case 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中建议的变通代码确实改变了一些事情:

  1. 用户和角色之间的关联度为not created
  2. ..但是角色实体的名称被空字符串覆盖(如3.2中所示)

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

4. Versions

4.1 Symfony2 v2.0.15

4.2 Doctrine2 v2.1.7

4.3 FOSUserBundle version: 6fb81861d84d460f1d070ceb8ec180aac841f7fa

5. Summary

我try 过许多不同的方法(以上只是最新的方法),在花了数小时研究代码、搜索和寻找答案之后,我就是无法让它工作.

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

推荐答案

所以一年过go 了,这个问题变得相当流行.从那以后,Symfony发生了变化,我的技能和知识也有所提高,我现在解决这个问题的方法也是如此.

我已经为symfony2创建了一组表单扩展(请参见github上的FormExtensionsBundle project),其中包括一个用于处理一个/多个ToMany关系的表单类型.

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

因此,我不得不将添加/删除关联代码移到其他地方-而正确的位置自然是EventListener:)

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

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

Php相关问答推荐

在PHP中结合每周日程安排和特殊日子查找下一个可用日期

PHP日期操作,将多个日期输出为格式化字符串

致命错误:未捕获错误:Google\Auth\HttpHandler\HttpHandlerFactory::build()

ForHumans函数中的月当量与单位限制

Laveel Livewire Get Groupby姓名列表

WooCommerce HPOS管理订单列表中带有订单元数据的定制列

在Symfony 6.4中将工作流动态添加到服务

限制某些产品只能在WooCommerce的特定邮政编码/邮政编码范围内发货

哪里的表现能更好?

向WooCommerce管理订单总额添加自定义总额行

调用模型[App\Models\Employee]上未定义的关系[title]

有没有办法像引用枚举一样引用注册表中的对象?

Font Awesome 加载图标未显示在 WooCommerce 管理员列表中

laravel Eloquent 模型的问题

当所有对象都属于同一类型时,我可以省略 PHP in_array() 中的 strict 参数吗?

Laravel auth()->id() 在生产服务器中不工作

同时执行odbc_execute、odbc_fetch_row和odbc_result

为什么在 phpunit 的开头测试 PHP_VERSION?

如何在供应商名称后将自定义徽章添加到商品详情

来自命令行的 PHP 没有给出正确的结果