Skip to content

Commit a8005eb

Browse files
feat(transformer) #117 MappingTransformer: deprecate writing a nested path target property (e.g. address.city) as a literal key of an array destination, it will throw in v6.0
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 23a667b commit a8005eb

4 files changed

Lines changed: 102 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Latest
55
* [#197](https://github.com/cleverage/process-bundle/issues/197) Add missing tests: every Task and Transformer is now covered by unit tests. Remove the obsolete `tests.old` directory.
66
* [#143](https://github.com/cleverage/process-bundle/issues/143) Improve PHPStan configuration: remove all `ignoreErrors` and `@phpstan-ignore` comments (report unmatched ignored errors again), add missing iterable value types and generic types in PHPDoc, remove unreachable code in AbstractIterableOutputTask and InputAggregatorTask.
77
* [#143](https://github.com/cleverage/process-bundle/issues/143) Improve PHPStan level from 6 to 7, and fix the level 8 errors that do not require a signature change (the remaining ones, due to nullable return types such as `AbstractConfigurableTask::getOptions(): ?array`, will be fixed in v6.0). Default to a `ContextualOptionResolver` in ProcessState and to a property accessor in ConditionTrait when none is set.
8+
* [#117](https://github.com/cleverage/process-bundle/issues/117) MappingTransformer: writing a target property that is a nested path (e.g. `address.city`) on an array destination, which silently created an `address.city` literal key, is deprecated and will throw an `\UnexpectedValueException` in v6.0. Use `[address.city]` for a literal key or `[address][city]` for a nested array; a simple property name (e.g. `name`) is still added as a key. Update documentation, add tests.
89

910
## Fixes
1011
* [#143](https://github.com/cleverage/process-bundle/issues/143) Fix InputIteratorTask: an `\IteratorAggregate` input whose `getIterator()` does not return an `\Iterator` (e.g. another `\IteratorAggregate`) is iterated instead of failing with a `TypeError`. Update documentation, add tests.

‎docs/reference/transformers/mapping_transformer.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,10 @@ Notes
137137
instead of throwing for a missing array index (`framework.property_access.throw_exception_on_invalid_index`), so
138138
`ignore_missing` mostly matters for objects.
139139
* When a sub-transformer fails, the thrown `TransformerException` reports the target property.
140+
* On an array destination, a target property that is not an index notation is added as a literal key: a simple
141+
property name (e.g. `name`) gives the `name` key. Doing so with a nested path (e.g. `address.city`, `address[city]`)
142+
is deprecated and will throw an `\UnexpectedValueException` in v6.0: use `[address.city]` to keep a literal key, or
143+
`[address][city]` to write in a nested array.
140144
* A missing property of a `\stdClass` destination is added only for a simple target property name (e.g. `name`): a
141145
nested path (e.g. `address.city`) or an index notation (e.g. `[name]`) that is not writable throws an
142146
`\UnexpectedValueException` (`Property '...' is not writable`), as for any other object.

‎src/Transformer/MappingTransformer.php‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,14 @@ public function transform(mixed $value, array $options = []): mixed
114114
} elseif ($this->accessor->isWritable($result, $targetProperty)) {
115115
$this->accessor->setValue($result, $targetProperty, $transformedValue);
116116
} elseif (\is_array($result)) {
117+
if (!$this->isSimplePropertyName($targetProperty)) {
118+
@trigger_error(
119+
"Setting the target property '{$targetProperty}' as a literal key of an array destination is deprecated, it will throw an \\UnexpectedValueException in v6.0. Use '[{$targetProperty}]' to keep a literal key, or the index notation for nested arrays (e.g. '[a][b]').",
120+
\E_USER_DEPRECATED
121+
);
122+
}
117123
$result[$targetProperty] = $transformedValue;
118-
} elseif ($result instanceof \stdClass && 1 === preg_match('/^[^.[\]]+$/', $targetProperty)) {
124+
} elseif ($result instanceof \stdClass && $this->isSimplePropertyName($targetProperty)) {
119125
// Only a simple property name can be added to a \stdClass, nested paths are not created
120126
$result->{$targetProperty} = $transformedValue;
121127
} else {
@@ -195,6 +201,15 @@ protected function extractInputValue(mixed $input, string $sourceProperty): mixe
195201
return $this->accessor->getValue($input, $sourceProperty);
196202
}
197203

204+
/**
205+
* A simple property name (e.g. "name") is neither a nested path (e.g. "address.city") nor an index notation (e.g.
206+
* "[name]").
207+
*/
208+
private function isSimplePropertyName(string $property): bool
209+
{
210+
return 1 === preg_match('/^[^.[\]]+$/', $property);
211+
}
212+
198213
/**
199214
* Wrap error handling when there is an property access error.
200215
*

‎tests/Transformer/MappingTransformerTest.php‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@
3636
#[\PHPUnit\Framework\Attributes\UsesClass(TransformerException::class)]
3737
class MappingTransformerTest extends TestCase
3838
{
39+
/**
40+
* @var list<string>
41+
*/
42+
private array $deprecations = [];
43+
3944
public function testGetCode(): void
4045
{
4146
self::assertSame('mapping', $this->createTransformer()->getCode());
@@ -423,6 +428,65 @@ public function testMissingNonSimpleTargetPropertyOfAStdClassThrows(string $targ
423428
$transformer->transform([], $options);
424429
}
425430

431+
public function testSimpleTargetPropertyIsAddedToAnArrayDestinationWithoutDeprecation(): void
432+
{
433+
$transformer = $this->createTransformer();
434+
$options = $this->resolveOptions($transformer, [
435+
'mapping' => [
436+
'field2' => ['code' => '[field]'],
437+
],
438+
]);
439+
440+
$result = $this->transformCollectingDeprecations($transformer, ['field' => 'value'], $options);
441+
442+
self::assertSame(['field2' => 'value'], $result);
443+
self::assertSame([], $this->deprecations);
444+
}
445+
446+
/**
447+
* @return iterable<string, array{string}>
448+
*/
449+
public static function nonSimpleArrayTargetPropertyProvider(): iterable
450+
{
451+
yield 'nested path' => ['field2.child'];
452+
yield 'index after a property' => ['field2[child]'];
453+
yield 'property after an index' => ['[field2].child'];
454+
}
455+
456+
#[DataProvider('nonSimpleArrayTargetPropertyProvider')]
457+
public function testNonSimpleTargetPropertyOfAnArrayDestinationIsDeprecated(string $targetProperty): void
458+
{
459+
$transformer = $this->createTransformer();
460+
$options = $this->resolveOptions($transformer, [
461+
'mapping' => [
462+
$targetProperty => ['code' => '[field]'],
463+
],
464+
]);
465+
466+
$result = $this->transformCollectingDeprecations($transformer, ['field' => 'value'], $options);
467+
468+
self::assertSame([$targetProperty => 'value'], $result);
469+
self::assertSame([
470+
"Setting the target property '{$targetProperty}' as a literal key of an array destination is deprecated, it will throw an \\UnexpectedValueException in v6.0. Use '[{$targetProperty}]' to keep a literal key, or the index notation for nested arrays (e.g. '[a][b]').",
471+
], $this->deprecations);
472+
}
473+
474+
public function testIndexNotationKeepsALiteralKeyWithoutDeprecation(): void
475+
{
476+
$transformer = $this->createTransformer();
477+
$options = $this->resolveOptions($transformer, [
478+
'mapping' => [
479+
'[field2.child]' => ['code' => '[field]'],
480+
'[field3][child]' => ['code' => '[field]'],
481+
],
482+
]);
483+
484+
$result = $this->transformCollectingDeprecations($transformer, ['field' => 'value'], $options);
485+
486+
self::assertSame(['field2.child' => 'value', 'field3' => ['child' => 'value']], $result);
487+
self::assertSame([], $this->deprecations);
488+
}
489+
426490
public function testKeepInputCopiesAnArrayInput(): void
427491
{
428492
$transformer = $this->createTransformer();
@@ -565,6 +629,23 @@ public function log($level, \Stringable|string $message, array $context = []): v
565629
};
566630
}
567631

632+
/**
633+
* @param array<string, mixed> $options
634+
*/
635+
private function transformCollectingDeprecations(MappingTransformer $transformer, mixed $value, array $options): mixed
636+
{
637+
set_error_handler(function (int $errno, string $errstr): bool {
638+
$this->deprecations[] = $errstr;
639+
640+
return true;
641+
}, \E_USER_DEPRECATED);
642+
try {
643+
return $transformer->transform($value, $options);
644+
} finally {
645+
restore_error_handler();
646+
}
647+
}
648+
568649
/**
569650
* @param array<string, mixed> $options
570651
*

0 commit comments

Comments
 (0)