diff --git a/src/Command/AddUserCommand.php b/src/Command/AddUserCommand.php index 129ee72a0..4e5f5be9e 100644 --- a/src/Command/AddUserCommand.php +++ b/src/Command/AddUserCommand.php @@ -17,11 +17,10 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Interact; use Symfony\Component\Console\Attribute\Option; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Stopwatch\Stopwatch; @@ -55,8 +54,6 @@ )] final class AddUserCommand extends Command { - private SymfonyStyle $io; - public function __construct( private readonly EntityManagerInterface $entityManager, private readonly UserPasswordHasherInterface $passwordHasher, @@ -66,32 +63,11 @@ public function __construct( parent::__construct(); } - /** - * This optional method is the first one executed for a command 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 __invoke(). Its purpose - * is to check if some 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 + #[Interact] + public function prompt(SymfonyStyle $io): void { /** @var string|null $username */ - $username = $input->getArgument('username'); + $username = $io->getArgument('username'); /** @var string|null $password */ $password = $input->getArgument('password'); /** @var string|null $email */ @@ -103,8 +79,8 @@ protected function interact(InputInterface $input, OutputInterface $output): voi return; } - $this->io->title('Add User Command Interactive Wizard'); - $this->io->text([ + $io->title('Add User Command Interactive Wizard'); + $io->text([ 'If you prefer to not use this interactive wizard, provide the', 'arguments required by this command as follows:', '', @@ -115,7 +91,7 @@ protected function interact(InputInterface $input, OutputInterface $output): voi // Ask for the username if it's not defined if (null !== $username) { - $this->io->text(' > Username: '.$username); + $io->text(' > Username: '.$username); } else { $username = $this->io->ask('Username', null, $this->validator->validateUsername(...)); $input->setArgument('username', $username); @@ -123,7 +99,7 @@ protected function interact(InputInterface $input, OutputInterface $output): voi // Ask for the password if it's not defined if (null !== $password) { - $this->io->text(' > Password: '.u('*')->repeat(u($password)->length())); + $io->text(' > Password: '.u('*')->repeat(u($password)->length())); } else { $password = $this->io->askHidden('Password (your type will be hidden)', $this->validator->validatePassword(...)); $input->setArgument('password', $password); @@ -131,17 +107,17 @@ protected function interact(InputInterface $input, OutputInterface $output): voi // Ask for the email if it's not defined if (null !== $email) { - $this->io->text(' > Email: '.$email); + $io->text(' > Email: '.$email); } else { - $email = $this->io->ask('Email', null, $this->validator->validateEmail(...)); + $email = $io->ask('Email', null, $this->validator->validateEmail(...)); $input->setArgument('email', $email); } // Ask for the full name if it's not defined if (null !== $fullName) { - $this->io->text(' > Full Name: '.$fullName); + $io->text(' > Full Name: '.$fullName); } else { - $fullName = $this->io->ask('Full Name', null, $this->validator->validateFullName(...)); + $fullName = $io->ask('Full Name', null, $this->validator->validateFullName(...)); $input->setArgument('full-name', $fullName); } } @@ -155,6 +131,7 @@ protected function interact(InputInterface $input, OutputInterface $output): voi * @see https://symfony.com/doc/current/console/input.html */ public function __invoke( + SymfonyStyle $io, #[Argument('The username of the new user')] string $username, #[Argument('The plain password of the new user', 'password')] string $plainPassword, #[Argument('The email of the new user')] string $email, diff --git a/src/Command/DeleteUserCommand.php b/src/Command/DeleteUserCommand.php index 39d9d7c72..1ab41269b 100644 --- a/src/Command/DeleteUserCommand.php +++ b/src/Command/DeleteUserCommand.php @@ -18,10 +18,9 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Console\Attribute\Argument; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Ask; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; /** @@ -55,8 +54,6 @@ )] final class DeleteUserCommand extends Command { - private SymfonyStyle $io; - public function __construct( private readonly EntityManagerInterface $entityManager, private readonly Validator $validator, @@ -66,40 +63,12 @@ public function __construct( parent::__construct(); } - 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); - } - - protected function interact(InputInterface $input, OutputInterface $output): void - { - /** @var string|null $username */ - $username = $input->getArgument('username'); - - if (null !== $username) { - return; - } - - $this->io->title('Delete User Command Interactive Wizard'); - $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:delete-user username', - '', - 'Now we\'ll ask you for the value of all the missing command arguments.', - '', - ]); - - $username = $this->io->ask('Username', null, $this->validator->validateUsername(...)); - $input->setArgument('username', $username); - } - - public function __invoke(#[Argument('The username of an existing user')] string $username): int - { + public function __invoke( + SymfonyStyle $io, + #[Argument('The username of an existing user')] + #[Ask('Username', maxAttempts: 3)] + string $username, + ): int { $username = $this->validator->validateUsername($username); /** @var User|null $user */ @@ -120,7 +89,7 @@ public function __invoke(#[Argument('The username of an existing user')] string $userUsername = $user->getUsername(); $userEmail = $user->getEmail(); - $this->io->success(\sprintf('User "%s" (ID: %d, email: %s) was successfully deleted.', $userUsername, $userId, $userEmail)); + $io->success(\sprintf('User "%s" (ID: %d, email: %s) was successfully deleted.', $userUsername, $userId, $userEmail)); // Logging is helpful and important to keep a trace of what happened in the software runtime flow. // See https://symfony.com/doc/current/logging.html diff --git a/src/Security/PostVoter.php b/src/Security/PostVoter.php index 5df5c3472..3b033dd75 100644 --- a/src/Security/PostVoter.php +++ b/src/Security/PostVoter.php @@ -50,12 +50,21 @@ protected function voteOnAttribute(string $attribute, $post, TokenInterface $tok // the user must be logged in; if not, deny permission if (!$user instanceof User) { + // votes can include explanations about the decisions. These can be: + // * internal: not shown to the end user, but useful for logging or debugging (you can include technical details) + // * public: (as in this case) meant to be shown to the end user (make sure to not include sensitive information) + $vote?->addReason(\sprintf('There is no user logged in, so it\'s not possible to %s the blog post.', $attribute)); + return false; } // the logic of this voter is pretty simple: if the logged-in user is the // author of the given blog post, grant permission; otherwise, deny it. // (the supports() method guarantees that $post is a Post object) - return $user === $post->getAuthor(); + if ($user === $post->getAuthor()) { + return true; + } + + $vote?->addReason(\sprintf('You can\'t %s this blog post because you are not its author.', $attribute)); } }