Skip to content

Commit

Permalink
Merge pull request #46 from teamneusta/feature/ArrayConvertingPopulator
Browse files Browse the repository at this point in the history
New Feature: ArrayConvertingPopulator
  • Loading branch information
mike4git committed Aug 15, 2023
2 parents c736c4f + 01c1109 commit 9b95e15
Show file tree
Hide file tree
Showing 17 changed files with 762 additions and 21 deletions.
8 changes: 8 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,11 @@ services:
neusta_converter.converting_populator:
abstract: true
class: Neusta\ConverterBundle\Populator\ConvertingPopulator

neusta_converter.array_property_mapping_populator:
abstract: true
class: Neusta\ConverterBundle\Populator\ArrayPropertyMappingPopulator

neusta_converter.array_converting_populator:
abstract: true
class: Neusta\ConverterBundle\Populator\ArrayConvertingPopulator
188 changes: 171 additions & 17 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,21 +98,21 @@ To put things together register the factory and populator as services:
```yaml
# config/services.yaml
services:
YourNamespace\PersonFactory: ~
YourNamespace\PersonNamePopulator: ~
YourNamespace\PersonFactory: ~
YourNamespace\PersonNamePopulator: ~
```

And then declare the following converter in your package config:

```yaml
# config/packages/neusta_converter.yaml
neusta_converter:
converter:
person.converter:
target_factory: YourNamespace\PersonFactory
populators:
- YourNamespace\PersonNamePopulator
# additional populators may follow
converter:
person.converter:
target_factory: YourNamespace\PersonFactory
populators:
- YourNamespace\PersonNamePopulator
# additional populators may follow
```

> Note: You can use a custom implementation of the `Converter` interface via the `converter` keyword.
Expand All @@ -129,23 +129,23 @@ You can use it in your converter config via the `properties` keyword:
```yaml
# config/packages/neusta_converter.yaml
neusta_converter:
converter:
person.converter:
...
properties:
email: ~
phoneNumber: phone
converter:
person.converter:
...
properties:
email: ~
phoneNumber: phone
```

Which will populate
Which will populate

`email` (property of the target object)
`email` (property of the target object)

with `email` (property of the source object)

and

`phoneNumber` (property of the target object)
`phoneNumber` (property of the target object)

with `phone` (property of the source object).

Expand Down Expand Up @@ -197,6 +197,160 @@ $person = $this->converter->convert($user);

Conversion done.

## Special Populators

After a while you will recognize that a lot of scenarios in population are very similiar to each other. Some of them
could be done with the same populator except the target and the source property name.

### Converting Populator

Let's go on with the following extended model classes:

```php
class Address
{
private string $street;
private string $number;
private string $postalCode;
private string $city;
// ...
}

class User
{
// ...
private Address $address;
// ...
}
```

and the target type is `Person`:

```php
class PersonAddress
{
private string $streetWithNumber;
private string $postalCodeAndCity;
// ...
}

class Person
{
// ...
private PersonAddress $address;
// ...
}
```

If you have a situation as above and your User will have an address which should be populated into Person than you have
to write a Populator which

* gets the address from User,
* converts it into a PersonAddress object
* and sets it in Person.

The second step is typically a task for a (e.g. Address-) Converter.

Therefore we have a ConvertingPopulator which can easily be used:

```yaml
# config/packages/neusta_converter.yaml
neusta_converter:
converter:
person.converter:
...
populators:
- person.address.populator

address.converter:
...

...
person.address.populator:
class: Neusta\ConverterBundle\Populator\ConvertingPopulator
arguments:
$converter: '@address.converter'
$sourcePropertyName: 'address'
$targetPropertyName: 'address'
```

Be aware - that both properties have the same name should not lead you think they have the same type.
There is really an object conversion behind done by `address.converter`.

### ArrayConvertingPopulator

If you think that there is no 1:1 relation between User and Address (or corresponding Person and PersonAddress) but a 1:
n relation then the ConvertingPopulator can not be used.

In these cases we have implemented an extended version of it called `ArrayConvertingPopulator`.

This populator uses the same internal technique but expects to convert eac item of a source array of properties before
it will be set into the target object.

#### Example: User to Person

So imagine the addresses will now be an array of addresses (billing address, shipping addresses, contact
addresses, ...).

```php
class Address
{
private string $street;
private string $number;
private string $postalCode;
private string $city;
// ...
}

class User
{
// ...
private Address[] $addresses;
// ...
}
```

and the target type is `Person`:

```php
class PersonAddress
{
private string $streetWithNumber;
private string $postalCodeAndCity;
// ...
}

class Person
{
// ...
private PersonAddress[] $addresses;
// ...
}
```

Now you have to declare the following populator:
```yaml
# config/packages/neusta_converter.yaml
neusta_converter:
converter:
person.converter:
...
populators:
- person.addresses.populator

address.converter:
...

...
person.addresses.populator:
class: Neusta\ConverterBundle\Populator\ArrayConvertingPopulator
arguments:
$converter: '@address.converter'
$sourcePropertyName: 'addresses'
$targetPropertyName: 'addresses'
```
There is no new converter but a different populator implementation for this.

## Context

Sometimes you will need parameterized conversion which is not depending on the objects themselves.
Expand Down
2 changes: 2 additions & 0 deletions src/DependencyInjection/NeustaConverterExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ private function registerConverterConfiguration(string $id, array $config, Conta
->setArguments([
'$targetProperty' => $targetProperty,
'$sourceProperty' => $sourceProperty ?? $targetProperty,
'$mapper' => null,
'$accessor' => new Reference('property_accessor'),
]);
}
Expand All @@ -49,6 +50,7 @@ private function registerConverterConfiguration(string $id, array $config, Conta
->setArguments([
'$targetProperty' => $targetProperty,
'$sourceProperty' => $sourceProperty ?? $targetProperty,
'$mapper' => null,
'$accessor' => new Reference('property_accessor'),
]);
}
Expand Down
58 changes: 58 additions & 0 deletions src/Populator/ArrayConvertingPopulator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types=1);

namespace Neusta\ConverterBundle\Populator;

use Neusta\ConverterBundle\Converter;
use Neusta\ConverterBundle\Exception\PopulationException;
use Neusta\ConverterBundle\Populator;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

/**
* A populator that uses a converter to convert a field of type array<TInnerSource> from TSource
* into an object of type array<TInnerTarget> for a field of TTarget.
*
* @template TSource of object
* @template TTarget of object
* @template TContext of object|null
*
* @implements Populator<TSource, TTarget, TContext>
*/
final class ArrayConvertingPopulator implements Populator
{
private ArrayPropertyMappingPopulator $populator;

/**
* @template TInnerSource of object
* @template TInnerTarget of object
*
* @param Converter<TInnerSource, TInnerTarget, TContext> $converter
*/
public function __construct(
Converter $converter,
string $sourceArrayPropertyName,
string $targetPropertyName,
?string $sourceArrayItemPropertyName = null,
PropertyAccessorInterface $itemAccessor = null,
PropertyAccessorInterface $accessor = null,
)
{
$this->populator = new ArrayPropertyMappingPopulator(
$targetPropertyName,
$sourceArrayPropertyName,
$sourceArrayItemPropertyName,
\Closure::fromCallable([$converter, 'convert']),
$itemAccessor,
$accessor,
);
}

/**
* @throws PopulationException
*/
public function populate(object $target, object $source, ?object $ctx = null): void
{
$this->populator->populate($target, $source, $ctx);
}
}
75 changes: 75 additions & 0 deletions src/Populator/ArrayPropertyMappingPopulator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

declare(strict_types=1);

namespace Neusta\ConverterBundle\Populator;

use Neusta\ConverterBundle\Exception\PopulationException;
use Neusta\ConverterBundle\Populator;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;

/**
* @template TSource of object
* @template TTarget of object
* @template TContext of object|null
*
* @implements Populator<TSource, TTarget, TContext>
*/
final class ArrayPropertyMappingPopulator implements Populator
{
/** @var \Closure(mixed, TContext=):mixed */
private \Closure $mapper;
private PropertyAccessorInterface $itemAccessor;
private PropertyAccessorInterface $accessor;

/**
* @param \Closure(mixed, TContext=):mixed|null $mapper
*/
public function __construct(
private string $targetProperty,
private string $sourceArrayProperty,
private ?string $sourceArrayItemProperty = null,
?\Closure $mapper = null,
PropertyAccessorInterface $itemAccessor = null,
PropertyAccessorInterface $accessor = null,
)
{
$this->mapper = $mapper ?? static fn($v) => $v;
$this->itemAccessor = $itemAccessor ?? PropertyAccess::createPropertyAccessor();
$this->accessor = $accessor ?? PropertyAccess::createPropertyAccessor();
}

/**
* @throws PopulationException
*/
public function populate(object $target, object $source, ?object $ctx = null): void
{
try {
$unwrappedArray = array_map(
function ($arrayItem) {
if (!empty($this->sourceArrayItemProperty)) {
return $this->itemAccessor->getValue($arrayItem, $this->sourceArrayItemProperty);
}
return $arrayItem;
},
$this->accessor->getValue($source, $this->sourceArrayProperty)
);


$this->accessor->setValue(
$target,
$this->targetProperty,
array_map(
function ($item) use ($ctx) {
return ($this->mapper)($item, $ctx);
},
$unwrappedArray
)
);
} catch (\Throwable $exception) {
throw new PopulationException($this->sourceArrayProperty, $this->targetProperty, $exception);
}
}

}
4 changes: 3 additions & 1 deletion src/Populator/PropertyMappingPopulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,9 @@ public function __construct(
public function populate(object $target, object $source, ?object $ctx = null): void
{
try {
$this->accessor->setValue($target, $this->targetProperty,
$this->accessor->setValue(
$target,
$this->targetProperty,
($this->mapper)($this->accessor->getValue($source, $this->sourceProperty), $ctx),
);
} catch (\Throwable $exception) {
Expand Down

0 comments on commit 9b95e15

Please sign in to comment.