diff --git a/CHANGELOG.md b/CHANGELOG.md index 14a633a5..8f222d9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Latest ## Changes * [#90](https://github.com/cleverage/process-bundle/issues/90) Installation documentation: describe the Symfony Flex recipe ([symfony/recipes-contrib#2081](https://github.com/symfony/recipes-contrib/pull/2081)), which registers the bundle and creates `config/packages/clever_age_process.yaml` importing the `config/packages/process/` directory, and the manual setup without Flex. +## Fixes +* [#327](https://github.com/cleverage/process-bundle/issues/327) `ContextualOptionResolver`: quote the context keys in the pattern of the `{{ key }}` placeholders (a key containing `/` made every string option fail, a key containing a regex character matched other placeholders), and leave the options as is with an empty context (`{{ }}` matched an empty key). + v5.4 ----- diff --git a/src/Context/ContextualOptionResolver.php b/src/Context/ContextualOptionResolver.php index f4d67677..4e066f8c 100644 --- a/src/Context/ContextualOptionResolver.php +++ b/src/Context/ContextualOptionResolver.php @@ -31,8 +31,11 @@ public function contextualizeOption(mixed $value, array $context): mixed return $this->contextualizeOptions($value, $context); } - if (\is_string($value)) { - $pattern = \sprintf('/{{[ ]*(%s){1}[ ]*}}/', implode('|', array_keys($context))); + // Without context, there is nothing to replace ("{{ }}" would match an empty key) + if (\is_string($value) && [] !== $context) { + // The keys are free (console, process_executor, UI...): quoted, they cannot alter the pattern + $keys = array_map(static fn (int|string $key): string => preg_quote((string) $key, '/'), array_keys($context)); + $pattern = \sprintf('/{{[ ]*(%s){1}[ ]*}}/', implode('|', $keys)); $matches = []; $result = preg_match($pattern, $value, $matches); diff --git a/tests/Context/ContextualOptionResolverTest.php b/tests/Context/ContextualOptionResolverTest.php new file mode 100644 index 00000000..0024086f --- /dev/null +++ b/tests/Context/ContextualOptionResolverTest.php @@ -0,0 +1,64 @@ +contextualizeOption('{{ ids }}', ['ids' => ['a', 'b']])); + } + + public function testPlaceholdersAreReplacedInStrings(): void + { + self::assertSame( + ['path' => 'var/data/books.csv', 'other' => 'no placeholder', 'books' => 1], + (new ContextualOptionResolver())->contextualizeOptions( + ['path' => 'var/{{dir}}/{{ name }}.csv', 'other' => 'no placeholder', '{{ name }}' => 1], + ['dir' => 'data', 'name' => 'books'] + ) + ); + } + + /** + * The context keys are free (console, process_executor, UI...): they must not alter the pattern. + */ + public function testContextKeysAreQuoted(): void + { + $resolver = new ContextualOptionResolver(); + + // A "/" made the pattern invalid, for every string option + self::assertSame('value', $resolver->contextualizeOption('{{ a/b }}', ['a/b' => 'value'])); + self::assertSame('no placeholder', $resolver->contextualizeOption('no placeholder', ['a/b' => 'value'])); + // "x." matched "{{ x }}" + self::assertSame('{{ x }}', $resolver->contextualizeOption('{{ x }}', ['x.' => 'value'])); + self::assertSame('value', $resolver->contextualizeOption('{{ x. }}', ['x.' => 'value'])); + } + + public function testEmptyContext(): void + { + // "{{ }}" matched an empty key + self::assertSame('{{ }} {{ name }}', (new ContextualOptionResolver())->contextualizeOption('{{ }} {{ name }}', [])); + } + + public function testNonStringValuesAreKept(): void + { + self::assertSame(12, (new ContextualOptionResolver())->contextualizeOption(12, ['name' => 'books'])); + } +}