Il peut être utile d’avoir la possibilité d’ajouter des utilisateurs, d’attribuer et de retirer des rôles, d’activer un compte ou de le désactiver et de modifier un mot de passe en passant par la console.

De cette manière, on n’est pas obligé de développer toutes ces fonctionnalités dans l’interface graphique et ça peut même dans certain cas éviter des problèmes de sécurité.

Prenons par exemple la création du compte super administrateur. Normalement, il est créé une seule fois dans la vie d’une application.

Il est donc plus facile de se connecter en SSH et de taper la commande suivante.

php bin/console app:user:create Martin GILBERT martin.gilbert@dev-fusion.com mon_mot_de_passe

FOSUserBundle implémentait toutes ces commandes. Puisque ce bundle n’est plus conseillé depuis Symfony 4, voyons voir comment on peut les développer par nous-même.

Cet article fait suite à un précédent : Symfony 4 / 5 - Créer rapidement un projet de base complet avec un espace utilisateur.

Pour cloner le projet de base :

git clone --single-branch --branch user-area git@github.com:official-dev-fusion/dev-fusion-skeleton-user.git

Liste des commandes

  • app:user:create : Créer un utilisateur
  • app:user:change-password : Changer un mot de passe
  • app:user:promote : Ajouter un rôle
  • app:user:demote : Supprimer un rôle
  • app:user:activate : Activer un utilisateur pour qu’il puisse se connecter
  • app:user:deactivate : Désactiver un utilisateur pour qu’il ne puisse plus se connecter

Commençons par créer un squelette de commande :

Nous allons utiliser le maker-bundle de Symfony.

php bin/console make:commande app:user:activate

 created: src/Command/UserActivateCommand.php


  Success!


 Next: open your new command class and customize it!
 Find the documentation at https://symfony.com/doc/current/console.html

Le constructeur

Dans tous les cas, nous allons avoir besoin de l’EntityManager et du UserRepository afin de récupérer les utilisateurs et les persister.

Nous allons donc ajouter deux propriétés et un constructeur à la classe générée par le MakerBundle afin d’injecter ces services à notre commande.

// ...
    /**
     *
     * @var EntityManagerInterface
     */
    private $em;
    
    /**
     *
     * @var UserRepository
     */
    private $userRepository;

    public function __construct(EntityManagerInterface $em, UserRepository $userRepository)
    {
        $this->em = $em;
        $this->userRepository = $userRepository;
        parent::__construct();
    }
// ...

La méthode config

Ensuite, nous allons également avoir besoin d’une description, d’un argument pour l’adresse mail afin de créer ou récupérer les utilisateurs et d’un texte d’aide.

// ...
    protected function configure()
    {
        $this
            ->setDescription('')
            ->addArgument('email', InputArgument::REQUIRED, 'The email')
            ->setHelp(implode("\n", [
                '',
            ]))
        ;
    }
// ...

La méthode interact

Afin d’interagir avec l’utilisateur de la console, nous allons ajouter le code suivant :

// ...
    protected function interact(InputInterface $input, OutputInterface $output)
    {
        $questions = [];

        if (!$input->getArgument('email')) {
            $question = new Question('Please give the email:');
            $question->setValidator(function ($email) {
                if (empty($email)) {
                    throw new \Exception('email can not be empty');
                }

                if (!$this->userRepository->findOneByEmail($email)) {
                    throw new \Exception('No user found with this email');
                }

                return $email;
            });
            $questions['email'] = $question;
        }
        
        foreach ($questions as $name => $question) {
            $answer = $this->getHelper('question')->ask($input, $output, $question);
            $input->setArgument($name, $answer);
        }
    }
// ...

Explication :

  • On crée une liste de questions de type Symfony\Component\Console\Question\Question;
  • On demande l’adresse mail de l’utilisateur à créer ou modifier;
  • On valide que la chaîne n’est pas vide sinon on lance une exception qui aura l’effet de redemander l’adresse mail;
  • On valide que l’utilisateur avec cette adresse existe ou pas selon la commande;
  • On lance la boucle pour exécuter les questions;

Voilà, nous avons un squelette de base pour écrire nos cinq commandes.

Activer et désactiver des utilisateurs

Nous avons seulement à compléter la méthode execute de notre squelette afin d’obtenir notre commande pour activer des utilisateurs.

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $email = $input->getArgument('email');
        $user = $this->userRepository->findOneByEmail($email);
        
        $user->setEnabled(true);
        $this->em->flush();
        
        $io->success(sprintf('User "%s" has been activated.', $email));
        return 0;
    }

Vous pouvez remplir la description et l’aide de la commande et c’est tout, nous avons terminé pour l’activation.

La même chose peut être répétée pour créer la commande de désactivation.

Octroyer et retirer des rôles à des utilisateurs

En repartant de notre squelette, il faut ajouter un argument rôle à nos commandes promote et demote.

N’oubliez pas de renommer les classes en UserPromoteCommand et UserDemoteCommand et de modifier la propriété $defaultName.

Voici le résultat pour la méthode config :

// ...
    protected function configure()
    {
        $this
            ->setDescription('Promotes a user by adding a role')
            ->addArgument('email', InputArgument::REQUIRED, 'The email')
            ->addArgument('role', InputArgument::REQUIRED, 'The new role')
            ->setHelp(implode("\n", [
                'The <info>app:user:promote</info> command add role to a user:',
                '<info>php %command.full_name% martin.gilbert@dev-fusion.com</info>',
                'This interactive shell will first ask you for a role.',
                'You can alternatively specify the role as a second argument:',
                '<info>php %command.full_name% martin.gilbert@dev-fusion.com ROLE_ADMIN</info>',
            ]))
        ;
    }
// ...

Il faut également ajouter une question dans la méthode interact :

// ...
        if (!$input->getArgument('role')) {
            $question = new Question('Please enter the new role:');
            $question->setValidator(function ($role) {
                if (empty($role)) {
                    throw new \Exception('role can not be empty');
                }

                return $role;
            });
            $questions['role'] = $question;
        }
// ...

Maintenant dans la méthode execute :

  • On demande l’utilisateur au repository;
  • Si l’utilisateur a déjà ce rôle, on affiche une erreur;
  • Sinon, on ajoute le rôle à l’utilisateur et on flush;
// ...
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $email = $input->getArgument('email');
        $role = $input->getArgument('role');
        $user = $this->userRepository->findOneByEmail($email);
        
        $roles = $user->getRoles();
        
        if (in_array($role, $roles)) {
            $io->error(sprintf("The user %s has already role %s", $email, $role));
            return 1;
        } else {
            $roles[] = $role;
            $user->setRoles($roles);
            $this->em->flush();
            $io->success(sprintf('The role %s has been added to the user %s.', $role, $email));
            return 0;
        }
    }
// ...

Enfin, on fait le contraire pour la commande demote :

// ...
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $email = $input->getArgument('email');
        $role = $input->getArgument('role');
        $user = $this->userRepository->findOneByEmail($email);
        
        $roles = $user->getRoles();
        
        if (!in_array($role, $roles)) {
            $io->error(sprintf("The user %s has not role %s.", $email, $role));
            return 1;
        } else {
            array_splice($roles, array_search($role, $roles), 1);
            $user->setRoles($roles);
            $this->em->flush();
            $io->success(sprintf('The role %s has been removed to user %s.', $role, $email));
            return 0;
        }
    }
// ...

Ajouter des utilisateurs

Nous allons devoir injecter un service pour encoder le mot de passe des utilisateurs.

// ...
    /**
     *
     * @var UserPasswordEncoderInterface
     */
    private $passwordEncoder;
// ...
    public function __construct(UserPasswordEncoderInterface $passwordEncoder, EntityManagerInterface $em, UserRepository $userRepository)
    {
        $this->passwordEncoder = $passwordEncoder;
        $this->em = $em;
        $this->userRepository = $userRepository;
        parent::__construct();
    }
// ...

Dans la méthode config :

  • Ajout d’un argument firstname
  • Ajout d’un argument lastname
  • Ajout d’un argument password
  • Ajout d’une option super-admin
  • Ajout d’une option inactive
    protected function configure()
    {
        $this
            ->setDescription('Create a user.')
            ->addArgument('firstname', InputArgument::REQUIRED, 'The firstname')
            ->addArgument('lastname', InputArgument::REQUIRED, 'The lastname')
            ->addArgument('email', InputArgument::REQUIRED, 'The email')
            ->addArgument('password', InputArgument::REQUIRED, 'The password')
            ->addOption('super-admin', null, InputOption::VALUE_NONE, 'Set the user as super admin')
            ->addOption('inactive', null, InputOption::VALUE_NONE, 'Set the user as inactive')
            ->setHelp(implode("\n", [
                'The <info>app:user:create</info> command creates a user:',
                '<info>php %command.full_name% Martin GILBERT</info>',
                'This interactive shell will ask you for an email and then a password.',
                'You can alternatively specify the email and password as the second and third arguments:',
                '<info>php %command.full_name% Martin GILBERt martin.gilbert@dev-fusion.com change_this_password</info>',
                'You can create a super admin via the super-admin flag:',
                '<info>php %command.full_name% --super-admin</info>',
                'You can create an inactive user (will not be able to log in):',
                '<info>php %command.full_name% --inactive</info>',
            ]))
        ;
    }

Voilà, de cette manière on va pouvoir rapidement créer un super administrateur ou un utilisateur désactivé.

Maintenant la méthode interact :

// ...
    protected function interact(InputInterface $input, OutputInterface $output)
    {
        $questions = [];

        if (!$input->getArgument('firstname')) {
            $question = new Question('Please enter the firstname:');
            $question->setValidator(function ($firstname) {
                if (empty($firstname)) {
                    throw new \Exception('Firstname can not be empty');
                }

                return $firstname;
            });
            $questions['firstname'] = $question;
        }

        if (!$input->getArgument('lastname')) {
            $question = new Question('Please enter the lastname:');
            $question->setValidator(function ($lastname) {
                if (empty($lastname)) {
                    throw new \Exception('Lastname can not be empty');
                }

                return $lastname;
            });
            $questions['lastname'] = $question;
        }

        if (!$input->getArgument('email')) {
            $question = new Question('Please enter an email:');
            $question->setValidator(function ($email) {
                if (empty($email)) {
                    throw new \Exception('Email can not be empty');
                }
                if ($this->userRepository->findOneByEmail($email)) {
                    throw new \Exception('Email is already used');
                }

                return $email;
            });
            $questions['email'] = $question;
        }

        if (!$input->getArgument('password')) {
            $question = new Question('Please choose a password:');
            $question->setValidator(function ($password) {
                if (empty($password)) {
                    throw new \Exception('Password can not be empty');
                }

                return $password;
            });
            $question->setHidden(true);
            $questions['password'] = $question;
        }

        foreach ($questions as $name => $question) {
            $answer = $this->getHelper('question')->ask($input, $output, $question);
            $input->setArgument($name, $answer);
        }
    }
// ...

Et voilà la méthode execute :

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $email = $input->getArgument('email');
        $user = new User();
        $user
            ->setFirstname($input->getArgument('firstname'))
            ->setLastname($input->getArgument('lastname'))
            ->setEmail($email);

        $user->setPassword(
            $this->passwordEncoder->encodePassword(
                $user,
                $input->getArgument('password')
            )
        );
        
        if ($input->getOption('inactive')) {
            $user->setEnabled(false);
        } else {
            $user->setEnabled(true);
        }
        
        if ($input->getOption('super-admin')) {
            $user->setRoles(['ROLE_SUPER_ADMIN']);
        }

        $this->em->persist($user);
        $this->em->flush();

        $io->success(sprintf('Created user with email %s.', $email));

        return 0;
    }

Modifier le mot de passe des utilisateurs

Pour la commande app:user:change-password nous pouvons utiliser le même constructeur que pour la création.

Dans la méthode interact, on peut garder les éléments suivants :

// ...
    protected function configure()
    {
        $this
            ->setDescription('Change the password of a user.')
            ->addArgument('email', InputArgument::REQUIRED, 'The email')
            ->addArgument('password', InputArgument::REQUIRED, 'The new password')
            ->setHelp(implode("\n", [
                'The <info>app:user:change-password</info> command changes the password of a user:',
                '<info>php %command.full_name% martin.gilbert@dev-fusion.com</info>',
                'This interactive shell will first ask you for a password.',
                'You can alternatively specify the password as a second argument:',
                '<info>php %command.full_name% martin.gilbert@dev-fusion.com change_this_password</info>',
            ]))
        ;
    }
// ...

Je vous donne également le code pour la méthode execute :

// ...
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $email = $input->getArgument('email');
        $user = $this->userRepository->findOneByEmail($email);
        
        $user->setPassword(
            $this->passwordEncoder->encodePassword(
                $user,
                $input->getArgument('password')
            )
        );
        
        $this->em->flush();
        
        $io->success(sprintf('Changed password for user %s.', $email));

        return 0;
    }
// ...

En conclusion

Nous avons finalement toutes les commandes nécessaires pour gérer nos utilisateurs comme dans friendsofsymfony/user-bundle.

Nous pouvons nous connecter en SSH à notre application pour créer des utilisateurs, changer leur mot de passe, leurs rôles, les activer et les désactiver.

Toutes nos commandes se ressemblent, nous pouvons donc facilement en créer de nouvelles si jamais le besoin se présente.

Vous pouvez retrouver tous les fichiers dans le repository Github du projet.

Pour cloner le projet de base :

git clone --single-branch --branch user-area-with-commands git@github.com:official-dev-fusion/dev-fusion-skeleton-user.git

Il manque seulement la gestion des utilisateurs du côté back-office. Nous allons voir cela dans un prochain tutoriel qui devrait venir rapidement. Ça ne devrait pas être très compliqué, car nous allons utiliser le devfusion/maker-bundle pour générer 90 % du code.

En espérant que tout cela vous sera utile.

Si vous rencontrer des bogues, des erreurs ou si vous avez des commentaires, vous pouvez m’en faire part à l’adresse martin.gilbert@dev-fusion.com.