Skip to content

Commit

Permalink
[AutoMapper] Add discriminator mapping
Browse files Browse the repository at this point in the history
  • Loading branch information
joelwurtz committed Jan 31, 2019
1 parent 3753afc commit f27d448
Show file tree
Hide file tree
Showing 8 changed files with 141 additions and 18 deletions.
10 changes: 8 additions & 2 deletions src/AutoMapper/AutoMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@
use Jane\AutoMapper\Compiler\Transformer\ObjectTransformerFactory;
use Jane\AutoMapper\Compiler\Transformer\UniqueTypeTransformerFactory;
use Jane\AutoMapper\Extractor\PrivateReflectionExtractor;
use PhpParser\ParserFactory;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
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;
Expand All @@ -35,8 +37,13 @@ class AutoMapper extends AbstractAutoMapper
*/
public static function create(bool $private = true, MapperClassLoaderInterface $loader = null, AdvancedNameConverterInterface $nameConverter = null, string $classPrefix = 'Mapper_'): self
{
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));

if ($loader === null) {
$loader = new EvalLoader(new Compiler());
$loader = new EvalLoader(new Compiler(
(new ParserFactory())->create(ParserFactory::PREFER_PHP7),
new ClassDiscriminatorFromClassMetadata($classMetadataFactory)
));
}

if ($private) {
Expand All @@ -53,7 +60,6 @@ public static function create(bool $private = true, MapperClassLoaderInterface $
[$reflectionExtractor]
);
$accessorExtractor = new ReflectionAccessorExtractor($private);
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$transformerFactory = new ChainTransformerFactory();

$sourceTargetMappingExtractor = new SourceTargetPropertiesMappingExtractor(
Expand Down
61 changes: 50 additions & 11 deletions src/AutoMapper/Compiler/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PhpParser\Node\Scalar;
use PhpParser\Parser;
use PhpParser\ParserFactory;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;

class Compiler
Expand All @@ -38,7 +39,6 @@ public function compile(MapperConfigurationInterface $mapperConfiguration)
$hashVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('sourceHash'));
$contextVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('context'));
$constructStatements = [];
$injectMapperStatements = [];
$addedDependencies = [];
$canHaveCircularDependency = $mapperConfiguration->canHaveCircularDependency() && $mapperConfiguration->getSource() !== 'array';

Expand Down Expand Up @@ -69,7 +69,7 @@ public function compile(MapperConfigurationInterface $mapperConfiguration)
]);
}

[$createObjectStmts, $inConstructor, $constructStatementsForCreateObjects] = $this->getCreateObjectStatements($mapperConfiguration, $result, $contextVariable, $sourceInput, $uniqueVariableScope);
[$createObjectStmts, $inConstructor, $constructStatementsForCreateObjects, $injectMapperStatements] = $this->getCreateObjectStatements($mapperConfiguration, $result, $contextVariable, $sourceInput, $uniqueVariableScope);
$constructStatements = array_merge($constructStatements, $constructStatementsForCreateObjects);

$statements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\MethodCall($contextVariable, 'getObjectToPopulate')));
Expand Down Expand Up @@ -264,18 +264,57 @@ public function compile(MapperConfigurationInterface $mapperConfiguration)

private function getCreateObjectStatements(MapperConfigurationInterface $mapperConfiguration, Expr\Variable $result, Expr\Variable $contextVariable, Expr\Variable $sourceInput, UniqueVariableScope $uniqueVariableScope): array
{
$reflectionClass = $mapperConfiguration->getTarget() === 'array' ? null : new \ReflectionClass($mapperConfiguration->getTarget());
$targetConstructor = $reflectionClass ? $reflectionClass->getConstructor() : null;
$propertiesMapping = $mapperConfiguration->getPropertiesMapping();

if ($mapperConfiguration->getTarget() === 'array') {
return [[new Stmt\Expression(new Expr\Assign($result, new Expr\Array_()))], [], [], []];
}

if ($mapperConfiguration->getTarget() === \stdClass::class) {
return [[new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name(\stdClass::class))))], [], [], []];
}

$reflectionClass = new \ReflectionClass($mapperConfiguration->getTarget());
$targetConstructor = $reflectionClass->getConstructor();
$createObjectStatements = [];
$inConstructor = [];
$constructStatements = [];
$injectMapperStatements = [];
/** @var ClassDiscriminatorMapping $classDiscriminatorMapping */
$classDiscriminatorMapping = $mapperConfiguration->getTarget() !== 'array' && null !== $this->classDiscriminator ? $this->classDiscriminator->getMappingForClass($mapperConfiguration->getTarget()) : null;

if ($mapperConfiguration->getTarget() === 'array') {
$createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\Array_()));
} elseif ($mapperConfiguration->getTarget() === \stdClass::class) {
$createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name(\stdClass::class))));
} elseif ($targetConstructor !== null && $mapperConfiguration->hasConstructor()) {
if (null !== $classDiscriminatorMapping && null !== ($propertyMapping = $mapperConfiguration->getPropertyMapping($classDiscriminatorMapping->getTypeProperty()))) {
[$output, $createObjectStatements] = $propertyMapping->getTransformer()->transform($propertyMapping->getReadAccessor()->getExpression($sourceInput), $uniqueVariableScope, $propertyMapping);

foreach ($classDiscriminatorMapping->getTypesMapping() as $typeValue => $typeTarget) {
$mapperName = 'Discriminator_Mapper_' . $mapperConfiguration->getSource() . '_' . $typeTarget;

$injectMapperStatements[] = new Stmt\Expression(new Expr\Assign(
new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'), new Scalar\String_($mapperName)),
new Expr\MethodCall(new Expr\Variable('autoMapper'), 'getMapper', [
new Arg(new Scalar\String_($mapperConfiguration->getSource())),
new Arg(new Scalar\String_($typeTarget)),
])
));
$createObjectStatements[] = new Stmt\If_(new Expr\BinaryOp\Identical(
new Scalar\String_($typeValue),
$output
), [
'stmts' => [
new Stmt\Return_(new Expr\MethodCall(new Expr\ArrayDimFetch(
new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'),
new Scalar\String_($mapperName)
), 'map', [
new Arg($sourceInput),
new Expr\Variable('context'),
])),
],
]);
}
}

$propertiesMapping = $mapperConfiguration->getPropertiesMapping();

if ($targetConstructor !== null && $mapperConfiguration->hasConstructor()) {
$constructArguments = [];

/** @var PropertyMapping $propertyMapping */
Expand Down Expand Up @@ -353,7 +392,7 @@ private function getCreateObjectStatements(MapperConfigurationInterface $mapperC
$createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name\FullyQualified($mapperConfiguration->getTarget()))));
}

return [$createObjectStatements, $inConstructor, $constructStatements];
return [$createObjectStatements, $inConstructor, $constructStatements, $injectMapperStatements];
}

private function getValueAsExpr($value)
Expand Down
30 changes: 26 additions & 4 deletions src/AutoMapper/MapperConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class MapperConfiguration extends AbstractMapperConfiguration

private $customMapping = [];

private $propertiesMapping;

private $autoMapperRegister;

public function __construct(AutoMapperRegisterInterface $autoMapperRegister, PropertiesMappingExtractorInterface $mappingExtractor, string $source, string $target, string $classPrefix = 'Mapper_')
Expand All @@ -28,19 +30,39 @@ public function __construct(AutoMapperRegisterInterface $autoMapperRegister, Pro
*/
public function getPropertiesMapping(): array
{
$mappings = $this->mappingExtractor->getPropertiesMapping($this->source, $this->target, $this);
if ($this->propertiesMapping === null) {
$this->buildPropertyMapping();
}

return $this->propertiesMapping;
}

public function getPropertyMapping(string $property): ?PropertyMapping
{
if ($this->propertiesMapping === null) {
$this->buildPropertyMapping();
}

return $this->propertiesMapping[$property] ?? null;
}

private function buildPropertyMapping()
{
$this->propertiesMapping = [];

foreach ($this->mappingExtractor->getPropertiesMapping($this->source, $this->target, $this) as $propertyMapping) {
$this->propertiesMapping[$propertyMapping->getProperty()] = $propertyMapping;
}

foreach ($this->customMapping as $property => $callback) {
$mappings[] = new PropertyMapping(
$this->propertiesMapping[$property] = new PropertyMapping(
new ReadAccessor(ReadAccessor::TYPE_SOURCE, $property),
$this->mappingExtractor->getWriteMutator($this->source, $this->target, $property),
new CallbackTransformer($property),
$property,
false
);
}

return $mappings;
}

public function createMapper(): Mapper
Expand Down
2 changes: 2 additions & 0 deletions src/AutoMapper/MapperConfigurationInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public function getTarget(): string;
*/
public function getPropertiesMapping(): array;

public function getPropertyMapping(string $property): ?PropertyMapping;

public function getMapperClassName(): string;

public function createMapper(): Mapper;
Expand Down
25 changes: 24 additions & 1 deletion src/AutoMapper/Tests/AutoMapperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,31 @@

namespace Jane\AutoMapper\Tests;

use Doctrine\Common\Annotations\AnnotationReader;
use Jane\AutoMapper\AutoMapper;
use Jane\AutoMapper\Compiler\Compiler;
use Jane\AutoMapper\Compiler\FileLoader;
use Jane\AutoMapper\Context;
use Jane\AutoMapper\Exception\CircularReferenceException;
use Jane\AutoMapper\Tests\Domain\Address;
use Jane\AutoMapper\Tests\Domain\AddressDTO;
use Jane\AutoMapper\Tests\Domain\Cat;
use Jane\AutoMapper\Tests\Domain\Foo;
use Jane\AutoMapper\Tests\Domain\FooMaxDepth;
use Jane\AutoMapper\Tests\Domain\Node;
use Jane\AutoMapper\Tests\Domain\Pet;
use Jane\AutoMapper\Tests\Domain\PrivateUser;
use Jane\AutoMapper\Tests\Domain\PrivateUserDTO;
use Jane\AutoMapper\Tests\Domain\User;
use Jane\AutoMapper\Tests\Domain\UserConstructorDTO;
use Jane\AutoMapper\Tests\Domain\UserDTO;
use Jane\AutoMapper\Tests\Domain\UserDTONoAge;
use Jane\AutoMapper\Tests\Domain\UserDTONoName;
use PhpParser\ParserFactory;
use PHPUnit\Framework\TestCase;
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;

class AutoMapperTest extends TestCase
Expand All @@ -30,7 +37,12 @@ class AutoMapperTest extends TestCase
public function setUp()
{
@unlink(__DIR__ . '/cache/registry.php');
$loader = new FileLoader(new Compiler(), __DIR__ . '/cache');

$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader()));
$loader = new FileLoader(new Compiler(
(new ParserFactory())->create(ParserFactory::PREFER_PHP7),
new ClassDiscriminatorFromClassMetadata($classMetadataFactory)
), __DIR__ . '/cache');

$this->autoMapper = AutoMapper::create(true, $loader);
}
Expand Down Expand Up @@ -412,4 +424,15 @@ public function testDefaultArguments()
self::assertInstanceOf(UserConstructorDTO::class, $userDto);
self::assertSame(50, $userDto->getAge());
}

public function testDiscriminator()
{
$data = [
'type' => 'cat'
];

$pet = $this->autoMapper->map($data, Pet::class);

self::assertInstanceOf(Cat::class, $pet);
}
}
7 changes: 7 additions & 0 deletions src/AutoMapper/Tests/Domain/Cat.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Jane\AutoMapper\Tests\Domain;

class Cat extends Pet
{
}
7 changes: 7 additions & 0 deletions src/AutoMapper/Tests/Domain/Dog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Jane\AutoMapper\Tests\Domain;

class Dog extends Pet
{
}
17 changes: 17 additions & 0 deletions src/AutoMapper/Tests/Domain/Pet.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace Jane\AutoMapper\Tests\Domain;

use Symfony\Component\Serializer\Annotation\DiscriminatorMap;

/**
* @DiscriminatorMap(typeProperty="type", mapping={
* "cat"="Jane\AutoMapper\Tests\Domain\Cat",
* "dog"="Jane\AutoMapper\Tests\Domain\Dog"
* })
*/
class Pet
{
/** @var string */
public $type;
}

0 comments on commit f27d448

Please sign in to comment.