Skip to content

Commit

Permalink
Add GenericTargetFactory and related configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
jdreesen committed Jan 31, 2023
1 parent efc200a commit c749382
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 34 deletions.
41 changes: 10 additions & 31 deletions docs/usage.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
## Usage

After the bundle is activated you can directly use it by implementing a factory and a populator for your target and
source types.
After the bundle is activated you can directly use it by implementing populators for your target and source types.

Imagine your source type is `User`:

Expand Down Expand Up @@ -41,31 +40,6 @@ Separation of Concerns.

You should use the Converter-and-Populator-pattern. But how?!

Implement the following three artifacts:

### Factory

Implement a comfortable factory for your target type:

```php
use Neusta\ConverterBundle\Converter\Context\GenericContext;
use Neusta\ConverterBundle\TargetFactory;

/**
* @implements TargetFactory<Person, GenericContext>
*/
class PersonFactory implements TargetFactory
{
public function create(?object $ctx = null): Person
{
return new Person();
}
}
```

Skip thinking about the converter context at the moment. It will help you...
maybe not now but in a few weeks. You will see.

### Populators

Implement one or several populators:
Expand All @@ -91,25 +65,27 @@ As you can see implementation here is quite simple - just concatenation of two a
But however transformation will become more and more complex it should be done in a testable,
separated Populator or in several of them.

Skip thinking about the converter context at the moment. It will help you...
maybe not now but in a few weeks. You will see.

### Configuration

To put things together register the factory and populator as services:
First register the populator as a service:

```yaml
# config/services.yaml
services:
YourNamespace\PersonFactory: ~
YourNamespace\PersonNamePopulator: ~
```
And then declare the following converter in your package config:
Then declare the following converter in your package config:
```yaml
# config/packages/neusta_converter.yaml
neusta_converter:
converter:
person.converter:
target_factory: YourNamespace\PersonFactory
target: YourNamespace\Person
populators:
- YourNamespace\PersonNamePopulator
# additional populators may follow
Expand All @@ -118,6 +94,9 @@ neusta_converter:
> Note: You can use a custom implementation of the `Converter` interface via the `converter` keyword.
> Its constructor must contain the two parameters `TargetFactory $factory` and `array $populators`.
> Note: You can use a custom implementation of the `TargetTypeFactory` interface via the `target_factory` keyword,
> if you have special needs when creating the target object.
#### Mapping properties

If you just want to map a single property from the source to the target without transforming it in between, you don't
Expand Down
9 changes: 7 additions & 2 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ private function addConverterSection(ArrayNodeDefinition $rootNode): void
->info('Class name of the "Converter" implementation')
->defaultValue(GenericConverter::class)
->end()
->scalarNode('target')
->info('Class name of the target')
->end()
->scalarNode('target_factory')
->info('Service id of the "TargetFactory"')
->isRequired()
->cannotBeEmpty()
->end()
->arrayNode('populators')
->info('Service ids of the "Populator"s')
Expand All @@ -53,6 +54,10 @@ private function addConverterSection(ArrayNodeDefinition $rootNode): void
->prototype('scalar')->end()
->end()
->end()
->validate()
->ifTrue(fn (array $c) => !isset($c['target']) && !isset($c['target_factory']))
->thenInvalid('Either "target" or "target_factory" must be defined.')
->end()
->validate()
->ifTrue(fn (array $c) => empty($c['populators']) && empty($c['properties']))
->thenInvalid('At least one "populator" or "property" must be defined.')
Expand Down
10 changes: 9 additions & 1 deletion src/DependencyInjection/NeustaConverterExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Neusta\ConverterBundle\Converter;
use Neusta\ConverterBundle\Populator\PropertyMappingPopulator;
use Neusta\ConverterBundle\TargetFactory\GenericTargetFactory;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
Expand All @@ -32,6 +33,13 @@ public function loadInternal(array $config, ContainerBuilder $container): void
*/
private function registerConverterConfiguration(string $id, array $config, ContainerBuilder $container): void
{
if (!$targetFactoryId = $config['target_factory'] ?? null) {
$container->register($targetFactoryId = "{$id}.target_factory", GenericTargetFactory::class)
->setArguments([
'$type' => new Reference($config['target']),
]);
}

foreach ($config['properties'] ?? [] as $targetProperty => $sourceProperty) {
$config['populators'][] = $propertyPopulatorId = "{$id}.populator.{$targetProperty}";
$container->register($propertyPopulatorId, PropertyMappingPopulator::class)
Expand All @@ -46,7 +54,7 @@ private function registerConverterConfiguration(string $id, array $config, Conta
$container->register($id, $config['converter'])
->setPublic(true)
->setArguments([
'$factory' => new Reference($config['target_factory']),
'$factory' => new Reference($targetFactoryId),
'$populators' => array_map(
static fn (string $populator) => new Reference($populator),
$config['populators'],
Expand Down
43 changes: 43 additions & 0 deletions src/TargetFactory/GenericTargetFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

declare(strict_types=1);

namespace Neusta\ConverterBundle\TargetFactory;

use Neusta\ConverterBundle\TargetFactory;

/**
* @template T of object
*
* @implements TargetFactory<T, object>
*/
final class GenericTargetFactory implements TargetFactory
{
/** @var \ReflectionClass<T> */
private \ReflectionClass $type;

/**
* @param class-string<T> $type
*/
public function __construct(string $type)
{
$this->type = new \ReflectionClass($type);

if (!$this->type->isInstantiable()) {
throw new \InvalidArgumentException(sprintf('Target class "%s" is not instantiable.', $type));
}

if ($this->type->getConstructor()?->getNumberOfRequiredParameters()) {
throw new \InvalidArgumentException(sprintf('Target class "%s" has required constructor parameters.', $type));
}
}

public function create(?object $ctx = null): object
{
try {
return $this->type->newInstance();
} catch (\ReflectionException $e) {
throw new \LogicException(sprintf('Cannot create new instance of "%s" because: %s', $this->type->getName(), $e->getMessage()), 0, $e);
}
}
}
25 changes: 25 additions & 0 deletions tests/DependencyInjection/NeustaConverterExtensionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
use Neusta\ConverterBundle\DependencyInjection\NeustaConverterExtension;
use Neusta\ConverterBundle\NeustaConverterBundle;
use Neusta\ConverterBundle\Populator\PropertyMappingPopulator;
use Neusta\ConverterBundle\TargetFactory\GenericTargetFactory;
use Neusta\ConverterBundle\Tests\Fixtures\Model\PersonFactory;
use Neusta\ConverterBundle\Tests\Fixtures\Model\Person;
use Neusta\ConverterBundle\Tests\Fixtures\Populator\PersonNamePopulator;
use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand Down Expand Up @@ -42,6 +44,29 @@ public function test_with_generic_converter(): void
self::assertIsReference(PersonNamePopulator::class, $converter->getArgument('$populators')[0]);
}

public function test_with_generic_target_factory(): void
{
$container = $this->buildContainer([
'converter' => [
'foobar' => [
'target' => Person::class,
'populators' => [
PersonNamePopulator::class,
],
],
],
]);

// converter
$converter = $container->getDefinition('foobar');
self::assertIsReference('foobar.target_factory', $converter->getArgument('$factory'));

// target type factory
$factory = $container->getDefinition('foobar.target_factory');
self::assertSame(GenericTargetFactory::class, $factory->getClass());
self::assertIsReference(Person::class, $factory->getArgument('$type'));
}

public function test_with_mapped_properties(): void
{
$container = $this->buildContainer([
Expand Down

0 comments on commit c749382

Please sign in to comment.