Skip to content

Commit

Permalink
Handle DoctrineCollections within AutoMapper (#223)
Browse files Browse the repository at this point in the history
  • Loading branch information
Korbeil authored Feb 14, 2025
2 parents e66c9aa + ed43e79 commit 1f4b26f
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- [GH#223](https://github.com/jolicode/automapper/pull/223) Handle array to Doctrine Collection transformations

## [9.2.1] - 2025-01-31
### Fixed
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"require-dev": {
"api-platform/core": "^3.0.4",
"doctrine/annotations": "~1.0",
"doctrine/collections": "^2.2",
"doctrine/inflector": "^2.0",
"matthiasnoback/symfony-dependency-injection-test": "^5.1",
"moneyphp/money": "^3.3.2",
Expand Down
2 changes: 2 additions & 0 deletions src/Metadata/MetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use AutoMapper\Transformer\CopyTransformerFactory;
use AutoMapper\Transformer\DateTimeTransformerFactory;
use AutoMapper\Transformer\DependentTransformerInterface;
use AutoMapper\Transformer\DoctrineCollectionTransformerFactory;
use AutoMapper\Transformer\EnumTransformerFactory;
use AutoMapper\Transformer\MapperDependency;
use AutoMapper\Transformer\MultipleTransformerFactory;
Expand Down Expand Up @@ -370,6 +371,7 @@ public static function create(
new UniqueTypeTransformerFactory(),
new DateTimeTransformerFactory(),
new BuiltinTransformerFactory(),
new DoctrineCollectionTransformerFactory(),
new ArrayTransformerFactory(),
new ObjectTransformerFactory(),
new EnumTransformerFactory(),
Expand Down
61 changes: 61 additions & 0 deletions src/Transformer/ArrayToDoctrineCollectionTransformer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Transformer;

use AutoMapper\Generator\UniqueVariableScope;
use AutoMapper\Metadata\PropertyMetadata;
use Doctrine\Common\Collections\ArrayCollection;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PhpParser\Node\Stmt;

/**
* Transform an array to Money\Money object.
*
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
*/
final class ArrayToDoctrineCollectionTransformer implements TransformerInterface, DependentTransformerInterface, PrioritizedTransformerFactoryInterface
{
public function __construct(
protected TransformerInterface $itemTransformer,
) {
}

public function transform(Expr $input, Expr $target, PropertyMetadata $propertyMapping, UniqueVariableScope $uniqueVariableScope, Expr\Variable $source): array
{
/**
* $collection = new ArrayCollection();.
*/
$collectionVar = new Expr\Variable($uniqueVariableScope->getUniqueName('collection'));
$statements = [
new Stmt\Expression(new Expr\Assign($collectionVar, new Expr\New_(new Name(ArrayCollection::class)))),
];

$loopValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('value'));
[$output, $itemStatements] = $this->itemTransformer->transform($loopValueVar, $target, $propertyMapping, $uniqueVariableScope, $source);
$itemStatements[] = new Stmt\Expression(new Expr\MethodCall($collectionVar, 'add', [new Arg($output)]));

$statements[] = new Stmt\Foreach_(new Expr\BinaryOp\Coalesce($input, new Expr\Array_()), $loopValueVar, [
'stmts' => $itemStatements,
]);

return [$collectionVar, $statements];
}

public function getDependencies(): array
{
if (!$this->itemTransformer instanceof DependentTransformerInterface) {
return [];
}

return $this->itemTransformer->getDependencies();
}

public function getPriority(): int
{
return 0;
}
}
44 changes: 44 additions & 0 deletions src/Transformer/DoctrineCollectionTransformerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Transformer;

use AutoMapper\Metadata\MapperMetadata;
use AutoMapper\Metadata\SourcePropertyMetadata;
use AutoMapper\Metadata\TargetPropertyMetadata;
use AutoMapper\Metadata\TypesMatching;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\PropertyInfo\Type;

/**
* @author Baptiste Leduc <baptiste.leduc@gmail.com>
*/
final class DoctrineCollectionTransformerFactory extends AbstractUniqueTypeTransformerFactory implements ChainTransformerFactoryAwareInterface
{
use ChainTransformerFactoryAwareTrait;

protected function createTransformer(Type $sourceType, Type $targetType, SourcePropertyMetadata $source, TargetPropertyMetadata $target, MapperMetadata $mapperMetadata): ?TransformerInterface
{
if (!interface_exists(Collection::class)) {
return null;
}

if (Type::BUILTIN_TYPE_OBJECT !== $targetType->getBuiltinType() || !\is_string($targetType->getClassName())) {
return null;
}

if (Collection::class === $targetType->getClassName() || (false !== ($classImplements = class_implements($targetType->getClassName())) && \in_array(Collection::class, $classImplements))) {
$types = TypesMatching::fromSourceAndTargetTypes($sourceType->getCollectionValueTypes(), $targetType->getCollectionValueTypes());

$subItemTransformer = $this->chainTransformerFactory->getTransformer($types, $source, $target, $mapperMetadata);
if (null === $subItemTransformer) {
return null;
}

return new ArrayToDoctrineCollectionTransformer($subItemTransformer);
}

return null;
}
}
40 changes: 40 additions & 0 deletions tests/AutoMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
use AutoMapper\Tests\Fixtures\ClassWithPrivateProperty;
use AutoMapper\Tests\Fixtures\ConstructorWithDefaultValues;
use AutoMapper\Tests\Fixtures\DifferentSetterGetterType;
use AutoMapper\Tests\Fixtures\DoctrineCollections\Book;
use AutoMapper\Tests\Fixtures\DoctrineCollections\Library;
use AutoMapper\Tests\Fixtures\Dog;
use AutoMapper\Tests\Fixtures\Fish;
use AutoMapper\Tests\Fixtures\FooGenerator;
Expand Down Expand Up @@ -56,6 +58,7 @@
use AutoMapper\Tests\Fixtures\Transformer\MoneyTransformerFactory;
use AutoMapper\Tests\Fixtures\Uninitialized;
use AutoMapper\Tests\Fixtures\UserPromoted;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Serializer\Attribute\Groups;
Expand Down Expand Up @@ -1624,4 +1627,41 @@ public function testParamDocBlock(): void
'foo' => ['foo1', 'foo2'],
], $array);
}

public function testDoctrineCollectionsToArray(): void
{
$library = new Library();
$library->books = new ArrayCollection([
new Book('The Empyrean Onyx Storm'),
new Book('Valentina'),
new Book('Imbalance'),
]);

$data = $this->autoMapper->map($library, 'array');

self::assertCount(3, $data['books']);
self::assertEquals('The Empyrean Onyx Storm', $data['books'][0]['name']);
self::assertEquals('Valentina', $data['books'][1]['name']);
self::assertEquals('Imbalance', $data['books'][2]['name']);
}

public function testArrayToDoctrineCollections(): void
{
$data = [
'books' => [
['name' => 'The Empyrean Onyx Storm'],
['name' => 'Valentina'],
['name' => 'Imbalance'],
],
];

$library = $this->autoMapper->map($data, Library::class);
var_dump($library);

self::assertInstanceOf(Library::class, $library);
self::assertCount(3, $library->books);
self::assertEquals('The Empyrean Onyx Storm', $library->books[0]->name);
self::assertEquals('Valentina', $library->books[1]->name);
self::assertEquals('Imbalance', $library->books[2]->name);
}
}
13 changes: 13 additions & 0 deletions tests/Fixtures/DoctrineCollections/Book.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures\DoctrineCollections;

class Book
{
public function __construct(
public string $name
) {
}
}
13 changes: 13 additions & 0 deletions tests/Fixtures/DoctrineCollections/Library.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\Fixtures\DoctrineCollections;

use Doctrine\Common\Collections\Collection;

class Library
{
/** @var Collection<Book> */
public Collection $books;
}

0 comments on commit 1f4b26f

Please sign in to comment.