Skip to content

Commit c14e862

Browse files
fix(options) #228 Add the missing setAllowedTypes() on HashTransformer (raw_output), SimpleBatchTask (batch_count), ConditionTrait (empty, not_empty), NormalizerTask / SerializerTask / DeserializerTask (context) and ObjectUpdaterTask (property_path): a wrong type is now reported when the options are resolved. Add tests.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent d2c921a commit c14e862

9 files changed

Lines changed: 196 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Latest
2323
* [#220](https://github.com/cleverage/process-bundle/issues/220) Fix the stop error strategy: throw a `ProcessFailedException` (with the original exception as `previous`) instead of a `FatalError`, so that the command exits with a non-zero code when a process fails and `ProcessLauncherTask` detects failed sub-processes. Update documentation, add tests.
2424
* [#221](https://github.com/cleverage/process-bundle/issues/221) Fix CsvSplitterTask: each produced file contains `max_lines` data lines (instead of `max_lines - 2`), no infinite loop with `max_lines` <= 2 (`max_lines` must now be an integer greater than 0), no header-only file at the end. Update documentation, add tests.
2525
* [#223](https://github.com/cleverage/process-bundle/issues/223) Fix CounterTask: the final count is outputted once, as `flush()` may be called several times. Document that `flush()` implementations must be idempotent. Update documentation, add tests.
26+
* [#228](https://github.com/cleverage/process-bundle/issues/228) Add the missing `setAllowedTypes()` on HashTransformer (`raw_output`), SimpleBatchTask (`batch_count`), ConditionTrait (`empty`, `not_empty`), NormalizerTask / SerializerTask / DeserializerTask (`context`) and ObjectUpdaterTask (`property_path`): a wrong type is now reported when the options are resolved. Add tests.
2627

2728
## Deprecated
2829
* [#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.

‎src/Task/ObjectUpdaterTask.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use CleverAge\ProcessBundle\Model\ProcessState;
1818
use Symfony\Component\OptionsResolver\OptionsResolver;
1919
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
20+
use Symfony\Component\PropertyAccess\PropertyPathInterface;
2021

2122
/**
2223
* Takes an array containing an object and a value updates an object's property with this value, then return the object.
@@ -44,5 +45,6 @@ public function execute(ProcessState $state): void
4445
protected function configureOptions(OptionsResolver $resolver): void
4546
{
4647
$resolver->setRequired(['property_path']);
48+
$resolver->setAllowedTypes('property_path', ['string', PropertyPathInterface::class]);
4749
}
4850
}

‎src/Task/Serialization/DeserializerTask.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,6 @@ protected function configureOptions(OptionsResolver $resolver): void
4545
$resolver->setDefaults([
4646
'context' => [],
4747
]);
48+
$resolver->setAllowedTypes('context', ['array']);
4849
}
4950
}

‎src/Task/Serialization/NormalizerTask.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,6 @@ protected function configureOptions(OptionsResolver $resolver): void
5151
$resolver->setDefaults([
5252
'context' => [],
5353
]);
54+
$resolver->setAllowedTypes('context', ['array']);
5455
}
5556
}

‎src/Task/Serialization/SerializerTask.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,5 +39,6 @@ protected function configureOptions(OptionsResolver $resolver): void
3939
$resolver->setDefaults([
4040
'context' => [],
4141
]);
42+
$resolver->setAllowedTypes('context', ['array']);
4243
}
4344
}

‎src/Task/SimpleBatchTask.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,5 +53,6 @@ protected function configureOptions(OptionsResolver $resolver): void
5353
$resolver->setDefaults([
5454
'batch_count' => 10,
5555
]);
56+
$resolver->setAllowedTypes('batch_count', ['int', 'null']);
5657
}
5758
}

‎src/Transformer/ConditionTrait.php‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ protected function configureConditionOptions(OptionsResolver $resolver): void
9999
$resolver->setDefault('match_regexp', []);
100100
$resolver->setAllowedTypes('not_match', 'array');
101101
$resolver->setAllowedTypes('match', 'array');
102+
$resolver->setAllowedTypes('not_empty', 'array');
103+
$resolver->setAllowedTypes('empty', 'array');
102104
$resolver->setAllowedTypes('not_match_regexp', 'array');
103105
$resolver->setAllowedTypes('match_regexp', 'array');
104106
}

‎src/Transformer/String/HashTransformer.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public function configureOptions(OptionsResolver $resolver): void
2929

3030
$resolver->setDefined('raw_output');
3131
$resolver->setDefault('raw_output', false);
32+
$resolver->setAllowedTypes('raw_output', 'bool');
3233
}
3334

3435
public function transform(mixed $value, array $options = []): string

‎tests/OptionAllowedTypesTest.php‎

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the CleverAge/ProcessBundle package.
7+
*
8+
* Copyright (c) Clever-Age
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace CleverAge\ProcessBundle\Tests;
15+
16+
use CleverAge\ProcessBundle\Configuration\ProcessConfiguration;
17+
use CleverAge\ProcessBundle\Configuration\TaskConfiguration;
18+
use CleverAge\ProcessBundle\Context\ContextualOptionResolver;
19+
use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
20+
use CleverAge\ProcessBundle\Model\ProcessHistory;
21+
use CleverAge\ProcessBundle\Model\ProcessState;
22+
use CleverAge\ProcessBundle\Task\ObjectUpdaterTask;
23+
use CleverAge\ProcessBundle\Task\Serialization\DeserializerTask;
24+
use CleverAge\ProcessBundle\Task\Serialization\NormalizerTask;
25+
use CleverAge\ProcessBundle\Task\Serialization\SerializerTask;
26+
use CleverAge\ProcessBundle\Task\SimpleBatchTask;
27+
use CleverAge\ProcessBundle\Transformer\Array\ArrayFilterTransformer;
28+
use CleverAge\ProcessBundle\Transformer\ConditionTrait;
29+
use CleverAge\ProcessBundle\Transformer\ConfigurableTransformerInterface;
30+
use CleverAge\ProcessBundle\Transformer\String\HashTransformer;
31+
use PHPUnit\Framework\Attributes\DataProvider;
32+
use PHPUnit\Framework\TestCase;
33+
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
34+
use Symfony\Component\OptionsResolver\OptionsResolver;
35+
use Symfony\Component\PropertyAccess\PropertyAccess;
36+
use Symfony\Component\PropertyAccess\PropertyPath;
37+
use Symfony\Component\Serializer\Serializer;
38+
39+
/**
40+
* Options are validated when resolved, instead of failing later with an unclear error.
41+
*/
42+
#[\PHPUnit\Framework\Attributes\CoversClass(HashTransformer::class)]
43+
#[\PHPUnit\Framework\Attributes\CoversClass(ArrayFilterTransformer::class)]
44+
#[\PHPUnit\Framework\Attributes\CoversTrait(ConditionTrait::class)]
45+
#[\PHPUnit\Framework\Attributes\CoversClass(SimpleBatchTask::class)]
46+
#[\PHPUnit\Framework\Attributes\CoversClass(NormalizerTask::class)]
47+
#[\PHPUnit\Framework\Attributes\CoversClass(SerializerTask::class)]
48+
#[\PHPUnit\Framework\Attributes\CoversClass(DeserializerTask::class)]
49+
#[\PHPUnit\Framework\Attributes\CoversClass(ObjectUpdaterTask::class)]
50+
#[\PHPUnit\Framework\Attributes\UsesClass(AbstractConfigurableTask::class)]
51+
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessConfiguration::class)]
52+
#[\PHPUnit\Framework\Attributes\UsesClass(TaskConfiguration::class)]
53+
#[\PHPUnit\Framework\Attributes\UsesClass(ContextualOptionResolver::class)]
54+
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessHistory::class)]
55+
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessState::class)]
56+
class OptionAllowedTypesTest extends TestCase
57+
{
58+
/**
59+
* @return iterable<string, array{string, array<string, mixed>}>
60+
*/
61+
public static function invalidTransformerOptionsProvider(): iterable
62+
{
63+
yield 'hash raw_output' => [HashTransformer::class, ['algo' => 'md5', 'raw_output' => 'yes']];
64+
yield 'condition empty' => [ArrayFilterTransformer::class, ['condition' => ['empty' => 'name']]];
65+
yield 'condition not_empty' => [ArrayFilterTransformer::class, ['condition' => ['not_empty' => 'name']]];
66+
}
67+
68+
/**
69+
* @param class-string<ConfigurableTransformerInterface> $class
70+
* @param array<string, mixed> $options
71+
*/
72+
#[DataProvider('invalidTransformerOptionsProvider')]
73+
public function testInvalidTransformerOptionIsRejected(string $class, array $options): void
74+
{
75+
$this->expectException(InvalidOptionsException::class);
76+
77+
$this->resolveTransformerOptions($class, $options);
78+
}
79+
80+
/**
81+
* @return iterable<string, array{string, array<string, mixed>}>
82+
*/
83+
public static function validTransformerOptionsProvider(): iterable
84+
{
85+
yield 'hash raw_output' => [HashTransformer::class, ['algo' => 'md5', 'raw_output' => true]];
86+
yield 'condition empty' => [ArrayFilterTransformer::class, ['condition' => ['empty' => ['name' => null]]]];
87+
yield 'condition not_empty' => [ArrayFilterTransformer::class, ['condition' => ['not_empty' => ['name' => null]]]];
88+
}
89+
90+
/**
91+
* @param class-string<ConfigurableTransformerInterface> $class
92+
* @param array<string, mixed> $options
93+
*/
94+
#[DataProvider('validTransformerOptionsProvider')]
95+
public function testValidTransformerOptionIsAccepted(string $class, array $options): void
96+
{
97+
self::assertNotEmpty($this->resolveTransformerOptions($class, $options));
98+
}
99+
100+
/**
101+
* @return iterable<string, array{string, array<string, mixed>}>
102+
*/
103+
public static function invalidTaskOptionsProvider(): iterable
104+
{
105+
yield 'simple batch batch_count' => [SimpleBatchTask::class, ['batch_count' => '10']];
106+
yield 'normalizer context' => [NormalizerTask::class, ['format' => 'json', 'context' => 'groups']];
107+
yield 'serializer context' => [SerializerTask::class, ['format' => 'json', 'context' => 'groups']];
108+
yield 'deserializer context' => [DeserializerTask::class, ['type' => 'array', 'format' => 'json', 'context' => 'groups']];
109+
yield 'object updater property_path' => [ObjectUpdaterTask::class, ['property_path' => ['name']]];
110+
}
111+
112+
/**
113+
* @param class-string<AbstractConfigurableTask> $class
114+
* @param array<string, mixed> $options
115+
*/
116+
#[DataProvider('invalidTaskOptionsProvider')]
117+
public function testInvalidTaskOptionIsRejected(string $class, array $options): void
118+
{
119+
$this->expectException(InvalidOptionsException::class);
120+
121+
$this->initializeTask($class, $options);
122+
}
123+
124+
/**
125+
* @return iterable<string, array{string, array<string, mixed>}>
126+
*/
127+
public static function validTaskOptionsProvider(): iterable
128+
{
129+
yield 'simple batch batch_count' => [SimpleBatchTask::class, ['batch_count' => 10]];
130+
yield 'simple batch null batch_count' => [SimpleBatchTask::class, ['batch_count' => null]];
131+
yield 'normalizer context' => [NormalizerTask::class, ['format' => 'json', 'context' => ['groups' => ['a']]]];
132+
yield 'serializer context' => [SerializerTask::class, ['format' => 'json', 'context' => []]];
133+
yield 'deserializer context' => [DeserializerTask::class, ['type' => 'array', 'format' => 'json', 'context' => []]];
134+
yield 'object updater string property_path' => [ObjectUpdaterTask::class, ['property_path' => 'name']];
135+
yield 'object updater PropertyPath property_path' => [ObjectUpdaterTask::class, ['property_path' => new PropertyPath('name')]];
136+
}
137+
138+
/**
139+
* @param class-string<AbstractConfigurableTask> $class
140+
* @param array<string, mixed> $options
141+
*/
142+
#[DataProvider('validTaskOptionsProvider')]
143+
public function testValidTaskOptionIsAccepted(string $class, array $options): void
144+
{
145+
$this->expectNotToPerformAssertions();
146+
147+
$this->initializeTask($class, $options);
148+
}
149+
150+
/**
151+
* @param class-string<ConfigurableTransformerInterface> $class
152+
* @param array<string, mixed> $options
153+
*
154+
* @return array<string, mixed>
155+
*/
156+
private function resolveTransformerOptions(string $class, array $options): array
157+
{
158+
$transformer = ArrayFilterTransformer::class === $class
159+
? new ArrayFilterTransformer(PropertyAccess::createPropertyAccessor())
160+
: new $class();
161+
$resolver = new OptionsResolver();
162+
$transformer->configureOptions($resolver);
163+
164+
return $resolver->resolve($options);
165+
}
166+
167+
/**
168+
* @param class-string<AbstractConfigurableTask> $class
169+
* @param array<string, mixed> $options
170+
*/
171+
private function initializeTask(string $class, array $options): void
172+
{
173+
$task = match ($class) {
174+
NormalizerTask::class, SerializerTask::class, DeserializerTask::class => new $class(new Serializer()),
175+
ObjectUpdaterTask::class => new ObjectUpdaterTask(PropertyAccess::createPropertyAccessor()),
176+
default => new $class(),
177+
};
178+
179+
$processConfiguration = new ProcessConfiguration('test', []);
180+
$state = new ProcessState($processConfiguration, new ProcessHistory($processConfiguration));
181+
$state->setContextualOptionResolver(new ContextualOptionResolver());
182+
$state->setContext([]);
183+
$state->setTaskConfiguration(new TaskConfiguration('task', $class, $options));
184+
$task->initialize($state);
185+
}
186+
}

0 commit comments

Comments
 (0)