Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Feature: ArrayConvertingPopulator #46

Merged
merged 7 commits into from
Aug 15, 2023
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
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
mike4git marked this conversation as resolved.
Show resolved Hide resolved

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
Loading