Repository: janephp/janephp (php)

Issue:
[AutoMapper] Add custom transformer for Symfony Uid component classes
**Is your feature request related to a problem? Please describe.**
Actually, [Symfony Uid component](https://github.com/symfony/uid) has custom objects to handle Ulid and Uuid that can't be mapped automatically.

**Describe the solution you'd like**
Add a transformer for each of theses Uid implementation.


Pull request description:
[AutoMapper] Add Symfony Uid transformers
Fixes #490 

Add transformers for Symfony Uid component, works in all possible transformations:
- array -> object
- object -> array
- object -> object


Patch:
diff --git a/composer.json b/composer.json
index d32b1e95a..4bc4c3818 100644
--- a/composer.json
+++ b/composer.json
@@ -48,6 +48,7 @@
         "jane-php/open-api-runtime": "self.version"
     },
     "require-dev": {
+        "api-platform/core": "^2.5",
         "doctrine/annotations": "~1.0",
         "friendsofphp/php-cs-fixer": "2.17.2",
         "kriswallsmith/buzz": "^1.1",
@@ -59,7 +60,7 @@
         "sensio/framework-extra-bundle": "^5.6",
         "symfony/framework-bundle": "^4.4 || ^5.0",
         "symfony/twig-bundle": "^4.4 || ^5.0",
-        "api-platform/core": "^2.5"
+        "symfony/uid": "^5.2"
     },
     "suggest": {
         "friendsofphp/php-cs-fixer": "Allow to automatically fix cs on generated code for better visualisation",
diff --git a/src/Bundle/AutoMapperBundle/DependencyInjection/JaneAutoMapperExtension.php b/src/Bundle/AutoMapperBundle/DependencyInjection/JaneAutoMapperExtension.php
index e145b2748..45b5b05d1 100644
--- a/src/Bundle/AutoMapperBundle/DependencyInjection/JaneAutoMapperExtension.php
+++ b/src/Bundle/AutoMapperBundle/DependencyInjection/JaneAutoMapperExtension.php
@@ -8,12 +8,14 @@ use Jane\Component\AutoMapper\Extractor\FromTargetMappingExtractor;
 use Jane\Component\AutoMapper\MapperGeneratorMetadataFactory;
 use Jane\Component\AutoMapper\MapperGeneratorMetadataInterface;
 use Jane\Component\AutoMapper\Normalizer\AutoMapperNormalizer;
+use Jane\Component\AutoMapper\Transformer\SymfonyUidTransformerFactory;
 use Jane\Component\AutoMapper\Transformer\TransformerFactoryInterface;
 use Symfony\Component\Config\FileLocator;
 use Symfony\Component\DependencyInjection\ContainerBuilder;
 use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
 use Symfony\Component\DependencyInjection\Reference;
 use Symfony\Component\HttpKernel\DependencyInjection\Extension;
+use Symfony\Component\Uid\AbstractUid;
 
 class JaneAutoMapperExtension extends Extension
 {
@@ -34,6 +36,12 @@ class JaneAutoMapperExtension extends Extension
         $container->getDefinition(MapperGeneratorMetadataFactory::class)->replaceArgument(5, $config['date_time_format']);
         $container->registerForAutoconfiguration(TransformerFactoryInterface::class)->addTag('jane_auto_mapper.transformer_factory');
 
+        if (class_exists(AbstractUid::class)) {
+            $container
+                ->getDefinition(SymfonyUidTransformerFactory::class)
+                ->addTag('jane_auto_mapper.transformer_factory', ['priority' => '-1004']);
+        }
+
         if ($config['normalizer']) {
             $container
                 ->getDefinition(AutoMapperNormalizer::class)
diff --git a/src/Bundle/AutoMapperBundle/Resources/config/services.xml b/src/Bundle/AutoMapperBundle/Resources/config/services.xml
index 9bc145b91..46c5a2742 100644
--- a/src/Bundle/AutoMapperBundle/Resources/config/services.xml
+++ b/src/Bundle/AutoMapperBundle/Resources/config/services.xml
@@ -92,5 +92,7 @@
             <argument type="service" id="Jane\Component\AutoMapper\AutoMapperRegistryInterface" />
             <tag name="jane_auto_mapper.transformer_factory" priority="-1003" />
         </service>
+
+        <service id="Jane\Component\AutoMapper\Transformer\SymfonyUidTransformerFactory" />
     </services>
 </container>
diff --git a/src/Component/AutoMapper/AutoMapper.php b/src/Component/AutoMapper/AutoMapper.php
index d37e438c9..036c7a6d0 100644
--- a/src/Component/AutoMapper/AutoMapper.php
+++ b/src/Component/AutoMapper/AutoMapper.php
@@ -17,6 +17,7 @@ use Jane\Component\AutoMapper\Transformer\DateTimeTransformerFactory;
 use Jane\Component\AutoMapper\Transformer\MultipleTransformerFactory;
 use Jane\Component\AutoMapper\Transformer\NullableTransformerFactory;
 use Jane\Component\AutoMapper\Transformer\ObjectTransformerFactory;
+use Jane\Component\AutoMapper\Transformer\SymfonyUidTransformerFactory;
 use Jane\Component\AutoMapper\Transformer\TransformerFactoryInterface;
 use Jane\Component\AutoMapper\Transformer\UniqueTypeTransformerFactory;
 use PhpParser\ParserFactory;
@@ -27,6 +28,7 @@ use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
 use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
 use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader;
 use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
+use Symfony\Component\Uid\AbstractUid;
 
 /**
  * Maps a source data structure (object or array) to a target one.
@@ -260,6 +262,10 @@ class AutoMapper implements AutoMapperInterface, AutoMapperRegistryInterface, Ma
         $transformerFactory->addTransformerFactory(new ArrayTransformerFactory($transformerFactory));
         $transformerFactory->addTransformerFactory(new ObjectTransformerFactory($autoMapper));
 
+        if (class_exists(AbstractUid::class)) {
+            $transformerFactory->addTransformerFactory(new SymfonyUidTransformerFactory());
+        }
+
         return $autoMapper;
     }
 }
diff --git a/src/Component/AutoMapper/Transformer/StringToSymfonyUidTransformer.php b/src/Component/AutoMapper/Transformer/StringToSymfonyUidTransformer.php
new file mode 100644
index 000000000..6249ead62
--- /dev/null
+++ b/src/Component/AutoMapper/Transformer/StringToSymfonyUidTransformer.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Jane\Component\AutoMapper\Transformer;
+
+use Jane\Component\AutoMapper\Extractor\PropertyMapping;
+use Jane\Component\AutoMapper\Generator\UniqueVariableScope;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Name;
+
+/**
+ * Transform a string to a Symfony Uid object.
+ *
+ * @author Baptiste Leduc <baptiste.leduc@gmail.com>
+ */
+final class StringToSymfonyUidTransformer implements TransformerInterface
+{
+    private $className;
+
+    public function __construct(string $className)
+    {
+        $this->className = $className;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array
+    {
+        return [
+            new Expr\New_(new Name($this->className), [new Arg($input)]),
+            [],
+        ];
+    }
+}
diff --git a/src/Component/AutoMapper/Transformer/SymfonyUidCopyTransformer.php b/src/Component/AutoMapper/Transformer/SymfonyUidCopyTransformer.php
new file mode 100644
index 000000000..c6a6369f0
--- /dev/null
+++ b/src/Component/AutoMapper/Transformer/SymfonyUidCopyTransformer.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Jane\Component\AutoMapper\Transformer;
+
+use Jane\Component\AutoMapper\Extractor\PropertyMapping;
+use Jane\Component\AutoMapper\Generator\UniqueVariableScope;
+use PhpParser\Node\Arg;
+use PhpParser\Node\Expr;
+use PhpParser\Node\Name;
+use Symfony\Component\Uid\Ulid;
+use Symfony\Component\Uid\Uuid;
+
+/**
+ * Transform Symfony Uid to the same object.
+ *
+ * @author Baptiste Leduc <baptiste.leduc@gmail.com>
+ */
+final class SymfonyUidCopyTransformer implements TransformerInterface
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array
+    {
+        return [
+            new Expr\Ternary(
+                new Expr\Instanceof_($input, new Name(Ulid::class)),
+                new Expr\New_(new Name(Ulid::class), [new Arg(new Expr\MethodCall($input, 'toBase32'))]),
+                new Expr\New_(new Name(Uuid::class), [new Arg(new Expr\MethodCall($input, 'toRfc4122'))])
+            ),
+            [],
+        ];
+    }
+}
diff --git a/src/Component/AutoMapper/Transformer/SymfonyUidToStringTransformer.php b/src/Component/AutoMapper/Transformer/SymfonyUidToStringTransformer.php
new file mode 100644
index 000000000..c5dbb8563
--- /dev/null
+++ b/src/Component/AutoMapper/Transformer/SymfonyUidToStringTransformer.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Jane\Component\AutoMapper\Transformer;
+
+use Jane\Component\AutoMapper\Extractor\PropertyMapping;
+use Jane\Component\AutoMapper\Generator\UniqueVariableScope;
+use PhpParser\Node\Expr;
+
+/**
+ * Transform a \DateTimeInterface object to a string.
+ *
+ * @author Joel Wurtz <jwurtz@jolicode.com>
+ */
+final class SymfonyUidToStringTransformer implements TransformerInterface
+{
+    private $isUlid;
+
+    public function __construct(bool $isUlid)
+    {
+        $this->isUlid = $isUlid;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array
+    {
+        if ($this->isUlid) {
+            return [
+                // ulid
+                new Expr\MethodCall($input, 'toBase32'),
+                [],
+            ];
+        }
+
+        return [
+            // uuid
+            new Expr\MethodCall($input, 'toRfc4122'),
+            [],
+        ];
+    }
+}
diff --git a/src/Component/AutoMapper/Transformer/SymfonyUidTransformerFactory.php b/src/Component/AutoMapper/Transformer/SymfonyUidTransformerFactory.php
new file mode 100644
index 000000000..44168a713
--- /dev/null
+++ b/src/Component/AutoMapper/Transformer/SymfonyUidTransformerFactory.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Jane\Component\AutoMapper\Transformer;
+
+use Jane\Component\AutoMapper\MapperMetadataInterface;
+use Symfony\Component\PropertyInfo\Type;
+use Symfony\Component\Uid\AbstractUid;
+use Symfony\Component\Uid\Ulid;
+
+/**
+ * @author Baptiste Leduc <baptiste.leduc@gmail.com>
+ */
+final class SymfonyUidTransformerFactory extends AbstractUniqueTypeTransformerFactory implements PrioritizedTransformerFactoryInterface
+{
+    /**
+     * @var array
+     */
+    private $reflectionCache = [];
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function createTransformer(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface
+    {
+        $isSourceUid = $this->isUid($sourceType);
+        $isTargetUid = $this->isUid($targetType);
+
+        if ($isSourceUid && $isTargetUid) {
+            return new SymfonyUidCopyTransformer();
+        }
+
+        if ($isSourceUid) {
+            return new SymfonyUidToStringTransformer($this->reflectionCache[$sourceType->getClassName()][1]);
+        }
+
+        if ($isTargetUid) {
+            return new StringToSymfonyUidTransformer($targetType->getClassName());
+        }
+
+        return null;
+    }
+
+    private function isUid(Type $type): bool
+    {
+        if (Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) {
+            return false;
+        }
+
+        if (!\array_key_exists($type->getClassName(), $this->reflectionCache)) {
+            $reflClass = new \ReflectionClass($type->getClassName());
+            $this->reflectionCache[$type->getClassName()] = [$reflClass->isSubclassOf(AbstractUid::class), $type->getClassName() === Ulid::class];
+        }
+
+        return $this->reflectionCache[$type->getClassName()][0];
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getPriority(): int
+    {
+        return 24;
+    }
+}
diff --git a/src/Component/AutoMapper/composer.json b/src/Component/AutoMapper/composer.json
index 3bfd1e174..25d1f338e 100644
--- a/src/Component/AutoMapper/composer.json
+++ b/src/Component/AutoMapper/composer.json
@@ -24,9 +24,10 @@
     },
     "require-dev": {
         "doctrine/annotations": "~1.0",
-        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.0",
         "moneyphp/money": "^3.0",
-        "phpunit/phpunit": "^8.0"
+        "phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.0",
+        "phpunit/phpunit": "^8.0",
+        "symfony/uid": "^5.2"
     },
     "suggest": {
         "doctrine/annotations": "Docblock Annotations Parser",


Test patch:
diff --git a/src/Component/AutoMapper/Tests/AutoMapperTest.php b/src/Component/AutoMapper/Tests/AutoMapperTest.php
index 28dbcf18a..453e0e44c 100644
--- a/src/Component/AutoMapper/Tests/AutoMapperTest.php
+++ b/src/Component/AutoMapper/Tests/AutoMapperTest.php
@@ -9,6 +9,8 @@ use Jane\Component\AutoMapper\MapperContext;
 use Jane\Component\AutoMapper\Tests\Fixtures\Order;
 use Jane\Component\AutoMapper\Tests\Fixtures\Transformer\MoneyTransformerFactory;
 use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
+use Symfony\Component\Uid\Ulid;
+use Symfony\Component\Uid\Uuid;
 
 /**
  * @author Joel Wurtz <jwurtz@jolicode.com>
@@ -793,4 +795,59 @@ class AutoMapperTest extends AutoMapperBaseTest
         $data = $this->autoMapper->map($parameters, 'array');
         self::assertNotEquals($arguments, $data['parameters']);
     }
+
+    public function testSymfonyUlid(): void
+    {
+        // array -> object
+        $data = [
+            'ulid' => '01EXE87A54256F05N8P6SB2M9M',
+            'name' => 'Grégoire Pineau',
+        ];
+        /** @var Fixtures\SymfonyUlidUser $user */
+        $user = $this->autoMapper->map($data, Fixtures\SymfonyUlidUser::class);
+        self::assertInstanceOf(Ulid::class, $user->getUlid());
+        self::assertEquals('01EXE87A54256F05N8P6SB2M9M', $user->getUlid()->toBase32());
+        self::assertEquals('Grégoire Pineau', $user->name);
+
+        // object -> array
+        $user = new Fixtures\SymfonyUlidUser(new Ulid('01EXE89XR69GERC6GV3J4X38FJ'), 'Grégoire Pineau');
+        $data = $this->autoMapper->map($user, 'array');
+        self::assertEquals('01EXE89XR69GERC6GV3J4X38FJ', $data['ulid']);
+        self::assertEquals('Grégoire Pineau', $data['name']);
+
+        // object -> object
+        $user = new Fixtures\SymfonyUlidUser(new Ulid('01EXE8A6TNWVCEGMZ36AX8N9MC'), 'Grégoire Pineau');
+        /** @var Fixtures\SymfonyUlidUser $newUser */
+        $newUser = $this->autoMapper->map($user, Fixtures\SymfonyUlidUser::class);
+        self::assertInstanceOf(Ulid::class, $user->getUlid());
+        self::assertEquals('01EXE8A6TNWVCEGMZ36AX8N9MC', $newUser->getUlid()->toBase32());
+        self::assertEquals('Grégoire Pineau', $newUser->name);
+
+        // array -> object // uuid v1
+        $uuidV1 = Uuid::v1();
+        $data = [
+            'uuid' => $uuidV1->toRfc4122(),
+            'name' => 'Grégoire Pineau',
+        ];
+        /** @var Fixtures\SymfonyUuidUser $user */
+        $user = $this->autoMapper->map($data, Fixtures\SymfonyUuidUser::class);
+        self::assertInstanceOf(Uuid::class, $user->getUuid());
+        self::assertEquals($uuidV1->toRfc4122(), $user->getUuid()->toRfc4122());
+        self::assertEquals('Grégoire Pineau', $user->name);
+        // object -> array // uuid v3
+        $uuidV3 = Uuid::v3(Uuid::v4(), 'jolicode');
+        $user = new Fixtures\SymfonyUuidUser($uuidV3, 'Grégoire Pineau');
+        $data = $this->autoMapper->map($user, 'array');
+        self::assertEquals($uuidV3->toRfc4122(), $data['uuid']);
+        self::assertEquals('Grégoire Pineau', $data['name']);
+
+        // object -> object // uuid v4
+        $uuidV4 = Uuid::v4();
+        $user = new Fixtures\SymfonyUuidUser($uuidV4, 'Grégoire Pineau');
+        /** @var Fixtures\SymfonyUuidUser $newUser */
+        $newUser = $this->autoMapper->map($user, Fixtures\SymfonyUuidUser::class);
+        self::assertInstanceOf(Uuid::class, $user->getUuid());
+        self::assertEquals($uuidV4->toRfc4122(), $newUser->getUuid()->toRfc4122());
+        self::assertEquals('Grégoire Pineau', $newUser->name);
+    }
 }
diff --git a/src/Component/AutoMapper/Tests/Fixtures/SymfonyUlidUser.php b/src/Component/AutoMapper/Tests/Fixtures/SymfonyUlidUser.php
new file mode 100644
index 000000000..6882be346
--- /dev/null
+++ b/src/Component/AutoMapper/Tests/Fixtures/SymfonyUlidUser.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Jane\Component\AutoMapper\Tests\Fixtures;
+
+use Symfony\Component\Uid\Ulid;
+
+class SymfonyUlidUser
+{
+    /**
+     * @var Ulid
+     */
+    private $ulid;
+
+    /**
+     * @var string
+     */
+    public $name;
+
+    public function __construct(Ulid $ulid, string $name)
+    {
+        $this->ulid = $ulid;
+        $this->name = $name;
+    }
+
+    public function getUlid(): Ulid
+    {
+        return $this->ulid;
+    }
+}
diff --git a/src/Component/AutoMapper/Tests/Fixtures/SymfonyUuidUser.php b/src/Component/AutoMapper/Tests/Fixtures/SymfonyUuidUser.php
new file mode 100644
index 000000000..f55659715
--- /dev/null
+++ b/src/Component/AutoMapper/Tests/Fixtures/SymfonyUuidUser.php
@@ -0,0 +1,29 @@
+<?php
+
+namespace Jane\Component\AutoMapper\Tests\Fixtures;
+
+use Symfony\Component\Uid\Uuid;
+
+class SymfonyUuidUser
+{
+    /**
+     * @var Uuid
+     */
+    private $uuid;
+
+    /**
+     * @var string
+     */
+    public $name;
+
+    public function __construct(Uuid $uuid, string $name)
+    {
+        $this->uuid = $uuid;
+        $this->name = $name;
+    }
+
+    public function getUuid(): Uuid
+    {
+        return $this->uuid;
+    }
+}