diff --git a/CHANGELOG.md b/CHANGELOG.md index d98af95b..7be9c527 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Latest ------ ## Changes +* [#269](https://github.com/cleverage/process-bundle/issues/269) Composer: `symfony/process`, `symfony/stopwatch`, `symfony/var-dumper` and `symfony/monolog-bundle` are no longer required, they are suggested (applications using them must require them). Without `symfony/process` (`CommandRunnerTask`, `ProcessLauncherTask`) or `symfony/stopwatch` (`StopwatchTask`), the container still compiles and these tasks fail with an explicit message (`UnavailableServicesCompilerPass`); without `symfony/var-dumper`, nothing is dumped (as before); without `symfony/monolog-bundle`, the `cleverage_process*` channels and processors are not registered and the default logger is used. `monolog/monolog` stays required (`Logger\*Processor`). Remove the unused `symfony/monolog-bridge` dependency. `ProcessConfiguration` no longer uses `array_any()` (PHP 8.4), whose polyfill was only installed by the removed packages. Add a `conflict` with `cleverage/doctrine-process-bundle`, `cleverage/soap-process-bundle` and `cleverage/ui-process-bundle` `<3.3`, which use the MonologBundle without requiring it. Update documentation, add tests. * [#91](https://github.com/cleverage/process-bundle/issues/91) Reference a task by a code in the process configurations: new `task` key (`task: transformer`), next to its `options`. The code of a task service is declared with the new `code` attribute of the `cleverage.task` tag or the new `#[AsTask(code: '...')]` attribute (autoconfiguration); every task of the bundle has a code, given on its reference page (snake case of its class name without `Task`, e.g. `transformer`, `constant_output`, `csv_reader`). The codes are resolved when the container is compiled (`TaskLocatorCompilerPass`): an unknown or duplicate code fails the compilation. Referencing a task by its `service` (e.g. `service: '@CleverAge\ProcessBundle\Task\TransformerTask'`) is deprecated (configuration deprecation), it will be removed in v6.0: use `task: `. The documentation examples use the codes. Update documentation, add tests. * [#276](https://github.com/cleverage/process-bundle/issues/276) TransformerRegistry: the transformers are loaded lazily from a service locator indexed by code (new internal `TransformerLocatorCompilerPass`, new optional `TransformerRegistry` constructor argument): a transformer is only instantiated when a process uses it. The code is given by the new `code` attribute of the `cleverage.transformer` tag, or by the new `#[AsTransformer(code: '...')]` attribute (autoconfiguration); it must be the one returned by `getCode()`. All the bundle transformers and the generic transformers have their code. Duplicate codes fail the container compilation. A transformer tagged without `code` is still registered eagerly, with `TransformerRegistry::addTransformer()`, which is deprecated (the `code` will be required in v6.0, #280). `RegistryCompilerPass` is no longer used and deprecated. `TransformerInterface::getCode()` is deprecated (`@deprecated` PHPDoc: no runtime notice is possible for an interface method), it will be removed from the interface in v6.0 (#280): keep implementing it, returning the code of the tag until then. Update documentation, add tests. * [#149](https://github.com/cleverage/process-bundle/issues/149) Apply Symfony best practices to the tasks and transformers services: they are defined explicitly (no more autowiring, autoconfiguration nor bindings in the bundle), keeping their class name as service id. Same arguments, tags and `shared` settings as before (generated from the previously autowired definitions). They stay public, but fetching them directly from the service container (`$container->get()`) is deprecated (`container.private` tag): they will be private in v6.0, use dependency injection (autowiring by class and decoration keep working). Update documentation. diff --git a/composer.json b/composer.json index ccca5e17..5ecb8559 100644 --- a/composer.json +++ b/composer.json @@ -75,16 +75,11 @@ "symfony/finder": "^6.4|^7.4|^8", "symfony/framework-bundle": "^6.4|^7.4|^8", "symfony/http-kernel": "^6.4|^7.4|^8", - "symfony/monolog-bridge": "^6.4|^7.4|^8", - "symfony/monolog-bundle": "^3.11|^4", "symfony/options-resolver": "^6.4|^7.4|^8", - "symfony/process": "^6.4|^7.4|^8", "symfony/property-access": "^6.4|^7.4|^8", "symfony/serializer": "^6.4|^7.4|^8", "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.4|^8", "symfony/validator": "^6.4|^7.4|^8", - "symfony/var-dumper": "^6.4|^7.4|^8", "symfony/yaml": "^6.4|^7.4|^8" }, "require-dev": { @@ -96,9 +91,21 @@ "rector/rector": "*", "roave/security-advisories": "dev-latest", "symfony/cache": "^6.4|^7.4|^8", - "symfony/test-pack": "^1.1" + "symfony/process": "^6.4|^7.4|^8", + "symfony/stopwatch": "^6.4|^7.4|^8", + "symfony/test-pack": "^1.1", + "symfony/var-dumper": "^6.4|^7.4|^8" + }, + "conflict": { + "cleverage/doctrine-process-bundle": "<3.3", + "cleverage/soap-process-bundle": "<3.3", + "cleverage/ui-process-bundle": "<3.3" }, "suggest": { + "symfony/monolog-bundle": "To log in the dedicated cleverage_process, cleverage_process_task and cleverage_process_transformer channels, with the process and task data in the extra of the records", + "symfony/process": "Required by the CommandRunnerTask and the ProcessLauncherTask", + "symfony/stopwatch": "Required by the StopwatchTask", + "symfony/var-dumper": "To dump the values with the DebugTask, the DebugTransformer and the --output-format=dump option of cleverage:process:execute", "cleverage/archive-process-bundle": "Dedicated bundle for Archive dependencies for the Process Bundle", "cleverage/cache-process-bundle": "Dedicated bundle for Cache dependencies for the Process Bundle", "cleverage/doctrine-process-bundle": "Dedicated bundle for Doctrine dependencies for the process bundle", diff --git a/docs/01-quick_start.md b/docs/01-quick_start.md index f83b0a96..a1915fb6 100644 --- a/docs/01-quick_start.md +++ b/docs/01-quick_start.md @@ -53,6 +53,18 @@ framework: enabled: true ``` +Some features rely on optional packages, suggested by the bundle: + +| Package | Needed by | +| ------- | --------- | +| `symfony/process` | the [CommandRunnerTask](reference/tasks/command_runner_task.md) and the [ProcessLauncherTask](reference/tasks/process_launcher_task.md) | +| `symfony/stopwatch` | the [StopwatchTask](reference/tasks/stopwatch_task.md) | +| `symfony/var-dumper` | the dumps of the [DebugTask](reference/tasks/debug_task.md), of the `debug` transformer and of `cleverage:process:execute --output-format=dump` (nothing is dumped without it) | +| `symfony/monolog-bundle` | the `cleverage_process`, `cleverage_process_task` and `cleverage_process_transformer` log channels, with the process and task data in the `extra` of the records (see [logging](03-custom_tasks.md#logging)); without it, the default Symfony logger is used | + +Without `symfony/process` or `symfony/stopwatch`, the container still compiles, and a process using one of these tasks +fails with an explicit message giving the package to install. + ## Global configuration You can use `./bin/console config:dump-reference clever_age_process` to have a summary of current configuration. diff --git a/docs/03-custom_tasks.md b/docs/03-custom_tasks.md index f019c486..3f190222 100644 --- a/docs/03-custom_tasks.md +++ b/docs/03-custom_tasks.md @@ -314,7 +314,9 @@ transform: ## Logging -The bundle declares three [Monolog channels](https://symfony.com/doc/current/logging/channels_handlers.html): +The bundle declares three [Monolog channels](https://symfony.com/doc/current/logging/channels_handlers.html), created +when the MonologBundle is installed (`composer require symfony/monolog-bundle`, otherwise every log goes to the default +Symfony logger, without the `extra` values below): | Channel | Used by | | ------- | ------- | diff --git a/docs/reference/tasks/command_runner_task.md b/docs/reference/tasks/command_runner_task.md index ce2f9047..6298874f 100644 --- a/docs/reference/tasks/command_runner_task.md +++ b/docs/reference/tasks/command_runner_task.md @@ -4,6 +4,8 @@ CommandRunnerTask Runs a system command (with the Symfony Process component) for each input. The input is passed to the command as stdin, and the command standard output becomes the task output. +Requires `symfony/process` (`composer require symfony/process`): without it, the task fails with an explicit message. + Task reference -------------- diff --git a/docs/reference/tasks/process_launcher_task.md b/docs/reference/tasks/process_launcher_task.md index 228e0081..6957c04b 100644 --- a/docs/reference/tasks/process_launcher_task.md +++ b/docs/reference/tasks/process_launcher_task.md @@ -5,6 +5,8 @@ Launches a process in a separate system process (`bin/console cleverage:process: allowing parallelization. The task keeps a pool of at most `max_processes` running sub-processes and waits for a free slot before launching a new one. +Requires `symfony/process` (`composer require symfony/process`): without it, the task fails with an explicit message. + Task reference -------------- diff --git a/docs/reference/tasks/stopwatch_task.md b/docs/reference/tasks/stopwatch_task.md index 764ef9d3..7a5a1b2b 100644 --- a/docs/reference/tasks/stopwatch_task.md +++ b/docs/reference/tasks/stopwatch_task.md @@ -4,6 +4,8 @@ StopwatchTask Logs (at `info` level, on the `cleverage_process_task` channel) every event of the `__root__` section of the Symfony [Stopwatch component](https://symfony.com/doc/current/components/stopwatch.html). Useful to profile a process. +Requires `symfony/stopwatch` (`composer require symfony/stopwatch`): without it, the task fails with an explicit message. + Task reference -------------- diff --git a/src/Configuration/ProcessConfiguration.php b/src/Configuration/ProcessConfiguration.php index 37fe30cd..7c7ce9c7 100644 --- a/src/Configuration/ProcessConfiguration.php +++ b/src/Configuration/ProcessConfiguration.php @@ -146,7 +146,13 @@ public function getDependencyGroups(): array if (null === $this->dependencyGroups) { $this->dependencyGroups = []; foreach ($this->getTaskConfigurations() as $taskConfiguration) { - $isInBranch = array_any($this->dependencyGroups, static fn ($branch) => \in_array($taskConfiguration->getCode(), $branch, true)); + $isInBranch = false; + foreach ($this->dependencyGroups as $branch) { + if (\in_array($taskConfiguration->getCode(), $branch, true)) { + $isInBranch = true; + break; + } + } if (!$isInBranch) { $dependencies = $this->buildDependencies($taskConfiguration); $dependencies = $this->sortDependencies($dependencies); diff --git a/src/DependencyInjection/Compiler/UnavailableServicesCompilerPass.php b/src/DependencyInjection/Compiler/UnavailableServicesCompilerPass.php index dbb9f0ec..5f0244fe 100644 --- a/src/DependencyInjection/Compiler/UnavailableServicesCompilerPass.php +++ b/src/DependencyInjection/Compiler/UnavailableServicesCompilerPass.php @@ -13,6 +13,9 @@ namespace CleverAge\ProcessBundle\DependencyInjection\Compiler; +use CleverAge\ProcessBundle\Task\Debug\StopwatchTask; +use CleverAge\ProcessBundle\Task\Process\CommandRunnerTask; +use CleverAge\ProcessBundle\Task\Process\ProcessLauncherTask; use CleverAge\ProcessBundle\Task\Serialization\DenormalizerTask; use CleverAge\ProcessBundle\Task\Serialization\DeserializerTask; use CleverAge\ProcessBundle\Task\Serialization\NormalizerTask; @@ -26,6 +29,7 @@ use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\Process\Process; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; @@ -35,12 +39,24 @@ * * @internal */ -final class UnavailableServicesCompilerPass implements CompilerPassInterface +final readonly class UnavailableServicesCompilerPass implements CompilerPassInterface { public const SERIALIZER_MESSAGE = 'The "%s" service requires the Symfony serializer, which does not seem enabled: consider enabling framework.serializer (see https://symfony.com/doc/current/reference/configuration/framework.html#reference-serializer-enabled).'; public const VALIDATOR_MESSAGE = 'The "%s" service requires the Symfony validator, which does not seem enabled: consider enabling framework.validation (see https://symfony.com/doc/current/reference/configuration/framework.html#reference-validation-enabled).'; + public const STOPWATCH_MESSAGE = 'The "%s" service requires the Symfony Stopwatch component, which does not seem installed: try "composer require symfony/stopwatch".'; + + public const PROCESS_MESSAGE = 'The "%s" service requires the Symfony Process component, which does not seem installed: try "composer require symfony/process".'; + + /** + * @param (\Closure(class-string): bool)|null $classExists defaults to class_exists() (the tests check the missing packages) + */ + public function __construct( + private ?\Closure $classExists = null, + ) { + } + public function process(ContainerBuilder $container): void { if (!$container->has('serializer') && !$container->has(DenormalizerInterface::class)) { @@ -57,6 +73,15 @@ public function process(ContainerBuilder $container): void if (!$container->has('validator') && !$container->has(ValidatorInterface::class)) { $this->replace($container, self::VALIDATOR_MESSAGE, [ValidatorTask::class]); } + + // The debug.stopwatch service is registered by the FrameworkBundle when symfony/stopwatch is installed + if (!$container->has('debug.stopwatch')) { + $this->replace($container, self::STOPWATCH_MESSAGE, [StopwatchTask::class]); + } + + if (!($this->classExists ?? class_exists(...))(Process::class)) { + $this->replace($container, self::PROCESS_MESSAGE, [CommandRunnerTask::class, ProcessLauncherTask::class]); + } } /** diff --git a/tests/DependencyInjection/Compiler/UnavailableServicesCompilerPassTest.php b/tests/DependencyInjection/Compiler/UnavailableServicesCompilerPassTest.php index 466cbdb0..308f1030 100644 --- a/tests/DependencyInjection/Compiler/UnavailableServicesCompilerPassTest.php +++ b/tests/DependencyInjection/Compiler/UnavailableServicesCompilerPassTest.php @@ -16,6 +16,9 @@ use CleverAge\ProcessBundle\DependencyInjection\CleverAgeProcessExtension; use CleverAge\ProcessBundle\DependencyInjection\Compiler\UnavailableServicesCompilerPass; use CleverAge\ProcessBundle\DependencyInjection\Configuration; +use CleverAge\ProcessBundle\Task\Debug\StopwatchTask; +use CleverAge\ProcessBundle\Task\Process\CommandRunnerTask; +use CleverAge\ProcessBundle\Task\Process\ProcessLauncherTask; use CleverAge\ProcessBundle\Task\Serialization\DenormalizerTask; use CleverAge\ProcessBundle\Task\Serialization\DeserializerTask; use CleverAge\ProcessBundle\Task\Serialization\NormalizerTask; @@ -31,6 +34,7 @@ use Symfony\Component\DependencyInjection\Compiler\ResolveClassPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\Process\Process; #[CoversClass(UnavailableServicesCompilerPass::class)] #[UsesClass(CleverAgeProcessExtension::class)] @@ -45,7 +49,7 @@ public function testServicesAreKeptWhenTheirDependenciesAreAvailable(): void { $container = $this->createContainer(serializer: true, validator: true); - foreach ([...self::SERIALIZER_TASKS, NormalizeTransformer::class, DenormalizeTransformer::class, ValidatorTask::class] as $id) { + foreach ([...self::SERIALIZER_TASKS, NormalizeTransformer::class, DenormalizeTransformer::class, ValidatorTask::class, StopwatchTask::class, CommandRunnerTask::class, ProcessLauncherTask::class] as $id) { self::assertSame($id, $container->findDefinition($id)->getClass()); } } @@ -98,7 +102,33 @@ public function testValidatorTaskIsReplacedWithoutValidator(): void self::assertSame(NormalizerTask::class, $container->findDefinition(NormalizerTask::class)->getClass()); } - private function createContainer(bool $serializer, bool $validator): ContainerBuilder + public function testStopwatchTaskIsReplacedWithoutStopwatch(): void + { + $container = $this->createContainer(serializer: true, validator: true, stopwatch: false); + + $definition = $container->findDefinition(StopwatchTask::class); + self::assertSame(UnavailableTask::class, $definition->getClass()); + self::assertSame([\sprintf(UnavailableServicesCompilerPass::STOPWATCH_MESSAGE, StopwatchTask::class)], $definition->getArguments()); + self::assertTrue($definition->isPublic()); + self::assertFalse($definition->isShared()); + self::assertSame(CommandRunnerTask::class, $container->findDefinition(CommandRunnerTask::class)->getClass()); + } + + public function testProcessTasksAreReplacedWithoutProcessComponent(): void + { + $container = $this->createContainer(serializer: true, validator: true, process: false); + + foreach ([CommandRunnerTask::class, ProcessLauncherTask::class] as $id) { + $definition = $container->findDefinition($id); + self::assertSame(UnavailableTask::class, $definition->getClass(), $id); + self::assertSame([\sprintf(UnavailableServicesCompilerPass::PROCESS_MESSAGE, $id)], $definition->getArguments()); + self::assertTrue($definition->isPublic()); + self::assertFalse($definition->isShared()); + } + self::assertSame(StopwatchTask::class, $container->findDefinition(StopwatchTask::class)->getClass()); + } + + private function createContainer(bool $serializer, bool $validator, bool $stopwatch = true, bool $process = true): ContainerBuilder { $container = new ContainerBuilder(); (new CleverAgeProcessExtension())->load([], $container); @@ -110,7 +140,11 @@ private function createContainer(bool $serializer, bool $validator): ContainerBu if ($validator) { $container->setDefinition('validator', new Definition(\stdClass::class)); } - (new UnavailableServicesCompilerPass())->process($container); + if ($stopwatch) { + $container->setDefinition('debug.stopwatch', new Definition(\stdClass::class)); + } + $classExists = static fn (string $class): bool => (Process::class !== $class || $process) && class_exists($class); + (new UnavailableServicesCompilerPass($classExists))->process($container); return $container; }