From cfe65d5607030601d63b68d488e965342b2f04f0 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Tue, 20 Oct 2026 14:39:07 +0200 Subject: [PATCH] #309 - Composer: suggest ext-intl instead of requiring it - ext-intl moves from require to suggest: only the slugify transformer uses it (\Transliterator, intl_get_error_message()). - UnavailableServicesCompilerPass: without the extension, the slugify transformer is replaced by an UnavailableTransformer failing with an explicit message when it is used (same approach as #278 / #269). - Tests: the transformer is replaced without intl, kept with it. Documentation (quick start, slugify reference) and CHANGELOG updated. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + composer.json | 2 +- docs/01-quick_start.md | 5 +++-- .../transformers/slugify_transformer.md | 2 +- .../UnavailableServicesCompilerPass.php | 7 +++++++ .../UnavailableServicesCompilerPassTest.php | 20 ++++++++++++++++--- 6 files changed, 30 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 89a24625..43a38483 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Latest ------ ## Changes +* [#309](https://github.com/cleverage/process-bundle/issues/309) Composer: `ext-intl` is no longer required, it is suggested: it is only needed by the `slugify` transformer (`SlugifyTransformer`), which is replaced by a service failing with an explicit message when the extension is missing (`UnavailableServicesCompilerPass`). Update documentation, add tests. * [#305](https://github.com/cleverage/process-bundle/issues/305) Bundle best practices, residues: the generic transformers are no longer autowired (explicit arguments). `ProcessHelpCommand` receives the `TaskRegistry` instead of the service container (passing a container is deprecated). Fetching from the service container a task of the application made public by the `TaskInterface` autoconfiguration is deprecated (`container.private` tag, new internal `PublicTaskDeprecationCompilerPass`); the tasks made public by the application itself are not concerned. Trigger the missing deprecation notices: `CheckSerializerCompilerPass`, `errors` task configuration key (use `error_outputs`). `TaskConfiguration::getErrors()` triggers a single notice on PHP 8.4+. `CleverAgeProcessBundle` is marked `@final since 5.4`. Update documentation, add tests. * [#299](https://github.com/cleverage/process-bundle/issues/299) Require `symfony/polyfill-php83`, `symfony/polyfill-php84` and `symfony/polyfill-php85`: the functions of PHP 8.3 to 8.5 can be used while supporting PHP 8.2. Rector targets PHP 8.2 (`withPhpVersion()`, `withPhpSets()` read from `composer.json`) and applies the rules using a newer function when its polyfill is required: `ProcessConfiguration` uses `array_any()` again; `array_first()` replaces `reset()` / `current()` in `ProcessConfiguration::getMainTask()`, `ConstraintLoader` and `ProcessHelpCommand` (no internal array pointer, same results). * [#279](https://github.com/cleverage/process-bundle/issues/279) Define the public API (Symfony BC promise): the classes not meant to be extended are marked `@final since 5.4` (the engine: `ProcessManager`, `ProcessConfigurationRegistry`, `TransformerRegistry`, `ContextualOptionResolver`; the models: `ProcessState`, `ProcessHistory`, `SubprocessInstance`, the configurations, the events, the `Filesystem` files; the concrete tasks and transformers), they will be `final` in v6.0: extending them triggers a deprecation with the Symfony `DebugClassLoader`. The abstract classes, interfaces, traits, exceptions and the file tasks extended by other tasks (`FileReaderTask`, `LineReaderTask`, `FolderBrowserTask`, `CsvReaderTask`, `InputCsvReaderTask`, `Filesystem\CsvResource`) stay open. `TaskRegistry`, new in this version, is `final`. The infrastructure classes are marked `@internal`: `CleverAgeProcessExtension`, `Configuration`, the commands, `DataQueueEventListener`, `PhpFunctionProvider`, the Monolog processors, `ProcessLogger` and `TaskLogger`. Update documentation. diff --git a/composer.json b/composer.json index 0648a899..34f46723 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,6 @@ "require": { "php": ">=8.2", "ext-dom": "*", - "ext-intl": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", @@ -105,6 +104,7 @@ "cleverage/ui-process-bundle": "<3.3" }, "suggest": { + "ext-intl": "Required by the slugify transformer", "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", diff --git a/docs/01-quick_start.md b/docs/01-quick_start.md index a1915fb6..1b62da0c 100644 --- a/docs/01-quick_start.md +++ b/docs/01-quick_start.md @@ -57,13 +57,14 @@ Some features rely on optional packages, suggested by the bundle: | Package | Needed by | | ------- | --------- | +| `ext-intl` (PHP extension) | the [SlugifyTransformer](reference/transformers/slugify_transformer.md) | | `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. +Without `symfony/process`, `symfony/stopwatch` or `ext-intl`, the container still compiles, and a process using one of +these tasks or transformers fails with an explicit message giving the package or extension to install. ## Global configuration diff --git a/docs/reference/transformers/slugify_transformer.md b/docs/reference/transformers/slugify_transformer.md index fd85192a..a963f525 100644 --- a/docs/reference/transformers/slugify_transformer.md +++ b/docs/reference/transformers/slugify_transformer.md @@ -6,7 +6,7 @@ Convert a string into a slug. The value is transliterated with a tags are stripped, the result is trimmed and lowercased, every sequence of characters matching `replace` is replaced by `separator`, and leading and trailing separators are removed. -Requires the `intl` PHP extension. +Requires the `intl` PHP extension (suggested by the bundle): without it, the transformer fails with an explicit message. Transformer reference --------------------- diff --git a/src/DependencyInjection/Compiler/UnavailableServicesCompilerPass.php b/src/DependencyInjection/Compiler/UnavailableServicesCompilerPass.php index 5f0244fe..af76a019 100644 --- a/src/DependencyInjection/Compiler/UnavailableServicesCompilerPass.php +++ b/src/DependencyInjection/Compiler/UnavailableServicesCompilerPass.php @@ -24,6 +24,7 @@ use CleverAge\ProcessBundle\Task\Validation\ValidatorTask; use CleverAge\ProcessBundle\Transformer\Serialization\DenormalizeTransformer; use CleverAge\ProcessBundle\Transformer\Serialization\NormalizeTransformer; +use CleverAge\ProcessBundle\Transformer\String\SlugifyTransformer; use CleverAge\ProcessBundle\Transformer\TransformerInterface; use CleverAge\ProcessBundle\Transformer\UnavailableTransformer; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; @@ -49,6 +50,8 @@ public const PROCESS_MESSAGE = 'The "%s" service requires the Symfony Process component, which does not seem installed: try "composer require symfony/process".'; + public const INTL_MESSAGE = 'The "%s" service requires the PHP intl extension, which does not seem installed (see https://www.php.net/manual/en/book.intl.php).'; + /** * @param (\Closure(class-string): bool)|null $classExists defaults to class_exists() (the tests check the missing packages) */ @@ -82,6 +85,10 @@ public function process(ContainerBuilder $container): void if (!($this->classExists ?? class_exists(...))(Process::class)) { $this->replace($container, self::PROCESS_MESSAGE, [CommandRunnerTask::class, ProcessLauncherTask::class]); } + + if (!($this->classExists ?? class_exists(...))(\Transliterator::class)) { + $this->replace($container, self::INTL_MESSAGE, [SlugifyTransformer::class]); + } } /** diff --git a/tests/DependencyInjection/Compiler/UnavailableServicesCompilerPassTest.php b/tests/DependencyInjection/Compiler/UnavailableServicesCompilerPassTest.php index 308f1030..324a1bb6 100644 --- a/tests/DependencyInjection/Compiler/UnavailableServicesCompilerPassTest.php +++ b/tests/DependencyInjection/Compiler/UnavailableServicesCompilerPassTest.php @@ -27,6 +27,7 @@ use CleverAge\ProcessBundle\Task\Validation\ValidatorTask; use CleverAge\ProcessBundle\Transformer\Serialization\DenormalizeTransformer; use CleverAge\ProcessBundle\Transformer\Serialization\NormalizeTransformer; +use CleverAge\ProcessBundle\Transformer\String\SlugifyTransformer; use CleverAge\ProcessBundle\Transformer\UnavailableTransformer; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\UsesClass; @@ -49,7 +50,7 @@ public function testServicesAreKeptWhenTheirDependenciesAreAvailable(): void { $container = $this->createContainer(serializer: true, validator: true); - foreach ([...self::SERIALIZER_TASKS, NormalizeTransformer::class, DenormalizeTransformer::class, ValidatorTask::class, StopwatchTask::class, CommandRunnerTask::class, ProcessLauncherTask::class] as $id) { + foreach ([...self::SERIALIZER_TASKS, NormalizeTransformer::class, DenormalizeTransformer::class, ValidatorTask::class, StopwatchTask::class, CommandRunnerTask::class, ProcessLauncherTask::class, SlugifyTransformer::class] as $id) { self::assertSame($id, $container->findDefinition($id)->getClass()); } } @@ -128,7 +129,18 @@ public function testProcessTasksAreReplacedWithoutProcessComponent(): void self::assertSame(StopwatchTask::class, $container->findDefinition(StopwatchTask::class)->getClass()); } - private function createContainer(bool $serializer, bool $validator, bool $stopwatch = true, bool $process = true): ContainerBuilder + public function testSlugifyTransformerIsReplacedWithoutIntl(): void + { + $container = $this->createContainer(serializer: true, validator: true, intl: false); + + $definition = $container->findDefinition(SlugifyTransformer::class); + self::assertSame(UnavailableTransformer::class, $definition->getClass()); + self::assertSame(['slugify', \sprintf(UnavailableServicesCompilerPass::INTL_MESSAGE, SlugifyTransformer::class)], $definition->getArguments()); + self::assertSame([['code' => 'slugify']], $definition->getTag('cleverage.transformer')); + self::assertSame(StopwatchTask::class, $container->findDefinition(StopwatchTask::class)->getClass()); + } + + private function createContainer(bool $serializer, bool $validator, bool $stopwatch = true, bool $process = true, bool $intl = true): ContainerBuilder { $container = new ContainerBuilder(); (new CleverAgeProcessExtension())->load([], $container); @@ -143,7 +155,9 @@ private function createContainer(bool $serializer, bool $validator, bool $stopwa if ($stopwatch) { $container->setDefinition('debug.stopwatch', new Definition(\stdClass::class)); } - $classExists = static fn (string $class): bool => (Process::class !== $class || $process) && class_exists($class); + $classExists = static fn (string $class): bool => (Process::class !== $class || $process) + && (\Transliterator::class !== $class || $intl) + && class_exists($class); (new UnavailableServicesCompilerPass($classExists))->process($container); return $container;