Symfony2 + FOSUserBundle – Comment étendre la commande par défaut de création d’un utilisateur

La gestion des utilisateurs dans un projet symfony2 est une tâche répétitive qu’on confie la plupart du temps à un bundle qui gère parfaitement bien la chose : FOSUserBundle.
Le bundle fournit une couche d’abstraction via les UserManager et GroupManager qui permet au développeur de stocker ses utilisateurs via Doctrine ORM, MongoDB / CouchDB ODM ou encore via Propel.
Il fournit également tout un lot de commandes bien utiles, et notamment une qui permet d’ajouter en CLI un utilisateur en base en lui définissant ses attributs obligatoires. On peut éventuellement lui ajouter ou révoquer un ou plusieurs rôles, également via des commandes dédiées.

Une entité User de base est définie dans le bundle, entité que le développeur s’empresse en général de modifier pour lui rajouter les attributs nécessaires à son application.
Les champs email et username sont déjà obligatoires, on souhaite souvent rajouter par exemple :

  • Firstname
  • Lastname

On modifie alors notre entité User et on se retrouve à quelque chose du genre :

<?php

namespace Acme\AcmeUserBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Entity\User as BaseUser;

/**
 * @ORM\Table(name="users")
 */
class User extends BaseUser
{
    /**
     * @var integer $id
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    protected  $id;

    /**
     * @var string $last_name
     *
     * @ORM\Column(name="last_name", type="string", length=255)
     */
    private $last_name;

    /**
     * @var string $first_name
     *
     * @ORM\Column(name="first_name", type="string", length=255)
     */
    private $first_name;
    
    // ...
}
&#91;/sourcecode&#93;

Dans mon exemple, les attributs <strong>firstname</strong> et <strong>lastname</strong> sont mappés sur des champs not nullable.
Et c'est justement le fait que ces champs soient requis qui va nous poser problème lors de l'exécution de la commande de création d'un nouvel utilisateur.

<code>> php app/console fos:user:create
Please choose a username:test
Please choose an email:test@test.fr
Please choose a password:testtest
</code>

Et on se choppe, c'est normal, une grosse exception :

<code>[Doctrine\DBAL\DBALException]
[PDOException] 
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'last_name' cannot be null
</code>

La solution est donc de surcharger la commande du bundle pour y ajouter les contraintes propres à notre application, à savoir, la saisie du prénom et du nom de famille de l'utilisateur.
La commande <strong>CreateUser</strong> se trouve dans <code>FOS\UserBundle\Command\CreateUserCommand</code>.
On commence donc par créer la notre, dans notre propre UserBundle en la faisant étendre de celle du FOSUserBundle :


<?php

namespace Acme\AcmeUserBundle\Command;

use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use FOS\UserBundle\Command\CreateUserCommand as BaseCommand;

class CreateUserCommand extends BaseCommand
{
    // ...
}
&#91;/sourcecode&#93;

On souhaite rajouter deux nouveaux arguments obligatoires dans le configure et on le fait de la manière suivante :

&#91;sourcecode language="php"&#93;
<?php

// ...
class CreateUserCommand extends BaseCommand
{
    /**
     * @see Command
     */
    protected function configure()
    {
        parent::configure();
        $this
            ->setName('acme:user:create')
            ->getDefinition()->addArguments(array(
                new InputArgument('firstname', InputArgument::REQUIRED, 'The firstname'),
                new InputArgument('lastname', InputArgument::REQUIRED, 'The lastname')
            ))
        ;
        $this->setHelp(<<<EOT
// L'aide qui va bien
EOT
            );
    }

    // ...

    protected function interact(InputInterface $input, OutputInterface $output)
    {
        parent::interact($input, $output);
        if (!$input->getArgument('firstname')) {
            $firstname = $this->getHelper('dialog')->askAndValidate(
                $output,
                'Please choose a firstname:',
                function($firstname) {
                    if (empty($firstname)) {
                        throw new \Exception('Firstname can not be empty');
                    }

                    return $firstname;
                }
            );
            $input->setArgument('firstname', $firstname);
        }
        if (!$input->getArgument('lastname')) {
            $lastname = $this->getHelper('dialog')->askAndValidate(
                $output,
                'Please choose a lastname:',
                function($lastname) {
                    if (empty($lastname)) {
                        throw new \Exception('Lastname can not be empty');
                    }

                    return $lastname;
                }
            );
            $input->setArgument('lastname', $lastname);
        }
    }
    // ...
}

Et enfin, on surcharge la méthode execute. FOSUserBundle utiliser la classe UserManipulator via le service user_manipulator.
Libre à vous de créer votre propre UserManipulator ou de placer votre code de création directement au sein de la méthode, en passant bien par le UserManager, comme ceci :

<?php
    // ...

    /**
     * @see Command
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        /** @var \FOS\UserBundle\Model\UserManager $user_manager */
        $user_manager = $this->getContainer()->get('fos_user.user_manager');

        /** @var \Acme\AcmeUserBundle\Entity\User $user */
        $user = $user_manager->createUser();
        $user->setUsername($input->getArgument('username'));
        $user->setEmail($input->getArgument('email'));
        $user->setPlainPassword($input->getArgument('password'));
        $user->setEnabled(!$input->getOption('inactive'));
        $user->setSuperAdmin((bool)$input->getOption('super-admin'));
        $user->setFirstName($input->getArgument('firstname'));
        $user->setLastName($input->getArgument('lastname'));

        $user_manager->updateUser($user);

        $output->writeln(sprintf('Created user <comment>%s</comment>', $username));
    }
    // ...
}

Ci-dessous, la commande complète, à adapter selon vos besoins :

<?php

namespace Acme\AcmeUserBundle\Command;

use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use FOS\UserBundle\Model\User;
use FOS\UserBundle\Command\CreateUserCommand as BaseCommand;

class CreateUserCommand extends BaseCommand
{
    /**
     * @see Command
     */
    protected function configure()
    {
        parent::configure();
        $this
            ->setName('acme:user:create')
            ->getDefinition()->addArguments(array(
                new InputArgument('firstname', InputArgument::REQUIRED, 'The firstname'),
                new InputArgument('lastname', InputArgument::REQUIRED, 'The lastname')
            ))
        ;
        $this->setHelp(<<<EOT
// L'aide qui va bien
EOT
            );
    }

    /**
     * @see Command
     */
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $username   = $input->getArgument('username');
        $email      = $input->getArgument('email');
        $password   = $input->getArgument('password');
        $firstname  = $input->getArgument('firstname');
        $lastname   = $input->getArgument('lastname');
        $inactive   = $input->getOption('inactive');
        $superadmin = $input->getOption('super-admin');

        /** @var \FOS\UserBundle\Model\UserManager $user_manager */
        $user_manager = $this->getContainer()->get('fos_user.user_manager');

        /** @var \Acme\AcmeUserBundle\Entity\User $user */
        $user = $user_manager->createUser();
        $user->setUsername($username);
        $user->setEmail($email);
        $user->setPlainPassword($password);
        $user->setEnabled((Boolean) !$inactive);
        $user->setSuperAdmin((Boolean) $superadmin);
        $user->setFirstName($firstname);
        $user->setLastName($lastname);

        $user_manager->updateUser($user);

        $output->writeln(sprintf('Created user <comment>%s</comment>', $username));
    }

    /**
     * @see Command
     */
    protected function interact(InputInterface $input, OutputInterface $output)
    {
        parent::interact($input, $output);
        if (!$input->getArgument('firstname')) {
            $firstname = $this->getHelper('dialog')->askAndValidate(
                $output,
                'Please choose a firstname:',
                function($firstname) {
                    if (empty($firstname)) {
                        throw new \Exception('Firstname can not be empty');
                    }

                    return $firstname;
                }
            );
            $input->setArgument('firstname', $firstname);
        }
        if (!$input->getArgument('lastname')) {
            $lastname = $this->getHelper('dialog')->askAndValidate(
                $output,
                'Please choose a lastname:',
                function($lastname) {
                    if (empty($lastname)) {
                        throw new \Exception('Lastname can not be empty');
                    }

                    return $lastname;
                }
            );
            $input->setArgument('lastname', $lastname);
        }
    }
}
Cette entrée a été publiée dans Non classé le par .

À propos Olivier Balais

Jeune ingénieur logiciel basé à Lyon (@overnetcity) passionné par les NTIC et le développement Web, je suis actuellement salarié chez Reputation VIP et effectue en parallèle des missions ponctuelles en temps que Freelance. Passionné depuis toujours par l'informatique et le développement, suite à une formation solide à l'INSA de Lyon, je me suis spécialisé dans la réalisation de bout en bout de projets web complexes.

8 réflexions au sujet de « Symfony2 + FOSUserBundle – Comment étendre la commande par défaut de création d’un utilisateur »

  1. bendu38

    bonjour,
    je développe un site en utilisant le bundle FOSUser, et je n’arrive pas à trouvé de doc ou tuto clair détaillant la supression de l’attribut username que je n’utilise pas. auriez vous la solution?
    cordialement

    Répondre
    1. Olivier Balais Auteur de l’article

      Bonjour,

      A mon sens le FOSUserBundle n’est pas prévu pour fonctionner sans le username.
      Une astuce consisterait à utiliser l’adresse email pour définir le username et se connecter via l’adresse email.

      Quelques ressources à ce sujet ici :

      Répondre
  2. bendu38

    Salut,
    j’ai finalement décidé de garder le username, et de l’utiliser comme nom de famille dans mon formulaire d’inscritption, auqel je voulais rajouter d’autre attribut, comme le prénom, j’ai suivi ta méthode ci dessus. et j’ai une grosse exception quand je fais php app/console fos:user:create.
    voici mes code
    user.php:
    <?php
    // src/Hobiiz/UserBundle/Entity/User.php

    namespace Hobiiz\UserBundle\Entity;

    use FOS\UserBundle\Entity\User as BaseUser;
    use Doctrine\ORM\Mapping as ORM;

    /**
    * @ORM\Entity
    * @ORM\Table(name="user")
    */
    class User extends BaseUser
    {
    /**
    *@var integer $id
    *
    * @ORM\Id
    * @ORM\Column(type="integer")
    * @ORM\GeneratedValue(strategy="AUTO")
    */
    protected $id;

    /**
    *@var string $prenom
    * @ORM\Column(name="prenom", type="string",length=255)
    */
    private $prenom;
    }

    command.php:
    namespace Hobiiz\UserBundle\Command;

    use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
    use Symfony\Component\Console\Input\InputArgument;
    use Symfony\Component\Console\Input\InputOption;
    use Symfony\Component\Console\Input\InputInterface;
    use Symfony\Component\Console\Output\OutputInterface;

    setName(‘hobiiz:user:create’)
    ->getDefinition()->addArguments(array(
    new InputArgument(‘prenom’, InputArgument::REQUIRED, ‘The prenom’),
    ))
    ;
    $this->setHelp(<<getArgument(‘username’);
    $email = $input->getArgument(’email’);
    $password = $input->getArgument(‘password’);
    $prenom = $input->getArgument(‘prenom’);
    $inactive = $input->getOption(‘inactive’);
    $superadmin = $input->getOption(‘super-admin’);

    /** @var \FOS\UserBundle\Model\UserManager $user_manager */
    $user_manager = $this->getContainer()->get(‘fos_user.user_manager’);

    /** @var \Hobiiz\UserBundle\Entity\User $user */
    $user = $user_manager->createUser();
    $user->setUsername($username);
    $user->setEmail($email);
    $user->setPlainPassword($password);
    $user->setEnabled((Boolean) !$inactive);
    $user->setSuperAdmin((Boolean) $superadmin);
    $user->setPrenom($prenom);

    $user_manager->updateUser($user);

    $output->writeln(sprintf(‘Created user %s’, $username));
    }

    protected function interact(InputInterface $input, OutputInterface $output)
    {
    parent::interact($input, $output);
    if (!$input->getArgument(‘prenom’)) {
    $prenom = $this->getHelper(‘dialog’)->askAndValidate(
    $output,
    ‘Please choose a prenom:’,
    function($prenom) {
    if (empty($prenom)) {
    throw new \Exception(‘²Prenom can not be empty’);
    }

    return $prenom;
    }
    );
    $input->setArgument(‘prenom’, $prenom);

    }
    }

    je suis en traind de me demander si l’utilisation de FOSUser est vraiment indispensable vu comme je galère pour l’utiliser.
    cordialement

    Répondre
  3. Gilbert ARMENGAUD

    Bravo pour ce travail que je trouve fort instructif.

    Toutefois j’ai une erreur à l’exécution de la nouvelle commande :
    PHP Warning: Missing argument 1 for Symfony\Component\Console\Command\Command::setDefinition(), called in /home/gilbert/public_html/F1CMS_dev/src/F1rstCMS/UserBundle/Command/CreateUserCommand.php on line 24 and defined in /home/gilbert/public_html/F1CMS_dev/vendor/symfony/symfony/src/Symfony/Component/Console/Command/Command.php on line 310
    la ligne 24 de mon source correspond à la ligne 22 du votre.
    Il semble manquer un paramètre à setDefinition().
    J’utilise la version de 2.4.1 de Symfony avec la dernière version de FOSUserBundle (branche master)

    Auriez-vous quelque idée sur cette erreur ? Merci d’avance
    Gilbert ARMENGAUD

    Répondre
  4. sisi

    merci Olivier Balais pour ce tuto c est bien fait
    je l’ai suivie pour ajouter mes propres champs simplement je crois qu’il faut changer aussi quelque chose dans la vue register_content.html.twig pour afficher ces champs sinon comment faire ???

    Répondre
  5. jujules

    salut j’ai cette erreur quand je tente de me connecter.
    [Symfony\Component\Console\Exception\InvalidArgumentException]
    The helper « dialog » is not defined.

    vous pouvez m’aider svp?

    Répondre
  6. serge

    Bonjour à vous, j’aimerais savoir si vous pouviez avoir un tutoriel qui montre comment ajouter une image (avatar) au profil d’un utilisateur en étendant le Bundle FOSUser.
    Merci pour votre comprehension

    Répondre

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *