简体   繁体   中英

Symfony 4 - load fixtures for dynamic user roles

Currently I'm trying to modify my classes and looking for idea to save dynamic relations between users and roles.

I want to create associations when loading fixtures and also to have such a functionality in controller when I need to create an user with relation, example:

...
$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();

My User.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 file:

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

My data fixtures 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();
        }
    }

I'm looking for advises for:

  1. User entity is cached in annotation, because symfony on each request loads it. config part:

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

I'm caching all the data for 1min in redis. Any better solution?

  1. DB schema creates Foreign Keys (FK) and extra indexes on users_roles table and I would love to not to have FK, because IMHO it's "heavy" thing. I would prefer to have primary key on (user_id,role_id) only. Any recommendations of this?

  2. The solution for having flexible add/remove for user + role + roles_users

Big thanks!

I'll try to answer this but as I mentioned in my comment, this question is a bit large so some of this may be generalized.

  1. User entity is cached in annotation, because symfony on each request loads it. config part:
 orm: metadata_cache_driver: type: redis result_cache_driver: type: redis query_cache_driver: type: redis

I'm caching all the data for 1min in redis. Any better solution?

When you say "better" here, it's subjective. Each application is different. Caches, and the length at which caches stay alive, is specific to each application's requirements. I don't know if there's anything inherently wrong with this, but it's largely dependent on your app's requirements.

  1. DB schema creates Foreign Keys (FK) and extra indexes on users_roles table and I would love to not to have FK, because IMHO it's "heavy" thing. I would prefer to have primary key on (user_id,role_id) only. Any recommendations of this?

First, FKs are important, and they're intended to keep integrity in your data. They ensure that if someone attempted to delete a user or role that a user_roles row linked to, the operation would fail. You usually want this behavior so that you don't lose data or create orphaned data.

Secondly, I'm not sure what version of Doctrine you're using but my similar ManyToMany tables do create a PK and unique index on (user_id, role_id) .

  1. The solution for having flexible add/remove for user + role + roles_users

You do this by using Doctrine's ArrayCollections (click Collections in the menu to make sure the anchor link worked, they're broken sometimes).

There is one caveat when doing this with the the default Symfony User entity though. It implements the Symfony\\Component\\Security\\Core\\User\\UserInterface interface which defines a getRoles() method that is meant to return an array of roles as strings. This is so that certain Symfony security features work as expected. This means that if you have a private $roles property that you will have to rename its standard Doctrine getter to something else so that you can leave getRoles() functioning as expected.

So, for the User entity, I typically just rename my getter, setter, adder, and remover to something like getUserRoles() , setUserRoles() , addUserRole() , and removeUserRole() and then I leave getRoles() to implement the expected interface.

Here is an incomplete example of a user class with no Role class example.

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

    /* ... */
}

You can also do the reverse for the Role object if you wanted to, but this depends on if you wanted to add users to roles that way and it's often best to choose the owning side of the relation.

Here is an example of how this can be used anywhere where you're working with entities, fixtures or otherwise.

<?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();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM