diff --git a/.gitignore b/.gitignore index 0397a122..916c038c 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ tools/phpstan/cache/ cache/ site/ .build/ -.castor* \ No newline at end of file +.castor* +tests/Doctrine/db.sqlite \ No newline at end of file diff --git a/composer.json b/composer.json index d6d8bff1..47ffa10c 100644 --- a/composer.json +++ b/composer.json @@ -29,9 +29,10 @@ }, "require-dev": { "api-platform/core": "^3.0.4 || ^4", - "doctrine/annotations": "~1.0", + "doctrine/annotations": "^2.0", "doctrine/collections": "^2.2", "doctrine/inflector": "^2.0", + "doctrine/orm": "^3.3", "matthiasnoback/symfony-dependency-injection-test": "^5.1", "moneyphp/money": "^3.3.2", "phpunit/phpunit": "^9.0", diff --git a/src/AutoMapper.php b/src/AutoMapper.php index 9639eff3..7740ba9e 100644 --- a/src/AutoMapper.php +++ b/src/AutoMapper.php @@ -12,6 +12,7 @@ use AutoMapper\Loader\FileLoader; use AutoMapper\Metadata\MetadataFactory; use AutoMapper\Metadata\MetadataRegistry; +use AutoMapper\Provider\Doctrine\DoctrineProvider; use AutoMapper\Provider\ProviderInterface; use AutoMapper\Provider\ProviderRegistry; use AutoMapper\Symfony\ExpressionLanguageProvider; @@ -19,6 +20,7 @@ use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerRegistry; use AutoMapper\Transformer\TransformerFactoryInterface; use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; @@ -148,6 +150,7 @@ public static function create( EventDispatcherInterface $eventDispatcher = new EventDispatcher(), iterable $providers = [], bool $removeDefaultProperties = false, + ?EntityManagerInterface $entityManager = null, ): AutoMapperInterface { if (\count($transformerFactories) > 0) { trigger_deprecation('jolicode/automapper', '9.0', 'The "$transformerFactories" property will be removed in version 10.0, AST transformer factories must be included within AutoMapper.', __METHOD__); @@ -176,6 +179,12 @@ public static function create( $classDiscriminatorFromClassMetadata = new ClassDiscriminatorFromClassMetadata($classMetadataFactory); } + $providers = iterator_to_array($providers); + + if (null !== $entityManager) { + $providers[] = new DoctrineProvider($entityManager); + } + $customTransformerRegistry = new PropertyTransformerRegistry($propertyTransformers); $metadataRegistry = new MetadataRegistry($configuration); $providerRegistry = new ProviderRegistry($providers); @@ -192,6 +201,7 @@ public static function create( $expressionLanguage, $eventDispatcher, $removeDefaultProperties, + $entityManager, ); $mapperGenerator = new MapperGenerator( diff --git a/src/EventListener/Doctrine/DoctrineProviderListener.php b/src/EventListener/Doctrine/DoctrineProviderListener.php new file mode 100644 index 00000000..cce1336b --- /dev/null +++ b/src/EventListener/Doctrine/DoctrineProviderListener.php @@ -0,0 +1,26 @@ +entityManager->getMetadataFactory()->hasMetadataFor($event->mapperMetadata->target)) { + return; + } + + $event->provider = DoctrineProvider::class; + } +} diff --git a/src/Metadata/MetadataFactory.php b/src/Metadata/MetadataFactory.php index 7847817a..dd4fc3b3 100644 --- a/src/Metadata/MetadataFactory.php +++ b/src/Metadata/MetadataFactory.php @@ -9,6 +9,7 @@ use AutoMapper\Event\PropertyMetadataEvent; use AutoMapper\Event\SourcePropertyMetadata as SourcePropertyMetadataEvent; use AutoMapper\Event\TargetPropertyMetadata as TargetPropertyMetadataEvent; +use AutoMapper\EventListener\Doctrine\DoctrineProviderListener; use AutoMapper\EventListener\MapFromListener; use AutoMapper\EventListener\MapperListener; use AutoMapper\EventListener\MapProviderListener; @@ -44,6 +45,7 @@ use AutoMapper\Transformer\TransformerFactoryInterface; use AutoMapper\Transformer\UniqueTypeTransformerFactory; use AutoMapper\Transformer\VoidTransformer; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; @@ -354,6 +356,7 @@ public static function create( ExpressionLanguage $expressionLanguage = new ExpressionLanguage(), EventDispatcherInterface $eventDispatcher = new EventDispatcher(), bool $removeDefaultProperties = false, + ?EntityManagerInterface $entityManager = null, ): self { // Create property info extractors $flags = ReflectionExtractor::ALLOW_PUBLIC; @@ -375,6 +378,10 @@ public static function create( $eventDispatcher->addListener(PropertyMetadataEvent::class, new AdvancedNameConverterListener($nameConverter)); } + if (null !== $entityManager) { + $eventDispatcher->addListener(GenerateMapperEvent::class, new DoctrineProviderListener($entityManager)); + } + $eventDispatcher->addListener(PropertyMetadataEvent::class, new MapToContextListener($reflectionExtractor)); $eventDispatcher->addListener(GenerateMapperEvent::class, new MapToListener($customTransformerRegistry, $expressionLanguage)); $eventDispatcher->addListener(GenerateMapperEvent::class, new MapFromListener($customTransformerRegistry, $expressionLanguage)); diff --git a/src/Provider/Doctrine/DoctrineProvider.php b/src/Provider/Doctrine/DoctrineProvider.php new file mode 100644 index 00000000..42b40f52 --- /dev/null +++ b/src/Provider/Doctrine/DoctrineProvider.php @@ -0,0 +1,33 @@ +entityManager->getClassMetadata($targetType); + // @TODO support multiple identifiers + $identifier = $metadata->identifier; + + $result = $this->entityManager->createQueryBuilder() + ->select('e') + ->from($targetType, 'e') + ->where('e.' . $identifier[0] . ' = :id') + ->setParameter('id', $source[$identifier[0]]) + ->getQuery() + ->getOneOrNullResult(); + + return $result; + } +} diff --git a/tests/AutoMapperBuilder.php b/tests/AutoMapperBuilder.php index ad4b7688..cfb467b6 100644 --- a/tests/AutoMapperBuilder.php +++ b/tests/AutoMapperBuilder.php @@ -8,6 +8,7 @@ use AutoMapper\Configuration; use AutoMapper\ConstructorStrategy; use AutoMapper\Symfony\ExpressionLanguageProvider; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Filesystem\Filesystem; @@ -29,6 +30,7 @@ public static function buildAutoMapper( ?ExpressionLanguageProvider $expressionLanguageProvider = null, EventDispatcherInterface $eventDispatcher = new EventDispatcher(), bool $removeDefaultProperties = false, + ?EntityManagerInterface $entityManager = null, ): AutoMapper { $skipCacheRemove = $_SERVER['SKIP_CACHE_REMOVE'] ?? false; @@ -54,6 +56,7 @@ classPrefix: $classPrefix, eventDispatcher: $eventDispatcher, providers: $providers, removeDefaultProperties: $removeDefaultProperties, + entityManager: $entityManager, ); } } diff --git a/tests/Doctrine/DoctrineTest.php b/tests/Doctrine/DoctrineTest.php new file mode 100644 index 00000000..9b16c35e --- /dev/null +++ b/tests/Doctrine/DoctrineTest.php @@ -0,0 +1,77 @@ + + */ +class DoctrineTest extends AutoMapperTestCase +{ + private EntityManagerInterface $entityManager; + + protected function setUp(): void + { + parent::setUp(); + $this->buildDatabase(); + + $this->autoMapper = AutoMapperBuilder::buildAutoMapper(entityManager: $this->entityManager); + } + + private function buildDatabase() + { + // delete the database file + if (file_exists(__DIR__ . '/db.sqlite')) { + unlink(__DIR__ . '/db.sqlite'); + } + + $config = ORMSetup::createAttributeMetadataConfiguration( + paths: [__DIR__ . '/Entity'], + isDevMode: true, + ); + + $connection = DriverManager::getConnection([ + 'driver' => 'pdo_sqlite', + 'path' => __DIR__ . '/db.sqlite', + ], $config); + + $entityManager = new EntityManager($connection, $config); + + // Generate schema + $schemaTool = new SchemaTool($entityManager); + $schemaTool->createSchema($entityManager->getMetadataFactory()->getAllMetadata()); + + $this->entityManager = $entityManager; + } + + public function testAutoMapping(): void + { + $book = new Book(); + + $this->entityManager->persist($book); + $this->entityManager->flush(); + + $this->assertNotNull($book->id); + + $bookArray = $this->autoMapper->map($book, 'array'); + $bookArray['author'] = 'John Doe'; + + $this->autoMapper->map($bookArray, Book::class); + $this->entityManager->flush(); + $this->entityManager->clear(); + + $book = $this->entityManager->find(Book::class, $book->id); + + $this->assertEquals('John Doe', $book->author); + } +} diff --git a/tests/Doctrine/Entity/Book.php b/tests/Doctrine/Entity/Book.php new file mode 100644 index 00000000..2dbf5363 --- /dev/null +++ b/tests/Doctrine/Entity/Book.php @@ -0,0 +1,36 @@ + */ + public Collection $reviews; + + public function __construct() + { + $this->reviews = new ArrayCollection(); + } +} diff --git a/tests/Doctrine/Entity/Review.php b/tests/Doctrine/Entity/Review.php new file mode 100644 index 00000000..e122a82e --- /dev/null +++ b/tests/Doctrine/Entity/Review.php @@ -0,0 +1,30 @@ +