From 856a637af2713897be0e58cd0189c5a8732dc34e Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Fri, 6 Nov 2026 14:13:36 +0100 Subject: [PATCH] #475 - Generic transformers: restore, deprecated, the former names of the dashed transformer codes and contextual options Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + UPGRADE.md | 10 + .../03-generic_transformers_definition.md | 5 +- src/DependencyInjection/Configuration.php | 4 +- src/Transformer/GenericTransformer.php | 178 +++++++++++++++++- tests/Transformer/GenericTransformerTest.php | 178 ++++++++++++++++++ 6 files changed, 373 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd94f9d0..9ff28fda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Latest * [#483](https://github.com/cleverage/process-bundle/issues/483) Documentation: an exception thrown by `IterableTaskInterface::next()` makes the process fail right away with this exception, the `error_strategy` and the `error_outputs` of the task do not apply. ## Fixes +* [#475](https://github.com/cleverage/process-bundle/issues/475) Generic transformers: restore, deprecated, the former names (with underscores instead of dashes) of their transformer codes and contextual options, which no longer worked since 5.9 ([#438](https://github.com/cleverage/process-bundle/issues/438)): a transformer code with a dash that is only registered with underscores (`array-first` for `array_first`), and a contextual option with a dash given or used by its former name (`the_prefix`, `{{ the_prefix }}` for `the-prefix`). Update documentation, add tests. * [#491](https://github.com/cleverage/process-bundle/issues/491) `CounterTask`: `flush_every: 0` is rejected when the options are resolved, with a clear message, instead of failing on the first execution with `Modulo by zero`; a negative `flush_every`, which works as its absolute value, is deprecated. Update documentation, add tests. * [#477](https://github.com/cleverage/process-bundle/issues/477) "Stop the process on the first invalid item" (`error_strategy: skip` and an error branch ending on a `StopTask`) ends as stopped when the failing task is finalizable (`csv_reader`, `xml_stream_reader`, `stat_counter`...): the error of the skipped item, kept by its state, was taken for a finalization failure and the process ended as failed. Add tests. * [#473](https://github.com/cleverage/process-bundle/issues/473) A task failing with the `stop` error strategy makes the process fail even when its error branch stops the flow (`StopTask`, `ProcessState::stop()`): the failure was swallowed, the process ended as succeeded or stopped (`cleverage_process.end` event, exit code 0) and a parent process went on with a `null` output. Update documentation, add tests. diff --git a/UPGRADE.md b/UPGRADE.md index 88ff5d5d..63f506c4 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -18,6 +18,16 @@ A negative `flush_every` option of the `counter` task works as its absolute valu and will be rejected in 6.0: use a positive integer. `flush_every: 0`, which failed on the first execution (`Modulo by zero`), is now rejected when the options are resolved. +### Dashes in the generic transformers + +Since 5.9, the codes of the transformers and of the contextual options of a generic transformer keep their dashes, as +written. Before 5.9, they were replaced by underscores (unless the code also had an underscore), and only their former +name worked: `array-first` was found as the `array_first` transformer, and a `the-prefix` contextual option was given +and used as `the_prefix` / `{{ the_prefix }}`. These former names work again but are deprecated since 5.10 (`Since +cleverage/process-bundle 5.10: The transformer "array-first" of the generic transformer...`, `Using the contextual option +"the-prefix" of the generic transformer...`), and will not work in 6.0: write the code of the transformer as registered +(`array_first`), and the contextual option as defined in `contextual_options` (`the-prefix`, `{{ the-prefix }}`). + ## v5.8 ### Unknown task references diff --git a/docs/reference/03-generic_transformers_definition.md b/docs/reference/03-generic_transformers_definition.md index 2991b9a4..20c67304 100644 --- a/docs/reference/03-generic_transformers_definition.md +++ b/docs/reference/03-generic_transformers_definition.md @@ -86,4 +86,7 @@ clever_age_process: As any transformer code, generic transformer codes must be unique: use a prefix to avoid conflicts. A dash is kept in the code (it was replaced by an underscore before 5.7, the former name is deprecated), as in the codes of the -transformers and of the contextual options of a generic transformer. +transformers and of the contextual options of a generic transformer (replaced by an underscore before 5.9, unless the +code also had an underscore: `array-first` was found as the `array_first` transformer, and the `the-prefix` contextual +option was given and used as `the_prefix` / `{{ the_prefix }}`; these former names still work but are deprecated since +5.10, write the code of the transformer and of the contextual option as registered). diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index a6304ebc..d77b37c5 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -106,6 +106,7 @@ protected function appendTransformerConfigDefinition(NodeBuilder $definition): v ->info('Options of the transformer (code: definition with required, default, default_is_null), injected in the "transformers" with "{{ code }}"') ->example(['delimiter' => ['default' => ',']]) // Keep the dashes of the option codes, as written in the options given to the transformer and in "{{ code }}" + // (their former name, with underscores, is deprecated, see GenericTransformer) ->normalizeKeys(false) ->variablePrototype() ->end() @@ -113,7 +114,8 @@ protected function appendTransformerConfigDefinition(NodeBuilder $definition): v ->arrayNode('transformers') ->info('Transformers applied in sequence, by code, with their options') ->example(['explode' => ['delimiter' => '{{ separator }}']]) - // Keep the dashes of the transformer codes (the former name of a generic transformer is deprecated) + // Keep the dashes of the transformer codes (the former name of a generic transformer is deprecated, as the + // former name of a transformer code that is only registered with underscores, see GenericTransformer) ->normalizeKeys(false) ->variablePrototype() ->end() diff --git a/src/Transformer/GenericTransformer.php b/src/Transformer/GenericTransformer.php index 0f86ce4b..d2c5af38 100644 --- a/src/Transformer/GenericTransformer.php +++ b/src/Transformer/GenericTransformer.php @@ -13,10 +13,12 @@ namespace CleverAge\ProcessBundle\Transformer; +use CleverAge\ProcessBundle\Configuration\DeprecatedCodeAliases; use CleverAge\ProcessBundle\Context\ContextualOptionResolver; use CleverAge\ProcessBundle\Exception\InvalidArgumentException; use CleverAge\ProcessBundle\Exception\LogicException; use CleverAge\ProcessBundle\Registry\TransformerRegistry; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -41,6 +43,15 @@ class GenericTransformer implements ConfigurableTransformerInterface */ protected ?array $contextualOptions = null; + /** + * Former name (with underscores instead of dashes, before 5.9) of the contextual options whose code has a dash. + * + * Deprecated since 5.10, the former names will not be accepted in v6.0. + * + * @var array former name => code + */ + private array $deprecatedContextualOptions = []; + public function __construct( protected ContextualOptionResolver $contextualOptionResolver, TransformerRegistry $transformerRegistry, @@ -61,7 +72,13 @@ public function initialize(string $code, array $options = []): void $initialOptions = $resolver->resolve($options); $this->contextualOptions = $initialOptions['contextual_options']; - $this->preconfiguredTransformerOptions = $initialOptions['transformers']; + $this->deprecatedContextualOptions = DeprecatedCodeAliases::create(array_keys($this->contextualOptions)); + $transformers = $initialOptions['transformers']; + if (\is_array($transformers)) { + $transformers = $this->replaceFormerTransformerCodes($transformers); + $this->checkFormerContextualOptionPlaceholders($transformers); + } + $this->preconfiguredTransformerOptions = $transformers; } /** @@ -91,6 +108,11 @@ public function configureInitialOptions(OptionsResolver $resolver): void public function configureOptions(OptionsResolver $resolver): void { foreach ($this->contextualOptions ?? [] as $option => $optionConfig) { + $formerName = array_search($option, $this->deprecatedContextualOptions, true); + if (false !== $formerName) { + $this->configureFormerContextualOption($resolver, $option, $formerName, $optionConfig); + continue; + } $resolver->setDefined($option); if (null !== $optionConfig['default'] || $optionConfig['default_is_null']) { $resolver->setDefault($option, $optionConfig['default']); @@ -148,6 +170,10 @@ public function normalizeTransformerOptions(Options $options, array $transformer ? $options[$contextualOption] : null; } + // The former name of a contextual option whose code has a dash, in "{{ former_name }}" (deprecated) + foreach ($this->deprecatedContextualOptions as $formerName => $contextualOption) { + $contextualizedOptionValues[$formerName] = $contextualizedOptionValues[$contextualOption] ?? null; + } return $this->contextualOptionResolver->contextualizeOptions($transformerOptions, $contextualizedOptionValues); } @@ -165,4 +191,154 @@ public function configureContextualOptions(OptionsResolver $resolver): void $resolver->setDefault('default_is_null', false); $resolver->setAllowedTypes('default_is_null', 'bool'); } + + /** + * Before 5.9, the dashes of the transformer codes of a generic transformer were replaced by underscores (unless the + * code also had an underscore): "array-first" was found as "array_first". Such a code, when it is not registered + * but its former name is, is replaced by its former name (deprecated). + * + * Deprecated since 5.10, will be removed in v6.0. + * + * @param array $transformers + * + * @return array + */ + private function replaceFormerTransformerCodes(array $transformers): array + { + if (array_is_list($transformers)) { + return $transformers; + } + $replaced = []; + foreach ($transformers as $key => $transformerOptions) { + $key = $this->replaceFormerTransformerCode($key, $transformers); + $replaced[$key] = $transformerOptions; + } + + return $replaced; + } + + /** + * @param array $transformers + */ + private function replaceFormerTransformerCode(int|string $key, array $transformers): int|string + { + if (!\is_string($key) || !str_contains($key, '-') || str_contains($key, '_')) { + return $key; + } + $formerKey = str_replace('-', '_', $key); + // Both were written: none was renamed + if (\array_key_exists($formerKey, $transformers)) { + return $key; + } + // The code, without the "#" suffix of the key + $code = explode('#', $key, 2)[0]; + $formerCode = str_replace('-', '_', $code); + $registry = $this->transformerRegistry; + if (!$registry instanceof TransformerRegistry || $registry->hasTransformer($code) || !$registry->hasTransformer($formerCode)) { + return $key; + } + trigger_deprecation( + 'cleverage/process-bundle', + '5.10', + 'The transformer "%s" of the generic transformer "%s" is not registered, its former name "%s", with underscores instead of dashes, is used: write "%s". The dashes of the transformer codes of a generic transformer were replaced by underscores before 5.9, the former name will not be used in v6.0.', + $code, + $this->transformerCode, + $formerCode, + $formerCode, + ); + + return $formerKey; + } + + /** + * Before 5.9, a contextual option whose code has a dash was named with underscores, also in "{{ former_name }}". + * + * Deprecated since 5.10, will be removed in v6.0. + * + * @param array $transformers + */ + private function checkFormerContextualOptionPlaceholders(array $transformers): void + { + if ([] === $this->deprecatedContextualOptions) { + return; + } + $formerNames = array_map(static fn (string $formerName): string => preg_quote($formerName, '/'), array_keys($this->deprecatedContextualOptions)); + $pattern = \sprintf('/{{[ ]*(%s)[ ]*}}/', implode('|', $formerNames)); + $used = []; + foreach (self::getStrings($transformers) as $string) { + if (preg_match_all($pattern, $string, $matches)) { + $used = [...$used, ...$matches[1]]; + } + } + foreach (array_unique($used) as $formerName) { + trigger_deprecation( + 'cleverage/process-bundle', + '5.10', + 'Using the contextual option "%s" of the generic transformer "%s" as "{{ %s }}", with underscores instead of dashes, is deprecated: write "{{ %s }}". The dashes of the contextual option codes were replaced by underscores before 5.9, the former name will not be replaced in v6.0.', + $this->deprecatedContextualOptions[$formerName], + $this->transformerCode, + $formerName, + $this->deprecatedContextualOptions[$formerName], + ); + } + } + + /** + * A contextual option whose code has a dash can also be given by its former name, with underscores (deprecated). + * + * Deprecated since 5.10, will be removed in v6.0. + * + * @param array{required: bool, default: mixed, default_is_null: bool} $optionConfig + */ + private function configureFormerContextualOption(OptionsResolver $resolver, string $option, string $formerName, array $optionConfig): void + { + $resolver->setDefined($formerName); + $transformerCode = $this->transformerCode; + // Given by its code, by its former name, or the default value + $resolver->setDefault($option, static function (Options $options) use ($option, $formerName, $optionConfig, $transformerCode): mixed { + if ($options->offsetExists($formerName)) { + trigger_deprecation( + 'cleverage/process-bundle', + '5.10', + 'Using the contextual option "%s" of the generic transformer "%s" by the name "%s", with underscores instead of dashes, is deprecated: use "%s". The dashes of the contextual option codes were replaced by underscores before 5.9, the former name will not be accepted in v6.0.', + $option, + $transformerCode, + $formerName, + $option, + ); + + return $options[$formerName]; + } + if (null !== $optionConfig['default'] || $optionConfig['default_is_null']) { + return $optionConfig['default']; + } + if ($optionConfig['required']) { + throw new MissingOptionsException(\sprintf('The required option "%s" is missing.', $option)); + } + + return null; + }); + } + + /** + * The strings of the transformer options, keys and values, where "{{ code }}" is replaced. + * + * @param array $values + * + * @return list + */ + private static function getStrings(array $values): array + { + $strings = []; + foreach ($values as $key => $value) { + $strings[] = (string) $key; + if (\is_array($value)) { + $strings = [...$strings, ...self::getStrings($value)]; + } elseif (\is_string($value)) { + $strings[] = $value; + } + } + + return $strings; + } } diff --git a/tests/Transformer/GenericTransformerTest.php b/tests/Transformer/GenericTransformerTest.php index 7a2041df..eb99f027 100644 --- a/tests/Transformer/GenericTransformerTest.php +++ b/tests/Transformer/GenericTransformerTest.php @@ -13,10 +13,13 @@ namespace CleverAge\ProcessBundle\Tests\Transformer; +use CleverAge\ProcessBundle\Configuration\DeprecatedCodeAliases; use CleverAge\ProcessBundle\Context\ContextualOptionResolver; use CleverAge\ProcessBundle\Registry\TransformerRegistry; +use CleverAge\ProcessBundle\Transformer\Array\ArrayFirstTransformer; use CleverAge\ProcessBundle\Transformer\CallbackTransformer; use CleverAge\ProcessBundle\Transformer\GenericTransformer; +use CleverAge\ProcessBundle\Transformer\TransformerInterface; use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; @@ -26,6 +29,8 @@ #[\PHPUnit\Framework\Attributes\UsesClass(ContextualOptionResolver::class)] #[\PHPUnit\Framework\Attributes\UsesClass(TransformerRegistry::class)] #[\PHPUnit\Framework\Attributes\UsesClass(CallbackTransformer::class)] +#[\PHPUnit\Framework\Attributes\UsesClass(ArrayFirstTransformer::class)] +#[\PHPUnit\Framework\Attributes\UsesClass(DeprecatedCodeAliases::class)] class GenericTransformerTest extends TestCase { public function testRequiredOptionMustBeProvided(): void @@ -100,6 +105,179 @@ public function testGetCodeRequiresInitialization(): void $transformer->getCode(); } + /** + * Before 5.9, the dashes of the transformer codes of a generic transformer were replaced by underscores: a code + * that is not registered with its dashes but is with underscores still works, with a deprecation. + */ + public function testFormerNameOfATransformerCodeIsDeprecated(): void + { + $transformer = new GenericTransformer(new ContextualOptionResolver(), $this->createRegistry()); + $deprecations = $this->collectDeprecations(static fn () => $transformer->initialize('first_upper', [ + 'transformers' => [ + 'array-first' => null, + 'callback#upper-case' => ['callback' => 'strtoupper'], + ], + ])); + + self::assertSame( + ['Since cleverage/process-bundle 5.10: The transformer "array-first" of the generic transformer "first_upper" is not registered, its former name "array_first", with underscores instead of dashes, is used: write "array_first". The dashes of the transformer codes of a generic transformer were replaced by underscores before 5.9, the former name will not be used in v6.0.'], + $deprecations + ); + self::assertSame('ABC', $transformer->transform(['abc', 'def'], $this->resolveOptions($transformer, []))); + } + + /** + * A transformer code with a dash is used as written when it is registered, without deprecation. + */ + public function testRegisteredTransformerCodeWithADashIsUsed(): void + { + $last = new class implements TransformerInterface { + public function transform(mixed $value, array $options = []): mixed + { + return \is_array($value) ? end($value) : $value; + } + + public function getCode(): string + { + return 'array-first'; + } + }; + $registry = new TransformerRegistry(new ServiceLocator([ + 'array-first' => static fn (): TransformerInterface => $last, + 'array_first' => static fn (): ArrayFirstTransformer => new ArrayFirstTransformer(), + ])); + $transformer = new GenericTransformer(new ContextualOptionResolver(), $registry); + $deprecations = $this->collectDeprecations(static fn () => $transformer->initialize('last', [ + 'transformers' => ['array-first' => null], + ])); + + self::assertSame([], $deprecations); + self::assertSame('def', $transformer->transform(['abc', 'def'], $this->resolveOptions($transformer, []))); + } + + /** + * A contextual option with a dash is given and replaced as written, without deprecation. + */ + public function testContextualOptionWithADash(): void + { + $transformer = $this->createPrefixTransformer(['the-prefix' => ['required' => true]], '{{ the-prefix }}'); + + self::assertSame([], $this->collectDeprecations(function () use ($transformer): void { + self::assertSame('P-abc', $transformer->transform('abc', $this->resolveOptions($transformer, ['the-prefix' => 'P-']))); + })); + } + + /** + * Before 5.9, a contextual option with a dash was named with underscores: its former name, given as an option or + * in "{{ the_prefix }}", still works, with a deprecation. + */ + public function testFormerNameOfAContextualOptionIsDeprecated(): void + { + $deprecations = $this->collectDeprecations(function () use (&$transformer): void { + $transformer = $this->createPrefixTransformer(['the-prefix' => ['required' => true]], '{{ the_prefix }}'); + }); + self::assertSame( + ['Since cleverage/process-bundle 5.10: Using the contextual option "the-prefix" of the generic transformer "prefix" as "{{ the_prefix }}", with underscores instead of dashes, is deprecated: write "{{ the-prefix }}". The dashes of the contextual option codes were replaced by underscores before 5.9, the former name will not be replaced in v6.0.'], + $deprecations + ); + self::assertInstanceOf(GenericTransformer::class, $transformer); + + // Given by its code + self::assertSame([], $this->collectDeprecations(function () use ($transformer): void { + self::assertSame('P-abc', $transformer->transform('abc', $this->resolveOptions($transformer, ['the-prefix' => 'P-']))); + })); + + // Given by its former name + self::assertSame( + ['Since cleverage/process-bundle 5.10: Using the contextual option "the-prefix" of the generic transformer "prefix" by the name "the_prefix", with underscores instead of dashes, is deprecated: use "the-prefix". The dashes of the contextual option codes were replaced by underscores before 5.9, the former name will not be accepted in v6.0.'], + $this->collectDeprecations(function () use ($transformer): void { + self::assertSame('P-abc', $transformer->transform('abc', $this->resolveOptions($transformer, ['the_prefix' => 'P-']))); + }) + ); + + // Required + $this->expectException(MissingOptionsException::class); + $this->expectExceptionMessage('The required option "the-prefix" is missing.'); + $this->resolveOptions($transformer, []); + } + + /** + * The default value of a contextual option with a dash, given by its former name, is used when it is not given. + */ + public function testFormerNameOfAnOptionalContextualOption(): void + { + $transformer = $this->createPrefixTransformer(['the-prefix' => ['default' => 'D-']], '{{ the-prefix }}'); + self::assertSame('D-abc', $transformer->transform('abc', $this->resolveOptions($transformer, []))); + $this->collectDeprecations(function () use ($transformer): void { + self::assertSame('P-abc', $transformer->transform('abc', $this->resolveOptions($transformer, ['the_prefix' => 'P-']))); + }); + + $transformer = $this->createPrefixTransformer(['the-prefix' => ['required' => false]], '{{ the-prefix }}'); + self::assertSame('abc', $transformer->transform('abc', $this->resolveOptions($transformer, []))); + } + + /** + * When both the code with a dash and its former name are contextual options, none is deprecated. + */ + public function testContextualOptionsWithADashAndAnUnderscore(): void + { + $deprecations = $this->collectDeprecations(function () use (&$transformer): void { + $transformer = $this->createPrefixTransformer([ + 'the-prefix' => ['default' => 'D-'], + 'the_prefix' => ['default' => 'U-'], + ], '{{ the-prefix }}{{ the_prefix }}'); + }); + self::assertSame([], $deprecations); + self::assertInstanceOf(GenericTransformer::class, $transformer); + self::assertSame('D-U-abc', $transformer->transform('abc', $this->resolveOptions($transformer, []))); + } + + /** + * Generic transformer prefixing the value with the given contextual option. + * + * @param array> $contextualOptions + */ + private function createPrefixTransformer(array $contextualOptions, string $prefix): GenericTransformer + { + $transformer = new GenericTransformer(new ContextualOptionResolver(), $this->createRegistry()); + $transformer->initialize('prefix', [ + 'contextual_options' => $contextualOptions, + 'transformers' => [ + 'callback' => ['callback' => 'sprintf', 'left_parameters' => [$prefix.'%s']], + ], + ]); + + return $transformer; + } + + private function createRegistry(): TransformerRegistry + { + return new TransformerRegistry(new ServiceLocator([ + 'callback' => static fn (): CallbackTransformer => new CallbackTransformer(), + 'array_first' => static fn (): ArrayFirstTransformer => new ArrayFirstTransformer(), + ])); + } + + /** + * @return list + */ + private function collectDeprecations(callable $callable): array + { + $deprecations = []; + set_error_handler(static function (int $errno, string $errstr) use (&$deprecations): bool { + $deprecations[] = $errstr; + + return true; + }, \E_USER_DEPRECATED); + try { + $callable(); + } finally { + restore_error_handler(); + } + + return $deprecations; + } + /** * Generic transformer applying substr($value, {{ offset }}, {{ length }}). *