diff --git a/config/packages/neusta_converter.yaml b/config/packages/neusta_converter.yaml new file mode 100644 index 0000000..f47b464 --- /dev/null +++ b/config/packages/neusta_converter.yaml @@ -0,0 +1,25 @@ +neusta_converter: + converter: + test.person.converter: +# converter: Neusta\ConverterBundle\Converter\GenericConverter + target_factory: Neusta\ConverterBundle\Tests\Fixtures\Model\PersonFactory + populators: + - Neusta\ConverterBundle\Tests\Fixtures\Populator\PersonNamePopulator +# properties: +# fullName: ~ +# age: ageInYears +# context: +# group: ~ # same property name +# locale: language # different property names + + test.contactnumber.converter: + target_factory: Neusta\ConverterBundle\Tests\Fixtures\Model\ContactNumberFactory + properties: + phoneNumber: number + + populators: + test.person.address.populator: + converter: test.address.converter + property: + address: ~ + diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index f39f187..a0123da 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -5,6 +5,7 @@ namespace Neusta\ConverterBundle\DependencyInjection; use Neusta\ConverterBundle\Converter\GenericConverter; +use Neusta\ConverterBundle\Populator; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -17,6 +18,7 @@ public function getConfigTreeBuilder(): TreeBuilder $rootNode = $treeBuilder->getRootNode(); $this->addConverterSection($rootNode); + $this->addPopulatorSection($rootNode); return $treeBuilder; } @@ -68,4 +70,41 @@ private function addConverterSection(ArrayNodeDefinition $rootNode): void ->end() ; } + + private function addPopulatorSection(ArrayNodeDefinition $rootNode): void + { + $rootNode + ->children() + ->arrayNode('populators') + ->info('Populator configuration') + ->normalizeKeys(false) + ->useAttributeAsKey('name') + ->arrayPrototype() + ->children() + ->scalarNode('class') + ->info('class of the "Populator" implementation') + ->defaultValue(Populator\ConvertingPopulator::class) + ->end() + ->scalarNode('converter') + ->info('Service id of the internal "Converter"') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->arrayNode('property') + ->info('Mapping of source property (value) to target property (key)') + ->normalizeKeys(false) + ->useAttributeAsKey('target') + ->prototype('scalar') + ->isRequired() + ->end() + ->end() + ->end() + ->validate() + ->ifTrue(fn (array $c) => empty($c['converter']) && empty($c['property'])) + ->thenInvalid('A "converter" and "property" must be defined.') + ->end() + ->end() + ->end() + ; + } } diff --git a/src/DependencyInjection/NeustaConverterExtension.php b/src/DependencyInjection/NeustaConverterExtension.php index f1630ff..079322f 100644 --- a/src/DependencyInjection/NeustaConverterExtension.php +++ b/src/DependencyInjection/NeustaConverterExtension.php @@ -5,12 +5,15 @@ namespace Neusta\ConverterBundle\DependencyInjection; use Neusta\ConverterBundle\Converter; +use Neusta\ConverterBundle\Populator\ArrayConvertingPopulator; use Neusta\ConverterBundle\Populator\ContextMappingPopulator; +use Neusta\ConverterBundle\Populator\ConvertingPopulator; use Neusta\ConverterBundle\Populator\PropertyMappingPopulator; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\TypedReference; use Symfony\Component\HttpKernel\DependencyInjection\ConfigurableExtension; final class NeustaConverterExtension extends ConfigurableExtension @@ -26,6 +29,10 @@ public function loadInternal(array $mergedConfig, ContainerBuilder $container): foreach ($mergedConfig['converter'] as $converterId => $converter) { $this->registerConverterConfiguration($converterId, $converter, $container); } + + foreach ($mergedConfig['populators'] as $populatorId => $populator) { + $this->registerPopulatorConfiguration($populatorId, $populator, $container); + } } /** @@ -67,6 +74,63 @@ private function registerConverterConfiguration(string $id, array $config, Conta ]); } + /** + * @param array $config + */ + private function registerPopulatorConfiguration(string $id, array $config, ContainerBuilder $container): void + { + $arguments = []; + if (empty($config['class']) || ConvertingPopulator::class === $config['class']) { + $arguments = $this->buildArgumentsForConvertingPopulator($config); + } elseif (ArrayConvertingPopulator::class === $config['class']) { + $arguments = $this->buildArgumentsForArrayConvertingPopulator($config); + } + + $container->register($id, $config['class']) + ->setPublic(true) + ->setArguments($arguments); + } + + /** + * @param array $config + * + * @return array + */ + private function buildArgumentsForConvertingPopulator(array $config): array + { + $targetProperty = array_key_first($config['property']); + $sourceProperty = $config['property'][$targetProperty]; + $sourceProperty = $sourceProperty ?? $targetProperty; + + return + [ + '$converter' => new TypedReference($config['converter'], Converter::class), + '$sourcePropertyName' => $sourceProperty, + '$targetPropertyName' => $targetProperty, + '$accessor' => new Reference('property_accessor'), + ]; + } + + /** + * @param array $config + * + * @return array + */ + private function buildArgumentsForArrayConvertingPopulator(array $config): array + { + $innerPropertyArgument = []; + $innerProperty = $config['property']['itemProperty']; + $innerPropertyArgument['$sourceArrayItemPropertyName'] = $innerProperty; + unset($config['property']['itemProperty']); + + $arguments = array_merge( + $innerPropertyArgument, + $this->buildArgumentsForConvertingPopulator($config), + ); + + return $arguments; + } + private function appendSuffix(string $value, string $suffix): string { return str_ends_with($value, $suffix) ? $value : $value . $suffix; diff --git a/tests/DependencyInjection/NeustaConverterExtensionTest.php b/tests/DependencyInjection/NeustaConverterExtensionTest.php index b6b3c66..6f0c477 100644 --- a/tests/DependencyInjection/NeustaConverterExtensionTest.php +++ b/tests/DependencyInjection/NeustaConverterExtensionTest.php @@ -8,7 +8,9 @@ use Neusta\ConverterBundle\Converter\GenericConverter; use Neusta\ConverterBundle\DependencyInjection\NeustaConverterExtension; use Neusta\ConverterBundle\NeustaConverterBundle; +use Neusta\ConverterBundle\Populator\ArrayConvertingPopulator; use Neusta\ConverterBundle\Populator\ContextMappingPopulator; +use Neusta\ConverterBundle\Populator\ConvertingPopulator; use Neusta\ConverterBundle\Populator\PropertyMappingPopulator; use Neusta\ConverterBundle\Tests\Fixtures\Model\PersonFactory; use Neusta\ConverterBundle\Tests\Fixtures\Populator\PersonNamePopulator; @@ -16,6 +18,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\TypedReference; class NeustaConverterExtensionTest extends TestCase { @@ -123,6 +126,122 @@ public function test_with_mapped_context(): void self::assertSame('age', $ageInYearsPopulator->getArgument('$contextProperty')); } + public function test_with_converting_populator(): void + { + $container = $this->buildContainer([ + 'populators' => [ + 'foobar' => [ + 'converter' => GenericConverter::class, + 'property' => [ + 'targetTest' => 'sourceTest', + ], + ], + ], + ]); + + // converter + $populator = $container->getDefinition('foobar'); + + self::assertSame(ConvertingPopulator::class, $populator->getClass()); + self::assertTrue($populator->isPublic()); + self::assertInstanceOf(TypedReference::class, $populator->getArgument('$converter')); + self::assertSame('targetTest', $populator->getArgument('$targetPropertyName')); + self::assertSame('sourceTest', $populator->getArgument('$sourcePropertyName')); + } + + public function test_with_array_converting_populator(): void + { + $container = $this->buildContainer([ + 'populators' => [ + 'foobar' => [ + 'converter' => GenericConverter::class, + 'property' => [ + 'targetTest' => 'sourceTest', + ], + ], + ], + ]); + + // converter + $populator = $container->getDefinition('foobar'); + + self::assertSame(ConvertingPopulator::class, $populator->getClass()); + self::assertTrue($populator->isPublic()); + self::assertInstanceOf(TypedReference::class, $populator->getArgument('$converter')); + self::assertSame('targetTest', $populator->getArgument('$targetPropertyName')); + self::assertSame('sourceTest', $populator->getArgument('$sourcePropertyName')); + } + + public function test_with_array_converting_populator_with_inner_property(): void + { + $container = $this->buildContainer([ + 'populators' => [ + 'foobar' => [ + 'class' => ArrayConvertingPopulator::class, + 'converter' => GenericConverter::class, + 'property' => [ + 'targetTest' => 'sourceTest', + 'itemProperty' => 'value', + ], + ], + ], + ]); + + // converter + $populator = $container->getDefinition('foobar'); + + self::assertSame(ArrayConvertingPopulator::class, $populator->getClass()); + self::assertSame('value', $populator->getArgument('$sourceArrayItemPropertyName')); + } + + public function test_with_array_converting_populator_with_inner_property_same_name(): void + { + $container = $this->buildContainer([ + 'populators' => [ + 'foobar' => [ + 'class' => ArrayConvertingPopulator::class, + 'converter' => GenericConverter::class, + 'property' => [ + 'test' => null, // in yaml one will write ~ + 'itemProperty' => 'value', + ], + ], + ], + ]); + + // converter + $populator = $container->getDefinition('foobar'); + + self::assertSame(ArrayConvertingPopulator::class, $populator->getClass()); + self::assertSame('test', $populator->getArgument('$targetPropertyName')); + self::assertSame('test', $populator->getArgument('$sourcePropertyName')); + self::assertSame('value', $populator->getArgument('$sourceArrayItemPropertyName')); + } + + public function test_with_array_converting_populator_with_inner_property_first(): void + { + $container = $this->buildContainer([ + 'populators' => [ + 'foobar' => [ + 'class' => ArrayConvertingPopulator::class, + 'converter' => GenericConverter::class, + 'property' => [ + 'itemProperty' => 'value', + 'targetTest' => 'sourceTest', + ], + ], + ], + ]); + + // converter + $populator = $container->getDefinition('foobar'); + + self::assertSame(ArrayConvertingPopulator::class, $populator->getClass()); + self::assertSame('targetTest', $populator->getArgument('$targetPropertyName')); + self::assertSame('sourceTest', $populator->getArgument('$sourcePropertyName')); + self::assertSame('value', $populator->getArgument('$sourceArrayItemPropertyName')); + } + private static function assertIsReference(string $expected, mixed $actual): void { self::assertInstanceOf(Reference::class, $actual); diff --git a/tests/app/config/packages/neusta_converter.yaml b/tests/app/config/packages/neusta_converter.yaml index 868a0af..f47b464 100644 --- a/tests/app/config/packages/neusta_converter.yaml +++ b/tests/app/config/packages/neusta_converter.yaml @@ -16,3 +16,10 @@ neusta_converter: target_factory: Neusta\ConverterBundle\Tests\Fixtures\Model\ContactNumberFactory properties: phoneNumber: number + + populators: + test.person.address.populator: + converter: test.address.converter + property: + address: ~ + diff --git a/tests/app/config/services.yaml b/tests/app/config/services.yaml index 8ea99eb..81ea253 100644 --- a/tests/app/config/services.yaml +++ b/tests/app/config/services.yaml @@ -16,14 +16,6 @@ services: Neusta\ConverterBundle\Tests\Fixtures\Populator\PersonNamePopulator: ~ Neusta\ConverterBundle\Tests\Fixtures\Populator\AddressPopulator: ~ - test.person.address.populator: - parent: 'neusta_converter.converting_populator' - public: true - arguments: - $converter: '@test.address.converter' - $sourcePropertyName: 'address' - $targetPropertyName: 'address' - test.person.wrong.source.type.populator: parent: 'neusta_converter.converting_populator' public: true