Skip to content

Commit 00a39e7

Browse files
committed
feature #60389 [Console] Add support for SignalableCommandInterface with invokable commands (HypeMC)
This PR was merged into the 7.3 branch. Discussion ---------- [Console] Add support for `SignalableCommandInterface` with invokable commands | Q | A | ------------- | --- | Branch? | 7.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT Currently, it's not possible to use the `SignalableCommandInterface` with invokable commands. This PR adds support for that. ```php #[AsCommand(name: 'app:example')] class ExampleCommand implements SignalableCommandInterface { public function __invoke(InputInterface $input, OutputInterface $output): int { // ... return Command::SUCCESS; } public function getSubscribedSignals(): array { return [\SIGINT, \SIGTERM]; } public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false { // handle signal return 0; } } ``` Commits ------- bb97a2a [Console] Add support for `SignalableCommandInterface` with invokable commands
2 parents fc40d62 + bb97a2a commit 00a39e7

File tree

10 files changed

+141
-21
lines changed

10 files changed

+141
-21
lines changed

src/Symfony/Component/Console/Application.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
use Symfony\Component\Console\Command\HelpCommand;
1818
use Symfony\Component\Console\Command\LazyCommand;
1919
use Symfony\Component\Console\Command\ListCommand;
20-
use Symfony\Component\Console\Command\SignalableCommandInterface;
2120
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
2221
use Symfony\Component\Console\Completion\CompletionInput;
2322
use Symfony\Component\Console\Completion\CompletionSuggestions;
@@ -1005,8 +1004,7 @@ protected function doRunCommand(Command $command, InputInterface $input, OutputI
10051004
}
10061005
}
10071006

1008-
$commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : [];
1009-
if ($commandSignals || $this->dispatcher && $this->signalsToDispatchEvent) {
1007+
if (($commandSignals = $command->getSubscribedSignals()) || $this->dispatcher && $this->signalsToDispatchEvent) {
10101008
$signalRegistry = $this->getSignalRegistry();
10111009

10121010
if (Terminal::hasSttyAvailable()) {

src/Symfony/Component/Console/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ CHANGELOG
1414
* Add support for `LockableTrait` in invokable commands
1515
* Deprecate returning a non-integer value from a `\Closure` function set via `Command::setCode()`
1616
* Mark `#[AsCommand]` attribute as `@final`
17+
* Add support for `SignalableCommandInterface` with invokable commands
1718

1819
7.2
1920
---

src/Symfony/Component/Console/Command/Command.php

+11-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
*
3333
* @author Fabien Potencier <fabien@symfony.com>
3434
*/
35-
class Command
35+
class Command implements SignalableCommandInterface
3636
{
3737
// see https://tldp.org/LDP/abs/html/exitcodes.html
3838
public const SUCCESS = 0;
@@ -674,6 +674,16 @@ public function getHelper(string $name): HelperInterface
674674
return $this->helperSet->get($name);
675675
}
676676

677+
public function getSubscribedSignals(): array
678+
{
679+
return $this->code?->getSubscribedSignals() ?? [];
680+
}
681+
682+
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
683+
{
684+
return $this->code?->handleSignal($signal, $previousExitCode) ?? false;
685+
}
686+
677687
/**
678688
* Validates a command name.
679689
*

src/Symfony/Component/Console/Command/InvokableCommand.php

+13-1
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828
*
2929
* @internal
3030
*/
31-
class InvokableCommand
31+
class InvokableCommand implements SignalableCommandInterface
3232
{
3333
private readonly \Closure $code;
34+
private readonly ?SignalableCommandInterface $signalableCommand;
3435
private readonly \ReflectionFunction $reflection;
3536
private bool $triggerDeprecations = false;
3637

@@ -39,6 +40,7 @@ public function __construct(
3940
callable $code,
4041
) {
4142
$this->code = $this->getClosure($code);
43+
$this->signalableCommand = $code instanceof SignalableCommandInterface ? $code : null;
4244
$this->reflection = new \ReflectionFunction($this->code);
4345
}
4446

@@ -142,4 +144,14 @@ private function getParameters(InputInterface $input, OutputInterface $output):
142144

143145
return $parameters ?: [$input, $output];
144146
}
147+
148+
public function getSubscribedSignals(): array
149+
{
150+
return $this->signalableCommand?->getSubscribedSignals() ?? [];
151+
}
152+
153+
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
154+
{
155+
return $this->signalableCommand?->handleSignal($signal, $previousExitCode) ?? false;
156+
}
145157
}

src/Symfony/Component/Console/Command/TraceableCommand.php

+2-6
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
*
2828
* @author Jules Pietri <jules@heahprod.com>
2929
*/
30-
final class TraceableCommand extends Command implements SignalableCommandInterface
30+
final class TraceableCommand extends Command
3131
{
3232
public readonly Command $command;
3333
public int $exitCode;
@@ -89,15 +89,11 @@ public function __call(string $name, array $arguments): mixed
8989

9090
public function getSubscribedSignals(): array
9191
{
92-
return $this->command instanceof SignalableCommandInterface ? $this->command->getSubscribedSignals() : [];
92+
return $this->command->getSubscribedSignals();
9393
}
9494

9595
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
9696
{
97-
if (!$this->command instanceof SignalableCommandInterface) {
98-
return false;
99-
}
100-
10197
$event = $this->stopwatch->start($this->getName().'.handle_signal');
10298

10399
$exit = $this->command->handleSignal($signal, $previousExitCode);

src/Symfony/Component/Console/Tests/ApplicationTest.php

+70-4
Original file line numberDiff line numberDiff line change
@@ -2255,6 +2255,41 @@ public function testSignalableRestoresStty()
22552255
$this->assertSame($previousSttyMode, $sttyMode);
22562256
}
22572257

2258+
/**
2259+
* @requires extension pcntl
2260+
*/
2261+
public function testSignalableInvokableCommand()
2262+
{
2263+
$command = new Command();
2264+
$command->setName('signal-invokable');
2265+
$command->setCode($invokable = new class implements SignalableCommandInterface {
2266+
use SignalableInvokableCommandTrait;
2267+
});
2268+
2269+
$application = $this->createSignalableApplication($command, null);
2270+
$application->setSignalsToDispatchEvent(\SIGUSR1);
2271+
2272+
$this->assertSame(1, $application->run(new ArrayInput(['signal-invokable'])));
2273+
$this->assertTrue($invokable->signaled);
2274+
}
2275+
2276+
/**
2277+
* @requires extension pcntl
2278+
*/
2279+
public function testSignalableInvokableCommandThatExtendsBaseCommand()
2280+
{
2281+
$command = new class extends Command implements SignalableCommandInterface {
2282+
use SignalableInvokableCommandTrait;
2283+
};
2284+
$command->setName('signal-invokable');
2285+
2286+
$application = $this->createSignalableApplication($command, null);
2287+
$application->setSignalsToDispatchEvent(\SIGUSR1);
2288+
2289+
$this->assertSame(1, $application->run(new ArrayInput(['signal-invokable'])));
2290+
$this->assertTrue($command->signaled);
2291+
}
2292+
22582293
/**
22592294
* @requires extension pcntl
22602295
*/
@@ -2514,7 +2549,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
25142549
}
25152550

25162551
#[AsCommand(name: 'signal')]
2517-
class SignableCommand extends BaseSignableCommand implements SignalableCommandInterface
2552+
class SignableCommand extends BaseSignableCommand
25182553
{
25192554
public function getSubscribedSignals(): array
25202555
{
@@ -2531,7 +2566,7 @@ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|
25312566
}
25322567

25332568
#[AsCommand(name: 'signal')]
2534-
class TerminatableCommand extends BaseSignableCommand implements SignalableCommandInterface
2569+
class TerminatableCommand extends BaseSignableCommand
25352570
{
25362571
public function getSubscribedSignals(): array
25372572
{
@@ -2548,7 +2583,7 @@ public function handleSignal(int $signal, int|false $previousExitCode = 0): int|
25482583
}
25492584

25502585
#[AsCommand(name: 'signal')]
2551-
class TerminatableWithEventCommand extends Command implements SignalableCommandInterface, EventSubscriberInterface
2586+
class TerminatableWithEventCommand extends Command implements EventSubscriberInterface
25522587
{
25532588
private bool $shouldContinue = true;
25542589
private OutputInterface $output;
@@ -2615,8 +2650,39 @@ public static function getSubscribedEvents(): array
26152650
}
26162651
}
26172652

2653+
trait SignalableInvokableCommandTrait
2654+
{
2655+
public bool $signaled = false;
2656+
2657+
public function __invoke(): int
2658+
{
2659+
posix_kill(posix_getpid(), \SIGUSR1);
2660+
2661+
for ($i = 0; $i < 1000; ++$i) {
2662+
usleep(100);
2663+
if ($this->signaled) {
2664+
return 1;
2665+
}
2666+
}
2667+
2668+
return 0;
2669+
}
2670+
2671+
public function getSubscribedSignals(): array
2672+
{
2673+
return SignalRegistry::isSupported() ? [\SIGUSR1] : [];
2674+
}
2675+
2676+
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
2677+
{
2678+
$this->signaled = true;
2679+
2680+
return false;
2681+
}
2682+
}
2683+
26182684
#[AsCommand(name: 'alarm')]
2619-
class AlarmableCommand extends BaseSignableCommand implements SignalableCommandInterface
2685+
class AlarmableCommand extends BaseSignableCommand
26202686
{
26212687
public function __construct(private int $alarmInterval)
26222688
{

src/Symfony/Component/Console/Tests/DependencyInjection/AddConsoleCommandPassTest.php

+40
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Component\Console\Attribute\AsCommand;
1616
use Symfony\Component\Console\Command\Command;
1717
use Symfony\Component\Console\Command\LazyCommand;
18+
use Symfony\Component\Console\Command\SignalableCommandInterface;
1819
use Symfony\Component\Console\CommandLoader\ContainerCommandLoader;
1920
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
2021
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
@@ -325,6 +326,27 @@ public function testProcessInvokableCommand()
325326
self::assertSame('The command description', $command->getDescription());
326327
self::assertSame('The %command.name% command help content.', $command->getHelp());
327328
}
329+
330+
public function testProcessInvokableSignalableCommand()
331+
{
332+
$container = new ContainerBuilder();
333+
$container->addCompilerPass(new AddConsoleCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
334+
335+
$definition = new Definition(InvokableSignalableCommand::class);
336+
$definition->addTag('console.command', [
337+
'command' => 'invokable-signalable',
338+
'description' => 'The command description',
339+
'help' => 'The %command.name% command help content.',
340+
]);
341+
$container->setDefinition('invokable_signalable_command', $definition);
342+
343+
$container->compile();
344+
$command = $container->get('console.command_loader')->get('invokable-signalable');
345+
346+
self::assertTrue($container->has('invokable_signalable_command.command'));
347+
self::assertSame('The command description', $command->getDescription());
348+
self::assertSame('The %command.name% command help content.', $command->getHelp());
349+
}
328350
}
329351

330352
class MyCommand extends Command
@@ -361,3 +383,21 @@ public function __invoke(): void
361383
{
362384
}
363385
}
386+
387+
#[AsCommand(name: 'invokable-signalable', description: 'Just testing', help: 'The %command.name% help content.')]
388+
class InvokableSignalableCommand implements SignalableCommandInterface
389+
{
390+
public function __invoke(): void
391+
{
392+
}
393+
394+
public function getSubscribedSignals(): array
395+
{
396+
return [];
397+
}
398+
399+
public function handleSignal(int $signal, false|int $previousExitCode = 0): int|false
400+
{
401+
return false;
402+
}
403+
}

src/Symfony/Component/Console/Tests/Fixtures/application_signalable.php

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
<?php
22

3-
use Symfony\Component\Console\Command\SignalableCommandInterface;
43
use Symfony\Component\Console\Input\InputInterface;
54
use Symfony\Component\Console\Output\OutputInterface;
65
use Symfony\Component\Console\Question\ChoiceQuestion;
@@ -12,7 +11,7 @@
1211
}
1312
require $vendor.'/vendor/autoload.php';
1413

15-
(new class extends SingleCommandApplication implements SignalableCommandInterface {
14+
(new class extends SingleCommandApplication {
1615
public function getSubscribedSignals(): array
1716
{
1817
return [SIGINT];

src/Symfony/Component/Console/Tests/phpt/alarm/command_exit.phpt

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ Test command that exits
77

88
use Symfony\Component\Console\Application;
99
use Symfony\Component\Console\Command\Command;
10-
use Symfony\Component\Console\Command\SignalableCommandInterface;
1110
use Symfony\Component\Console\Helper\QuestionHelper;
1211
use Symfony\Component\Console\Input\InputInterface;
1312
use Symfony\Component\Console\Output\OutputInterface;
@@ -19,7 +18,7 @@ while (!file_exists($vendor.'/vendor')) {
1918
}
2019
require $vendor.'/vendor/autoload.php';
2120

22-
class MyCommand extends Command implements SignalableCommandInterface
21+
class MyCommand extends Command
2322
{
2423
protected function initialize(InputInterface $input, OutputInterface $output): void
2524
{

src/Symfony/Component/Console/Tests/phpt/signal/command_exit.phpt

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ Test command that exits
77

88
use Symfony\Component\Console\Application;
99
use Symfony\Component\Console\Command\Command;
10-
use Symfony\Component\Console\Command\SignalableCommandInterface;
1110
use Symfony\Component\Console\Helper\QuestionHelper;
1211
use Symfony\Component\Console\Input\InputInterface;
1312
use Symfony\Component\Console\Output\OutputInterface;
@@ -19,7 +18,7 @@ while (!file_exists($vendor.'/vendor')) {
1918
}
2019
require $vendor.'/vendor/autoload.php';
2120

22-
class MyCommand extends Command implements SignalableCommandInterface
21+
class MyCommand extends Command
2322
{
2423
protected function execute(InputInterface $input, OutputInterface $output): int
2524
{

0 commit comments

Comments
 (0)