Files
core/src/Command/AddUserCommand.php
2019-07-16 21:07:59 +02:00

262 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace Bolt\Command;
use Bolt\Entity\User;
use Bolt\Repository\UserRepository;
use Bolt\Utils\Str;
use Bolt\Utils\Validator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Symfony\Component\Stopwatch\Stopwatch;
/**
* A console command that creates users and stores them in the database.
*
* To use this command, open a terminal window, enter into your project
* directory and execute the following:
*
* $ php bin/console app:add-user
*
* To output detailed information, increase the command verbosity:
*
* $ php bin/console app:add-user -vv
*
* See https://symfony.com/doc/current/cookbook/console/console_command.html
* For more advanced uses, commands can be defined as services too. See
* https://symfony.com/doc/current/console/commands_as_services.html
*
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
* @author Yonel Ceruto <yonelceruto@gmail.com>
*/
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 app:add-user username password email@example.com',
'',
'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(' > <info>Username</info>: ' . $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(' > <info>Password</info>: ' . 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(' > <info>Email</info>: ' . $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(' > <info>Display Name</info>: ' . $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']);
// 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 <info>%command.name%</info> command creates new users and saves them in the database:
<info>php %command.full_name%</info> <comment>username password email</comment>
By default the command creates regular users. To create administrator users,
add the <comment>--admin</comment> option:
<info>php %command.full_name%</info> username password email <comment>--admin</comment>
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
<info>php %command.full_name%</info> <comment>username password</comment>
# command will ask you for the email and password
<info>php %command.full_name%</info> <comment>username</comment>
# command will ask you for all arguments
<info>php %command.full_name%</info>
HELP;
}
}