* @author Yonel Ceruto */ class AddUserCommand extends Command { // to make your command lazily loaded, configure the $defaultName static property, // so it will be instantiated only when the command is actually called. protected static $defaultName = 'bolt:add-user'; /** @var SymfonyStyle */ private $io; /** @var EntityManagerInterface */ private $entityManager; /** @var UserPasswordEncoderInterface */ private $passwordEncoder; /** @var Validator */ private $validator; /** @var UserRepository */ private $users; public function __construct(EntityManagerInterface $em, UserPasswordEncoderInterface $encoder, Validator $validator, UserRepository $users) { parent::__construct(); $this->entityManager = $em; $this->passwordEncoder = $encoder; $this->validator = $validator; $this->users = $users; } /** * {@inheritdoc} */ protected function configure(): void { $this ->setDescription('Creates users and stores them in the database') ->setHelp($this->getCommandHelp()) // commands can optionally define arguments and/or options (mandatory and optional) // see https://symfony.com/doc/current/components/console/console_arguments.html ->addArgument('username', InputArgument::OPTIONAL, 'The username of the new user') ->addArgument('password', InputArgument::OPTIONAL, 'The plain password of the new user') ->addArgument('email', InputArgument::OPTIONAL, 'The email of the new user') ->addArgument('display-name', InputArgument::OPTIONAL, 'The display name of the new user') ->addOption('admin', null, InputOption::VALUE_NONE, 'If set, the user is created as an administrator'); } /** * This optional method is the first one executed for a command after configure() * and is useful to initialize properties based on the input arguments and options. */ protected function initialize(InputInterface $input, OutputInterface $output): void { // SymfonyStyle is an optional feature that Symfony provides so you can // apply a consistent look to the commands of your application. // See https://symfony.com/doc/current/console/style.html $this->io = new SymfonyStyle($input, $output); } /** * This method is executed after initialize() and before execute(). Its purpose * is to check if some of the options/arguments are missing and interactively * ask the user for those values. * * This method is completely optional. If you are developing an internal console * command, you probably should not implement this method because it requires * quite a lot of work. However, if the command is meant to be used by external * users, this method is a nice way to fall back and prevent errors. */ protected function interact(InputInterface $input, OutputInterface $output): void { if ($input->getArgument('username') !== null && $input->getArgument('password') !== null && $input->getArgument('email') !== null && $input->getArgument('display-name') !== null) { return; } $this->io->title('Add Bolt User Command'); $this->io->text([ 'If you prefer to not use this interactive wizard, provide the', 'arguments required by this command as follows:', '', ' $ php bin/console bolt:add-user username password email@example.com DisplayName', '', 'Now we\'ll ask you for the value of all the missing command arguments.', ]); // Ask for the username if it's not defined $username = $input->getArgument('username'); if ($username !== null) { $this->io->text(' > Username: ' . $username); } else { $username = $this->io->ask('Username', null, [$this->validator, 'validateUsername']); $input->setArgument('username', $username); } // Ask for the password if it's not defined $password = $input->getArgument('password'); if ($password !== null) { $this->io->text(' > Password: ' . str_repeat('*', mb_strlen($password))); } else { $password = $this->io->ask('Password', Str::generatePassword(), [$this->validator, 'validatePassword']); $input->setArgument('password', $password); } // Ask for the email if it's not defined $email = $input->getArgument('email'); if ($email !== null) { $this->io->text(' > Email: ' . $email); } else { $email = $this->io->ask('Email', null, [$this->validator, 'validateEmail']); $input->setArgument('email', $email); } // Ask for the display name if it's not defined $displayName = $input->getArgument('display-name'); if ($displayName !== null) { $this->io->text(' > Display Name: ' . $displayName); } else { $displayName = $this->io->ask('Display Name', null, [$this->validator, 'validateDisplayName']); $input->setArgument('display-name', $displayName); } } /** * This method is executed after interact() and initialize(). It usually * contains the logic to execute to complete this command task. */ protected function execute(InputInterface $input, OutputInterface $output) { $stopwatch = new Stopwatch(); $stopwatch->start('add-user-command'); $username = $input->getArgument('username'); $plainPassword = $input->getArgument('password'); $email = $input->getArgument('email'); $displayName = $input->getArgument('display-name'); $isAdmin = $input->getOption('admin'); // make sure to validate the user data is correct $this->validateUserData($username, $plainPassword, $email, $displayName); // create the user and encode its password $user = new User(); $user->setDisplayName($displayName); $user->setUsername($username); $user->setEmail($email); $user->setRoles([$isAdmin ? 'ROLE_ADMIN' : 'ROLE_USER']); $user->setLocale('en'); $user->setBackendTheme('default'); // See https://symfony.com/doc/current/book/security.html#security-encoding-password $encodedPassword = $this->passwordEncoder->encodePassword($user, $plainPassword); $user->setPassword($encodedPassword); $this->entityManager->persist($user); $this->entityManager->flush(); $this->io->success(sprintf('%s was successfully created: %s (%s)', $isAdmin ? 'Administrator user' : 'User', $user->getUsername(), $user->getEmail())); $event = $stopwatch->stop('add-user-command'); if ($output->isVerbose()) { $this->io->comment(sprintf('New user database id: %d / Elapsed time: %.2f ms / Consumed memory: %.2f MB', $user->getId(), $event->getDuration(), $event->getMemory() / (1024 ** 2))); } return null; } private function validateUserData(string $username, string $plainPassword, string $email, string $displayName): void { // first check if a user with the same username already exists. $existingUser = $this->users->findOneBy(['username' => $username]); if ($existingUser !== null) { throw new RuntimeException(sprintf('There is already a user registered with the "%s" username.', $username)); } // @todo Validation must be moved to a separate UserValidator // validate password and email if is not this input means interactive. $this->validator->validatePassword($plainPassword); $this->validator->validateEmail($email); $this->validator->validateDisplayName($displayName); // check if a user with the same email already exists. $existingEmail = $this->users->findOneBy(['email' => $email]); if ($existingEmail !== null) { throw new RuntimeException(sprintf('There is already a user registered with the "%s" email.', $email)); } } /** * The command help is usually included in the configure() method, but when * it's too long, it's better to define a separate method to maintain the * code readability. */ private function getCommandHelp(): string { return <<<'HELP' The %command.name% command creates new users and saves them in the database: php %command.full_name% username password email By default the command creates regular users. To create administrator users, add the --admin option: php %command.full_name% username password email --admin If you omit any of the three required arguments, the command will ask you to provide the missing values: # command will ask you for the email php %command.full_name% username password # command will ask you for the email and password php %command.full_name% username # command will ask you for all arguments php %command.full_name% HELP; } }