From dd5b0b7370330827a79608401e537ef2ba282915 Mon Sep 17 00:00:00 2001 From: Kevin Bond Date: Sat, 25 Mar 2023 13:53:50 -0400 Subject: [PATCH] [Console][Messenger] add `RunCommandMessage` and `RunCommandMessageHandler` --- .../FrameworkExtension.php | 6 + .../Resources/config/console.php | 15 +++ src/Symfony/Component/Console/CHANGELOG.md | 1 + .../Exception/RunCommandFailedException.php | 29 +++++ .../Console/Messenger/RunCommandContext.php | 23 ++++ .../Console/Messenger/RunCommandMessage.php | 36 ++++++ .../Messenger/RunCommandMessageHandler.php | 48 ++++++++ .../RunCommandMessageHandlerTest.php | 114 ++++++++++++++++++ src/Symfony/Component/Console/composer.json | 1 + 9 files changed, 273 insertions(+) create mode 100644 src/Symfony/Component/Console/Exception/RunCommandFailedException.php create mode 100644 src/Symfony/Component/Console/Messenger/RunCommandContext.php create mode 100644 src/Symfony/Component/Console/Messenger/RunCommandMessage.php create mode 100644 src/Symfony/Component/Console/Messenger/RunCommandMessageHandler.php create mode 100644 src/Symfony/Component/Console/Tests/Messenger/RunCommandMessageHandlerTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 56242bf318de7..f533d4896b77d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -53,6 +53,7 @@ use Symfony\Component\Config\ResourceCheckerInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Messenger\RunCommandMessageHandler; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ChildDefinition; @@ -253,6 +254,11 @@ public function load(array $configs, ContainerBuilder $container) if (!class_exists(DebugCommand::class)) { $container->removeDefinition('console.command.dotenv_debug'); } + + if (!class_exists(RunCommandMessageHandler::class)) { + $container->removeDefinition('console.messenger.application'); + $container->removeDefinition('console.messenger.execute_command_handler'); + } } // Load Cache configuration first as it is used by other components diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php index 2be737e980111..c6eb86c62b3b5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.php @@ -38,8 +38,10 @@ use Symfony\Bundle\FrameworkBundle\Command\TranslationUpdateCommand; use Symfony\Bundle\FrameworkBundle\Command\WorkflowDumpCommand; use Symfony\Bundle\FrameworkBundle\Command\YamlLintCommand; +use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber; use Symfony\Component\Console\EventListener\ErrorListener; +use Symfony\Component\Console\Messenger\RunCommandMessageHandler; use Symfony\Component\Dotenv\Command\DebugCommand as DotenvDebugCommand; use Symfony\Component\Messenger\Command\ConsumeMessagesCommand; use Symfony\Component\Messenger\Command\DebugCommand as MessengerDebugCommand; @@ -364,5 +366,18 @@ service('secrets.local_vault')->ignoreOnInvalid(), ]) ->tag('console.command') + + ->set('console.messenger.application', Application::class) + ->share(false) + ->call('setAutoExit', [false]) + ->args([ + service('kernel'), + ]) + + ->set('console.messenger.execute_command_handler', RunCommandMessageHandler::class) + ->args([ + service('console.messenger.application'), + ]) + ->tag('messenger.message_handler') ; }; diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index 7132a052c8ec0..5af9c7cf7e3b8 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -7,6 +7,7 @@ CHANGELOG * Add `SignalMap` to map signal value to its name * Multi-line text in vertical tables is aligned properly * The application can also catch errors with `Application::setCatchErrors(true)` + * Add `RunCommandMessage` and `RunCommandMessageHandler` 6.3 --- diff --git a/src/Symfony/Component/Console/Exception/RunCommandFailedException.php b/src/Symfony/Component/Console/Exception/RunCommandFailedException.php new file mode 100644 index 0000000000000..5d87ec949a44a --- /dev/null +++ b/src/Symfony/Component/Console/Exception/RunCommandFailedException.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Exception; + +use Symfony\Component\Console\Messenger\RunCommandContext; + +/** + * @author Kevin Bond + */ +final class RunCommandFailedException extends RuntimeException +{ + public function __construct(\Throwable|string $exception, public readonly RunCommandContext $context) + { + parent::__construct( + $exception instanceof \Throwable ? $exception->getMessage() : $exception, + $exception instanceof \Throwable ? $exception->getCode() : 0, + $exception instanceof \Throwable ? $exception : null, + ); + } +} diff --git a/src/Symfony/Component/Console/Messenger/RunCommandContext.php b/src/Symfony/Component/Console/Messenger/RunCommandContext.php new file mode 100644 index 0000000000000..35d5cbeba904a --- /dev/null +++ b/src/Symfony/Component/Console/Messenger/RunCommandContext.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Messenger; + +/** + * @author Kevin Bond + */ +final class RunCommandContext extends RunCommandMessage +{ + public function __construct(RunCommandMessage $message, public readonly int $exitCode, public readonly string $output) + { + parent::__construct($message->input, $message->throwOnFailure, $message->catchExceptions); + } +} diff --git a/src/Symfony/Component/Console/Messenger/RunCommandMessage.php b/src/Symfony/Component/Console/Messenger/RunCommandMessage.php new file mode 100644 index 0000000000000..1cae9d1f7032a --- /dev/null +++ b/src/Symfony/Component/Console/Messenger/RunCommandMessage.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Messenger; + +use Symfony\Component\Console\Exception\RunCommandFailedException; + +/** + * @author Kevin Bond + */ +class RunCommandMessage implements \Stringable +{ + /** + * @param bool $throwOnFailure If the command has a non-zero exit code, throw {@see RunCommandFailedException} + * @param bool $catchExceptions @see Application::setCatchExceptions() + */ + public function __construct( + public readonly string $input, + public readonly bool $throwOnFailure = true, + public readonly bool $catchExceptions = false, + ) { + } + + public function __toString(): string + { + return $this->input; + } +} diff --git a/src/Symfony/Component/Console/Messenger/RunCommandMessageHandler.php b/src/Symfony/Component/Console/Messenger/RunCommandMessageHandler.php new file mode 100644 index 0000000000000..14f9c17644bb4 --- /dev/null +++ b/src/Symfony/Component/Console/Messenger/RunCommandMessageHandler.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Messenger; + +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\RunCommandFailedException; +use Symfony\Component\Console\Input\StringInput; +use Symfony\Component\Console\Output\BufferedOutput; + +/** + * @author Kevin Bond + */ +final class RunCommandMessageHandler +{ + public function __construct(private readonly Application $application) + { + } + + public function __invoke(RunCommandMessage $message): RunCommandContext + { + $input = new StringInput($message->input); + $output = new BufferedOutput(); + + $this->application->setCatchExceptions($message->catchExceptions); + + try { + $exitCode = $this->application->run($input, $output); + } catch (\Throwable $e) { + throw new RunCommandFailedException($e, new RunCommandContext($message, Command::FAILURE, $output->fetch())); + } + + if ($message->throwOnFailure && Command::SUCCESS !== $exitCode) { + throw new RunCommandFailedException(sprintf('Command "%s" exited with code "%s".', $message->input, $exitCode), new RunCommandContext($message, $exitCode, $output->fetch())); + } + + return new RunCommandContext($message, $exitCode, $output->fetch()); + } +} diff --git a/src/Symfony/Component/Console/Tests/Messenger/RunCommandMessageHandlerTest.php b/src/Symfony/Component/Console/Tests/Messenger/RunCommandMessageHandlerTest.php new file mode 100644 index 0000000000000..adc31e0ec271c --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Messenger/RunCommandMessageHandlerTest.php @@ -0,0 +1,114 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Messenger; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\RunCommandFailedException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Messenger\RunCommandMessage; +use Symfony\Component\Console\Messenger\RunCommandMessageHandler; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Kevin Bond + */ +final class RunCommandMessageHandlerTest extends TestCase +{ + public function testExecutesCommand() + { + $handler = new RunCommandMessageHandler($this->createApplicationWithCommand()); + $context = $handler(new RunCommandMessage('test:command')); + + $this->assertSame(0, $context->exitCode); + $this->assertStringContainsString('some message', $context->output); + } + + public function testExecutesCommandThatThrowsException() + { + $handler = new RunCommandMessageHandler($this->createApplicationWithCommand()); + + try { + $handler(new RunCommandMessage('test:command --throw')); + } catch (RunCommandFailedException $e) { + $this->assertSame(1, $e->context->exitCode); + $this->assertStringContainsString('some message', $e->context->output); + $this->assertInstanceOf(\RuntimeException::class, $e->getPrevious()); + $this->assertSame('exception message', $e->getMessage()); + + return; + } + + $this->fail('Exception not thrown.'); + } + + public function testExecutesCommandThatCatchesThrownException() + { + $handler = new RunCommandMessageHandler($this->createApplicationWithCommand()); + $context = $handler(new RunCommandMessage('test:command --throw -v', throwOnFailure: false, catchExceptions: true)); + + $this->assertSame(1, $context->exitCode); + $this->assertStringContainsString('[RuntimeException]', $context->output); + $this->assertStringContainsString('exception message', $context->output); + } + + public function testThrowOnNonSuccess() + { + $handler = new RunCommandMessageHandler($this->createApplicationWithCommand()); + + try { + $handler(new RunCommandMessage('test:command --exit=1')); + } catch (RunCommandFailedException $e) { + $this->assertSame(1, $e->context->exitCode); + $this->assertStringContainsString('some message', $e->context->output); + $this->assertSame('Command "test:command --exit=1" exited with code "1".', $e->getMessage()); + $this->assertNull($e->getPrevious()); + + return; + } + + $this->fail('Exception not thrown.'); + } + + private function createApplicationWithCommand(): Application + { + $application = new Application(); + $application->setAutoExit(false); + $application->addCommands([ + new class() extends Command { + public function configure(): void + { + $this + ->setName('test:command') + ->addOption('throw') + ->addOption('exit', null, InputOption::VALUE_REQUIRED, 0) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->write('some message'); + + if ($input->getOption('throw')) { + throw new \RuntimeException('exception message'); + } + + return (int) $input->getOption('exit'); + } + }, + ]); + + return $application; + } +} diff --git a/src/Symfony/Component/Console/composer.json b/src/Symfony/Component/Console/composer.json index 618cbcac94f8e..31d1810442830 100644 --- a/src/Symfony/Component/Console/composer.json +++ b/src/Symfony/Component/Console/composer.json @@ -27,6 +27,7 @@ "symfony/event-dispatcher": "^5.4|^6.0|^7.0", "symfony/dependency-injection": "^5.4|^6.0|^7.0", "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", "symfony/process": "^5.4|^6.0|^7.0", "symfony/var-dumper": "^5.4|^6.0|^7.0", "psr/log": "^1|^2|^3"