Skip to content

Commit

Permalink
SymfonyCommandJob
Browse files Browse the repository at this point in the history
  • Loading branch information
mabar committed Mar 16, 2024
1 parent 5b14536 commit 3904939
Show file tree
Hide file tree
Showing 11 changed files with 493 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- explains cron expression syntax
- `ListCommand`
- adds `--explain` option to explain whole expression
- `SymfonyCommandJob`

### Fixed

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"orisai/exceptions": "^1.1.0",
"symfony/console": "^5.3.0|^6.0.0",
"symfony/lock": "^5.3.0|^6.0.0",
"symfony/polyfill-php80": "^1.29.0",
"symfony/process": "^5.3.0|^6.0.0"
},
"require-dev": {
Expand Down
31 changes: 30 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,35 @@ $scheduler->addJob(
);
```

### Symfony command job

Run [symfony/console](https://github.com/symfony/console) command as a job

- if job succeeds (returns zero code), command output is ignored
- if job fails (returns non-zero code), exception is thrown, including command return code, output and if thrown by the
command, the exception

```php
use Orisai\Scheduler\Job\SymfonyCommandJob;

$job = new SymfonyCommandJob($command, $application);
$scheduler->addJob(
$job,
/* ... */,
);

```

Command can be parametrized:

```php
$job->setCommandParameters([
'argument' => 'value',
'--option' => 'value',
'--boolean-option' => true,
]);
```

## Job info and result

Status information available via [events](#events) and [run summary](#run-summary)
Expand Down Expand Up @@ -582,7 +611,7 @@ thrown `JobFailure`.

## CLI commands

For symfony/console you may use our commands:
For [symfony/console](https://github.com/symfony/console) you may use our commands:

- [Run](#run-command)
- [Run job](#run-job-command)
Expand Down
109 changes: 109 additions & 0 deletions src/Job/SymfonyCommandJob.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php declare(strict_types = 1);

namespace Orisai\Scheduler\Job;

use Orisai\Exceptions\Logic\InvalidState;
use Orisai\Exceptions\Message;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Throwable;
use function array_merge;
use function assert;
use function is_numeric;

final class SymfonyCommandJob implements Job
{

private Command $command;

private Application $application;

/** @var array<int|string, mixed> */
private array $parameters = [];

public function __construct(Command $command, Application $application)
{
$this->command = $command;
$this->application = $application;
}

/**
* @param array<int|string, mixed> $parameters
*/
public function setCommandParameters(array $parameters): void
{
$this->parameters = $parameters;
}

public function getName(): string
{
$name = $this->command->getName();
assert($name !== null); // It must be set in constructor

return 'symfony/console: ' . $this->createInput();
}

public function run(JobLock $lock): void
{
$input = $this->createInput();
$output = new BufferedOutput();

try {
// Using doRun() to prevent auto-exiting and error-handling
$exitCode = $this->application->doRun($input, $output);
} catch (Throwable $commandException) {
$exitCode = $this->getExceptionCode($commandException);
}

$outputString = $output->fetch();

if ($exitCode !== 0) {
$message = Message::create()
->withContext("Running command '{$this->command->getName()}'.")
->withProblem("Run failed with code '$exitCode'.");

if ($outputString !== '') {
$message->with('Output', $outputString);
}

$exception = InvalidState::create()
->withCode($exitCode)
->withMessage($message);

if (isset($commandException)) {
$exception->withPrevious($commandException)
->withSuppressed([$commandException]);
}

throw $exception;
}
}

private function createInput(): ArrayInput
{
return new ArrayInput(array_merge(
[$this->command->getName()],
$this->parameters,
));
}

private function getExceptionCode(Throwable $exception): int
{
$exitCode = $exception->getCode();

if (is_numeric($exitCode)) {
$exitCode = (int) $exitCode;
if ($exitCode <= 0) {
$exitCode = 1;
}

return $exitCode;
}

/** @codeCoverageIgnore Hard to simulate, only php extensions can return non-int code */
return 1;
}

}
35 changes: 35 additions & 0 deletions tests/Doubles/TestExceptionCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php declare(strict_types = 1);

namespace Tests\Orisai\Scheduler\Doubles;

use Orisai\Exceptions\Logic\NotImplemented;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class TestExceptionCommand extends Command
{

private int $code;

public function __construct(int $code)
{
parent::__construct();
$this->code = $code;
}

protected function configure(): void
{
$this->setName('test:exception');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Failure!');

throw NotImplemented::create()
->withMessage('Message')
->withCode($this->code);
}

}
22 changes: 22 additions & 0 deletions tests/Doubles/TestFailNoOutputCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php declare(strict_types = 1);

namespace Tests\Orisai\Scheduler\Doubles;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class TestFailNoOutputCommand extends Command
{

protected function configure(): void
{
$this->setName('test:fail-no-output');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
return 1;
}

}
25 changes: 25 additions & 0 deletions tests/Doubles/TestFailOutputCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php declare(strict_types = 1);

namespace Tests\Orisai\Scheduler\Doubles;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class TestFailOutputCommand extends Command
{

protected function configure(): void
{
$this->setName('test:fail-output');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Failure!');
$output->writeln('New line!');

return 256;
}

}
27 changes: 27 additions & 0 deletions tests/Doubles/TestParametrizedCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php declare(strict_types = 1);

namespace Tests\Orisai\Scheduler\Doubles;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

final class TestParametrizedCommand extends Command
{

protected function configure(): void
{
$this->setName('test:parameters');
$this->addArgument('argument', InputArgument::REQUIRED);
$this->addOption('option', null, InputOption::VALUE_REQUIRED);
$this->addOption('bool-option', null, InputOption::VALUE_NONE);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
return 0;
}

}
24 changes: 24 additions & 0 deletions tests/Doubles/TestSuccessCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php declare(strict_types = 1);

namespace Tests\Orisai\Scheduler\Doubles;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;

final class TestSuccessCommand extends Command
{

protected function configure(): void
{
$this->setName('test:success');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln('Success!');

return 0;
}

}
11 changes: 9 additions & 2 deletions tests/Helpers/CommandOutputHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,16 @@
final class CommandOutputHelper
{

public static function getCommandOutput(CommandTester $tester): string
/**
* @param string|CommandTester $output
*/
public static function getCommandOutput($output): string
{
$display = preg_replace('~\R~u', PHP_EOL, $tester->getDisplay());
if ($output instanceof CommandTester) {
$output = $output->getDisplay();
}

$display = preg_replace('~\R~u', PHP_EOL, $output);
assert($display !== null);

return implode(
Expand Down
Loading

0 comments on commit 3904939

Please sign in to comment.