Skip to content

Commit 00ea597

Browse files
authored
Allow configuration of job launcher with Symfony configuration (#121)
* Allow configuration of job launcher with Symfony configuration * Fixed code style * Fixed static analysis and extract job launcher definition factory * Add symfony/messenger as suggested dependency of yokai/batch-symfony-framework * Added documentation about job launcher DSN
1 parent b333150 commit 00ea597

File tree

13 files changed

+326
-90
lines changed

13 files changed

+326
-90
lines changed

src/batch-symfony-framework/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"suggest": {
3939
"sonata-project/admin-bundle": "If you want a SonataAdmin like rendering in the user interface",
4040
"symfony/form": "If you want the JobExecution form filter in the user interface",
41+
"symfony/messenger": "If you to launch jobs via messenger",
4142
"symfony/security-bundle": "If you want to secure the access to JobExecution in the user interface",
4243
"symfony/translation": "Required if you want to enable the user interface",
4344
"symfony/twig-bundle": "Required if you want to enable the user interface"

src/batch-symfony-framework/docs/getting-started.md

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,57 @@ return [
1010
];
1111
```
1212

13-
There is few things that can be configured in the bundle at the moment.
14-
But the most important one will be the `JobExecution` storage:
13+
### Job launcher
14+
15+
You can use many different job launcher in your application, you will be able to register these using configuration:
16+
17+
```yaml
18+
# config/packages/yokai_batch.yaml
19+
yokai_batch:
20+
launcher:
21+
default: simple
22+
launchers:
23+
simple: ...
24+
async: ...
25+
```
26+
27+
> **note**: if you do not configure anything here, you will be using the [`SimpleJobLauncher`](https://github.com/yokai-php/batch/blob/0.x/src/Launcher/SimpleJobLauncher.php).
28+
29+
The `default` job launcher, must reference a launcher name, defined in the `launchers` list.
30+
The `default` job launcher will be the autowired instance of job launcher when you ask for one.
31+
All `launchers` will be registered as a service, and an autowire named alias will be configured for it.
32+
For instance, in the example below, you will be able to register all these launchers like this:
33+
34+
```php
35+
<?php
36+
37+
use Yokai\Batch\Launcher\JobLauncherInterface;
38+
39+
final class YourAppCode
40+
{
41+
public function __construct(
42+
private JobLauncherInterface $jobLauncher, // will inject the default job launcher
43+
private JobLauncherInterface $simpleJobLauncher, // will inject the "simple" job launcher
44+
private JobLauncherInterface $messengerJobLauncher, // will inject the "messenger" job launcher
45+
) {
46+
}
47+
}
48+
```
49+
50+
All `launchers` are configured using a DSN, every scheme has it's own associated factory.
51+
- `simple://simple`: a [`SimpleJobLauncher`](https://github.com/yokai-php/batch/blob/0.x/src/Launcher/SimpleJobLauncher.php), no configuration allowed
52+
- `messenger://messenger`: a [`DispatchMessageJobLauncher`](https://github.com/yokai-php/batch-symfony-messenger/blob/0.x/src/DispatchMessageJobLauncher.php), no configuration allowed
53+
- `console://console`: a [`RunCommandJobLauncher`](https://github.com/yokai-php/batch-symfony-console/blob/0.x/src/RunCommandJobLauncher.php), configurable options:
54+
- `log`: the filename where command output will be redirected (defaults to `batch_execute.log`)
55+
- `service://service`: pointing to a service of your choice, configurable options:
56+
- `service`: the id of the service to use (required, an exception will be thrown otherwise)
57+
58+
### JobExecution storage
59+
60+
You can have only one storage for your `JobExecution`, and you have several options:
1561
- `filesystem` will create a file for each `JobExecution` in `%kernel.project_dir%/var/batch/{execution.jobName}/{execution.id}.json`
1662
- `dbal` will create a row in a table for each `JobExecution`
63+
- `service` will use a service you have defined in your application
1764

1865
```yaml
1966
# config/packages/yokai_batch.yaml
@@ -22,12 +69,16 @@ yokai_batch:
2269
filesystem: ~
2370
# Or with yokai/batch-doctrine-dbal (& doctrine/dbal)
2471
# dbal: ~
72+
# Or with a service of yours
73+
# service: ~
2574
```
2675

2776
> **note**: the default storage is `filesystem`, because it only requires a writeable filesystem.
2877
> But if you already have `doctrine/dbal` in your project, it is highly recommended to use it instead.
2978
> Because querying `JobExecution` in a filesystem might be slow, specially if you are planing to add UIs on top.
3079

80+
### Job as a service
81+
3182
As Symfony supports registering all classes in `src/` as a service,
3283
we can leverage this behaviour to register all jobs in `src/`.
3384
We will add a tag to every found class in `src/` that implements `Yokai\Batch\Job\JobInterface`:
@@ -88,13 +139,13 @@ use Yokai\Batch\Storage\JobExecutionStorageInterface;
88139
final class MyClass
89140
{
90141
public function __construct(
91-
private JobLauncherInterface $executionStorage,
142+
private JobLauncherInterface $jobLauncher,
92143
) {
93144
}
94145
95146
public function method(): void
96147
{
97-
$this->launcher->launch('job.import_users');
148+
$this->jobLauncher->launch('job.name');
98149
}
99150
}
100151
```
@@ -122,14 +173,13 @@ final readonly class YourService
122173
private LoggerInterface $yokaiBatchLogger,
123174
) {
124175
}
125-
176+
126177
public function method()
127178
{
128179
$this->yokaiBatchLogger->error(...);
129180
}
130181
}
131182
```
132-
```
133183

134184
## On the same subject
135185

src/batch-symfony-framework/src/DependencyInjection/Configuration.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*
1414
* @phpstan-type Config array{
1515
* storage: StorageConfig,
16+
* launcher: LauncherConfig,
1617
* ui: UserInterfaceConfig,
1718
* }
1819
* @phpstan-type StorageConfig array{
@@ -26,6 +27,10 @@
2627
* dir: string,
2728
* },
2829
* }
30+
* @phpstan-type LauncherConfig array{
31+
* default: string|null,
32+
* launchers: array<string, string>,
33+
* }
2934
* @phpstan-type UserInterfaceConfig array{
3035
* enabled: bool,
3136
* security: array{
@@ -53,6 +58,7 @@ public function getConfigTreeBuilder(): TreeBuilder
5358
$root
5459
->children()
5560
->append($this->storage())
61+
->append($this->launcher())
5662
->append($this->ui())
5763
->end()
5864
;
@@ -97,6 +103,34 @@ private function storage(): ArrayNodeDefinition
97103
return $node;
98104
}
99105

106+
private function launcher(): ArrayNodeDefinition
107+
{
108+
/** @var ArrayNodeDefinition $node */
109+
$node = (new TreeBuilder('launcher'))->getRootNode();
110+
111+
$isInvalidDsn = fn(string $value) => \parse_url($value) === false
112+
|| (\parse_url($value)['scheme'] ?? null) === null;
113+
114+
$node
115+
->addDefaultsIfNotSet()
116+
->children()
117+
->scalarNode('default')
118+
->defaultValue('simple')
119+
->end()
120+
->arrayNode('launchers')
121+
->defaultValue(['simple' => 'simple://simple'])
122+
->useAttributeAsKey('name')
123+
->scalarPrototype()
124+
->validate()
125+
->ifTrue($isInvalidDsn)->thenInvalid('Invalid job launcher DSN.')
126+
->end()
127+
->end()
128+
->end()
129+
;
130+
131+
return $node;
132+
}
133+
100134
private function ui(): ArrayNodeDefinition
101135
{
102136
/** @var ArrayNodeDefinition $node */
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yokai\Batch\Bridge\Symfony\Framework\DependencyInjection;
6+
7+
use Symfony\Component\DependencyInjection\ContainerBuilder;
8+
use Symfony\Component\DependencyInjection\Definition;
9+
use Symfony\Component\DependencyInjection\Exception\LogicException;
10+
use Symfony\Component\DependencyInjection\Reference;
11+
use Symfony\Component\Messenger\MessageBusInterface;
12+
use Yokai\Batch\Bridge\Symfony\Console\CommandRunner;
13+
use Yokai\Batch\Bridge\Symfony\Console\RunCommandJobLauncher;
14+
use Yokai\Batch\Bridge\Symfony\Messenger\DispatchMessageJobLauncher;
15+
use Yokai\Batch\Launcher\JobLauncherInterface;
16+
use Yokai\Batch\Launcher\SimpleJobLauncher;
17+
use Yokai\Batch\Storage\JobExecutionStorageInterface;
18+
19+
/**
20+
* This is a helper for building services definitions of {@see JobLauncherInterface}.
21+
*/
22+
final class JobLauncherDefinitionFactory
23+
{
24+
/**
25+
* Build a service definition from DSN string.
26+
*/
27+
public static function fromDsn(ContainerBuilder $container, string $dsn): Definition
28+
{
29+
$dsnParts = \parse_url($dsn);
30+
$launcherType = $dsnParts['scheme'] ?? null;
31+
\parse_str($dsnParts['query'] ?? '', $launcherConfig);
32+
/** @var array<string, string> $launcherConfig */
33+
34+
return match ($launcherType) {
35+
'simple' => self::simple(),
36+
'console' => self::console($launcherConfig),
37+
'messenger' => self::messenger(),
38+
'service' => self::service($container, $launcherConfig),
39+
default => throw new LogicException('Unsupported job launcher type "' . $launcherType . '".'),
40+
};
41+
}
42+
43+
private static function simple(): Definition
44+
{
45+
return new Definition(SimpleJobLauncher::class, [
46+
'$jobExecutionAccessor' => new Reference('yokai_batch.job_execution_accessor'),
47+
'$jobExecutor' => new Reference('yokai_batch.job_executor'),
48+
]);
49+
}
50+
51+
/**
52+
* @param array<string, string> $config
53+
*/
54+
private static function console(array $config): Definition
55+
{
56+
$log = $config['log'] ?? 'batch_execute.log';
57+
58+
return new Definition(RunCommandJobLauncher::class, [
59+
'$jobExecutionFactory' => new Reference('yokai_batch.job_execution_factory'),
60+
'$commandRunner' => new Definition(CommandRunner::class, [
61+
'$binDir' => '%kernel.project_dir%/bin',
62+
'$logDir' => '%kernel.logs_dir%',
63+
]),
64+
'$jobExecutionStorage' => new Reference(JobExecutionStorageInterface::class),
65+
'$logFilename' => $log,
66+
]);
67+
}
68+
69+
private static function messenger(): Definition
70+
{
71+
return new Definition(DispatchMessageJobLauncher::class, [
72+
'$jobExecutionFactory' => new Reference('yokai_batch.job_execution_factory'),
73+
'$jobExecutionStorage' => new Reference(JobExecutionStorageInterface::class),
74+
'$messageBus' => new Reference(MessageBusInterface::class),
75+
]);
76+
}
77+
78+
/**
79+
* @param array<string, string> $config
80+
*/
81+
private static function service(ContainerBuilder $container, array $config): Definition
82+
{
83+
$service = $config['service'] ?? throw new LogicException(
84+
'Missing "service" parameter to configure the job launcher.',
85+
);
86+
87+
return $container->getDefinition($service);
88+
}
89+
}

src/batch-symfony-framework/src/DependencyInjection/YokaiBatchExtension.php

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
*
3434
* @phpstan-import-type Config from Configuration
3535
* @phpstan-import-type StorageConfig from Configuration
36+
* @phpstan-import-type LauncherConfig from Configuration
3637
* @phpstan-import-type UserInterfaceConfig from Configuration
3738
*/
3839
final class YokaiBatchExtension extends Extension
@@ -62,16 +63,9 @@ public function load(array $configs, ContainerBuilder $container): void
6263
}
6364

6465
$this->configureStorage($container, $config['storage']);
66+
$this->configureLauncher($container, $config['launcher']);
6567
$this->configureUserInterface($container, $loader, $config['ui']);
6668

67-
$launchers = [
68-
'yokai_batch.job_launcher.dispatch_message' => $this->installed('symfony-messenger'),
69-
'yokai_batch.job_launcher.run_command' => $this->installed('symfony-console'),
70-
];
71-
$container->setAlias(
72-
JobLauncherInterface::class,
73-
\array_keys(\array_filter($launchers))[0] ?? 'yokai_batch.job_launcher.simple'
74-
);
7569
$container->registerAliasForArgument('yokai_batch.logger', LoggerInterface::class, 'yokaiBatchLogger');
7670
}
7771

@@ -168,6 +162,33 @@ private function configureStorage(ContainerBuilder $container, array $config): v
168162
}
169163
}
170164

165+
/**
166+
* @param LauncherConfig $config
167+
*/
168+
private function configureLauncher(ContainerBuilder $container, array $config): void
169+
{
170+
if (!isset($config['launchers'][$config['default']])) {
171+
throw new LogicException(\sprintf(
172+
"Default job launcher \"%s\" was not registered in launchers config. Available launchers are %s.",
173+
$config['default'],
174+
\json_encode(\array_keys($config['launchers']), flags: \JSON_THROW_ON_ERROR),
175+
));
176+
}
177+
178+
foreach ($config['launchers'] as $name => $dsn) {
179+
$definition = JobLauncherDefinitionFactory::fromDsn($container, $dsn);
180+
$launcherId = 'yokai_batch.job_launcher.' . $name;
181+
$container->setDefinition($launcherId, $definition);
182+
$parameterName = $name . 'JobLauncher';
183+
$container->registerAliasForArgument($launcherId, LoggerInterface::class, $parameterName);
184+
}
185+
186+
$container->setAlias(
187+
JobLauncherInterface::class,
188+
'yokai_batch.job_launcher.' . $config['default'],
189+
);
190+
}
191+
171192
/**
172193
* @param UserInterfaceConfig $config
173194
*/

src/batch-symfony-framework/src/Resources/services/global/launcher.xml

Lines changed: 0 additions & 16 deletions
This file was deleted.

src/batch-symfony-framework/src/Resources/services/symfony/console/launcher.xml

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/batch-symfony-framework/src/Resources/services/symfony/console/util.xml

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)