Skip to content

Commit

Permalink
[CronJob/FrameworkExtraBundle] Support queue cron post command execution
Browse files Browse the repository at this point in the history
  • Loading branch information
mpoiriert committed Jun 26, 2024
1 parent f067813 commit 174ae1e
Show file tree
Hide file tree
Showing 14 changed files with 266 additions and 27 deletions.
12 changes: 10 additions & 2 deletions app/src/Command/NullCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

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

class NullCommand extends Command
Expand All @@ -12,13 +13,20 @@ protected function configure(): void
{
$this
->setName('app:null')
->setDescription('This command does nothing.');
->setDescription('This command does nothing.')
->addOption(
'exit-code',
null,
InputOption::VALUE_REQUIRED,
'The exit code to return.',
Command::SUCCESS
);
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->write('This does nothing.');

return static::SUCCESS;
return (int) $input->getOption('exit-code');
}
}
10 changes: 10 additions & 0 deletions app/src/DataFixtures/AppFixtures.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Persistence\ObjectManager;
use Draw\Component\Application\Configuration\Entity\Config;
use Draw\Component\CronJob\Entity\CronJob;
use Draw\DoctrineExtra\Common\DataFixtures\ObjectReferenceTrait;

class AppFixtures extends Fixture
Expand Down Expand Up @@ -42,6 +43,15 @@ public function load(ObjectManager $manager): void
->setValue(['enabled' => false, 'limit' => 10]),
]
);

$this->persistAndFlush(
$manager,
[
(new CronJob())
->setName('test')
->setCommand('echo "test"'),
]
);
}

private function loadTags(): iterable
Expand Down
4 changes: 2 additions & 2 deletions app/src/Message/NewUserMessage.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
use App\Entity\User;
use Draw\Component\Messenger\AutoStamp\Message\StampingAwareInterface;
use Draw\Component\Messenger\DoctrineEnvelopeEntityReference\Message\DoctrineReferenceAwareInterface;
use Draw\Component\Messenger\Message\AsyncHighPriorityMessageInterface;
use Draw\Component\Messenger\Message\AsyncLowPriorityMessageInterface;
use Draw\Component\Messenger\Searchable\Stamp\SearchableTagStamp;
use Symfony\Component\Messenger\Envelope;

class NewUserMessage implements DoctrineReferenceAwareInterface, AsyncHighPriorityMessageInterface, StampingAwareInterface
class NewUserMessage implements DoctrineReferenceAwareInterface, AsyncLowPriorityMessageInterface, StampingAwareInterface
{
public function __construct(private ?User $user)
{
Expand Down
3 changes: 2 additions & 1 deletion config/packages/messenger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ when@test:
transports:
sync: 'in-memory://'
async: 'in-memory://'
async_high_priority: 'in-memory://'

framework:
messenger:
Expand All @@ -18,7 +19,7 @@ framework:

routing:
Draw\Component\CronJob\Message\ExecuteCronJobMessage: 'async_high_priority'
App\Message\NewTestDocumentMessage: ['sync', 'async_high_priority']
App\Message\NewTestDocumentMessage: ['sync', 'async_low_priority']
Draw\Component\Messenger\ManualTrigger\Message\ManuallyTriggeredInterface: 'async'
Draw\Component\Messenger\Message\RetryFailedMessageMessage: 'async'
Draw\Bundle\UserBundle\Message\NewUserLockMessage: 'sync'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

namespace Draw\Component\CronJob\EventListener;

use Doctrine\Persistence\ManagerRegistry;
use Draw\Component\CronJob\CronJobProcessor;
use Draw\Component\CronJob\Entity\CronJob;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;

/**
* This command listener allow to queue a cron job by name after the execution if it's a success.
*
* Example:
* console/bin acme:purge-database --draw-draw-post-execution-queue-cron-job
*/
class PostExecutionQueueCronJobListener
{
final public const OPTION_POST_EXECUTION_QUEUE_CRON_JOB = 'draw-post-execution-queue-cron-job';

public function __construct(
private CronJobProcessor $cronJobProcessor,
private ManagerRegistry $managerRegistry,
private ?LoggerInterface $logger = new NullLogger(),
) {
}

#[AsEventListener(priority: -1000)]
public function triggerCronJob(ConsoleTerminateEvent $event): void
{
if (Command::SUCCESS !== $event->getExitCode()) {
return;
}

$input = $event->getInput();

if (!$input->hasOption(static::OPTION_POST_EXECUTION_QUEUE_CRON_JOB)) {
return;
}

$cronJobRepository = $this->managerRegistry->getRepository(CronJob::class);

$cronJobNames = $input->getOption(static::OPTION_POST_EXECUTION_QUEUE_CRON_JOB);

foreach ($cronJobNames as $cronJobName) {
$cronJob = $cronJobRepository
->findOneBy(['name' => $cronJobName]);

if (null === $cronJob) {
$this->logger->error(sprintf('Cron job "%s" could not be found.', $cronJobName));

continue;
}

$this->logger->info(sprintf('Queueing cron job "%s"...', $cronJob->getName()));

$this->cronJobProcessor->queue($cronJob, true);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler;

use Draw\Component\CronJob\EventListener\PostExecutionQueueCronJobListener;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;

class AddPostCronJobExecutionOptionPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
try {
$container->findDefinition(PostExecutionQueueCronJobListener::class);
} catch (ServiceNotFoundException) {
return;
}

foreach (array_keys($container->findTaggedServiceIds('console.command')) as $serviceId) {
$definition = $container->getDefinition($serviceId);
if (!$definition->isAbstract()) {
$definition
->addMethodCall(
'addOption',
[
PostExecutionQueueCronJobListener::OPTION_POST_EXECUTION_QUEUE_CRON_JOB,
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'Queue does cron job by name after execution of the command.',
]
);
}
}
}
}
5 changes: 5 additions & 0 deletions packages/framework-extra-bundle/DrawFrameworkExtraBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\AddCommandExecutionOptionsCompilerPass;
use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\AddNewestInstanceRoleCommandOptionPass;
use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\AddPostCronJobExecutionOptionPass;
use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\EmailWriterCompilerPass;
use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\EntityMigratorCompilerPass;
use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\JmsDoctrineObjectConstructionCompilerPass;
Expand Down Expand Up @@ -55,6 +56,10 @@ public function build(ContainerBuilder $container): void
$container->addCompilerPass(new AddNewestInstanceRoleCommandOptionPass());
}

if (class_exists(AddPostCronJobExecutionOptionPass::class)) {
$container->addCompilerPass(new AddPostCronJobExecutionOptionPass());
}

if (class_exists(CommandFlowListener::class)) {
$container->addCompilerPass(new AddCommandExecutionOptionsCompilerPass());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Draw\Component\CronJob\Command\QueueCronJobByNameCommand;
use Draw\Component\CronJob\Command\QueueDueCronJobsCommand;
use Draw\Component\CronJob\CronJobProcessor;
use Draw\Component\CronJob\EventListener\PostExecutionQueueCronJobListener;
use Draw\Component\CronJob\MessageHandler\ExecuteCronJobMessageHandler;
use PHPUnit\Framework\Attributes\CoversClass;

Expand Down Expand Up @@ -47,6 +48,12 @@ public static function provideTestLoad(): iterable
QueueDueCronJobsCommand::class,
]
),
new ServiceConfiguration(
'draw.cron_job.event_listener.post_execution_queue_cron_job_listener',
[
PostExecutionQueueCronJobListener::class,
]
),
new ServiceConfiguration(
'draw.cron_job.cron_job_processor',
[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\AddCommandExecutionOptionsCompilerPass;
use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\AddNewestInstanceRoleCommandOptionPass;
use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\AddPostCronJobExecutionOptionPass;
use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\EmailWriterCompilerPass;
use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\EntityMigratorCompilerPass;
use Draw\Bundle\FrameworkExtraBundle\DependencyInjection\Compiler\JmsDoctrineObjectConstructionCompilerPass;
Expand Down Expand Up @@ -38,7 +39,7 @@ public function testBuild(): void
$containerBuilder = $this->createMock(ContainerBuilder::class);

$containerBuilder
->expects(static::exactly(11))
->expects(static::exactly(12))
->method('addCompilerPass')
->with(
...static::withConsecutive(
Expand Down Expand Up @@ -72,6 +73,11 @@ public function testBuild(): void
PassConfig::TYPE_BEFORE_OPTIMIZATION,
0,
],
[
static::isInstanceOf(AddPostCronJobExecutionOptionPass::class),
PassConfig::TYPE_BEFORE_OPTIMIZATION,
0,
],
[
static::isInstanceOf(AddCommandExecutionOptionsCompilerPass::class),
PassConfig::TYPE_BEFORE_OPTIMIZATION,
Expand Down
14 changes: 14 additions & 0 deletions tests/Command/NullCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ public static function provideTestArgument(): iterable

public static function provideTestOption(): iterable
{
yield [
'exit-code',
null,
InputOption::VALUE_REQUIRED,
Command::SUCCESS,
];

yield [
'draw-execution-id',
null,
Expand All @@ -47,6 +54,13 @@ public static function provideTestOption(): iterable
null,
InputOption::VALUE_REQUIRED,
];

yield [
'draw-post-execution-queue-cron-job',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
[],
];
}

public function testExecute(): void
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ Usage:
draw:console:generate-documentation [options] [--] <path>

Arguments:
path The path where the documentation will be generated.
path The path where the documentation will be generated.

Options:
--format=FORMAT The format of the documentation (txt|md|json|xml). [default: "txt"]
--aws-newest-instance-role=AWS-NEWEST-INSTANCE-ROLE The instance role the server must be the newest of to run the command.
--draw-execution-id=DRAW-EXECUTION-ID The existing execution id of the command. Use internally by the DrawCommandBundle.
--draw-execution-ignore Flag to ignore login of the execution to the databases.
-h, --help Display help for the given command. When no command is given display help for the list command
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
--format=FORMAT The format of the documentation (txt|md|json|xml). [default: "txt"]
--aws-newest-instance-role=AWS-NEWEST-INSTANCE-ROLE The instance role the server must be the newest of to run the command.
--draw-post-execution-queue-cron-job=DRAW-POST-EXECUTION-QUEUE-CRON-JOB Queue does cron job by name after execution of the command. (multiple values allowed)
--draw-execution-id=DRAW-EXECUTION-ID The existing execution id of the command. Use internally by the DrawCommandBundle.
--draw-execution-ignore Flag to ignore login of the execution to the databases.
-h, --help Display help for the given command. When no command is given display help for the list command
-q, --quiet Do not output any message
-V, --version Display this application version
--ansi|--no-ansi Force (or disable --no-ansi) ANSI output
-n, --no-interaction Do not ask any interactive question
-v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace App\Tests\CronJob\EventListener;

use App\Command\NullCommand;
use Draw\Bundle\TesterBundle\Messenger\TransportTester;
use Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire\AutowiredCompletionAwareInterface;
use Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire\AutowireService;
use Draw\Bundle\TesterBundle\PHPUnit\Extension\SetUpAutowire\AutowireTransportTester;
use Draw\Component\Core\FilterExpression\Expression\Expression;
use Draw\Component\CronJob\Message\ExecuteCronJobMessage;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;

class PostExecutionQueueCronJobListenerTest extends KernelTestCase implements AutowiredCompletionAwareInterface
{
#[AutowireService]
private NullCommand $command;

#[AutowireTransportTester('async_high_priority')]
private TransportTester $transportTester;

private Application $application;

public function postAutowire(): void
{
$this->application = new Application(static::$kernel);
$this->application->add($this->command);
$this->application->setAutoExit(false);
}

public function testTriggerCronJob(): void
{
$result = $this->application
->run(
new ArrayInput([
'app:null',
'--draw-post-execution-queue-cron-job' => ['test', 'test'],
]),
new NullOutput()
);

static::assertSame(
Command::SUCCESS,
$result
);

$this->transportTester
->assertMessageMatch(
ExecuteCronJobMessage::class,
Expression::andWhereEqual([
'execution.cronJob.name' => 'test',
]),
2
);
}

public function testTriggerCronJobError(): void
{
$result = $this->application
->run(
new ArrayInput([
'app:null',
'--exit-code' => Command::FAILURE,
'--draw-post-execution-queue-cron-job' => ['test', 'test'],
]),
new NullOutput()
);

static::assertSame(
Command::FAILURE,
$result
);

$this->transportTester
->assertMessageMatch(
ExecuteCronJobMessage::class,
count: 0
);
}
}
Loading

0 comments on commit 174ae1e

Please sign in to comment.