-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[AutoMapper] New component to automatically map a source object to a target object #30248
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
Closed
Closed
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
b5592ee
Initial commit and implementation of symfony/automapper
joelwurtz 936bc2f
Add normalizer bridge
joelwurtz 2ea1951
Add missing licence
joelwurtz d272d9c
Fix typo in interface
joelwurtz beb0f49
Add attribute checking option
joelwurtz e4c1385
Allow attribute checking to be set in factory
joelwurtz cf1fdf2
Add empty test classes
joelwurtz fc1d87d
Add automapper tests
joelwurtz bd387f5
Apply suggestions from @dunglas code review
dunglas 20b7735
Add expiremental annotation where needed, add final to class that sho…
joelwurtz 12e5d1f
Fix cs and typo
joelwurtz 26e0c8a
Avoid too many function calls
joelwurtz 678537a
Add interface for generator metadatas
joelwurtz ceeeaf1
Use array for context and provide helper class
joelwurtz ec97f74
Use new context construction in normalizer bridge
joelwurtz 92784b1
Fix test case class test
joelwurtz eb6f638
Remove useless class
joelwurtz fab7135
expiremental > expiremental in 4.3
joelwurtz 5154440
Fixing tests
Korbeil c688855
Added AutoMapperNormalizerTest
Korbeil 9f16b43
Context tests
Korbeil ed00ec3
Add MapperGeneratorMetadataFactory tests
Korbeil ab82b9f
Add FromSourceMappingExtractor tests
Korbeil ee908a6
Add FromTargetMappingExtractor tests
Korbeil cd2d414
WIP PrivateReflectionExtractor tests
Korbeil cd85e2d
Remove internal for generated mapper
joelwurtz f74e22e
Fix context rebase
joelwurtz ee671dd
Add missing deps in dev
joelwurtz 78ef805
Use property read / write info extractor
joelwurtz 1e4bd5e
Fix cs
joelwurtz 20a37df
Add tests and date time mutable / immutable transformations
joelwurtz af1fdf2
Fix header, expiremental in 5.1
joelwurtz c07edd2
Fix php version
joelwurtz ad190a2
Fix createFromImmutable not available in php 7.2
joelwurtz 9c3cb23
Fix test
joelwurtz fecf949
Better conditions on automapper
joelwurtz ad0f487
Remove bad deps on rebase
joelwurtz 5ba8171
Fix class exists
joelwurtz File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
vendor/ | ||
composer.lock | ||
phpunit.xml |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,257 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\AutoMapper; | ||
|
||
use Doctrine\Common\Annotations\AnnotationReader; | ||
use PhpParser\ParserFactory; | ||
use Symfony\Component\AutoMapper\Exception\NoMappingFoundException; | ||
use Symfony\Component\AutoMapper\Extractor\FromSourceMappingExtractor; | ||
use Symfony\Component\AutoMapper\Extractor\FromTargetMappingExtractor; | ||
use Symfony\Component\AutoMapper\Extractor\SourceTargetMappingExtractor; | ||
use Symfony\Component\AutoMapper\Generator\Generator; | ||
use Symfony\Component\AutoMapper\Loader\ClassLoaderInterface; | ||
use Symfony\Component\AutoMapper\Loader\EvalLoader; | ||
use Symfony\Component\AutoMapper\Transformer\ArrayTransformerFactory; | ||
use Symfony\Component\AutoMapper\Transformer\BuiltinTransformerFactory; | ||
use Symfony\Component\AutoMapper\Transformer\ChainTransformerFactory; | ||
use Symfony\Component\AutoMapper\Transformer\DateTimeTransformerFactory; | ||
use Symfony\Component\AutoMapper\Transformer\MultipleTransformerFactory; | ||
use Symfony\Component\AutoMapper\Transformer\NullableTransformerFactory; | ||
use Symfony\Component\AutoMapper\Transformer\ObjectTransformerFactory; | ||
use Symfony\Component\AutoMapper\Transformer\UniqueTypeTransformerFactory; | ||
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; | ||
|
||
/** | ||
* Maps a source data structure (object or array) to a target one. | ||
* | ||
* @expiremental in 5.1 | ||
* | ||
* @author Joel Wurtz <jwurtz@jolicode.com> | ||
*/ | ||
final class AutoMapper implements AutoMapperInterface, AutoMapperRegistryInterface, MapperGeneratorMetadataRegistryInterface | ||
{ | ||
/** @var MapperGeneratorMetadataInterface[] */ | ||
private $metadata = []; | ||
|
||
/** @var GeneratedMapper[] */ | ||
private $mapperRegistry = []; | ||
|
||
private $classLoader; | ||
|
||
private $mapperConfigurationFactory; | ||
|
||
public function __construct(ClassLoaderInterface $classLoader, MapperGeneratorMetadataFactoryInterface $mapperConfigurationFactory = null) | ||
{ | ||
$this->classLoader = $classLoader; | ||
$this->mapperConfigurationFactory = $mapperConfigurationFactory; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function register(MapperGeneratorMetadataInterface $metadata): void | ||
{ | ||
$this->metadata[$metadata->getSource()][$metadata->getTarget()] = $metadata; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getMapper(string $source, string $target): MapperInterface | ||
{ | ||
$metadata = $this->getMetadata($source, $target); | ||
|
||
if (null === $metadata) { | ||
throw new NoMappingFoundException('No mapping found for source '.$source.' and target '.$target); | ||
} | ||
|
||
$className = $metadata->getMapperClassName(); | ||
|
||
if (\array_key_exists($className, $this->mapperRegistry)) { | ||
return $this->mapperRegistry[$className]; | ||
} | ||
|
||
if (!class_exists($className)) { | ||
joelwurtz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$this->classLoader->loadClass($metadata); | ||
} | ||
|
||
$this->mapperRegistry[$className] = new $className(); | ||
$this->mapperRegistry[$className]->injectMappers($this); | ||
|
||
foreach ($metadata->getCallbacks() as $property => $callback) { | ||
$this->mapperRegistry[$className]->addCallback($property, $callback); | ||
} | ||
|
||
return $this->mapperRegistry[$className]; | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function hasMapper(string $source, string $target): bool | ||
{ | ||
return null !== $this->getMetadata($source, $target); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function map($sourceData, $targetData, array $context = []) | ||
{ | ||
$source = null; | ||
$target = null; | ||
|
||
if (null === $sourceData) { | ||
return null; | ||
} | ||
|
||
if (\is_object($sourceData)) { | ||
$source = \get_class($sourceData); | ||
} elseif (\is_array($sourceData)) { | ||
$source = 'array'; | ||
} | ||
|
||
if (null === $source) { | ||
throw new NoMappingFoundException('Cannot map this value, source is neither an object or an array.'); | ||
} | ||
|
||
if (\is_object($targetData)) { | ||
$target = \get_class($targetData); | ||
$context[MapperContext::TARGET_TO_POPULATE] = $targetData; | ||
} elseif (\is_array($targetData)) { | ||
$target = 'array'; | ||
$context[MapperContext::TARGET_TO_POPULATE] = $targetData; | ||
} elseif (\is_string($targetData)) { | ||
$target = $targetData; | ||
} | ||
|
||
if (null === $target) { | ||
joelwurtz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
throw new NoMappingFoundException('Cannot map this value, target is neither an object or an array.'); | ||
} | ||
|
||
if ('array' === $source && 'array' === $target) { | ||
throw new NoMappingFoundException('Cannot map this value, both source and target are array.'); | ||
} | ||
|
||
return $this->getMapper($source, $target)->map($sourceData, $context); | ||
} | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function getMetadata(string $source, string $target): ?MapperGeneratorMetadataInterface | ||
{ | ||
if (!isset($this->metadata[$source][$target])) { | ||
if (null === $this->mapperConfigurationFactory) { | ||
return null; | ||
} | ||
|
||
$this->register($this->mapperConfigurationFactory->create($this, $source, $target)); | ||
} | ||
|
||
return $this->metadata[$source][$target]; | ||
} | ||
|
||
/** | ||
* Create an automapper. | ||
*/ | ||
public static function create( | ||
joelwurtz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
bool $private = true, | ||
ClassLoaderInterface $loader = null, | ||
AdvancedNameConverterInterface $nameConverter = null, | ||
string $classPrefix = 'Mapper_', | ||
bool $attributeChecking = true, | ||
bool $autoRegister = true | ||
): self { | ||
$classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); | ||
|
||
if (null === $loader) { | ||
$loader = new EvalLoader(new Generator( | ||
(new ParserFactory())->create(ParserFactory::PREFER_PHP7), | ||
new ClassDiscriminatorFromClassMetadata($classMetadataFactory) | ||
)); | ||
} | ||
|
||
$flags = ReflectionExtractor::ALLOW_PUBLIC; | ||
|
||
if ($private) { | ||
joelwurtz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$flags |= ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE; | ||
} | ||
|
||
$reflectionExtractor = new ReflectionExtractor( | ||
null, | ||
null, | ||
null, | ||
true, | ||
$flags | ||
); | ||
|
||
$phpDocExtractor = new PhpDocExtractor(); | ||
$propertyInfoExtractor = new PropertyInfoExtractor( | ||
[$reflectionExtractor], | ||
[$phpDocExtractor, $reflectionExtractor], | ||
[$reflectionExtractor], | ||
[$reflectionExtractor] | ||
); | ||
|
||
$transformerFactory = new ChainTransformerFactory(); | ||
$sourceTargetMappingExtractor = new SourceTargetMappingExtractor( | ||
$propertyInfoExtractor, | ||
$reflectionExtractor, | ||
$reflectionExtractor, | ||
$transformerFactory, | ||
$classMetadataFactory | ||
); | ||
|
||
$fromTargetMappingExtractor = new FromTargetMappingExtractor( | ||
$propertyInfoExtractor, | ||
$reflectionExtractor, | ||
$reflectionExtractor, | ||
$transformerFactory, | ||
$classMetadataFactory, | ||
$nameConverter | ||
); | ||
|
||
$fromSourceMappingExtractor = new FromSourceMappingExtractor( | ||
$propertyInfoExtractor, | ||
$reflectionExtractor, | ||
$reflectionExtractor, | ||
$transformerFactory, | ||
$classMetadataFactory, | ||
$nameConverter | ||
); | ||
|
||
$autoMapper = $autoRegister ? new self($loader, new MapperGeneratorMetadataFactory( | ||
$sourceTargetMappingExtractor, | ||
$fromSourceMappingExtractor, | ||
$fromTargetMappingExtractor, | ||
$classPrefix, | ||
$attributeChecking | ||
)) : new self($loader); | ||
|
||
$transformerFactory->addTransformerFactory(new MultipleTransformerFactory($transformerFactory)); | ||
$transformerFactory->addTransformerFactory(new NullableTransformerFactory($transformerFactory)); | ||
$transformerFactory->addTransformerFactory(new UniqueTypeTransformerFactory($transformerFactory)); | ||
$transformerFactory->addTransformerFactory(new DateTimeTransformerFactory()); | ||
$transformerFactory->addTransformerFactory(new BuiltinTransformerFactory()); | ||
$transformerFactory->addTransformerFactory(new ArrayTransformerFactory($transformerFactory)); | ||
$transformerFactory->addTransformerFactory(new ObjectTransformerFactory($autoMapper)); | ||
|
||
return $autoMapper; | ||
} | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\AutoMapper; | ||
|
||
/** | ||
* An auto mapper has the role of mapping a source to a target. | ||
* | ||
* @expiremental in 5.1 | ||
* | ||
* @author Joel Wurtz <jwurtz@jolicode.com> | ||
*/ | ||
interface AutoMapperInterface | ||
{ | ||
/** | ||
* Maps data from a source to a target. | ||
* | ||
* @param array|object $source Any data object, which may be an object or an array | ||
* @param string|array|object $target To which type of data, or data, the source should be mapped | ||
* @param array $context Mapper context | ||
* | ||
* @return array|object The mapped object | ||
*/ | ||
public function map($source, $target, array $context = []); | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.