diff --git a/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/ConfigureLauncherPass.php b/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/ConfigureLauncherPass.php new file mode 100644 index 00000000..0b28340c --- /dev/null +++ b/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/ConfigureLauncherPass.php @@ -0,0 +1,45 @@ +getAlias(JobLauncherInterface::class); + + try { + $launcherService = $container->findDefinition($templatingActualService); + } catch (ServiceNotFoundException $exception) { + throw new LogicException( + message: \sprintf( + 'Job launcher service "%s" does not exists.', + $templatingActualService, + ), + previous: $exception, + ); + } + + $jobLauncherServiceClass = (string)$launcherService->getClass(); + if (!\is_a($jobLauncherServiceClass, JobLauncherInterface::class, true)) { + throw new LogicException( + \sprintf( + 'Job launcher service "%s" must implements interface "%s".', + $templatingActualService, + JobLauncherInterface::class, + ), + ); + } + } +} diff --git a/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/ConfigureStoragePass.php b/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/ConfigureStoragePass.php new file mode 100644 index 00000000..2f5456d5 --- /dev/null +++ b/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/ConfigureStoragePass.php @@ -0,0 +1,61 @@ +getAlias(JobExecutionStorageInterface::class); + + try { + $jobExecutionStorageService = $container->getDefinition($jobExecutionStorageActualService); + } catch (ServiceNotFoundException $exception) { + throw new LogicException( + message: \sprintf( + 'Job execution storage service "%s" does not exists.', + $jobExecutionStorageActualService, + ), + previous: $exception, + ); + } + + $jobExecutionStorageServiceClass = (string)$jobExecutionStorageService->getClass(); + if (!\is_a($jobExecutionStorageServiceClass, JobExecutionStorageInterface::class, true)) { + throw new LogicException( + \sprintf( + 'Job execution storage service "%s" must implements interface "%s".', + $jobExecutionStorageActualService, + JobExecutionStorageInterface::class, + ), + ); + } + + $optionalInterfaces = [ + ListableJobExecutionStorageInterface::class, + QueryableJobExecutionStorageInterface::class, + ]; + foreach ($optionalInterfaces as $interface) { + if (\is_a($jobExecutionStorageServiceClass, $interface, true)) { + $container + ->setAlias($interface, $jobExecutionStorageActualService) + ->setPublic(true) + ; + } + } + } +} diff --git a/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/ConfigureTemplatingPass.php b/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/ConfigureTemplatingPass.php new file mode 100644 index 00000000..df31c041 --- /dev/null +++ b/src/batch-symfony-framework/src/DependencyInjection/CompilerPass/ConfigureTemplatingPass.php @@ -0,0 +1,45 @@ +getAlias(TemplatingInterface::class); + + try { + $templatingService = $container->findDefinition($templatingActualService); + } catch (ServiceNotFoundException $exception) { + throw new LogicException( + message: \sprintf( + 'UI templating service "%s" does not exists.', + $templatingActualService, + ), + previous: $exception, + ); + } + + $templatingServiceClass = (string)$templatingService->getClass(); + if (!\is_a($templatingServiceClass, TemplatingInterface::class, true)) { + throw new LogicException( + \sprintf( + 'UI templating service "%s" must implements interface "%s".', + $templatingActualService, + TemplatingInterface::class, + ), + ); + } + } +} diff --git a/src/batch-symfony-framework/src/DependencyInjection/JobLauncherDefinitionFactory.php b/src/batch-symfony-framework/src/DependencyInjection/JobLauncherDefinitionFactory.php index a0524fe5..d7185592 100644 --- a/src/batch-symfony-framework/src/DependencyInjection/JobLauncherDefinitionFactory.php +++ b/src/batch-symfony-framework/src/DependencyInjection/JobLauncherDefinitionFactory.php @@ -4,7 +4,6 @@ namespace Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection; -use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Reference; @@ -24,7 +23,7 @@ final class JobLauncherDefinitionFactory /** * Build a service definition from DSN string. */ - public static function fromDsn(ContainerBuilder $container, string $dsn): Definition + public static function fromDsn(string $dsn): Definition|Reference { $dsnParts = \parse_url($dsn); $launcherType = $dsnParts['scheme'] ?? null; @@ -35,7 +34,7 @@ public static function fromDsn(ContainerBuilder $container, string $dsn): Defini 'simple' => self::simple(), 'console' => self::console($launcherConfig), 'messenger' => self::messenger(), - 'service' => self::service($container, $launcherConfig), + 'service' => self::service($launcherConfig), default => throw new LogicException('Unsupported job launcher type "' . $launcherType . '".'), }; } @@ -78,12 +77,12 @@ private static function messenger(): Definition /** * @param array $config */ - private static function service(ContainerBuilder $container, array $config): Definition + private static function service(array $config): Reference { $service = $config['service'] ?? throw new LogicException( 'Missing "service" parameter to configure the job launcher.', ); - return $container->getDefinition($service); + return new Reference($service); } } diff --git a/src/batch-symfony-framework/src/DependencyInjection/YokaiBatchExtension.php b/src/batch-symfony-framework/src/DependencyInjection/YokaiBatchExtension.php index 41a7edbc..a02fc65f 100644 --- a/src/batch-symfony-framework/src/DependencyInjection/YokaiBatchExtension.php +++ b/src/batch-symfony-framework/src/DependencyInjection/YokaiBatchExtension.php @@ -11,8 +11,8 @@ use Symfony\Component\Config\Loader as ConfigLoader; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\LogicException; -use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Loader as DependencyInjectionLoader; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Form\AbstractType; @@ -27,8 +27,6 @@ use Yokai\Batch\Launcher\JobLauncherInterface; use Yokai\Batch\Storage\FilesystemJobExecutionStorage; use Yokai\Batch\Storage\JobExecutionStorageInterface; -use Yokai\Batch\Storage\ListableJobExecutionStorageInterface; -use Yokai\Batch\Storage\QueryableJobExecutionStorageInterface; /** * Dependency injection extension for yokai/batch Symfony Bundle. @@ -123,47 +121,10 @@ private function configureStorage(ContainerBuilder $container, array $config): v $defaultStorage = 'yokai_batch.storage.filesystem'; } - try { - $defaultStorageDefinition = $container->getDefinition($defaultStorage); - } catch (ServiceNotFoundException $exception) { - throw new LogicException( - \sprintf('Configured default job execution storage service "%s" does not exists.', $defaultStorage), - 0, - $exception - ); - } - - $defaultStorageClass = $defaultStorageDefinition->getClass(); - if ($defaultStorageClass === null) { - throw new LogicException( - \sprintf('Job execution storage service "%s", has no class.', $defaultStorage) - ); - } - - $interfaces = [ - JobExecutionStorageInterface::class => true, - ListableJobExecutionStorageInterface::class => false, - QueryableJobExecutionStorageInterface::class => false, - ]; - foreach ($interfaces as $interface => $required) { - if (!\is_a($defaultStorageClass, $interface, true)) { - if ($required) { - throw new LogicException( - \sprintf( - 'Job execution storage service "%s", is of class "%s", and must implements interface "%s".', - $defaultStorage, - $defaultStorageClass, - $interface - ) - ); - } - continue; - } - $container - ->setAlias($interface, $defaultStorage) - ->setPublic(true) - ; - } + $container + ->setAlias(JobExecutionStorageInterface::class, $defaultStorage) + ->setPublic(true) + ; } /** @@ -179,17 +140,24 @@ private function configureLauncher(ContainerBuilder $container, array $config): )); } + $launcherIdPerLauncherName = []; foreach ($config['launchers'] as $name => $dsn) { - $definition = JobLauncherDefinitionFactory::fromDsn($container, $dsn); - $launcherId = 'yokai_batch.job_launcher.' . $name; - $container->setDefinition($launcherId, $definition); + $definitionOrReference = JobLauncherDefinitionFactory::fromDsn($dsn); + if ($definitionOrReference instanceof Definition) { + $launcherId = 'yokai_batch.job_launcher.' . $name; + $container->setDefinition($launcherId, $definitionOrReference); + } else { + $launcherId = (string)$definitionOrReference; + } + + $launcherIdPerLauncherName[$name] = $launcherId; $parameterName = $name . 'JobLauncher'; $container->registerAliasForArgument($launcherId, LoggerInterface::class, $parameterName); } $container->setAlias( JobLauncherInterface::class, - 'yokai_batch.job_launcher.' . $config['default'], + $launcherIdPerLauncherName[$config['default']], ); } @@ -240,25 +208,6 @@ private function configureUserInterface(ContainerBuilder $container, LoaderInter $templating = $config['templating']; if ($templating['service'] !== null) { - try { - $templatingClass = $container->getDefinition($templating['service'])->getClass(); - if ($templatingClass === null || !\is_a($templatingClass, TemplatingInterface::class, true)) { - throw new LogicException( - \sprintf( - 'Configured UI templating service "%s" must implements interface "%s".', - $templating['service'], - TemplatingInterface::class, - ), - ); - } - } catch (ServiceNotFoundException $exception) { - throw new LogicException( - \sprintf('Configured UI templating service "%s" does not exists.', $templating['service']), - 0, - $exception - ); - } - $container->setAlias(TemplatingInterface::class, $templating['service']); } elseif ($templating['prefix'] !== null) { $container->register('yokai_batch.ui.templating', ConfigurableTemplating::class) diff --git a/src/batch-symfony-framework/src/YokaiBatchBundle.php b/src/batch-symfony-framework/src/YokaiBatchBundle.php index 09a7685d..c156e471 100644 --- a/src/batch-symfony-framework/src/YokaiBatchBundle.php +++ b/src/batch-symfony-framework/src/YokaiBatchBundle.php @@ -6,6 +6,9 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection\CompilerPass\ConfigureLauncherPass; +use Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection\CompilerPass\ConfigureStoragePass; +use Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection\CompilerPass\ConfigureTemplatingPass; use Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection\CompilerPass\RegisterJobsCompilerPass; /** @@ -15,6 +18,9 @@ final class YokaiBatchBundle extends Bundle { public function build(ContainerBuilder $container): void { + $container->addCompilerPass(new ConfigureLauncherPass()); + $container->addCompilerPass(new ConfigureStoragePass()); + $container->addCompilerPass(new ConfigureTemplatingPass()); $container->addCompilerPass(new RegisterJobsCompilerPass()); } } diff --git a/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/ConfigureLauncherPassTest.php b/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/ConfigureLauncherPassTest.php new file mode 100644 index 00000000..eb37d851 --- /dev/null +++ b/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/ConfigureLauncherPassTest.php @@ -0,0 +1,55 @@ +process(function (ContainerBuilder $container) { + $container->register('yokai_batch.launcher.simple', SimpleJobLauncher::class); + $container->setAlias(JobLauncherInterface::class, 'yokai_batch.launcher.simple'); + }); + + self::assertTrue(true, 'No exception was raised'); + } + + public function testMissingService(): void + { + $this->expectExceptionObject( + new LogicException('Job launcher service "app.yokai_batch_job_launcher" does not exists.') + ); + $this->process(function (ContainerBuilder $container) { + $container->setAlias(JobLauncherInterface::class, 'app.yokai_batch_job_launcher'); + }); + } + + public function testInvalidService(): void + { + $this->expectExceptionObject( + new LogicException( + 'Job launcher service "app.yokai_batch_job_launcher" must implements interface "' . JobLauncherInterface::class . '".', + ), + ); + $this->process(function (ContainerBuilder $container) { + $container->register('app.yokai_batch_job_launcher', self::class); + $container->setAlias(JobLauncherInterface::class, 'app.yokai_batch_job_launcher'); + }); + } + + private function process(\Closure $configure): void + { + $container = new ContainerBuilder(); + $configure($container); + (new ConfigureLauncherPass())->process($container); + } +} diff --git a/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/ConfigureStoragePassTest.php b/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/ConfigureStoragePassTest.php new file mode 100644 index 00000000..48a9f946 --- /dev/null +++ b/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/ConfigureStoragePassTest.php @@ -0,0 +1,60 @@ +process(function (ContainerBuilder $container) { + $container->register('yokai_batch.storage.filesystem', FilesystemJobExecutionStorage::class); + $container->setAlias(JobExecutionStorageInterface::class, 'yokai_batch.storage.filesystem'); + }); + + self::assertTrue($container->hasAlias(ListableJobExecutionStorageInterface::class)); + self::assertTrue($container->hasAlias(QueryableJobExecutionStorageInterface::class)); + } + + public function testMissingService(): void + { + $this->expectExceptionObject( + new LogicException('Job execution storage service "app.yokai_batch_storage" does not exists.'), + ); + $this->process(function (ContainerBuilder $container) { + $container->setAlias(JobExecutionStorageInterface::class, 'app.yokai_batch_storage'); + }); + } + + public function testInvalidService(): void + { + $this->expectExceptionObject( + new LogicException( + 'Job execution storage service "app.yokai_batch_storage" must implements interface "' . JobExecutionStorageInterface::class . '".', + ), + ); + $this->process(function (ContainerBuilder $container) { + $container->register('app.yokai_batch_storage', self::class); + $container->setAlias(JobExecutionStorageInterface::class, 'app.yokai_batch_storage'); + }); + } + + private function process(\Closure $configure): ContainerBuilder + { + $container = new ContainerBuilder(); + $configure($container); + (new ConfigureStoragePass())->process($container); + + return $container; + } +} diff --git a/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/ConfigureTemplatingPassTest.php b/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/ConfigureTemplatingPassTest.php new file mode 100644 index 00000000..6c2cca1b --- /dev/null +++ b/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/ConfigureTemplatingPassTest.php @@ -0,0 +1,55 @@ +process(function (ContainerBuilder $container) { + $container->register('yokai_batch.ui.templating', ConfigurableTemplating::class); + $container->setAlias(TemplatingInterface::class, 'yokai_batch.ui.templating'); + }); + + self::assertTrue(true, 'No exception was raised'); + } + + public function testMissingService(): void + { + $this->expectExceptionObject( + new LogicException('UI templating service "app.yokai_batch_templating" does not exists.') + ); + $this->process(function (ContainerBuilder $container) { + $container->setAlias(TemplatingInterface::class, 'app.yokai_batch_templating'); + }); + } + + public function testInvalidService(): void + { + $this->expectExceptionObject( + new LogicException( + 'UI templating service "app.yokai_batch_templating" must implements interface "' . TemplatingInterface::class . '".', + ), + ); + $this->process(function (ContainerBuilder $container) { + $container->register('app.yokai_batch_templating', self::class); + $container->setAlias(TemplatingInterface::class, 'app.yokai_batch_templating'); + }); + } + + private function process(\Closure $configure): void + { + $container = new ContainerBuilder(); + $configure($container); + (new ConfigureTemplatingPass())->process($container); + } +} diff --git a/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/RegisterJobsCompilerPassTest.php b/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/RegisterJobsCompilerPassTest.php new file mode 100644 index 00000000..9a937bae --- /dev/null +++ b/src/batch-symfony-framework/tests/DependencyInjection/CompilerPass/RegisterJobsCompilerPassTest.php @@ -0,0 +1,53 @@ +register('yokai_batch.job_registry', JobRegistry::class) + ->setPublic(true); + + $container->register(DummyJobWithName::class, DummyJobWithName::class) + ->addTag('yokai_batch.job'); + $container->register(DummyJob::class, DummyJob::class) + ->addTag('yokai_batch.job'); + $container->register('job.named.with.service.id', DummyJob::class) + ->addTag('yokai_batch.job'); + $container->register('job.named.with.tag.attribute', DummyJob::class) + ->addTag('yokai_batch.job', ['job' => 'job.name.in.attribute']); + + (new RegisterJobsCompilerPass())->process($container); + $container->compile(); + + self::assertNotNull($this->getJob($container, 'export_orders_job')); + self::assertNotNull($this->getJob($container, DummyJob::class)); + self::assertNotNull($this->getJob($container, 'job.named.with.service.id')); + self::assertNotNull($this->getJob($container, 'job.name.in.attribute')); + } + + private function getJob(ContainerBuilder $container, string $name): JobInterface|null + { + /** @var JobRegistry $registry */ + $registry = $container->get('yokai_batch.job_registry'); + + try { + return $registry->get($name); + } catch (UndefinedJobException) { + return null; + } + } +} diff --git a/src/batch-symfony-framework/tests/DependencyInjection/YokaiBatchExtensionTest.php b/src/batch-symfony-framework/tests/DependencyInjection/YokaiBatchExtensionTest.php index 57d85ed5..003e6c74 100644 --- a/src/batch-symfony-framework/tests/DependencyInjection/YokaiBatchExtensionTest.php +++ b/src/batch-symfony-framework/tests/DependencyInjection/YokaiBatchExtensionTest.php @@ -12,6 +12,7 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; +use Yokai\Batch\Bridge\Doctrine\DBAL\DoctrineDBALJobExecutionStorage; use Yokai\Batch\Bridge\Symfony\Console\RunCommandJobLauncher; use Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection\YokaiBatchExtension; use Yokai\Batch\Bridge\Symfony\Framework\UserInterface\Templating\ConfigurableTemplating; @@ -24,6 +25,7 @@ use Yokai\Batch\Factory\JobExecutionParametersBuilderInterface; use Yokai\Batch\Launcher\JobLauncherInterface; use Yokai\Batch\Launcher\SimpleJobLauncher; +use Yokai\Batch\Storage\FilesystemJobExecutionStorage; use Yokai\Batch\Storage\JobExecutionStorageInterface; use Yokai\Batch\Storage\NullJobExecutionStorage; use Yokai\Batch\Test\Launcher\BufferingJobLauncher; @@ -31,32 +33,108 @@ class YokaiBatchExtensionTest extends TestCase { /** - * @dataProvider configs + * @dataProvider storage */ - public function test( - array $config, - ?callable $configure, - string $storage, - ?string $launcher, - ?callable $templating, - ?array $security - ): void { - $container = new ContainerBuilder(); - if ($configure !== null) { - $configure($container); + public function testStorage(array $config, \Closure|null $configure, string $storage): void + { + $container = $this->createContainer($config, $configure); + + $jobExecutionStorageService = $this->getDefinition($container, JobExecutionStorageInterface::class); + self::assertNotNull($jobExecutionStorageService); + self::assertSame($storage, $jobExecutionStorageService->getClass()); + } + + public function storage(): \Generator + { + yield 'Default config' => [ + [], + null, + FilesystemJobExecutionStorage::class, + ]; + yield 'Filesystem with default values' => [ + ['storage' => ['filesystem' => null]], + null, + FilesystemJobExecutionStorage::class, + ]; + yield 'DBAL with default values' => [ + ['storage' => ['dbal' => null]], + null, + DoctrineDBALJobExecutionStorage::class, + ]; + yield 'Custom service' => [ + ['storage' => ['service' => NullJobExecutionStorage::class]], + fn(ContainerBuilder $container) => $container->register(NullJobExecutionStorage::class), + NullJobExecutionStorage::class, + ]; + } + + /** + * @dataProvider launcher + */ + public function testLauncher(array $config, \Closure|null $configure, array $launchers, string $default): void + { + $container = $this->createContainer($config, $configure); + + self::assertSame($default, (string)$container->getAlias(JobLauncherInterface::class)); + foreach ($launchers as $id => $class) { + self::assertSame($class, $container->getDefinition($id)->getClass()); } + } - (new YokaiBatchExtension())->load([$config], $container); + public function launcher(): \Generator + { + yield 'Default config' => [ + [], + null, + ['yokai_batch.job_launcher.simple' => SimpleJobLauncher::class], + 'yokai_batch.job_launcher.simple', + ]; + yield 'Multiple launchers' => [ + [ + 'launcher' => [ + 'default' => 'messenger', + 'launchers' => [ + 'messenger' => 'messenger://messenger', + 'console' => 'console://console', + ], + ], + ], + null, + [ + 'yokai_batch.job_launcher.messenger' => DispatchMessageJobLauncher::class, + 'yokai_batch.job_launcher.console' => RunCommandJobLauncher::class, + ], + 'yokai_batch.job_launcher.messenger', + ]; + yield 'Service launcher' => [ + [ + 'launcher' => [ + 'default' => 'service', + 'launchers' => [ + 'service' => 'service://service?service=app.job_launcher', + ], + ], + ], + fn(ContainerBuilder $container) => $container->register( + 'app.job_launcher', + BufferingJobLauncher::class, + ), + ['app.job_launcher' => BufferingJobLauncher::class], + 'app.job_launcher', + ]; + } + + /** + * @dataProvider userInterface + */ + public function testUserInterface( + array $config, + \Closure|null $configure, + \Closure|null $templating, + array|null $security, + ): void { + $container = $this->createContainer($config, $configure); - $launcherActualService = (string)$container->getAlias(JobLauncherInterface::class); - self::assertSame( - $launcher ?? SimpleJobLauncher::class, - $container->getDefinition($launcherActualService)->getClass() - ); - self::assertSame( - $storage, - (string)$container->getAlias(JobExecutionStorageInterface::class) - ); if ($templating === null && $security === null) { self::assertFalse($container->hasAlias(TemplatingInterface::class)); self::assertFalse($container->hasParameter('yokai_batch.ui.security_list_attribute')); @@ -69,65 +147,34 @@ public function test( self::assertSame( $security['list'], - (string)$container->getParameter('yokai_batch.ui.security_list_attribute') + (string)$container->getParameter('yokai_batch.ui.security_list_attribute'), ); self::assertSame( $security['view'], - (string)$container->getParameter('yokai_batch.ui.security_view_attribute') + (string)$container->getParameter('yokai_batch.ui.security_view_attribute'), ); self::assertSame( $security['traces'], - (string)$container->getParameter('yokai_batch.ui.security_traces_attribute') + (string)$container->getParameter('yokai_batch.ui.security_traces_attribute'), ); self::assertSame( $security['logs'], - (string)$container->getParameter('yokai_batch.ui.security_logs_attribute') + (string)$container->getParameter('yokai_batch.ui.security_logs_attribute'), ); } } - public function configs(): \Generator + public function userInterface(): \Generator { - yield [ + yield 'Default config' => [ [], null, - 'yokai_batch.storage.filesystem', - null, null, null, ]; - yield [ - ['storage' => ['filesystem' => null]], - null, - 'yokai_batch.storage.filesystem', - null, - null, - null, - ]; - yield [ - ['storage' => ['dbal' => null]], - null, - 'yokai_batch.storage.dbal', - null, - null, - null, - ]; - yield [ - ['storage' => ['service' => 'app.yokai_batch.storage']], - fn(ContainerBuilder $container) => $container->register( - 'app.yokai_batch.storage', - NullJobExecutionStorage::class, - ), - 'app.yokai_batch.storage', - null, - null, - null, - ]; - yield [ + yield 'Default when enabled' => [ ['ui' => ['enabled' => true]], null, - 'yokai_batch.storage.filesystem', - null, function (Definition $templating) { self::assertSame(ConfigurableTemplating::class, $templating->getClass()); self::assertSame('@YokaiBatch/bootstrap4', $templating->getArgument(0)); @@ -140,11 +187,9 @@ function (Definition $templating) { 'logs' => 'IS_AUTHENTICATED', ], ]; - yield [ + yield 'Bootstrap4 shortcut' => [ ['ui' => ['enabled' => true, 'templating' => 'bootstrap4']], null, - 'yokai_batch.storage.filesystem', - null, function (Definition $templating) { self::assertSame(ConfigurableTemplating::class, $templating->getClass()); self::assertSame('@YokaiBatch/bootstrap4', $templating->getArgument(0)); @@ -157,7 +202,7 @@ function (Definition $templating) { 'logs' => 'IS_AUTHENTICATED', ], ]; - yield [ + yield 'Custom prefix & base template' => [ [ 'ui' => [ 'enabled' => true, @@ -165,8 +210,6 @@ function (Definition $templating) { ], ], null, - 'yokai_batch.storage.filesystem', - null, function (Definition $templating) { self::assertSame(ConfigurableTemplating::class, $templating->getClass()); self::assertSame('yokai-batch/tailwind', $templating->getArgument(0)); @@ -179,14 +222,12 @@ function (Definition $templating) { 'logs' => 'IS_AUTHENTICATED', ], ]; - yield [ + yield 'Custom service' => [ ['ui' => ['enabled' => true, 'templating' => ['service' => 'app.yokai_batch_templating']]], fn(ContainerBuilder $container) => $container->register( 'app.yokai_batch_templating', ConfigurableTemplating::class, ), - 'yokai_batch.storage.filesystem', - null, function (Definition $templating, string $id) { self::assertSame($id, 'app.yokai_batch_templating'); }, @@ -197,7 +238,7 @@ function (Definition $templating, string $id) { 'logs' => 'IS_AUTHENTICATED', ], ]; - yield [ + yield 'Sonata with custom roles' => [ [ 'ui' => [ 'enabled' => true, @@ -213,8 +254,6 @@ function (Definition $templating, string $id) { ], ], null, - 'yokai_batch.storage.filesystem', - null, function (Definition $templating) { self::assertSame(SonataAdminTemplating::class, $templating->getClass()); }, @@ -225,172 +264,6 @@ function (Definition $templating) { 'logs' => 'ROLE_SUPERADMIN', ], ]; - yield [ - [ - 'launcher' => [ - 'default' => 'simple', - 'launchers' => [ - 'simple' => 'simple://simple', - ], - ], - ], - null, - 'yokai_batch.storage.filesystem', - SimpleJobLauncher::class, - null, - null, - ]; - yield [ - [ - 'launcher' => [ - 'default' => 'console', - 'launchers' => [ - 'console' => 'console://console', - ], - ], - ], - null, - 'yokai_batch.storage.filesystem', - RunCommandJobLauncher::class, - null, - null, - ]; - yield [ - [ - 'launcher' => [ - 'default' => 'messenger', - 'launchers' => [ - 'messenger' => 'messenger://messenger', - ], - ], - ], - null, - 'yokai_batch.storage.filesystem', - DispatchMessageJobLauncher::class, - null, - null, - ]; - yield [ - [ - 'launcher' => [ - 'default' => 'service', - 'launchers' => [ - 'service' => 'service://service?service=app.job_launcher', - ], - ], - ], - fn(ContainerBuilder $container) => $container->register( - 'app.job_launcher', - BufferingJobLauncher::class, - ), - 'yokai_batch.storage.filesystem', - BufferingJobLauncher::class, - null, - null, - ]; - } - - /** - * @dataProvider errors - */ - public function testErrors(array $config, ?callable $configure, Exception $error): void - { - $this->expectExceptionObject($error); - - $container = new ContainerBuilder(); - if ($configure !== null) { - $configure($container); - } - - (new YokaiBatchExtension())->load([$config], $container); - } - - public function errors(): \Generator - { - yield 'Storage : Unknown service' => [ - ['storage' => ['service' => 'unknown.service']], - null, - new LogicException('Configured default job execution storage service "unknown.service" does not exists.'), - ]; - yield 'Storage : Service with no class' => [ - ['storage' => ['service' => 'service.with.no.class']], - fn(ContainerBuilder $container) => $container->register('service.with.no.class'), - new LogicException('Job execution storage service "service.with.no.class", has no class.'), - ]; - yield 'Storage : Service without required interface' => [ - ['storage' => ['service' => 'service.without.required.interface']], - fn(ContainerBuilder $container) => $container->register('service.without.required.interface', __CLASS__), - new LogicException( - 'Job execution storage service "service.without.required.interface",' . - ' is of class' . - ' "Yokai\Batch\Tests\Bridge\Symfony\Framework\DependencyInjection\YokaiBatchExtensionTest",' . - ' and must implements interface "Yokai\Batch\Storage\JobExecutionStorageInterface".' - ), - ]; - yield 'Templating : Not configured' => [ - ['ui' => ['enabled' => true, 'templating' => []]], - null, - new \InvalidArgumentException('You must either configure "service" or "prefix".'), - ]; - yield 'Templating : Both configured' => [ - ['ui' => ['enabled' => true, 'templating' => ['service' => 'service.id', 'prefix' => 'prefix/']]], - null, - new \InvalidArgumentException('You cannot configure "service" and "prefix" at the same time.'), - ]; - yield 'Templating : Unknown service' => [ - ['ui' => ['enabled' => true, 'templating' => ['service' => 'unknown.service']]], - null, - new LogicException('Configured UI templating service "unknown.service" does not exists.'), - ]; - yield 'Templating : Service with no class' => [ - ['ui' => ['enabled' => true, 'templating' => ['service' => 'service.with.no.class']]], - fn(ContainerBuilder $container) => $container->register('service.with.no.class'), - new LogicException( - 'Configured UI templating service "service.with.no.class" ' . - 'must implements interface' . - ' "Yokai\Batch\Bridge\Symfony\Framework\UserInterface\Templating\TemplatingInterface".', - ), - ]; - yield 'Templating : Service without required interface' => [ - ['ui' => ['enabled' => true, 'templating' => ['service' => 'service.without.required.interface']]], - fn(ContainerBuilder $container) => $container->register('service.without.required.interface', __CLASS__), - new LogicException( - 'Configured UI templating service "service.without.required.interface" ' . - 'must implements interface' . - ' "Yokai\Batch\Bridge\Symfony\Framework\UserInterface\Templating\TemplatingInterface".', - ), - ]; - yield 'Job Launcher : Empty DSN' => [ - ['launcher' => ['default' => 'invalid', 'launchers' => ['invalid' => '']]], - null, - new InvalidConfigurationException( - 'Invalid configuration for path "yokai_batch.launcher.launchers.invalid": Invalid job launcher DSN.' - ), - ]; - yield 'Job Launcher : Invalid DSN' => [ - ['launcher' => ['default' => 'invalid', 'launchers' => ['invalid' => 'not a DSN']]], - null, - new InvalidConfigurationException( - 'Invalid configuration for path "yokai_batch.launcher.launchers.invalid": Invalid job launcher DSN.' - ), - ]; - yield 'Job Launcher : Unregistered launcher' => [ - ['launcher' => ['default' => 'unknown', 'launchers' => ['simple' => 'simple://simple']]], - null, - new LogicException( - 'Default job launcher "unknown" was not registered in launchers config. Available launchers are ["simple"].' - ), - ]; - yield 'Job Launcher : Unsupported launcher type' => [ - ['launcher' => ['default' => 'invalid', 'launchers' => ['invalid' => 'unknown://unknown']]], - null, - new LogicException('Unsupported job launcher type "unknown".'), - ]; - yield 'Job Launcher : Unknown service' => [ - ['launcher' => ['default' => 'service', 'launchers' => ['service' => 'service://service?service=app.unknown']]], - null, - new ServiceNotFoundException('app.unknown'), - ]; } /** @@ -450,16 +323,46 @@ public function parameters(): \Generator } /** - * @dataProvider invalidParameters + * @dataProvider errors */ - public function testInvalidParameters(array $config, \Exception $error): void + public function testErrors(array $config, Exception $error): void { $this->expectExceptionObject($error); $this->createContainer($config); } - public function invalidParameters(): \Generator + public function errors(): \Generator { + yield 'Templating : Not configured' => [ + ['ui' => ['enabled' => true, 'templating' => []]], + new \InvalidArgumentException('You must either configure "service" or "prefix".'), + ]; + yield 'Templating : Both configured' => [ + ['ui' => ['enabled' => true, 'templating' => ['service' => 'service.id', 'prefix' => 'prefix/']]], + new \InvalidArgumentException('You cannot configure "service" and "prefix" at the same time.'), + ]; + yield 'Job Launcher : Empty DSN' => [ + ['launcher' => ['default' => 'invalid', 'launchers' => ['invalid' => '']]], + new InvalidConfigurationException( + 'Invalid configuration for path "yokai_batch.launcher.launchers.invalid": Invalid job launcher DSN.', + ), + ]; + yield 'Job Launcher : Invalid DSN' => [ + ['launcher' => ['default' => 'invalid', 'launchers' => ['invalid' => 'not a DSN']]], + new InvalidConfigurationException( + 'Invalid configuration for path "yokai_batch.launcher.launchers.invalid": Invalid job launcher DSN.', + ), + ]; + yield 'Job Launcher : Unregistered launcher' => [ + ['launcher' => ['default' => 'unknown', 'launchers' => ['simple' => 'simple://simple']]], + new LogicException( + 'Default job launcher "unknown" was not registered in launchers config. Available launchers are ["simple"].', + ), + ]; + yield 'Job Launcher : Unsupported launcher type' => [ + ['launcher' => ['default' => 'invalid', 'launchers' => ['invalid' => 'unknown://unknown']]], + new LogicException('Unsupported job launcher type "unknown".'), + ]; yield 'Per job parameters value must be an array' => [ ['parameters' => ['per_job' => ['job.foo' => 'string']]], new InvalidConfigurationException( diff --git a/src/batch-symfony-framework/tests/YokaiBatchBundleTest.php b/src/batch-symfony-framework/tests/YokaiBatchBundleTest.php index 6b6f555c..74513fb1 100644 --- a/src/batch-symfony-framework/tests/YokaiBatchBundleTest.php +++ b/src/batch-symfony-framework/tests/YokaiBatchBundleTest.php @@ -6,36 +6,35 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection\CompilerPass\ConfigureLauncherPass; +use Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection\CompilerPass\ConfigureStoragePass; +use Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection\CompilerPass\ConfigureTemplatingPass; +use Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection\CompilerPass\RegisterJobsCompilerPass; use Yokai\Batch\Bridge\Symfony\Framework\YokaiBatchBundle; -use Yokai\Batch\Job\JobInterface; -use Yokai\Batch\Registry\JobRegistry; -use Yokai\Batch\Tests\Bridge\Symfony\Framework\Fixtures\DummyJob; -use Yokai\Batch\Tests\Bridge\Symfony\Framework\Fixtures\DummyJobWithName; class YokaiBatchBundleTest extends TestCase { public function testBuild(): void { $container = new ContainerBuilder(); - $container->register('yokai_batch.job_registry', JobRegistry::class) - ->setPublic(true); - - $container->register(DummyJobWithName::class, DummyJobWithName::class) - ->addTag('yokai_batch.job'); - $container->register(DummyJob::class, DummyJob::class) - ->addTag('yokai_batch.job'); - $container->register('job.named.with.service.id', DummyJob::class) - ->addTag('yokai_batch.job'); - $container->register('job.named.with.tag.attribute', DummyJob::class) - ->addTag('yokai_batch.job', ['job' => 'job.name.in.attribute']); + $passesBefore = $container->getCompilerPassConfig()->getPasses(); (new YokaiBatchBundle())->build($container); - $container->compile(); - $registry = $container->get('yokai_batch.job_registry'); - self::assertInstanceOf(JobInterface::class, $registry->get('export_orders_job')); - self::assertInstanceOf(JobInterface::class, $registry->get(DummyJob::class)); - self::assertInstanceOf(JobInterface::class, $registry->get('job.named.with.service.id')); - self::assertInstanceOf(JobInterface::class, $registry->get('job.name.in.attribute')); + $passes = []; + foreach ($container->getCompilerPassConfig()->getBeforeOptimizationPasses() as $pass) { + if (!\in_array($pass, $passesBefore, true)) { + $passes[] = $pass::class; + } + } + self::assertSame( + [ + ConfigureLauncherPass::class, + ConfigureStoragePass::class, + ConfigureTemplatingPass::class, + RegisterJobsCompilerPass::class, + ], + $passes, + ); } }