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 + + + + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy