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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Latest
* [#240](https://github.com/cleverage/process-bundle/issues/240) Fix FileMoverTask: with `autoincrement`, the suffix was computed on the whole path (a dot in a directory name was taken as the extension, making the move fail for a file without extension) and an existing numeric suffix of the file name was replaced (`report-2024.csv` became `report-1.csv`). The suffix is now appended to the file name only (`report-2024-1.csv`, `file-1`, `.env-1`). Update documentation, add tests.
* [#243](https://github.com/cleverage/process-bundle/issues/243) Fix RecursivePropertySetterTransformer: a `\stdClass` item without the property was replaced by a copy in the output, so the input object was not modified; the property is now added to the item itself. Update documentation, add tests.
* [#242](https://github.com/cleverage/process-bundle/issues/242) Fix InputFileReaderTask: an input that is not a non-empty string (e.g. `null`) throws an explicit `\UnexpectedValueException` (`No file path given as input`) instead of a PHP warning followed by a `TypeError`. Update documentation, add tests.
* [#244](https://github.com/cleverage/process-bundle/issues/244) Fix MappingTransformer: a missing target property of a `\stdClass` destination (`initial_value` or `keep_input`) threw `Property '...' is not writable`, it is now added when the target is a simple property name (nested paths still throw). Update documentation, add tests.

v5.1
-----
Expand Down
8 changes: 6 additions & 2 deletions docs/reference/transformers/mapping_transformer.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ The algorithm is:
* for each target property of `mapping`:
- get the source value (from `constant`, `set_null`, or the `code` property path(s))
- apply the property `transformers` on this value
- write the result into the destination (with `merge_callback`, the property accessor, or as a simple array key)
- write the result into the destination (with `merge_callback`, the property accessor, as a simple array key, or as
a new property of a `\stdClass`)

Transformer reference
---------------------
Expand All @@ -32,7 +33,7 @@ Options

| Code | Type | Required | Default | Description |
|------------------|------------------|:--------:|---------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `mapping` | `array` | **X** | | List of `target property => property options` (see below). The target is a writable property path of the destination, or a plain array key |
| `mapping` | `array` | **X** | | List of `target property => property options` (see below). The target is a writable property path of the destination, a plain array key or a `\stdClass` property |
| `ignore_missing` | `bool` | | `false` | Ignore property accessor read errors for the whole mapping (the property is then skipped) |
| `keep_input` | `bool` | | `false` | Use the input as the destination. Cannot be combined with a non-empty `initial_value`. Due to PHP behavior, arrays are copied while objects are modified in place |
| `initial_value` | `any` | | `[]` | The destination to fill |
Expand Down Expand Up @@ -136,5 +137,8 @@ Notes
instead of throwing for a missing array index (`framework.property_access.throw_exception_on_invalid_index`), so
`ignore_missing` mostly matters for objects.
* When a sub-transformer fails, the thrown `TransformerException` reports the target property.
* A missing property of a `\stdClass` destination is added only for a simple target property name (e.g. `name`): a
nested path (e.g. `address.city`) or an index notation (e.g. `[name]`) that is not writable throws an
`\UnexpectedValueException` (`Property '...' is not writable`), as for any other object.
* `merge_callback` receives the destination by value: to modify an array destination, the callable must take its
first argument by reference.
3 changes: 3 additions & 0 deletions src/Transformer/MappingTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ public function transform(mixed $value, array $options = []): mixed
$this->accessor->setValue($result, $targetProperty, $transformedValue);
} elseif (\is_array($result)) {
$result[$targetProperty] = $transformedValue;
} elseif ($result instanceof \stdClass && 1 === preg_match('/^[^.[\]]+$/', $targetProperty)) {
// Only a simple property name can be added to a \stdClass, nested paths are not created
$result->{$targetProperty} = $transformedValue;
} else {
throw new \UnexpectedValueException("Property '{$targetProperty}' is not writable");
}
Expand Down
59 changes: 59 additions & 0 deletions tests/Transformer/MappingTransformerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use CleverAge\ProcessBundle\Transformer\CallbackTransformer;
use CleverAge\ProcessBundle\Transformer\MappingTransformer;
use CleverAge\ProcessBundle\Transformer\TransformerTrait;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Psr\Log\AbstractLogger;
use Psr\Log\LoggerInterface;
Expand Down Expand Up @@ -364,6 +365,64 @@ public function testInitialValueCanBeAnObject(): void
self::assertEquals((object) ['field2' => 'value'], $result);
}

public function testNewPropertyIsAddedToAStdClassInitialValue(): void
{
$transformer = $this->createTransformer();
$options = $this->resolveOptions($transformer, [
'initial_value' => new \stdClass(),
'mapping' => [
'field2' => ['code' => '[field]'],
],
]);

$result = $transformer->transform(['field' => 'value'], $options);

self::assertEquals((object) ['field2' => 'value'], $result);
}

public function testKeepInputAddsANewPropertyToAStdClassInput(): void
{
$transformer = $this->createTransformer();
$options = $this->resolveOptions($transformer, [
'keep_input' => true,
'mapping' => [
'field2' => ['code' => 'field'],
],
]);
$input = (object) ['field' => 'value'];

$result = $transformer->transform($input, $options);

self::assertSame($input, $result);
self::assertEquals((object) ['field' => 'value', 'field2' => 'value'], $input);
}

/**
* @return iterable<string, array{string}>
*/
public static function nonSimpleTargetPropertyProvider(): iterable
{
yield 'nested path' => ['field2.child'];
yield 'index notation' => ['[field2]'];
}

#[DataProvider('nonSimpleTargetPropertyProvider')]
public function testMissingNonSimpleTargetPropertyOfAStdClassThrows(string $targetProperty): void
{
$transformer = $this->createTransformer();
$options = $this->resolveOptions($transformer, [
'initial_value' => new \stdClass(),
'mapping' => [
$targetProperty => ['constant' => 'value'],
],
]);

$this->expectException(\UnexpectedValueException::class);
$this->expectExceptionMessage("Property '{$targetProperty}' is not writable");

$transformer->transform([], $options);
}

public function testKeepInputCopiesAnArrayInput(): void
{
$transformer = $this->createTransformer();
Expand Down
Loading