Skip to content

Commit 8a8c488

Browse files
committed
Promote Invokable Commands
1 parent 30d7fe5 commit 8a8c488

File tree

9 files changed

+91
-135
lines changed

9 files changed

+91
-135
lines changed

src/Command/AddUserCommand.php

Lines changed: 42 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,12 @@
1515
use App\Repository\UserRepository;
1616
use App\Utils\Validator;
1717
use Doctrine\ORM\EntityManagerInterface;
18+
use Symfony\Component\Console\Attribute\Argument;
1819
use Symfony\Component\Console\Attribute\AsCommand;
20+
use Symfony\Component\Console\Attribute\Option;
1921
use Symfony\Component\Console\Command\Command;
2022
use Symfony\Component\Console\Exception\RuntimeException;
21-
use Symfony\Component\Console\Input\InputArgument;
2223
use Symfony\Component\Console\Input\InputInterface;
23-
use Symfony\Component\Console\Input\InputOption;
2424
use Symfony\Component\Console\Output\OutputInterface;
2525
use Symfony\Component\Console\Style\SymfonyStyle;
2626
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
@@ -49,7 +49,8 @@
4949
*/
5050
#[AsCommand(
5151
name: 'app:add-user',
52-
description: 'Creates users and stores them in the database'
52+
description: 'Creates users and stores them in the database',
53+
help: HELP,
5354
)]
5455
final class AddUserCommand extends Command
5556
{
@@ -64,23 +65,9 @@ public function __construct(
6465
parent::__construct();
6566
}
6667

67-
protected function configure(): void
68-
{
69-
$this
70-
->setHelp($this->getCommandHelp())
71-
// commands can optionally define arguments and/or options (mandatory and optional)
72-
// see https://symfony.com/doc/current/components/console/console_arguments.html
73-
->addArgument('username', InputArgument::OPTIONAL, 'The username of the new user')
74-
->addArgument('password', InputArgument::OPTIONAL, 'The plain password of the new user')
75-
->addArgument('email', InputArgument::OPTIONAL, 'The email of the new user')
76-
->addArgument('full-name', InputArgument::OPTIONAL, 'The full name of the new user')
77-
->addOption('admin', null, InputOption::VALUE_NONE, 'If set, the user is created as an administrator')
78-
;
79-
}
80-
8168
/**
82-
* This optional method is the first one executed for a command after configure()
83-
* and is useful to initialize properties based on the input arguments and options.
69+
* This optional method is the first one executed for a command and is useful
70+
* to initialize properties based on the input arguments and options.
8471
*/
8572
protected function initialize(InputInterface $input, OutputInterface $output): void
8673
{
@@ -91,9 +78,9 @@ protected function initialize(InputInterface $input, OutputInterface $output): v
9178
}
9279

9380
/**
94-
* This method is executed after initialize() and before execute(). Its purpose
95-
* is to check if some of the options/arguments are missing and interactively
96-
* ask the user for those values.
81+
* This method is executed after initialize() and before __invoke(). Its purpose
82+
* is to check if some options/arguments are missing and interactively ask the user
83+
* for those values.
9784
*
9885
* This method is completely optional. If you are developing an internal console
9986
* command, you probably should not implement this method because it requires
@@ -161,26 +148,21 @@ protected function interact(InputInterface $input, OutputInterface $output): voi
161148
/**
162149
* This method is executed after interact() and initialize(). It usually
163150
* contains the logic to execute to complete this command task.
151+
*
152+
* Commands can optionally define arguments and/or options (mandatory and optional)
153+
*
154+
* @see https://symfony.com/doc/current/components/console/console_arguments.html
164155
*/
165-
protected function execute(InputInterface $input, OutputInterface $output): int
166-
{
156+
public function __invoke(
157+
#[Argument('The username of the new user')] string $username,
158+
#[Argument('The plain password of the new user', 'password')] string $plainPassword,
159+
#[Argument('The email of the new user')] string $email,
160+
#[Argument('The full name of the new user')] string $fullName,
161+
#[Option('If set, the user is created as an administrator', 'admin')] bool $isAdmin = false,
162+
): int {
167163
$stopwatch = new Stopwatch();
168164
$stopwatch->start('add-user-command');
169165

170-
/** @var string $username */
171-
$username = $input->getArgument('username');
172-
173-
/** @var string $plainPassword */
174-
$plainPassword = $input->getArgument('password');
175-
176-
/** @var string $email */
177-
$email = $input->getArgument('email');
178-
179-
/** @var string $fullName */
180-
$fullName = $input->getArgument('full-name');
181-
182-
$isAdmin = $input->getOption('admin');
183-
184166
// make sure to validate the user data is correct
185167
$this->validateUserData($username, $plainPassword, $email, $fullName);
186168

@@ -202,7 +184,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
202184

203185
$event = $stopwatch->stop('add-user-command');
204186

205-
if ($output->isVerbose()) {
187+
if ($this->io->isVerbose()) {
206188
$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)));
207189
}
208190

@@ -230,35 +212,32 @@ private function validateUserData(string $username, string $plainPassword, strin
230212
throw new RuntimeException(\sprintf('There is already a user registered with the "%s" email.', $email));
231213
}
232214
}
215+
}
233216

234-
/**
235-
* The command help is usually included in the configure() method, but when
236-
* it's too long, it's better to define a separate method to maintain the
237-
* code readability.
238-
*/
239-
private function getCommandHelp(): string
240-
{
241-
return <<<'HELP'
242-
The <info>%command.name%</info> command creates new users and saves them in the database:
217+
/**
218+
* The command help is usually included in the #[AsCommand] attribute, but when
219+
* it's too long, it's better to define a separate constant to maintain the
220+
* code readability.
221+
*/
222+
const HELP = <<<'HELP'
223+
The <info>%command.name%</info> command creates new users and saves them in the database:
243224
244-
<info>php %command.full_name%</info> <comment>username password email</comment>
225+
<info>php %command.full_name%</info> <comment>username password email</comment>
245226
246-
By default the command creates regular users. To create administrator users,
247-
add the <comment>--admin</comment> option:
227+
By default the command creates regular users. To create administrator users,
228+
add the <comment>--admin</comment> option:
248229
249-
<info>php %command.full_name%</info> username password email <comment>--admin</comment>
230+
<info>php %command.full_name%</info> username password email <comment>--admin</comment>
250231
251-
If you omit any of the three required arguments, the command will ask you to
252-
provide the missing values:
232+
If you omit any of the three required arguments, the command will ask you to
233+
provide the missing values:
253234
254-
# command will ask you for the email
255-
<info>php %command.full_name%</info> <comment>username password</comment>
235+
# command will ask you for the email
236+
<info>php %command.full_name%</info> <comment>username password</comment>
256237
257-
# command will ask you for the email and password
258-
<info>php %command.full_name%</info> <comment>username</comment>
238+
# command will ask you for the email and password
239+
<info>php %command.full_name%</info> <comment>username</comment>
259240
260-
# command will ask you for all arguments
261-
<info>php %command.full_name%</info>
262-
HELP;
263-
}
264-
}
241+
# command will ask you for all arguments
242+
<info>php %command.full_name%</info>
243+
HELP;

src/Command/DeleteUserCommand.php

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
use App\Utils\Validator;
1717
use Doctrine\ORM\EntityManagerInterface;
1818
use Psr\Log\LoggerInterface;
19+
use Symfony\Component\Console\Attribute\Argument;
1920
use Symfony\Component\Console\Attribute\AsCommand;
2021
use Symfony\Component\Console\Command\Command;
2122
use Symfony\Component\Console\Exception\RuntimeException;
22-
use Symfony\Component\Console\Input\InputArgument;
2323
use Symfony\Component\Console\Input\InputInterface;
2424
use Symfony\Component\Console\Output\OutputInterface;
2525
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -41,7 +41,17 @@
4141
*/
4242
#[AsCommand(
4343
name: 'app:delete-user',
44-
description: 'Deletes users from the database'
44+
description: 'Deletes users from the database',
45+
help: <<<'HELP'
46+
The <info>%command.name%</info> command deletes users from the database:
47+
48+
<info>php %command.full_name%</info> <comment>username</comment>
49+
50+
If you omit the argument, the command will ask you to
51+
provide the missing value:
52+
53+
<info>php %command.full_name%</info>
54+
HELP,
4555
)]
4656
final class DeleteUserCommand extends Command
4757
{
@@ -56,23 +66,6 @@ public function __construct(
5666
parent::__construct();
5767
}
5868

59-
protected function configure(): void
60-
{
61-
$this
62-
->addArgument('username', InputArgument::REQUIRED, 'The username of an existing user')
63-
->setHelp(<<<'HELP'
64-
The <info>%command.name%</info> command deletes users from the database:
65-
66-
<info>php %command.full_name%</info> <comment>username</comment>
67-
68-
If you omit the argument, the command will ask you to
69-
provide the missing value:
70-
71-
<info>php %command.full_name%</info>
72-
HELP
73-
);
74-
}
75-
7669
protected function initialize(InputInterface $input, OutputInterface $output): void
7770
{
7871
// SymfonyStyle is an optional feature that Symfony provides so you can
@@ -105,10 +98,8 @@ protected function interact(InputInterface $input, OutputInterface $output): voi
10598
$input->setArgument('username', $username);
10699
}
107100

108-
protected function execute(InputInterface $input, OutputInterface $output): int
101+
public function __invoke(#[Argument('The username of an existing user')] string $username): int
109102
{
110-
/** @var string|null $username */
111-
$username = $input->getArgument('username');
112103
$username = $this->validator->validateUsername($username);
113104

114105
/** @var User|null $user */

src/Command/ListUsersCommand.php

Lines changed: 28 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@
1414
use App\Entity\User;
1515
use App\Repository\UserRepository;
1616
use Symfony\Component\Console\Attribute\AsCommand;
17+
use Symfony\Component\Console\Attribute\Option;
1718
use Symfony\Component\Console\Command\Command;
1819
use Symfony\Component\Console\Input\InputInterface;
19-
use Symfony\Component\Console\Input\InputOption;
2020
use Symfony\Component\Console\Output\BufferedOutput;
2121
use Symfony\Component\Console\Output\OutputInterface;
2222
use Symfony\Component\Console\Style\SymfonyStyle;
@@ -42,54 +42,47 @@
4242
#[AsCommand(
4343
name: 'app:list-users',
4444
description: 'Lists all the existing users',
45-
aliases: ['app:users']
45+
aliases: ['app:users'],
46+
help: <<<'HELP'
47+
The <info>%command.name%</info> command lists all the users registered in the application:
48+
49+
<info>php %command.full_name%</info>
50+
51+
By default the command only displays the 50 most recent users. Set the number of
52+
results to display with the <comment>--max-results</comment> option:
53+
54+
<info>php %command.full_name%</info> <comment>--max-results=2000</comment>
55+
56+
In addition to displaying the user list, you can also send this information to
57+
the email address specified in the <comment>--send-to</comment> option:
58+
59+
<info>php %command.full_name%</info> <comment>[email protected]</comment>
60+
HELP,
4661
)]
47-
final class ListUsersCommand extends Command
62+
final class ListUsersCommand
4863
{
4964
public function __construct(
5065
private readonly MailerInterface $mailer,
5166
#[Autowire('%app.notifications.email_sender%')]
5267
private readonly string $emailSender,
5368
private readonly UserRepository $users,
5469
) {
55-
parent::__construct();
56-
}
57-
58-
protected function configure(): void
59-
{
60-
$this
61-
->setHelp(<<<'HELP'
62-
The <info>%command.name%</info> command lists all the users registered in the application:
63-
64-
<info>php %command.full_name%</info>
65-
66-
By default the command only displays the 50 most recent users. Set the number of
67-
results to display with the <comment>--max-results</comment> option:
68-
69-
<info>php %command.full_name%</info> <comment>--max-results=2000</comment>
70-
71-
In addition to displaying the user list, you can also send this information to
72-
the email address specified in the <comment>--send-to</comment> option:
73-
74-
<info>php %command.full_name%</info> <comment>[email protected]</comment>
75-
HELP
76-
)
77-
// commands can optionally define arguments and/or options (mandatory and optional)
78-
// see https://symfony.com/doc/current/components/console/console_arguments.html
79-
->addOption('max-results', null, InputOption::VALUE_OPTIONAL, 'Limits the number of users listed', 50)
80-
->addOption('send-to', null, InputOption::VALUE_OPTIONAL, 'If set, the result is sent to the given email address')
81-
;
8270
}
8371

8472
/**
8573
* This method is executed after initialize(). It usually contains the logic
8674
* to execute to complete this command task.
75+
*
76+
* Commands can optionally define arguments and/or options (mandatory and optional)
77+
*
78+
* @see https://symfony.com/doc/current/components/console/console_arguments.html
8779
*/
88-
protected function execute(InputInterface $input, OutputInterface $output): int
89-
{
90-
/** @var int|null $maxResults */
91-
$maxResults = $input->getOption('max-results');
92-
80+
public function __invoke(
81+
InputInterface $input,
82+
OutputInterface $output,
83+
#[Option('If set, the result is sent to the given email address', 'send-to')] ?string $email = null,
84+
#[Option('Limits the number of users listed')] int $maxResults = 50,
85+
): int {
9386
// Use ->findBy() instead of ->findAll() to allow result sorting and limiting
9487
$allUsers = $this->users->findBy([], ['id' => 'DESC'], $maxResults);
9588

@@ -122,9 +115,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int
122115
$usersAsATable = $bufferedOutput->fetch();
123116
$output->write($usersAsATable);
124117

125-
/** @var string|null $email */
126-
$email = $input->getOption('send-to');
127-
128118
if (null !== $email) {
129119
$this->sendReport($usersAsATable, $email);
130120
}

src/Controller/.gitignore

Whitespace-only changes.

src/Entity/.gitignore

Whitespace-only changes.

src/Repository/.gitignore

Whitespace-only changes.

tests/Command/AbstractCommandTestCase.php

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
use Symfony\Bundle\FrameworkBundle\Console\Application;
1515
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
16-
use Symfony\Component\Console\Command\Command;
1716
use Symfony\Component\Console\Tester\CommandTester;
1817

1918
abstract class AbstractCommandTestCase extends KernelTestCase
@@ -29,11 +28,10 @@ abstract class AbstractCommandTestCase extends KernelTestCase
2928
protected function executeCommand(array $arguments, array $inputs = []): CommandTester
3029
{
3130
$kernel = self::bootKernel();
31+
$application = new Application($kernel);
3232

33-
// this uses a special testing container that allows you to fetch private services
34-
/** @var Command $command */
35-
$command = static::getContainer()->get($this->getCommandFqcn());
36-
$command->setApplication(new Application($kernel));
33+
$command = $application->find($this->getCommandName());
34+
$command->setApplication($application);
3735

3836
$commandTester = new CommandTester($command);
3937
$commandTester->setInputs($inputs);
@@ -42,5 +40,5 @@ protected function executeCommand(array $arguments, array $inputs = []): Command
4240
return $commandTester;
4341
}
4442

45-
abstract protected function getCommandFqcn(): string;
43+
abstract protected function getCommandName(): string;
4644
}

tests/Command/AddUserCommandTest.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace App\Tests\Command;
1313

14-
use App\Command\AddUserCommand;
1514
use App\Repository\UserRepository;
1615
use PHPUnit\Framework\Attributes\DataProvider;
1716
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
@@ -103,8 +102,8 @@ private function assertUserCreated(bool $isAdmin): void
103102
$this->assertSame($isAdmin ? ['ROLE_ADMIN'] : ['ROLE_USER'], $user->getRoles());
104103
}
105104

106-
protected function getCommandFqcn(): string
105+
protected function getCommandName(): string
107106
{
108-
return AddUserCommand::class;
107+
return 'app:add-user';
109108
}
110109
}

0 commit comments

Comments
 (0)