Symfony 4 - 加载动态用户角色的固定装置

Posted

技术标签:

【中文标题】Symfony 4 - 加载动态用户角色的固定装置【英文标题】:Symfony 4 - load fixtures for dynamic user roles 【发布时间】:2018-08-11 09:37:31 【问题描述】:

目前我正在尝试修改我的类并寻找保存用户和角色之间动态关系的想法。

我想在加载固定装置时创建关联,并在需要创建具有关系的用户时在控制器中具有这样的功能,例如:

...
$user = new User();
$user->setName($_POST['name']);
$user->setPassword($_POST['password']);
...
$user->setRole('ROLE_USER');//Role for everyone
...
$role = new Role();
$role->setName('ROLE_' . strtoupper($_POST['name']) );//Role for personal use
...
//Here need to implement user+role association (I'm looking for recommendations)
...
$entityManager->persist($user);
$entityManager->persist($role);
//Persist role+user assosiacion
$entityManager->flush();
$entityManager->clear();

我的用户.php

    <?php

    namespace App\Entity;

    use DateTime;
    use Doctrine\Common\Collections\ArrayCollection;
    use Doctrine\ORM\Mapping as ORM;
    use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
    use Symfony\Component\Security\Core\User\UserInterface;

    /**
     * User
     *
     * @ORM\Table(name="user", uniqueConstraints=@ORM\UniqueConstraint(name="user_name", columns="user_name"), @ORM\UniqueConstraint(name="email", columns="email"))
     * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
     * @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="fast_cache")
     * @UniqueEntity(fields="email", message="Email already taken")
     * @UniqueEntity(fields="username", message="Username already taken")
     */
    class User implements UserInterface, \Serializable
    
        /**
         * @var ArrayCollection
         *
         * @ORM\ManyToMany(targetEntity="App\Entity\Role", inversedBy="users", cascade="remove")
         * @ORM\JoinTable(name="users_roles",
         *      joinColumns=@ORM\JoinColumn(name="user_id", referencedColumnName="id"),
         *      inverseJoinColumns=@ORM\JoinColumn(name="role_id", referencedColumnName="id")
         *      )
         */
        protected $roles;
        /**
         * @var int
         *
         * @ORM\Column(name="id", type="smallint", nullable=false, options="unsigned"=true)
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;
        /**
         * @var string
         *
         * @ORM\Column(name="user_name", type="string", length=255, nullable=false)
         */
        private $username;
        /**
         * @var string
         *
         * @ORM\Column(name="email", type="string", length=255, nullable=false)
         */
        private $email;
        /**
         * @var string
         *
         * @ORM\Column(name="password", type="string", length=255, nullable=false)
         */
        private $password;
        /**
         * @var bool
         *
         * @ORM\Column(name="is_enabled", type="boolean", nullable=false)
         */
        private $isEnabled;
        /**
         * @var bool
         *
         * @ORM\Column(name="is_verified", type="boolean", nullable=false)
         */
        private $isVerified;
        /**
         * @var DateTime
         *
         * @ORM\Column(name="created_at", type="datetime", nullable=false)
         */
        private $createdAt;
        /**
         * @var DateTime
         *
         * @ORM\Column(name="updated_at", type="datetime", nullable=false)
         */
        private $updatedAt;

        /**
         * @return int
         */
        public function getId(): int
        
            return $this->id;
        

        /**
         * @return string
         */
        public function getEmail(): string
        
            return $this->email;
        

        /**
         * @param string $email
         */
        public function setEmail(string $email): void
        
            $this->email = $email;
        

        /**
         * @return bool
         */
        public function isEnabled(): bool
        
            return $this->isEnabled;
        

        /**
         * @param bool $isEnabled
         */
        public function setIsEnabled(bool $isEnabled): void
        
            $this->isEnabled = $isEnabled;
        

        /**
         * @return bool
         */
        public function isVerified(): bool
        
            return $this->isVerified;
        

        /**
         * @param bool $isVerified
         */
        public function setIsVerified(bool $isVerified): void
        
            $this->isVerified = $isVerified;
        

        /**
         * @return DateTime
         */
        public function getCreatedAt(): DateTime
        
            return $this->createdAt;
        

        /**
         * @param DateTime $createdAt
         */
        public function setCreatedAt(DateTime $createdAt): void
        
            $this->createdAt = $createdAt;
        

        /**
         * @return DateTime
         */
        public function getUpdatedAt(): DateTime
        
            return $this->updatedAt;
        

        /**
         * @param DateTime $updatedAt
         */
        public function setUpdatedAt(DateTime $updatedAt): void
        
            $this->updatedAt = $updatedAt;
        

        /**
         * String representation of object
         * @link http://php.net/manual/en/serializable.serialize.php
         * @return string the string representation of the object or null
         * @since 5.1.0
         * NOTE: SYNFONY BUG 3.4 -> 4.1; https://github.com/symfony/symfony-docs/pull/9914
         */
        public function serialize(): string
        
            // add $this->salt too if you don't use Bcrypt or Argon2i
            return serialize([$this->id, $this->username, $this->password]);
        

        /**
         * Constructs the object
         * @link http://php.net/manual/en/serializable.unserialize.php
         * @param string $serialized <p>
         * The string representation of the object.
         * </p>
         * @return void
         * @since 5.1.0
         */
        public function unserialize($serialized): void
        
            // add $this->salt too if you don't use Bcrypt or Argon2i
            [$this->id, $this->username, $this->password] = unserialize($serialized, ['allowed_classes' => false]);
        

        /**
         * Returns the roles granted to the user.
         *
         * <code>
         * public function getRoles()
         * 
         *     return array('ROLE_USER');
         * 
         * </code>
         *
         * Alternatively, the roles might be stored on a ``roles`` property,
         * and populated in any number of different ways when the user object
         * is created.
         *
         * @return array The user roles
         */
        public function getRoles(): array
        
            $roles = [];
            foreach ($this->roles->toArray() AS $role) 
                $roles[] = $role->getName();
            
            return $roles;
        

        /**
         * Returns the password used to authenticate the user.
         *
         * This should be the encoded password. On authentication, a plain-text
         * password will be salted, encoded, and then compared to this value.
         *
         * @return string The password
         */
        public function getPassword(): string
        
            return $this->password;
        

        /**
         * @param string $password
         */
        public function setPassword(string $password): void
        
            $this->password = $password;
        

        /**
         * Returns the salt that was originally used to encode the password.
         *
         * This can return null if the password was not encoded using a salt.
         *
         * @return string|null The salt
         */
        public function getSalt()
        
            // See "Do you need to use a Salt?" at https://symfony.com/doc/current/cookbook/security/entity_provider.html
            // we're using bcrypt in security.yml to encode the password, so
            // the salt value is built-in and you don't have to generate one

            return null;
        

        /**
         * Returns the username used to authenticate the user.
         *
         * @return string The username
         */
        public function getUsername()
        
            return $this->username;
        

        /**
         * @param string $username
         */
        public function setUsername(string $username): void
        
            $this->username = $username;
        

        /**
         * Removes sensitive data from the user.
         *
         * This is important if, at any given point, sensitive information like
         * the plain-text password is stored on this object.
         */
        public function eraseCredentials()
        
            // if you had a plainPassword property, you'd nullify it here
            $this->plainPassword = null;
        
    

Role.php 文件:

    <?php

    namespace App\Entity;

    use DateTime;
    use Doctrine\Common\Collections\ArrayCollection;
    use Doctrine\ORM\Mapping as ORM;

    /**
     * Role
     *
     * @ORM\Table(name="role", uniqueConstraints=@ORM\UniqueConstraint(name="name", columns="name"))
     * @ORM\Entity(repositoryClass="App\Repository\RoleRepository")
     */
    class Role
    
        /**
         * @var ArrayCollection
         *
         * @ORM\ManyToMany(targetEntity="App\Entity\User", mappedBy="roles", cascade="remove")
         * @ORM\JoinTable(name="users_roles",
         *      joinColumns=@ORM\JoinColumn(name="role_id", referencedColumnName="id"),
         *      inverseJoinColumns=@ORM\JoinColumn(name="user_id", referencedColumnName="id")
         *      )
         */
        protected $users;
        /**
         * @var int
         *
         * @ORM\Column(name="id", type="smallint", nullable=false, options="unsigned"=true)
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;
        /**
         * @var string
         *
         * @ORM\Column(name="name", type="string", length=255, nullable=false)
         */
        private $name;
        /**
         * @var DateTime
         *
         * @ORM\Column(name="created_at", type="datetime", nullable=false)
         */
        private $createdAt;
        /**
         * @var DateTime
         *
         * @ORM\Column(name="updated_at", type="datetime", nullable=false)
         */
        private $updatedAt;

        /**
         * Role constructor.
         */
        public function __construct()
        
            $this->users = new ArrayCollection();
        

        /**
         * @return array
         */
        public function getUsers(): array
        
            return $this->users->toArray();
        

        /**
         * @return int
         */
        public function getId(): int
        
            return $this->id;
        

        /**
         * @param int $id
         */
        public function setId(int $id): void
        
            $this->id = $id;
        

        /**
         * @return string
         */
        public function getName(): string
        
            return $this->name;
        

        /**
         * @param string $name
         */
        public function setName(string $name): void
        
            $this->name = $name;
        

        /**
         * @return DateTime
         */
        public function getCreatedAt(): DateTime
        
            return $this->createdAt;
        

        /**
         * @param DateTime $createdAt
         */
        public function setCreatedAt(DateTime $createdAt): void
        
            $this->createdAt = $createdAt;
        

        /**
         * @return DateTime
         */
        public function getUpdatedAt(): DateTime
        
            return $this->updatedAt;
        

        /**
         * @param DateTime $updatedAt
         */
        public function setUpdatedAt(DateTime $updatedAt): void
        
            $this->updatedAt = $updatedAt;
        
    

我的数据夹具 AppFixtures.php:

    <?php

    namespace App\DataFixtures;

    use App\Entity\Role;
    use App\Entity\User;
    use DateTime;
    use Doctrine\Bundle\FixturesBundle\Fixture;
    use Doctrine\Common\Persistence\ObjectManager;
    use Doctrine\ORM\EntityManagerInterface;
    use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

    /**
     * Class AppFixtures
     * @package App\DataFixtures
     */
    class AppFixtures extends Fixture
    
        /**
         * @var UserPasswordEncoderInterface
         */
        private $encoder;
        /**
         * @var EntityManagerInterface
         */
        private $entityManager;

        /**
         * AppFixtures constructor.
         * @param UserPasswordEncoderInterface $userPasswordEncoder
         * @param EntityManagerInterface $entityManager
         */
        public function __construct(UserPasswordEncoderInterface $userPasswordEncoder, EntityManagerInterface $entityManager)
        
            $this->encoder = $userPasswordEncoder;
            $this->entityManager = $entityManager;
        

        /**
         * @param ObjectManager $manager
         */
        public function load(ObjectManager $manager)
        
            //Creating default roles
            $role = new Role();
            $role->setName('ROLE_USER');
            $role->setCreatedAt(new DateTime());
            $role->setUpdatedAt(new DateTime());
            $manager->persist($role);
            $role = new Role();
            $role->setName('ROLE_MODERATOR');
            $role->setCreatedAt(new DateTime());
            $role->setUpdatedAt(new DateTime());
            $manager->persist($role);
            $role = new Role();
            $role->setName('ROLE_ADMIN');
            $role->setCreatedAt(new DateTime());
            $role->setUpdatedAt(new DateTime());
            $manager->persist($role);
            $manager->flush();
            $manager->clear();
            //Creating users
            $user = new User();
            $user->setUserName('john');
            $user->setEmail('john@localhost');
            //$user->setRoles(['ROLE_USER', 'ROLE_JOHN']);
            //$roleAssociation = null;
            $user->setPassword($this->encoder->encodePassword($user, 'test'));
            $user->setCreatedAt(new DateTime());
            $user->setUpdatedAt(new DateTime());
            $user->setIsVerified(true);
            $user->setIsEnabled(true);
            //$manager->persist($roleAssociation);
            $manager->persist($user);

            $user = new User();
            $user->setUserName('tom');
            $user->setEmail('tom@localhost');
            //$user->setRoles(['ROLE_USER', 'ROLE_TOM', 'ROLE_MODERATOR']);
            //$roleAssociation = null;
            $user->setPassword($this->encoder->encodePassword($user, 'test'));
            $user->setCreatedAt(new DateTime());
            $user->setUpdatedAt(new DateTime());
            $user->setIsVerified(true);
            $user->setIsEnabled(true);
            //$manager->persist($roleAssociation);
            $manager->persist($user);

            $user = new User();
            $user->setUserName('jimmy');
            $user->setEmail('jimmy@localhost');
            //$user->setRoles(['ROLE_USER', 'ROLE_JIMMY', 'ROLE_ADMIN']);
            //$roleAssociation = null;
            $user->setPassword($this->encoder->encodePassword($user, 'test'));
            $user->setCreatedAt(new DateTime());
            $user->setUpdatedAt(new DateTime());
            $user->setIsVerified(true);
            $user->setIsEnabled(true);
            //$manager->persist($roleAssociation);
            $manager->persist($user);

            $manager->flush();
            $manager->clear();
        
    

我正在寻找以下方面的建议:

    用户实体被缓存在注解中,因为 symfony 在每个请求上都会加载它。配置部分:

    orm:
        metadata_cache_driver:
          type: redis
        result_cache_driver:
          type: redis
        query_cache_driver:
          type: redis
    

我在 redis 中缓存了 1 分钟的所有数据。有更好的解决方案吗?

    DB 架构在 users_roles 上创建外键 (FK) 和额外索引 桌子,我不想有 FK,因为恕我直言,这是“沉重”的事情。我宁愿只在 (user_id,role_id) 上有主键。有什么建议吗?

    灵活添加/删除用户+角色+角色用户的解决方案

非常感谢!

【问题讨论】:

无意冒犯,但我认为您可能在一个问题上问得太多了。由于一些额外的原因,查看how to ask a good question 和this question 可能会有所帮助。您的问题主题与您最后提出的问题无关,并且您有几个问题,因此解释所有细节将是一个相当大的答案。我会将这个问题分成单独的具体问题。 【参考方案1】:

我会尝试回答这个问题,但正如我在评论中提到的,这个问题有点大,所以其中一些可能会被概括。

    用户实体缓存在注解中,因为 symfony 在每个请求上 加载它。配置部分:
orm:
    metadata_cache_driver:
      type: redis
    result_cache_driver:
      type: redis
    query_cache_driver:
      type: redis

我在 redis 中缓存了 1 分钟的所有数据。有更好的解决方案吗?

当您在这里说“更好”时,它是主观的。每个应用程序都是不同的。缓存以及缓存保持活动的长度取决于每个应用程序的要求。我不知道这是否存在任何固有的错误,但这在很大程度上取决于您的应用程序的要求。

    DB 模式在 users_roles 表上创建外键 (FK) 和额外索引,我希望不使用 FK,因为恕我直言 这是“重”的东西。我希望有主键 (user_id,role_id) 仅限。有什么建议吗?

首先,FK 很重要,它们旨在保持数据的完整性。它们确保如果有人试图删除 user_roles 行链接到的用户或角色,操作将失败。您通常希望这种行为不会丢失数据或创建孤立数据。

其次,我不确定您使用的是什么版本的 Doctrine,但我类似的 ManyToMany 表确实(user_id, role_id) 上创建了一个 PK 和唯一索引。

    灵活添加/删除用户 + 角色 + 角色用户的解决方案

您可以使用 Doctrine 的 ArrayCollections 来执行此操作(单击菜单中的集合以确保锚链接有效,它们有时会损坏)。

使用默认的 Symfony User 实体执行此操作时有一个警告。它实现了Symfony\Component\Security\Core\User\UserInterface 接口,该接口定义了一个getRoles() 方法,该方法旨在将角色数组作为字符串返回。这是为了让某些 Symfony 安全功能按预期工作。这意味着,如果您有一个 private $roles 属性,则必须将其标准 Doctrine getter 重命名为其他名称,以便您可以使 getRoles() 按预期运行。

所以,对于 User 实体,我通常只是将我的 getter、setter、adder 和 remover 重命名为 getUserRoles()setUserRoles()addUserRole()removeUserRole(),然后我离开 @987654335 @ 实现预期的接口。

这是一个不完整的用户类示例,没有角色类示例。

<?php

declare(strict_types=1);

namespace App\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection/*Interface*/;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use \InvalidArgumentException;
use function array_unique;

class User implements UserInterface

    /* ... */

    /**
     * @var Role[]|array User's roles
     *
     * Note that the getter, setter, adder, and remover for this method are renamed so that the getRoles() method that
     * we implement from Symfony\Component\Security\Core\User\UserInterface can function as expected.
     *
     * @see UserInterface
     * @see User::getRoles()
     * @see User::getUserRoles()
     * @see User::setUserRoles()
     * @see User::addUserRole()
     * @see User::removeUserRole()
     *
     * @ORM\ManyToMany(targetEntity="App\Entity\Role", inversedBy="users", cascade="persist")
     */
    private $roles;

    /* ... */

    /**
     * Constructor
     */
    public function __construct()
    
        $this->roles = new ArrayCollection();
    

    /* ... */


    /**
     * @return Collection|Role[]
     */
    public function getUserRoles(): Collection
    
        return $this->roles;
    

    /**
     * @param Collection|Role[] $roles
     *
     * @return self
     */
    public function setUserRoles(Collection/*Interface*/ $roles): self
    
        $this->roles = $roles;

        return $this;
    

    /**
     * @param Role $role
     *
     * @return self
     */
    public function addUserRole(Role $role): self
    
        $this->roles->add($role);

        return $this;
    

    /**
     * @param Role $role
     *
     * @return self
     */
    public function removeUserRole(Role $role): self
    
        $this->roles->removeElement($role);

        return $this;
    

    /**
     * Get array of roles as strings
     *
     * This method is an implementation of UserInterface::getRoles(). The getter for self::$roles is
     * self::getUserRoles().
     *
     * @return string[]|array
     *
     * @see UserInterface
     */
    public function getRoles()
    
        $roleStrings = [];

        foreach ($this->roles as $role) 
            $roleStrings[] = $role->getName();
        

        // guarantee every user at least has ROLE_USER
        $roleStrings[] = 'ROLE_USER';

        return array_unique($roleStrings);
    

    /* ... */


如果您愿意,您也可以对 Role 对象执行相反的操作,但这取决于您是否希望以这种方式将用户添加到角色,并且通常最好选择关系的拥有方。

这是一个示例,说明如何在您使用实体、固定装置或其他方式的任何地方使用它。

<?php

use Doctrine\Common\Collections\ArrayCollection;
use App\Entity\User;
use App\Entity\Role;

$entityManager = (get entity manager);

$user = new User();

// You can also add the name as an argument to Role::__construct()
// then call from setName() from that if you wanted to simplify this.
$roleFoo = (new Role())->setName('ROLE_FOO');
$roleBar = (new Role())->setName('ROLE_BAR');

// set roles using ArrayCollection of roles.
$user->setUserRoles(new ArrayCollection($roleFoo, $roleBar));

// add new role, see User::addUserRole() for 
$user->addUserRole((new Role()->setName('ROLE_WIN'));

// remove ROLE_BAR
// You can also do this with entities that you find with Doctrine
// if you want to remove them from a persisted collection.
$user->removeUserRole($roleBar);

// get roles as a collection of Role objects.
// This will return either ArrayCollection or PersistentCollection
// depending on context. These are objects that act like arrays
// and each element will be a Role object.
$roles = $user->getUserRoles();

// get roles as strings... mostly used by Symfony's security system
// but could be used by your app too.
$roleStrings = $user->getRoles();

【讨论】:

以上是关于Symfony 4 - 加载动态用户角色的固定装置的主要内容,如果未能解决你的问题,请参考以下文章

Symfony 3.4.0 找不到要加载的任何夹具服务

Symfony 学说数据加载分段错误

Symfony 4 - 从数据库定义自定义用户角色

Symfony 固定装置和多对多关系(学说)

如何从现有数据库数据生成 Symfony 固定装置 YAML?

如何在 rails (4.1.5) 中创建固定装置(对于 Devise 用户)作为 yml.erb?