Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ Latest
* [#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.
* [#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.

## Fixes
* [#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.

v3.0.2
------

Expand Down
24 changes: 17 additions & 7 deletions src/Manager/ProcessConfigurationsManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,16 +83,26 @@ private function resolveUiOptions(array $options): array
'ui_launch_mode' => 'modal',
'constraints' => [],
'run' => null,
'default' => static function (OptionsResolver $defaultResolver) {
$defaultResolver->setDefault('input', null);
$defaultResolver->setDefault('context', static function (OptionsResolver $contextResolver) {
$contextResolver->setPrototype(true);
$contextResolver->setRequired(['key', 'value']);
});
},
'default' => [],
]
);
$uiResolver->setAllowedValues('entrypoint_type', ['text', 'file']);
// Resolved by a normalizer rather than as nested options: nested options defined with setDefault() are
// deprecated since symfony/options-resolver 7.3 and removed in 8.0, setOptions() does not exist before 7.3
$uiResolver->setAllowedTypes('default', 'array');
$uiResolver->setNormalizer('default', static function (Options $options, array $default): array {
$defaultResolver = new OptionsResolver();
$defaultResolver->setDefaults(['input' => null, 'context' => []]);
$defaultResolver->setAllowedTypes('context', 'array[]');
$defaultResolver->setNormalizer('context', static function (Options $options, array $context): array {
$contextResolver = new OptionsResolver();
$contextResolver->setRequired(['key', 'value']);

return array_map($contextResolver->resolve(...), $context);
});

return $defaultResolver->resolve($default);
});
$uiResolver->setNormalizer('constraints', static fn (Options $options, array $values): array => (new ConstraintLoader())->buildConstraints($values));
$uiResolver->setAllowedValues('ui_launch_mode', ['modal', null, 'form']);

Expand Down
5 changes: 0 additions & 5 deletions tests/App/config/packages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,6 @@ clever_age_process:
ui:
ui_launch_mode: form
entrypoint_type: file
# Required with Symfony 8: without it, ProcessConfigurationsManager returns the `default` nested
# options closure (nested options defined with setDefault() are no longer supported)
default:
input: 'not used by a file input'
context: []
tasks:
data:
service: '@CleverAge\ProcessBundle\Task\DummyTask'
Expand Down
2 changes: 2 additions & 0 deletions tests/Functional/LaunchActionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,10 @@ public function testLaunchWithFileUpload(): void
$file = tempnam(sys_get_temp_dir(), 'upload');
file_put_contents((string) $file, 'line1');

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

$form = $crawler->selectButton('Launch')->form();
Expand Down
56 changes: 53 additions & 3 deletions tests/Manager/ProcessConfigurationsManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@
use CleverAge\ProcessBundle\Registry\ProcessConfigurationRegistry;
use CleverAge\UiProcessBundle\Manager\ProcessConfigurationsManager;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\OptionsResolver\Exception\ExceptionInterface as OptionsResolverException;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\Exception\MissingOptionsException;
use Symfony\Component\OptionsResolver\Exception\UndefinedOptionsException;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;

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

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

public function testDefaultWithoutContext(): void
{
$manager = $this->createManager([
'test.process' => $this->rawProcess(['ui' => ['default' => ['input' => 'data.csv']]]),
]);

$options = $manager->getUiOptions('test.process');

self::assertNotNull($options);
self::assertSame(['input' => 'data.csv', 'context' => []], $options['default']);
}

/**
* @param array<string, mixed> $default
* @param class-string<OptionsResolverException> $exception
*/
#[DataProvider('provideInvalidDefault')]
public function testInvalidDefault(array $default, string $exception): void
{
$manager = $this->createManager(['test.process' => $this->rawProcess(['ui' => ['default' => $default]])]);

$this->expectException($exception);
$manager->getUiOptions('test.process');
}

/**
* @return iterable<string, array{array<string, mixed>, class-string<OptionsResolverException>}>
*/
public static function provideInvalidDefault(): iterable
{
yield 'unknown option' => [['inputs' => 'data.csv'], UndefinedOptionsException::class];
yield 'context is not an array' => [['context' => 'foo'], InvalidOptionsException::class];
yield 'context item is not an array' => [['context' => ['foo']], InvalidOptionsException::class];
yield 'context item without value' => [['context' => [['key' => 'foo']]], MissingOptionsException::class];
yield 'context item with an unknown option' => [
['context' => [['key' => 'foo', 'value' => 'bar', 'type' => 'string']]],
UndefinedOptionsException::class,
];
}

public function testDefaultIsNotAnArray(): void
{
$manager = $this->createManager(['test.process' => $this->rawProcess(['ui' => ['default' => 'data.csv']])]);

$this->expectException(InvalidOptionsException::class);
$manager->getUiOptions('test.process');
}

public function testInvalidEntrypointType(): void
{
$manager = $this->createManager(['test.process' => $this->rawProcess(['ui' => ['entrypoint_type' => 'csv']])]);
Expand Down
Loading