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));
}
}