8000 feature #49814 [Console][Messenger] add `RunCommandMessage` and `RunC… · symfony/symfony@fe30946 · GitHub
[go: up one dir, main page]

Skip to content

Commit fe30946

Browse files
committed
feature #49814 [Console][Messenger] add RunCommandMessage and RunCommandMessageHandler (kbond)
This PR was squashed before being merged into the 6.4 branch. Discussion ---------- [Console][Messenger] add `RunCommandMessage` and `RunCommandMessageHandler` | Q | A | ------------- | --- | Branch? | 6.3 | Bug fix? | no | New feature? | yes | Deprecations? | no | Tickets | n/a | License | MIT | Doc PR | todo Similar to #49813, when using the scheduler it could be useful to execute commands. ## Usage ```php use Symfony\Component\Console\Exception\RunCommandFailedException; use Symfony\Component\Console\Messenger\RunCommandMessage; try { $context = $bus->dispatch(new RunCommandMessage('my:command')); $context->output; // string - output of command $context->exitCode; // int - the exit code catch(RunCommandFailedException $e) { // if exit code is non-zero or command threw exception $e->context->output; // string - output of command $e->context->exitCode; // int - the exit code $e->getPrevious(); // null|\Throwable exception command threw if applicable } // "never" fail $context = $bus->dispatch(new RunCommandMessage('my:command', throwOnNonSuccess: false, catchExceptions: true)); ``` TODO: - [x] wire up - [x] tests Commits ------- dd5b0b7 [Console][Messenger] add `RunCommandMessage` and `RunCommandMessageHandler`
2 parents 3b20051 + dd5b0b7 commit fe30946

File tree

9 files changed

+273
-0
lines changed

9 files changed

+273
-0
lines changed

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

+6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
use Symfony\Component\Config\ResourceCheckerInterface;
5454
use Symfony\Component\Console\Application;
5555
use Symfony\Component\Console\Command\Command;
56+
use Symfony\Component\Console\Messenger\RunCommandMessageHandler;
5657
use Symfony\Component\DependencyInjection\Alias;
5758
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
5859
use Symfony\Component\DependencyInjection\ChildDefinition;
@@ -261,6 +262,11 @@ public function load(array $configs, ContainerBuilder $container)
261262
if (!class_exists(DebugCommand::class)) {
262263
$container->removeDefinition('console.command.dotenv_debug');
263264
}
265+
266+
if (!class_exists(RunCommandMessageHandler::class)) {
267+
$container->removeDefinition('console.messenger.application');
268+
$container->removeDefinition('console.messenger.execute_command_handler');
269+
}
264270
}
265271

266272
// Load Cache configuration first as it is used by other components

src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php

+15
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@
3838
use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand;
3939
use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand;
4040
use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand;
41+
use Symfony\Bundle\FrameworkBundle\Console\Application;
4142
use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber;
4243
use Symfony\Component\Console\EventListener\ErrorListener;
44+
use Symfony\Component\Console\Messenger\RunCommandMessageHandler;
4345
use Symfony\Component\Dotenv\Command\DebugCommand as DotenvDebugCommand;
4446
use Symfony\Component\Messenger\Command\ConsumeMessagesCommand;
4547
use Symfony\Component\Messenger\Command\DebugCommand as MessengerDebugCommand;
@@ -364,5 +366,18 @@
364366
service('secrets.local_vault')->ignoreOnInvalid(),
365367
])
366368
->tag('console.command')
369+
370+
->set('console.messenger.application', Application::class)
371+
->share(false)
372+
->call('setAutoExit', [false])
373+
->args([
374+
service('kernel'),
375+
])
376+
377+
->set('console.messenger.execute_command_handler', RunCommandMessageHandler::class)
378+
->args([
379+
service('console.messenger.application'),
380+
])
381+
->tag('messenger.message_handler')
367382
;
368383
};

src/Symfony/Component/Console/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add `SignalMap` to map signal value to its name
88
* Multi-line text in vertical tables is aligned properly
99
* The application can also catch errors with `Application::setCatchErrors(true)`
10+
* Add `RunCommandMessage` and `RunCommandMessageHandler`
1011

1112
6.3
1213
---
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Exception;
13+
14+
use Symfony\Component\Console\Messenger\RunCommandContext;
15+
16+
/**
17+
* @author Kevin Bond <kevinbond@gmail.com>
18+
*/
19+
final class RunCommandFailedException extends RuntimeException
20+
{
21+
public function __construct(\Throwable|string $exception, public readonly RunCommandContext $context)
22+
{
23+
parent::__construct(
24+
$exception instanceof \Throwable ? $exception->getMessage() : $exception,
25+
$exception instanceof \Throwable ? $exception->getCode() : 0,
26+
$exception instanceof \Throwable ? $exception : null,
27+
);
28+
}
29+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Messenger;
13+
14+
/**
15+
* @author Kevin Bond <kevinbond@gmail.com>
16+
*/
17+
final class RunCommandContext extends RunCommandMessage
18+
{
19+
public function __construct(RunCommandMessage $message, public readonly int $exitCode, public readonly string $output)
20+
{
21+
parent::__construct($message->input, $message->throwOnFailure, $message->catchExceptions);
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Messenger;
13+
14+
use Symfony\Component\Console\Exception\RunCommandFailedException;
15+
16+
/**
17+
* @author Kevin Bond <kevinbond@gmail.com>
18+
*/
19+
class RunCommandMessage implements \Stringable
20+
{
21+
/**
22+
* @param bool $throwOnFailure If the command has a non-zero exit code, throw {@see RunCommandFailedException}
23+
* @param bool $catchExceptions @see Application::setCatchExceptions()
24+
*/
25+
public function __construct(
26+
public readonly string $input,
27+
public readonly bool $throwOnFailure = true,
28+
public readonly bool $catchExceptions = false,
29+
) {
30+
}
31+
32+
public function __toString(): string
33+
{
34+
return $this->input;
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Messenger;
13+
14+
use Symfony\Component\Console\Application;
15+
use Symfony\Component\Console\Command\Command;
16+
use Symfony\Component\Console\Exception\RunCommandFailedException;
17+
use Symfony\Component\Console\Input\StringInput;
18+
use Symfony\Component\Console\Output\BufferedOutput;
19+
20+
/**
21+
* @author Kevin Bond <kevinbond@gmail.com>
22+
*/
23+
final class RunCommandMessageHandler
24+
{
25+
public function __construct(private readonly Application $application)
26+
{
27+
}
28+
29+
public function __invoke(RunCommandMessage $message): RunCommandContext
30+
{
31+
$input = new StringInput($message->input);
32+
$output = new BufferedOutput();
33+
34+
$this->application->setCatchExceptions($message->catchExceptions);
35+
36+
try {
37+
$exitCode = $this->application->run($input, $output);
38+
} catch (\Throwable $e) {
39+
throw new RunCommandFailedException($e, new RunCommandContext($message, Command::FAILURE, $output->fetch()));
40+
}
41+
42+
if ($message->throwOnFailure && Command::SUCCESS !== $exitCode) {
43+
throw new RunCommandFailedException(sprintf('Command "%s" exited with code "%s".', $message->input, $exitCode), new RunCommandContext($message, $exitCode, $output->fetch()));
44+
}
45+
46+
return new RunCommandContext($message, $exitCode, $output->fetch());
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Console\Tests\Messenger;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\Console\Application;
16+
use Symfony\Component\Console\Command\Command;
17+
use Symfony\Component\Console\Exception\RunCommandFailedException;
18+
use Symfony\Component\Console\Input\InputInterface;
19+
use Symfony\Component\Console\Input\InputOption;
20+
use Symfony\Component\Console\Messenger\RunCommandMessage;
21+
use Symfony\Component\Console\Messenger\RunCommandMessageHandler;
22+
use Symfony\Component\Console\Output\OutputInterface;
23+
24+
/**
25+
* @author Kevin Bond <kevinbond@gmail.com>
26+
*/
27+
final class RunCommandMessageHandlerTest extends TestCase
28+
{
29+
public function testExecutesCommand()
30+
{
31+
$handler = new RunCommandMessageHandler($this->createApplicationWithCommand());
32+
$context = $handler(new RunCommandMessage('test:command'));
33+
34+
$this->assertSame(0, $context->exitCode);
35+
$this->assertStringContainsString('some message', $context->output);
36+
}
37+
38+
public function testExecutesCommandThatThrowsException()
39+
{
40+
$handler = new RunCommandMessageHandler($this->createApplicationWithCommand());
41+
42+
try {
43+
$handler(new RunCommandMessage('test:command --throw'));
44+
} catch (RunCommandFailedException $e) {
45+
$this->assertSame(1, $e->context->exitCode);
46+
$this->assertStringContainsString('some message', $e->context->output);
47+
$this->assertInstanceOf(\RuntimeException::class, $e->getPrevious());
48+
$this->assertSame('exception message', $e->getMessage());
49+
50+
return;
51+
}
52+
53+
$this->fail('Exception not thrown.');
54+
}
55+
56+
public function testExecutesCommandThatCatchesThrownException()
57+
{
58+
$handler = new RunCommandMessageHandler($this->createApplicationWithCommand());
59+
$context = $handler(new RunCommandMessage('test:command --throw -v', throwOnFailure: false, catchExceptions: true));
60+
61+
$this->assertSame(1, $context->exitCode);
62+
$this->assertStringContainsString('[RuntimeException]', $context->output);
63+
$this->assertStringContainsString('exception message', $context->output);
64+
}
65+
66+
public function testThrowOnNonSuccess()
67+
{
68+
$handler = new RunCommandMessageHandler($this->createApplicationWithCommand());
69+
70+
try {
71+
$handler(new RunCommandMessage('test:command --exit=1'));
72+
} catch (RunCommandFailedException $e) {
73+
$this->assertSame(1, $e->context->exitCode);
74+
$this->assertStringContainsString('some message', $e->context->output);
75+
$this->assertSame('Command "test:command --exit=1" exited with code "1".', $e->getMessage());
76+
$this->assertNull($e->getPrevious());
77+
78+
return;
79+
}
80+
81+
$this->fail('Exception not thrown.');
82+
}
83+
84+
private function createApplicationWithCommand(): Application
85+
{
86+
$application = new Application();
87+
$application->setAutoExit(false);
88+
$application->addCommands([
89+
new class() extends Command {
90+
public function configure(): void
91+
{
92+
$this
93+
->setName('test:command')
94+
->addOption('throw')
95+
->addOption('exit', null, InputOption::VALUE_REQUIRED, 0)
96+
;
97+
}
98+
99+
protected function execute(InputInterface $input, OutputInterface $output): int
100+
{
101+
$output->write('some message');
102+
103+
if ($input->getOption('throw')) {
104+
throw new \RuntimeException('exception message');
105+
}
106+
107+
return (int) $input->getOption('exit');
108+
}
109+
},
110+
]);
111+
112+
return $application;
113+
}
114+
}

src/Symfony/Component/Console/composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"symfony/event-dispatcher": "^5.4|^6.0|^7.0",
2828
"symfony/dependency-injection": "^5.4|^6.0|^7.0",
2929
"symfony/lock": "^5.4|^6.0|^7.0",
30+
"symfony/messenger": "^5.4|^6.0|^7.0",
3031
"symfony/process": "^5.4|^6.0|^7.0",
3132
"symfony/var-dumper": "^5.4|^6.0|^7.0",
3233
"psr/log": "^1|^2|^3"

0 commit comments

Comments
 (0)
0