diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f13d4d9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.travis.yml export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0492424 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + pull_request: + push: + branches: + - master +jobs: + tests: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.0', '8.1', '8.2'] + + name: PHP ${{ matrix.php }} tests + + steps: + - uses: actions/checkout@v2 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - uses: "ramsey/composer-install@v1" + with: + composer-options: "--prefer-source" + + - run: vendor/bin/phpunit --exclude-group=functional diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b9cf57f..0000000 --- a/.travis.yml +++ /dev/null @@ -1,21 +0,0 @@ -sudo: false - -git: - depth: 10 - -language: php - -php: - - '5.6' - - '7.0' - -cache: - directories: - - $HOME/.composer/cache - -install: - - composer self-update - - composer install --prefer-source - -script: - - vendor/bin/phpunit --exclude-group=functional diff --git a/RunCommandResult.php b/CommandResult.php similarity index 54% rename from RunCommandResult.php rename to CommandResult.php index ce35a03..10080d5 100644 --- a/RunCommandResult.php +++ b/CommandResult.php @@ -2,7 +2,7 @@ namespace Enqueue\AsyncCommand; -final class RunCommandResult implements \JsonSerializable +final class CommandResult implements \JsonSerializable { /** * @var int @@ -19,43 +19,29 @@ final class RunCommandResult implements \JsonSerializable */ private $errorOutput; - /** - * @param int $exitCode - * @param string $output - * @param string $errorOutput - */ - public function __construct($exitCode, $output, $errorOutput) + public function __construct(int $exitCode, string $output, string $errorOutput) { $this->exitCode = $exitCode; $this->output = $output; $this->errorOutput = $errorOutput; } - /** - * @return int - */ public function getExitCode(): int { return $this->exitCode; } - /** - * @return string - */ public function getOutput(): string { return $this->output; } - /** - * @return string - */ public function getErrorOutput(): string { return $this->errorOutput; } - public function jsonSerialize() + public function jsonSerialize(): array { return [ 'exitCode' => $this->exitCode, @@ -64,20 +50,11 @@ public function jsonSerialize() ]; } - /** - * @param string $json - * - * @return self - */ - public static function jsonUnserialize($json) + public static function jsonUnserialize(string $json): self { $data = json_decode($json, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); } return new self($data['exitCode'], $data['output'], $data['errorOutput']); diff --git a/Commands.php b/Commands.php index 0d751ac..abc015c 100644 --- a/Commands.php +++ b/Commands.php @@ -4,5 +4,5 @@ final class Commands { - const RUN_COMMAND = 'run_command'; + public const RUN_COMMAND = 'run_command'; } diff --git a/DependencyInjection/AsyncCommandExtension.php b/DependencyInjection/AsyncCommandExtension.php index 77db687..c1a0fa8 100644 --- a/DependencyInjection/AsyncCommandExtension.php +++ b/DependencyInjection/AsyncCommandExtension.php @@ -2,19 +2,39 @@ namespace Enqueue\AsyncCommand\DependencyInjection; -use Symfony\Component\Config\FileLocator; +use Enqueue\AsyncCommand\Commands; +use Enqueue\AsyncCommand\RunCommandProcessor; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; -use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; class AsyncCommandExtension extends Extension { - /** - * {@inheritdoc} - */ public function load(array $configs, ContainerBuilder $container) { - $loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); - $loader->load('services.yml'); + foreach ($configs['clients'] as $client) { + // BC compatibility + if (!is_array($client)) { + $client = [ + 'name' => $client, + 'command_name' => Commands::RUN_COMMAND, + 'queue_name' => Commands::RUN_COMMAND, + 'timeout' => 60, + ]; + } + + $id = sprintf('enqueue.async_command.%s.run_command_processor', $client['name']); + $container->register($id, RunCommandProcessor::class) + ->addArgument('%kernel.project_dir%') + ->addArgument($client['timeout']) + ->addTag('enqueue.processor', [ + 'client' => $client['name'], + 'command' => $client['command_name'] ?? Commands::RUN_COMMAND, + 'queue' => $client['queue_name'] ?? Commands::RUN_COMMAND, + 'prefix_queue' => false, + 'exclusive' => true, + ]) + ->addTag('enqueue.transport.processor') + ; + } } } diff --git a/README.md b/README.md index 17a9103..711e971 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,33 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + # Symfony Async Command. [![Gitter](https://badges.gitter.im/php-enqueue/Lobby.svg)](https://gitter.im/php-enqueue/Lobby) -[![Build Status](https://travis-ci.org/php-enqueue/async-command.png?branch=master)](https://travis-ci.org/php-enqueue/async-command) +[![Build Status](https://img.shields.io/github/actions/workflow/status/php-enqueue/async-command/ci.yml?branch=master)](https://github.com/php-enqueue/async-command/actions?query=workflow%3ACI) [![Total Downloads](https://poser.pugx.org/enqueue/async-command/d/total.png)](https://packagist.org/packages/enqueue/async-command) [![Latest Stable Version](https://poser.pugx.org/enqueue/async-command/version.png)](https://packagist.org/packages/enqueue/async-command) - -It contains an extension to Symfony's [Console](https://symfony.com/doc/current/components/console.html) component. -It allows to execute Symfony's command async by sending the request to message queue. + +It contains an extension to Symfony's [Console](https://symfony.com/doc/current/components/console.html) component. +It allows to execute Symfony's command async by sending the request to message queue. ## Resources * [Site](https://enqueue.forma-pro.com/) -* [Documentation](https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md) +* [Documentation](https://php-enqueue.github.io/) * [Questions](https://gitter.im/php-enqueue/Lobby) * [Issue Tracker](https://github.com/php-enqueue/enqueue-dev/issues) ## Developed by Forma-Pro -Forma-Pro is a full stack development company which interests also spread to open source development. -Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. +Forma-Pro is a full stack development company which interests also spread to open source development. +Being a team of strong professionals we have an aim an ability to help community by developing cutting edge solutions in the areas of e-commerce, docker & microservice oriented architecture where we have accumulated a huge many-years experience. Our main specialization is Symfony framework based solution, but we are always looking to the technologies that allow us to do our job the best way. We are committed to creating solutions that revolutionize the way how things are developed in aspects of architecture & scalability. If you have any questions and inquires about our open source development, this product particularly or any other matter feel free to contact at opensource@forma-pro.com diff --git a/Resources/config/services.yml b/Resources/config/services.yml deleted file mode 100644 index 97355cb..0000000 --- a/Resources/config/services.yml +++ /dev/null @@ -1,8 +0,0 @@ -services: - enqueue.async_command.run_command_processor: - class: 'Enqueue\AsyncCommand\RunCommandProcessor' - public: public - arguments: - - '%kernel.root_dir%/..' - tags: - - { name: 'enqueue.client.processor' } diff --git a/RunCommand.php b/RunCommand.php index b595916..573a620 100644 --- a/RunCommand.php +++ b/RunCommand.php @@ -20,40 +20,16 @@ final class RunCommand implements \JsonSerializable private $options; /** - * @param string $command * @param string[] $arguments * @param string[] $options */ - public function __construct($command, array $arguments = [], array $options = []) + public function __construct(string $command, array $arguments = [], array $options = []) { $this->command = $command; $this->arguments = $arguments; $this->options = $options; } - /** - * @return string - */ - public function getCommandLine() - { - $optionsString = ''; - foreach ($this->options as $name => $value) { - $optionsString .= " $name=$value"; - } - $optionsString = trim($optionsString); - - $argumentsString = ''; - foreach ($this->arguments as $value) { - $argumentsString .= " $value"; - } - $argumentsString = trim($argumentsString); - - return trim($this->command.' '.$argumentsString.' '.$optionsString); - } - - /** - * @return string - */ public function getCommand(): string { return $this->command; @@ -75,7 +51,7 @@ public function getOptions(): array return $this->options; } - public function jsonSerialize() + public function jsonSerialize(): array { return [ 'command' => $this->command, @@ -84,20 +60,11 @@ public function jsonSerialize() ]; } - /** - * @param string $json - * - * @return self - */ - public static function jsonUnserialize($json) + public static function jsonUnserialize(string $json): self { $data = json_decode($json, true); - if (JSON_ERROR_NONE !== json_last_error()) { - throw new \InvalidArgumentException(sprintf( - 'The malformed json given. Error %s and message %s', - json_last_error(), - json_last_error_msg() - )); + if (\JSON_ERROR_NONE !== json_last_error()) { + throw new \InvalidArgumentException(sprintf('The malformed json given. Error %s and message %s', json_last_error(), json_last_error_msg())); } return new self($data['command'], $data['arguments'], $data['options']); diff --git a/RunCommandProcessor.php b/RunCommandProcessor.php index 0352824..2c4462f 100644 --- a/RunCommandProcessor.php +++ b/RunCommandProcessor.php @@ -2,49 +2,65 @@ namespace Enqueue\AsyncCommand; -use Enqueue\Client\CommandSubscriberInterface; use Enqueue\Consumption\Result; -use Interop\Queue\PsrContext; -use Interop\Queue\PsrMessage; -use Interop\Queue\PsrProcessor; +use Interop\Queue\Context; +use Interop\Queue\Message; +use Interop\Queue\Processor; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; -final class RunCommandProcessor implements PsrProcessor, CommandSubscriberInterface +final class RunCommandProcessor implements Processor { + /** + * @var int + */ + private $timeout; + /** * @var string */ private $projectDir; - public function __construct($projectDir) + public function __construct(string $projectDir, int $timeout = 60) { $this->projectDir = $projectDir; + $this->timeout = $timeout; } - public function process(PsrMessage $message, PsrContext $context) + public function process(Message $message, Context $context): Result { $command = RunCommand::jsonUnserialize($message->getBody()); $phpBin = (new PhpExecutableFinder())->find(); $consoleBin = file_exists($this->projectDir.'/bin/console') ? './bin/console' : './app/console'; - $process = new Process($phpBin.' '.$consoleBin.' '.$command->getCommandLine(), $this->projectDir); - + $process = new Process(array_merge( + [$phpBin, $consoleBin, $command->getCommand()], + $command->getArguments(), + $this->getCommandLineOptions($command) + ), $this->projectDir); + $process->setTimeout($this->timeout); $process->run(); - $result = new RunCommandResult($process->getExitCode(), $process->getOutput(), $process->getErrorOutput()); + if ($message->getReplyTo()) { + $result = new CommandResult($process->getExitCode(), $process->getOutput(), $process->getErrorOutput()); - return Result::reply($context->createMessage(json_encode($result))); + return Result::reply($context->createMessage(json_encode($result))); + } + + return Result::ack(); } - public static function getSubscribedCommand() + /** + * @return string[] + */ + private function getCommandLineOptions(RunCommand $command): array { - return [ - 'processorName' => Commands::RUN_COMMAND, - 'queueName' => Commands::RUN_COMMAND, - 'queueNameHardcoded' => true, - 'exclusive' => true, - ]; + $options = []; + foreach ($command->getOptions() as $name => $value) { + $options[] = "$name=$value"; + } + + return $options; } } diff --git a/Tests/CommandResultTest.php b/Tests/CommandResultTest.php new file mode 100644 index 0000000..03a6155 --- /dev/null +++ b/Tests/CommandResultTest.php @@ -0,0 +1,69 @@ +assertTrue($rc->implementsInterface(\JsonSerializable::class)); + } + + public function testShouldBeFinal() + { + $rc = new \ReflectionClass(CommandResult::class); + + $this->assertTrue($rc->isFinal()); + } + + public function testShouldAllowGetExitCodeSetInConstructor() + { + $result = new CommandResult(123, '', ''); + + $this->assertSame(123, $result->getExitCode()); + } + + public function testShouldAllowGetOutputSetInConstructor() + { + $result = new CommandResult(0, 'theOutput', ''); + + $this->assertSame('theOutput', $result->getOutput()); + } + + public function testShouldAllowGetErrorOutputSetInConstructor() + { + $result = new CommandResult(0, '', 'theErrorOutput'); + + $this->assertSame('theErrorOutput', $result->getErrorOutput()); + } + + public function testShouldSerializeAndUnserialzeCommand() + { + $result = new CommandResult(123, 'theOutput', 'theErrorOutput'); + + $jsonCommand = json_encode($result); + + // guard + $this->assertNotEmpty($jsonCommand); + + $unserializedResult = CommandResult::jsonUnserialize($jsonCommand); + + $this->assertInstanceOf(CommandResult::class, $unserializedResult); + $this->assertSame(123, $unserializedResult->getExitCode()); + $this->assertSame('theOutput', $unserializedResult->getOutput()); + $this->assertSame('theErrorOutput', $unserializedResult->getErrorOutput()); + } + + public function testThrowExceptionIfInvalidJsonGiven() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The malformed json given.'); + + CommandResult::jsonUnserialize('{]'); + } +} diff --git a/Tests/Functional/UseCasesTest.php b/Tests/Functional/UseCasesTest.php new file mode 100644 index 0000000..03eb525 --- /dev/null +++ b/Tests/Functional/UseCasesTest.php @@ -0,0 +1,41 @@ +setReplyTo('aReplyToQueue'); + + $processor = new RunCommandProcessor(__DIR__); + + $result = $processor->process($Message, new NullContext()); + + $this->assertInstanceOf(Result::class, $result); + $this->assertInstanceOf(Message::class, $result->getReply()); + + $replyMessage = $result->getReply(); + + $commandResult = CommandResult::jsonUnserialize($replyMessage->getBody()); + + $this->assertSame(123, $commandResult->getExitCode()); + $this->assertSame('Command Output', $commandResult->getOutput()); + $this->assertSame('Command Error Output', $commandResult->getErrorOutput()); + } +} diff --git a/Tests/Functional/bin/console b/Tests/Functional/bin/console new file mode 100644 index 0000000..40d7e35 --- /dev/null +++ b/Tests/Functional/bin/console @@ -0,0 +1,6 @@ +assertTrue($rc->implementsInterface(Processor::class)); + } + + public function testShouldBeFinal() + { + $rc = new \ReflectionClass(RunCommandProcessor::class); + + $this->assertTrue($rc->isFinal()); + } + + public function testCouldBeConstructedWithProjectDirAsFirstArgument() + { + $processor = new RunCommandProcessor('aProjectDir'); + + $this->assertAttributeSame('aProjectDir', 'projectDir', $processor); + } + + public function testCouldBeConstructedWithTimeoutAsSecondArgument() + { + $processor = new RunCommandProcessor('aProjectDir', 60); + + $this->assertAttributeSame(60, 'timeout', $processor); + } +} diff --git a/Tests/RunCommandTest.php b/Tests/RunCommandTest.php new file mode 100644 index 0000000..a673e06 --- /dev/null +++ b/Tests/RunCommandTest.php @@ -0,0 +1,87 @@ +assertTrue($rc->implementsInterface(\JsonSerializable::class)); + } + + public function testShouldBeFinal() + { + $rc = new \ReflectionClass(RunCommand::class); + + $this->assertTrue($rc->isFinal()); + } + + public function testShouldAllowGetCommandSetInConstructor() + { + $command = new RunCommand('theCommand'); + + $this->assertSame('theCommand', $command->getCommand()); + } + + public function testShouldReturnEmptyArrayByDefaultOnGetArguments() + { + $command = new RunCommand('aCommand'); + + $this->assertSame([], $command->getArguments()); + } + + public function testShouldReturnEmptyArrayByDefaultOnGetOptions() + { + $command = new RunCommand('aCommand'); + + $this->assertSame([], $command->getOptions()); + } + + public function testShouldReturnArgumentsSetInConstructor() + { + $command = new RunCommand('aCommand', ['theArgument' => 'theValue']); + + $this->assertSame(['theArgument' => 'theValue'], $command->getArguments()); + } + + public function testShouldReturnOptionsSetInConstructor() + { + $command = new RunCommand('aCommand', [], ['theOption' => 'theValue']); + + $this->assertSame(['theOption' => 'theValue'], $command->getOptions()); + } + + public function testShouldSerializeAndUnserialzeCommand() + { + $command = new RunCommand( + 'theCommand', + ['theArgument' => 'theValue'], + ['theOption' => 'theValue'] + ); + + $jsonCommand = json_encode($command); + + // guard + $this->assertNotEmpty($jsonCommand); + + $unserializedCommand = RunCommand::jsonUnserialize($jsonCommand); + + $this->assertInstanceOf(RunCommand::class, $unserializedCommand); + $this->assertSame('theCommand', $unserializedCommand->getCommand()); + $this->assertSame(['theArgument' => 'theValue'], $unserializedCommand->getArguments()); + $this->assertSame(['theOption' => 'theValue'], $unserializedCommand->getOptions()); + } + + public function testThrowExceptionIfInvalidJsonGiven() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The malformed json given.'); + + RunCommand::jsonUnserialize('{]'); + } +} diff --git a/composer.json b/composer.json index 37ee38a..48fd6f0 100644 --- a/composer.json +++ b/composer.json @@ -6,19 +6,22 @@ "homepage": "https://enqueue.forma-pro.com/", "license": "MIT", "require": { - "php": ">=5.6", - "enqueue/enqueue": "^0.8@dev", - "symfony/console": "^2.8|^3|^4" + "php": "^7.4|^8.0", + "enqueue/enqueue": "^0.10", + "queue-interop/queue-interop": "^0.8", + "symfony/console": "^5.4|^6.0", + "symfony/process": "^5.4|^6.0" }, "require-dev": { - "phpunit/phpunit": "~5.5", - "symfony/dependency-injection": "^2.8|^3|^4", - "symfony/config": "^2.8|^3|^4", - "symfony/http-kernel": "^2.8|^3|^4", - "symfony/filesystem": "^2.8|^3|^4", - "enqueue/null": "^0.8@dev", - "enqueue/fs": "^0.8@dev", - "enqueue/test": "^0.8@dev" + "phpunit/phpunit": "^9.5", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/config": "^5.4|^6.0", + "symfony/http-kernel": "^5.4|^6.0", + "symfony/filesystem": "^5.4|^6.0", + "symfony/yaml": "^5.4|^6.0", + "enqueue/null": "0.10.x-dev", + "enqueue/fs": "0.10.x-dev", + "enqueue/test": "0.10.x-dev" }, "support": { "email": "opensource@forma-pro.com", @@ -28,7 +31,7 @@ "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" }, "suggest": { - "symfony/dependency-injection": "^2.8|^3|^4 If you'd like to use async event dispatcher container extension." + "symfony/dependency-injection": "^5.4|^6.0 If you'd like to use async event dispatcher container extension." }, "autoload": { "psr-4": { "Enqueue\\AsyncCommand\\": "" }, @@ -38,7 +41,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.10.x-dev" } } }