Skip to content

Commit 48cd3da

Browse files
fix #89 Resolve the ui.default option with a normalizer instead of nested options removed in Symfony 8
Nested options defined with setDefault() are deprecated since symfony/options-resolver 7.3 and removed in 8.0: the closure itself was returned, so the launch form of a process without ui.default failed (Cannot use object of type Closure as array) and ui.default was no longer validated. setOptions() does not exist before 7.3: a normalizer works with Symfony 6.4, 7.4 and 8. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent c0bb648 commit 48cd3da

5 files changed

Lines changed: 75 additions & 15 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Latest
77
* [#84](https://github.com/cleverage/ui-process-bundle/issues/84) Add `LogRecord::hasContextInfo()`, deprecate the misnamed `LogRecord::contextIsEmpty()` (it returns `true` when the context is not empty). Add tests.
88
* [#85](https://github.com/cleverage/ui-process-bundle/issues/85) `ProcessHandler`: default report increment level aligned on the bundle configuration (`Warning`); declare the `symfony/ux-twig-component` dependency. Add tests.
99

10+
## Fixes
11+
* [#89](https://github.com/cleverage/ui-process-bundle/issues/89) `ProcessConfigurationsManager`: resolve the `ui.default` option with a normalizer instead of nested options defined with `setDefault()` (deprecated since symfony/options-resolver 7.3, removed in 8.0). With Symfony 8, a process launched with the UI form (`ui_launch_mode: form`) without `ui.default` no longer fails (`Cannot use object of type Closure as array`), and `ui.default` is validated again. Add tests.
12+
1013
v3.0.2
1114
------
1215

‎src/Manager/ProcessConfigurationsManager.php‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,26 @@ private function resolveUiOptions(array $options): array
8383
'ui_launch_mode' => 'modal',
8484
'constraints' => [],
8585
'run' => null,
86-
'default' => static function (OptionsResolver $defaultResolver) {
87-
$defaultResolver->setDefault('input', null);
88-
$defaultResolver->setDefault('context', static function (OptionsResolver $contextResolver) {
89-
$contextResolver->setPrototype(true);
90-
$contextResolver->setRequired(['key', 'value']);
91-
});
92-
},
86+
'default' => [],
9387
]
9488
);
9589
$uiResolver->setAllowedValues('entrypoint_type', ['text', 'file']);
90+
// Resolved by a normalizer rather than as nested options: nested options defined with setDefault() are
91+
// deprecated since symfony/options-resolver 7.3 and removed in 8.0, setOptions() does not exist before 7.3
92+
$uiResolver->setAllowedTypes('default', 'array');
93+
$uiResolver->setNormalizer('default', static function (Options $options, array $default): array {
94+
$defaultResolver = new OptionsResolver();
95+
$defaultResolver->setDefaults(['input' => null, 'context' => []]);
96+
$defaultResolver->setAllowedTypes('context', 'array[]');
97+
$defaultResolver->setNormalizer('context', static function (Options $options, array $context): array {
98+
$contextResolver = new OptionsResolver();
99+
$contextResolver->setRequired(['key', 'value']);
100+
101+
return array_map($contextResolver->resolve(...), $context);
102+
});
103+
104+
return $defaultResolver->resolve($default);
105+
});
96106
$uiResolver->setNormalizer('constraints', static fn (Options $options, array $values): array => (new ConstraintLoader())->buildConstraints($values));
97107
$uiResolver->setAllowedValues('ui_launch_mode', ['modal', null, 'form']);
98108

‎tests/App/config/packages.yaml‎

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,6 @@ clever_age_process:
7373
ui:
7474
ui_launch_mode: form
7575
entrypoint_type: file
76-
# Required with Symfony 8: without it, ProcessConfigurationsManager returns the `default` nested
77-
# options closure (nested options defined with setDefault() are no longer supported)
78-
default:
79-
input: 'not used by a file input'
80-
context: []
8176
tasks:
8277
data:
8378
service: '@CleverAge\ProcessBundle\Task\DummyTask'

‎tests/Functional/LaunchActionTest.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,10 @@ public function testLaunchWithFileUpload(): void
121121
$file = tempnam(sys_get_temp_dir(), 'upload');
122122
file_put_contents((string) $file, 'line1');
123123

124+
// test.upload has no "default" UI option: the form must be displayed with empty default values
124125
$crawler = $this->client->request('GET', '/process?routeName=process_launch&process=test.upload');
125126
self::assertResponseIsSuccessful();
127+
self::assertCount(0, $crawler->filter('input[name^="launch[context]"]'));
126128
self::assertCount(1, $crawler->filter('input[type="file"][name="launch[input]"]'));
127129

128130
$form = $crawler->selectButton('Launch')->form();

‎tests/Manager/ProcessConfigurationsManagerTest.php‎

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
use CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry;
1818
use CleverAge\UiProcessBundle\Manager\ProcessConfigurationsManager;
1919
use PHPUnit\Framework\Attributes\CoversClass;
20+
use PHPUnit\Framework\Attributes\DataProvider;
2021
use PHPUnit\Framework\TestCase;
22+
use Symfony\Component\OptionsResolver\Exception\ExceptionInterface as OptionsResolverException;
2123
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
24+
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
25+
use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
2226
use Symfony\Component\Validator\Constraints\Length;
2327
use Symfony\Component\Validator\Constraints\NotBlank;
2428

@@ -59,9 +63,7 @@ public function testDefaultUiOptions(): void
5963
/** @var mixed $run the PHPDoc type of the option is wrong */
6064
$run = $options['run'];
6165
self::assertNull($run);
62-
// The "default" nested option is not asserted: defined with setDefault() and a closure, it is not resolved as
63-
// a nested option with symfony/options-resolver 8 (the closure itself is returned)
64-
self::assertArrayHasKey('default', $options);
66+
self::assertSame(['input' => null, 'context' => []], $options['default']);
6567
}
6668

6769
public function testConfiguredUiOptions(): void
@@ -100,6 +102,54 @@ public function testConfiguredUiOptions(): void
100102
self::assertSame(10, $options['constraints'][1]->max);
101103
}
102104

105+
public function testDefaultWithoutContext(): void
106+
{
107+
$manager = $this->createManager([
108+
'test.process' => $this->rawProcess(['ui' => ['default' => ['input' => 'data.csv']]]),
109+
]);
110+
111+
$options = $manager->getUiOptions('test.process');
112+
113+
self::assertNotNull($options);
114+
self::assertSame(['input' => 'data.csv', 'context' => []], $options['default']);
115+
}
116+
117+
/**
118+
* @param array<string, mixed> $default
119+
* @param class-string<OptionsResolverException> $exception
120+
*/
121+
#[DataProvider('provideInvalidDefault')]
122+
public function testInvalidDefault(array $default, string $exception): void
123+
{
124+
$manager = $this->createManager(['test.process' => $this->rawProcess(['ui' => ['default' => $default]])]);
125+
126+
$this->expectException($exception);
127+
$manager->getUiOptions('test.process');
128+
}
129+
130+
/**
131+
* @return iterable<string, array{array<string, mixed>, class-string<OptionsResolverException>}>
132+
*/
133+
public static function provideInvalidDefault(): iterable
134+
{
135+
yield 'unknown option' => [['inputs' => 'data.csv'], UndefinedOptionsException::class];
136+
yield 'context is not an array' => [['context' => 'foo'], InvalidOptionsException::class];
137+
yield 'context item is not an array' => [['context' => ['foo']], InvalidOptionsException::class];
138+
yield 'context item without value' => [['context' => [['key' => 'foo']]], MissingOptionsException::class];
139+
yield 'context item with an unknown option' => [
140+
['context' => [['key' => 'foo', 'value' => 'bar', 'type' => 'string']]],
141+
UndefinedOptionsException::class,
142+
];
143+
}
144+
145+
public function testDefaultIsNotAnArray(): void
146+
{
147+
$manager = $this->createManager(['test.process' => $this->rawProcess(['ui' => ['default' => 'data.csv']])]);
148+
149+
$this->expectException(InvalidOptionsException::class);
150+
$manager->getUiOptions('test.process');
151+
}
152+
103153
public function testInvalidEntrypointType(): void
104154
{
105155
$manager = $this->createManager(['test.process' => $this->rawProcess(['ui' => ['entrypoint_type' => 'csv']])]);

0 commit comments

Comments
 (0)