Skip to content

Feature: Doctrine Provider #253

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

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ tools/phpstan/cache/
cache/
site/
.build/
.castor*
.castor*
tests/Doctrine/db.sqlite
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
10 changes: 10 additions & 0 deletions src/AutoMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
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;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface;
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;
Expand Down Expand Up @@ -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__);
Expand Down Expand Up @@ -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);
Expand All @@ -192,6 +201,7 @@ public static function create(
$expressionLanguage,
$eventDispatcher,
$removeDefaultProperties,
$entityManager,
);

$mapperGenerator = new MapperGenerator(
Expand Down
26 changes: 26 additions & 0 deletions src/EventListener/Doctrine/DoctrineProviderListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace AutoMapper\EventListener\Doctrine;

use AutoMapper\Event\GenerateMapperEvent;
use AutoMapper\Provider\Doctrine\DoctrineProvider;
use Doctrine\ORM\EntityManagerInterface;

final readonly class DoctrineProviderListener
{
public function __construct(
private EntityManagerInterface $entityManager
) {
}

public function __invoke(GenerateMapperEvent $event): void
{
if (!$this->entityManager->getMetadataFactory()->hasMetadataFor($event->mapperMetadata->target)) {

Check failure on line 20 in src/EventListener/Doctrine/DoctrineProviderListener.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter #1 $className of method Doctrine\Persistence\Mapping\AbstractClassMetadataFactory<Doctrine\ORM\Mapping\ClassMetadata>::hasMetadataFor() expects class-string, string given.
return;
}

$event->provider = DoctrineProvider::class;
}
}
7 changes: 7 additions & 0 deletions src/Metadata/MetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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));
Expand Down
33 changes: 33 additions & 0 deletions src/Provider/Doctrine/DoctrineProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Provider\Doctrine;

use AutoMapper\Provider\ProviderInterface;
use Doctrine\ORM\EntityManagerInterface;

final readonly class DoctrineProvider implements ProviderInterface
{
public function __construct(
private EntityManagerInterface $entityManager
) {
}

public function provide(string $targetType, mixed $source, array $context): object|array|null
{
$metadata = $this->entityManager->getClassMetadata($targetType);
// @TODO support multiple identifiers
$identifier = $metadata->identifier;

$result = $this->entityManager->createQueryBuilder()
->select('e')
->from($targetType, 'e')

Check failure on line 25 in src/Provider/Doctrine/DoctrineProvider.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter #1 $from of method Doctrine\ORM\QueryBuilder::from() expects class-string, string given.
->where('e.' . $identifier[0] . ' = :id')
->setParameter('id', $source[$identifier[0]])

Check failure on line 27 in src/Provider/Doctrine/DoctrineProvider.php

View workflow job for this annotation

GitHub Actions / phpstan

Cannot access offset string on mixed.
->getQuery()
->getOneOrNullResult();

return $result;

Check failure on line 31 in src/Provider/Doctrine/DoctrineProvider.php

View workflow job for this annotation

GitHub Actions / phpstan

Method AutoMapper\Provider\Doctrine\DoctrineProvider::provide() should return array|object|null but returns mixed.
}
}
3 changes: 3 additions & 0 deletions tests/AutoMapperBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -54,6 +56,7 @@ classPrefix: $classPrefix,
eventDispatcher: $eventDispatcher,
providers: $providers,
removeDefaultProperties: $removeDefaultProperties,
entityManager: $entityManager,
);
}
}
77 changes: 77 additions & 0 deletions tests/Doctrine/DoctrineTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Doctrine;

use AutoMapper\Tests\AutoMapperBuilder;
use AutoMapper\Tests\AutoMapperTestCase;
use AutoMapper\Tests\Doctrine\Entity\Book;
use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\ORMSetup;
use Doctrine\ORM\Tools\SchemaTool;

/**
* @author Joel Wurtz <[email protected]>
*/
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);
}
}
36 changes: 36 additions & 0 deletions tests/Doctrine/Entity/Book.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Doctrine\Entity;

use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Book
{
#[ORM\Id]
#[ORM\Column(type: 'integer')]
#[ORM\GeneratedValue]
public ?int $id = null;

#[ORM\Column(type: 'string')]
public string $title = '';

#[ORM\Column(type: 'string')]
public string $description = '';

#[ORM\Column(type: 'string')]
public string $author = '';

#[ORM\OneToMany(targetEntity: Review::class, mappedBy: 'book', cascade: ['persist', 'remove'])]
/** @var Collection<int, Review> */
public Collection $reviews;

public function __construct()
{
$this->reviews = new ArrayCollection();
}
}
30 changes: 30 additions & 0 deletions tests/Doctrine/Entity/Review.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Doctrine\Entity;

use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Review
{
#[ORM\Id, ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;

#[ORM\Column(type: 'integer')]
public int $rating = 0;

#[ORM\Column(type: 'string')]
public string $body = '';

#[ORM\Column(type: 'string')]
public string $author = '';

#[ORM\Column(type: 'datetime')]
public \DateTimeImmutable $publicationDate;

#[ORM\ManyToOne(targetEntity: Book::class, inversedBy: 'reviews')]
public Book $book;
}
Loading