From a7f01c7e29eb7147a113ead9f001a91a3786e1e9 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Mon, 28 Sep 2026 12:04:17 +0200 Subject: [PATCH 1/2] fix(transformer) #200 Fix GenericTransformer: a contextual option declared with `required: false` and no default can now be used; its placeholders are replaced by `null` when omitted. Update documentation, add tests. Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 1 + .../03-generic_transformers_definition.md | 3 +- .../transformers/generic_transformer.md | 4 +- src/Transformer/GenericTransformer.php | 6 +- tests/Transformer/GenericTransformerTest.php | 107 ++++++++++++++++++ 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 tests/Transformer/GenericTransformerTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index e918fb94..80ab36ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Latest * [#192](https://github.com/cleverage/process-bundle/issues/192) Fix CommandRunnerTask: only pass the `options` option to `Process::setOptions()`, support string `commandline` through `Process::fromShellCommandline()`, validate option types. Update documentation, add tests. * [#194](https://github.com/cleverage/process-bundle/issues/194) Fix ProcessLauncherTask: the `process_options` normalizer returned an array despite its scalar return type, so the task always failed with a `TypeError`. Update documentation, add tests. * [#189](https://github.com/cleverage/process-bundle/issues/189) Fix EventDispatcherTask: dispatch the event under the `event_name` option (regression since v4.0). `event_name` is now optional: when `null` (default), the event is dispatched under its class name. Update documentation, add tests. +* [#200](https://github.com/cleverage/process-bundle/issues/200) Fix GenericTransformer: a contextual option declared with `required: false` and no default can now be used; its placeholders are replaced by `null` when omitted. Update documentation, add tests. ## Deprecated * [#189](https://github.com/cleverage/process-bundle/issues/189) EventDispatcherTask: when `event_name` is set, listening to `CleverAge\ProcessBundle\Event\EventDispatcherTaskEvent` is deprecated (the event is still dispatched under its class name, with an `E_USER_DEPRECATED` error, if it has listeners). Listen to the configured `event_name` instead: the BC layer will be removed in v6.0. diff --git a/docs/reference/03-generic_transformers_definition.md b/docs/reference/03-generic_transformers_definition.md index 1883a819..013a8f0d 100644 --- a/docs/reference/03-generic_transformers_definition.md +++ b/docs/reference/03-generic_transformers_definition.md @@ -33,7 +33,8 @@ For each contextual option, you can define | `default_is_null` | `bool` | | `false` | If you need `null` to be the default value, use this option | Note that an option with a default value is still required by default, which has no effect since the default is used: -set `required: false` for an option without default value that may be omitted. +set `required: false` for an option without default value that may be omitted: when omitted, its placeholders are +replaced by `null`. The `transformers` list uses the same syntax as any other transformer using a sub-list of transformers (see [TransformerTrait](traits/transformer_trait.md)). diff --git a/docs/reference/transformers/generic_transformer.md b/docs/reference/transformers/generic_transformer.md index 135967d7..a8b9ec0a 100644 --- a/docs/reference/transformers/generic_transformer.md +++ b/docs/reference/transformers/generic_transformer.md @@ -102,5 +102,5 @@ Notes ----- * Contextual options are transformer options, unrelated to the process context (`-c key:value`). -* An option declared with `required: false` and no default is neither required nor defined, so it cannot be used: - always give such an option a `default` or `default_is_null: true`. +* An option declared with `required: false` and no default may be omitted: its placeholders are then replaced by + `null`. diff --git a/src/Transformer/GenericTransformer.php b/src/Transformer/GenericTransformer.php index f1863aa4..bf53d955 100644 --- a/src/Transformer/GenericTransformer.php +++ b/src/Transformer/GenericTransformer.php @@ -79,6 +79,7 @@ public function configureInitialOptions(OptionsResolver $resolver): void public function configureOptions(OptionsResolver $resolver): void { foreach ($this->contextualOptions as $option => $optionConfig) { + $resolver->setDefined($option); if (null !== $optionConfig['default'] || $optionConfig['default_is_null']) { $resolver->setDefault($option, $optionConfig['default']); } @@ -118,7 +119,10 @@ public function normalizeTransformerOptions(Options $options, array $transformer { $contextualizedOptionValues = []; foreach ($this->contextualOptions as $contextualOption => $contextualOptionConfig) { - $contextualizedOptionValues[$contextualOption] = $options[$contextualOption]; + // An optional contextual option without default value is replaced by null when not provided + $contextualizedOptionValues[$contextualOption] = $options->offsetExists($contextualOption) + ? $options[$contextualOption] + : null; } return $this->contextualOptionResolver->contextualizeOptions($transformerOptions, $contextualizedOptionValues); diff --git a/tests/Transformer/GenericTransformerTest.php b/tests/Transformer/GenericTransformerTest.php new file mode 100644 index 00000000..89d4b9ab --- /dev/null +++ b/tests/Transformer/GenericTransformerTest.php @@ -0,0 +1,107 @@ +createSubstrTransformer(['offset' => ['required' => true]]); + + $this->expectException(MissingOptionsException::class); + + $this->resolveOptions($transformer, []); + } + + public function testOptionWithDefault(): void + { + $transformer = $this->createSubstrTransformer([ + 'offset' => ['default' => 2], + 'length' => ['default_is_null' => true], + ]); + + $this->assertSame('llo', $transformer->transform('hello', $this->resolveOptions($transformer, []))); + $this->assertSame('ll', $transformer->transform('hello', $this->resolveOptions($transformer, ['length' => 2]))); + } + + public function testOptionalOptionWithoutDefaultCanBeProvided(): void + { + $transformer = $this->createSubstrTransformer([ + 'offset' => ['required' => true], + 'length' => ['required' => false], + ]); + + $options = $this->resolveOptions($transformer, ['offset' => 1, 'length' => 3]); + + $this->assertSame('ell', $transformer->transform('hello', $options)); + } + + public function testOptionalOptionWithoutDefaultIsNullWhenOmitted(): void + { + $transformer = $this->createSubstrTransformer([ + 'offset' => ['required' => true], + 'length' => ['required' => false], + ]); + + $options = $this->resolveOptions($transformer, ['offset' => 1]); + + $this->assertSame('ello', $transformer->transform('hello', $options)); + } + + /** + * Generic transformer applying substr($value, {{ offset }}, {{ length }}). + * + * @param array> $contextualOptions + */ + private function createSubstrTransformer(array $contextualOptions): GenericTransformer + { + $registry = new TransformerRegistry(); + $registry->addTransformer(new CallbackTransformer()); + + $transformer = new GenericTransformer(new ContextualOptionResolver(), $registry); + $transformer->initialize('substr', [ + 'contextual_options' => $contextualOptions, + 'transformers' => [ + 'callback' => [ + 'callback' => 'substr', + 'right_parameters' => ['{{ offset }}', '{{ length }}'], + ], + ], + ]); + + return $transformer; + } + + /** + * @param array $options + * + * @return array + */ + private function resolveOptions(GenericTransformer $transformer, array $options): array + { + $resolver = new OptionsResolver(); + $transformer->configureOptions($resolver); + + return $resolver->resolve($options); + } +} From ff856ca55c154a584a3c6b7e0a8327f2499d8d89 Mon Sep 17 00:00:00 2001 From: Nicolas Joubert Date: Mon, 28 Sep 2026 13:59:01 +0200 Subject: [PATCH 2/2] test #200 Declare the classes used by the new tests (UsesClass), fixing risky tests when coverage is enabled Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/Transformer/GenericTransformerTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Transformer/GenericTransformerTest.php b/tests/Transformer/GenericTransformerTest.php index 89d4b9ab..001a9183 100644 --- a/tests/Transformer/GenericTransformerTest.php +++ b/tests/Transformer/GenericTransformerTest.php @@ -22,6 +22,9 @@ use Symfony\Component\OptionsResolver\OptionsResolver; #[\PHPUnit\Framework\Attributes\CoversClass(GenericTransformer::class)] +#[\PHPUnit\Framework\Attributes\UsesClass(ContextualOptionResolver::class)] +#[\PHPUnit\Framework\Attributes\UsesClass(TransformerRegistry::class)] +#[\PHPUnit\Framework\Attributes\UsesClass(CallbackTransformer::class)] class GenericTransformerTest extends TestCase { public function testRequiredOptionMustBeProvided(): void