From eebd3a85b90539934cec349c38f1a7f73df947c5 Mon Sep 17 00:00:00 2001 From: Loulier Guillaume Date: Thu, 14 Oct 2021 14:49:42 +0200 Subject: [PATCH 1/5] feat(core): started --- src/Command/ExportCommand.php | 50 ++++++++++++++++++++++++ src/Export/CronExporter.php | 12 ++++++ src/Export/ExporterInterface.php | 12 ++++++ src/Export/ExporterRegistry.php | 24 ++++++++++++ src/Export/ExporterRegistryInterface.php | 12 ++++++ tests/Command/ExportCommandTest.php | 14 +++++++ tests/Export/CronExporterTest.php | 14 +++++++ tests/Export/ExportRegistryTest.php | 14 +++++++ 8 files changed, 152 insertions(+) create mode 100644 src/Command/ExportCommand.php create mode 100644 src/Export/CronExporter.php create mode 100644 src/Export/ExporterInterface.php create mode 100644 src/Export/ExporterRegistry.php create mode 100644 src/Export/ExporterRegistryInterface.php create mode 100644 tests/Command/ExportCommandTest.php create mode 100644 tests/Export/CronExporterTest.php create mode 100644 tests/Export/ExportRegistryTest.php diff --git a/src/Command/ExportCommand.php b/src/Command/ExportCommand.php new file mode 100644 index 00000000..e11783bd --- /dev/null +++ b/src/Command/ExportCommand.php @@ -0,0 +1,50 @@ + + */ +final class ExportCommand extends Command +{ + private ExporterRegistryInterface $exporterRegistry; + + protected static $defaultName = 'scheduler:export'; + + public function __construct(ExporterRegistryInterface $exporterRegistry) + { + $this->exporterRegistry = $exporterRegistry; + + parent::__construct(); + } + + /** + * {@inheritdoc} + */ + protected function configure(): void + { + $this + ->setDescription('Export tasks to a specific format') + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $style = new SymfonyStyle($input, $output); + + // TODO + + return Command::SUCCESS; + } +} diff --git a/src/Export/CronExporter.php b/src/Export/CronExporter.php new file mode 100644 index 00000000..1270d1cb --- /dev/null +++ b/src/Export/CronExporter.php @@ -0,0 +1,12 @@ + + */ +final class CronExporter implements ExporterInterface +{ +} diff --git a/src/Export/ExporterInterface.php b/src/Export/ExporterInterface.php new file mode 100644 index 00000000..5d854723 --- /dev/null +++ b/src/Export/ExporterInterface.php @@ -0,0 +1,12 @@ + + */ +interface ExporterInterface +{ +} diff --git a/src/Export/ExporterRegistry.php b/src/Export/ExporterRegistry.php new file mode 100644 index 00000000..eba461e9 --- /dev/null +++ b/src/Export/ExporterRegistry.php @@ -0,0 +1,24 @@ + + */ +final class ExporterRegistry implements ExporterRegistryInterface +{ + /** + * @var ExporterInterface[] + */ + private iterable $exporterList; + + /** + * @param ExporterInterface[] $exporterList + */ + public function __construct(iterable $exporterList) + { + $this->exporterList = $exporterList; + } +} diff --git a/src/Export/ExporterRegistryInterface.php b/src/Export/ExporterRegistryInterface.php new file mode 100644 index 00000000..7f8c55c7 --- /dev/null +++ b/src/Export/ExporterRegistryInterface.php @@ -0,0 +1,12 @@ + + */ +interface ExporterRegistryInterface +{ +} diff --git a/tests/Command/ExportCommandTest.php b/tests/Command/ExportCommandTest.php new file mode 100644 index 00000000..c859cac5 --- /dev/null +++ b/tests/Command/ExportCommandTest.php @@ -0,0 +1,14 @@ + + */ +final class ExportCommandTest extends TestCase +{ +} diff --git a/tests/Export/CronExporterTest.php b/tests/Export/CronExporterTest.php new file mode 100644 index 00000000..f127e92c --- /dev/null +++ b/tests/Export/CronExporterTest.php @@ -0,0 +1,14 @@ + + */ +final class CronExporterTest extends TestCase +{ +} diff --git a/tests/Export/ExportRegistryTest.php b/tests/Export/ExportRegistryTest.php new file mode 100644 index 00000000..6fbb3126 --- /dev/null +++ b/tests/Export/ExportRegistryTest.php @@ -0,0 +1,14 @@ + + */ +final class ExportRegistryTest extends TestCase +{ +} From 91c4d755bfa420c765d412da265e526421386949 Mon Sep 17 00:00:00 2001 From: Loulier Guillaume Date: Thu, 14 Oct 2021 18:57:45 +0200 Subject: [PATCH 2/5] feat(core): improvements --- composer.json | 2 + .../SchedulerBundleExtension.php | 27 +++++++ src/Export/CronExporter.php | 12 --- src/Export/CronTabExporter.php | 22 ++++++ src/Export/ExporterInterface.php | 5 ++ src/Export/ExporterRegistry.php | 55 +++++++++++++ src/Export/ExporterRegistryInterface.php | 16 +++- .../SchedulerBundleExtensionTest.php | 33 ++++++++ ...porterTest.php => CronTabExporterTest.php} | 2 +- tests/Export/ExportRegistryTest.php | 14 ---- tests/Export/ExporterRegistryTest.php | 79 +++++++++++++++++++ 11 files changed, 239 insertions(+), 28 deletions(-) delete mode 100644 src/Export/CronExporter.php create mode 100644 src/Export/CronTabExporter.php rename tests/Export/{CronExporterTest.php => CronTabExporterTest.php} (78%) delete mode 100644 tests/Export/ExportRegistryTest.php create mode 100644 tests/Export/ExporterRegistryTest.php diff --git a/composer.json b/composer.json index 24cc8ee9..51b92d6f 100644 --- a/composer.json +++ b/composer.json @@ -34,6 +34,7 @@ "SchedulerBundle\\Event\\": "src/Event/", "SchedulerBundle\\EventListener\\": "src/EventListener/", "SchedulerBundle\\Exception\\": "src/Exception/", + "SchedulerBundle\\Export\\": "src/Export/", "SchedulerBundle\\Expression\\": "src/Expression/", "SchedulerBundle\\Fiber\\": "src/Fiber/", "SchedulerBundle\\Messenger\\": "src/Messenger/", @@ -74,6 +75,7 @@ "Tests\\SchedulerBundle\\DependencyInjection\\Assets\\": "tests/DependencyInjection/Assets/", "Tests\\SchedulerBundle\\Event\\": "tests/Event/", "Tests\\SchedulerBundle\\EventListener\\": "tests/EventListener/", + "Tests\\SchedulerBundle\\Export\\": "tests/Export/", "Tests\\SchedulerBundle\\Expression\\": "tests/Expression/", "Tests\\SchedulerBundle\\Messenger\\": "tests/Messenger/", "Tests\\SchedulerBundle\\Middleware\\": "tests/Middleware/", diff --git a/src/DependencyInjection/SchedulerBundleExtension.php b/src/DependencyInjection/SchedulerBundleExtension.php index 2a6c297b..8d389a4d 100644 --- a/src/DependencyInjection/SchedulerBundleExtension.php +++ b/src/DependencyInjection/SchedulerBundleExtension.php @@ -31,6 +31,10 @@ use SchedulerBundle\EventListener\TaskLoggerSubscriber; use SchedulerBundle\EventListener\TaskSubscriber; use SchedulerBundle\EventListener\WorkerLifecycleSubscriber; +use SchedulerBundle\Export\CronTabExporter; +use SchedulerBundle\Export\ExporterInterface; +use SchedulerBundle\Export\ExporterRegistry; +use SchedulerBundle\Export\ExporterRegistryInterface; use SchedulerBundle\Expression\BuilderInterface; use SchedulerBundle\Expression\ComputedExpressionBuilder; use SchedulerBundle\Expression\CronExpressionBuilder; @@ -185,6 +189,7 @@ final class SchedulerBundleExtension extends Extension private const EXECUTION_POLICY_TAG = 'scheduler.execution_policy'; private const WORKER_TAG = 'scheduler.worker'; private const SCHEDULER_MIDDLEWARE_TAG = 'scheduler.middleware'; + private const TASK_EXPORTER_TAG = 'scheduler.task_exporter'; public function load(array $configs, ContainerBuilder $container): void { @@ -208,6 +213,7 @@ public function load(array $configs, ContainerBuilder $container): void $this->registerExpressionFactoryAndPolicies($container); $this->registerBuilders($container); $this->registerRunners($container); + $this->registerExportTools($container); $this->registerNormalizer($container); $this->registerMessengerTools($container); $this->registerSubscribers($container); @@ -262,6 +268,7 @@ private function registerAutoConfigure(ContainerBuilder $container): void $container->registerForAutoconfiguration(TaskBagInterface::class)->addTag('scheduler.task_bag'); $container->registerForAutoconfiguration(SchedulerAwareInterface::class)->addTag('scheduler.entry_point'); $container->registerForAutoconfiguration(ExecutionPolicyInterface::class)->addTag(self::EXECUTION_POLICY_TAG); + $container->registerForAutoconfiguration(ExporterInterface::class)->addTag(self::TASK_EXPORTER_TAG); } private function registerConfigurationFactories(ContainerBuilder $container): void @@ -961,6 +968,26 @@ private function registerRunners(ContainerBuilder $container): void ; } + public function registerExportTools(ContainerBuilder $container): void + { + $container->register(ExporterRegistry::class, ExporterRegistry::class) + ->setArguments([ + new TaggedIteratorArgument(self::TASK_EXPORTER_TAG), + ]) + ->addTag('container.preload', [ + 'class' => ExporterRegistry::class, + ]) + ; + $container->setAlias(ExporterRegistryInterface::class, ExporterRegistry::class); + + $container->register(CronTabExporter::class, CronTabExporter::class) + ->addTag(self::TASK_EXPORTER_TAG) + ->addTag('container.preload', [ + 'class' => CronTabExporter::class, + ]) + ; + } + private function registerNormalizer(ContainerBuilder $container): void { $container->register(TaskNormalizer::class, TaskNormalizer::class) diff --git a/src/Export/CronExporter.php b/src/Export/CronExporter.php deleted file mode 100644 index 1270d1cb..00000000 --- a/src/Export/CronExporter.php +++ /dev/null @@ -1,12 +0,0 @@ - - */ -final class CronExporter implements ExporterInterface -{ -} diff --git a/src/Export/CronTabExporter.php b/src/Export/CronTabExporter.php new file mode 100644 index 00000000..c7f837d0 --- /dev/null +++ b/src/Export/CronTabExporter.php @@ -0,0 +1,22 @@ + + */ +final class CronTabExporter implements ExporterInterface +{ + public function export(string $filename, TaskInterface $task): void + { + } + + public function support(string $format): bool + { + return 'crontab' === $format; + } +} diff --git a/src/Export/ExporterInterface.php b/src/Export/ExporterInterface.php index 5d854723..99ab177c 100644 --- a/src/Export/ExporterInterface.php +++ b/src/Export/ExporterInterface.php @@ -4,9 +4,14 @@ namespace SchedulerBundle\Export; +use SchedulerBundle\Task\TaskInterface; + /** * @author Guillaume Loulier */ interface ExporterInterface { + public function export(string $filename, TaskInterface $task): void; + + public function support(string $format): bool; } diff --git a/src/Export/ExporterRegistry.php b/src/Export/ExporterRegistry.php index eba461e9..ec2c6322 100644 --- a/src/Export/ExporterRegistry.php +++ b/src/Export/ExporterRegistry.php @@ -4,6 +4,14 @@ namespace SchedulerBundle\Export; +use Closure; +use SchedulerBundle\Exception\InvalidArgumentException; +use SchedulerBundle\Exception\RuntimeException; +use function array_filter; +use function count; +use function current; +use const ARRAY_FILTER_USE_BOTH; + /** * @author Guillaume Loulier */ @@ -21,4 +29,51 @@ public function __construct(iterable $exporterList) { $this->exporterList = $exporterList; } + + /** + * {@inheritdoc} + */ + public function find(string $format): ExporterInterface + { + $filteredExporterList = $this->filter(static fn (ExporterInterface $exporter): bool => $exporter->support($format)); + + if (0 === $filteredExporterList->count()) { + throw new InvalidArgumentException(sprintf('No exporter found for the format "%s"', $format)); + } + + if (1 < $filteredExporterList->count()) { + throw new InvalidArgumentException('More than one exporter support this format, please consider using this exporter directly'); + } + + return $filteredExporterList->current(); + } + + /** + * {@inheritdoc} + */ + public function filter(Closure $func): self + { + return new self(array_filter($this->exporterList, $func, ARRAY_FILTER_USE_BOTH)); + } + + /** + * {@inheritdoc} + */ + public function current(): ExporterInterface + { + $exporter = current($this->exporterList); + if (false === $exporter) { + throw new RuntimeException('The current runner cannot be found'); + } + + return $exporter; + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + return count($this->exporterList); + } } diff --git a/src/Export/ExporterRegistryInterface.php b/src/Export/ExporterRegistryInterface.php index 7f8c55c7..8911b941 100644 --- a/src/Export/ExporterRegistryInterface.php +++ b/src/Export/ExporterRegistryInterface.php @@ -4,9 +4,23 @@ namespace SchedulerBundle\Export; +use Closure; +use Countable; + /** * @author Guillaume Loulier */ -interface ExporterRegistryInterface +interface ExporterRegistryInterface extends Countable { + /** + * Return a {@see ExporterInterface} using the @param string $format to determine which one can perform the export. + */ + public function find(string $format): ExporterInterface; + + public function filter(Closure $func): self; + + /** + * Return the currently available {@see ExporterInterface} + */ + public function current(): ExporterInterface; } diff --git a/tests/DependencyInjection/SchedulerBundleExtensionTest.php b/tests/DependencyInjection/SchedulerBundleExtensionTest.php index a8cbee37..ea1ec2cb 100644 --- a/tests/DependencyInjection/SchedulerBundleExtensionTest.php +++ b/tests/DependencyInjection/SchedulerBundleExtensionTest.php @@ -35,6 +35,10 @@ use SchedulerBundle\EventListener\TaskLoggerSubscriber; use SchedulerBundle\EventListener\TaskSubscriber; use SchedulerBundle\EventListener\WorkerLifecycleSubscriber; +use SchedulerBundle\Export\CronTabExporter; +use SchedulerBundle\Export\ExporterInterface; +use SchedulerBundle\Export\ExporterRegistry; +use SchedulerBundle\Export\ExporterRegistryInterface; use SchedulerBundle\Expression\BuilderInterface; use SchedulerBundle\Expression\ComputedExpressionBuilder; use SchedulerBundle\Expression\CronExpressionBuilder; @@ -252,6 +256,8 @@ public function testInterfacesForAutoconfigureAreRegistered(): void self::assertTrue($autoconfigurationInterfaces[SchedulerAwareInterface::class]->hasTag('scheduler.entry_point')); self::assertArrayHasKey(ExecutionPolicyInterface::class, $autoconfigurationInterfaces); self::assertTrue($autoconfigurationInterfaces[ExecutionPolicyInterface::class]->hasTag('scheduler.execution_policy')); + self::assertArrayHasKey(ExporterInterface::class, $autoconfigurationInterfaces); + self::assertTrue($autoconfigurationInterfaces[ExporterInterface::class]->hasTag('scheduler.task_exporter')); } public function testConfigurationFactoriesAreRegistered(): void @@ -1072,6 +1078,33 @@ public function testRunnersAreRegistered(): void self::assertSame(ChainedTaskRunner::class, $container->getDefinition(ChainedTaskRunner::class)->getTag('container.preload')[0]['class']); } + public function testExportToolsAreRegistered(): void + { + $container = $this->getContainer([ + 'path' => '/_foo', + 'timezone' => 'Europe/Paris', + 'transport' => [ + 'dsn' => 'memory://first_in_first_out', + ], + 'tasks' => [], + 'lock_store' => null, + ]); + + self::assertTrue($container->hasAlias(ExporterRegistryInterface::class)); + self::assertTrue($container->hasDefinition(ExporterRegistry::class)); + self::assertFalse($container->getDefinition(ExporterRegistry::class)->isPublic()); + self::assertCount(1, $container->getDefinition(ExporterRegistry::class)->getArguments()); + self::assertInstanceOf(TaggedIteratorArgument::class, $container->getDefinition(ExporterRegistry::class)->getArgument(0)); + self::assertTrue($container->getDefinition(ExporterRegistry::class)->hasTag('container.preload')); + self::assertSame(ExporterRegistry::class, $container->getDefinition(ExporterRegistry::class)->getTag('container.preload')[0]['class']); + + self::assertTrue($container->hasDefinition(CronTabExporter::class)); + self::assertCount(0, $container->getDefinition(CronTabExporter::class)->getArguments()); + self::assertTrue($container->getDefinition(CronTabExporter::class)->hasTag('scheduler.task_exporter')); + self::assertTrue($container->getDefinition(CronTabExporter::class)->hasTag('container.preload')); + self::assertSame(CronTabExporter::class, $container->getDefinition(CronTabExporter::class)->getTag('container.preload')[0]['class']); + } + public function testNormalizersAreRegistered(): void { $container = $this->getContainer([ diff --git a/tests/Export/CronExporterTest.php b/tests/Export/CronTabExporterTest.php similarity index 78% rename from tests/Export/CronExporterTest.php rename to tests/Export/CronTabExporterTest.php index f127e92c..dc67c765 100644 --- a/tests/Export/CronExporterTest.php +++ b/tests/Export/CronTabExporterTest.php @@ -9,6 +9,6 @@ /** * @author Guillaume Loulier */ -final class CronExporterTest extends TestCase +final class CronTabExporterTest extends TestCase { } diff --git a/tests/Export/ExportRegistryTest.php b/tests/Export/ExportRegistryTest.php deleted file mode 100644 index 6fbb3126..00000000 --- a/tests/Export/ExportRegistryTest.php +++ /dev/null @@ -1,14 +0,0 @@ - - */ -final class ExportRegistryTest extends TestCase -{ -} diff --git a/tests/Export/ExporterRegistryTest.php b/tests/Export/ExporterRegistryTest.php new file mode 100644 index 00000000..8101f210 --- /dev/null +++ b/tests/Export/ExporterRegistryTest.php @@ -0,0 +1,79 @@ + + */ +final class ExporterRegistryTest extends TestCase +{ + public function testRegistryCanReturnEmptyExporterList(): void + { + $registry = new ExporterRegistry([]); + + self::assertCount(0, $registry); + } + + public function testRegistryCanFilterExporterList(): void + { + $registry = new ExporterRegistry([ + new CronTabExporter(), + ]); + + $registry->filter(static fn (ExporterInterface $exporter): bool => $exporter instanceof CronTabExporter); + self::assertCount(1, $registry); + } + + public function testRegistryCannotFindExporterWithEmptyList(): void + { + $registry = new ExporterRegistry([ + new CronTabExporter(), + ]); + + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('No exporter found for the format "foo"'); + self::expectExceptionCode(0); + $registry->find('foo'); + } + + public function testRegistryCannotFindExporterWithMultipleSupportingExporter(): void + { + $registry = new ExporterRegistry([ + new CronTabExporter(), + new CronTabExporter(), + ]); + + self::expectException(InvalidArgumentException::class); + self::expectExceptionMessage('More than one exporter support this format, please consider using this exporter directly'); + self::expectExceptionCode(0); + $registry->find('crontab'); + } + + public function testRegistryCanFindExporter(): void + { + $registry = new ExporterRegistry([ + new CronTabExporter(), + ]); + + $exporter = $registry->find('crontab'); + self::assertInstanceOf(CronTabExporter::class, $exporter); + } + + public function testRegistryCanReturnCurrentExporter(): void + { + $registry = new ExporterRegistry([ + new CronTabExporter(), + ]); + + $exporter = $registry->current(); + self::assertInstanceOf(CronTabExporter::class, $exporter); + } +} From 5fc7559287b87adc8d6340857bbc9d315d71dba1 Mon Sep 17 00:00:00 2001 From: Loulier Guillaume Date: Fri, 15 Oct 2021 14:00:20 +0200 Subject: [PATCH 3/5] tests(core): improvements --- .github/workflows/code-style.yml | 2 +- .github/workflows/infection.yml | 2 +- .github/workflows/phpunit.yml | 2 +- .github/workflows/rector.yml | 2 +- .github/workflows/security.yml | 2 +- .github/workflows/static-analysis.yml | 2 +- src/Command/ExportCommand.php | 41 +++++++++++++++++-- .../SchedulerBundleExtension.php | 12 ++++++ src/Export/CronTabExporter.php | 7 ++++ src/Export/ExporterInterface.php | 3 ++ .../SchedulerBundleExtensionTest.php | 17 ++++++++ 11 files changed, 83 insertions(+), 9 deletions(-) diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index b8bcdcd5..aadc68cc 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: php-cs-fixer: diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index 91e62836..d477ef14 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: infection: diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index cb4fb0e7..cf18f3a5 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: phpunit: diff --git a/.github/workflows/rector.yml b/.github/workflows/rector.yml index 9a2f2fc3..557887ea 100644 --- a/.github/workflows/rector.yml +++ b/.github/workflows/rector.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: phpstan: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 0876add6..f236933f 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: security: diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index f1d1a0b4..26e5a317 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -7,7 +7,7 @@ on: branches: - main schedule: - - cron: "0 0 * * *" + - cron: "5 0 * * *" jobs: phpstan: diff --git a/src/Command/ExportCommand.php b/src/Command/ExportCommand.php index e11783bd..769ee442 100644 --- a/src/Command/ExportCommand.php +++ b/src/Command/ExportCommand.php @@ -5,10 +5,14 @@ namespace SchedulerBundle\Command; use SchedulerBundle\Export\ExporterRegistryInterface; +use SchedulerBundle\SchedulerInterface; +use SchedulerBundle\Task\TaskInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Throwable; /** * @author Guillaume Loulier @@ -16,12 +20,16 @@ final class ExportCommand extends Command { private ExporterRegistryInterface $exporterRegistry; + private SchedulerInterface $scheduler; protected static $defaultName = 'scheduler:export'; - public function __construct(ExporterRegistryInterface $exporterRegistry) - { + public function __construct( + ExporterRegistryInterface $exporterRegistry, + SchedulerInterface $scheduler + ) { $this->exporterRegistry = $exporterRegistry; + $this->scheduler = $scheduler; parent::__construct(); } @@ -33,6 +41,10 @@ protected function configure(): void { $this ->setDescription('Export tasks to a specific format') + ->setDefinition([ + new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'The format used to export tasks', 'crontab'), + new InputOption('filename', null, InputOption::VALUE_OPTIONAL, 'The name of the filename used to export tasks', 'crontab'), + ]) ; } @@ -43,7 +55,30 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $style = new SymfonyStyle($input, $output); - // TODO + try { + $tasks = $this->scheduler->getTasks(); + if (0 === $tasks->count()) { + $style->warning('No tasks found'); + + return Command::FAILURE; + } + + $filename = $input->getOption('filename'); + $exporter = $this->exporterRegistry->find($input->getOption('format')); + + $tasks->walk(function (TaskInterface $task) use ($exporter, $filename): void { + $exporter->export($filename, $task); + }); + } catch (Throwable $throwable) { + $style->error([ + 'An error occurred when exporting tasks', + $throwable->getMessage(), + ]); + + return Command::FAILURE; + } + + $style->success('The export has succeed'); return Command::SUCCESS; } diff --git a/src/DependencyInjection/SchedulerBundleExtension.php b/src/DependencyInjection/SchedulerBundleExtension.php index 8d389a4d..0381395e 100644 --- a/src/DependencyInjection/SchedulerBundleExtension.php +++ b/src/DependencyInjection/SchedulerBundleExtension.php @@ -17,6 +17,7 @@ use SchedulerBundle\Command\DebugProbeCommand; use SchedulerBundle\Command\ExecuteExternalProbeCommand; use SchedulerBundle\Command\ExecuteTaskCommand; +use SchedulerBundle\Command\ExportCommand; use SchedulerBundle\Command\ListFailedTasksCommand; use SchedulerBundle\Command\ListTasksCommand; use SchedulerBundle\Command\RebootSchedulerCommand; @@ -606,6 +607,17 @@ private function registerCommands(ContainerBuilder $container): void ]) ; + $container->register(ExportCommand::class, ExportCommand::class) + ->setArguments([ + new Reference(ExporterRegistryInterface::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE), + new Reference(SchedulerInterface::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE), + ]) + ->addTag('console.command') + ->addTag('container.preload', [ + 'class' => ExportCommand::class, + ]) + ; + $container->register(ListFailedTasksCommand::class, ListFailedTasksCommand::class) ->setArguments([ new Reference(WorkerInterface::class, ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE), diff --git a/src/Export/CronTabExporter.php b/src/Export/CronTabExporter.php index c7f837d0..19e1d549 100644 --- a/src/Export/CronTabExporter.php +++ b/src/Export/CronTabExporter.php @@ -5,6 +5,7 @@ namespace SchedulerBundle\Export; use SchedulerBundle\Task\TaskInterface; +use function file_exists; /** * @author Guillaume Loulier @@ -13,8 +14,14 @@ final class CronTabExporter implements ExporterInterface { public function export(string $filename, TaskInterface $task): void { + if (file_exists($filename)) { + return; + } } + /** + * {@inheritdoc} + */ public function support(string $format): bool { return 'crontab' === $format; diff --git a/src/Export/ExporterInterface.php b/src/Export/ExporterInterface.php index 99ab177c..df36661f 100644 --- a/src/Export/ExporterInterface.php +++ b/src/Export/ExporterInterface.php @@ -13,5 +13,8 @@ interface ExporterInterface { public function export(string $filename, TaskInterface $task): void; + /** + * Determine if the exporter support the current @param string $format. + */ public function support(string $format): bool; } diff --git a/tests/DependencyInjection/SchedulerBundleExtensionTest.php b/tests/DependencyInjection/SchedulerBundleExtensionTest.php index ea1ec2cb..55537c0f 100644 --- a/tests/DependencyInjection/SchedulerBundleExtensionTest.php +++ b/tests/DependencyInjection/SchedulerBundleExtensionTest.php @@ -19,6 +19,7 @@ use SchedulerBundle\Command\DebugProbeCommand; use SchedulerBundle\Command\ExecuteExternalProbeCommand; use SchedulerBundle\Command\ExecuteTaskCommand; +use SchedulerBundle\Command\ExportCommand; use SchedulerBundle\Command\ListFailedTasksCommand; use SchedulerBundle\Command\ListTasksCommand; use SchedulerBundle\Command\RebootSchedulerCommand; @@ -723,10 +724,24 @@ public function testCommandsAreRegistered(): void self::assertTrue($container->getDefinition(ExecuteTaskCommand::class)->hasTag('container.preload')); self::assertSame(ExecuteTaskCommand::class, $container->getDefinition(ExecuteTaskCommand::class)->getTag('container.preload')[0]['class']); + self::assertTrue($container->hasDefinition(ExportCommand::class)); + self::assertCount(2, $container->getDefinition(ExportCommand::class)->getArguments()); + self::assertInstanceOf(Reference::class, $container->getDefinition(ExportCommand::class)->getArgument(0)); + self::assertSame(ExporterRegistryInterface::class, (string) $container->getDefinition(ExportCommand::class)->getArgument(0)); + self::assertSame(ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $container->getDefinition(ExportCommand::class)->getArgument(0)->getInvalidBehavior()); + self::assertInstanceOf(Reference::class, $container->getDefinition(ExportCommand::class)->getArgument(1)); + self::assertSame(SchedulerInterface::class, (string) $container->getDefinition(ExportCommand::class)->getArgument(1)); + self::assertSame(ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE, $container->getDefinition(ExportCommand::class)->getArgument(1)->getInvalidBehavior()); + self::assertCount(2, $container->getDefinition(ExportCommand::class)->getTags()); + self::assertTrue($container->getDefinition(ExportCommand::class)->hasTag('console.command')); + self::assertTrue($container->getDefinition(ExportCommand::class)->hasTag('container.preload')); + self::assertSame(ExportCommand::class, $container->getDefinition(ExportCommand::class)->getTag('container.preload')[0]['class']); + self::assertTrue($container->hasDefinition(ListFailedTasksCommand::class)); self::assertCount(1, $container->getDefinition(ListFailedTasksCommand::class)->getArguments()); self::assertInstanceOf(Reference::class, $container->getDefinition(ListFailedTasksCommand::class)->getArgument(0)); self::assertSame(WorkerInterface::class, (string) $container->getDefinition(ListFailedTasksCommand::class)->getArgument(0)); + self::assertCount(2, $container->getDefinition(ListFailedTasksCommand::class)->getTags()); self::assertTrue($container->getDefinition(ListFailedTasksCommand::class)->hasTag('console.command')); self::assertTrue($container->getDefinition(ListFailedTasksCommand::class)->hasTag('container.preload')); self::assertSame(ListFailedTasksCommand::class, $container->getDefinition(ListFailedTasksCommand::class)->getTag('container.preload')[0]['class']); @@ -735,6 +750,7 @@ public function testCommandsAreRegistered(): void self::assertCount(1, $container->getDefinition(ListTasksCommand::class)->getArguments()); self::assertInstanceOf(Reference::class, $container->getDefinition(ListTasksCommand::class)->getArgument(0)); self::assertSame(SchedulerInterface::class, (string) $container->getDefinition(ListTasksCommand::class)->getArgument(0)); + self::assertCount(2, $container->getDefinition(ListTasksCommand::class)->getTags()); self::assertTrue($container->getDefinition(ListTasksCommand::class)->hasTag('console.command')); self::assertTrue($container->getDefinition(ListTasksCommand::class)->hasTag('container.preload')); self::assertSame(ListTasksCommand::class, $container->getDefinition(ListTasksCommand::class)->getTag('container.preload')[0]['class']); @@ -749,6 +765,7 @@ public function testCommandsAreRegistered(): void self::assertSame(EventDispatcherInterface::class, (string) $container->getDefinition(RebootSchedulerCommand::class)->getArgument(2)); self::assertInstanceOf(Reference::class, $container->getDefinition(RebootSchedulerCommand::class)->getArgument(3)); self::assertSame(LoggerInterface::class, (string) $container->getDefinition(RebootSchedulerCommand::class)->getArgument(3)); + self::assertCount(3, $container->getDefinition(RebootSchedulerCommand::class)->getTags()); self::assertTrue($container->getDefinition(RebootSchedulerCommand::class)->hasTag('console.command')); self::assertTrue($container->getDefinition(RebootSchedulerCommand::class)->hasTag('monolog.logger')); self::assertSame('scheduler', $container->getDefinition(RebootSchedulerCommand::class)->getTag('monolog.logger')[0]['channel']); From 0ed9dce80c93446b9d6531eaade8145a7a9dda7c Mon Sep 17 00:00:00 2001 From: Loulier Guillaume Date: Fri, 15 Oct 2021 19:20:29 +0200 Subject: [PATCH 4/5] refactor(command): option moved to argument --- src/Command/ExportCommand.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Command/ExportCommand.php b/src/Command/ExportCommand.php index 769ee442..bd944be6 100644 --- a/src/Command/ExportCommand.php +++ b/src/Command/ExportCommand.php @@ -8,6 +8,7 @@ use SchedulerBundle\SchedulerInterface; use SchedulerBundle\Task\TaskInterface; 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; @@ -42,8 +43,8 @@ protected function configure(): void $this ->setDescription('Export tasks to a specific format') ->setDefinition([ - new InputOption('format', null, InputOption::VALUE_OPTIONAL, 'The format used to export tasks', 'crontab'), - new InputOption('filename', null, InputOption::VALUE_OPTIONAL, 'The name of the filename used to export tasks', 'crontab'), + new InputArgument('format', InputArgument::REQUIRED, 'The format used to export tasks', 'crontab'), + new InputOption('filename', null, InputOption::VALUE_OPTIONAL, 'The name of the filename used to export tasks', '/etc/cron.d'), ]) ; } @@ -64,7 +65,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $filename = $input->getOption('filename'); - $exporter = $this->exporterRegistry->find($input->getOption('format')); + $exporter = $this->exporterRegistry->find($input->getArgument('format')); $tasks->walk(function (TaskInterface $task) use ($exporter, $filename): void { $exporter->export($filename, $task); From c7875210615d6ec0e43d196b60b54a3da8d30476 Mon Sep 17 00:00:00 2001 From: Loulier Guillaume Date: Sun, 24 Oct 2021 16:48:39 +0200 Subject: [PATCH 5/5] tests(export): progress --- .gitignore | 1 + src/Command/ExportCommand.php | 2 +- .../SchedulerBundleExtension.php | 3 ++ src/Export/AbstractExporter.php | 23 ++++++++++ src/Export/CronTabExporter.php | 22 ++++++++-- .../SchedulerBundleExtensionTest.php | 5 ++- tests/Export/CronTabExporterTest.php | 44 +++++++++++++++++++ tests/Export/ExporterRegistryTest.php | 12 ++--- tests/Export/assets/.gitkeep | 0 9 files changed, 101 insertions(+), 11 deletions(-) create mode 100644 src/Export/AbstractExporter.php create mode 100644 tests/Export/assets/.gitkeep diff --git a/.gitignore b/.gitignore index 4fd8718f..0e93efa8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ summary.log .php-cs-fixer.cache # Tests +tests/**/assets/ .phpunit.result.cache tests/.assets/**/*.json diff --git a/src/Command/ExportCommand.php b/src/Command/ExportCommand.php index bd944be6..c80a1c85 100644 --- a/src/Command/ExportCommand.php +++ b/src/Command/ExportCommand.php @@ -72,7 +72,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int }); } catch (Throwable $throwable) { $style->error([ - 'An error occurred when exporting tasks', + 'An error occurred when exporting tasks:', $throwable->getMessage(), ]); diff --git a/src/DependencyInjection/SchedulerBundleExtension.php b/src/DependencyInjection/SchedulerBundleExtension.php index 0381395e..2efd0723 100644 --- a/src/DependencyInjection/SchedulerBundleExtension.php +++ b/src/DependencyInjection/SchedulerBundleExtension.php @@ -993,6 +993,9 @@ public function registerExportTools(ContainerBuilder $container): void $container->setAlias(ExporterRegistryInterface::class, ExporterRegistry::class); $container->register(CronTabExporter::class, CronTabExporter::class) + ->setArguments([ + $container->getParameter('kernel.project_dir'), + ]) ->addTag(self::TASK_EXPORTER_TAG) ->addTag('container.preload', [ 'class' => CronTabExporter::class, diff --git a/src/Export/AbstractExporter.php b/src/Export/AbstractExporter.php new file mode 100644 index 00000000..e1cf1d44 --- /dev/null +++ b/src/Export/AbstractExporter.php @@ -0,0 +1,23 @@ + + */ +abstract class AbstractExporter implements ExporterInterface +{ + private string $projectDir; + + public function __construct(string $projectDir) + { + $this->projectDir = $projectDir; + } + + protected function getProjectDir(): string + { + return $this->projectDir; + } +} diff --git a/src/Export/CronTabExporter.php b/src/Export/CronTabExporter.php index 19e1d549..076f82d0 100644 --- a/src/Export/CronTabExporter.php +++ b/src/Export/CronTabExporter.php @@ -5,18 +5,34 @@ namespace SchedulerBundle\Export; use SchedulerBundle\Task\TaskInterface; -use function file_exists; +use Symfony\Component\Filesystem\Filesystem; +use function sprintf; /** * @author Guillaume Loulier */ -final class CronTabExporter implements ExporterInterface +final class CronTabExporter extends AbstractExporter { + /** + * {@inheritdoc} + */ public function export(string $filename, TaskInterface $task): void { - if (file_exists($filename)) { + $finalFilename = sprintf('%s/%s', $this->getProjectDir(), $task->getName()); + + $fs = new Filesystem(); + + if ($fs->exists($finalFilename)) { return; } + + $fs->touch($finalFilename); + $fs->dumpFile($finalFilename, sprintf( + '%s cd %s && php bin/console scheduler:execute --name %s', + $task->getExpression(), + $this->getProjectDir(), + $task->getName(), + )); } /** diff --git a/tests/DependencyInjection/SchedulerBundleExtensionTest.php b/tests/DependencyInjection/SchedulerBundleExtensionTest.php index 55537c0f..0aebf976 100644 --- a/tests/DependencyInjection/SchedulerBundleExtensionTest.php +++ b/tests/DependencyInjection/SchedulerBundleExtensionTest.php @@ -465,6 +465,7 @@ public function testTransportIsRegistered(): void $schedulerBundleExtension = new SchedulerBundleExtension(); $containerBuilder = new ContainerBuilder(); + $containerBuilder->setParameter('kernel.project_dir', 'foo'); $containerBuilder->register(SerializerInterface::class, SerializerInterface::class); $schedulerBundleExtension->load([ @@ -1116,7 +1117,8 @@ public function testExportToolsAreRegistered(): void self::assertSame(ExporterRegistry::class, $container->getDefinition(ExporterRegistry::class)->getTag('container.preload')[0]['class']); self::assertTrue($container->hasDefinition(CronTabExporter::class)); - self::assertCount(0, $container->getDefinition(CronTabExporter::class)->getArguments()); + self::assertCount(1, $container->getDefinition(CronTabExporter::class)->getArguments()); + self::assertSame('foo', $container->getDefinition(CronTabExporter::class)->getArgument(0)); self::assertTrue($container->getDefinition(CronTabExporter::class)->hasTag('scheduler.task_exporter')); self::assertTrue($container->getDefinition(CronTabExporter::class)->hasTag('container.preload')); self::assertSame(CronTabExporter::class, $container->getDefinition(CronTabExporter::class)->getTag('container.preload')[0]['class']); @@ -2315,6 +2317,7 @@ public function provideDoctrineDsn(): Generator private function getContainer(array $configuration = [], Closure $extraDefinitions = null, Closure $extraPasses = null): ContainerBuilder { $containerBuilder = new ContainerBuilder(); + $containerBuilder->setParameter('kernel.project_dir', 'foo'); $containerBuilder->registerExtension(new SchedulerBundleExtension()); $containerBuilder->loadFromExtension('scheduler_bundle', $configuration); diff --git a/tests/Export/CronTabExporterTest.php b/tests/Export/CronTabExporterTest.php index dc67c765..6921857c 100644 --- a/tests/Export/CronTabExporterTest.php +++ b/tests/Export/CronTabExporterTest.php @@ -5,10 +5,54 @@ namespace Tests\SchedulerBundle\Export; use PHPUnit\Framework\TestCase; +use SchedulerBundle\Export\CronTabExporter; +use SchedulerBundle\Task\NullTask; +use function file_get_contents; +use function sprintf; +use function unlink; /** * @author Guillaume Loulier */ final class CronTabExporterTest extends TestCase { + public function testExporterSupport(): void + { + $exporter = new CronTabExporter(__DIR__); + + self::assertFalse($exporter->support('cli')); + self::assertTrue($exporter->support('crontab')); + } + + public function testExporterCannotExportExistingFile(): void + { + $exporter = new CronTabExporter(__DIR__.'/assets'); + + self::assertFileExists(sprintf('%s/assets/foo', __DIR__)); + + $exporter->export('foo', new NullTask('foo')); + + self::assertFileExists(sprintf('%s/assets/foo', __DIR__)); + } + + public function testExporterCanExportUndefinedFile(): void + { + unlink(__DIR__.'/assets/bar'); + + $task = new NullTask('bar'); + + $exporter = new CronTabExporter(__DIR__.'/assets'); + + self::assertFileDoesNotExist(sprintf('%s/bar', __DIR__.'/assets')); + + $exporter->export('bar', $task); + + self::assertFileExists(sprintf('%s/bar', __DIR__.'/assets')); + self::assertSame(sprintf( + '%s cd %s && php bin/console scheduler:execute --name %s', + $task->getExpression(), + __DIR__.'/assets', + $task->getName(), + ), file_get_contents(__DIR__.'/assets/bar')); + } } diff --git a/tests/Export/ExporterRegistryTest.php b/tests/Export/ExporterRegistryTest.php index 8101f210..67ae33f8 100644 --- a/tests/Export/ExporterRegistryTest.php +++ b/tests/Export/ExporterRegistryTest.php @@ -25,7 +25,7 @@ public function testRegistryCanReturnEmptyExporterList(): void public function testRegistryCanFilterExporterList(): void { $registry = new ExporterRegistry([ - new CronTabExporter(), + new CronTabExporter(__DIR__), ]); $registry->filter(static fn (ExporterInterface $exporter): bool => $exporter instanceof CronTabExporter); @@ -35,7 +35,7 @@ public function testRegistryCanFilterExporterList(): void public function testRegistryCannotFindExporterWithEmptyList(): void { $registry = new ExporterRegistry([ - new CronTabExporter(), + new CronTabExporter(__DIR__), ]); self::expectException(InvalidArgumentException::class); @@ -47,8 +47,8 @@ public function testRegistryCannotFindExporterWithEmptyList(): void public function testRegistryCannotFindExporterWithMultipleSupportingExporter(): void { $registry = new ExporterRegistry([ - new CronTabExporter(), - new CronTabExporter(), + new CronTabExporter(__DIR__), + new CronTabExporter(__DIR__), ]); self::expectException(InvalidArgumentException::class); @@ -60,7 +60,7 @@ public function testRegistryCannotFindExporterWithMultipleSupportingExporter(): public function testRegistryCanFindExporter(): void { $registry = new ExporterRegistry([ - new CronTabExporter(), + new CronTabExporter(__DIR__), ]); $exporter = $registry->find('crontab'); @@ -70,7 +70,7 @@ public function testRegistryCanFindExporter(): void public function testRegistryCanReturnCurrentExporter(): void { $registry = new ExporterRegistry([ - new CronTabExporter(), + new CronTabExporter(__DIR__), ]); $exporter = $registry->current(); diff --git a/tests/Export/assets/.gitkeep b/tests/Export/assets/.gitkeep new file mode 100644 index 00000000..e69de29b