diff --git a/composer.json b/composer.json index b1d530e233186..2a15956313430 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "ext-xml": "*", "doctrine/event-manager": "~1.0", "doctrine/persistence": "^1.3", + "nikic/php-parser": "^4.0", "twig/twig": "^2.10|^3.0", "psr/cache": "~1.0", "psr/container": "^1.0", @@ -40,6 +41,7 @@ "replace": { "symfony/asset": "self.version", "symfony/amazon-mailer": "self.version", + "symfony/auto-mapper": "self.version", "symfony/browser-kit": "self.version", "symfony/cache": "self.version", "symfony/config": "self.version", diff --git a/src/Symfony/Component/AutoMapper/.gitignore b/src/Symfony/Component/AutoMapper/.gitignore new file mode 100644 index 0000000000000..c49a5d8df5c65 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/.gitignore @@ -0,0 +1,3 @@ +vendor/ +composer.lock +phpunit.xml diff --git a/src/Symfony/Component/AutoMapper/AutoMapper.php b/src/Symfony/Component/AutoMapper/AutoMapper.php new file mode 100644 index 0000000000000..d30fb4602f6ce --- /dev/null +++ b/src/Symfony/Component/AutoMapper/AutoMapper.php @@ -0,0 +1,257 @@ + + * + * 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 + */ +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)) { + $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) { + 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( + 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) { + $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; + } +} diff --git a/src/Symfony/Component/AutoMapper/AutoMapperInterface.php b/src/Symfony/Component/AutoMapper/AutoMapperInterface.php new file mode 100644 index 0000000000000..facf2b704bcd2 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/AutoMapperInterface.php @@ -0,0 +1,33 @@ + + * + * 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 + */ +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 = []); +} diff --git a/src/Symfony/Component/AutoMapper/AutoMapperNormalizer.php b/src/Symfony/Component/AutoMapper/AutoMapperNormalizer.php new file mode 100644 index 0000000000000..47c297fe16399 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/AutoMapperNormalizer.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper; + +use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; +use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; + +/** + * Bridge for symfony/serializer. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +class AutoMapperNormalizer implements NormalizerInterface, DenormalizerInterface +{ + private $autoMapper; + + public function __construct(AutoMapper $autoMapper) + { + $this->autoMapper = $autoMapper; + } + + public function normalize($object, $format = null, array $context = []) + { + $autoMapperContext = $this->createAutoMapperContext($context); + + return $this->autoMapper->map($object, 'array', $autoMapperContext); + } + + public function denormalize($data, $class, $format = null, array $context = []) + { + $autoMapperContext = $this->createAutoMapperContext($context); + + return $this->autoMapper->map($data, $class, $autoMapperContext); + } + + public function supportsNormalization($data, $format = null) + { + if (!\is_object($data) || $data instanceof \stdClass) { + return false; + } + + return $this->autoMapper->hasMapper(\get_class($data), 'array'); + } + + public function supportsDenormalization($data, $type, $format = null) + { + return $this->autoMapper->hasMapper('array', $type); + } + + public function hasCacheableSupportsMethod(): bool + { + return true; + } + + private function createAutoMapperContext(array $serializerContext = []): array + { + $context = [ + MapperContext::GROUPS => $serializerContext[AbstractNormalizer::GROUPS] ?? null, + MapperContext::ALLOWED_ATTRIBUTES => $serializerContext[AbstractNormalizer::ATTRIBUTES] ?? null, + MapperContext::IGNORED_ATTRIBUTES => $serializerContext[AbstractNormalizer::IGNORED_ATTRIBUTES] ?? null, + MapperContext::TARGET_TO_POPULATE => $serializerContext[AbstractNormalizer::OBJECT_TO_POPULATE] ?? null, + MapperContext::CIRCULAR_REFERENCE_LIMIT => $serializerContext[AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT] ?? 1, + MapperContext::CIRCULAR_REFERENCE_HANDLER => $serializerContext[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER] ?? null, + ]; + + if (\array_key_exists(AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS, $serializerContext) && is_iterable($serializerContext[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS])) { + foreach ($serializerContext[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS] as $class => $keyArgs) { + foreach ($keyArgs as $key => $value) { + $context[MapperContext::CONSTRUCTOR_ARGUMENTS][$class][$key] = $value; + } + } + } + + return $context; + } +} diff --git a/src/Symfony/Component/AutoMapper/AutoMapperRegistryInterface.php b/src/Symfony/Component/AutoMapper/AutoMapperRegistryInterface.php new file mode 100644 index 0000000000000..0b2dc60feada3 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/AutoMapperRegistryInterface.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper; + +/** + * Allows to retrieve a mapper. + * + * @internal + * + * @author Joel Wurtz + */ +interface AutoMapperRegistryInterface +{ + /** + * Gets a specific mapper for a source type and a target type. + * + * @param string $source Source type + * @param string $target Target type + * + * @return MapperInterface return associated mapper + */ + public function getMapper(string $source, string $target): MapperInterface; + + /** + * Does a specific mapper exist. + * + * @param string $source Source type + * @param string $target Target type + */ + public function hasMapper(string $source, string $target): bool; +} diff --git a/src/Symfony/Component/AutoMapper/CHANGELOG.md b/src/Symfony/Component/AutoMapper/CHANGELOG.md new file mode 100644 index 0000000000000..9f62849f35bd8 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/CHANGELOG.md @@ -0,0 +1,7 @@ +CHANGELOG +========= + +5.1.0 +----- + + * Initial release diff --git a/src/Symfony/Component/AutoMapper/Exception/CircularReferenceException.php b/src/Symfony/Component/AutoMapper/Exception/CircularReferenceException.php new file mode 100644 index 0000000000000..6e0e0852357b1 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Exception/CircularReferenceException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Exception; + +/** + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +class CircularReferenceException extends \RuntimeException +{ +} diff --git a/src/Symfony/Component/AutoMapper/Exception/CompileException.php b/src/Symfony/Component/AutoMapper/Exception/CompileException.php new file mode 100644 index 0000000000000..4f2db214b1774 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Exception/CompileException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Exception; + +/** + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +class CompileException extends \RuntimeException +{ +} diff --git a/src/Symfony/Component/AutoMapper/Exception/InvalidMappingException.php b/src/Symfony/Component/AutoMapper/Exception/InvalidMappingException.php new file mode 100644 index 0000000000000..f741bd68a4a85 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Exception/InvalidMappingException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Exception; + +/** + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +class InvalidMappingException extends \RuntimeException +{ +} diff --git a/src/Symfony/Component/AutoMapper/Exception/NoMappingFoundException.php b/src/Symfony/Component/AutoMapper/Exception/NoMappingFoundException.php new file mode 100644 index 0000000000000..b9ffe095c696f --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Exception/NoMappingFoundException.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Exception; + +/** + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +class NoMappingFoundException extends \RuntimeException +{ +} diff --git a/src/Symfony/Component/AutoMapper/Extractor/FromSourceMappingExtractor.php b/src/Symfony/Component/AutoMapper/Extractor/FromSourceMappingExtractor.php new file mode 100644 index 0000000000000..a732d131b4636 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Extractor/FromSourceMappingExtractor.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Extractor; + +use Symfony\Component\AutoMapper\Exception\InvalidMappingException; +use Symfony\Component\AutoMapper\MapperMetadataInterface; +use Symfony\Component\AutoMapper\Transformer\TransformerFactoryInterface; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; + +/** + * Mapping extracted only from source, useful when not having metadata on the target for dynamic data like array, \stdClass, ... + * + * Can use a NameConverter to use specific properties name in the target + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class FromSourceMappingExtractor extends MappingExtractor +{ + private const ALLOWED_TARGETS = ['array', \stdClass::class]; + + private $nameConverter; + + public function __construct(PropertyInfoExtractorInterface $propertyInfoExtractor, PropertyReadInfoExtractorInterface $readInfoExtractor, PropertyWriteInfoExtractorInterface $writeInfoExtractor, TransformerFactoryInterface $transformerFactory, ClassMetadataFactoryInterface $classMetadataFactory = null, AdvancedNameConverterInterface $nameConverter = null) + { + parent::__construct($propertyInfoExtractor, $readInfoExtractor, $writeInfoExtractor, $transformerFactory, $classMetadataFactory); + + $this->nameConverter = $nameConverter; + } + + /** + * {@inheritdoc} + */ + public function getPropertiesMapping(MapperMetadataInterface $mapperMetadata): array + { + $sourceProperties = $this->propertyInfoExtractor->getProperties($mapperMetadata->getSource()); + + if (!\in_array($mapperMetadata->getTarget(), self::ALLOWED_TARGETS, true)) { + throw new InvalidMappingException('Only array or stdClass are accepted as a target'); + } + + if (null === $sourceProperties) { + return []; + } + + $sourceProperties = array_unique($sourceProperties); + $mapping = []; + + foreach ($sourceProperties as $property) { + if (!$this->propertyInfoExtractor->isReadable($mapperMetadata->getSource(), $property)) { + continue; + } + + $sourceTypes = $this->propertyInfoExtractor->getTypes($mapperMetadata->getSource(), $property); + + if (null === $sourceTypes) { + continue; + } + + $targetTypes = []; + + foreach ($sourceTypes as $type) { + $targetTypes[] = $this->transformType($mapperMetadata->getTarget(), $type); + } + + $transformer = $this->transformerFactory->getTransformer($sourceTypes, $targetTypes, $mapperMetadata); + + if (null === $transformer) { + continue; + } + + $mapping[] = new PropertyMapping( + $this->getReadAccessor($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property), + $this->getWriteMutator($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property), + null, + $transformer, + $property, + false, + $this->getGroups($mapperMetadata->getSource(), $property), + $this->getGroups($mapperMetadata->getTarget(), $property), + $this->getMaxDepth($mapperMetadata->getSource(), $property) + ); + } + + return $mapping; + } + + private function transformType(string $target, Type $type = null): ?Type + { + if (null === $type) { + return null; + } + + $builtinType = $type->getBuiltinType(); + $className = $type->getClassName(); + + if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && \stdClass::class !== $type->getClassName()) { + $builtinType = 'array' === $target ? Type::BUILTIN_TYPE_ARRAY : Type::BUILTIN_TYPE_OBJECT; + $className = 'array' === $target ? null : \stdClass::class; + } + + // Use string for datetime + if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && (\DateTimeInterface::class === $type->getClassName() || is_subclass_of($type->getClassName(), \DateTimeInterface::class))) { + $builtinType = 'string'; + } + + return new Type( + $builtinType, + $type->isNullable(), + $className, + $type->isCollection(), + $this->transformType($target, $type->getCollectionKeyType()), + $this->transformType($target, $type->getCollectionValueType()) + ); + } + + /** + * {@inheritdoc} + */ + public function getWriteMutator(string $source, string $target, string $property, array $context = []): WriteMutator + { + if (null !== $this->nameConverter) { + $property = $this->nameConverter->normalize($property, $source, $target); + } + + $targetMutator = new WriteMutator(WriteMutator::TYPE_ARRAY_DIMENSION, $property, false); + + if (\stdClass::class === $target) { + $targetMutator = new WriteMutator(WriteMutator::TYPE_PROPERTY, $property, false); + } + + return $targetMutator; + } +} diff --git a/src/Symfony/Component/AutoMapper/Extractor/FromTargetMappingExtractor.php b/src/Symfony/Component/AutoMapper/Extractor/FromTargetMappingExtractor.php new file mode 100644 index 0000000000000..e861cf2439f0e --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Extractor/FromTargetMappingExtractor.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Extractor; + +use Symfony\Component\AutoMapper\Exception\InvalidMappingException; +use Symfony\Component\AutoMapper\MapperMetadataInterface; +use Symfony\Component\AutoMapper\Transformer\TransformerFactoryInterface; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; +use Symfony\Component\PropertyInfo\Type; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; +use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; + +/** + * Mapping extracted only from target, useful when not having metadata on the source for dynamic data like array, \stdClass, ... + * + * Can use a NameConverter to use specific properties name in the source + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class FromTargetMappingExtractor extends MappingExtractor +{ + private const ALLOWED_SOURCES = ['array', \stdClass::class]; + + private $nameConverter; + + public function __construct(PropertyInfoExtractorInterface $propertyInfoExtractor, PropertyReadInfoExtractorInterface $readInfoExtractor, PropertyWriteInfoExtractorInterface $writeInfoExtractor, TransformerFactoryInterface $transformerFactory, ClassMetadataFactoryInterface $classMetadataFactory = null, AdvancedNameConverterInterface $nameConverter = null) + { + parent::__construct($propertyInfoExtractor, $readInfoExtractor, $writeInfoExtractor, $transformerFactory, $classMetadataFactory); + + $this->nameConverter = $nameConverter; + } + + /** + * {@inheritdoc} + */ + public function getPropertiesMapping(MapperMetadataInterface $mapperMetadata): array + { + $targetProperties = array_unique($this->propertyInfoExtractor->getProperties($mapperMetadata->getTarget()) ?? []); + + if (!\in_array($mapperMetadata->getSource(), self::ALLOWED_SOURCES, true)) { + throw new InvalidMappingException('Only array or stdClass are accepted as a source'); + } + + if (null === $targetProperties) { + return []; + } + + $mapping = []; + + foreach ($targetProperties as $property) { + if (!$this->propertyInfoExtractor->isWritable($mapperMetadata->getTarget(), $property)) { + continue; + } + + $targetTypes = $this->propertyInfoExtractor->getTypes($mapperMetadata->getTarget(), $property); + + if (null === $targetTypes) { + continue; + } + + $sourceTypes = []; + + foreach ($targetTypes as $type) { + $sourceTypes[] = $this->transformType($mapperMetadata->getSource(), $type); + } + + $transformer = $this->transformerFactory->getTransformer($sourceTypes, $targetTypes, $mapperMetadata); + + if (null === $transformer) { + continue; + } + + $mapping[] = new PropertyMapping( + $this->getReadAccessor($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property), + $this->getWriteMutator($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property, [ + 'enable_constructor_extraction' => false, + ]), + $this->getWriteMutator($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property, [ + 'enable_constructor_extraction' => true, + ]), + $transformer, + $property, + true, + $this->getGroups($mapperMetadata->getSource(), $property), + $this->getGroups($mapperMetadata->getTarget(), $property), + $this->getMaxDepth($mapperMetadata->getTarget(), $property) + ); + } + + return $mapping; + } + + public function getReadAccessor(string $source, string $target, string $property): ?ReadAccessor + { + if (null !== $this->nameConverter) { + $property = $this->nameConverter->normalize($property, $target, $source); + } + + $sourceAccessor = new ReadAccessor(ReadAccessor::TYPE_ARRAY_DIMENSION, $property); + + if (\stdClass::class === $source) { + $sourceAccessor = new ReadAccessor(ReadAccessor::TYPE_PROPERTY, $property); + } + + return $sourceAccessor; + } + + private function transformType(string $source, Type $type = null): ?Type + { + if (null === $type) { + return null; + } + + $builtinType = $type->getBuiltinType(); + $className = $type->getClassName(); + + if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && \stdClass::class !== $type->getClassName()) { + $builtinType = 'array' === $source ? Type::BUILTIN_TYPE_ARRAY : Type::BUILTIN_TYPE_OBJECT; + $className = 'array' === $source ? null : \stdClass::class; + } + + if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && (\DateTimeInterface::class === $type->getClassName() || is_subclass_of($type->getClassName(), \DateTimeInterface::class))) { + $builtinType = 'string'; + } + + return new Type( + $builtinType, + $type->isNullable(), + $className, + $type->isCollection(), + $this->transformType($source, $type->getCollectionKeyType()), + $this->transformType($source, $type->getCollectionValueType()) + ); + } +} diff --git a/src/Symfony/Component/AutoMapper/Extractor/MappingExtractor.php b/src/Symfony/Component/AutoMapper/Extractor/MappingExtractor.php new file mode 100644 index 0000000000000..60c46a3eaee34 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Extractor/MappingExtractor.php @@ -0,0 +1,164 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Extractor; + +use Symfony\Component\AutoMapper\Transformer\TransformerFactoryInterface; +use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfo; +use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfo; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; + +/** + * @internal + * + * @author Joel Wurtz + */ +abstract class MappingExtractor implements MappingExtractorInterface +{ + protected $propertyInfoExtractor; + + protected $transformerFactory; + + protected $readInfoExtractor; + + protected $writeInfoExtractor; + + protected $classMetadataFactory; + + public function __construct(PropertyInfoExtractorInterface $propertyInfoExtractor, PropertyReadInfoExtractorInterface $readInfoExtractor, PropertyWriteInfoExtractorInterface $writeInfoExtractor, TransformerFactoryInterface $transformerFactory, ClassMetadataFactoryInterface $classMetadataFactory = null) + { + $this->propertyInfoExtractor = $propertyInfoExtractor; + $this->readInfoExtractor = $readInfoExtractor; + $this->writeInfoExtractor = $writeInfoExtractor; + $this->transformerFactory = $transformerFactory; + $this->classMetadataFactory = $classMetadataFactory; + } + + /** + * {@inheritdoc} + */ + public function getReadAccessor(string $source, string $target, string $property): ?ReadAccessor + { + $readInfo = $this->readInfoExtractor->getReadInfo($source, $property); + + if (null === $readInfo) { + return null; + } + + $type = ReadAccessor::TYPE_PROPERTY; + + if (PropertyReadInfo::TYPE_METHOD === $readInfo->getType()) { + $type = ReadAccessor::TYPE_METHOD; + } + + return new ReadAccessor( + $type, + $readInfo->getName(), + PropertyReadInfo::VISIBILITY_PUBLIC !== $readInfo->getVisibility() + ); + } + + /** + * {@inheritdoc} + */ + public function getWriteMutator(string $source, string $target, string $property, array $context = []): ?WriteMutator + { + $writeInfo = $this->writeInfoExtractor->getWriteInfo($target, $property, $context); + + if (null === $writeInfo) { + return null; + } + + if (PropertyWriteInfo::TYPE_NONE === $writeInfo->getType()) { + return null; + } + + if (PropertyWriteInfo::TYPE_CONSTRUCTOR === $writeInfo->getType()) { + $parameter = new \ReflectionParameter([$target, '__construct'], $writeInfo->getName()); + + return new WriteMutator(WriteMutator::TYPE_CONSTRUCTOR, $writeInfo->getName(), false, $parameter); + } + + $type = WriteMutator::TYPE_PROPERTY; + + if (PropertyWriteInfo::TYPE_METHOD === $writeInfo->getType()) { + $type = WriteMutator::TYPE_METHOD; + } + + return new WriteMutator( + $type, + $writeInfo->getName(), + PropertyReadInfo::VISIBILITY_PUBLIC !== $writeInfo->getVisibility() + ); + } + + protected function getMaxDepth($class, $property): ?int + { + if ('array' === $class) { + return null; + } + + if (null === $this->classMetadataFactory) { + return null; + } + + if (!$this->classMetadataFactory->getMetadataFor($class)) { + return null; + } + + $serializerClassMetadata = $this->classMetadataFactory->getMetadataFor($class); + $maxDepth = null; + + foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) { + if ($serializerAttributeMetadata->getName() === $property) { + $maxDepth = $serializerAttributeMetadata->getMaxDepth(); + } + } + + return $maxDepth; + } + + protected function getGroups($class, $property): ?array + { + if ('array' === $class) { + return null; + } + + if (null === $this->classMetadataFactory || !$this->classMetadataFactory->getMetadataFor($class)) { + return null; + } + + $serializerClassMetadata = $this->classMetadataFactory->getMetadataFor($class); + $anyGroupFound = false; + $groups = []; + + foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) { + $groupsFound = $serializerAttributeMetadata->getGroups(); + + if ($groupsFound) { + $anyGroupFound = true; + } + + if ($serializerAttributeMetadata->getName() === $property) { + $groups = $groupsFound; + } + } + + if (!$anyGroupFound) { + return null; + } + + return $groups; + } +} diff --git a/src/Symfony/Component/AutoMapper/Extractor/MappingExtractorInterface.php b/src/Symfony/Component/AutoMapper/Extractor/MappingExtractorInterface.php new file mode 100644 index 0000000000000..e6e3bd4e3857b --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Extractor/MappingExtractorInterface.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Extractor; + +use Symfony\Component\AutoMapper\MapperMetadataInterface; + +/** + * Extracts mapping. + * + * @internal + * + * @author Joel Wurtz + */ +interface MappingExtractorInterface +{ + /** + * Extracts properties mapped for a given source and target. + * + * @return PropertyMapping[] + */ + public function getPropertiesMapping(MapperMetadataInterface $mapperMetadata): array; + + /** + * Extracts read accessor for a given source, target and property. + */ + public function getReadAccessor(string $source, string $target, string $property): ?ReadAccessor; + + /** + * Extracts write mutator for a given source, target and property. + */ + public function getWriteMutator(string $source, string $target, string $property, array $context = []): ?WriteMutator; +} diff --git a/src/Symfony/Component/AutoMapper/Extractor/PropertyMapping.php b/src/Symfony/Component/AutoMapper/Extractor/PropertyMapping.php new file mode 100644 index 0000000000000..2cda5745cd370 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Extractor/PropertyMapping.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Extractor; + +use Symfony\Component\AutoMapper\Transformer\TransformerInterface; + +/** + * Property mapping. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class PropertyMapping +{ + private $readAccessor; + + private $writeMutator; + + private $writeMutatorConstructor; + + private $transformer; + + private $checkExists; + + private $property; + + private $sourceGroups; + + private $targetGroups; + + private $maxDepth; + + public function __construct( + ReadAccessor $readAccessor, + ?WriteMutator $writeMutator, + ?WriteMutator $writeMutatorConstructor, + TransformerInterface $transformer, + string $property, + bool $checkExists = false, + array $sourceGroups = null, + array $targetGroups = null, + ?int $maxDepth = null + ) { + $this->readAccessor = $readAccessor; + $this->writeMutator = $writeMutator; + $this->writeMutatorConstructor = $writeMutatorConstructor; + $this->transformer = $transformer; + $this->property = $property; + $this->checkExists = $checkExists; + $this->sourceGroups = $sourceGroups; + $this->targetGroups = $targetGroups; + $this->maxDepth = $maxDepth; + } + + public function getReadAccessor(): ReadAccessor + { + return $this->readAccessor; + } + + public function getWriteMutator(): ?WriteMutator + { + return $this->writeMutator; + } + + public function getWriteMutatorConstructor(): ?WriteMutator + { + return $this->writeMutatorConstructor; + } + + public function getTransformer(): TransformerInterface + { + return $this->transformer; + } + + public function getProperty(): string + { + return $this->property; + } + + public function checkExists(): bool + { + return $this->checkExists; + } + + public function getSourceGroups(): ?array + { + return $this->sourceGroups; + } + + public function getTargetGroups(): ?array + { + return $this->targetGroups; + } + + public function getMaxDepth(): ?int + { + return $this->maxDepth; + } +} diff --git a/src/Symfony/Component/AutoMapper/Extractor/ReadAccessor.php b/src/Symfony/Component/AutoMapper/Extractor/ReadAccessor.php new file mode 100644 index 0000000000000..138e9aebca0fd --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Extractor/ReadAccessor.php @@ -0,0 +1,106 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Extractor; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PhpParser\Node\Param; +use PhpParser\Node\Scalar; +use PhpParser\Node\Stmt; +use Symfony\Component\AutoMapper\Exception\CompileException; + +/** + * Read accessor tell how to read from a property. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class ReadAccessor +{ + public const TYPE_METHOD = 1; + public const TYPE_PROPERTY = 2; + public const TYPE_ARRAY_DIMENSION = 3; + public const TYPE_SOURCE = 4; + + private $type; + + private $name; + + private $private; + + public function __construct(int $type, string $name, $private = false) + { + $this->type = $type; + $this->name = $name; + $this->private = $private; + } + + /** + * Get AST expression for reading property from an input. + * + * @throws CompileException + */ + public function getExpression(Expr\Variable $input): Expr + { + if (self::TYPE_METHOD === $this->type) { + return new Expr\MethodCall($input, $this->name); + } + + if (self::TYPE_PROPERTY === $this->type) { + if ($this->private) { + return new Expr\FuncCall( + new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->name)), + [ + new Arg($input), + ] + ); + } + + return new Expr\PropertyFetch($input, $this->name); + } + + if (self::TYPE_ARRAY_DIMENSION === $this->type) { + return new Expr\ArrayDimFetch($input, new Scalar\String_($this->name)); + } + + if (self::TYPE_SOURCE === $this->type) { + return $input; + } + + throw new CompileException('Invalid accessor for read expression'); + } + + /** + * Get AST expression for binding closure when dealing with a private property. + */ + public function getExtractCallback($className): ?Expr + { + if (self::TYPE_PROPERTY !== $this->type || !$this->private) { + return null; + } + + return new Expr\StaticCall(new Name\FullyQualified(\Closure::class), 'bind', [ + new Arg(new Expr\Closure([ + 'params' => [ + new Param(new Expr\Variable('object')), + ], + 'stmts' => [ + new Stmt\Return_(new Expr\PropertyFetch(new Expr\Variable('object'), $this->name)), + ], + ])), + new Arg(new Expr\ConstFetch(new Name('null'))), + new Arg(new Scalar\String_(new Name\FullyQualified($className))), + ]); + } +} diff --git a/src/Symfony/Component/AutoMapper/Extractor/SourceTargetMappingExtractor.php b/src/Symfony/Component/AutoMapper/Extractor/SourceTargetMappingExtractor.php new file mode 100644 index 0000000000000..7276a451a8b5a --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Extractor/SourceTargetMappingExtractor.php @@ -0,0 +1,97 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Extractor; + +use Symfony\Component\AutoMapper\MapperMetadataInterface; + +/** + * Extracts mapping between two objects, only gives properties that have the same name. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +class SourceTargetMappingExtractor extends MappingExtractor +{ + /** + * {@inheritdoc} + */ + public function getPropertiesMapping(MapperMetadataInterface $mapperMetadata): array + { + $sourceProperties = $this->propertyInfoExtractor->getProperties($mapperMetadata->getSource()); + $targetProperties = $this->propertyInfoExtractor->getProperties($mapperMetadata->getTarget()); + + if (null === $sourceProperties || null === $targetProperties) { + return []; + } + + $sourceProperties = array_unique($sourceProperties ?? []); + $targetProperties = array_unique($targetProperties ?? []); + + $mapping = []; + + foreach ($sourceProperties as $property) { + if (!$this->propertyInfoExtractor->isReadable($mapperMetadata->getSource(), $property)) { + continue; + } + + if (\in_array($property, $targetProperties, true)) { + $targetMutatorConstruct = $this->getWriteMutator($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property, [ + 'enable_constructor_extraction' => true, + ]); + + if ((null === $targetMutatorConstruct || null === $targetMutatorConstruct->getParameter()) && !$this->propertyInfoExtractor->isWritable($mapperMetadata->getTarget(), $property)) { + continue; + } + + $sourceTypes = $this->propertyInfoExtractor->getTypes($mapperMetadata->getSource(), $property); + $targetTypes = $this->propertyInfoExtractor->getTypes($mapperMetadata->getTarget(), $property); + $transformer = $this->transformerFactory->getTransformer($sourceTypes, $targetTypes, $mapperMetadata); + + if (null === $transformer) { + continue; + } + + $sourceAccessor = $this->getReadAccessor($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property); + $targetMutator = $this->getWriteMutator($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property, [ + 'enable_constructor_extraction' => false, + ]); + + $maxDepthSource = $this->getMaxDepth($mapperMetadata->getSource(), $property); + $maxDepthTarget = $this->getMaxDepth($mapperMetadata->getTarget(), $property); + $maxDepth = null; + + if (null !== $maxDepthSource && null !== $maxDepthTarget) { + $maxDepth = min($maxDepthSource, $maxDepthTarget); + } elseif (null !== $maxDepthSource) { + $maxDepth = $maxDepthSource; + } elseif (null !== $maxDepthTarget) { + $maxDepth = $maxDepthTarget; + } + + $mapping[] = new PropertyMapping( + $sourceAccessor, + $targetMutator, + WriteMutator::TYPE_CONSTRUCTOR === $targetMutatorConstruct->getType() ? $targetMutatorConstruct : null, + $transformer, + $property, + false, + $this->getGroups($mapperMetadata->getSource(), $property), + $this->getGroups($mapperMetadata->getTarget(), $property), + $maxDepth + ); + } + } + + return $mapping; + } +} diff --git a/src/Symfony/Component/AutoMapper/Extractor/WriteMutator.php b/src/Symfony/Component/AutoMapper/Extractor/WriteMutator.php new file mode 100644 index 0000000000000..3b16999cdaec5 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Extractor/WriteMutator.php @@ -0,0 +1,126 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Extractor; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PhpParser\Node\Param; +use PhpParser\Node\Scalar; +use PhpParser\Node\Stmt; +use Symfony\Component\AutoMapper\Exception\CompileException; + +/** + * Writes mutator tell how to write to a property. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class WriteMutator +{ + public const TYPE_METHOD = 1; + public const TYPE_PROPERTY = 2; + public const TYPE_ARRAY_DIMENSION = 3; + public const TYPE_CONSTRUCTOR = 4; + + private $type; + private $name; + private $private; + private $parameter; + + public function __construct(int $type, string $name, bool $private = false, \ReflectionParameter $parameter = null) + { + $this->type = $type; + $this->name = $name; + $this->private = $private; + $this->parameter = $parameter; + } + + public function getType(): int + { + return $this->type; + } + + /** + * Get AST expression for writing from a value to an output. + * + * @throws CompileException + */ + public function getExpression(Expr\Variable $output, Expr $value, bool $byRef = false): ?Expr + { + if (self::TYPE_METHOD === $this->type) { + return new Expr\MethodCall($output, $this->name, [ + new Arg($value), + ]); + } + + if (self::TYPE_PROPERTY === $this->type) { + if ($this->private) { + return new Expr\FuncCall( + new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'hydrateCallbacks'), new Scalar\String_($this->name)), + [ + new Arg($output), + new Arg($value), + ] + ); + } + if ($byRef) { + return new Expr\AssignRef(new Expr\PropertyFetch($output, $this->name), $value); + } + + return new Expr\Assign(new Expr\PropertyFetch($output, $this->name), $value); + } + + if (self::TYPE_ARRAY_DIMENSION === $this->type) { + if ($byRef) { + return new Expr\AssignRef(new Expr\ArrayDimFetch($output, new Scalar\String_($this->name)), $value); + } + + return new Expr\Assign(new Expr\ArrayDimFetch($output, new Scalar\String_($this->name)), $value); + } + + throw new CompileException('Invalid accessor for write expression'); + } + + /** + * Get AST expression for binding closure when dealing with private property. + */ + public function getHydrateCallback($className): ?Expr + { + if (self::TYPE_PROPERTY !== $this->type || !$this->private) { + return null; + } + + return new Expr\StaticCall(new Name\FullyQualified(\Closure::class), 'bind', [ + new Arg(new Expr\Closure([ + 'params' => [ + new Param(new Expr\Variable('object')), + new Param(new Expr\Variable('value')), + ], + 'stmts' => [ + new Stmt\Expression(new Expr\Assign(new Expr\PropertyFetch(new Expr\Variable('object'), $this->name), new Expr\Variable('value'))), + ], + ])), + new Arg(new Expr\ConstFetch(new Name('null'))), + new Arg(new Scalar\String_(new Name\FullyQualified($className))), + ]); + } + + /** + * Get reflection parameter. + */ + public function getParameter(): ?\ReflectionParameter + { + return $this->parameter; + } +} diff --git a/src/Symfony/Component/AutoMapper/GeneratedMapper.php b/src/Symfony/Component/AutoMapper/GeneratedMapper.php new file mode 100644 index 0000000000000..11a4cc58a6cc7 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/GeneratedMapper.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper; + +/** + * Class derived for each generated mapper. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +abstract class GeneratedMapper implements MapperInterface +{ + protected $mappers = []; + + protected $callbacks; + + protected $hydrateCallbacks = []; + + protected $extractCallbacks = []; + + protected $cachedTarget; + + protected $circularReferenceHandler; + + protected $circularReferenceLimit; + + /** + * Add a callable for a specific property. + */ + public function addCallback(string $name, callable $callback): void + { + $this->callbacks[$name] = $callback; + } + + /** + * Inject sub mappers. + */ + public function injectMappers(AutoMapperRegistryInterface $autoMapperRegistry): void + { + } + + public function setCircularReferenceHandler(?callable $circularReferenceHandler): void + { + $this->circularReferenceHandler = $circularReferenceHandler; + } + + public function setCircularReferenceLimit(?int $circularReferenceLimit): void + { + $this->circularReferenceLimit = $circularReferenceLimit; + } +} diff --git a/src/Symfony/Component/AutoMapper/Generator/Generator.php b/src/Symfony/Component/AutoMapper/Generator/Generator.php new file mode 100644 index 0000000000000..18cf48e7c57fa --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Generator/Generator.php @@ -0,0 +1,464 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Generator; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PhpParser\Node\Param; +use PhpParser\Node\Scalar; +use PhpParser\Node\Stmt; +use PhpParser\Parser; +use PhpParser\ParserFactory; +use Symfony\Component\AutoMapper\AutoMapperRegistryInterface; +use Symfony\Component\AutoMapper\Exception\CompileException; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\GeneratedMapper; +use Symfony\Component\AutoMapper\MapperContext; +use Symfony\Component\AutoMapper\MapperGeneratorMetadataInterface; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface; + +/** + * Generates code for a mapping class. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class Generator +{ + private $parser; + + private $classDiscriminator; + + public function __construct(Parser $parser = null, ClassDiscriminatorResolverInterface $classDiscriminator = null) + { + $this->parser = $parser ?? (new ParserFactory())->create(ParserFactory::PREFER_PHP7); + $this->classDiscriminator = $classDiscriminator; + } + + /** + * Generate Class AST given metadata for a mapper. + * + * @throws CompileException + */ + public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetadata): Stmt\Class_ + { + $propertiesMapping = $mapperGeneratorMetadata->getPropertiesMapping(); + + $uniqueVariableScope = new UniqueVariableScope(); + $sourceInput = new Expr\Variable($uniqueVariableScope->getUniqueName('value')); + $result = new Expr\Variable($uniqueVariableScope->getUniqueName('result')); + $hashVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('sourceHash')); + $contextVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('context')); + $constructStatements = []; + $addedDependencies = []; + $canHaveCircularDependency = $mapperGeneratorMetadata->canHaveCircularReference() && 'array' !== $mapperGeneratorMetadata->getSource(); + + $statements = [ + new Stmt\If_(new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), $sourceInput), [ + 'stmts' => [new Stmt\Return_($sourceInput)], + ]), + ]; + + if ($canHaveCircularDependency) { + $statements[] = new Stmt\Expression(new Expr\Assign($hashVariable, new Expr\BinaryOp\Concat(new Expr\FuncCall(new Name('spl_object_hash'), [ + new Arg($sourceInput), + ]), + new Scalar\String_($mapperGeneratorMetadata->getTarget()) + ))); + $statements[] = new Stmt\If_(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), new Name('shouldHandleCircularReference'), [ + new Arg($contextVariable), + new Arg($hashVariable), + new Arg(new Expr\PropertyFetch(new Expr\Variable('this'), 'circularReferenceLimit')), + ]), [ + 'stmts' => [ + new Stmt\Return_(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'handleCircularReference', [ + new Arg($contextVariable), + new Arg($hashVariable), + new Arg($sourceInput), + new Arg(new Expr\PropertyFetch(new Expr\Variable('this'), 'circularReferenceLimit')), + new Arg(new Expr\PropertyFetch(new Expr\Variable('this'), 'circularReferenceHandler')), + ])), + ], + ]); + } + + [$createObjectStmts, $inConstructor, $constructStatementsForCreateObjects, $injectMapperStatements] = $this->getCreateObjectStatements($mapperGeneratorMetadata, $result, $contextVariable, $sourceInput, $uniqueVariableScope); + $constructStatements = array_merge($constructStatements, $constructStatementsForCreateObjects); + + $statements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\BinaryOp\Coalesce( + new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::TARGET_TO_POPULATE)), + new Expr\ConstFetch(new Name('null')) + ))); + $statements[] = new Stmt\If_(new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), $result), [ + 'stmts' => $createObjectStmts, + ]); + + /** @var PropertyMapping $propertyMapping */ + foreach ($propertiesMapping as $propertyMapping) { + $transformer = $propertyMapping->getTransformer(); + + /* @var PropertyMapping $propertyMapping */ + foreach ($transformer->getDependencies() as $dependency) { + if (isset($addedDependencies[$dependency->getName()])) { + continue; + } + + $injectMapperStatements[] = new Stmt\Expression(new Expr\Assign( + new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'), new Scalar\String_($dependency->getName())), + new Expr\MethodCall(new Expr\Variable('autoMapperRegistry'), 'getMapper', [ + new Arg(new Scalar\String_($dependency->getSource())), + new Arg(new Scalar\String_($dependency->getTarget())), + ]) + )); + $addedDependencies[$dependency->getName()] = true; + } + } + + if ($addedDependencies) { + if ($canHaveCircularDependency) { + $statements[] = new Stmt\Expression(new Expr\Assign( + $contextVariable, + new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'withReference', [ + new Arg($contextVariable), + new Arg($hashVariable), + new Arg($result), + ]) + )); + } + + $statements[] = new Stmt\Expression(new Expr\Assign( + $contextVariable, + new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'withIncrementedDepth', [ + new Arg($contextVariable), + ]) + )); + } + + /** @var PropertyMapping $propertyMapping */ + foreach ($propertiesMapping as $propertyMapping) { + $transformer = $propertyMapping->getTransformer(); + + if (\in_array($propertyMapping->getProperty(), $inConstructor, true)) { + continue; + } + + [$output, $propStatements] = $transformer->transform($propertyMapping->getReadAccessor()->getExpression($sourceInput), $propertyMapping, $uniqueVariableScope); + $writeExpression = $propertyMapping->getWriteMutator()->getExpression($result, $output, $transformer->assignByRef()); + + if (null === $writeExpression) { + continue; + } + + $propStatements[] = new Stmt\Expression($writeExpression); + $conditions = []; + + $extractCallback = $propertyMapping->getReadAccessor()->getExtractCallback($mapperGeneratorMetadata->getSource()); + $hydrateCallback = $propertyMapping->getWriteMutator()->getHydrateCallback($mapperGeneratorMetadata->getTarget()); + + if (null !== $extractCallback) { + $constructStatements[] = new Stmt\Expression(new Expr\Assign( + new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($propertyMapping->getProperty())), + $extractCallback + )); + } + + if (null !== $hydrateCallback) { + $constructStatements[] = new Stmt\Expression(new Expr\Assign( + new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'hydrateCallbacks'), new Scalar\String_($propertyMapping->getProperty())), + $hydrateCallback + )); + } + + if ($propertyMapping->checkExists()) { + if (\stdClass::class === $mapperGeneratorMetadata->getSource()) { + $conditions[] = new Expr\FuncCall(new Name('property_exists'), [ + new Arg($sourceInput), + new Arg(new Scalar\String_($propertyMapping->getProperty())), + ]); + } + + if ('array' === $mapperGeneratorMetadata->getSource()) { + $conditions[] = new Expr\FuncCall(new Name('array_key_exists'), [ + new Arg(new Scalar\String_($propertyMapping->getProperty())), + new Arg($sourceInput), + ]); + } + } + + if ($mapperGeneratorMetadata->shouldCheckAttributes()) { + $conditions[] = new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'isAllowedAttribute', [ + new Arg($contextVariable), + new Arg(new Scalar\String_($propertyMapping->getProperty())), + ]); + } + + if (null !== $propertyMapping->getSourceGroups()) { + $conditions[] = new Expr\BinaryOp\BooleanAnd( + new Expr\BinaryOp\NotIdentical( + new Expr\ConstFetch(new Name('null')), + new Expr\BinaryOp\Coalesce( + new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::GROUPS)), + new Expr\Array_() + ) + ), + new Expr\FuncCall(new Name('array_intersect'), [ + new Arg(new Expr\BinaryOp\Coalesce( + new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::GROUPS)), + new Expr\Array_() + )), + new Arg(new Expr\Array_(array_map(function (string $group) { + return new Expr\ArrayItem(new Scalar\String_($group)); + }, $propertyMapping->getSourceGroups()))), + ]) + ); + } + + if (null !== $propertyMapping->getTargetGroups()) { + $conditions[] = new Expr\BinaryOp\BooleanAnd( + new Expr\BinaryOp\NotIdentical( + new Expr\ConstFetch(new Name('null')), + new Expr\BinaryOp\Coalesce( + new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::GROUPS)), + new Expr\Array_() + ) + ), + new Expr\FuncCall(new Name('array_intersect'), [ + new Arg(new Expr\BinaryOp\Coalesce( + new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::GROUPS)), + new Expr\Array_() + )), + new Arg(new Expr\Array_(array_map(function (string $group) { + return new Expr\ArrayItem(new Scalar\String_($group)); + }, $propertyMapping->getTargetGroups()))), + ]) + ); + } + + if (null !== $propertyMapping->getMaxDepth()) { + $conditions[] = new Expr\BinaryOp\SmallerOrEqual( + new Expr\BinaryOp\Coalesce( + new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::DEPTH)), + new Expr\ConstFetch(new Name('0')) + ), + new Scalar\LNumber($propertyMapping->getMaxDepth()) + ); + } + + if ($conditions) { + $condition = array_shift($conditions); + + while ($conditions) { + $condition = new Expr\BinaryOp\BooleanAnd($condition, array_shift($conditions)); + } + + $propStatements = [new Stmt\If_($condition, [ + 'stmts' => $propStatements, + ])]; + } + + foreach ($propStatements as $propStatement) { + $statements[] = $propStatement; + } + } + + $statements[] = new Stmt\Return_($result); + + $mapMethod = new Stmt\ClassMethod('map', [ + 'flags' => Stmt\Class_::MODIFIER_PUBLIC, + 'params' => [ + new Param(new Expr\Variable($sourceInput->name)), + new Param(new Expr\Variable('context'), new Expr\Array_(), 'array'), + ], + 'byRef' => true, + 'stmts' => $statements, + ]); + + $constructMethod = new Stmt\ClassMethod('__construct', [ + 'flags' => Stmt\Class_::MODIFIER_PUBLIC, + 'stmts' => $constructStatements, + ]); + + $classStmts = [$constructMethod, $mapMethod]; + + if (\count($injectMapperStatements) > 0) { + $classStmts[] = new Stmt\ClassMethod('injectMappers', [ + 'flags' => Stmt\Class_::MODIFIER_PUBLIC, + 'params' => [ + new Param(new Expr\Variable('autoMapperRegistry'), null, new Name\FullyQualified(AutoMapperRegistryInterface::class)), + ], + 'returnType' => 'void', + 'stmts' => $injectMapperStatements, + ]); + } + + return new Stmt\Class_(new Name($mapperGeneratorMetadata->getMapperClassName()), [ + 'flags' => Stmt\Class_::MODIFIER_FINAL, + 'extends' => new Name\FullyQualified(GeneratedMapper::class), + 'stmts' => $classStmts, + ]); + } + + private function getCreateObjectStatements(MapperGeneratorMetadataInterface $mapperMetadata, Expr\Variable $result, Expr\Variable $contextVariable, Expr\Variable $sourceInput, UniqueVariableScope $uniqueVariableScope): array + { + $target = $mapperMetadata->getTarget(); + $source = $mapperMetadata->getSource(); + + if ('array' === $target) { + return [[new Stmt\Expression(new Expr\Assign($result, new Expr\Array_()))], [], [], []]; + } + + if (\stdClass::class === $target) { + return [[new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name(\stdClass::class))))], [], [], []]; + } + + $reflectionClass = new \ReflectionClass($target); + $targetConstructor = $reflectionClass->getConstructor(); + $createObjectStatements = []; + $inConstructor = []; + $constructStatements = []; + $injectMapperStatements = []; + /** @var ClassDiscriminatorMapping $classDiscriminatorMapping */ + $classDiscriminatorMapping = 'array' !== $target && null !== $this->classDiscriminator ? $this->classDiscriminator->getMappingForClass($target) : null; + + if (null !== $classDiscriminatorMapping && null !== ($propertyMapping = $mapperMetadata->getPropertyMapping($classDiscriminatorMapping->getTypeProperty()))) { + [$output, $createObjectStatements] = $propertyMapping->getTransformer()->transform($propertyMapping->getReadAccessor()->getExpression($sourceInput), $propertyMapping, $uniqueVariableScope); + + foreach ($classDiscriminatorMapping->getTypesMapping() as $typeValue => $typeTarget) { + $mapperName = 'Discriminator_Mapper_'.$source.'_'.$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('autoMapperRegistry'), 'getMapper', [ + new Arg(new Scalar\String_($source)), + 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 = $mapperMetadata->getPropertiesMapping(); + + if (null !== $targetConstructor && $mapperMetadata->hasConstructor()) { + $constructArguments = []; + + /** @var PropertyMapping $propertyMapping */ + foreach ($propertiesMapping as $propertyMapping) { + if (null === $propertyMapping->getWriteMutatorConstructor() || null === ($parameter = $propertyMapping->getWriteMutatorConstructor()->getParameter())) { + continue; + } + + $constructVar = new Expr\Variable($uniqueVariableScope->getUniqueName('constructArg')); + + [$output, $propStatements] = $propertyMapping->getTransformer()->transform($propertyMapping->getReadAccessor()->getExpression($sourceInput), $propertyMapping, $uniqueVariableScope); + $constructArguments[$parameter->getPosition()] = new Arg($constructVar); + + $propStatements[] = new Stmt\Expression(new Expr\Assign($constructVar, $output)); + $createObjectStatements[] = new Stmt\If_(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'hasConstructorArgument', [ + new Arg($contextVariable), + new Arg(new Scalar\String_($target)), + new Arg(new Scalar\String_($propertyMapping->getProperty())), + ]), [ + 'stmts' => [ + new Stmt\Expression(new Expr\Assign($constructVar, new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'getConstructorArgument', [ + new Arg($contextVariable), + new Arg(new Scalar\String_($target)), + new Arg(new Scalar\String_($propertyMapping->getProperty())), + ]))), + ], + 'else' => new Stmt\Else_($propStatements), + ]); + + $inConstructor[] = $propertyMapping->getProperty(); + } + + foreach ($targetConstructor->getParameters() as $constructorParameter) { + if (!\array_key_exists($constructorParameter->getPosition(), $constructArguments) && $constructorParameter->isDefaultValueAvailable()) { + $constructVar = new Expr\Variable($uniqueVariableScope->getUniqueName('constructArg')); + + $createObjectStatements[] = new Stmt\If_(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'hasConstructorArgument', [ + new Arg($contextVariable), + new Arg(new Scalar\String_($target)), + new Arg(new Scalar\String_($constructorParameter->getName())), + ]), [ + 'stmts' => [ + new Stmt\Expression(new Expr\Assign($constructVar, new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'getConstructorArgument', [ + new Arg($contextVariable), + new Arg(new Scalar\String_($target)), + new Arg(new Scalar\String_($constructorParameter->getName())), + ]))), + ], + 'else' => new Stmt\Else_([ + new Stmt\Expression(new Expr\Assign($constructVar, $this->getValueAsExpr($constructorParameter->getDefaultValue()))), + ]), + ]); + + $constructArguments[$constructorParameter->getPosition()] = new Arg($constructVar); + } + } + + ksort($constructArguments); + + $createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name\FullyQualified($target), $constructArguments))); + } elseif (null !== $targetConstructor && $mapperMetadata->isTargetCloneable()) { + $constructStatements[] = new Stmt\Expression(new Expr\Assign( + new Expr\PropertyFetch(new Expr\Variable('this'), 'cachedTarget'), + new Expr\MethodCall(new Expr\New_(new Name\FullyQualified(\ReflectionClass::class), [ + new Arg(new Scalar\String_($target)), + ]), 'newInstanceWithoutConstructor') + )); + $createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\Clone_(new Expr\PropertyFetch(new Expr\Variable('this'), 'cachedTarget')))); + } elseif (null !== $targetConstructor) { + $constructStatements[] = new Stmt\Expression(new Expr\Assign( + new Expr\PropertyFetch(new Expr\Variable('this'), 'cachedTarget'), + new Expr\New_(new Name\FullyQualified(\ReflectionClass::class), [ + new Arg(new Scalar\String_($target)), + ]) + )); + $createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\MethodCall( + new Expr\PropertyFetch(new Expr\Variable('this'), 'cachedTarget'), + 'newInstanceWithoutConstructor' + ))); + } else { + $createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name\FullyQualified($target)))); + } + + return [$createObjectStatements, $inConstructor, $constructStatements, $injectMapperStatements]; + } + + private function getValueAsExpr($value) + { + $expr = $this->parser->parse('expr; + } + + return $expr; + } +} diff --git a/src/Symfony/Component/AutoMapper/Generator/UniqueVariableScope.php b/src/Symfony/Component/AutoMapper/Generator/UniqueVariableScope.php new file mode 100644 index 0000000000000..ef6b7e9be95e9 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Generator/UniqueVariableScope.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Generator; + +/** + * Allows to get a unique variable name for a scope (like a method). + * + * @internal + * + * @author Joel Wurtz + */ +final class UniqueVariableScope +{ + private $registry = []; + + /** + * Return an unique name for a variable name. + */ + public function getUniqueName(string $name): string + { + $name = strtolower($name); + + if (!isset($this->registry[$name])) { + $this->registry[$name] = 0; + + return $name; + } + + ++$this->registry[$name]; + + return "{$name}_{$this->registry[$name]}"; + } +} diff --git a/src/Symfony/Component/AutoMapper/LICENSE b/src/Symfony/Component/AutoMapper/LICENSE new file mode 100644 index 0000000000000..1a1869751d250 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2019 Fabien Potencier + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Symfony/Component/AutoMapper/Loader/ClassLoaderInterface.php b/src/Symfony/Component/AutoMapper/Loader/ClassLoaderInterface.php new file mode 100644 index 0000000000000..c70070f1400b4 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Loader/ClassLoaderInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Loader; + +use Symfony\Component\AutoMapper\MapperGeneratorMetadataInterface; + +/** + * Loads (require) a mapping given metadata. + * + * @expiremental in 5.1 + */ +interface ClassLoaderInterface +{ + public function loadClass(MapperGeneratorMetadataInterface $mapperMetadata): void; +} diff --git a/src/Symfony/Component/AutoMapper/Loader/EvalLoader.php b/src/Symfony/Component/AutoMapper/Loader/EvalLoader.php new file mode 100644 index 0000000000000..88ca4e68a1f32 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Loader/EvalLoader.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Loader; + +use PhpParser\PrettyPrinter\Standard; +use Symfony\Component\AutoMapper\Generator\Generator; +use Symfony\Component\AutoMapper\MapperGeneratorMetadataInterface; + +/** + * Use eval to load mappers. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class EvalLoader implements ClassLoaderInterface +{ + private $generator; + + private $printer; + + public function __construct(Generator $generator) + { + $this->generator = $generator; + $this->printer = new Standard(); + } + + /** + * {@inheritdoc} + */ + public function loadClass(MapperGeneratorMetadataInterface $mapperGeneratorMetadata): void + { + $class = $this->generator->generate($mapperGeneratorMetadata); + + eval($this->printer->prettyPrint([$class])); + } +} diff --git a/src/Symfony/Component/AutoMapper/Loader/FileLoader.php b/src/Symfony/Component/AutoMapper/Loader/FileLoader.php new file mode 100644 index 0000000000000..cfd5ccd148e16 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Loader/FileLoader.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Loader; + +use PhpParser\PrettyPrinter\Standard; +use Symfony\Component\AutoMapper\Generator\Generator; +use Symfony\Component\AutoMapper\MapperGeneratorMetadataInterface; + +/** + * Use file system to load mapper, and persist them using a registry. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class FileLoader implements ClassLoaderInterface +{ + private $generator; + + private $directory; + + private $hotReload; + + private $printer; + + private $registry; + + public function __construct(Generator $generator, string $directory, bool $hotReload = true) + { + $this->generator = $generator; + $this->directory = $directory; + $this->hotReload = $hotReload; + $this->printer = new Standard(); + } + + /** + * {@inheritdoc} + */ + public function loadClass(MapperGeneratorMetadataInterface $mapperGeneratorMetadata): void + { + $className = $mapperGeneratorMetadata->getMapperClassName(); + $classPath = $this->directory.\DIRECTORY_SEPARATOR.$className.'.php'; + + if (!$this->hotReload) { + require $classPath; + } + + $hash = $mapperGeneratorMetadata->getHash(); + $registry = $this->getRegistry(); + + if (!isset($registry[$className]) || $registry[$className] !== $hash || !file_exists($classPath)) { + $this->saveMapper($mapperGeneratorMetadata); + } + + require $classPath; + } + + public function saveMapper(MapperGeneratorMetadataInterface $mapperGeneratorMetadata): void + { + $className = $mapperGeneratorMetadata->getMapperClassName(); + $classPath = $this->directory.\DIRECTORY_SEPARATOR.$className.'.php'; + $hash = $mapperGeneratorMetadata->getHash(); + $classCode = $this->printer->prettyPrint([$this->generator->generate($mapperGeneratorMetadata)]); + + file_put_contents($classPath, "addHashToRegistry($className, $hash); + } + + private function addHashToRegistry($className, $hash) + { + $registryPath = $this->directory.\DIRECTORY_SEPARATOR.'registry.php'; + $this->registry[$className] = $hash; + file_put_contents($registryPath, "registry, true).";\n"); + } + + private function getRegistry() + { + if (!file_exists($this->directory)) { + mkdir($this->directory); + } + + if (!$this->registry) { + $registryPath = $this->directory.\DIRECTORY_SEPARATOR.'registry.php'; + + if (!file_exists($registryPath)) { + $this->registry = []; + } else { + $this->registry = require $registryPath; + } + } + + return $this->registry; + } +} diff --git a/src/Symfony/Component/AutoMapper/MapperContext.php b/src/Symfony/Component/AutoMapper/MapperContext.php new file mode 100644 index 0000000000000..12027c278778c --- /dev/null +++ b/src/Symfony/Component/AutoMapper/MapperContext.php @@ -0,0 +1,248 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper; + +use Symfony\Component\AutoMapper\Exception\CircularReferenceException; + +/** + * Context for mapping. + * + * Allows to customize how is done the mapping + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +class MapperContext +{ + public const GROUPS = 'groups'; + public const ALLOWED_ATTRIBUTES = 'allowed_attributes'; + public const IGNORED_ATTRIBUTES = 'ignored_attributes'; + public const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit'; + public const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler'; + public const CIRCULAR_REFERENCE_REGISTRY = 'circular_reference_registry'; + public const CIRCULAR_COUNT_REFERENCE_REGISTRY = 'circular_count_reference_registry'; + public const DEPTH = 'depth'; + public const TARGET_TO_POPULATE = 'target_to_populate'; + public const CONSTRUCTOR_ARGUMENTS = 'constructor_arguments'; + + private $context = [ + self::DEPTH => 0, + self::CIRCULAR_REFERENCE_REGISTRY => [], + self::CIRCULAR_COUNT_REFERENCE_REGISTRY => [], + self::CONSTRUCTOR_ARGUMENTS => [], + ]; + + public function toArray(): array + { + return $this->context; + } + + /** + * @return $this + */ + public function setGroups(?array $groups) + { + $this->context[self::GROUPS] = $groups; + + return $this; + } + + /** + * @return $this + */ + public function setAllowedAttributes(?array $allowedAttributes) + { + $this->context[self::ALLOWED_ATTRIBUTES] = $allowedAttributes; + + return $this; + } + + /** + * @return $this + */ + public function setIgnoredAttributes(?array $ignoredAttributes) + { + $this->context[self::IGNORED_ATTRIBUTES] = $ignoredAttributes; + + return $this; + } + + /** + * @return $this + */ + public function setCircularReferenceLimit(?int $circularReferenceLimit) + { + $this->context[self::CIRCULAR_REFERENCE_LIMIT] = $circularReferenceLimit; + + return $this; + } + + /** + * @return $this + */ + public function setCircularReferenceHandler(?callable $circularReferenceHandler) + { + $this->context[self::CIRCULAR_REFERENCE_HANDLER] = $circularReferenceHandler; + + return $this; + } + + /** + * @return $this + */ + public function setTargetToPopulate($target) + { + $this->context[self::TARGET_TO_POPULATE] = $target; + + return $this; + } + + /** + * @return $this + */ + public function setConstructorArgument(string $class, string $key, $value): void + { + $this->context[self::CONSTRUCTOR_ARGUMENTS][$class][$key] = $value; + } + + /** + * Whether a reference has reached it's limit. + */ + public static function shouldHandleCircularReference(array $context, string $reference, ?int $circularReferenceLimit = null): bool + { + if (!\array_key_exists($reference, $context[self::CIRCULAR_REFERENCE_REGISTRY] ?? [])) { + return false; + } + + if (null === $circularReferenceLimit) { + $circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? null; + } + + if (null !== $circularReferenceLimit) { + return $circularReferenceLimit <= ($context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference] ?? 0); + } + + return true; + } + + /** + * Handle circular reference for a specific reference. + * + * By default will try to keep it and return the previous value + * + * @return mixed + */ + public static function &handleCircularReference(array &$context, string $reference, $object, ?int $circularReferenceLimit = null, callable $callback = null) + { + if (null === $callback) { + $callback = $context[self::CIRCULAR_REFERENCE_HANDLER] ?? null; + } + + if (null !== $callback) { + // Cannot directly return here, as we need to return by reference, and callback may not be declared as reference return + $value = $callback($object, $context); + + return $value; + } + + if (null === $circularReferenceLimit) { + $circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? null; + } + + if (null !== $circularReferenceLimit) { + if ($circularReferenceLimit <= ($context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference] ?? 0)) { + throw new CircularReferenceException(sprintf('A circular reference has been detected when mapping the object of type "%s" (configured limit: %d)', \is_object($object) ? \get_class($object) : 'array', $circularReferenceLimit)); + } + + ++$context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference]; + } + + // When no limit defined return the object referenced + return $context[self::CIRCULAR_REFERENCE_REGISTRY][$reference]; + } + + /** + * Create a new context with a new reference. + */ + public static function withReference(array $context, string $reference, &$object): array + { + $context[self::CIRCULAR_REFERENCE_REGISTRY][$reference] = &$object; + $context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference] = $context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference] ?? 0; + ++$context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference]; + + return $context; + } + + /** + * Check whether an attribute is allowed to be mapped. + */ + public static function isAllowedAttribute(array $context, string $attribute): bool + { + if (($context[self::IGNORED_ATTRIBUTES] ?? false) && \in_array($attribute, $context[self::IGNORED_ATTRIBUTES], true)) { + return false; + } + + if (!($context[self::ALLOWED_ATTRIBUTES] ?? false)) { + return true; + } + + return \in_array($attribute, $context[self::ALLOWED_ATTRIBUTES], true); + } + + /** + * Clone context with a incremented depth. + */ + public static function withIncrementedDepth(array $context): array + { + $context[self::DEPTH] = $context[self::DEPTH] ?? 0; + ++$context[self::DEPTH]; + + return $context; + } + + /** + * Check wether an argument exist for the constructor for a specific class. + */ + public static function hasConstructorArgument(array $context, string $class, string $key): bool + { + return \array_key_exists($key, $context[self::CONSTRUCTOR_ARGUMENTS][$class] ?? []); + } + + /** + * Get constructor argument for a specific class. + */ + public static function getConstructorArgument(array $context, string $class, string $key) + { + return $context[self::CONSTRUCTOR_ARGUMENTS][$class][$key] ?? null; + } + + /** + * Create a new context, and reload attribute mapping for it. + */ + public static function withNewContext(array $context, string $attribute): array + { + if (!($context[self::ALLOWED_ATTRIBUTES] ?? false) && !($context[self::IGNORED_ATTRIBUTES] ?? false)) { + return $context; + } + + if (\is_array($context[self::IGNORED_ATTRIBUTES][$attribute] ?? false)) { + $context[self::IGNORED_ATTRIBUTES] = $context[self::IGNORED_ATTRIBUTES][$attribute]; + } + + if (\is_array($context[self::ALLOWED_ATTRIBUTES][$attribute] ?? false)) { + $context[self::ALLOWED_ATTRIBUTES] = $context[self::ALLOWED_ATTRIBUTES][$attribute]; + } + + return $context; + } +} diff --git a/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataFactory.php b/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataFactory.php new file mode 100644 index 0000000000000..ce6724dca90c4 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataFactory.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper; + +use Symfony\Component\AutoMapper\Extractor\FromSourceMappingExtractor; +use Symfony\Component\AutoMapper\Extractor\FromTargetMappingExtractor; +use Symfony\Component\AutoMapper\Extractor\SourceTargetMappingExtractor; + +/** + * Metadata factory, used to autoregistering new mapping without creating them. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class MapperGeneratorMetadataFactory implements MapperGeneratorMetadataFactoryInterface +{ + private $sourceTargetPropertiesMappingExtractor; + private $fromSourcePropertiesMappingExtractor; + private $fromTargetPropertiesMappingExtractor; + private $classPrefix; + private $attributeChecking; + + public function __construct( + SourceTargetMappingExtractor $sourceTargetPropertiesMappingExtractor, + FromSourceMappingExtractor $fromSourcePropertiesMappingExtractor, + FromTargetMappingExtractor $fromTargetPropertiesMappingExtractor, + string $classPrefix = 'Mapper_', + bool $attributeChecking = true + ) { + $this->sourceTargetPropertiesMappingExtractor = $sourceTargetPropertiesMappingExtractor; + $this->fromSourcePropertiesMappingExtractor = $fromSourcePropertiesMappingExtractor; + $this->fromTargetPropertiesMappingExtractor = $fromTargetPropertiesMappingExtractor; + $this->classPrefix = $classPrefix; + $this->attributeChecking = $attributeChecking; + } + + /** + * Create metadata for a source and target. + */ + public function create(MapperGeneratorMetadataRegistryInterface $autoMapperRegister, string $source, string $target): MapperGeneratorMetadataInterface + { + $extractor = $this->sourceTargetPropertiesMappingExtractor; + + if ('array' === $source || 'stdClass' === $source) { + $extractor = $this->fromTargetPropertiesMappingExtractor; + } + + if ('array' === $target || 'stdClass' === $target) { + $extractor = $this->fromSourcePropertiesMappingExtractor; + } + + $mapperMetadata = new MapperMetadata($autoMapperRegister, $extractor, $source, $target, $this->classPrefix); + $mapperMetadata->setAttributeChecking($this->attributeChecking); + + return $mapperMetadata; + } +} diff --git a/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataFactoryInterface.php b/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataFactoryInterface.php new file mode 100644 index 0000000000000..b80084ec81873 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataFactoryInterface.php @@ -0,0 +1,24 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper; + +/** + * Metadata factory, used to autoregistering new mapping without creating them. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +interface MapperGeneratorMetadataFactoryInterface +{ + public function create(MapperGeneratorMetadataRegistryInterface $autoMapperRegister, string $source, string $target): MapperGeneratorMetadataInterface; +} diff --git a/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataInterface.php b/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataInterface.php new file mode 100644 index 0000000000000..12887ffafe5b1 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataInterface.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper; + +/** + * Stores metadata needed when generating a mapper. + * + * @internal + * + * @author Joel Wurtz + */ +interface MapperGeneratorMetadataInterface extends MapperMetadataInterface +{ + /** + * Get mapper class name. + */ + public function getMapperClassName(): string; + + /** + * Get hash (unique key) for those metadatas. + */ + public function getHash(): string; + + /** + * Get a list of callbacks to add for this mapper. + * + * @return callable[] + */ + public function getCallbacks(): array; + + /** + * Whether the target class has a constructor. + */ + public function hasConstructor(): bool; + + /** + * Whether we can use target constructor. + */ + public function isConstructorAllowed(): bool; + + /** + * Whether we should generate attributes checking. + */ + public function shouldCheckAttributes(): bool; + + /** + * If not using target constructor, allow to know if we can clone a empty target. + */ + public function isTargetCloneable(): bool; + + /** + * Whether the mapping can have circular reference. + * + * If not the case, allow to not generate code about circular references + */ + public function canHaveCircularReference(): bool; +} diff --git a/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataRegistryInterface.php b/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataRegistryInterface.php new file mode 100644 index 0000000000000..957fdb475f340 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/MapperGeneratorMetadataRegistryInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper; + +/** + * Registry of metadata. + * + * @internal + * + * @author Joel Wurtz + */ +interface MapperGeneratorMetadataRegistryInterface +{ + /** + * Register metadata. + */ + public function register(MapperGeneratorMetadataInterface $configuration): void; + + /** + * Get metadata for a source and a target. + */ + public function getMetadata(string $source, string $target): ?MapperGeneratorMetadataInterface; +} diff --git a/src/Symfony/Component/AutoMapper/MapperInterface.php b/src/Symfony/Component/AutoMapper/MapperInterface.php new file mode 100644 index 0000000000000..aa5567259fb8b --- /dev/null +++ b/src/Symfony/Component/AutoMapper/MapperInterface.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper; + +/** + * Interface implemented by a single mapper. + * + * Each specific mapper should implements this interface + * + * @internal + * + * @author Joel Wurtz + */ +interface MapperInterface +{ + /** + * @param mixed $value Value to map + * @param array $context Options mapper have access to + * + * @return mixed The mapped value + */ + public function &map($value, array $context = []); +} diff --git a/src/Symfony/Component/AutoMapper/MapperMetadata.php b/src/Symfony/Component/AutoMapper/MapperMetadata.php new file mode 100644 index 0000000000000..b294cf894cd26 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/MapperMetadata.php @@ -0,0 +1,320 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper; + +use Symfony\Component\AutoMapper\Extractor\MappingExtractorInterface; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Extractor\ReadAccessor; +use Symfony\Component\AutoMapper\Transformer\CallbackTransformer; +use Symfony\Component\AutoMapper\Transformer\MapperDependency; + +/** + * Mapper metadata. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +class MapperMetadata implements MapperGeneratorMetadataInterface +{ + private $mappingExtractor; + + private $customMapping = []; + + private $propertiesMapping; + + private $metadataRegistry; + + private $source; + + private $target; + + private $className; + + private $isConstructorAllowed; + + private $dateTimeFormat; + + private $classPrefix; + + private $attributeChecking; + + private $targetReflectionClass = null; + + public function __construct(MapperGeneratorMetadataRegistryInterface $metadataRegistry, MappingExtractorInterface $mappingExtractor, string $source, string $target, string $classPrefix = 'Mapper_') + { + $this->mappingExtractor = $mappingExtractor; + $this->metadataRegistry = $metadataRegistry; + $this->source = $source; + $this->target = $target; + $this->isConstructorAllowed = true; + $this->dateTimeFormat = \DateTime::RFC3339; + $this->classPrefix = $classPrefix; + $this->attributeChecking = true; + } + + private function getCachedTargetReflectionClass(): \ReflectionClass + { + if (null === $this->targetReflectionClass) { + $this->targetReflectionClass = new \ReflectionClass($this->getTarget()); + } + + return $this->targetReflectionClass; + } + + /** + * {@inheritdoc} + */ + public function getPropertiesMapping(): array + { + if (null === $this->propertiesMapping) { + $this->buildPropertyMapping(); + } + + return $this->propertiesMapping; + } + + /** + * {@inheritdoc} + */ + public function getPropertyMapping(string $property): ?PropertyMapping + { + return $this->getPropertiesMapping()[$property] ?? null; + } + + /** + * {@inheritdoc} + */ + public function hasConstructor(): bool + { + if (!$this->isConstructorAllowed()) { + return false; + } + + if (\in_array($this->target, ['array', \stdClass::class], true)) { + return false; + } + + $reflection = $this->getCachedTargetReflectionClass(); + $constructor = $reflection->getConstructor(); + + if (null === $constructor) { + return false; + } + + $parameters = $constructor->getParameters(); + $mandatoryParameters = []; + + foreach ($parameters as $parameter) { + if (!$parameter->isOptional() && !$parameter->allowsNull()) { + $mandatoryParameters[] = $parameter; + } + } + + if (!$mandatoryParameters) { + return true; + } + + foreach ($mandatoryParameters as $mandatoryParameter) { + $readAccessor = $this->mappingExtractor->getReadAccessor($this->source, $this->target, $mandatoryParameter->getName()); + + if (null === $readAccessor) { + return false; + } + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function isTargetCloneable(): bool + { + try { + $reflection = $this->getCachedTargetReflectionClass(); + + return $reflection->isCloneable() && !$reflection->hasMethod('__clone'); + } catch (\ReflectionException $e) { + // if we have a \ReflectionException, then we can't clone target + return false; + } + } + + /** + * {@inheritdoc} + */ + public function canHaveCircularReference(): bool + { + $checked = []; + + return $this->checkCircularMapperConfiguration($this, $checked); + } + + /** + * {@inheritdoc} + */ + public function getMapperClassName(): string + { + if (null !== $this->className) { + return $this->className; + } + + return $this->className = sprintf('%s%s_%s', $this->classPrefix, str_replace('\\', '_', $this->source), str_replace('\\', '_', $this->target)); + } + + /** + * {@inheritdoc} + */ + public function getHash(): string + { + $hash = ''; + + if (!\in_array($this->source, ['array', \stdClass::class], true)) { + $reflection = new \ReflectionClass($this->source); + $hash .= filemtime($reflection->getFileName()); + } + + if (!\in_array($this->target, ['array', \stdClass::class], true)) { + $reflection = $this->getCachedTargetReflectionClass(); + $hash .= filemtime($reflection->getFileName()); + } + + return $hash; + } + + /** + * {@inheritdoc} + */ + public function isConstructorAllowed(): bool + { + return $this->isConstructorAllowed; + } + + /** + * {@inheritdoc} + */ + public function getSource(): string + { + return $this->source; + } + + /** + * {@inheritdoc} + */ + public function getTarget(): string + { + return $this->target; + } + + /** + * {@inheritdoc} + */ + public function getDateTimeFormat(): string + { + return $this->dateTimeFormat; + } + + /** + * {@inheritdoc} + */ + public function getCallbacks(): array + { + return $this->customMapping; + } + + /** + * {@inheritdoc} + */ + public function shouldCheckAttributes(): bool + { + return $this->attributeChecking; + } + + /** + * Set DateTime format to use when generating a mapper. + */ + public function setDateTimeFormat(string $dateTimeFormat): void + { + $this->dateTimeFormat = $dateTimeFormat; + } + + /** + * Whether or not the constructor should be used. + */ + public function setConstructorAllowed(bool $isConstructorAllowed): void + { + $this->isConstructorAllowed = $isConstructorAllowed; + } + + /** + * Set a callable to use when mapping a specific property. + */ + public function forMember(string $property, callable $callback): void + { + $this->customMapping[$property] = $callback; + } + + /** + * Whether or not attribute checking code should be generated. + */ + public function setAttributeChecking(bool $attributeChecking): void + { + $this->attributeChecking = $attributeChecking; + } + + private function buildPropertyMapping() + { + $this->propertiesMapping = []; + + foreach ($this->mappingExtractor->getPropertiesMapping($this) as $propertyMapping) { + $this->propertiesMapping[$propertyMapping->getProperty()] = $propertyMapping; + } + + foreach ($this->customMapping as $property => $callback) { + $this->propertiesMapping[$property] = new PropertyMapping( + new ReadAccessor(ReadAccessor::TYPE_SOURCE, $property), + $this->mappingExtractor->getWriteMutator($this->source, $this->target, $property), + null, + new CallbackTransformer($property), + $property, + false + ); + } + } + + private function checkCircularMapperConfiguration(MapperGeneratorMetadataInterface $configuration, &$checked) + { + foreach ($configuration->getPropertiesMapping() as $propertyMapping) { + /** @var MapperDependency $dependency */ + foreach ($propertyMapping->getTransformer()->getDependencies() as $dependency) { + if (isset($checked[$dependency->getName()])) { + continue; + } + + $checked[$dependency->getName()] = true; + + if ($dependency->getSource() === $this->getSource() && $dependency->getTarget() === $this->getTarget()) { + return true; + } + + $subConfiguration = $this->metadataRegistry->getMetadata($dependency->getSource(), $dependency->getTarget()); + + if (null !== $subConfiguration && true === $this->checkCircularMapperConfiguration($subConfiguration, $checked)) { + return true; + } + } + } + + return false; + } +} diff --git a/src/Symfony/Component/AutoMapper/MapperMetadataInterface.php b/src/Symfony/Component/AutoMapper/MapperMetadataInterface.php new file mode 100644 index 0000000000000..19702c71e9e61 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/MapperMetadataInterface.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper; + +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; + +/** + * Stores metadata needed for mapping data. + * + * @internal + * + * @author Joel Wurtz + */ +interface MapperMetadataInterface +{ + /** + * Get the source type mapped. + */ + public function getSource(): string; + + /** + * Get the target type mapped. + */ + public function getTarget(): string; + + /** + * Get properties to map between source and target. + * + * @return PropertyMapping[] + */ + public function getPropertiesMapping(): array; + + /** + * Get property to map by name, or null if not mapped. + */ + public function getPropertyMapping(string $property): ?PropertyMapping; + + /** + * Get date time format to use when mapping date time to string. + */ + public function getDateTimeFormat(): string; +} diff --git a/src/Symfony/Component/AutoMapper/README.md b/src/Symfony/Component/AutoMapper/README.md new file mode 100644 index 0000000000000..6ed424aedc4d3 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/README.md @@ -0,0 +1,18 @@ +AutoMapper Component +==================== + +The AutoMapper component maps data between different domains. + +**This Component is experimental**. +[Experimental features](https://symfony.com/doc/current/contributing/code/experimental.html) +are not covered by Symfony's +[Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html). + +Resources +--------- + + * [Documentation](https://symfony.com/doc/current/components/auto_mapper/introduction.html) + * [Contributing](https://symfony.com/doc/current/contributing/index.html) + * [Report issues](https://github.com/symfony/symfony/issues) and + [send Pull Requests](https://github.com/symfony/symfony/pulls) + in the [main Symfony repository](https://github.com/symfony/symfony) diff --git a/src/Symfony/Component/AutoMapper/Tests/AutoMapperBaseTest.php b/src/Symfony/Component/AutoMapper/Tests/AutoMapperBaseTest.php new file mode 100644 index 0000000000000..a000d59add7c1 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/AutoMapperBaseTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests; + +use Doctrine\Common\Annotations\AnnotationReader; +use PhpParser\ParserFactory; +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\AutoMapper; +use Symfony\Component\AutoMapper\Generator\Generator; +use Symfony\Component\AutoMapper\Loader\ClassLoaderInterface; +use Symfony\Component\AutoMapper\Loader\FileLoader; +use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; + +/** + * @author Baptiste Leduc + */ +abstract class AutoMapperBaseTest extends TestCase +{ + /** @var AutoMapper */ + protected $autoMapper; + + /** @var ClassLoaderInterface */ + protected $loader; + + public function setUp(): void + { + @unlink(__DIR__.'/cache/registry.php'); + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + + $this->loader = new FileLoader(new Generator( + (new ParserFactory())->create(ParserFactory::PREFER_PHP7), + new ClassDiscriminatorFromClassMetadata($classMetadataFactory) + ), __DIR__.'/cache'); + + $this->autoMapper = AutoMapper::create(true, $this->loader); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/AutoMapperNormalizerTest.php b/src/Symfony/Component/AutoMapper/Tests/AutoMapperNormalizerTest.php new file mode 100644 index 0000000000000..4156a767e738e --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/AutoMapperNormalizerTest.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests; + +use Symfony\Component\AutoMapper\AutoMapperNormalizer; + +/** + * @author Baptiste Leduc + */ +class AutoMapperNormalizerTest extends AutoMapperBaseTest +{ + /** @var AutoMapperNormalizer */ + protected $normalizer; + + public function setUp(): void + { + parent::setUp(); + $this->normalizer = new AutoMapperNormalizer($this->autoMapper); + } + + public function testNormalize(): void + { + $object = new Fixtures\User(1, 'Jack', 37); + $expected = ['id' => 1, 'name' => 'Jack', 'age' => 37]; + + $normalized = $this->normalizer->normalize($object); + self::assertIsArray($normalized); + self::assertEquals($expected['id'], $normalized['id']); + self::assertEquals($expected['name'], $normalized['name']); + self::assertEquals($expected['age'], $normalized['age']); + } + + public function testDenormalize(): void + { + $source = ['id' => 1, 'name' => 'Jack', 'age' => 37]; + + /** @var Fixtures\User $denormalized */ + $denormalized = $this->normalizer->denormalize($source, Fixtures\User::class); + self::assertInstanceOf(Fixtures\User::class, $denormalized); + self::assertEquals($source['id'], $denormalized->getId()); + self::assertEquals($source['name'], $denormalized->name); + self::assertEquals($source['age'], $denormalized->age); + } + + public function testSupportsNormalization(): void + { + self::assertFalse($this->normalizer->supportsNormalization(['foo'])); + self::assertFalse($this->normalizer->supportsNormalization('{"foo":1}')); + + $object = new Fixtures\User(1, 'Jack', 37); + self::assertTrue($this->normalizer->supportsNormalization($object)); + + $stdClass = new \stdClass(); + $stdClass->id = 1; + $stdClass->name = 'Jack'; + $stdClass->age = 37; + self::assertFalse($this->normalizer->supportsNormalization($stdClass)); + } + + public function testSupportsDenormalization(): void + { + self::assertTrue($this->normalizer->supportsDenormalization(['foo' => 1], 'array')); + self::assertTrue($this->normalizer->supportsDenormalization(['foo' => 1], 'json')); + + $user = ['id' => 1, 'name' => 'Jack', 'age' => 37]; + self::assertTrue($this->normalizer->supportsDenormalization($user, Fixtures\User::class)); + self::assertTrue($this->normalizer->supportsDenormalization($user, \stdClass::class)); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/AutoMapperTest.php b/src/Symfony/Component/AutoMapper/Tests/AutoMapperTest.php new file mode 100644 index 0000000000000..eaacab9571abd --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/AutoMapperTest.php @@ -0,0 +1,639 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests; + +use Symfony\Component\AutoMapper\AutoMapper; +use Symfony\Component\AutoMapper\Exception\CircularReferenceException; +use Symfony\Component\AutoMapper\Exception\NoMappingFoundException; +use Symfony\Component\AutoMapper\MapperContext; +use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface; + +/** + * @author Joel Wurtz + */ +class AutoMapperTest extends AutoMapperBaseTest +{ + public function testAutoMapping(): void + { + $userMetadata = $this->autoMapper->getMetadata(Fixtures\User::class, Fixtures\UserDTO::class); + $userMetadata->forMember('yearOfBirth', function (Fixtures\User $user) { + return ((int) date('Y')) - ((int) $user->age); + }); + + $address = new Fixtures\Address(); + $address->setCity('Toulon'); + $user = new Fixtures\User(1, 'yolo', '13'); + $user->address = $address; + $user->addresses[] = $address; + $user->money = 20.10; + + /** @var Fixtures\UserDTO $userDto */ + $userDto = $this->autoMapper->map($user, Fixtures\UserDTO::class); + + self::assertInstanceOf(Fixtures\UserDTO::class, $userDto); + self::assertSame(1, $userDto->id); + self::assertSame('yolo', $userDto->getName()); + self::assertSame(13, $userDto->age); + self::assertSame(((int) date('Y')) - 13, $userDto->yearOfBirth); + self::assertCount(1, $userDto->addresses); + self::assertInstanceOf(Fixtures\AddressDTO::class, $userDto->address); + self::assertInstanceOf(Fixtures\AddressDTO::class, $userDto->addresses[0]); + self::assertSame('Toulon', $userDto->address->city); + self::assertSame('Toulon', $userDto->addresses[0]->city); + self::assertIsArray($userDto->money); + self::assertCount(1, $userDto->money); + self::assertSame(20.10, $userDto->money[0]); + } + + public function testAutoMapperFromArray(): void + { + $user = [ + 'id' => 1, + 'address' => [ + 'city' => 'Toulon', + ], + 'createdAt' => '1987-04-30T06:00:00Z', + ]; + + /** @var Fixtures\UserDTO $userDto */ + $userDto = $this->autoMapper->map($user, Fixtures\UserDTO::class); + + self::assertInstanceOf(Fixtures\UserDTO::class, $userDto); + self::assertEquals(1, $userDto->id); + self::assertInstanceOf(Fixtures\AddressDTO::class, $userDto->address); + self::assertSame('Toulon', $userDto->address->city); + self::assertInstanceOf(\DateTimeInterface::class, $userDto->createdAt); + self::assertEquals(1987, $userDto->createdAt->format('Y')); + } + + public function testAutoMapperFromArrayCustomDateTime(): void + { + $dateTime = \DateTime::createFromFormat(\DateTime::RFC3339, '1987-04-30T06:00:00Z'); + $customFormat = 'U'; + $user = [ + 'id' => 1, + 'address' => [ + 'city' => 'Toulon', + ], + 'createdAt' => $dateTime->format($customFormat), + ]; + + $autoMapper = AutoMapper::create(true, $this->loader, null, 'CustomDateTime_'); + $configuration = $autoMapper->getMetadata('array', Fixtures\UserDTO::class); + $configuration->setDateTimeFormat($customFormat); + + /** @var Fixtures\UserDTO $userDto */ + $userDto = $autoMapper->map($user, Fixtures\UserDTO::class); + + self::assertInstanceOf(Fixtures\UserDTO::class, $userDto); + self::assertEquals($dateTime->format($customFormat), $userDto->createdAt->format($customFormat)); + } + + public function testAutoMapperToArray(): void + { + $address = new Fixtures\Address(); + $address->setCity('Toulon'); + $user = new Fixtures\User(1, 'yolo', '13'); + $user->address = $address; + $user->addresses[] = $address; + + $userData = $this->autoMapper->map($user, 'array'); + + self::assertIsArray($userData); + self::assertEquals(1, $userData['id']); + self::assertIsArray($userData['address']); + self::assertIsString($userData['createdAt']); + } + + public function testAutoMapperFromStdObject(): void + { + $user = new \stdClass(); + $user->id = 1; + + /** @var Fixtures\UserDTO $userDto */ + $userDto = $this->autoMapper->map($user, Fixtures\UserDTO::class); + + self::assertInstanceOf(Fixtures\UserDTO::class, $userDto); + self::assertEquals(1, $userDto->id); + } + + public function testAutoMapperToStdObject(): void + { + $userDto = new Fixtures\UserDTO(); + $userDto->id = 1; + + $user = $this->autoMapper->map($userDto, \stdClass::class); + + self::assertInstanceOf(\stdClass::class, $user); + self::assertEquals(1, $user->id); + } + + public function testNotReadable(): void + { + $autoMapper = AutoMapper::create(false, $this->loader, null, 'NotReadable_'); + $address = new Fixtures\Address(); + $address->setCity('test'); + + $addressArray = $autoMapper->map($address, 'array'); + + self::assertIsArray($addressArray); + self::assertArrayNotHasKey('city', $addressArray); + + $addressMapped = $autoMapper->map($address, Fixtures\Address::class); + + self::assertInstanceOf(Fixtures\Address::class, $addressMapped); + + $property = (new \ReflectionClass($addressMapped))->getProperty('city'); + $property->setAccessible(true); + + $city = $property->getValue($addressMapped); + + self::assertNull($city); + } + + public function testNoTypes(): void + { + $autoMapper = AutoMapper::create(false, $this->loader, null, 'NotReadable_'); + $address = new Fixtures\AddressNoTypes(); + $address->city = 'test'; + + $addressArray = $autoMapper->map($address, 'array'); + + self::assertIsArray($addressArray); + self::assertArrayNotHasKey('city', $addressArray); + } + + public function testNoTransformer(): void + { + $addressFoo = new Fixtures\AddressFoo(); + $addressFoo->city = new Fixtures\CityFoo(); + $addressFoo->city->name = 'test'; + + $addressBar = $this->autoMapper->map($addressFoo, Fixtures\AddressBar::class); + + self::assertInstanceOf(Fixtures\AddressBar::class, $addressBar); + self::assertNull($addressBar->city); + } + + public function testNoProperties(): void + { + $noProperties = new Fixtures\FooNoProperties(); + $noPropertiesMapped = $this->autoMapper->map($noProperties, Fixtures\FooNoProperties::class); + + self::assertInstanceOf(Fixtures\FooNoProperties::class, $noPropertiesMapped); + self::assertNotSame($noProperties, $noPropertiesMapped); + } + + public function testGroupsSourceTarget(): void + { + $foo = new Fixtures\Foo(); + $foo->setId(10); + + $bar = $this->autoMapper->map($foo, Fixtures\Bar::class, [MapperContext::GROUPS => ['group2']]); + + self::assertInstanceOf(Fixtures\Bar::class, $bar); + self::assertEquals(10, $bar->getId()); + + $bar = $this->autoMapper->map($foo, Fixtures\Bar::class, [MapperContext::GROUPS => ['group1', 'group3']]); + + self::assertInstanceOf(Fixtures\Bar::class, $bar); + self::assertEquals(10, $bar->getId()); + + $bar = $this->autoMapper->map($foo, Fixtures\Bar::class, [MapperContext::GROUPS => ['group1']]); + + self::assertInstanceOf(Fixtures\Bar::class, $bar); + self::assertNull($bar->getId()); + + $bar = $this->autoMapper->map($foo, Fixtures\Bar::class, [MapperContext::GROUPS => []]); + + self::assertInstanceOf(Fixtures\Bar::class, $bar); + self::assertNull($bar->getId()); + + $bar = $this->autoMapper->map($foo, Fixtures\Bar::class); + + self::assertInstanceOf(Fixtures\Bar::class, $bar); + self::assertNull($bar->getId()); + } + + public function testGroupsToArray(): void + { + $foo = new Fixtures\Foo(); + $foo->setId(10); + + $fooArray = $this->autoMapper->map($foo, 'array', [MapperContext::GROUPS => ['group1']]); + + self::assertIsArray($fooArray); + self::assertEquals(10, $fooArray['id']); + + $fooArray = $this->autoMapper->map($foo, 'array', [MapperContext::GROUPS => []]); + + self::assertIsArray($fooArray); + self::assertArrayNotHasKey('id', $fooArray); + + $fooArray = $this->autoMapper->map($foo, 'array'); + + self::assertIsArray($fooArray); + self::assertArrayNotHasKey('id', $fooArray); + } + + public function testDeepCloning(): void + { + $nodeA = new Fixtures\Node(); + $nodeB = new Fixtures\Node(); + $nodeB->parent = $nodeA; + $nodeC = new Fixtures\Node(); + $nodeC->parent = $nodeB; + $nodeA->parent = $nodeC; + + $newNode = $this->autoMapper->map($nodeA, Fixtures\Node::class); + + self::assertInstanceOf(Fixtures\Node::class, $newNode); + self::assertNotSame($newNode, $nodeA); + self::assertInstanceOf(Fixtures\Node::class, $newNode->parent); + self::assertNotSame($newNode->parent, $nodeA->parent); + self::assertInstanceOf(Fixtures\Node::class, $newNode->parent->parent); + self::assertNotSame($newNode->parent->parent, $nodeA->parent->parent); + self::assertInstanceOf(Fixtures\Node::class, $newNode->parent->parent->parent); + self::assertSame($newNode, $newNode->parent->parent->parent); + } + + public function testDeepCloningArray(): void + { + $nodeA = new Fixtures\Node(); + $nodeB = new Fixtures\Node(); + $nodeB->parent = $nodeA; + $nodeC = new Fixtures\Node(); + $nodeC->parent = $nodeB; + $nodeA->parent = $nodeC; + + $newNode = $this->autoMapper->map($nodeA, 'array'); + + self::assertIsArray($newNode); + self::assertIsArray($newNode['parent']); + self::assertIsArray($newNode['parent']['parent']); + self::assertIsArray($newNode['parent']['parent']['parent']); + self::assertSame($newNode, $newNode['parent']['parent']['parent']); + } + + public function testCircularReferenceDeep(): void + { + $foo = new Fixtures\CircularFoo(); + $bar = new Fixtures\CircularBar(); + $baz = new Fixtures\CircularBaz(); + + $foo->bar = $bar; + $bar->baz = $baz; + $baz->foo = $foo; + + + $newFoo = $this->autoMapper->map($foo, Fixtures\CircularFoo::class); + + self::assertNotSame($foo, $newFoo); + self::assertNotNull($newFoo->bar); + self::assertNotSame($bar, $newFoo->bar); + self::assertNotNull($newFoo->bar->baz); + self::assertNotSame($baz, $newFoo->bar->baz); + self::assertNotNull($newFoo->bar->baz->foo); + self::assertSame($newFoo, $newFoo->bar->baz->foo); + } + + public function testCircularReferenceArray(): void + { + $nodeA = new Fixtures\Node(); + $nodeB = new Fixtures\Node(); + + $nodeA->childs[] = $nodeB; + $nodeB->childs[] = $nodeA; + + $newNode = $this->autoMapper->map($nodeA, 'array'); + + self::assertIsArray($newNode); + self::assertIsArray($newNode['childs'][0]); + self::assertIsArray($newNode['childs'][0]['childs'][0]); + self::assertSame($newNode, $newNode['childs'][0]['childs'][0]); + } + + public function testPrivate(): void + { + $user = new Fixtures\PrivateUser(10, 'foo', 'bar'); + /** @var Fixtures\PrivateUserDTO $userDto */ + $userDto = $this->autoMapper->map($user, Fixtures\PrivateUserDTO::class); + + self::assertInstanceOf(Fixtures\PrivateUserDTO::class, $userDto); + self::assertSame(10, $userDto->getId()); + self::assertSame('foo', $userDto->getFirstName()); + self::assertSame('bar', $userDto->getLastName()); + } + + public function testConstructor(): void + { + $autoMapper = AutoMapper::create(false, $this->loader); + + $user = new Fixtures\UserDTO(); + $user->id = 10; + $user->setName('foo'); + $user->age = 3; + /** @var Fixtures\UserConstructorDTO $userDto */ + $userDto = $autoMapper->map($user, Fixtures\UserConstructorDTO::class); + + self::assertInstanceOf(Fixtures\UserConstructorDTO::class, $userDto); + self::assertSame('10', $userDto->getId()); + self::assertSame('foo', $userDto->getName()); + self::assertSame(3, $userDto->getAge()); + self::assertTrue($userDto->getConstructor()); + } + + public function testConstructorNotAllowed(): void + { + $autoMapper = AutoMapper::create(true, $this->loader, null, 'NotAllowedMapper_'); + $configuration = $autoMapper->getMetadata(Fixtures\UserDTO::class, Fixtures\UserConstructorDTO::class); + $configuration->setConstructorAllowed(false); + + $user = new Fixtures\UserDTO(); + $user->id = 10; + $user->setName('foo'); + $user->age = 3; + + /** @var Fixtures\UserConstructorDTO $userDto */ + $userDto = $autoMapper->map($user, Fixtures\UserConstructorDTO::class); + + self::assertInstanceOf(Fixtures\UserConstructorDTO::class, $userDto); + self::assertSame('10', $userDto->getId()); + self::assertSame('foo', $userDto->getName()); + self::assertSame(3, $userDto->getAge()); + self::assertFalse($userDto->getConstructor()); + } + + public function testConstructorWithDefault(): void + { + $user = new Fixtures\UserDTONoAge(); + $user->id = 10; + $user->name = 'foo'; + /** @var Fixtures\UserConstructorDTO $userDto */ + $userDto = $this->autoMapper->map($user, Fixtures\UserConstructorDTO::class); + + self::assertInstanceOf(Fixtures\UserConstructorDTO::class, $userDto); + self::assertSame('10', $userDto->getId()); + self::assertSame('foo', $userDto->getName()); + self::assertSame(30, $userDto->getAge()); + } + + public function testConstructorDisable(): void + { + $user = new Fixtures\UserDTONoName(); + $user->id = 10; + /** @var Fixtures\UserConstructorDTO $userDto */ + $userDto = $this->autoMapper->map($user, Fixtures\UserConstructorDTO::class); + + self::assertInstanceOf(Fixtures\UserConstructorDTO::class, $userDto); + self::assertSame('10', $userDto->getId()); + self::assertNull($userDto->getName()); + self::assertNull($userDto->getAge()); + } + + public function testMaxDepth(): void + { + $foo = new Fixtures\FooMaxDepth(0, new Fixtures\FooMaxDepth(1, new Fixtures\FooMaxDepth(2, new Fixtures\FooMaxDepth(3, new Fixtures\FooMaxDepth(4))))); + $fooArray = $this->autoMapper->map($foo, 'array'); + + self::assertNotNull($fooArray['child']); + self::assertNotNull($fooArray['child']['child']); + self::assertFalse(isset($fooArray['child']['child']['child'])); + } + + public function testObjectToPopulate(): void + { + $configurationUser = $this->autoMapper->getMetadata(Fixtures\User::class, Fixtures\UserDTO::class); + $configurationUser->forMember('yearOfBirth', function (Fixtures\User $user) { + return ((int) date('Y')) - ((int) $user->age); + }); + + $user = new Fixtures\User(1, 'yolo', '13'); + $userDtoToPopulate = new Fixtures\UserDTO(); + + $userDto = $this->autoMapper->map($user, Fixtures\UserDTO::class, [MapperContext::TARGET_TO_POPULATE => $userDtoToPopulate]); + + self::assertSame($userDtoToPopulate, $userDto); + } + + public function testObjectToPopulateWithoutContext(): void + { + $configurationUser = $this->autoMapper->getMetadata(Fixtures\User::class, Fixtures\UserDTO::class); + $configurationUser->forMember('yearOfBirth', function (Fixtures\User $user) { + return ((int) date('Y')) - ((int) $user->age); + }); + + $user = new Fixtures\User(1, 'yolo', '13'); + $userDtoToPopulate = new Fixtures\UserDTO(); + + $userDto = $this->autoMapper->map($user, $userDtoToPopulate); + + self::assertSame($userDtoToPopulate, $userDto); + } + + public function testArrayToPopulate(): void + { + $configurationUser = $this->autoMapper->getMetadata(Fixtures\User::class, Fixtures\UserDTO::class); + $configurationUser->forMember('yearOfBirth', function (Fixtures\User $user) { + return ((int) date('Y')) - ((int) $user->age); + }); + + $user = new Fixtures\User(1, 'yolo', '13'); + $array = []; + $arrayMapped = $this->autoMapper->map($user, $array); + + self::assertIsArray($arrayMapped); + self::assertSame(1, $arrayMapped['id']); + self::assertSame('yolo', $arrayMapped['name']); + self::assertSame('13', $arrayMapped['age']); + } + + public function testCircularReferenceLimitOnContext(): void + { + $nodeA = new Fixtures\Node(); + $nodeA->parent = $nodeA; + + $context = new MapperContext(); + $context->setCircularReferenceLimit(1); + + $this->expectException(CircularReferenceException::class); + + $this->autoMapper->map($nodeA, 'array', $context->toArray()); + } + + public function testCircularReferenceLimitOnMapper(): void + { + $nodeA = new Fixtures\Node(); + $nodeA->parent = $nodeA; + + $mapper = $this->autoMapper->getMapper(Fixtures\Node::class, 'array'); + $mapper->setCircularReferenceLimit(1); + + $this->expectException(CircularReferenceException::class); + + $mapper->map($nodeA); + } + + public function testCircularReferenceHandlerOnContext(): void + { + $nodeA = new Fixtures\Node(); + $nodeA->parent = $nodeA; + + $context = new MapperContext(); + $context->setCircularReferenceHandler(function () { + return 'foo'; + }); + + $nodeArray = $this->autoMapper->map($nodeA, 'array', $context->toArray()); + + self::assertSame('foo', $nodeArray['parent']); + } + + public function testCircularReferenceHandlerOnMapper(): void + { + $nodeA = new Fixtures\Node(); + $nodeA->parent = $nodeA; + + $mapper = $this->autoMapper->getMapper(Fixtures\Node::class, 'array'); + $mapper->setCircularReferenceHandler(function () { + return 'foo'; + }); + + $nodeArray = $mapper->map($nodeA); + + self::assertSame('foo', $nodeArray['parent']); + } + + public function testAllowedAttributes(): void + { + $configurationUser = $this->autoMapper->getMetadata(Fixtures\User::class, Fixtures\UserDTO::class); + $configurationUser->forMember('yearOfBirth', function (Fixtures\User $user) { + return ((int) date('Y')) - ((int) $user->age); + }); + + $user = new Fixtures\User(1, 'yolo', '13'); + + $userDto = $this->autoMapper->map($user, Fixtures\UserDTO::class, [MapperContext::ALLOWED_ATTRIBUTES => ['id', 'age']]); + + self::assertNull($userDto->getName()); + } + + public function testIgnoredAttributes(): void + { + $configurationUser = $this->autoMapper->getMetadata(Fixtures\User::class, Fixtures\UserDTO::class); + $configurationUser->forMember('yearOfBirth', function (Fixtures\User $user) { + return ((int) date('Y')) - ((int) $user->age); + }); + + $user = new Fixtures\User(1, 'yolo', '13'); + $userDto = $this->autoMapper->map($user, Fixtures\UserDTO::class, [MapperContext::IGNORED_ATTRIBUTES => ['name']]); + + self::assertNull($userDto->getName()); + } + + public function testNameConverter(): void + { + $nameConverter = new class() implements AdvancedNameConverterInterface { + public function normalize($propertyName, string $class = null, string $format = null, array $context = []) + { + if ('id' === $propertyName) { + return '@id'; + } + + return $propertyName; + } + + public function denormalize($propertyName, string $class = null, string $format = null, array $context = []) + { + if ('@id' === $propertyName) { + return 'id'; + } + + return $propertyName; + } + }; + + $autoMapper = AutoMapper::create(true, null, $nameConverter, 'Mapper2_'); + $user = new Fixtures\User(1, 'yolo', '13'); + + $userArray = $autoMapper->map($user, 'array'); + + self::assertIsArray($userArray); + self::assertArrayHasKey('@id', $userArray); + self::assertSame(1, $userArray['@id']); + } + + public function testDefaultArguments(): void + { + $user = new Fixtures\UserDTONoAge(); + $user->id = 10; + $user->name = 'foo'; + + $context = new MapperContext(); + $context->setConstructorArgument(Fixtures\UserConstructorDTO::class, 'age', 50); + + /** @var Fixtures\UserConstructorDTO $userDto */ + $userDto = $this->autoMapper->map($user, Fixtures\UserConstructorDTO::class, $context->toArray()); + + self::assertInstanceOf(Fixtures\UserConstructorDTO::class, $userDto); + self::assertSame(50, $userDto->getAge()); + } + + public function testDiscriminator(): void + { + $data = [ + 'type' => 'cat', + ]; + + $pet = $this->autoMapper->map($data, Fixtures\Pet::class); + + self::assertInstanceOf(Fixtures\Cat::class, $pet); + } + + public function testAutomapNull(): void + { + $array = $this->autoMapper->map(null, 'array'); + + self::assertNull($array); + } + + public function testInvalidMappingBothArray(): void + { + self::expectException(NoMappingFoundException::class); + + $data = ['test' => 'foo']; + $array = $this->autoMapper->map($data, 'array'); + } + + public function testInvalidMappingSource(): void + { + self::expectException(NoMappingFoundException::class); + + $array = $this->autoMapper->map('test', 'array'); + } + + public function testInvalidMappingTarget(): void + { + self::expectException(NoMappingFoundException::class); + + $data = ['test' => 'foo']; + $array = $this->autoMapper->map($data, 3); + } + + public function testNoAutoRegister(): void + { + self::expectException(NoMappingFoundException::class); + + $automapper = AutoMapper::create(false, null, null, 'Mapper_', true, false); + $automapper->getMapper(Fixtures\User::class, Fixtures\UserDTO::class); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Extractor/FromSourceMappingExtractorTest.php b/src/Symfony/Component/AutoMapper/Tests/Extractor/FromSourceMappingExtractorTest.php new file mode 100644 index 0000000000000..6428cb9804dd2 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Extractor/FromSourceMappingExtractorTest.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Extractor; + +use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\AutoMapper\Exception\InvalidMappingException; +use Symfony\Component\AutoMapper\Extractor\FromSourceMappingExtractor; +use Symfony\Component\AutoMapper\Extractor\PrivateReflectionExtractor; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\MapperMetadata; +use Symfony\Component\AutoMapper\Tests\AutoMapperBaseTest; +use Symfony\Component\AutoMapper\Tests\Fixtures; +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\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; + +/** + * @author Baptiste Leduc + */ +class FromSourceMappingExtractorTest extends AutoMapperBaseTest +{ + /** @var FromSourceMappingExtractor */ + protected $fromSourceMappingExtractor; + + public function setUp(): void + { + parent::setUp(); + $this->fromSourceMappingExtractorBootstrap(); + } + + private function fromSourceMappingExtractorBootstrap(bool $private = true): void + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $flags = ReflectionExtractor::ALLOW_PUBLIC; + + if ($private) { + $flags |= ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE; + } + + $reflectionExtractor = new ReflectionExtractor(null, null, null, true, $flags); + $transformerFactory = new ChainTransformerFactory(); + + $phpDocExtractor = new PhpDocExtractor(); + $propertyInfoExtractor = new PropertyInfoExtractor( + [$reflectionExtractor], + [$phpDocExtractor, $reflectionExtractor], + [$reflectionExtractor], + [$reflectionExtractor] + ); + + $this->fromSourceMappingExtractor = new FromSourceMappingExtractor( + $propertyInfoExtractor, + $reflectionExtractor, + $reflectionExtractor, + $transformerFactory, + $classMetadataFactory + ); + + $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($this->autoMapper)); + } + + public function testWithTargetAsArray(): void + { + $userReflection = new \ReflectionClass(Fixtures\User::class); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, Fixtures\User::class, 'array'); + $sourcePropertiesMapping = $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); + + self::assertCount(\count($userReflection->getProperties()), $sourcePropertiesMapping); + /** @var PropertyMapping $propertyMapping */ + foreach ($sourcePropertiesMapping as $propertyMapping) { + self::assertTrue($userReflection->hasProperty($propertyMapping->getProperty())); + } + } + + public function testWithTargetAsStdClass(): void + { + $userReflection = new \ReflectionClass(Fixtures\User::class); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, Fixtures\User::class, 'stdClass'); + $sourcePropertiesMapping = $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); + + self::assertCount(\count($userReflection->getProperties()), $sourcePropertiesMapping); + /** @var PropertyMapping $propertyMapping */ + foreach ($sourcePropertiesMapping as $propertyMapping) { + self::assertTrue($userReflection->hasProperty($propertyMapping->getProperty())); + } + } + + public function testWithSourceAsEmpty(): void + { + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, Fixtures\Empty_::class, 'array'); + $sourcePropertiesMapping = $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); + + self::assertCount(0, $sourcePropertiesMapping); + } + + public function testWithSourceAsPrivate(): void + { + $privateReflection = new \ReflectionClass(Fixtures\Private_::class); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, Fixtures\Private_::class, 'array'); + $sourcePropertiesMapping = $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); + self::assertCount(\count($privateReflection->getProperties()), $sourcePropertiesMapping); + + $this->fromSourceMappingExtractorBootstrap(false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, Fixtures\Private_::class, 'array'); + $sourcePropertiesMapping = $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); + self::assertCount(0, $sourcePropertiesMapping); + } + + public function testWithSourceAsArray(): void + { + self::expectException(InvalidMappingException::class); + self::expectExceptionMessage('Only array or stdClass are accepted as a target'); + + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, 'array', Fixtures\User::class); + $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); + } + + public function testWithSourceAsStdClass(): void + { + self::expectException(InvalidMappingException::class); + self::expectExceptionMessage('Only array or stdClass are accepted as a target'); + + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, 'stdClass', Fixtures\User::class); + $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Extractor/FromTargetMappingExtractorTest.php b/src/Symfony/Component/AutoMapper/Tests/Extractor/FromTargetMappingExtractorTest.php new file mode 100644 index 0000000000000..3c77d97beea09 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Extractor/FromTargetMappingExtractorTest.php @@ -0,0 +1,151 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Extractor; + +use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\AutoMapper\Exception\InvalidMappingException; +use Symfony\Component\AutoMapper\Extractor\FromTargetMappingExtractor; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\MapperMetadata; +use Symfony\Component\AutoMapper\Tests\AutoMapperBaseTest; +use Symfony\Component\AutoMapper\Tests\Fixtures; +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\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; + +/** + * @author Baptiste Leduc + */ +class FromTargetMappingExtractorTest extends AutoMapperBaseTest +{ + /** @var FromTargetMappingExtractor */ + protected $fromTargetMappingExtractor; + + public function setUp(): void + { + parent::setUp(); + $this->fromTargetMappingExtractorBootstrap(); + } + + private function fromTargetMappingExtractorBootstrap(bool $private = true): void + { + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $flags = ReflectionExtractor::ALLOW_PUBLIC; + + if ($private) { + $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(); + + $this->fromTargetMappingExtractor = new FromTargetMappingExtractor( + $propertyInfoExtractor, + $reflectionExtractor, + $reflectionExtractor, + $transformerFactory, + $classMetadataFactory + ); + + $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($this->autoMapper)); + } + + public function testWithSourceAsArray(): void + { + $userReflection = new \ReflectionClass(Fixtures\User::class); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, 'array', Fixtures\User::class); + $targetPropertiesMapping = $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); + + self::assertCount(\count($userReflection->getProperties()), $targetPropertiesMapping); + /** @var PropertyMapping $propertyMapping */ + foreach ($targetPropertiesMapping as $propertyMapping) { + self::assertTrue($userReflection->hasProperty($propertyMapping->getProperty())); + } + } + + public function testWithSourceAsStdClass(): void + { + $userReflection = new \ReflectionClass(Fixtures\User::class); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, 'stdClass', Fixtures\User::class); + $targetPropertiesMapping = $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); + + self::assertCount(\count($userReflection->getProperties()), $targetPropertiesMapping); + /** @var PropertyMapping $propertyMapping */ + foreach ($targetPropertiesMapping as $propertyMapping) { + self::assertTrue($userReflection->hasProperty($propertyMapping->getProperty())); + } + } + + public function testWithTargetAsEmpty(): void + { + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, 'array', Fixtures\Empty_::class); + $targetPropertiesMapping = $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); + + self::assertCount(0, $targetPropertiesMapping); + } + + public function testWithTargetAsPrivate(): void + { + $privateReflection = new \ReflectionClass(Fixtures\Private_::class); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, 'array', Fixtures\Private_::class); + $targetPropertiesMapping = $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); + self::assertCount(\count($privateReflection->getProperties()), $targetPropertiesMapping); + + $this->fromTargetMappingExtractorBootstrap(false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, 'array', Fixtures\Private_::class); + $targetPropertiesMapping = $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); + self::assertCount(0, $targetPropertiesMapping); + } + + public function testWithTargetAsArray(): void + { + self::expectException(InvalidMappingException::class); + self::expectExceptionMessage('Only array or stdClass are accepted as a source'); + + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, Fixtures\User::class, 'array'); + $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); + } + + public function testWithTargetAsStdClass(): void + { + self::expectException(InvalidMappingException::class); + self::expectExceptionMessage('Only array or stdClass are accepted as a source'); + + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, Fixtures\User::class, 'stdClass'); + $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/Address.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Address.php new file mode 100644 index 0000000000000..39eada6e322a3 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Address.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class Address +{ + /** + * @var string|null + */ + private $city; + + /** + * @param string $city + */ + public function setCity(?string $city): void + { + $this->city = $city; + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressBar.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressBar.php new file mode 100644 index 0000000000000..253b797892033 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressBar.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class AddressBar +{ + /** + * @var string|null + */ + public $city; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressDTO.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressDTO.php new file mode 100644 index 0000000000000..a8f91820ffe21 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressDTO.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class AddressDTO +{ + /** + * @var string|null + */ + public $city; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressFoo.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressFoo.php new file mode 100644 index 0000000000000..777c22851197e --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressFoo.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class AddressFoo +{ + /** + * @var CityFoo + */ + public $city; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressNoTypes.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressNoTypes.php new file mode 100644 index 0000000000000..7c237b472e704 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressNoTypes.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class AddressNoTypes +{ + public $city; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressNotWritable.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressNotWritable.php new file mode 100644 index 0000000000000..565337c36229b --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/AddressNotWritable.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class AddressNotWritable +{ + /** + * @var string|null + */ + private $city; + + public function getCity(): ?string + { + return $this->city; + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/Bar.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Bar.php new file mode 100644 index 0000000000000..e7c542f9920a1 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Bar.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\Groups; + +class Bar +{ + /** + * @var int|null + * + * @Groups({"group2", "group3"}) + */ + private $id; + + public function getId(): ?int + { + return $this->id; + } + + public function setId(?int $id): void + { + $this->id = $id; + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/Cat.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Cat.php new file mode 100644 index 0000000000000..2d2bdd84199c6 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Cat.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class Cat extends Pet +{ +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/CircularBar.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/CircularBar.php new file mode 100644 index 0000000000000..4c03f031ae326 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/CircularBar.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class CircularBar +{ + /** @var CircularBaz */ + public $baz; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/CircularBaz.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/CircularBaz.php new file mode 100644 index 0000000000000..8711dc8b751a0 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/CircularBaz.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class CircularBaz +{ + /** @var CircularFoo */ + public $foo; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/CircularFoo.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/CircularFoo.php new file mode 100644 index 0000000000000..3b68c49b55871 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/CircularFoo.php @@ -0,0 +1,18 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class CircularFoo +{ + /** @var CircularBar */ + public $bar; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/CityFoo.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/CityFoo.php new file mode 100644 index 0000000000000..99a61b2264bed --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/CityFoo.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class CityFoo +{ + public $name; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/Dog.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Dog.php new file mode 100644 index 0000000000000..140362eff8a92 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Dog.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class Dog extends Pet +{ +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/Empty_.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Empty_.php new file mode 100644 index 0000000000000..fd315c67ff037 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Empty_.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class Empty_ +{ +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/Foo.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Foo.php new file mode 100644 index 0000000000000..7f254931b5de7 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Foo.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\Groups; + +class Foo +{ + /** + * @var int + * + * @Groups({"group1", "group2", "group3"}) + */ + private $id = 0; + + public function getId(): int + { + return $this->id; + } + + public function setId(int $id): void + { + $this->id = $id; + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/FooMaxDepth.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/FooMaxDepth.php new file mode 100644 index 0000000000000..413212803bfba --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/FooMaxDepth.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\MaxDepth; + +class FooMaxDepth +{ + /** + * @var int + */ + private $id; + + /** + * @var FooMaxDepth|null + * + * @MaxDepth(2) + */ + private $child; + + public function __construct(int $id, ?self $child = null) + { + $this->id = $id; + $this->child = $child; + } + + public function getId(): int + { + return $this->id; + } + + public function getChild(): ?self + { + return $this->child; + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/FooNoProperties.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/FooNoProperties.php new file mode 100644 index 0000000000000..a0c1292b7c274 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/FooNoProperties.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class FooNoProperties +{ +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/Node.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Node.php new file mode 100644 index 0000000000000..3aa953d994efc --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Node.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class Node +{ + /** + * @var Node + */ + public $parent; + + /** + * @var Node[] + */ + public $childs = []; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/Pet.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Pet.php new file mode 100644 index 0000000000000..d71303f71a42e --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Pet.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +use Symfony\Component\Serializer\Annotation\DiscriminatorMap; + +/** + * @DiscriminatorMap(typeProperty="type", mapping={ + * "cat"="Symfony\Component\AutoMapper\Tests\Fixtures\Cat", + * "dog"="Symfony\Component\AutoMapper\Tests\Fixtures\Dog" + * }) + */ +class Pet +{ + /** @var string */ + public $type; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/PrivateUser.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/PrivateUser.php new file mode 100644 index 0000000000000..8ac22a738b255 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/PrivateUser.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class PrivateUser +{ + /** @var int */ + private $id; + + /** @var string */ + private $firstName; + + /** @var string */ + private $lastName; + + public function __construct(int $id, string $firstName, string $lastName) + { + $this->id = $id; + $this->firstName = $firstName; + $this->lastName = $lastName; + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/PrivateUserDTO.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/PrivateUserDTO.php new file mode 100644 index 0000000000000..841f4d8982fa4 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/PrivateUserDTO.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class PrivateUserDTO +{ + /** @var int */ + private $id; + + /** @var string */ + private $firstName; + + /** @var string */ + private $lastName; + + public function getId(): int + { + return $this->id; + } + + public function getFirstName(): string + { + return $this->firstName; + } + + public function getLastName(): string + { + return $this->lastName; + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/Private_.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Private_.php new file mode 100644 index 0000000000000..2e39e855b0e3f --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/Private_.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class Private_ +{ + /** + * @var int + */ + private $id; + + public function __construct(int $id) + { + $this->id = $id; + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/ReflectionExtractorTestFixture.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/ReflectionExtractorTestFixture.php new file mode 100644 index 0000000000000..145e60dbf7af9 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/ReflectionExtractorTestFixture.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class ReflectionExtractorTestFixture +{ + public function __construct($propertyConstruct) + { + } + + public function getFoo(): string + { + return 'string'; + } + + public function setFoo(string $foo) + { + } + + public function bar(?string $bar): string + { + return 'string'; + } + + public function isBaz(): bool + { + return true; + } + + public function hasFoz(): bool + { + return false; + } + + public function __get($name) + { + } + + public function __set($name, $value) + { + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/User.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/User.php new file mode 100644 index 0000000000000..4b5d4873f2c4a --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/User.php @@ -0,0 +1,79 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class User +{ + /** + * @var int + */ + private $id; + + /** + * @var string + */ + public $name; + + /** + * @var string|int + */ + public $age; + + /** + * @var string + */ + private $email; + + /** + * @var Address + */ + public $address; + + /** + * @var Address[] + */ + public $addresses = []; + + /** + * @var \DateTime + */ + public $createdAt; + + /** + * @var float + */ + public $money; + + /** + * @var iterable + */ + public $languages; + + public function __construct($id, $name, $age) + { + $this->id = $id; + $this->name = $name; + $this->age = $age; + $this->email = 'test'; + $this->createdAt = new \DateTime(); + $this->money = 20.10; + $this->languages = new \ArrayObject(); + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserConstructorDTO.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserConstructorDTO.php new file mode 100644 index 0000000000000..6227159d5bd36 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserConstructorDTO.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class UserConstructorDTO +{ + /** + * @var string + */ + private $id; + /** + * @var ?string + */ + private $name; + + /** + * @var int + */ + private $age; + + /** + * @var bool + */ + private $constructor = false; + + public function __construct(string $id, string $name, int $age = 30) + { + $this->id = $id; + $this->name = $name; + $this->age = $age; + $this->constructor = true; + } + + /** + * @return int + */ + public function getId(): string + { + return $this->id; + } + + /** + * @return string + */ + public function getName(): ?string + { + return $this->name; + } + + /** + * @return int|null + */ + public function getAge() + { + return $this->age; + } + + public function getConstructor(): bool + { + return $this->constructor; + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserDTO.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserDTO.php new file mode 100644 index 0000000000000..0ad412609b9bd --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserDTO.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class UserDTO +{ + /** + * @var int + */ + public $id; + + /** + * @var string + */ + private $name; + + /** + * @var int + */ + public $age; + + /** + * @var int + */ + public $yearOfBirth; + + /** + * @var string + */ + public $email; + + /** + * @var AddressDTO|null + */ + public $address; + + /** + * @var AddressDTO[] + */ + public $addresses = []; + + /** + * @var \DateTime|null + */ + public $createdAt; + + /** + * @var array|null + */ + public $money; + + /** + * @var array + */ + public $languages = []; + + public function setName($name) + { + $this->name = $name; + } + + public function getName() + { + return $this->name; + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserDTONoAge.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserDTONoAge.php new file mode 100644 index 0000000000000..8e1cb033742a7 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserDTONoAge.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class UserDTONoAge +{ + /** + * @var int + */ + public $id; + + /** + * @var string + */ + public $name; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserDTONoName.php b/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserDTONoName.php new file mode 100644 index 0000000000000..b8ad0a08d2faf --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Fixtures/UserDTONoName.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Fixtures; + +class UserDTONoName +{ + /** + * @var int + */ + public $id; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Generator/UniqueVariableScopeTest.php b/src/Symfony/Component/AutoMapper/Tests/Generator/UniqueVariableScopeTest.php new file mode 100644 index 0000000000000..243a0e5eb410b --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Generator/UniqueVariableScopeTest.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Generator; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; + +class UniqueVariableScopeTest extends TestCase +{ + public function testVariableNameNotEquals(): void + { + $uniqueVariable = new UniqueVariableScope(); + $var1 = $uniqueVariable->getUniqueName('value'); + $var2 = $uniqueVariable->getUniqueName('value'); + $var3 = $uniqueVariable->getUniqueName('VALUE'); + + self::assertNotSame($var1, $var2); + self::assertNotSame($var1, $var3); + self::assertNotSame($var2, $var3); + self::assertNotSame(strtolower($var1), strtolower($var3)); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/MapperContextTest.php b/src/Symfony/Component/AutoMapper/Tests/MapperContextTest.php new file mode 100644 index 0000000000000..ccba9759324dd --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/MapperContextTest.php @@ -0,0 +1,142 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\MapperContext; +use Symfony\Component\AutoMapper\Exception\CircularReferenceException; +use Symfony\Component\AutoMapper\Exception\NoConstructorArgumentFoundException; + +/** + * @author Baptiste Leduc + */ +class MapperContextTest extends TestCase +{ + public function testIsAllowedAttribute(): void + { + $context = new MapperContext(); + $context->setAllowedAttributes(['id', 'age']); + $context->setIgnoredAttributes(['age']); + + self::assertTrue(MapperContext::isAllowedAttribute($context->toArray(), 'id')); + self::assertFalse(MapperContext::isAllowedAttribute($context->toArray(), 'age')); + self::assertFalse(MapperContext::isAllowedAttribute($context->toArray(), 'name')); + } + + public function testCircularReferenceLimit(): void + { + // with no circularReferenceLimit + $object = new \stdClass(); + $context = MapperContext::withReference([], 'reference', $object); + + self::assertTrue(MapperContext::shouldHandleCircularReference($context,'reference')); + + // with circularReferenceLimit + $object = new \stdClass(); + $context = new MapperContext(); + $context->setCircularReferenceLimit(3); + $context = MapperContext::withReference($context->toArray(), 'reference', $object); + + for ($i = 0; $i <= 2; ++$i) { + if (2 === $i) { + self::assertTrue(MapperContext::shouldHandleCircularReference($context,'reference')); + break; + } + + self::assertFalse(MapperContext::shouldHandleCircularReference($context,'reference')); + + // fake handleCircularReference to increment countReferenceRegistry + MapperContext::handleCircularReference($context,'reference', $object); + } + + self::expectException(CircularReferenceException::class); + self::expectExceptionMessage('A circular reference has been detected when mapping the object of type "stdClass" (configured limit: 3)'); + MapperContext::handleCircularReference($context,'reference', $object); + } + + public function testCircularReferenceHandler(): void + { + $object = new \stdClass(); + $context = new MapperContext(); + $context->setCircularReferenceHandler(function ($object) { + return $object; + }); + $context = MapperContext::withReference($context->toArray(),'reference', $object); + + self::assertTrue(MapperContext::shouldHandleCircularReference($context,'reference')); + self::assertEquals($object, MapperContext::handleCircularReference($context,'reference', $object)); + } + + public function testConstructorArgument(): void + { + $context = new MapperContext(); + $context->setConstructorArgument(Fixtures\User::class, 'id', 10); + $context->setConstructorArgument(Fixtures\User::class, 'age', 50); + + self::assertTrue(MapperContext::hasConstructorArgument($context->toArray(),Fixtures\User::class, 'id')); + self::assertFalse(MapperContext::hasConstructorArgument($context->toArray(),Fixtures\User::class, 'name')); + self::assertTrue(MapperContext::hasConstructorArgument($context->toArray(),Fixtures\User::class, 'age')); + + self::assertEquals(10, MapperContext::getConstructorArgument($context->toArray(),Fixtures\User::class, 'id')); + self::assertEquals(50, MapperContext::getConstructorArgument($context->toArray(),Fixtures\User::class, 'age')); + + self::assertNull(MapperContext::getConstructorArgument($context->toArray(),Fixtures\User::class, 'name')); + } + + public function testGroups(): void + { + $expected = ['group1', 'group4']; + $context = new MapperContext(); + $context->setGroups($expected); + + self::assertEquals($expected, $context->toArray()[MapperContext::GROUPS]); + self::assertContains('group1', $context->toArray()[MapperContext::GROUPS]); + self::assertNotContains('group2', $context->toArray()[MapperContext::GROUPS]); + } + + public function testTargetToPopulate(): void + { + $object = new \stdClass(); + $context = new MapperContext(); + $context->setTargetToPopulate($object); + + self::assertSame($object, $context->toArray()[MapperContext::TARGET_TO_POPULATE]); + } + + public function testWithNewContextIgnoredAttributesNested(): void + { + $context = [ + MapperContext::IGNORED_ATTRIBUTES => [ + 'foo' => ['bar'], + 'baz', + ] + ]; + + $newContext = MapperContext::withNewContext($context, 'foo'); + + self::assertEquals(['bar'], $newContext[MapperContext::IGNORED_ATTRIBUTES]); + } + + public function testWithNewContextAllowedAttributesNested(): void + { + $context = [ + MapperContext::ALLOWED_ATTRIBUTES => [ + 'foo' => ['bar'], + 'baz', + ] + ]; + + $newContext = MapperContext::withNewContext($context, 'foo'); + + self::assertEquals(['bar'], $newContext[MapperContext::ALLOWED_ATTRIBUTES]); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/MapperGeneratorMetadataFactoryTest.php b/src/Symfony/Component/AutoMapper/Tests/MapperGeneratorMetadataFactoryTest.php new file mode 100644 index 0000000000000..8457f929091b9 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/MapperGeneratorMetadataFactoryTest.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests; + +use Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\AutoMapper\Extractor\FromSourceMappingExtractor; +use Symfony\Component\AutoMapper\Extractor\FromTargetMappingExtractor; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Extractor\SourceTargetMappingExtractor; +use Symfony\Component\AutoMapper\MapperGeneratorMetadataFactory; +use Symfony\Component\AutoMapper\MapperGeneratorMetadataFactoryInterface; +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\Factory\ClassMetadataFactory; +use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; + +/** + * @author Baptiste Leduc + */ +class MapperGeneratorMetadataFactoryTest extends AutoMapperBaseTest +{ + /** @var MapperGeneratorMetadataFactoryInterface */ + protected $factory; + + public function setUp(): void + { + parent::setUp(); + + $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); + $reflectionExtractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE); + + $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 + ); + + $fromSourceMappingExtractor = new FromSourceMappingExtractor( + $propertyInfoExtractor, + $reflectionExtractor, + $reflectionExtractor, + $transformerFactory, + $classMetadataFactory + ); + + $this->factory = new MapperGeneratorMetadataFactory( + $sourceTargetMappingExtractor, + $fromSourceMappingExtractor, + $fromTargetMappingExtractor + ); + + $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($this->autoMapper)); + } + + public function testCreateObjectToArray(): void + { + $userReflection = new \ReflectionClass(Fixtures\User::class); + + $metadata = $this->factory->create($this->autoMapper, Fixtures\User::class, 'array'); + self::assertFalse($metadata->hasConstructor()); + self::assertTrue($metadata->shouldCheckAttributes()); + self::assertFalse($metadata->isTargetCloneable()); + self::assertEquals(Fixtures\User::class, $metadata->getSource()); + self::assertEquals('array', $metadata->getTarget()); + self::assertCount(\count($userReflection->getProperties()), $metadata->getPropertiesMapping()); + self::assertInstanceOf(PropertyMapping::class, $metadata->getPropertyMapping('id')); + self::assertInstanceOf(PropertyMapping::class, $metadata->getPropertyMapping('name')); + self::assertInstanceOf(PropertyMapping::class, $metadata->getPropertyMapping('email')); + } + + public function testCreateArrayToObject(): void + { + $userReflection = new \ReflectionClass(Fixtures\User::class); + + $metadata = $this->factory->create($this->autoMapper, 'array', Fixtures\User::class); + self::assertTrue($metadata->hasConstructor()); + self::assertTrue($metadata->shouldCheckAttributes()); + self::assertTrue($metadata->isTargetCloneable()); + self::assertEquals('array', $metadata->getSource()); + self::assertEquals(Fixtures\User::class, $metadata->getTarget()); + self::assertCount(\count($userReflection->getProperties()), $metadata->getPropertiesMapping()); + self::assertInstanceOf(PropertyMapping::class, $metadata->getPropertyMapping('id')); + self::assertInstanceOf(PropertyMapping::class, $metadata->getPropertyMapping('name')); + self::assertInstanceOf(PropertyMapping::class, $metadata->getPropertyMapping('email')); + } + + public function testCreateWithBothObjects(): void + { + $metadata = $this->factory->create($this->autoMapper, Fixtures\UserConstructorDTO::class, Fixtures\User::class); + self::assertTrue($metadata->hasConstructor()); + self::assertTrue($metadata->shouldCheckAttributes()); + self::assertTrue($metadata->isTargetCloneable()); + self::assertEquals(Fixtures\UserConstructorDTO::class, $metadata->getSource()); + self::assertEquals(Fixtures\User::class, $metadata->getTarget()); + self::assertInstanceOf(PropertyMapping::class, $metadata->getPropertyMapping('id')); + self::assertInstanceOf(PropertyMapping::class, $metadata->getPropertyMapping('name')); + self::assertNull($metadata->getPropertyMapping('email')); + } + + public function testHasNotConstructor(): void + { + $metadata = $this->factory->create($this->autoMapper, 'array', Fixtures\UserDTO::class); + + self::assertFalse($metadata->hasConstructor()); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/ArrayTransformerFactoryTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/ArrayTransformerFactoryTest.php new file mode 100644 index 0000000000000..a7aa12fb0c597 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/ArrayTransformerFactoryTest.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\MapperMetadata; +use Symfony\Component\AutoMapper\Transformer\ArrayTransformer; +use Symfony\Component\AutoMapper\Transformer\ArrayTransformerFactory; +use Symfony\Component\AutoMapper\Transformer\ChainTransformerFactory; +use Symfony\Component\PropertyInfo\Type; + +class ArrayTransformerFactoryTest extends TestCase +{ + public function testGetTransformer(): void + { + $chainFactory = new ChainTransformerFactory(); + $factory = new ArrayTransformerFactory($chainFactory); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('array', false, null, true)], [new Type('array', false, null, true)], $mapperMetadata); + + self::assertInstanceOf(ArrayTransformer::class, $transformer); + } + + public function testNoTransformerTargetNoCollection(): void + { + $chainFactory = new ChainTransformerFactory(); + $factory = new ArrayTransformerFactory($chainFactory); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('array', false, null, true)], [new Type('string')], $mapperMetadata); + + self::assertNull($transformer); + } + + public function testNoTransformerSourceNoCollection(): void + { + $chainFactory = new ChainTransformerFactory(); + $factory = new ArrayTransformerFactory($chainFactory); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('string')], [new Type('array', false, null, true)], $mapperMetadata); + + self::assertNull($transformer); + } + + public function testNoTransformerIfNoSubTypeTransformerNoCollection(): void + { + $chainFactory = new ChainTransformerFactory(); + $factory = new ArrayTransformerFactory($chainFactory); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $stringType = new Type('string'); + $transformer = $factory->getTransformer([new Type('array', false, null, true, null, $stringType)], [new Type('array', false, null, true, null, $stringType)], $mapperMetadata); + + self::assertNull($transformer); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/ArrayTransformerTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/ArrayTransformerTest.php new file mode 100644 index 0000000000000..ae64b6df05041 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/ArrayTransformerTest.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Transformer\ArrayTransformer; +use Symfony\Component\AutoMapper\Transformer\BuiltinTransformer; +use Symfony\Component\PropertyInfo\Type; + +class ArrayTransformerTest extends TestCase +{ + use EvalTransformerTrait; + + public function testArrayToArray(): void + { + $transformer = new ArrayTransformer(new BuiltinTransformer(new Type('string'), [new Type('string')])); + $output = $this->evalTransformer($transformer, ['test']); + + self::assertEquals(['test'], $output); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/BuiltinTransformerFactoryTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/BuiltinTransformerFactoryTest.php new file mode 100644 index 0000000000000..3088f36daebda --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/BuiltinTransformerFactoryTest.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\MapperMetadata; +use Symfony\Component\AutoMapper\Transformer\BuiltinTransformer; +use Symfony\Component\AutoMapper\Transformer\BuiltinTransformerFactory; +use Symfony\Component\PropertyInfo\Type; + +class BuiltinTransformerFactoryTest extends TestCase +{ + public function testGetTransformer(): void + { + $factory = new BuiltinTransformerFactory(); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('string')], [new Type('string')], $mapperMetadata); + + self::assertInstanceOf(BuiltinTransformer::class, $transformer); + + $transformer = $factory->getTransformer([new Type('bool')], [new Type('string')], $mapperMetadata); + + self::assertInstanceOf(BuiltinTransformer::class, $transformer); + } + + public function testNoTransformer(): void + { + $factory = new BuiltinTransformerFactory(); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([], [new Type('string')], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer(null, [new Type('string')], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer(['test'], [new Type('string')], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('string'), new Type('string')], [new Type('string')], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('array')], [new Type('string')], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('object')], [new Type('string')], $mapperMetadata); + + self::assertNull($transformer); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/BuiltinTransformerTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/BuiltinTransformerTest.php new file mode 100644 index 0000000000000..9f73772fbd505 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/BuiltinTransformerTest.php @@ -0,0 +1,299 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Transformer\BuiltinTransformer; +use Symfony\Component\PropertyInfo\Type; + +class BuiltinTransformerTest extends TestCase +{ + use EvalTransformerTrait; + + public function testStringToString() + { + $transformer = new BuiltinTransformer(new Type('string'), [new Type('string')]); + $output = $this->evalTransformer($transformer, 'foo'); + + self::assertSame('foo', $output); + } + + public function testStringToArray() + { + $transformer = new BuiltinTransformer(new Type('string'), [new Type('array')]); + $output = $this->evalTransformer($transformer, 'foo'); + + self::assertSame(['foo'], $output); + } + + public function testStringToIterable() + { + $transformer = new BuiltinTransformer(new Type('string'), [new Type('iterable')]); + $output = $this->evalTransformer($transformer, 'foo'); + + self::assertSame(['foo'], $output); + } + + public function testStringToFloat() + { + $transformer = new BuiltinTransformer(new Type('string'), [new Type('float')]); + $output = $this->evalTransformer($transformer, '12.2'); + + self::assertSame(12.2, $output); + } + + public function testStringToInt() + { + $transformer = new BuiltinTransformer(new Type('string'), [new Type('int')]); + $output = $this->evalTransformer($transformer, '12'); + + self::assertSame(12, $output); + } + + public function testStringToBool() + { + $transformer = new BuiltinTransformer(new Type('string'), [new Type('bool')]); + $output = $this->evalTransformer($transformer, 'foo'); + + self::assertSame(true, $output); + + $output = $this->evalTransformer($transformer, ''); + + self::assertSame(false, $output); + } + + public function testBoolToInt() + { + $transformer = new BuiltinTransformer(new Type('bool'), [new Type('int')]); + $output = $this->evalTransformer($transformer, true); + + self::assertSame(1, $output); + + $output = $this->evalTransformer($transformer, false); + + self::assertSame(0, $output); + } + + public function testBoolToString() + { + $transformer = new BuiltinTransformer(new Type('bool'), [new Type('string')]); + + $output = $this->evalTransformer($transformer, true); + + self::assertSame('1', $output); + + $output = $this->evalTransformer($transformer, false); + + self::assertSame('', $output); + } + + public function testBoolToFloat() + { + $transformer = new BuiltinTransformer(new Type('bool'), [new Type('float')]); + + $output = $this->evalTransformer($transformer, true); + + self::assertSame(1.0, $output); + + $output = $this->evalTransformer($transformer, false); + + self::assertSame(0.0, $output); + } + + public function testBoolToArray() + { + $transformer = new BuiltinTransformer(new Type('bool'), [new Type('array')]); + + $output = $this->evalTransformer($transformer, true); + + self::assertSame([true], $output); + + $output = $this->evalTransformer($transformer, false); + + self::assertSame([false], $output); + } + + public function testBoolToIterable() + { + $transformer = new BuiltinTransformer(new Type('bool'), [new Type('iterable')]); + + $output = $this->evalTransformer($transformer, true); + + self::assertSame([true], $output); + + $output = $this->evalTransformer($transformer, false); + + self::assertSame([false], $output); + } + + public function testBoolToBool() + { + $transformer = new BuiltinTransformer(new Type('bool'), [new Type('bool')]); + + $output = $this->evalTransformer($transformer, true); + + self::assertSame(true, $output); + + $output = $this->evalTransformer($transformer, false); + + self::assertSame(false, $output); + } + + public function testFloatToString() + { + $transformer = new BuiltinTransformer(new Type('float'), [new Type('string')]); + + $output = $this->evalTransformer($transformer, 12.23); + + self::assertSame('12.23', $output); + } + + public function testFloatToInt() + { + $transformer = new BuiltinTransformer(new Type('float'), [new Type('int')]); + + $output = $this->evalTransformer($transformer, 12.23); + + self::assertSame(12, $output); + } + + public function testFloatToBool() + { + $transformer = new BuiltinTransformer(new Type('float'), [new Type('bool')]); + + $output = $this->evalTransformer($transformer, 12.23); + + self::assertSame(true, $output); + + $output = $this->evalTransformer($transformer, 0.0); + + self::assertSame(false, $output); + } + + public function testFloatToArray() + { + $transformer = new BuiltinTransformer(new Type('float'), [new Type('array')]); + + $output = $this->evalTransformer($transformer, 12.23); + + self::assertSame([12.23], $output); + } + + public function testFloatToIterable() + { + $transformer = new BuiltinTransformer(new Type('float'), [new Type('iterable')]); + + $output = $this->evalTransformer($transformer, 12.23); + + self::assertSame([12.23], $output); + } + + public function testFloatToFloat() + { + $transformer = new BuiltinTransformer(new Type('float'), [new Type('float')]); + + $output = $this->evalTransformer($transformer, 12.23); + + self::assertSame(12.23, $output); + } + + public function testIntToInt() + { + $transformer = new BuiltinTransformer(new Type('int'), [new Type('int')]); + + $output = $this->evalTransformer($transformer, 12); + + self::assertSame(12, $output); + } + + public function testIntToFloat() + { + $transformer = new BuiltinTransformer(new Type('int'), [new Type('float')]); + + $output = $this->evalTransformer($transformer, 12); + + self::assertSame(12.0, $output); + } + + public function testIntToString() + { + $transformer = new BuiltinTransformer(new Type('int'), [new Type('string')]); + + $output = $this->evalTransformer($transformer, 12); + + self::assertSame('12', $output); + } + + public function testIntToBool() + { + $transformer = new BuiltinTransformer(new Type('int'), [new Type('bool')]); + + $output = $this->evalTransformer($transformer, 12); + + self::assertSame(true, $output); + + $output = $this->evalTransformer($transformer, 0); + + self::assertSame(false, $output); + } + + public function testIntToArray() + { + $transformer = new BuiltinTransformer(new Type('int'), [new Type('array')]); + + $output = $this->evalTransformer($transformer, 12); + + self::assertSame([12], $output); + } + + public function testIntToIterable() + { + $transformer = new BuiltinTransformer(new Type('int'), [new Type('iterable')]); + + $output = $this->evalTransformer($transformer, 12); + + self::assertSame([12], $output); + } + + public function testIterableToArray() + { + $transformer = new BuiltinTransformer(new Type('iterable'), [new Type('array')]); + + $closure = function () { + yield 1; + yield 2; + }; + + $output = $this->evalTransformer($transformer, $closure()); + + self::assertSame([1, 2], $output); + } + + public function testArrayToIterable() + { + $transformer = new BuiltinTransformer(new Type('array'), [new Type('iterable')]); + $output = $this->evalTransformer($transformer, [1, 2]); + + self::assertSame([1, 2], $output); + } + + public function testToUnknowCast() + { + $transformer = new BuiltinTransformer(new Type('callable'), [new Type('string')]); + + $output = $this->evalTransformer($transformer, function ($test) { + return $test; + }); + + self::assertIsCallable($output); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/CallbackTransformerTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/CallbackTransformerTest.php new file mode 100644 index 0000000000000..d643597233b66 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/CallbackTransformerTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Transformer\CallbackTransformer; + +class CallbackTransformerTest extends TestCase +{ + use EvalTransformerTrait; + + public function testCallbackTransform() + { + $transformer = new CallbackTransformer('test'); + $function = $this->createTransformerFunction($transformer); + $class = new class () { + public $callbacks; + + public function __construct() + { + $this->callbacks['test'] = function ($input) { + return 'output'; + }; + } + }; + + $transform = \Closure::bind($function, $class); + + $output = $transform('input'); + + self::assertEquals('output', $output); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/ChainTransformerFactoryTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/ChainTransformerFactoryTest.php new file mode 100644 index 0000000000000..66cb86a94e98c --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/ChainTransformerFactoryTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\MapperMetadata; +use Symfony\Component\AutoMapper\Transformer\ChainTransformerFactory; +use Symfony\Component\AutoMapper\Transformer\CopyTransformer; +use Symfony\Component\AutoMapper\Transformer\TransformerFactoryInterface; + +class ChainTransformerFactoryTest extends TestCase +{ + public function testGetTransformer() + { + $chainTransformerFactory = new ChainTransformerFactory(); + $transformer = new CopyTransformer(); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + $subTransformer = $this + ->getMockBuilder(TransformerFactoryInterface::class) + ->getMock() + ; + + $subTransformer->expects($this->any())->method('getTransformer')->willReturn($transformer); + $chainTransformerFactory->addTransformerFactory($subTransformer); + + $transformerReturned = $chainTransformerFactory->getTransformer([], [], $mapperMetadata); + + self::assertSame($transformer, $transformerReturned); + } + public function testNoTransformer() + { + $chainTransformerFactory = new ChainTransformerFactory(); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + $subTransformer = $this + ->getMockBuilder(TransformerFactoryInterface::class) + ->getMock() + ; + + $subTransformer->expects($this->any())->method('getTransformer')->willReturn(null); + $chainTransformerFactory->addTransformerFactory($subTransformer); + + $transformerReturned = $chainTransformerFactory->getTransformer([], [], $mapperMetadata); + + self::assertNull($transformerReturned); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/CopyTransformerTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/CopyTransformerTest.php new file mode 100644 index 0000000000000..f0dd3fc7686df --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/CopyTransformerTest.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Transformer\CopyTransformer; + +class CopyTransformerTest extends TestCase +{ + use EvalTransformerTrait; + + public function testCopyTransformer() + { + $transformer = new CopyTransformer(); + + $output = $this->evalTransformer($transformer, 'foo'); + + self::assertSame('foo', $output); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeImmutableToMutableTransformerTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeImmutableToMutableTransformerTest.php new file mode 100644 index 0000000000000..766ed0d8de915 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeImmutableToMutableTransformerTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Transformer\DateTimeImmutableToMutableTransformer; + +class DateTimeImmutableToMutableTransformerTest extends TestCase +{ + use EvalTransformerTrait; + + public function testDateTimeImmutableTransformer() + { + $transformer = new DateTimeImmutableToMutableTransformer(); + + $date = new \DateTimeImmutable(); + $output = $this->evalTransformer($transformer, $date); + + self::assertInstanceOf(\DateTime::class, $output); + self::assertSame($date->format(\DateTime::RFC3339), $output->format(\DateTime::RFC3339)); + } + + public function testAssignByRef() + { + $transformer = new DateTimeImmutableToMutableTransformer(); + + self::assertFalse($transformer->assignByRef()); + } + + public function testEmptyDependencies() + { + $transformer = new DateTimeImmutableToMutableTransformer(); + + self::assertEmpty($transformer->getDependencies()); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeMutableToImmutableTransformerTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeMutableToImmutableTransformerTest.php new file mode 100644 index 0000000000000..01f907aece0c2 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeMutableToImmutableTransformerTest.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Transformer\DateTimeMutableToImmutableTransformer; + +class DateTimeMutableToImmutableTransformerTest extends TestCase +{ + use EvalTransformerTrait; + + public function testDateTimeImmutableTransformer() + { + $transformer = new DateTimeMutableToImmutableTransformer(); + + $date = new \DateTime(); + $output = $this->evalTransformer($transformer, $date); + + self::assertInstanceOf(\DateTimeImmutable::class, $output); + self::assertSame($date->format(\DateTime::RFC3339), $output->format(\DateTime::RFC3339)); + } + + public function testAssignByRef() + { + $transformer = new DateTimeMutableToImmutableTransformer(); + + self::assertFalse($transformer->assignByRef()); + } + + public function testEmptyDependencies() + { + $transformer = new DateTimeMutableToImmutableTransformer(); + + self::assertEmpty($transformer->getDependencies()); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeToStringTransformerTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeToStringTransformerTest.php new file mode 100644 index 0000000000000..7d776d3715c5f --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeToStringTransformerTest.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Transformer\DateTimeToStringTansformer; + +class DateTimeToStringTransformerTest extends TestCase +{ + use EvalTransformerTrait; + + public function testDateTimeTransformer() + { + $transformer = new DateTimeToStringTansformer(); + + $date = new \DateTime(); + $output = $this->evalTransformer($transformer, new \DateTime()); + + self::assertSame($date->format(\DateTime::RFC3339), $output); + } + + public function testDateTimeTransformerCustomFormat() + { + $transformer = new DateTimeToStringTansformer(\DateTime::COOKIE); + + $date = new \DateTime(); + $output = $this->evalTransformer($transformer, new \DateTime()); + + self::assertSame($date->format(\DateTime::COOKIE), $output); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeTransformerFactoryTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeTransformerFactoryTest.php new file mode 100644 index 0000000000000..fa8462843cba5 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/DateTimeTransformerFactoryTest.php @@ -0,0 +1,86 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\MapperMetadata; +use Symfony\Component\AutoMapper\Transformer\CopyTransformer; +use Symfony\Component\AutoMapper\Transformer\DateTimeImmutableToMutableTransformer; +use Symfony\Component\AutoMapper\Transformer\DateTimeMutableToImmutableTransformer; +use Symfony\Component\AutoMapper\Transformer\DateTimeToStringTansformer; +use Symfony\Component\AutoMapper\Transformer\DateTimeTransformerFactory; +use Symfony\Component\AutoMapper\Transformer\StringToDateTimeTransformer; +use Symfony\Component\PropertyInfo\Type; + +class DateTimeTransformerFactoryTest extends TestCase +{ + public function testGetTransformer() + { + $factory = new DateTimeTransformerFactory(); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('object', false, \DateTime::class)], [new Type('object', false, \DateTime::class)], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(CopyTransformer::class, $transformer); + + $transformer = $factory->getTransformer([new Type('object', false, \DateTime::class)], [new Type('string')], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(DateTimeToStringTansformer::class, $transformer); + + $transformer = $factory->getTransformer([new Type('string')], [new Type('object', false, \DateTime::class)], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(StringToDateTimeTransformer::class, $transformer); + } + + public function testGetTransformerImmutable() + { + $factory = new DateTimeTransformerFactory(); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('object', false, \DateTimeImmutable::class)], [new Type('object', false, \DateTime::class)], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(DateTimeImmutableToMutableTransformer::class, $transformer); + } + + public function testGetTransformerMutable() + { + $factory = new DateTimeTransformerFactory(); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('object', false, \DateTime::class)], [new Type('object', false, \DateTimeImmutable::class)], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(DateTimeMutableToImmutableTransformer::class, $transformer); + } + + public function testNoTransformer() + { + $factory = new DateTimeTransformerFactory(); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('string')], [new Type('string')], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('object', false, \DateTime::class)], [new Type('bool')], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('bool')], [new Type('object', false, \DateTime::class)], $mapperMetadata); + + self::assertNull($transformer); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/EvalTransformerTrait.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/EvalTransformerTrait.php new file mode 100644 index 0000000000000..fd846b86e712b --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/EvalTransformerTrait.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PhpParser\Node\Expr; +use PhpParser\Node\Stmt; +use PhpParser\Node\Param; +use PhpParser\PrettyPrinter\Standard; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Extractor\ReadAccessor; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; +use Symfony\Component\AutoMapper\Transformer\TransformerInterface; + +trait EvalTransformerTrait +{ + private function createTransformerFunction(TransformerInterface $transformer, PropertyMapping $propertyMapping = null): \Closure + { + if (null === $propertyMapping) { + $propertyMapping = new PropertyMapping( + new ReadAccessor(ReadAccessor::TYPE_PROPERTY, 'dummy'), + null, + null, + $transformer, + 'dummy' + ); + } + + $variableScope = new UniqueVariableScope(); + $inputName = $variableScope->getUniqueName('input'); + $inputExpr = new Expr\Variable($inputName); + + [$outputExpr, $stmts] = $transformer->transform($inputExpr, $propertyMapping, $variableScope); + + $stmts[] = new Stmt\Return_($outputExpr); + + $functionExpr = new Expr\Closure([ + 'stmts' => $stmts, + 'params' => [new Param($inputExpr), new Param(new Expr\Variable('context'), new Expr\Array_())] + ]); + + $printer = new Standard(); + $code = $printer->prettyPrint([new Stmt\Return_($functionExpr)]); + + return eval($code); + } + + private function evalTransformer(TransformerInterface $transformer, $input, PropertyMapping $propertyMapping = null) + { + $function = $this->createTransformerFunction($transformer, $propertyMapping); + + return $function($input); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/MultipleTransformerFactoryTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/MultipleTransformerFactoryTest.php new file mode 100644 index 0000000000000..c8c94c75fa575 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/MultipleTransformerFactoryTest.php @@ -0,0 +1,80 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\MapperMetadata; +use Symfony\Component\AutoMapper\Transformer\BuiltinTransformer; +use Symfony\Component\AutoMapper\Transformer\BuiltinTransformerFactory; +use Symfony\Component\AutoMapper\Transformer\ChainTransformerFactory; +use Symfony\Component\AutoMapper\Transformer\MultipleTransformer; +use Symfony\Component\AutoMapper\Transformer\MultipleTransformerFactory; +use Symfony\Component\PropertyInfo\Type; + +class MultipleTransformerFactoryTest extends TestCase +{ + public function testGetTransformer() + { + $chainFactory = new ChainTransformerFactory(); + $factory = new MultipleTransformerFactory($chainFactory); + + $chainFactory->addTransformerFactory($factory); + $chainFactory->addTransformerFactory(new BuiltinTransformerFactory()); + + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('string'), new Type('int')], [], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(MultipleTransformer::class, $transformer); + + $transformer = $factory->getTransformer([new Type('string'), new Type('object')], [], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(BuiltinTransformer::class, $transformer); + } + + public function testNoTransformerIfNoSubTransformer() + { + $chainFactory = new ChainTransformerFactory(); + $factory = new MultipleTransformerFactory($chainFactory); + + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('string'), new Type('int')], [], $mapperMetadata); + + self::assertNull($transformer); + } + + public function testNoTransformer() + { + $chainFactory = new ChainTransformerFactory(); + $factory = new MultipleTransformerFactory($chainFactory); + + $chainFactory->addTransformerFactory($factory); + $chainFactory->addTransformerFactory(new BuiltinTransformerFactory()); + + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer(null, null, $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([], null, $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('string')], null, $mapperMetadata); + + self::assertNull($transformer); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/MultipleTransformerTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/MultipleTransformerTest.php new file mode 100644 index 0000000000000..7e1582c1180f3 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/MultipleTransformerTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Transformer\BuiltinTransformer; +use Symfony\Component\AutoMapper\Transformer\MultipleTransformer; +use Symfony\Component\PropertyInfo\Type; + +class MultipleTransformerTest extends TestCase +{ + use EvalTransformerTrait; + + public function testMultipleTransformer() + { + $transformer = new MultipleTransformer([ + [ + 'transformer' => new BuiltinTransformer(new Type('string'), [new Type('int')]), + 'type' => new Type('string'), + ], + [ + 'transformer' => new BuiltinTransformer(new Type('int'), [new Type('string')]), + 'type' => new Type('int'), + ], + ]); + + $output = $this->evalTransformer($transformer, '12'); + + self::assertSame(12, $output); + + $output = $this->evalTransformer($transformer, 12); + + self::assertSame('12', $output); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/NullableTransformerFactoryTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/NullableTransformerFactoryTest.php new file mode 100644 index 0000000000000..dc39c38935f87 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/NullableTransformerFactoryTest.php @@ -0,0 +1,93 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\MapperMetadata; +use Symfony\Component\AutoMapper\Transformer\BuiltinTransformerFactory; +use Symfony\Component\AutoMapper\Transformer\ChainTransformerFactory; +use Symfony\Component\AutoMapper\Transformer\NullableTransformer; +use Symfony\Component\AutoMapper\Transformer\NullableTransformerFactory; +use Symfony\Component\PropertyInfo\Type; + +class NullableTransformerFactoryTest extends TestCase +{ + private $isTargetNullableProperty; + + public function setUp(): void + { + $this->isTargetNullableProperty = (new \ReflectionClass(NullableTransformer::class))->getProperty('isTargetNullable'); + $this->isTargetNullableProperty->setAccessible(true); + } + + public function testGetTransformer(): void + { + $chainFactory = new ChainTransformerFactory(); + $factory = new NullableTransformerFactory($chainFactory); + + $chainFactory->addTransformerFactory($factory); + $chainFactory->addTransformerFactory(new BuiltinTransformerFactory()); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('string', true)], [new Type('string')], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(NullableTransformer::class, $transformer); + self::assertFalse($this->isTargetNullableProperty->getValue($transformer)); + + $transformer = $factory->getTransformer([new Type('string', true)], [new Type('string', true)], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(NullableTransformer::class, $transformer); + self::assertTrue($this->isTargetNullableProperty->getValue($transformer)); + + $transformer = $factory->getTransformer([new Type('string', true)], [new Type('string'), new Type('int', true)], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(NullableTransformer::class, $transformer); + self::assertTrue($this->isTargetNullableProperty->getValue($transformer)); + + $transformer = $factory->getTransformer([new Type('string', true)], [new Type('string'), new Type('int')], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(NullableTransformer::class, $transformer); + self::assertFalse($this->isTargetNullableProperty->getValue($transformer)); + } + + public function testNullTransformerIfSourceTypeNotNullable(): void + { + $chainFactory = new ChainTransformerFactory(); + $factory = new NullableTransformerFactory($chainFactory); + + $chainFactory->addTransformerFactory($factory); + $chainFactory->addTransformerFactory(new BuiltinTransformerFactory()); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('string')], [new Type('string')], $mapperMetadata); + + self::assertNull($transformer); + } + + public function testNullTransformerIfMultipleSource(): void + { + $chainFactory = new ChainTransformerFactory(); + $factory = new NullableTransformerFactory($chainFactory); + + $chainFactory->addTransformerFactory($factory); + $chainFactory->addTransformerFactory(new BuiltinTransformerFactory()); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('string', true), new Type('string')], [new Type('string')], $mapperMetadata); + + self::assertNull($transformer); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/NullableTransformerTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/NullableTransformerTest.php new file mode 100644 index 0000000000000..6750e6d64d656 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/NullableTransformerTest.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Transformer\BuiltinTransformer; +use Symfony\Component\AutoMapper\Transformer\NullableTransformer; +use Symfony\Component\PropertyInfo\Type; + +class NullableTransformerTest extends TestCase +{ + use EvalTransformerTrait; + + public function testNullTransformerTargetNullable() + { + $transformer = new NullableTransformer(new BuiltinTransformer(new Type('string'), [new Type('string', true)]), true); + + $output = $this->evalTransformer($transformer, 'foo'); + + self::assertSame('foo', $output); + + $output = $this->evalTransformer($transformer, null); + + self::assertNull($output); + } + + public function testNullTransformerTargetNotNullable() + { + $transformer = new NullableTransformer(new BuiltinTransformer(new Type('string'), [new Type('string')]), false); + + $output = $this->evalTransformer($transformer, 'foo'); + + self::assertSame('foo', $output); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/ObjectTransformerFactoryTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/ObjectTransformerFactoryTest.php new file mode 100644 index 0000000000000..f8a64f324862b --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/ObjectTransformerFactoryTest.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\AutoMapperRegistryInterface; +use Symfony\Component\AutoMapper\MapperMetadata; +use Symfony\Component\AutoMapper\Transformer\ObjectTransformer; +use Symfony\Component\AutoMapper\Transformer\ObjectTransformerFactory; +use Symfony\Component\PropertyInfo\Type; + +class ObjectTransformerFactoryTest extends TestCase +{ + public function testGetTransformer(): void + { + $autoMapperRegistry = $this->getMockBuilder(AutoMapperRegistryInterface::class)->getMock(); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + $factory = new ObjectTransformerFactory($autoMapperRegistry); + + $autoMapperRegistry + ->expects($this->any()) + ->method('hasMapper') + ->willReturn(true) + ; + + $transformer = $factory->getTransformer([new Type('object', false, \stdClass::class)], [new Type('object', false, \stdClass::class)], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(ObjectTransformer::class, $transformer); + + $transformer = $factory->getTransformer([new Type('array')], [new Type('object', false, \stdClass::class)], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(ObjectTransformer::class, $transformer); + + $transformer = $factory->getTransformer([new Type('object', false, \stdClass::class)], [new Type('array')], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(ObjectTransformer::class, $transformer); + } + + public function testNoTransformer(): void + { + $autoMapperRegistry = $this->getMockBuilder(AutoMapperRegistryInterface::class)->getMock(); + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + $factory = new ObjectTransformerFactory($autoMapperRegistry); + + $transformer = $factory->getTransformer([], [], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('object')], [], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([], [new Type('object')], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('object'), new Type('object')], [new Type('object')], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('object')], [new Type('object'), new Type('object')], $mapperMetadata); + + self::assertNull($transformer); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/ObjectTransformerTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/ObjectTransformerTest.php new file mode 100644 index 0000000000000..ec15600f5c61d --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/ObjectTransformerTest.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Transformer\ObjectTransformer; +use Symfony\Component\PropertyInfo\Type; + +class ObjectTransformerTest extends TestCase +{ + use EvalTransformerTrait; + + public function testObjectTransformer() + { + $transformer = new ObjectTransformer(new Type('object', false, Foo::class), new Type('object', false, Foo::class)); + + $function = $this->createTransformerFunction($transformer); + $class = new class () { + public $mappers; + + public function __construct() + { + $this->mappers['Mapper_' . Foo::class . '_' . Foo::class] = new class () { + public function map() + { + return new Foo(); + } + }; + } + }; + + $transform = \Closure::bind($function, $class); + $output = $transform(new Foo()); + + self::assertNotNull($output); + self::assertInstanceOf(Foo::class, $output); + } +} + +class Foo { + public $bar; +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/StringToDateTimeTransformerTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/StringToDateTimeTransformerTest.php new file mode 100644 index 0000000000000..c19964323d72b --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/StringToDateTimeTransformerTest.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\Transformer\StringToDateTimeTransformer; + +class StringToDateTimeTransformerTest extends TestCase +{ + use EvalTransformerTrait; + + public function testDateTimeTransformer() + { + $transformer = new StringToDateTimeTransformer(\DateTime::class); + + $date = new \DateTime(); + $output = $this->evalTransformer($transformer, $date->format(\DateTime::RFC3339)); + + self::assertInstanceOf(\DateTime::class, $output); + self::assertSame($date->format(\DateTime::RFC3339), $output->format(\DateTime::RFC3339)); + } + + public function testDateTimeTransformerCustomFormat() + { + $transformer = new StringToDateTimeTransformer(\DateTime::class, \DateTime::COOKIE); + + $date = new \DateTime(); + $output = $this->evalTransformer($transformer, $date->format(\DateTime::COOKIE)); + + self::assertInstanceOf(\DateTime::class, $output); + self::assertSame($date->format(\DateTime::RFC3339), $output->format(\DateTime::RFC3339)); + } + + public function testDateTimeTransformerImmutable() + { + $transformer = new StringToDateTimeTransformer(\DateTimeImmutable::class, \DateTime::COOKIE); + + $date = new \DateTime(); + $output = $this->evalTransformer($transformer, $date->format(\DateTime::COOKIE)); + + self::assertInstanceOf(\DateTimeImmutable::class, $output); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/Transformer/UniqueTypeTransformerFactoryTest.php b/src/Symfony/Component/AutoMapper/Tests/Transformer/UniqueTypeTransformerFactoryTest.php new file mode 100644 index 0000000000000..0b31d0ec22642 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/Transformer/UniqueTypeTransformerFactoryTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Tests\Transformer; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\AutoMapper\MapperMetadata; +use Symfony\Component\AutoMapper\Transformer\BuiltinTransformer; +use Symfony\Component\AutoMapper\Transformer\BuiltinTransformerFactory; +use Symfony\Component\AutoMapper\Transformer\ChainTransformerFactory; +use Symfony\Component\AutoMapper\Transformer\UniqueTypeTransformerFactory; +use Symfony\Component\PropertyInfo\Type; + +class UniqueTypeTransformerFactoryTest extends TestCase +{ + public function testGetTransformer(): void + { + $chainFactory = new ChainTransformerFactory(); + $factory = new UniqueTypeTransformerFactory($chainFactory); + + $chainFactory->addTransformerFactory($factory); + $chainFactory->addTransformerFactory(new BuiltinTransformerFactory()); + + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer([new Type('string')], [new Type('string'), new Type('string')], $mapperMetadata); + + self::assertNotNull($transformer); + self::assertInstanceOf(BuiltinTransformer::class, $transformer); + } + + public function testNullTransformer(): void + { + $chainFactory = new ChainTransformerFactory(); + $factory = new UniqueTypeTransformerFactory($chainFactory); + + $chainFactory->addTransformerFactory($factory); + $chainFactory->addTransformerFactory(new BuiltinTransformerFactory()); + + $mapperMetadata = $this->getMockBuilder(MapperMetadata::class)->disableOriginalConstructor()->getMock(); + + $transformer = $factory->getTransformer(null, [], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([], [], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('string')], [], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('string'), new Type('string')], [], $mapperMetadata); + + self::assertNull($transformer); + + $transformer = $factory->getTransformer([new Type('string')], [new Type('string')], $mapperMetadata); + + self::assertNull($transformer); + } +} diff --git a/src/Symfony/Component/AutoMapper/Tests/cache/.gitignore b/src/Symfony/Component/AutoMapper/Tests/cache/.gitignore new file mode 100644 index 0000000000000..72e8ffc0db8aa --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Tests/cache/.gitignore @@ -0,0 +1 @@ +* diff --git a/src/Symfony/Component/AutoMapper/Transformer/AbstractUniqueTypeTransformerFactory.php b/src/Symfony/Component/AutoMapper/Transformer/AbstractUniqueTypeTransformerFactory.php new file mode 100644 index 0000000000000..9ed1826bd74e2 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/AbstractUniqueTypeTransformerFactory.php @@ -0,0 +1,44 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use Symfony\Component\AutoMapper\MapperMetadataInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * Abstract transformer which is used by transformer needing transforming only from one single type to one single type. + * + * @author Joel Wurtz + */ +abstract class AbstractUniqueTypeTransformerFactory implements TransformerFactoryInterface +{ + /** + * {@inheritdoc} + */ + public function getTransformer(?array $sourcesTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface + { + $nbSourcesTypes = $sourcesTypes ? \count($sourcesTypes) : 0; + $nbTargetsTypes = $targetTypes ? \count($targetTypes) : 0; + + if (0 === $nbSourcesTypes || $nbSourcesTypes > 1 || !$sourcesTypes[0] instanceof Type) { + return null; + } + + if (0 === $nbTargetsTypes || $nbTargetsTypes > 1 || !$targetTypes[0] instanceof Type) { + return null; + } + + return $this->createTransformer($sourcesTypes[0], $targetTypes[0], $mapperMetadata); + } + + abstract protected function createTransformer(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface; +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/ArrayTransformer.php b/src/Symfony/Component/AutoMapper/Transformer/ArrayTransformer.php new file mode 100644 index 0000000000000..659fc9c666009 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/ArrayTransformer.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Expr; +use PhpParser\Node\Stmt; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; + +/** + * Transformer array decorator. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class ArrayTransformer implements TransformerInterface +{ + private $itemTransformer; + + public function __construct(TransformerInterface $itemTransformer) + { + $this->itemTransformer = $itemTransformer; + } + + /** + * {@inheritdoc} + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array + { + $valuesVar = new Expr\Variable($uniqueVariableScope->getUniqueName('values')); + $statements = [ + // $values = []; + new Stmt\Expression(new Expr\Assign($valuesVar, new Expr\Array_())), + ]; + + $loopValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('value')); + + [$output, $itemStatements] = $this->itemTransformer->transform($loopValueVar, $propertyMapping, $uniqueVariableScope); + + if ($this->itemTransformer->assignByRef()) { + $itemStatements[] = new Stmt\Expression(new Expr\AssignRef(new Expr\ArrayDimFetch($valuesVar), $output)); + } else { + $itemStatements[] = new Stmt\Expression(new Expr\Assign(new Expr\ArrayDimFetch($valuesVar), $output)); + } + + $statements[] = new Stmt\Foreach_($input, $loopValueVar, [ + 'stmts' => $itemStatements, + ]); + + return [$valuesVar, $statements]; + } + + /** + * {@inheritdoc} + */ + public function assignByRef(): bool + { + return false; + } + + /** + * {@inheritdoc} + */ + public function getDependencies(): array + { + return $this->itemTransformer->getDependencies(); + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/ArrayTransformerFactory.php b/src/Symfony/Component/AutoMapper/Transformer/ArrayTransformerFactory.php new file mode 100644 index 0000000000000..1c14023280d9f --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/ArrayTransformerFactory.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use Symfony\Component\AutoMapper\MapperMetadataInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * Create a decorated transformer to handle array type. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class ArrayTransformerFactory extends AbstractUniqueTypeTransformerFactory +{ + private $chainTransformerFactory; + + public function __construct(ChainTransformerFactory $chainTransformerFactory) + { + $this->chainTransformerFactory = $chainTransformerFactory; + } + + /** + * {@inheritdoc} + */ + protected function createTransformer(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface + { + if (!$sourceType->isCollection()) { + return null; + } + + if (!$targetType->isCollection()) { + return null; + } + + if (null === $sourceType->getCollectionValueType() || null === $targetType->getCollectionValueType()) { + $subItemTransformer = new CopyTransformer(); + } else { + $subItemTransformer = $this->chainTransformerFactory->getTransformer([$sourceType->getCollectionValueType()], [$targetType->getCollectionValueType()], $mapperMetadata); + } + + if (null !== $subItemTransformer) { + return new ArrayTransformer($subItemTransformer); + } + + return null; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/BuiltinTransformer.php b/src/Symfony/Component/AutoMapper/Transformer/BuiltinTransformer.php new file mode 100644 index 0000000000000..bf4f53e3c1643 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/BuiltinTransformer.php @@ -0,0 +1,135 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Expr\Cast; +use PhpParser\Node\Name; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; +use Symfony\Component\PropertyInfo\Type; + +/** + * Built in transformer to handle PHP scalar types. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class BuiltinTransformer implements TransformerInterface +{ + private const CAST_MAPPING = [ + Type::BUILTIN_TYPE_BOOL => [ + Type::BUILTIN_TYPE_INT => Cast\Int_::class, + Type::BUILTIN_TYPE_STRING => Cast\String_::class, + Type::BUILTIN_TYPE_FLOAT => Cast\Double::class, + Type::BUILTIN_TYPE_ARRAY => 'toArray', + Type::BUILTIN_TYPE_ITERABLE => 'toArray', + ], + Type::BUILTIN_TYPE_FLOAT => [ + Type::BUILTIN_TYPE_STRING => Cast\String_::class, + Type::BUILTIN_TYPE_INT => Cast\Int_::class, + Type::BUILTIN_TYPE_BOOL => Cast\Bool_::class, + Type::BUILTIN_TYPE_ARRAY => 'toArray', + Type::BUILTIN_TYPE_ITERABLE => 'toArray', + ], + Type::BUILTIN_TYPE_INT => [ + Type::BUILTIN_TYPE_FLOAT => Cast\Double::class, + Type::BUILTIN_TYPE_STRING => Cast\String_::class, + Type::BUILTIN_TYPE_BOOL => Cast\Bool_::class, + Type::BUILTIN_TYPE_ARRAY => 'toArray', + Type::BUILTIN_TYPE_ITERABLE => 'toArray', + ], + Type::BUILTIN_TYPE_ITERABLE => [ + Type::BUILTIN_TYPE_ARRAY => 'fromIteratorToArray', + ], + Type::BUILTIN_TYPE_ARRAY => [], + Type::BUILTIN_TYPE_STRING => [ + Type::BUILTIN_TYPE_ARRAY => 'toArray', + Type::BUILTIN_TYPE_ITERABLE => 'toArray', + Type::BUILTIN_TYPE_FLOAT => Cast\Double::class, + Type::BUILTIN_TYPE_INT => Cast\Int_::class, + Type::BUILTIN_TYPE_BOOL => Cast\Bool_::class, + ], + Type::BUILTIN_TYPE_CALLABLE => [], + Type::BUILTIN_TYPE_RESOURCE => [], + ]; + + /** @var Type */ + private $sourceType; + + /** @var Type[] */ + private $targetTypes; + + public function __construct(Type $sourceType, array $targetTypes) + { + $this->sourceType = $sourceType; + $this->targetTypes = $targetTypes; + } + + /** + * {@inheritdoc} + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array + { + $targetTypes = array_map(function (Type $type) { + return $type->getBuiltinType(); + }, $this->targetTypes); + + // Source type is in target => no cast + if (\in_array($this->sourceType->getBuiltinType(), $targetTypes, true)) { + return [$input, []]; + } + + // Cast needed + foreach (self::CAST_MAPPING[$this->sourceType->getBuiltinType()] as $castType => $castMethod) { + if (\in_array($castType, $targetTypes, true)) { + if (method_exists($this, $castMethod)) { + return [$this->$castMethod($input), []]; + } + + return [new $castMethod($input), []]; + } + } + + return [$input, []]; + } + + /** + * {@inheritdoc} + */ + public function getDependencies(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function assignByRef(): bool + { + return false; + } + + private function toArray(Expr $input) + { + return new Expr\Array_([new Expr\ArrayItem($input)]); + } + + private function fromIteratorToArray(Expr $input) + { + return new Expr\FuncCall(new Name('iterator_to_array'), [ + new Arg($input), + ]); + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/BuiltinTransformerFactory.php b/src/Symfony/Component/AutoMapper/Transformer/BuiltinTransformerFactory.php new file mode 100644 index 0000000000000..c751b393adec3 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/BuiltinTransformerFactory.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use Symfony\Component\AutoMapper\MapperMetadataInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class BuiltinTransformerFactory implements TransformerFactoryInterface +{ + private const BUILTIN = [ + Type::BUILTIN_TYPE_BOOL, + Type::BUILTIN_TYPE_CALLABLE, + Type::BUILTIN_TYPE_FLOAT, + Type::BUILTIN_TYPE_INT, + Type::BUILTIN_TYPE_ITERABLE, + Type::BUILTIN_TYPE_NULL, + Type::BUILTIN_TYPE_RESOURCE, + Type::BUILTIN_TYPE_STRING, + ]; + + public function getTransformer(?array $sourcesTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface + { + $nbSourcesTypes = $sourcesTypes ? \count($sourcesTypes) : 0; + + if (null === $sourcesTypes || 0 === $nbSourcesTypes || $nbSourcesTypes > 1 || !$sourcesTypes[0] instanceof Type) { + return null; + } + + /** @var Type $propertyType */ + $propertyType = $sourcesTypes[0]; + + if (\in_array($propertyType->getBuiltinType(), self::BUILTIN, true)) { + return new BuiltinTransformer($propertyType, $targetTypes); + } + + return null; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/CallbackTransformer.php b/src/Symfony/Component/AutoMapper/Transformer/CallbackTransformer.php new file mode 100644 index 0000000000000..693ff9a3ef524 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/CallbackTransformer.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Scalar; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; + +/** + * Handle custom callback transformation. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class CallbackTransformer implements TransformerInterface +{ + private $callbackName; + + public function __construct(string $callbackName) + { + $this->callbackName = $callbackName; + } + + /** + * {@inheritdoc} + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array + { + /* + * $output = $this->callbacks[$callbackName]($input); + */ + return [new Expr\FuncCall( + new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'callbacks'), new Scalar\String_($this->callbackName)), [ + new Arg($input), + ]), + [], + ]; + } + + /** + * {@inheritdoc} + */ + public function getDependencies(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function assignByRef(): bool + { + return false; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/ChainTransformerFactory.php b/src/Symfony/Component/AutoMapper/Transformer/ChainTransformerFactory.php new file mode 100644 index 0000000000000..5b40373bfec03 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/ChainTransformerFactory.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use Symfony\Component\AutoMapper\MapperMetadataInterface; + +/** + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class ChainTransformerFactory implements TransformerFactoryInterface +{ + /** @var TransformerFactoryInterface[] */ + private $factories = []; + + public function addTransformerFactory(TransformerFactoryInterface $transformerFactory) + { + $this->factories[] = $transformerFactory; + } + + /** + * {@inheritdoc} + */ + public function getTransformer(?array $sourcesTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface + { + foreach ($this->factories as $factory) { + $transformer = $factory->getTransformer($sourcesTypes, $targetTypes, $mapperMetadata); + + if (null !== $transformer) { + return $transformer; + } + } + + return null; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/CopyTransformer.php b/src/Symfony/Component/AutoMapper/Transformer/CopyTransformer.php new file mode 100644 index 0000000000000..683554b57bd93 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/CopyTransformer.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Expr; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; + +/** + * Does not do any transformation, output = input. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class CopyTransformer implements TransformerInterface +{ + /** + * {@inheritdoc} + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array + { + return [$input, []]; + } + + /** + * {@inheritdoc} + */ + public function getDependencies(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function assignByRef(): bool + { + return false; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/DateTimeImmutableToMutableTransformer.php b/src/Symfony/Component/AutoMapper/Transformer/DateTimeImmutableToMutableTransformer.php new file mode 100644 index 0000000000000..d0ecd25bb73d9 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/DateTimeImmutableToMutableTransformer.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\String_; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; + +/** + * @expiremental in 5.1 + * + * Transform DateTimeImmutable to DateTime. + * + * @author Joel Wurtz + */ +final class DateTimeImmutableToMutableTransformer implements TransformerInterface +{ + /** + * {@inheritdoc} + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array + { + return [ + new Expr\StaticCall(new Name\FullyQualified(\DateTime::class), 'createFromFormat', [ + new Arg(new String_(\DateTime::RFC3339)), + new Arg(new Expr\MethodCall($input, 'format', [ + new Arg(new String_(\DateTime::RFC3339)), + ])), + ]), + [] + ]; + } + + /** + * {@inheritdoc} + */ + public function getDependencies(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function assignByRef(): bool + { + return false; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/DateTimeMutableToImmutableTransformer.php b/src/Symfony/Component/AutoMapper/Transformer/DateTimeMutableToImmutableTransformer.php new file mode 100644 index 0000000000000..682ac787d4016 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/DateTimeMutableToImmutableTransformer.php @@ -0,0 +1,57 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; + +/** + * @expiremental in 5.1 + * + * Transform DateTime to DateTimeImmutable. + * + * @author Joel Wurtz + */ +final class DateTimeMutableToImmutableTransformer implements TransformerInterface +{ + /** + * {@inheritdoc} + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array + { + return [ + new Expr\StaticCall(new Name\FullyQualified(\DateTimeImmutable::class), 'createFromMutable', [ + new Arg($input) + ]), + [] + ]; + } + + /** + * {@inheritdoc} + */ + public function getDependencies(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function assignByRef(): bool + { + return false; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/DateTimeToStringTansformer.php b/src/Symfony/Component/AutoMapper/Transformer/DateTimeToStringTansformer.php new file mode 100644 index 0000000000000..615429df4123b --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/DateTimeToStringTansformer.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Scalar\String_; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; + +/** + * Transform a \DateTimeInterface object to a string. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class DateTimeToStringTansformer implements TransformerInterface +{ + private $format; + + public function __construct(string $format = \DateTimeInterface::RFC3339) + { + $this->format = $format; + } + + /** + * {@inheritdoc} + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array + { + return [new Expr\MethodCall($input, 'format', [ + new Arg(new String_($this->format)), + ]), []]; + } + + /** + * {@inheritdoc} + */ + public function getDependencies(): array + { + return []; + } + + /** + * {@inheritdoc} + */ + public function assignByRef(): bool + { + return false; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/DateTimeTransformerFactory.php b/src/Symfony/Component/AutoMapper/Transformer/DateTimeTransformerFactory.php new file mode 100644 index 0000000000000..4509b807b5d86 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/DateTimeTransformerFactory.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use Symfony\Component\AutoMapper\MapperMetadataInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class DateTimeTransformerFactory extends AbstractUniqueTypeTransformerFactory +{ + /** + * {@inheritdoc} + */ + protected function createTransformer(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface + { + $isSourceDate = $this->isDateTimeType($sourceType); + $isTargetDate = $this->isDateTimeType($targetType); + + if ($isSourceDate && $isTargetDate) { + return $this->createTransformerForSourceAndTarget($sourceType, $targetType); + } + + if ($isSourceDate) { + return $this->createTransformerForSource($targetType, $mapperMetadata); + } + + if ($isTargetDate) { + return $this->createTransformerForTarget($sourceType, $targetType, $mapperMetadata); + } + + return null; + } + + protected function createTransformerForSourceAndTarget(Type $sourceType, Type $targetType): ?TransformerInterface + { + $isSourceMutable = $this->isDateTimeMutable($sourceType); + $isTargetMutable = $this->isDateTimeMutable($targetType); + + if ($isSourceMutable === $isTargetMutable) { + return new CopyTransformer(); + } + + if ($isSourceMutable) { + return new DateTimeMutableToImmutableTransformer(); + } + + return new DateTimeImmutableToMutableTransformer(); + } + + protected function createTransformerForSource(Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface + { + if (Type::BUILTIN_TYPE_STRING === $targetType->getBuiltinType()) { + return new DateTimeToStringTansformer($mapperMetadata->getDateTimeFormat()); + } + + return null; + } + + protected function createTransformerForTarget(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface + { + if (Type::BUILTIN_TYPE_STRING === $sourceType->getBuiltinType()) { + return new StringToDateTimeTransformer($this->getClassName($targetType), $mapperMetadata->getDateTimeFormat()); + } + + return null; + } + + private function isDateTimeType(Type $type): bool + { + if (Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) { + return false; + } + + if (\DateTimeInterface::class !== $type->getClassName() && !is_subclass_of($type->getClassName(), \DateTimeInterface::class)) { + return false; + } + + return true; + } + + private function getClassName(Type $type): ?string + { + if (\DateTimeInterface::class !== $type->getClassName()) { + return \DateTimeImmutable::class; + } + + return $type->getClassName(); + } + + private function isDateTimeMutable(Type $type): bool + { + if (\DateTime::class !== $type->getClassName() && !is_subclass_of($type->getClassName(), \DateTime::class)) { + return false; + } + + return true; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/MapperDependency.php b/src/Symfony/Component/AutoMapper/Transformer/MapperDependency.php new file mode 100644 index 0000000000000..cb13dda2c1cf0 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/MapperDependency.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +/** + * Represent a dependency on a mapper (allow to inject sub mappers). + * + * @internal + * + * @author Joel Wurtz + */ +final class MapperDependency +{ + private $name; + + private $source; + + private $target; + + public function __construct(string $name, string $source, string $target) + { + $this->name = $name; + $this->source = $source; + $this->target = $target; + } + + public function getName(): string + { + return $this->name; + } + + public function getSource(): string + { + return $this->source; + } + + public function getTarget(): string + { + return $this->target; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/MultipleTransformer.php b/src/Symfony/Component/AutoMapper/Transformer/MultipleTransformer.php new file mode 100644 index 0000000000000..8703a482e3bb5 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/MultipleTransformer.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PhpParser\Node\Stmt; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; +use Symfony\Component\PropertyInfo\Type; + +/** + * Multiple transformer decorator. + * + * Decorate transformers with condition to handle property with multiples source types + * It will always use the first target type possible for transformation + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class MultipleTransformer implements TransformerInterface +{ + private const CONDITION_MAPPING = [ + Type::BUILTIN_TYPE_BOOL => 'is_bool', + Type::BUILTIN_TYPE_INT => 'is_int', + Type::BUILTIN_TYPE_FLOAT => 'is_float', + Type::BUILTIN_TYPE_STRING => 'is_string', + Type::BUILTIN_TYPE_NULL => 'is_null', + Type::BUILTIN_TYPE_ARRAY => 'is_array', + Type::BUILTIN_TYPE_OBJECT => 'is_object', + Type::BUILTIN_TYPE_RESOURCE => 'is_resource', + Type::BUILTIN_TYPE_CALLABLE => 'is_callable', + Type::BUILTIN_TYPE_ITERABLE => 'is_iterable', + ]; + + private $transformers = []; + + public function __construct(array $transformers) + { + $this->transformers = $transformers; + } + + /** + * {@inheritdoc} + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array + { + $output = new Expr\Variable($uniqueVariableScope->getUniqueName('value')); + $statements = [ + new Stmt\Expression(new Expr\Assign($output, $input)), + ]; + + foreach ($this->transformers as $transformerData) { + $transformer = $transformerData['transformer']; + $type = $transformerData['type']; + + [$transformerOutput, $transformerStatements] = $transformer->transform($input, $propertyMapping, $uniqueVariableScope); + + $assignClass = $transformer->assignByRef() ? Expr\AssignRef::class : Expr\Assign::class; + $statements[] = new Stmt\If_( + new Expr\FuncCall( + new Name(self::CONDITION_MAPPING[$type->getBuiltinType()]), + [ + new Arg($input), + ] + ), + [ + 'stmts' => array_merge( + $transformerStatements, [ + new Stmt\Expression(new $assignClass($output, $transformerOutput)), + ] + ), + ] + ); + } + + return [$output, $statements]; + } + + /** + * {@inheritdoc} + */ + public function assignByRef(): bool + { + return false; + } + + /** + * {@inheritdoc} + */ + public function getDependencies(): array + { + $dependencies = []; + + foreach ($this->transformers as $transformerData) { + $dependencies = array_merge($dependencies, $transformerData['transformer']->getDependencies()); + } + + return $dependencies; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/MultipleTransformerFactory.php b/src/Symfony/Component/AutoMapper/Transformer/MultipleTransformerFactory.php new file mode 100644 index 0000000000000..72e28ab432f26 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/MultipleTransformerFactory.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use Symfony\Component\AutoMapper\MapperMetadataInterface; + +/** + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class MultipleTransformerFactory implements TransformerFactoryInterface +{ + private $chainTransformerFactory; + + public function __construct(ChainTransformerFactory $chainTransformerFactory) + { + $this->chainTransformerFactory = $chainTransformerFactory; + } + + /** + * {@inheritdoc} + */ + public function getTransformer(?array $sourcesTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface + { + if (null === $sourcesTypes || \count($sourcesTypes) <= 1) { + return null; + } + + $transformers = []; + + foreach ($sourcesTypes as $sourceType) { + $transformer = $this->chainTransformerFactory->getTransformer([$sourceType], $targetTypes, $mapperMetadata); + + if (null !== $transformer) { + $transformers[] = [ + 'transformer' => $transformer, + 'type' => $sourceType + ]; + } + } + + if (\count($transformers) > 1) { + return new MultipleTransformer($transformers); + } + + if (\count($transformers) === 1) { + return $transformers[0]['transformer']; + } + + return null; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/NullableTransformer.php b/src/Symfony/Component/AutoMapper/Transformer/NullableTransformer.php new file mode 100644 index 0000000000000..e880bd5601181 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/NullableTransformer.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PhpParser\Node\Stmt; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; + +/** + * Tansformer decorator to handle null values. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class NullableTransformer implements TransformerInterface +{ + private $itemTransformer; + private $isTargetNullable; + + public function __construct(TransformerInterface $itemTransformer, bool $isTargetNullable) + { + $this->itemTransformer = $itemTransformer; + $this->isTargetNullable = $isTargetNullable; + } + + /** + * {@inheritdoc} + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array + { + [$output, $itemStatements] = $this->itemTransformer->transform($input, $propertyMapping, $uniqueVariableScope); + + $newOutput = null; + $statements = []; + $assignClass = $this->itemTransformer->assignByRef() ? Expr\AssignRef::class : Expr\Assign::class; + + if ($this->isTargetNullable) { + $newOutput = new Expr\Variable($uniqueVariableScope->getUniqueName('value')); + $statements[] = new Stmt\Expression(new Expr\Assign($newOutput, new Expr\ConstFetch(new Name('null')))); + $itemStatements[] = new Stmt\Expression(new $assignClass($newOutput, $output)); + } + + $statements[] = new Stmt\If_(new Expr\BinaryOp\NotIdentical(new Expr\ConstFetch(new Name('null')), $input), [ + 'stmts' => $itemStatements, + ]); + + return [$newOutput ?? $output, $statements]; + } + + /** + * {@inheritdoc} + */ + public function getDependencies(): array + { + return $this->itemTransformer->getDependencies(); + } + + /** + * {@inheritdoc} + */ + public function assignByRef(): bool + { + return false; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/NullableTransformerFactory.php b/src/Symfony/Component/AutoMapper/Transformer/NullableTransformerFactory.php new file mode 100644 index 0000000000000..b2e43c7b7383b --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/NullableTransformerFactory.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use Symfony\Component\AutoMapper\MapperMetadataInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class NullableTransformerFactory implements TransformerFactoryInterface +{ + private $chainTransformerFactory; + + public function __construct(ChainTransformerFactory $chainTransformerFactory) + { + $this->chainTransformerFactory = $chainTransformerFactory; + } + + /** + * {@inheritdoc} + */ + public function getTransformer(?array $sourcesTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface + { + $nbSourcesTypes = $sourcesTypes ? \count($sourcesTypes) : 0; + + if (null === $sourcesTypes || 0 === $nbSourcesTypes || $nbSourcesTypes > 1) { + return null; + } + + /** @var Type $propertyType */ + $propertyType = $sourcesTypes[0]; + + if (!$propertyType->isNullable()) { + return null; + } + + $isTargetNullable = false; + + foreach ($targetTypes as $targetType) { + if ($targetType->isNullable()) { + $isTargetNullable = true; + + break; + } + } + + $subTransformer = $this->chainTransformerFactory->getTransformer([new Type( + $propertyType->getBuiltinType(), + false, + $propertyType->getClassName(), + $propertyType->isCollection(), + $propertyType->getCollectionKeyType(), + $propertyType->getCollectionValueType() + )], $targetTypes, $mapperMetadata); + + if (null === $subTransformer) { + return null; + } + + // Remove nullable property here to avoid infinite loop + return new NullableTransformer($subTransformer, $isTargetNullable); + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/ObjectTransformer.php b/src/Symfony/Component/AutoMapper/Transformer/ObjectTransformer.php new file mode 100644 index 0000000000000..12d848a0cf425 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/ObjectTransformer.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; +use Symfony\Component\AutoMapper\MapperContext; +use Symfony\Component\PropertyInfo\Type; + +/** + * Transform to an object which can be mapped by AutoMapper (sub mapping). + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class ObjectTransformer implements TransformerInterface +{ + private $sourceType; + + private $targetType; + + public function __construct(Type $sourceType, Type $targetType) + { + $this->sourceType = $sourceType; + $this->targetType = $targetType; + } + + /** + * {@inheritdoc} + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array + { + $mapperName = $this->getDependencyName(); + + return [new Expr\MethodCall(new Expr\ArrayDimFetch( + new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'), + new Scalar\String_($mapperName) + ), 'map', [ + new Arg($input), + new Arg(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'withNewContext', [ + new Arg(new Expr\Variable('context')), + new Arg(new Scalar\String_($propertyMapping->getProperty())), + ])), + ]), []]; + } + + /** + * {@inheritdoc} + */ + public function assignByRef(): bool + { + return true; + } + + /** + * {@inheritdoc} + */ + public function getDependencies(): array + { + return [new MapperDependency($this->getDependencyName(), $this->getSource(), $this->getTarget())]; + } + + private function getDependencyName(): string + { + return 'Mapper_'.$this->getSource().'_'.$this->getTarget(); + } + + private function getSource(): string + { + $sourceTypeName = 'array'; + + if (Type::BUILTIN_TYPE_OBJECT === $this->sourceType->getBuiltinType()) { + $sourceTypeName = $this->sourceType->getClassName(); + } + + return $sourceTypeName; + } + + private function getTarget(): string + { + $targetTypeName = 'array'; + + if (Type::BUILTIN_TYPE_OBJECT === $this->targetType->getBuiltinType()) { + $targetTypeName = $this->targetType->getClassName(); + } + + return $targetTypeName; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/ObjectTransformerFactory.php b/src/Symfony/Component/AutoMapper/Transformer/ObjectTransformerFactory.php new file mode 100644 index 0000000000000..909dacccb0c4b --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/ObjectTransformerFactory.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use Symfony\Component\AutoMapper\AutoMapperRegistryInterface; +use Symfony\Component\AutoMapper\MapperMetadataInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class ObjectTransformerFactory extends AbstractUniqueTypeTransformerFactory +{ + private $autoMapper; + + public function __construct(AutoMapperRegistryInterface $autoMapper) + { + $this->autoMapper = $autoMapper; + } + + /** + * {@inheritdoc} + */ + protected function createTransformer(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface + { + // Only deal with source type being an object or an array that is not a collection + if (!$this->isObjectType($sourceType) || !$this->isObjectType($targetType)) { + return null; + } + + $sourceTypeName = 'array'; + $targetTypeName = 'array'; + + if (Type::BUILTIN_TYPE_OBJECT === $sourceType->getBuiltinType()) { + $sourceTypeName = $sourceType->getClassName(); + } + + if (Type::BUILTIN_TYPE_OBJECT === $targetType->getBuiltinType()) { + $targetTypeName = $targetType->getClassName(); + } + + if (null !== $sourceTypeName && null !== $targetTypeName && $this->autoMapper->hasMapper($sourceTypeName, $targetTypeName)) { + return new ObjectTransformer($sourceType, $targetType); + } + + return null; + } + + private function isObjectType(Type $type): bool + { + return + Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() + || + (Type::BUILTIN_TYPE_ARRAY === $type->getBuiltinType() && !$type->isCollection()) + ; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/StringToDateTimeTransformer.php b/src/Symfony/Component/AutoMapper/Transformer/StringToDateTimeTransformer.php new file mode 100644 index 0000000000000..870f9fc0afc81 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/StringToDateTimeTransformer.php @@ -0,0 +1,66 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Arg; +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PhpParser\Node\Scalar\String_; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; + +/** + * Transform a string to a \DateTimeInterface object. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class StringToDateTimeTransformer implements TransformerInterface +{ + private $className; + + private $format; + + public function __construct(string $className, string $format = \DateTimeInterface::RFC3339) + { + $this->className = $className; + $this->format = $format; + } + + /** + * {@inheritdoc} + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array + { + return [new Expr\StaticCall(new Name\FullyQualified($this->className), 'createFromFormat', [ + new Arg(new String_($this->format)), + new Arg($input), + ]), []]; + } + + /** + * {@inheritdoc} + */ + public function assignByRef(): bool + { + return false; + } + + /** + * {@inheritdoc} + */ + public function getDependencies(): array + { + return []; + } +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/TransformerFactoryInterface.php b/src/Symfony/Component/AutoMapper/Transformer/TransformerFactoryInterface.php new file mode 100644 index 0000000000000..61c5809cdb8f7 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/TransformerFactoryInterface.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use Symfony\Component\AutoMapper\MapperMetadataInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * Create transformer. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +interface TransformerFactoryInterface +{ + /** + * Get transformer to use when mapping from an array of type to another array of type. + * + * @param Type[] $sourcesTypes + * @param Type[] $targetTypes + */ + public function getTransformer(?array $sourcesTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface; +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/TransformerInterface.php b/src/Symfony/Component/AutoMapper/Transformer/TransformerInterface.php new file mode 100644 index 0000000000000..3eb4ff4fada10 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/TransformerInterface.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use PhpParser\Node\Expr; +use PhpParser\Node\Stmt; +use Symfony\Component\AutoMapper\Extractor\PropertyMapping; +use Symfony\Component\AutoMapper\Generator\UniqueVariableScope; + +/** + * Transformer tell how to transform a property mapping. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +interface TransformerInterface +{ + /** + * Get AST output and expressions for transforming a property mapping given an input. + * + * @return [Expr, Stmt[]] First value is the output expression, second value is an array of stmt needed to get the output + */ + public function transform(Expr $input, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array; + + /** + * Get dependencies for this transformer. + * + * @return MapperDependency[] + */ + public function getDependencies(): array; + + /** + * Should the resulting output be assigned by ref. + */ + public function assignByRef(): bool; +} diff --git a/src/Symfony/Component/AutoMapper/Transformer/UniqueTypeTransformerFactory.php b/src/Symfony/Component/AutoMapper/Transformer/UniqueTypeTransformerFactory.php new file mode 100644 index 0000000000000..9d985961ba463 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/Transformer/UniqueTypeTransformerFactory.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\AutoMapper\Transformer; + +use Symfony\Component\AutoMapper\MapperMetadataInterface; + +/** + * Reduce array of type to only one type on source and target. + * + * @expiremental in 5.1 + * + * @author Joel Wurtz + */ +final class UniqueTypeTransformerFactory implements TransformerFactoryInterface +{ + private $chainTransformerFactory; + + public function __construct(ChainTransformerFactory $chainTransformerFactory) + { + $this->chainTransformerFactory = $chainTransformerFactory; + } + + /** + * {@inheritdoc} + */ + public function getTransformer(?array $sourcesTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface + { + $nbSourcesTypes = $sourcesTypes ? \count($sourcesTypes) : 0; + $nbTargetsTypes = $targetTypes ? \count($targetTypes) : 0; + + if (null === $sourcesTypes || 0 === $nbSourcesTypes || $nbSourcesTypes > 1) { + return null; + } + + if (null === $targetTypes || $nbTargetsTypes <= 1) { + return null; + } + + foreach ($targetTypes as $targetType) { + if (null === $targetType) { + continue; + } + + $transformer = $this->chainTransformerFactory->getTransformer($sourcesTypes, [$targetType], $mapperMetadata); + + if (null !== $transformer) { + return $transformer; + } + } + + return null; + } +} diff --git a/src/Symfony/Component/AutoMapper/composer.json b/src/Symfony/Component/AutoMapper/composer.json new file mode 100644 index 0000000000000..ee72bdde5fe6a --- /dev/null +++ b/src/Symfony/Component/AutoMapper/composer.json @@ -0,0 +1,46 @@ +{ + "name": "symfony/auto-mapper", + "type": "library", + "description": "Symfony AutoMapper Component", + "keywords": [], + "homepage": "https://symfony.com", + "license": "MIT", + "authors": [ + { + "name": "Joel Wurtz", + "email": "jwurtz@jolicode.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "require": { + "php": "^7.2.5", + "nikic/php-parser": "^4.0", + "symfony/property-info": "~5.1" + }, + "require-dev": { + "doctrine/annotations": "~1.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0", + "symfony/serializer": "^4.2" + }, + "suggest": { + "symfony/serializer": "Allow to bridge mappers to normalizer and denormalizer" + }, + "conflict": { + "symfony/serializer": "<4.2" + }, + "autoload": { + "psr-4": { "Symfony\\Component\\AutoMapper\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "5.1-dev" + } + } +} diff --git a/src/Symfony/Component/AutoMapper/phpunit.xml.dist b/src/Symfony/Component/AutoMapper/phpunit.xml.dist new file mode 100644 index 0000000000000..4e17ef5e45ed5 --- /dev/null +++ b/src/Symfony/Component/AutoMapper/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + + + + ./Tests/ + + + + + + ./ + + ./Tests + ./vendor + + + +