├── .github └── workflows │ └── read-only.yml ├── Attribute └── MapToContext.php ├── AutoMapper.php ├── AutoMapperInterface.php ├── AutoMapperRegistryInterface.php ├── Exception ├── CircularReferenceException.php ├── CompileException.php ├── InvalidMappingException.php ├── NoMappingFoundException.php ├── ReadOnlyTargetException.php └── RuntimeException.php ├── Extractor ├── FromSourceMappingExtractor.php ├── FromTargetMappingExtractor.php ├── MapToContextPropertyInfoExtractorDecorator.php ├── MappingExtractor.php ├── MappingExtractorInterface.php ├── PropertyMapping.php ├── ReadAccessor.php ├── SourceTargetMappingExtractor.php └── WriteMutator.php ├── GeneratedMapper.php ├── Generator ├── Generator.php └── UniqueVariableScope.php ├── LICENSE ├── Loader ├── ClassLoaderInterface.php ├── EvalLoader.php └── FileLoader.php ├── MapperContext.php ├── MapperGeneratorMetadataFactory.php ├── MapperGeneratorMetadataFactoryInterface.php ├── MapperGeneratorMetadataInterface.php ├── MapperGeneratorMetadataRegistryInterface.php ├── MapperInterface.php ├── MapperMetadata.php ├── MapperMetadataInterface.php ├── Normalizer └── AutoMapperNormalizer.php ├── README.md ├── Transformer ├── AbstractArrayTransformer.php ├── AbstractUniqueTypeTransformerFactory.php ├── ArrayTransformer.php ├── ArrayTransformerFactory.php ├── AssignedByReferenceTransformerInterface.php ├── BuiltinTransformer.php ├── BuiltinTransformerFactory.php ├── CallbackTransformer.php ├── ChainTransformerFactory.php ├── CopyEnumTransformer.php ├── CopyTransformer.php ├── DateTimeImmutableToMutableTransformer.php ├── DateTimeMutableToImmutableTransformer.php ├── DateTimeToStringTransformer.php ├── DateTimeTransformerFactory.php ├── DependentTransformerInterface.php ├── DictionaryTransformer.php ├── EnumTransformerFactory.php ├── MapperDependency.php ├── MultipleTransformer.php ├── MultipleTransformerFactory.php ├── NullableTransformer.php ├── NullableTransformerFactory.php ├── ObjectTransformer.php ├── ObjectTransformerFactory.php ├── PrioritizedTransformerFactoryInterface.php ├── SourceEnumTransformer.php ├── StringToDateTimeTransformer.php ├── StringToSymfonyUidTransformer.php ├── SymfonyUidCopyTransformer.php ├── SymfonyUidToStringTransformer.php ├── SymfonyUidTransformerFactory.php ├── TargetEnumTransformer.php ├── TransformerFactoryInterface.php ├── TransformerInterface.php └── UniqueTypeTransformerFactory.php └── composer.json /.github/workflows/read-only.yml: -------------------------------------------------------------------------------- 1 | name: read-only 2 | on: pull_request_target 3 | jobs: 4 | write_message: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: superbrothers/close-pull-request@v3 8 | with: 9 | comment: | 10 | Hey! 11 | Thanks for your PR and contributing to Jane! 12 | But this repository is read-only, if you want to contribute to Jane, please submit your PR to https://github.com/janephp/janephp. 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /Attribute/MapToContext.php: -------------------------------------------------------------------------------- 1 | contextName; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /AutoMapper.php: -------------------------------------------------------------------------------- 1 | 39 | */ 40 | class AutoMapper implements AutoMapperInterface, AutoMapperRegistryInterface, MapperGeneratorMetadataRegistryInterface 41 | { 42 | /** @var MapperGeneratorMetadataInterface[] */ 43 | private $metadata = []; 44 | 45 | /** @var GeneratedMapper[] */ 46 | private $mapperRegistry = []; 47 | 48 | /** @var ClassLoaderInterface */ 49 | private $classLoader; 50 | 51 | /** @var MapperGeneratorMetadataFactoryInterface|null */ 52 | private $mapperConfigurationFactory; 53 | 54 | /** @var ChainTransformerFactory */ 55 | private $chainTransformerFactory; 56 | 57 | public function __construct(ClassLoaderInterface $classLoader, ChainTransformerFactory $chainTransformerFactory, MapperGeneratorMetadataFactoryInterface $mapperConfigurationFactory = null) 58 | { 59 | $this->classLoader = $classLoader; 60 | $this->mapperConfigurationFactory = $mapperConfigurationFactory; 61 | $this->chainTransformerFactory = $chainTransformerFactory; 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function register(MapperGeneratorMetadataInterface $metadata): void 68 | { 69 | $this->metadata[$metadata->getSource()][$metadata->getTarget()] = $metadata; 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function getMapper(string $source, string $target): MapperInterface 76 | { 77 | $metadata = $this->getMetadata($source, $target); 78 | 79 | if (null === $metadata) { 80 | throw new NoMappingFoundException('No mapping found for source ' . $source . ' and target ' . $target); 81 | } 82 | 83 | $className = $metadata->getMapperClassName(); 84 | 85 | if (\array_key_exists($className, $this->mapperRegistry)) { 86 | return $this->mapperRegistry[$className]; 87 | } 88 | 89 | if (!class_exists($className)) { 90 | $this->classLoader->loadClass($metadata); 91 | } 92 | 93 | $this->mapperRegistry[$className] = new $className(); 94 | $this->mapperRegistry[$className]->injectMappers($this); 95 | 96 | foreach ($metadata->getCallbacks() as $property => $callback) { 97 | $this->mapperRegistry[$className]->addCallback($property, $callback); 98 | } 99 | 100 | return $this->mapperRegistry[$className]; 101 | } 102 | 103 | /** 104 | * {@inheritdoc} 105 | */ 106 | public function hasMapper(string $source, string $target): bool 107 | { 108 | return null !== $this->getMetadata($source, $target); 109 | } 110 | 111 | /** 112 | * {@inheritdoc} 113 | */ 114 | public function map($sourceData, $targetData, array $context = []) 115 | { 116 | $source = null; 117 | $target = null; 118 | 119 | if (null === $sourceData) { 120 | return null; 121 | } 122 | 123 | if (\is_object($sourceData)) { 124 | $source = \get_class($sourceData); 125 | } elseif (\is_array($sourceData)) { 126 | $source = 'array'; 127 | } 128 | 129 | if (null === $source) { 130 | throw new NoMappingFoundException('Cannot map this value, source is neither an object or an array.'); 131 | } 132 | 133 | if (\is_object($targetData)) { 134 | $target = \get_class($targetData); 135 | $context[MapperContext::TARGET_TO_POPULATE] = $targetData; 136 | } elseif (\is_array($targetData)) { 137 | $target = 'array'; 138 | $context[MapperContext::TARGET_TO_POPULATE] = $targetData; 139 | } elseif (\is_string($targetData)) { 140 | $target = $targetData; 141 | } 142 | 143 | if (null === $target) { 144 | throw new NoMappingFoundException('Cannot map this value, target is neither an object or an array.'); 145 | } 146 | 147 | if ('array' === $source && 'array' === $target) { 148 | throw new NoMappingFoundException('Cannot map this value, both source and target are array.'); 149 | } 150 | 151 | return $this->getMapper($source, $target)->map($sourceData, $context); 152 | } 153 | 154 | /** 155 | * {@inheritdoc} 156 | */ 157 | public function getMetadata(string $source, string $target): ?MapperGeneratorMetadataInterface 158 | { 159 | if (!isset($this->metadata[$source][$target])) { 160 | if (null === $this->mapperConfigurationFactory) { 161 | return null; 162 | } 163 | 164 | $this->register($this->mapperConfigurationFactory->create($this, $source, $target)); 165 | } 166 | 167 | return $this->metadata[$source][$target]; 168 | } 169 | 170 | /** 171 | * {@inheritdoc} 172 | */ 173 | public function bindTransformerFactory(TransformerFactoryInterface $transformerFactory): void 174 | { 175 | if (!$this->chainTransformerFactory->hasTransformerFactory($transformerFactory)) { 176 | $this->chainTransformerFactory->addTransformerFactory($transformerFactory); 177 | } 178 | } 179 | 180 | /** 181 | * Create an automapper. 182 | */ 183 | public static function create( 184 | bool $private = true, 185 | ClassLoaderInterface $loader = null, 186 | AdvancedNameConverterInterface $nameConverter = null, 187 | string $classPrefix = 'Mapper_', 188 | bool $attributeChecking = true, 189 | bool $autoRegister = true, 190 | string $dateTimeFormat = \DateTime::RFC3339, 191 | bool $allowReadOnlyTargetToPopulate = false 192 | ): self { 193 | $classMetadataFactory = new ClassMetadataFactory(new AnnotationLoader(new AnnotationReader())); 194 | 195 | if (null === $loader) { 196 | $loader = new EvalLoader(new Generator( 197 | (new ParserFactory())->create(ParserFactory::PREFER_PHP7), 198 | new ClassDiscriminatorFromClassMetadata($classMetadataFactory), 199 | $allowReadOnlyTargetToPopulate 200 | )); 201 | } 202 | 203 | $flags = ReflectionExtractor::ALLOW_PUBLIC; 204 | 205 | if ($private) { 206 | $flags |= ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE; 207 | } 208 | 209 | $reflectionExtractor = new ReflectionExtractor( 210 | null, 211 | null, 212 | null, 213 | true, 214 | $flags 215 | ); 216 | 217 | $phpDocExtractor = new PhpDocExtractor(); 218 | $propertyInfoExtractor = new PropertyInfoExtractor( 219 | [$reflectionExtractor], 220 | [$phpDocExtractor, $reflectionExtractor], 221 | [$reflectionExtractor], 222 | [new MapToContextPropertyInfoExtractorDecorator($reflectionExtractor)] 223 | ); 224 | 225 | $transformerFactory = new ChainTransformerFactory(); 226 | $sourceTargetMappingExtractor = new SourceTargetMappingExtractor( 227 | $propertyInfoExtractor, 228 | new MapToContextPropertyInfoExtractorDecorator($reflectionExtractor), 229 | $reflectionExtractor, 230 | $transformerFactory, 231 | $classMetadataFactory 232 | ); 233 | 234 | $fromTargetMappingExtractor = new FromTargetMappingExtractor( 235 | $propertyInfoExtractor, 236 | $reflectionExtractor, 237 | $reflectionExtractor, 238 | $transformerFactory, 239 | $classMetadataFactory, 240 | $nameConverter 241 | ); 242 | 243 | $fromSourceMappingExtractor = new FromSourceMappingExtractor( 244 | $propertyInfoExtractor, 245 | new MapToContextPropertyInfoExtractorDecorator($reflectionExtractor), 246 | $reflectionExtractor, 247 | $transformerFactory, 248 | $classMetadataFactory, 249 | $nameConverter 250 | ); 251 | 252 | $autoMapper = $autoRegister ? new self($loader, $transformerFactory, new MapperGeneratorMetadataFactory( 253 | $sourceTargetMappingExtractor, 254 | $fromSourceMappingExtractor, 255 | $fromTargetMappingExtractor, 256 | $classPrefix, 257 | $attributeChecking, 258 | $dateTimeFormat 259 | )) : new self($loader, $transformerFactory); 260 | 261 | $transformerFactory->addTransformerFactory(new MultipleTransformerFactory($transformerFactory)); 262 | $transformerFactory->addTransformerFactory(new NullableTransformerFactory($transformerFactory)); 263 | $transformerFactory->addTransformerFactory(new UniqueTypeTransformerFactory($transformerFactory)); 264 | $transformerFactory->addTransformerFactory(new DateTimeTransformerFactory()); 265 | $transformerFactory->addTransformerFactory(new BuiltinTransformerFactory()); 266 | $transformerFactory->addTransformerFactory(new ArrayTransformerFactory($transformerFactory)); 267 | $transformerFactory->addTransformerFactory(new ObjectTransformerFactory($autoMapper)); 268 | $transformerFactory->addTransformerFactory(new EnumTransformerFactory()); 269 | 270 | if (class_exists(AbstractUid::class)) { 271 | $transformerFactory->addTransformerFactory(new SymfonyUidTransformerFactory()); 272 | } 273 | 274 | return $autoMapper; 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /AutoMapperInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface AutoMapperInterface 11 | { 12 | /** 13 | * Maps data from a source to a target. 14 | * 15 | * @param array|object $source Any data object, which may be an object or an array 16 | * @param string|array|object $target To which type of data, or data, the source should be mapped 17 | * @param array $context Mapper context 18 | * 19 | * @return array|object The mapped object 20 | */ 21 | public function map($source, $target, array $context = []); 22 | } 23 | -------------------------------------------------------------------------------- /AutoMapperRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface AutoMapperRegistryInterface 11 | { 12 | /** 13 | * Gets a specific mapper for a source type and a target type. 14 | * 15 | * @param string $source Source type 16 | * @param string $target Target type 17 | * 18 | * @return MapperInterface return associated mapper 19 | */ 20 | public function getMapper(string $source, string $target): MapperInterface; 21 | 22 | /** 23 | * Does a specific mapper exist. 24 | * 25 | * @param string $source Source type 26 | * @param string $target Target type 27 | */ 28 | public function hasMapper(string $source, string $target): bool; 29 | } 30 | -------------------------------------------------------------------------------- /Exception/CircularReferenceException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jane\Component\AutoMapper\Exception; 13 | 14 | /** 15 | * @author Joel Wurtz 16 | */ 17 | class CircularReferenceException extends RuntimeException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/CompileException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jane\Component\AutoMapper\Exception; 13 | 14 | /** 15 | * @author Joel Wurtz 16 | */ 17 | class CompileException extends RuntimeException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/InvalidMappingException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jane\Component\AutoMapper\Exception; 13 | 14 | /** 15 | * @author Joel Wurtz 16 | */ 17 | class InvalidMappingException extends RuntimeException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/NoMappingFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Jane\Component\AutoMapper\Exception; 13 | 14 | /** 15 | * @author Joel Wurtz 16 | */ 17 | class NoMappingFoundException extends RuntimeException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /Exception/ReadOnlyTargetException.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ReadOnlyTargetException extends RuntimeException 11 | { 12 | public function __construct(int $code = 0, ?Throwable $previous = null) 13 | { 14 | parent::__construct(sprintf('Cannot use readonly class as an object to populate. You can opt-out this behavior by using the context "%s"', MapperContext::ALLOW_READONLY_TARGET_TO_POPULATE), $code, $previous); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Exception/RuntimeException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class RuntimeException extends \RuntimeException 9 | { 10 | } 11 | -------------------------------------------------------------------------------- /Extractor/FromSourceMappingExtractor.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | final class FromSourceMappingExtractor extends MappingExtractor 23 | { 24 | private const ALLOWED_TARGETS = ['array', \stdClass::class]; 25 | 26 | private $nameConverter; 27 | 28 | public function __construct(PropertyInfoExtractorInterface $propertyInfoExtractor, PropertyReadInfoExtractorInterface $readInfoExtractor, PropertyWriteInfoExtractorInterface $writeInfoExtractor, TransformerFactoryInterface $transformerFactory, ClassMetadataFactoryInterface $classMetadataFactory = null, AdvancedNameConverterInterface $nameConverter = null) 29 | { 30 | parent::__construct($propertyInfoExtractor, $readInfoExtractor, $writeInfoExtractor, $transformerFactory, $classMetadataFactory); 31 | 32 | $this->nameConverter = $nameConverter; 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getPropertiesMapping(MapperMetadataInterface $mapperMetadata): array 39 | { 40 | $sourceProperties = $this->propertyInfoExtractor->getProperties($mapperMetadata->getSource()); 41 | 42 | if (!\in_array($mapperMetadata->getTarget(), self::ALLOWED_TARGETS, true)) { 43 | throw new InvalidMappingException('Only array or stdClass are accepted as a target'); 44 | } 45 | 46 | if (null === $sourceProperties) { 47 | return []; 48 | } 49 | 50 | $sourceProperties = array_unique($sourceProperties); 51 | $mapping = []; 52 | 53 | foreach ($sourceProperties as $property) { 54 | if (!$this->propertyInfoExtractor->isReadable($mapperMetadata->getSource(), $property)) { 55 | continue; 56 | } 57 | 58 | $sourceTypes = $this->propertyInfoExtractor->getTypes($mapperMetadata->getSource(), $property); 59 | 60 | if (null === $sourceTypes) { 61 | $sourceTypes = [new Type(Type::BUILTIN_TYPE_NULL)]; // if no types found, we force a null type 62 | } 63 | 64 | $targetTypes = []; 65 | 66 | foreach ($sourceTypes as $type) { 67 | $targetTypes[] = $this->transformType($mapperMetadata->getTarget(), $type); 68 | } 69 | 70 | $transformer = $this->transformerFactory->getTransformer($sourceTypes, $targetTypes, $mapperMetadata); 71 | 72 | if (null === $transformer) { 73 | continue; 74 | } 75 | 76 | $mapping[] = new PropertyMapping( 77 | $this->getReadAccessor($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property), 78 | $this->getWriteMutator($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property), 79 | null, 80 | $transformer, 81 | $property, 82 | false, 83 | $this->getGroups($mapperMetadata->getSource(), $property), 84 | $this->getGroups($mapperMetadata->getTarget(), $property), 85 | $this->getMaxDepth($mapperMetadata->getSource(), $property), 86 | $this->isIgnoredProperty($mapperMetadata->getSource(), $property), 87 | $this->isIgnoredProperty($mapperMetadata->getTarget(), $property) 88 | ); 89 | } 90 | 91 | return $mapping; 92 | } 93 | 94 | private function transformType(string $target, Type $type = null): ?Type 95 | { 96 | if (null === $type) { 97 | return null; 98 | } 99 | 100 | $builtinType = $type->getBuiltinType(); 101 | $className = $type->getClassName(); 102 | 103 | if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && \stdClass::class !== $type->getClassName()) { 104 | $builtinType = 'array' === $target ? Type::BUILTIN_TYPE_ARRAY : Type::BUILTIN_TYPE_OBJECT; 105 | $className = 'array' === $target ? null : \stdClass::class; 106 | } 107 | 108 | // Use string for datetime 109 | if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && (\DateTimeInterface::class === $type->getClassName() || is_subclass_of($type->getClassName(), \DateTimeInterface::class))) { 110 | $builtinType = 'string'; 111 | } 112 | 113 | $collectionKeyTypes = $type->getCollectionKeyTypes(); 114 | $collectionValueTypes = $type->getCollectionValueTypes(); 115 | 116 | return new Type( 117 | $builtinType, 118 | $type->isNullable(), 119 | $className, 120 | $type->isCollection(), 121 | $this->transformType($target, $collectionKeyTypes[0] ?? null), 122 | $this->transformType($target, $collectionValueTypes[0] ?? null) 123 | ); 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function getWriteMutator(string $source, string $target, string $property, array $context = []): WriteMutator 130 | { 131 | if (null !== $this->nameConverter) { 132 | $property = $this->nameConverter->normalize($property, $source, $target); 133 | } 134 | 135 | $targetMutator = new WriteMutator(WriteMutator::TYPE_ARRAY_DIMENSION, $property, false); 136 | 137 | if (\stdClass::class === $target) { 138 | $targetMutator = new WriteMutator(WriteMutator::TYPE_PROPERTY, $property, false); 139 | } 140 | 141 | return $targetMutator; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Extractor/FromTargetMappingExtractor.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | final class FromTargetMappingExtractor extends MappingExtractor 24 | { 25 | private const ALLOWED_SOURCES = ['array', \stdClass::class]; 26 | 27 | private $nameConverter; 28 | 29 | public function __construct(PropertyInfoExtractorInterface $propertyInfoExtractor, PropertyReadInfoExtractorInterface $readInfoExtractor, PropertyWriteInfoExtractorInterface $writeInfoExtractor, TransformerFactoryInterface $transformerFactory, ClassMetadataFactoryInterface $classMetadataFactory = null, AdvancedNameConverterInterface $nameConverter = null) 30 | { 31 | parent::__construct($propertyInfoExtractor, $readInfoExtractor, $writeInfoExtractor, $transformerFactory, $classMetadataFactory); 32 | 33 | $this->nameConverter = $nameConverter; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function getPropertiesMapping(MapperMetadataInterface $mapperMetadata): array 40 | { 41 | $targetProperties = array_unique($this->propertyInfoExtractor->getProperties($mapperMetadata->getTarget()) ?? []); 42 | 43 | if (!\in_array($mapperMetadata->getSource(), self::ALLOWED_SOURCES, true)) { 44 | throw new InvalidMappingException('Only array or stdClass are accepted as a source'); 45 | } 46 | 47 | if (null === $targetProperties) { 48 | return []; 49 | } 50 | 51 | $mapping = []; 52 | 53 | foreach ($targetProperties as $property) { 54 | if (!$this->isWritable($mapperMetadata->getTarget(), $property)) { 55 | continue; 56 | } 57 | 58 | $targetTypes = $this->propertyInfoExtractor->getTypes($mapperMetadata->getTarget(), $property); 59 | 60 | if (null === $targetTypes) { 61 | continue; 62 | } 63 | 64 | $sourceTypes = []; 65 | 66 | foreach ($targetTypes as $type) { 67 | $sourceTypes[] = $this->transformType($mapperMetadata->getSource(), $type); 68 | } 69 | 70 | $transformer = $this->transformerFactory->getTransformer($sourceTypes, $targetTypes, $mapperMetadata); 71 | 72 | if (null === $transformer) { 73 | continue; 74 | } 75 | 76 | $mapping[] = new PropertyMapping( 77 | $this->getReadAccessor($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property), 78 | $this->getWriteMutator($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property, [ 79 | 'enable_constructor_extraction' => false, 80 | ]), 81 | $this->getWriteMutator($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property, [ 82 | 'enable_constructor_extraction' => true, 83 | ]), 84 | $transformer, 85 | $property, 86 | true, 87 | $this->getGroups($mapperMetadata->getSource(), $property), 88 | $this->getGroups($mapperMetadata->getTarget(), $property), 89 | $this->getMaxDepth($mapperMetadata->getTarget(), $property), 90 | $this->isIgnoredProperty($mapperMetadata->getSource(), $property), 91 | $this->isIgnoredProperty($mapperMetadata->getTarget(), $property) 92 | ); 93 | } 94 | 95 | return $mapping; 96 | } 97 | 98 | public function getReadAccessor(string $source, string $target, string $property): ?ReadAccessor 99 | { 100 | if (null !== $this->nameConverter) { 101 | $property = $this->nameConverter->normalize($property, $target, $source); 102 | } 103 | 104 | $sourceAccessor = new ReadAccessor(ReadAccessor::TYPE_ARRAY_DIMENSION, $property); 105 | 106 | if (\stdClass::class === $source) { 107 | $sourceAccessor = new ReadAccessor(ReadAccessor::TYPE_PROPERTY, $property); 108 | } 109 | 110 | return $sourceAccessor; 111 | } 112 | 113 | private function transformType(string $source, Type $type = null): ?Type 114 | { 115 | if (null === $type) { 116 | return null; 117 | } 118 | 119 | $builtinType = $type->getBuiltinType(); 120 | $className = $type->getClassName(); 121 | 122 | if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && \stdClass::class !== $type->getClassName()) { 123 | $builtinType = 'array' === $source ? Type::BUILTIN_TYPE_ARRAY : Type::BUILTIN_TYPE_OBJECT; 124 | $className = 'array' === $source ? null : \stdClass::class; 125 | } 126 | 127 | if (Type::BUILTIN_TYPE_OBJECT === $type->getBuiltinType() && (\DateTimeInterface::class === $type->getClassName() || is_subclass_of($type->getClassName(), \DateTimeInterface::class))) { 128 | $builtinType = 'string'; 129 | } 130 | 131 | $collectionKeyTypes = $type->getCollectionKeyTypes(); 132 | $collectionValueTypes = $type->getCollectionValueTypes(); 133 | 134 | return new Type( 135 | $builtinType, 136 | $type->isNullable(), 137 | $className, 138 | $type->isCollection(), 139 | $this->transformType($source, $collectionKeyTypes[0] ?? null), 140 | $this->transformType($source, $collectionValueTypes[0] ?? null) 141 | ); 142 | } 143 | 144 | /** 145 | * PropertyInfoExtractor::isWritable() is not enough: we want to know if the property is readonly and writable from the constructor. 146 | */ 147 | private function isWritable(string $target, string $property): bool 148 | { 149 | if ($this->propertyInfoExtractor->isWritable($target, $property)) { 150 | return true; 151 | } 152 | 153 | if (\PHP_VERSION_ID < 80100) { 154 | return false; 155 | } 156 | 157 | try { 158 | $reflectionProperty = new \ReflectionProperty($target, $property); 159 | } catch (\ReflectionException $e) { 160 | // the property does not exist 161 | return false; 162 | } 163 | 164 | if (!$reflectionProperty->isReadOnly()) { 165 | return false; 166 | } 167 | 168 | $writeInfo = $this->writeInfoExtractor->getWriteInfo($target, $property, ['enable_constructor_extraction' => true]); 169 | if (null === $writeInfo || $writeInfo->getType() !== PropertyWriteInfo::TYPE_CONSTRUCTOR) { 170 | return false; 171 | } 172 | 173 | return true; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /Extractor/MapToContextPropertyInfoExtractorDecorator.php: -------------------------------------------------------------------------------- 1 | propertyReadInfoExtractor = $propertyReadInfoExtractor; 21 | } 22 | 23 | public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo 24 | { 25 | $readInfo = $this->propertyReadInfoExtractor->getReadInfo($class, $property, $context); 26 | 27 | if (null === $readInfo || $readInfo->getType() === PropertyReadInfo::TYPE_PROPERTY && PropertyReadInfo::VISIBILITY_PUBLIC !== $readInfo->getVisibility()) { 28 | $reflClass = new \ReflectionClass($class); 29 | $camelProp = $this->camelize($property); 30 | 31 | // if we have not found a getter, it might be because it has parameters with MapToContext attribute 32 | foreach (ReflectionExtractor::$defaultAccessorPrefixes as $prefix) { 33 | $methodName = $prefix . $camelProp; 34 | 35 | if ( 36 | $reflClass->hasMethod($methodName) 37 | && $reflClass->getMethod($methodName)->getModifiers() === \ReflectionMethod::IS_PUBLIC 38 | && $reflClass->getMethod($methodName)->getNumberOfRequiredParameters() 39 | && $this->allParametersHaveMapToContextAttribute($reflClass->getMethod($methodName)) 40 | ) { 41 | $method = $reflClass->getMethod($methodName); 42 | 43 | return new PropertyReadInfo(PropertyReadInfo::TYPE_METHOD, $methodName, PropertyReadInfo::VISIBILITY_PUBLIC, $method->isStatic(), false); 44 | } 45 | } 46 | } 47 | 48 | return $readInfo; 49 | } 50 | 51 | public function isReadable(string $class, string $property, array $context = []) 52 | { 53 | if ($this->isAllowedProperty($class, $property)) { 54 | return true; 55 | } 56 | 57 | return null !== $this->getReadInfo($class, $property, $context); 58 | } 59 | 60 | public function isWritable(string $class, string $property, array $context = []) 61 | { 62 | return $this->propertyReadInfoExtractor->isWritable($class, $property, $context); 63 | } 64 | 65 | private function camelize(string $string): string 66 | { 67 | return str_replace(' ', '', ucwords(str_replace('_', ' ', $string))); 68 | } 69 | 70 | private function allParametersHaveMapToContextAttribute(\ReflectionMethod $method): bool 71 | { 72 | foreach ($method->getParameters() as $parameter) { 73 | if (!$parameter->getAttributes(MapToContext::class)) { 74 | return false; 75 | } 76 | } 77 | 78 | return true; 79 | } 80 | 81 | private function isAllowedProperty(string $class, string $property, bool $writeAccessRequired = false): bool 82 | { 83 | try { 84 | $reflectionProperty = new \ReflectionProperty($class, $property); 85 | 86 | if (\PHP_VERSION_ID >= 80100 && $writeAccessRequired && $reflectionProperty->isReadOnly()) { 87 | return false; 88 | } 89 | 90 | return (bool) ($reflectionProperty->getModifiers() & \ReflectionProperty::IS_PUBLIC); 91 | } catch (\ReflectionException $e) { 92 | // Return false if the property doesn't exist 93 | } 94 | 95 | return false; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Extractor/MappingExtractor.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | abstract class MappingExtractor implements MappingExtractorInterface 20 | { 21 | protected $propertyInfoExtractor; 22 | 23 | protected $transformerFactory; 24 | 25 | protected $readInfoExtractor; 26 | 27 | protected $writeInfoExtractor; 28 | 29 | protected $classMetadataFactory; 30 | 31 | public function __construct(PropertyInfoExtractorInterface $propertyInfoExtractor, PropertyReadInfoExtractorInterface $readInfoExtractor, PropertyWriteInfoExtractorInterface $writeInfoExtractor, TransformerFactoryInterface $transformerFactory, ClassMetadataFactoryInterface $classMetadataFactory = null) 32 | { 33 | $this->propertyInfoExtractor = $propertyInfoExtractor; 34 | $this->readInfoExtractor = $readInfoExtractor; 35 | $this->writeInfoExtractor = $writeInfoExtractor; 36 | $this->transformerFactory = $transformerFactory; 37 | $this->classMetadataFactory = $classMetadataFactory; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function getReadAccessor(string $source, string $target, string $property): ?ReadAccessor 44 | { 45 | $readInfo = $this->readInfoExtractor->getReadInfo($source, $property); 46 | 47 | if (null === $readInfo) { 48 | return null; 49 | } 50 | 51 | $type = ReadAccessor::TYPE_PROPERTY; 52 | 53 | if (PropertyReadInfo::TYPE_METHOD === $readInfo->getType()) { 54 | $type = ReadAccessor::TYPE_METHOD; 55 | } 56 | 57 | return new ReadAccessor( 58 | $type, 59 | $readInfo->getName(), 60 | $source, 61 | PropertyReadInfo::VISIBILITY_PUBLIC !== $readInfo->getVisibility() 62 | ); 63 | } 64 | 65 | /** 66 | * {@inheritdoc} 67 | */ 68 | public function getWriteMutator(string $source, string $target, string $property, array $context = []): ?WriteMutator 69 | { 70 | $writeInfo = $this->writeInfoExtractor->getWriteInfo($target, $property, $context); 71 | 72 | if (null === $writeInfo) { 73 | return null; 74 | } 75 | 76 | if (PropertyWriteInfo::TYPE_NONE === $writeInfo->getType()) { 77 | return null; 78 | } 79 | 80 | if (PropertyWriteInfo::TYPE_CONSTRUCTOR === $writeInfo->getType()) { 81 | $parameter = new \ReflectionParameter([$target, '__construct'], $writeInfo->getName()); 82 | 83 | return new WriteMutator(WriteMutator::TYPE_CONSTRUCTOR, $writeInfo->getName(), false, $parameter); 84 | } 85 | 86 | // The reported WriteInfo of readonly promoted properties is incorrectly returned as a writeable property when constructor extraction is disabled. 87 | // see https://github.com/symfony/symfony/pull/48108 88 | if ( 89 | ($context['enable_constructor_extraction'] ?? true) === false 90 | && \PHP_VERSION_ID >= 80100 91 | && PropertyWriteInfo::TYPE_PROPERTY === $writeInfo->getType() 92 | ) { 93 | $reflectionProperty = new \ReflectionProperty($target, $property); 94 | 95 | if ($reflectionProperty->isReadOnly() || $reflectionProperty->isPromoted()) { 96 | return null; 97 | } 98 | } 99 | 100 | $type = WriteMutator::TYPE_PROPERTY; 101 | 102 | if (PropertyWriteInfo::TYPE_METHOD === $writeInfo->getType()) { 103 | $type = WriteMutator::TYPE_METHOD; 104 | } 105 | 106 | if (PropertyWriteInfo::TYPE_ADDER_AND_REMOVER === $writeInfo->getType()) { 107 | $type = WriteMutator::TYPE_ADDER_AND_REMOVER; 108 | $writeInfo = $writeInfo->getAdderInfo(); 109 | } 110 | 111 | return new WriteMutator( 112 | $type, 113 | $writeInfo->getName(), 114 | PropertyReadInfo::VISIBILITY_PUBLIC !== $writeInfo->getVisibility() 115 | ); 116 | } 117 | 118 | protected function getMaxDepth($class, $property): ?int 119 | { 120 | if ('array' === $class) { 121 | return null; 122 | } 123 | 124 | if (null === $this->classMetadataFactory) { 125 | return null; 126 | } 127 | 128 | if (!$this->classMetadataFactory->getMetadataFor($class)) { 129 | return null; 130 | } 131 | 132 | $serializerClassMetadata = $this->classMetadataFactory->getMetadataFor($class); 133 | $maxDepth = null; 134 | 135 | foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) { 136 | if ($serializerAttributeMetadata->getName() === $property) { 137 | $maxDepth = $serializerAttributeMetadata->getMaxDepth(); 138 | } 139 | } 140 | 141 | return $maxDepth; 142 | } 143 | 144 | protected function getGroups($class, $property): ?array 145 | { 146 | if ('array' === $class) { 147 | return null; 148 | } 149 | 150 | if (null === $this->classMetadataFactory || !$this->classMetadataFactory->getMetadataFor($class)) { 151 | return null; 152 | } 153 | 154 | $serializerClassMetadata = $this->classMetadataFactory->getMetadataFor($class); 155 | $anyGroupFound = false; 156 | $groups = []; 157 | 158 | foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) { 159 | $groupsFound = $serializerAttributeMetadata->getGroups(); 160 | 161 | if ($groupsFound) { 162 | $anyGroupFound = true; 163 | } 164 | 165 | if ($serializerAttributeMetadata->getName() === $property) { 166 | $groups = $groupsFound; 167 | } 168 | } 169 | 170 | if (!$anyGroupFound) { 171 | return null; 172 | } 173 | 174 | return $groups; 175 | } 176 | 177 | protected function isIgnoredProperty($class, $property): bool 178 | { 179 | if ('array' === $class || !method_exists(AttributeMetadataInterface::class, 'isIgnored')) { 180 | return false; 181 | } 182 | 183 | if (null === $this->classMetadataFactory || !$this->classMetadataFactory->getMetadataFor($class)) { 184 | return false; 185 | } 186 | 187 | $serializerClassMetadata = $this->classMetadataFactory->getMetadataFor($class); 188 | 189 | foreach ($serializerClassMetadata->getAttributesMetadata() as $serializerAttributeMetadata) { 190 | if ($serializerAttributeMetadata->getName() === $property) { 191 | return $serializerAttributeMetadata->isIgnored(); 192 | } 193 | } 194 | 195 | return false; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /Extractor/MappingExtractorInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | interface MappingExtractorInterface 15 | { 16 | /** 17 | * Extracts properties mapped for a given source and target. 18 | * 19 | * @return PropertyMapping[] 20 | */ 21 | public function getPropertiesMapping(MapperMetadataInterface $mapperMetadata): array; 22 | 23 | /** 24 | * Extracts read accessor for a given source, target and property. 25 | */ 26 | public function getReadAccessor(string $source, string $target, string $property): ?ReadAccessor; 27 | 28 | /** 29 | * Extracts write mutator for a given source, target and property. 30 | */ 31 | public function getWriteMutator(string $source, string $target, string $property, array $context = []): ?WriteMutator; 32 | } 33 | -------------------------------------------------------------------------------- /Extractor/PropertyMapping.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class PropertyMapping 13 | { 14 | private $readAccessor; 15 | 16 | private $writeMutator; 17 | 18 | private $writeMutatorConstructor; 19 | 20 | private $transformer; 21 | 22 | private $checkExists; 23 | 24 | private $property; 25 | 26 | private $sourceGroups; 27 | 28 | private $targetGroups; 29 | 30 | private $maxDepth; 31 | 32 | private $sourceIgnored; 33 | 34 | private $targetIgnored; 35 | 36 | public function __construct( 37 | ReadAccessor $readAccessor, 38 | ?WriteMutator $writeMutator, 39 | ?WriteMutator $writeMutatorConstructor, 40 | TransformerInterface $transformer, 41 | string $property, 42 | bool $checkExists = false, 43 | array $sourceGroups = null, 44 | array $targetGroups = null, 45 | ?int $maxDepth = null, 46 | bool $sourceIgnored = false, 47 | bool $targetIgnored = false 48 | ) { 49 | $this->readAccessor = $readAccessor; 50 | $this->writeMutator = $writeMutator; 51 | $this->writeMutatorConstructor = $writeMutatorConstructor; 52 | $this->transformer = $transformer; 53 | $this->property = $property; 54 | $this->checkExists = $checkExists; 55 | $this->sourceGroups = $sourceGroups; 56 | $this->targetGroups = $targetGroups; 57 | $this->maxDepth = $maxDepth; 58 | $this->sourceIgnored = $sourceIgnored; 59 | $this->targetIgnored = $targetIgnored; 60 | } 61 | 62 | public function getReadAccessor(): ReadAccessor 63 | { 64 | return $this->readAccessor; 65 | } 66 | 67 | public function getWriteMutator(): ?WriteMutator 68 | { 69 | return $this->writeMutator; 70 | } 71 | 72 | public function getWriteMutatorConstructor(): ?WriteMutator 73 | { 74 | return $this->writeMutatorConstructor; 75 | } 76 | 77 | public function getTransformer(): TransformerInterface 78 | { 79 | return $this->transformer; 80 | } 81 | 82 | public function getProperty(): string 83 | { 84 | return $this->property; 85 | } 86 | 87 | public function checkExists(): bool 88 | { 89 | return $this->checkExists; 90 | } 91 | 92 | public function getSourceGroups(): ?array 93 | { 94 | return $this->sourceGroups; 95 | } 96 | 97 | public function getTargetGroups(): ?array 98 | { 99 | return $this->targetGroups; 100 | } 101 | 102 | public function getMaxDepth(): ?int 103 | { 104 | return $this->maxDepth; 105 | } 106 | 107 | public function shouldIgnoreProperty(): bool 108 | { 109 | return $this->sourceIgnored || $this->targetIgnored; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Extractor/ReadAccessor.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | final class ReadAccessor 21 | { 22 | public const TYPE_METHOD = 1; 23 | public const TYPE_PROPERTY = 2; 24 | public const TYPE_ARRAY_DIMENSION = 3; 25 | public const TYPE_SOURCE = 4; 26 | 27 | private $type; 28 | 29 | private $name; 30 | 31 | private $sourceClass; 32 | 33 | private $private; 34 | 35 | public function __construct(int $type, string $name, string $sourceClass = null, $private = false) 36 | { 37 | $this->type = $type; 38 | $this->name = $name; 39 | $this->sourceClass = $sourceClass; 40 | $this->private = $private; 41 | 42 | if (self::TYPE_METHOD === $this->type && null === $this->sourceClass) { 43 | throw new \InvalidArgumentException('Source class must be provided when using "method" type.'); 44 | } 45 | } 46 | 47 | /** 48 | * Get AST expression for reading property from an input. 49 | * 50 | * @throws CompileException 51 | */ 52 | public function getExpression(Expr\Variable $input): Expr 53 | { 54 | if (self::TYPE_METHOD === $this->type) { 55 | $methodCallArguments = []; 56 | 57 | if (\PHP_VERSION_ID >= 80000 && class_exists($this->sourceClass)) { 58 | $parameters = (new \ReflectionMethod($this->sourceClass, $this->name))->getParameters(); 59 | 60 | foreach ($parameters as $parameter) { 61 | if ($attribute = ($parameter->getAttributes(MapToContext::class)[0] ?? null)) { 62 | // generates code similar to: 63 | // $value->getValue( 64 | // $context['map_to_accessor_parameter']['some_key'] ?? throw new \InvalidArgumentException('error message'); 65 | // ) 66 | 67 | $methodCallArguments[] = new Arg( 68 | new Expr\BinaryOp\Coalesce( 69 | new Expr\ArrayDimFetch( 70 | new Expr\ArrayDimFetch( 71 | new Expr\Variable('context'), 72 | new Scalar\String_(MapperContext::MAP_TO_ACCESSOR_PARAMETER) 73 | ), 74 | new Scalar\String_($attribute->newInstance()->getContextName()) 75 | ), 76 | new Expr\Throw_( 77 | new Expr\New_( 78 | new Name\FullyQualified(\InvalidArgumentException::class), 79 | [ 80 | new Arg( 81 | new Scalar\String_( 82 | "Parameter \"\${$parameter->getName()}\" of method \"{$this->sourceClass}\"::\"{$this->name}()\" is configured to be mapped to context but no value was found in the context." 83 | ) 84 | ), 85 | ] 86 | ) 87 | ) 88 | ) 89 | ); 90 | } elseif (!$parameter->isDefaultValueAvailable()) { 91 | throw new \InvalidArgumentException("Accessors method \"{$this->sourceClass}\"::\"{$this->name}()\" parameters must have either a default value or the #[MapToContext] attribute."); 92 | } 93 | } 94 | } 95 | 96 | return new Expr\MethodCall($input, $this->name, $methodCallArguments); 97 | } 98 | 99 | if (self::TYPE_PROPERTY === $this->type) { 100 | if ($this->private) { 101 | return new Expr\FuncCall( 102 | new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->name)), 103 | [ 104 | new Arg($input), 105 | ] 106 | ); 107 | } 108 | 109 | return new Expr\PropertyFetch($input, $this->name); 110 | } 111 | 112 | if (self::TYPE_ARRAY_DIMENSION === $this->type) { 113 | return new Expr\ArrayDimFetch($input, new Scalar\String_($this->name)); 114 | } 115 | 116 | if (self::TYPE_SOURCE === $this->type) { 117 | return $input; 118 | } 119 | 120 | throw new CompileException('Invalid accessor for read expression'); 121 | } 122 | 123 | /** 124 | * Get AST expression for binding closure when dealing with a private property. 125 | */ 126 | public function getExtractCallback($className): ?Expr 127 | { 128 | if (self::TYPE_PROPERTY !== $this->type || !$this->private) { 129 | return null; 130 | } 131 | 132 | return new Expr\StaticCall(new Name\FullyQualified(\Closure::class), 'bind', [ 133 | new Arg(new Expr\Closure([ 134 | 'params' => [ 135 | new Param(new Expr\Variable('object')), 136 | ], 137 | 'stmts' => [ 138 | new Stmt\Return_(new Expr\PropertyFetch(new Expr\Variable('object'), $this->name)), 139 | ], 140 | ])), 141 | new Arg(new Expr\ConstFetch(new Name('null'))), 142 | new Arg(new Scalar\String_(new Name\FullyQualified($className))), 143 | ]); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Extractor/SourceTargetMappingExtractor.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class SourceTargetMappingExtractor extends MappingExtractor 13 | { 14 | /** 15 | * {@inheritdoc} 16 | */ 17 | public function getPropertiesMapping(MapperMetadataInterface $mapperMetadata): array 18 | { 19 | $sourceProperties = $this->propertyInfoExtractor->getProperties($mapperMetadata->getSource()); 20 | $targetProperties = $this->propertyInfoExtractor->getProperties($mapperMetadata->getTarget()); 21 | 22 | if (null === $sourceProperties || null === $targetProperties) { 23 | return []; 24 | } 25 | 26 | $sourceProperties = array_unique($sourceProperties ?? []); 27 | $targetProperties = array_unique($targetProperties ?? []); 28 | 29 | $mapping = []; 30 | 31 | foreach ($sourceProperties as $property) { 32 | if (!$this->propertyInfoExtractor->isReadable($mapperMetadata->getSource(), $property)) { 33 | continue; 34 | } 35 | 36 | if (\in_array($property, $targetProperties, true)) { 37 | $targetMutatorConstruct = $this->getWriteMutator($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property, [ 38 | 'enable_constructor_extraction' => true, 39 | ]); 40 | 41 | if ((null === $targetMutatorConstruct || null === $targetMutatorConstruct->getParameter()) && !$this->propertyInfoExtractor->isWritable($mapperMetadata->getTarget(), $property)) { 42 | continue; 43 | } 44 | 45 | $sourceTypes = $this->propertyInfoExtractor->getTypes($mapperMetadata->getSource(), $property); 46 | $targetTypes = $this->propertyInfoExtractor->getTypes($mapperMetadata->getTarget(), $property); 47 | $transformer = $this->transformerFactory->getTransformer($sourceTypes, $targetTypes, $mapperMetadata); 48 | 49 | if (null === $transformer) { 50 | continue; 51 | } 52 | 53 | $sourceAccessor = $this->getReadAccessor($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property); 54 | $targetMutator = $this->getWriteMutator($mapperMetadata->getSource(), $mapperMetadata->getTarget(), $property, [ 55 | 'enable_constructor_extraction' => false, 56 | ]); 57 | 58 | $maxDepthSource = $this->getMaxDepth($mapperMetadata->getSource(), $property); 59 | $maxDepthTarget = $this->getMaxDepth($mapperMetadata->getTarget(), $property); 60 | $maxDepth = null; 61 | 62 | if (null !== $maxDepthSource && null !== $maxDepthTarget) { 63 | $maxDepth = min($maxDepthSource, $maxDepthTarget); 64 | } elseif (null !== $maxDepthSource) { 65 | $maxDepth = $maxDepthSource; 66 | } elseif (null !== $maxDepthTarget) { 67 | $maxDepth = $maxDepthTarget; 68 | } 69 | 70 | $mapping[] = new PropertyMapping( 71 | $sourceAccessor, 72 | $targetMutator, 73 | WriteMutator::TYPE_CONSTRUCTOR === $targetMutatorConstruct->getType() ? $targetMutatorConstruct : null, 74 | $transformer, 75 | $property, 76 | false, 77 | $this->getGroups($mapperMetadata->getSource(), $property), 78 | $this->getGroups($mapperMetadata->getTarget(), $property), 79 | $maxDepth, 80 | $this->isIgnoredProperty($mapperMetadata->getSource(), $property), 81 | $this->isIgnoredProperty($mapperMetadata->getTarget(), $property) 82 | ); 83 | } 84 | } 85 | 86 | return $mapping; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Extractor/WriteMutator.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class WriteMutator 19 | { 20 | public const TYPE_METHOD = 1; 21 | public const TYPE_PROPERTY = 2; 22 | public const TYPE_ARRAY_DIMENSION = 3; 23 | public const TYPE_CONSTRUCTOR = 4; 24 | public const TYPE_ADDER_AND_REMOVER = 5; 25 | 26 | private $type; 27 | private $name; 28 | private $private; 29 | private $parameter; 30 | 31 | public function __construct(int $type, string $name, bool $private = false, \ReflectionParameter $parameter = null) 32 | { 33 | $this->type = $type; 34 | $this->name = $name; 35 | $this->private = $private; 36 | $this->parameter = $parameter; 37 | } 38 | 39 | public function getType(): int 40 | { 41 | return $this->type; 42 | } 43 | 44 | /** 45 | * Get AST expression for writing from a value to an output. 46 | * 47 | * @throws CompileException 48 | */ 49 | public function getExpression(Expr\Variable $output, Expr $value, bool $byRef = false): ?Expr 50 | { 51 | if (self::TYPE_METHOD === $this->type || self::TYPE_ADDER_AND_REMOVER === $this->type) { 52 | return new Expr\MethodCall($output, $this->name, [ 53 | new Arg($value), 54 | ]); 55 | } 56 | 57 | if (self::TYPE_PROPERTY === $this->type) { 58 | if ($this->private) { 59 | return new Expr\FuncCall( 60 | new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'hydrateCallbacks'), new Scalar\String_($this->name)), 61 | [ 62 | new Arg($output), 63 | new Arg($value), 64 | ] 65 | ); 66 | } 67 | if ($byRef) { 68 | return new Expr\AssignRef(new Expr\PropertyFetch($output, $this->name), $value); 69 | } 70 | 71 | return new Expr\Assign(new Expr\PropertyFetch($output, $this->name), $value); 72 | } 73 | 74 | if (self::TYPE_ARRAY_DIMENSION === $this->type) { 75 | if ($byRef) { 76 | return new Expr\AssignRef(new Expr\ArrayDimFetch($output, new Scalar\String_($this->name)), $value); 77 | } 78 | 79 | return new Expr\Assign(new Expr\ArrayDimFetch($output, new Scalar\String_($this->name)), $value); 80 | } 81 | 82 | throw new CompileException('Invalid accessor for write expression'); 83 | } 84 | 85 | /** 86 | * Get AST expression for binding closure when dealing with private property. 87 | */ 88 | public function getHydrateCallback($className): ?Expr 89 | { 90 | if (self::TYPE_PROPERTY !== $this->type || !$this->private) { 91 | return null; 92 | } 93 | 94 | return new Expr\StaticCall(new Name\FullyQualified(\Closure::class), 'bind', [ 95 | new Arg(new Expr\Closure([ 96 | 'params' => [ 97 | new Param(new Expr\Variable('object')), 98 | new Param(new Expr\Variable('value')), 99 | ], 100 | 'stmts' => [ 101 | new Stmt\Expression(new Expr\Assign(new Expr\PropertyFetch(new Expr\Variable('object'), $this->name), new Expr\Variable('value'))), 102 | ], 103 | ])), 104 | new Arg(new Expr\ConstFetch(new Name('null'))), 105 | new Arg(new Scalar\String_(new Name\FullyQualified($className))), 106 | ]); 107 | } 108 | 109 | /** 110 | * Get reflection parameter. 111 | */ 112 | public function getParameter(): ?\ReflectionParameter 113 | { 114 | return $this->parameter; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /GeneratedMapper.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | abstract class GeneratedMapper implements MapperInterface 11 | { 12 | protected $mappers = []; 13 | 14 | protected $callbacks; 15 | 16 | protected $hydrateCallbacks = []; 17 | 18 | protected $extractCallbacks = []; 19 | 20 | protected $cachedTarget; 21 | 22 | protected $circularReferenceHandler; 23 | 24 | protected $circularReferenceLimit; 25 | 26 | /** 27 | * Add a callable for a specific property. 28 | */ 29 | public function addCallback(string $name, callable $callback): void 30 | { 31 | $this->callbacks[$name] = $callback; 32 | } 33 | 34 | /** 35 | * Inject sub mappers. 36 | */ 37 | public function injectMappers(AutoMapperRegistryInterface $autoMapperRegistry): void 38 | { 39 | } 40 | 41 | public function setCircularReferenceHandler(?callable $circularReferenceHandler): void 42 | { 43 | $this->circularReferenceHandler = $circularReferenceHandler; 44 | } 45 | 46 | public function setCircularReferenceLimit(?int $circularReferenceLimit): void 47 | { 48 | $this->circularReferenceLimit = $circularReferenceLimit; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Generator/Generator.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | final class Generator 30 | { 31 | private $parser; 32 | 33 | private $classDiscriminator; 34 | 35 | private $allowReadOnlyTargetToPopulate; 36 | 37 | public function __construct(Parser $parser = null, ClassDiscriminatorResolverInterface $classDiscriminator = null, bool $allowReadOnlyTargetToPopulate = false) 38 | { 39 | $this->parser = $parser ?? (new ParserFactory())->create(ParserFactory::PREFER_PHP7); 40 | $this->classDiscriminator = $classDiscriminator; 41 | $this->allowReadOnlyTargetToPopulate = $allowReadOnlyTargetToPopulate; 42 | } 43 | 44 | /** 45 | * Generate Class AST given metadata for a mapper. 46 | * 47 | * @throws CompileException 48 | */ 49 | public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetadata): Stmt\Class_ 50 | { 51 | $propertiesMapping = $mapperGeneratorMetadata->getPropertiesMapping(); 52 | 53 | $uniqueVariableScope = new UniqueVariableScope(); 54 | $sourceInput = new Expr\Variable($uniqueVariableScope->getUniqueName('value')); 55 | $result = new Expr\Variable($uniqueVariableScope->getUniqueName('result')); 56 | $hashVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('sourceHash')); 57 | $contextVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('context')); 58 | $constructStatements = []; 59 | $addedDependencies = []; 60 | $canHaveCircularDependency = $mapperGeneratorMetadata->canHaveCircularReference() && 'array' !== $mapperGeneratorMetadata->getSource(); 61 | 62 | $statements = [ 63 | new Stmt\If_(new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), $sourceInput), [ 64 | 'stmts' => [new Stmt\Return_($sourceInput)], 65 | ]), 66 | ]; 67 | 68 | if ($canHaveCircularDependency) { 69 | $statements[] = new Stmt\Expression(new Expr\Assign($hashVariable, new Expr\BinaryOp\Concat(new Expr\FuncCall(new Name('spl_object_hash'), [ 70 | new Arg($sourceInput), 71 | ]), 72 | new Scalar\String_($mapperGeneratorMetadata->getTarget()) 73 | ))); 74 | $statements[] = new Stmt\If_(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), new Name('shouldHandleCircularReference'), [ 75 | new Arg($contextVariable), 76 | new Arg($hashVariable), 77 | new Arg(new Expr\PropertyFetch(new Expr\Variable('this'), 'circularReferenceLimit')), 78 | ]), [ 79 | 'stmts' => [ 80 | new Stmt\Return_(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'handleCircularReference', [ 81 | new Arg($contextVariable), 82 | new Arg($hashVariable), 83 | new Arg($sourceInput), 84 | new Arg(new Expr\PropertyFetch(new Expr\Variable('this'), 'circularReferenceLimit')), 85 | new Arg(new Expr\PropertyFetch(new Expr\Variable('this'), 'circularReferenceHandler')), 86 | ])), 87 | ], 88 | ]); 89 | } 90 | 91 | [$createObjectStmts, $inConstructor, $constructStatementsForCreateObjects, $injectMapperStatements] = $this->getCreateObjectStatements($mapperGeneratorMetadata, $result, $contextVariable, $sourceInput, $uniqueVariableScope); 92 | $constructStatements = array_merge($constructStatements, $constructStatementsForCreateObjects); 93 | 94 | $targetToPopulate = new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::TARGET_TO_POPULATE)); 95 | $statements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\BinaryOp\Coalesce( 96 | $targetToPopulate, 97 | new Expr\ConstFetch(new Name('null')) 98 | ))); 99 | if (!$this->allowReadOnlyTargetToPopulate && $mapperGeneratorMetadata->isTargetReadOnlyClass()) { 100 | $statements[] = new Stmt\If_( 101 | new Expr\BinaryOp\BooleanAnd( 102 | new Expr\BooleanNot(new Expr\BinaryOp\Coalesce(new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::ALLOW_READONLY_TARGET_TO_POPULATE)), new Expr\ConstFetch(new Name('false')))), 103 | new Expr\FuncCall(new Name('is_object'), [new Arg(new Expr\BinaryOp\Coalesce($targetToPopulate, new Expr\ConstFetch(new Name('null'))))]) 104 | ), [ 105 | 'stmts' => [new Stmt\Expression(new Expr\Throw_(new Expr\New_(new Name(ReadOnlyTargetException::class))))], 106 | ]); 107 | } 108 | 109 | $statements[] = new Stmt\If_(new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), $result), [ 110 | 'stmts' => $createObjectStmts, 111 | ]); 112 | 113 | foreach ($propertiesMapping as $propertyMapping) { 114 | if (!$propertyMapping->getTransformer() instanceof DependentTransformerInterface) { 115 | continue; 116 | } 117 | 118 | foreach ($propertyMapping->getTransformer()->getDependencies() as $dependency) { 119 | if (isset($addedDependencies[$dependency->getName()])) { 120 | continue; 121 | } 122 | 123 | $injectMapperStatements[] = new Stmt\Expression(new Expr\Assign( 124 | new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'), new Scalar\String_($dependency->getName())), 125 | new Expr\MethodCall(new Expr\Variable('autoMapperRegistry'), 'getMapper', [ 126 | new Arg(new Scalar\String_($dependency->getSource())), 127 | new Arg(new Scalar\String_($dependency->getTarget())), 128 | ]) 129 | )); 130 | $addedDependencies[$dependency->getName()] = true; 131 | } 132 | } 133 | 134 | $addedDependenciesStatements = []; 135 | if ($addedDependencies) { 136 | if ($canHaveCircularDependency) { 137 | $addedDependenciesStatements[] = new Stmt\Expression(new Expr\Assign( 138 | $contextVariable, 139 | new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'withReference', [ 140 | new Arg($contextVariable), 141 | new Arg($hashVariable), 142 | new Arg($result), 143 | ]) 144 | )); 145 | } 146 | 147 | $addedDependenciesStatements[] = new Stmt\Expression(new Expr\Assign( 148 | $contextVariable, 149 | new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'withIncrementedDepth', [ 150 | new Arg($contextVariable), 151 | ]) 152 | )); 153 | } 154 | 155 | $duplicatedStatements = []; 156 | $setterStatements = []; 157 | foreach ($propertiesMapping as $propertyMapping) { 158 | if ($propertyMapping->shouldIgnoreProperty()) { 159 | continue; 160 | } 161 | 162 | $transformer = $propertyMapping->getTransformer(); 163 | 164 | $fieldValueVariable = new Expr\Variable($uniqueVariableScope->getUniqueName('fieldValue')); 165 | $sourcePropertyAccessor = new Expr\Assign($fieldValueVariable, $propertyMapping->getReadAccessor()->getExpression($sourceInput)); 166 | 167 | [$output, $propStatements] = $transformer->transform($fieldValueVariable, $result, $propertyMapping, $uniqueVariableScope); 168 | 169 | $extractCallback = $propertyMapping->getReadAccessor()->getExtractCallback($mapperGeneratorMetadata->getSource()); 170 | 171 | if (null !== $extractCallback) { 172 | $constructStatements[] = new Stmt\Expression(new Expr\Assign( 173 | new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($propertyMapping->getProperty())), 174 | $extractCallback 175 | )); 176 | } 177 | 178 | if (null === $propertyMapping->getWriteMutator()) { 179 | continue; 180 | } 181 | 182 | if ($propertyMapping->getWriteMutator()->getType() !== WriteMutator::TYPE_ADDER_AND_REMOVER) { 183 | $writeExpression = $propertyMapping->getWriteMutator()->getExpression($result, $output, $transformer instanceof AssignedByReferenceTransformerInterface ? $transformer->assignByRef() : false); 184 | if (null === $writeExpression) { 185 | continue; 186 | } 187 | 188 | $propStatements[] = new Stmt\Expression($writeExpression); 189 | } 190 | 191 | $hydrateCallback = $propertyMapping->getWriteMutator()->getHydrateCallback($mapperGeneratorMetadata->getTarget()); 192 | 193 | if (null !== $hydrateCallback) { 194 | $constructStatements[] = new Stmt\Expression(new Expr\Assign( 195 | new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'hydrateCallbacks'), new Scalar\String_($propertyMapping->getProperty())), 196 | $hydrateCallback 197 | )); 198 | } 199 | 200 | $conditions = []; 201 | 202 | if ($propertyMapping->checkExists()) { 203 | if (\stdClass::class === $mapperGeneratorMetadata->getSource()) { 204 | $conditions[] = new Expr\FuncCall(new Name('property_exists'), [ 205 | new Arg($sourceInput), 206 | new Arg(new Scalar\String_($propertyMapping->getProperty())), 207 | ]); 208 | } 209 | 210 | if ('array' === $mapperGeneratorMetadata->getSource()) { 211 | $conditions[] = new Expr\FuncCall(new Name('array_key_exists'), [ 212 | new Arg(new Scalar\String_($propertyMapping->getProperty())), 213 | new Arg($sourceInput), 214 | ]); 215 | } 216 | } 217 | 218 | if ($mapperGeneratorMetadata->shouldCheckAttributes()) { 219 | $conditions[] = new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'isAllowedAttribute', [ 220 | new Arg($contextVariable), 221 | new Arg(new Scalar\String_($propertyMapping->getProperty())), 222 | new Arg($sourcePropertyAccessor), 223 | ]); 224 | } 225 | 226 | if (null !== $propertyMapping->getSourceGroups()) { 227 | $conditions[] = new Expr\BinaryOp\BooleanAnd( 228 | new Expr\BinaryOp\NotIdentical( 229 | new Expr\ConstFetch(new Name('null')), 230 | new Expr\BinaryOp\Coalesce( 231 | new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::GROUPS)), 232 | new Expr\Array_() 233 | ) 234 | ), 235 | new Expr\FuncCall(new Name('array_intersect'), [ 236 | new Arg(new Expr\BinaryOp\Coalesce( 237 | new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::GROUPS)), 238 | new Expr\Array_() 239 | )), 240 | new Arg(new Expr\Array_(array_map(function (string $group) { 241 | return new Expr\ArrayItem(new Scalar\String_($group)); 242 | }, $propertyMapping->getSourceGroups()))), 243 | ]) 244 | ); 245 | } 246 | 247 | if (null !== $propertyMapping->getTargetGroups()) { 248 | $conditions[] = new Expr\BinaryOp\BooleanAnd( 249 | new Expr\BinaryOp\NotIdentical( 250 | new Expr\ConstFetch(new Name('null')), 251 | new Expr\BinaryOp\Coalesce( 252 | new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::GROUPS)), 253 | new Expr\Array_() 254 | ) 255 | ), 256 | new Expr\FuncCall(new Name('array_intersect'), [ 257 | new Arg(new Expr\BinaryOp\Coalesce( 258 | new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::GROUPS)), 259 | new Expr\Array_() 260 | )), 261 | new Arg(new Expr\Array_(array_map(function (string $group) { 262 | return new Expr\ArrayItem(new Scalar\String_($group)); 263 | }, $propertyMapping->getTargetGroups()))), 264 | ]) 265 | ); 266 | } 267 | 268 | if (null !== $propertyMapping->getMaxDepth()) { 269 | $conditions[] = new Expr\BinaryOp\SmallerOrEqual( 270 | new Expr\BinaryOp\Coalesce( 271 | new Expr\ArrayDimFetch($contextVariable, new Scalar\String_(MapperContext::DEPTH)), 272 | new Expr\ConstFetch(new Name('0')) 273 | ), 274 | new Scalar\LNumber($propertyMapping->getMaxDepth()) 275 | ); 276 | } 277 | 278 | if ($conditions) { 279 | $condition = array_shift($conditions); 280 | 281 | while ($conditions) { 282 | $condition = new Expr\BinaryOp\BooleanAnd($condition, array_shift($conditions)); 283 | } 284 | 285 | $propStatements = [new Stmt\If_($condition, [ 286 | 'stmts' => $propStatements, 287 | ])]; 288 | } 289 | 290 | $propInConstructor = \in_array($propertyMapping->getProperty(), $inConstructor, true); 291 | foreach ($propStatements as $propStatement) { 292 | if ($propInConstructor) { 293 | $duplicatedStatements[] = $propStatement; 294 | } else { 295 | $setterStatements[] = $propStatement; 296 | } 297 | } 298 | } 299 | 300 | if (\count($duplicatedStatements) > 0 && \count($inConstructor)) { 301 | $statements[] = new Stmt\Else_(array_merge($addedDependenciesStatements, $duplicatedStatements)); 302 | } else { 303 | foreach ($addedDependenciesStatements as $statement) { 304 | $statements[] = $statement; 305 | } 306 | } 307 | 308 | foreach ($setterStatements as $propStatement) { 309 | $statements[] = $propStatement; 310 | } 311 | 312 | $statements[] = new Stmt\Return_($result); 313 | 314 | $mapMethod = new Stmt\ClassMethod('map', [ 315 | 'flags' => Stmt\Class_::MODIFIER_PUBLIC, 316 | 'params' => [ 317 | new Param(new Expr\Variable($sourceInput->name)), 318 | new Param(new Expr\Variable('context'), new Expr\Array_(), 'array'), 319 | ], 320 | 'byRef' => true, 321 | 'stmts' => $statements, 322 | 'returnType' => \PHP_VERSION_ID >= 80000 ? 'mixed' : null, 323 | ]); 324 | 325 | $constructMethod = new Stmt\ClassMethod('__construct', [ 326 | 'flags' => Stmt\Class_::MODIFIER_PUBLIC, 327 | 'stmts' => $constructStatements, 328 | ]); 329 | 330 | $classStmts = [$constructMethod, $mapMethod]; 331 | 332 | if (\count($injectMapperStatements) > 0) { 333 | $classStmts[] = new Stmt\ClassMethod('injectMappers', [ 334 | 'flags' => Stmt\Class_::MODIFIER_PUBLIC, 335 | 'params' => [ 336 | new Param(new Expr\Variable('autoMapperRegistry'), null, new Name\FullyQualified(AutoMapperRegistryInterface::class)), 337 | ], 338 | 'returnType' => 'void', 339 | 'stmts' => $injectMapperStatements, 340 | ]); 341 | } 342 | 343 | return new Stmt\Class_(new Name($mapperGeneratorMetadata->getMapperClassName()), [ 344 | 'flags' => Stmt\Class_::MODIFIER_FINAL, 345 | 'extends' => new Name\FullyQualified(GeneratedMapper::class), 346 | 'stmts' => $classStmts, 347 | ]); 348 | } 349 | 350 | private function getCreateObjectStatements(MapperGeneratorMetadataInterface $mapperMetadata, Expr\Variable $result, Expr\Variable $contextVariable, Expr\Variable $sourceInput, UniqueVariableScope $uniqueVariableScope): array 351 | { 352 | $target = $mapperMetadata->getTarget(); 353 | $source = $mapperMetadata->getSource(); 354 | 355 | if ('array' === $target) { 356 | return [[new Stmt\Expression(new Expr\Assign($result, new Expr\Array_()))], [], [], []]; 357 | } 358 | 359 | if (\stdClass::class === $target && \stdClass::class === $source) { 360 | return [[new Stmt\Expression(new Expr\Assign($result, new Expr\FuncCall(new Name('unserialize'), [new Arg(new Expr\FuncCall(new Name('serialize'), [new Arg($sourceInput)]))])))], [], [], []]; 361 | } elseif (\stdClass::class === $target) { 362 | return [[new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name(\stdClass::class))))], [], [], []]; 363 | } 364 | 365 | $reflectionClass = new \ReflectionClass($target); 366 | $targetConstructor = $reflectionClass->getConstructor(); 367 | $createObjectStatements = []; 368 | $inConstructor = []; 369 | $constructStatements = []; 370 | $injectMapperStatements = []; 371 | $classDiscriminatorMapping = 'array' !== $target && null !== $this->classDiscriminator ? $this->classDiscriminator->getMappingForClass($target) : null; 372 | 373 | if (null !== $classDiscriminatorMapping && null !== ($propertyMapping = $mapperMetadata->getPropertyMapping($classDiscriminatorMapping->getTypeProperty()))) { 374 | [$output, $createObjectStatements] = $propertyMapping->getTransformer()->transform($propertyMapping->getReadAccessor()->getExpression($sourceInput), $result, $propertyMapping, $uniqueVariableScope); 375 | 376 | foreach ($classDiscriminatorMapping->getTypesMapping() as $typeValue => $typeTarget) { 377 | $mapperName = 'Discriminator_Mapper_' . $source . '_' . $typeTarget; 378 | 379 | $injectMapperStatements[] = new Stmt\Expression(new Expr\Assign( 380 | new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'), new Scalar\String_($mapperName)), 381 | new Expr\MethodCall(new Expr\Variable('autoMapperRegistry'), 'getMapper', [ 382 | new Arg(new Scalar\String_($source)), 383 | new Arg(new Scalar\String_($typeTarget)), 384 | ]) 385 | )); 386 | $createObjectStatements[] = new Stmt\If_(new Expr\BinaryOp\Identical( 387 | new Scalar\String_($typeValue), 388 | $output 389 | ), [ 390 | 'stmts' => [ 391 | new Stmt\Return_(new Expr\MethodCall(new Expr\ArrayDimFetch( 392 | new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'), 393 | new Scalar\String_($mapperName) 394 | ), 'map', [ 395 | new Arg($sourceInput), 396 | new Expr\Variable('context'), 397 | ])), 398 | ], 399 | ]); 400 | } 401 | } 402 | 403 | $propertiesMapping = $mapperMetadata->getPropertiesMapping(); 404 | 405 | if (null !== $targetConstructor && $mapperMetadata->hasConstructor()) { 406 | $constructArguments = []; 407 | 408 | foreach ($propertiesMapping as $propertyMapping) { 409 | if (null === $propertyMapping->getWriteMutatorConstructor() || null === ($parameter = $propertyMapping->getWriteMutatorConstructor()->getParameter())) { 410 | continue; 411 | } 412 | 413 | $constructVar = new Expr\Variable($uniqueVariableScope->getUniqueName('constructArg')); 414 | 415 | [$output, $propStatements] = $propertyMapping->getTransformer()->transform($propertyMapping->getReadAccessor()->getExpression($sourceInput), $constructVar, $propertyMapping, $uniqueVariableScope); 416 | $constructArguments[$parameter->getPosition()] = new Arg($constructVar); 417 | 418 | $propStatements[] = new Stmt\Expression(new Expr\Assign($constructVar, $output)); 419 | $createObjectStatements[] = new Stmt\If_(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'hasConstructorArgument', [ 420 | new Arg($contextVariable), 421 | new Arg(new Scalar\String_($target)), 422 | new Arg(new Scalar\String_($propertyMapping->getProperty())), 423 | ]), [ 424 | 'stmts' => [ 425 | new Stmt\Expression(new Expr\Assign($constructVar, new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'getConstructorArgument', [ 426 | new Arg($contextVariable), 427 | new Arg(new Scalar\String_($target)), 428 | new Arg(new Scalar\String_($propertyMapping->getProperty())), 429 | ]))), 430 | ], 431 | 'else' => new Stmt\Else_($propStatements), 432 | ]); 433 | 434 | $inConstructor[] = $propertyMapping->getProperty(); 435 | } 436 | 437 | foreach ($targetConstructor->getParameters() as $constructorParameter) { 438 | if (!\array_key_exists($constructorParameter->getPosition(), $constructArguments) && $constructorParameter->isDefaultValueAvailable()) { 439 | $constructVar = new Expr\Variable($uniqueVariableScope->getUniqueName('constructArg')); 440 | 441 | $createObjectStatements[] = new Stmt\If_(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'hasConstructorArgument', [ 442 | new Arg($contextVariable), 443 | new Arg(new Scalar\String_($target)), 444 | new Arg(new Scalar\String_($constructorParameter->getName())), 445 | ]), [ 446 | 'stmts' => [ 447 | new Stmt\Expression(new Expr\Assign($constructVar, new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'getConstructorArgument', [ 448 | new Arg($contextVariable), 449 | new Arg(new Scalar\String_($target)), 450 | new Arg(new Scalar\String_($constructorParameter->getName())), 451 | ]))), 452 | ], 453 | 'else' => new Stmt\Else_([ 454 | new Stmt\Expression(new Expr\Assign($constructVar, $this->getValueAsExpr($constructorParameter->getDefaultValue()))), 455 | ]), 456 | ]); 457 | 458 | $constructArguments[$constructorParameter->getPosition()] = new Arg($constructVar); 459 | } 460 | } 461 | 462 | ksort($constructArguments); 463 | 464 | $createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name\FullyQualified($target), $constructArguments))); 465 | } elseif (null !== $targetConstructor && $mapperMetadata->isTargetCloneable()) { 466 | $constructStatements[] = new Stmt\Expression(new Expr\Assign( 467 | new Expr\PropertyFetch(new Expr\Variable('this'), 'cachedTarget'), 468 | new Expr\MethodCall(new Expr\New_(new Name\FullyQualified(\ReflectionClass::class), [ 469 | new Arg(new Scalar\String_($target)), 470 | ]), 'newInstanceWithoutConstructor') 471 | )); 472 | $createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\Clone_(new Expr\PropertyFetch(new Expr\Variable('this'), 'cachedTarget')))); 473 | } elseif (null !== $targetConstructor) { 474 | $constructStatements[] = new Stmt\Expression(new Expr\Assign( 475 | new Expr\PropertyFetch(new Expr\Variable('this'), 'cachedTarget'), 476 | new Expr\New_(new Name\FullyQualified(\ReflectionClass::class), [ 477 | new Arg(new Scalar\String_($target)), 478 | ]) 479 | )); 480 | $createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\MethodCall( 481 | new Expr\PropertyFetch(new Expr\Variable('this'), 'cachedTarget'), 482 | 'newInstanceWithoutConstructor' 483 | ))); 484 | } else { 485 | $createObjectStatements[] = new Stmt\Expression(new Expr\Assign($result, new Expr\New_(new Name\FullyQualified($target)))); 486 | } 487 | 488 | return [$createObjectStatements, $inConstructor, $constructStatements, $injectMapperStatements]; 489 | } 490 | 491 | private function getValueAsExpr($value) 492 | { 493 | $expr = $this->parser->parse('expr; 497 | } 498 | 499 | return $expr; 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /Generator/UniqueVariableScope.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class UniqueVariableScope 13 | { 14 | private $registry = []; 15 | 16 | /** 17 | * Return an unique name for a variable name. 18 | */ 19 | public function getUniqueName(string $name): string 20 | { 21 | $name = strtolower($name); 22 | 23 | if (!isset($this->registry[$name])) { 24 | $this->registry[$name] = 0; 25 | 26 | return $name; 27 | } 28 | 29 | ++$this->registry[$name]; 30 | 31 | return "{$name}_{$this->registry[$name]}"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2016-2017 Joel Wurtz 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Loader/ClassLoaderInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface ClassLoaderInterface 13 | { 14 | public function loadClass(MapperGeneratorMetadataInterface $mapperMetadata): void; 15 | } 16 | -------------------------------------------------------------------------------- /Loader/EvalLoader.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class EvalLoader implements ClassLoaderInterface 15 | { 16 | private $generator; 17 | 18 | private $printer; 19 | 20 | public function __construct(Generator $generator) 21 | { 22 | $this->generator = $generator; 23 | $this->printer = new Standard(); 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function loadClass(MapperGeneratorMetadataInterface $mapperGeneratorMetadata): void 30 | { 31 | $class = $this->generator->generate($mapperGeneratorMetadata); 32 | 33 | eval($this->printer->prettyPrint([$class])); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Loader/FileLoader.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class FileLoader implements ClassLoaderInterface 15 | { 16 | private $generator; 17 | private $directory; 18 | private $hotReload; 19 | private $printer; 20 | private $registry; 21 | 22 | public function __construct(Generator $generator, string $directory, bool $hotReload = true) 23 | { 24 | $this->generator = $generator; 25 | $this->directory = $directory; 26 | $this->hotReload = $hotReload; 27 | $this->printer = new Standard(); 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function loadClass(MapperGeneratorMetadataInterface $mapperGeneratorMetadata): void 34 | { 35 | $className = $mapperGeneratorMetadata->getMapperClassName(); 36 | $classPath = $this->directory . \DIRECTORY_SEPARATOR . $className . '.php'; 37 | 38 | if (!$this->hotReload && file_exists($classPath)) { 39 | require $classPath; 40 | 41 | return; 42 | } 43 | 44 | $shouldSaveMapper = true; 45 | if ($this->hotReload) { 46 | $registry = $this->getRegistry(); 47 | $hash = $mapperGeneratorMetadata->getHash(); 48 | $shouldSaveMapper = !isset($registry[$className]) || $registry[$className] !== $hash || !file_exists($classPath); 49 | } 50 | 51 | if ($shouldSaveMapper) { 52 | $this->saveMapper($mapperGeneratorMetadata); 53 | } 54 | 55 | require $classPath; 56 | } 57 | 58 | /** 59 | * @return string The generated class name 60 | */ 61 | public function saveMapper(MapperGeneratorMetadataInterface $mapperGeneratorMetadata): string 62 | { 63 | $className = $mapperGeneratorMetadata->getMapperClassName(); 64 | $classPath = $this->directory . \DIRECTORY_SEPARATOR . $className . '.php'; 65 | $classCode = $this->printer->prettyPrint([$this->generator->generate($mapperGeneratorMetadata)]); 66 | 67 | $this->write($classPath, "hotReload) { 69 | $this->addHashToRegistry($className, $mapperGeneratorMetadata->getHash()); 70 | } 71 | 72 | return $className; 73 | } 74 | 75 | private function addHashToRegistry($className, $hash): void 76 | { 77 | $registryPath = $this->directory . \DIRECTORY_SEPARATOR . 'registry.php'; 78 | $this->registry[$className] = $hash; 79 | $this->write($registryPath, "registry, true) . ";\n"); 80 | } 81 | 82 | private function getRegistry() 83 | { 84 | if (!$this->registry) { 85 | $registryPath = $this->directory . \DIRECTORY_SEPARATOR . 'registry.php'; 86 | 87 | if (!file_exists($registryPath)) { 88 | $this->registry = []; 89 | } else { 90 | $this->registry = require $registryPath; 91 | } 92 | } 93 | 94 | return $this->registry; 95 | } 96 | 97 | private function write(string $file, string $contents): void 98 | { 99 | if (!file_exists($this->directory)) { 100 | mkdir($this->directory); 101 | } 102 | 103 | $fp = fopen($file, 'w'); 104 | 105 | if (flock($fp, LOCK_EX)) { 106 | fwrite($fp, $contents); 107 | } 108 | 109 | fclose($fp); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /MapperContext.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class MapperContext 15 | { 16 | public const GROUPS = 'groups'; 17 | public const ALLOWED_ATTRIBUTES = 'allowed_attributes'; 18 | public const IGNORED_ATTRIBUTES = 'ignored_attributes'; 19 | public const CIRCULAR_REFERENCE_LIMIT = 'circular_reference_limit'; 20 | public const CIRCULAR_REFERENCE_HANDLER = 'circular_reference_handler'; 21 | public const CIRCULAR_REFERENCE_REGISTRY = 'circular_reference_registry'; 22 | public const CIRCULAR_COUNT_REFERENCE_REGISTRY = 'circular_count_reference_registry'; 23 | public const DEPTH = 'depth'; 24 | public const TARGET_TO_POPULATE = 'target_to_populate'; 25 | public const CONSTRUCTOR_ARGUMENTS = 'constructor_arguments'; 26 | public const SKIP_NULL_VALUES = 'skip_null_values'; 27 | public const ALLOW_READONLY_TARGET_TO_POPULATE = 'allow_readonly_target_to_populate'; 28 | public const DATETIME_FORMAT = 'datetime_format'; 29 | public const MAP_TO_ACCESSOR_PARAMETER = 'map_to_accessor_parameter'; 30 | 31 | private $context = [ 32 | self::DEPTH => 0, 33 | self::CIRCULAR_REFERENCE_REGISTRY => [], 34 | self::CIRCULAR_COUNT_REFERENCE_REGISTRY => [], 35 | self::CONSTRUCTOR_ARGUMENTS => [], 36 | self::MAP_TO_ACCESSOR_PARAMETER => [], 37 | ]; 38 | 39 | public function toArray(): array 40 | { 41 | return $this->context; 42 | } 43 | 44 | public function setGroups(?array $groups): self 45 | { 46 | $this->context[self::GROUPS] = $groups; 47 | 48 | return $this; 49 | } 50 | 51 | public function setAllowedAttributes(?array $allowedAttributes): self 52 | { 53 | $this->context[self::ALLOWED_ATTRIBUTES] = $allowedAttributes; 54 | 55 | return $this; 56 | } 57 | 58 | public function setIgnoredAttributes(?array $ignoredAttributes): self 59 | { 60 | $this->context[self::IGNORED_ATTRIBUTES] = $ignoredAttributes; 61 | 62 | return $this; 63 | } 64 | 65 | public function setCircularReferenceLimit(?int $circularReferenceLimit): self 66 | { 67 | $this->context[self::CIRCULAR_REFERENCE_LIMIT] = $circularReferenceLimit; 68 | 69 | return $this; 70 | } 71 | 72 | public function setCircularReferenceHandler(?callable $circularReferenceHandler): self 73 | { 74 | $this->context[self::CIRCULAR_REFERENCE_HANDLER] = $circularReferenceHandler; 75 | 76 | return $this; 77 | } 78 | 79 | public function setTargetToPopulate($target): self 80 | { 81 | $this->context[self::TARGET_TO_POPULATE] = $target; 82 | 83 | return $this; 84 | } 85 | 86 | public function setConstructorArgument(string $class, string $key, $value): self 87 | { 88 | $this->context[self::CONSTRUCTOR_ARGUMENTS][$class][$key] = $value; 89 | 90 | return $this; 91 | } 92 | 93 | public function setSkipNullValues(bool $skipNullValues): self 94 | { 95 | $this->context[self::SKIP_NULL_VALUES] = $skipNullValues; 96 | 97 | return $this; 98 | } 99 | 100 | public function setAllowReadOnlyTargetToPopulate(bool $allowReadOnlyTargetToPopulate): self 101 | { 102 | $this->context[self::ALLOW_READONLY_TARGET_TO_POPULATE] = $allowReadOnlyTargetToPopulate; 103 | 104 | return $this; 105 | } 106 | 107 | /** 108 | * Whether a reference has reached it's limit. 109 | */ 110 | public static function shouldHandleCircularReference(array $context, string $reference, ?int $circularReferenceLimit = null): bool 111 | { 112 | if (!\array_key_exists($reference, $context[self::CIRCULAR_REFERENCE_REGISTRY] ?? [])) { 113 | return false; 114 | } 115 | 116 | if (null === $circularReferenceLimit) { 117 | $circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? null; 118 | } 119 | 120 | if (null !== $circularReferenceLimit) { 121 | return $circularReferenceLimit <= ($context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference] ?? 0); 122 | } 123 | 124 | return true; 125 | } 126 | 127 | /** 128 | * Handle circular reference for a specific reference. 129 | * 130 | * By default will try to keep it and return the previous value 131 | */ 132 | public static function &handleCircularReference(array &$context, string $reference, $object, ?int $circularReferenceLimit = null, callable $callback = null) 133 | { 134 | if (null === $callback) { 135 | $callback = $context[self::CIRCULAR_REFERENCE_HANDLER] ?? null; 136 | } 137 | 138 | if (null !== $callback) { 139 | // Cannot directly return here, as we need to return by reference, and callback may not be declared as reference return 140 | $value = $callback($object, $context); 141 | 142 | return $value; 143 | } 144 | 145 | if (null === $circularReferenceLimit) { 146 | $circularReferenceLimit = $context[self::CIRCULAR_REFERENCE_LIMIT] ?? null; 147 | } 148 | 149 | if (null !== $circularReferenceLimit) { 150 | if ($circularReferenceLimit <= ($context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference] ?? 0)) { 151 | 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)); 152 | } 153 | 154 | ++$context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference]; 155 | } 156 | 157 | // When no limit defined return the object referenced 158 | return $context[self::CIRCULAR_REFERENCE_REGISTRY][$reference]; 159 | } 160 | 161 | /** 162 | * Create a new context with a new reference. 163 | */ 164 | public static function withReference(array $context, string $reference, &$object): array 165 | { 166 | $context[self::CIRCULAR_REFERENCE_REGISTRY][$reference] = &$object; 167 | $context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference] = $context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference] ?? 0; 168 | ++$context[self::CIRCULAR_COUNT_REFERENCE_REGISTRY][$reference]; 169 | 170 | return $context; 171 | } 172 | 173 | /** 174 | * Check whether an attribute is allowed to be mapped. 175 | */ 176 | public static function isAllowedAttribute(array $context, string $attribute, $value): bool 177 | { 178 | if (($context[self::SKIP_NULL_VALUES] ?? false) && null === $value) { 179 | return false; 180 | } 181 | 182 | if (($context[self::IGNORED_ATTRIBUTES] ?? false) && \in_array($attribute, $context[self::IGNORED_ATTRIBUTES], true)) { 183 | return false; 184 | } 185 | 186 | if (!($context[self::ALLOWED_ATTRIBUTES] ?? false)) { 187 | return true; 188 | } 189 | 190 | return \in_array($attribute, $context[self::ALLOWED_ATTRIBUTES], true) // current field is allowed 191 | || isset($context[self::ALLOWED_ATTRIBUTES][$attribute]) // some nested fields are allowed 192 | ; 193 | } 194 | 195 | /** 196 | * Clone context with a incremented depth. 197 | */ 198 | public static function withIncrementedDepth(array $context): array 199 | { 200 | $context[self::DEPTH] = $context[self::DEPTH] ?? 0; 201 | ++$context[self::DEPTH]; 202 | 203 | return $context; 204 | } 205 | 206 | /** 207 | * Check wether an argument exist for the constructor for a specific class. 208 | */ 209 | public static function hasConstructorArgument(array $context, string $class, string $key): bool 210 | { 211 | return \array_key_exists($key, $context[self::CONSTRUCTOR_ARGUMENTS][$class] ?? []); 212 | } 213 | 214 | /** 215 | * Get constructor argument for a specific class. 216 | */ 217 | public static function getConstructorArgument(array $context, string $class, string $key) 218 | { 219 | return $context[self::CONSTRUCTOR_ARGUMENTS][$class][$key] ?? null; 220 | } 221 | 222 | /** 223 | * Create a new context, and reload attribute mapping for it. 224 | */ 225 | public static function withNewContext(array $context, string $attribute): array 226 | { 227 | $context[self::TARGET_TO_POPULATE] = null; 228 | 229 | if (!($context[self::ALLOWED_ATTRIBUTES] ?? false) && !($context[self::IGNORED_ATTRIBUTES] ?? false)) { 230 | return $context; 231 | } 232 | 233 | if (\is_array($context[self::IGNORED_ATTRIBUTES][$attribute] ?? false)) { 234 | $context[self::IGNORED_ATTRIBUTES] = $context[self::IGNORED_ATTRIBUTES][$attribute]; 235 | } 236 | 237 | if (\is_array($context[self::ALLOWED_ATTRIBUTES][$attribute] ?? false)) { 238 | $context[self::ALLOWED_ATTRIBUTES] = $context[self::ALLOWED_ATTRIBUTES][$attribute]; 239 | } else { 240 | unset($context[self::ALLOWED_ATTRIBUTES]); 241 | } 242 | 243 | return $context; 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /MapperGeneratorMetadataFactory.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class MapperGeneratorMetadataFactory implements MapperGeneratorMetadataFactoryInterface 15 | { 16 | private $sourceTargetPropertiesMappingExtractor; 17 | private $fromSourcePropertiesMappingExtractor; 18 | private $fromTargetPropertiesMappingExtractor; 19 | private $classPrefix; 20 | private $attributeChecking; 21 | private $dateTimeFormat; 22 | 23 | public function __construct( 24 | SourceTargetMappingExtractor $sourceTargetPropertiesMappingExtractor, 25 | FromSourceMappingExtractor $fromSourcePropertiesMappingExtractor, 26 | FromTargetMappingExtractor $fromTargetPropertiesMappingExtractor, 27 | string $classPrefix = 'Mapper_', 28 | bool $attributeChecking = true, 29 | string $dateTimeFormat = \DateTime::RFC3339 30 | ) { 31 | $this->sourceTargetPropertiesMappingExtractor = $sourceTargetPropertiesMappingExtractor; 32 | $this->fromSourcePropertiesMappingExtractor = $fromSourcePropertiesMappingExtractor; 33 | $this->fromTargetPropertiesMappingExtractor = $fromTargetPropertiesMappingExtractor; 34 | $this->classPrefix = $classPrefix; 35 | $this->attributeChecking = $attributeChecking; 36 | $this->dateTimeFormat = $dateTimeFormat; 37 | } 38 | 39 | /** 40 | * Create metadata for a source and target. 41 | */ 42 | public function create(MapperGeneratorMetadataRegistryInterface $autoMapperRegister, string $source, string $target): MapperGeneratorMetadataInterface 43 | { 44 | $extractor = $this->sourceTargetPropertiesMappingExtractor; 45 | 46 | if ('array' === $source || 'stdClass' === $source) { 47 | $extractor = $this->fromTargetPropertiesMappingExtractor; 48 | } 49 | 50 | if ('array' === $target || 'stdClass' === $target) { 51 | $extractor = $this->fromSourcePropertiesMappingExtractor; 52 | } 53 | 54 | $mapperMetadata = new MapperMetadata($autoMapperRegister, $extractor, $source, $target, $this->isReadOnly($target), $this->classPrefix); 55 | $mapperMetadata->setAttributeChecking($this->attributeChecking); 56 | $mapperMetadata->setDateTimeFormat($this->dateTimeFormat); 57 | 58 | return $mapperMetadata; 59 | } 60 | 61 | private function isReadOnly(string $mappedType): bool 62 | { 63 | try { 64 | $reflClass = new \ReflectionClass($mappedType); 65 | } catch (\ReflectionException $e) { 66 | $reflClass = null; 67 | } 68 | if (\PHP_VERSION_ID >= 80200 && null !== $reflClass && $reflClass->isReadOnly()) { 69 | return true; 70 | } 71 | 72 | return false; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /MapperGeneratorMetadataFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface MapperGeneratorMetadataFactoryInterface 11 | { 12 | public function create(MapperGeneratorMetadataRegistryInterface $autoMapperRegister, string $source, string $target): MapperGeneratorMetadataInterface; 13 | } 14 | -------------------------------------------------------------------------------- /MapperGeneratorMetadataInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface MapperGeneratorMetadataInterface extends MapperMetadataInterface 11 | { 12 | /** 13 | * Get mapper class name. 14 | */ 15 | public function getMapperClassName(): string; 16 | 17 | /** 18 | * Get hash (unique key) for those metadatas. 19 | */ 20 | public function getHash(): string; 21 | 22 | /** 23 | * Get a list of callbacks to add for this mapper. 24 | * 25 | * @return callable[] 26 | */ 27 | public function getCallbacks(): array; 28 | 29 | /** 30 | * Whether the target class has a constructor. 31 | */ 32 | public function hasConstructor(): bool; 33 | 34 | /** 35 | * Whether we can use target constructor. 36 | */ 37 | public function isConstructorAllowed(): bool; 38 | 39 | /** 40 | * Whether we should generate attributes checking. 41 | */ 42 | public function shouldCheckAttributes(): bool; 43 | 44 | /** 45 | * If not using target constructor, allow to know if we can clone a empty target. 46 | */ 47 | public function isTargetCloneable(): bool; 48 | 49 | /** 50 | * Whether the mapping can have circular reference. 51 | * 52 | * If not the case, allow to not generate code about circular references 53 | */ 54 | public function canHaveCircularReference(): bool; 55 | } 56 | -------------------------------------------------------------------------------- /MapperGeneratorMetadataRegistryInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface MapperGeneratorMetadataRegistryInterface 13 | { 14 | /** 15 | * Register metadata. 16 | */ 17 | public function register(MapperGeneratorMetadataInterface $configuration): void; 18 | 19 | /** 20 | * Bind custom TransformerFactory to the AutoMapper. 21 | */ 22 | public function bindTransformerFactory(TransformerFactoryInterface $transformerFactory): void; 23 | 24 | /** 25 | * Get metadata for a source and a target. 26 | */ 27 | public function getMetadata(string $source, string $target): ?MapperGeneratorMetadataInterface; 28 | } 29 | -------------------------------------------------------------------------------- /MapperInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface MapperInterface 13 | { 14 | /** 15 | * @param mixed $value Value to map 16 | * @param array $context Options mapper have access to 17 | * 18 | * @return mixed The mapped value 19 | */ 20 | public function &map($value, array $context = []); 21 | } 22 | -------------------------------------------------------------------------------- /MapperMetadata.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class MapperMetadata implements MapperGeneratorMetadataInterface 17 | { 18 | private $mappingExtractor; 19 | 20 | private $customMapping = []; 21 | 22 | private $propertiesMapping; 23 | 24 | private $metadataRegistry; 25 | 26 | private $source; 27 | 28 | private $target; 29 | 30 | private $className; 31 | 32 | private $isConstructorAllowed; 33 | 34 | private $isTargetReadOnlyClass; 35 | 36 | private $dateTimeFormat; 37 | 38 | private $classPrefix; 39 | 40 | private $attributeChecking; 41 | 42 | private $targetReflectionClass; 43 | 44 | public function __construct(MapperGeneratorMetadataRegistryInterface $metadataRegistry, MappingExtractorInterface $mappingExtractor, string $source, string $target, bool $isTargetReadOnlyClass, string $classPrefix = 'Mapper_') 45 | { 46 | $this->mappingExtractor = $mappingExtractor; 47 | $this->metadataRegistry = $metadataRegistry; 48 | $this->source = $source; 49 | $this->target = $target; 50 | $this->isTargetReadOnlyClass = $isTargetReadOnlyClass; 51 | $this->isConstructorAllowed = true; 52 | $this->dateTimeFormat = \DateTime::RFC3339; 53 | $this->classPrefix = $classPrefix; 54 | $this->attributeChecking = true; 55 | } 56 | 57 | private function getCachedTargetReflectionClass(): \ReflectionClass 58 | { 59 | if (null === $this->targetReflectionClass) { 60 | $this->targetReflectionClass = new \ReflectionClass($this->getTarget()); 61 | } 62 | 63 | return $this->targetReflectionClass; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function getPropertiesMapping(): array 70 | { 71 | if (null === $this->propertiesMapping) { 72 | $this->buildPropertyMapping(); 73 | } 74 | 75 | return $this->propertiesMapping; 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function getPropertyMapping(string $property): ?PropertyMapping 82 | { 83 | return $this->getPropertiesMapping()[$property] ?? null; 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function hasConstructor(): bool 90 | { 91 | if (!$this->isConstructorAllowed()) { 92 | return false; 93 | } 94 | 95 | if (\in_array($this->target, ['array', \stdClass::class], true)) { 96 | return false; 97 | } 98 | 99 | $reflection = $this->getCachedTargetReflectionClass(); 100 | $constructor = $reflection->getConstructor(); 101 | 102 | if (null === $constructor) { 103 | return false; 104 | } 105 | 106 | $parameters = $constructor->getParameters(); 107 | $mandatoryParameters = []; 108 | 109 | foreach ($parameters as $parameter) { 110 | if (!$parameter->isOptional() && !$parameter->allowsNull()) { 111 | $mandatoryParameters[] = $parameter; 112 | } 113 | } 114 | 115 | if (!$mandatoryParameters) { 116 | return true; 117 | } 118 | 119 | foreach ($mandatoryParameters as $mandatoryParameter) { 120 | $readAccessor = $this->mappingExtractor->getReadAccessor($this->source, $this->target, $mandatoryParameter->getName()); 121 | 122 | if (null === $readAccessor) { 123 | return false; 124 | } 125 | } 126 | 127 | return true; 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function isTargetCloneable(): bool 134 | { 135 | try { 136 | $reflection = $this->getCachedTargetReflectionClass(); 137 | 138 | return $reflection->isCloneable() && !$reflection->hasMethod('__clone'); 139 | } catch (\ReflectionException $e) { 140 | // if we have a \ReflectionException, then we can't clone target 141 | return false; 142 | } 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function canHaveCircularReference(): bool 149 | { 150 | $checked = []; 151 | 152 | return $this->checkCircularMapperConfiguration($this, $checked); 153 | } 154 | 155 | /** 156 | * {@inheritdoc} 157 | */ 158 | public function getMapperClassName(): string 159 | { 160 | if (null !== $this->className) { 161 | return $this->className; 162 | } 163 | 164 | return $this->className = sprintf('%s%s_%s', $this->classPrefix, str_replace('\\', '_', $this->source), str_replace('\\', '_', $this->target)); 165 | } 166 | 167 | /** 168 | * {@inheritdoc} 169 | */ 170 | public function getHash(): string 171 | { 172 | $hash = ''; 173 | 174 | if (!\in_array($this->source, ['array', \stdClass::class], true) && class_exists($this->source)) { 175 | $reflection = new \ReflectionClass($this->source); 176 | $hash .= filemtime($reflection->getFileName()); 177 | } 178 | 179 | if (!\in_array($this->target, ['array', \stdClass::class], true)) { 180 | $reflection = $this->getCachedTargetReflectionClass(); 181 | $hash .= filemtime($reflection->getFileName()); 182 | } 183 | 184 | return $hash; 185 | } 186 | 187 | /** 188 | * {@inheritdoc} 189 | */ 190 | public function isConstructorAllowed(): bool 191 | { 192 | return $this->isConstructorAllowed; 193 | } 194 | 195 | /** 196 | * {@inheritdoc} 197 | */ 198 | public function getSource(): string 199 | { 200 | return $this->source; 201 | } 202 | 203 | /** 204 | * {@inheritdoc} 205 | */ 206 | public function getTarget(): string 207 | { 208 | return $this->target; 209 | } 210 | 211 | /** 212 | * {@inheritdoc} 213 | */ 214 | public function getDateTimeFormat(): string 215 | { 216 | return $this->dateTimeFormat; 217 | } 218 | 219 | /** 220 | * {@inheritdoc} 221 | */ 222 | public function getCallbacks(): array 223 | { 224 | return $this->customMapping; 225 | } 226 | 227 | /** 228 | * {@inheritdoc} 229 | */ 230 | public function shouldCheckAttributes(): bool 231 | { 232 | return $this->attributeChecking; 233 | } 234 | 235 | /** 236 | * Set DateTime format to use when generating a mapper. 237 | */ 238 | public function setDateTimeFormat(string $dateTimeFormat): void 239 | { 240 | $this->dateTimeFormat = $dateTimeFormat; 241 | } 242 | 243 | /** 244 | * Whether or not the constructor should be used. 245 | */ 246 | public function setConstructorAllowed(bool $isConstructorAllowed): void 247 | { 248 | $this->isConstructorAllowed = $isConstructorAllowed; 249 | } 250 | 251 | /** 252 | * Set a callable to use when mapping a specific property. 253 | */ 254 | public function forMember(string $property, callable $callback): void 255 | { 256 | $this->customMapping[$property] = $callback; 257 | } 258 | 259 | /** 260 | * Whether or not attribute checking code should be generated. 261 | */ 262 | public function setAttributeChecking(bool $attributeChecking): void 263 | { 264 | $this->attributeChecking = $attributeChecking; 265 | } 266 | 267 | private function buildPropertyMapping(): void 268 | { 269 | $this->propertiesMapping = []; 270 | 271 | foreach ($this->mappingExtractor->getPropertiesMapping($this) as $propertyMapping) { 272 | $this->propertiesMapping[$propertyMapping->getProperty()] = $propertyMapping; 273 | } 274 | 275 | foreach ($this->customMapping as $property => $callback) { 276 | $this->propertiesMapping[$property] = new PropertyMapping( 277 | new ReadAccessor(ReadAccessor::TYPE_SOURCE, $property), 278 | $this->mappingExtractor->getWriteMutator($this->source, $this->target, $property), 279 | null, 280 | new CallbackTransformer($property), 281 | $property, 282 | false 283 | ); 284 | } 285 | } 286 | 287 | private function checkCircularMapperConfiguration(MapperGeneratorMetadataInterface $configuration, &$checked): bool 288 | { 289 | foreach ($configuration->getPropertiesMapping() as $propertyMapping) { 290 | if (!$propertyMapping->getTransformer() instanceof DependentTransformerInterface) { 291 | continue; 292 | } 293 | 294 | foreach ($propertyMapping->getTransformer()->getDependencies() as $dependency) { 295 | if (isset($checked[$dependency->getName()])) { 296 | continue; 297 | } 298 | 299 | $checked[$dependency->getName()] = true; 300 | 301 | if ($dependency->getSource() === $this->getSource() && $dependency->getTarget() === $this->getTarget()) { 302 | return true; 303 | } 304 | 305 | $subConfiguration = $this->metadataRegistry->getMetadata($dependency->getSource(), $dependency->getTarget()); 306 | 307 | if (null !== $subConfiguration && true === $this->checkCircularMapperConfiguration($subConfiguration, $checked)) { 308 | return true; 309 | } 310 | } 311 | } 312 | 313 | return false; 314 | } 315 | 316 | public function isTargetReadOnlyClass(): bool 317 | { 318 | return $this->isTargetReadOnlyClass; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /MapperMetadataInterface.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface MapperMetadataInterface 13 | { 14 | /** 15 | * Get the source type mapped. 16 | */ 17 | public function getSource(): string; 18 | 19 | /** 20 | * Get the target type mapped. 21 | */ 22 | public function getTarget(): string; 23 | 24 | /** 25 | * Check if the target is a read-only class. 26 | */ 27 | public function isTargetReadOnlyClass(): bool; 28 | 29 | /** 30 | * Get properties to map between source and target. 31 | * 32 | * @return PropertyMapping[] 33 | */ 34 | public function getPropertiesMapping(): array; 35 | 36 | /** 37 | * Get property to map by name, or null if not mapped. 38 | */ 39 | public function getPropertyMapping(string $property): ?PropertyMapping; 40 | 41 | /** 42 | * Get date time format to use when mapping date time to string. 43 | */ 44 | public function getDateTimeFormat(): string; 45 | } 46 | -------------------------------------------------------------------------------- /Normalizer/AutoMapperNormalizer.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class AutoMapperNormalizer implements NormalizerInterface, DenormalizerInterface, CacheableSupportsMethodInterface 19 | { 20 | private const SERIALIZER_CONTEXT_MAPPING = [ 21 | AbstractNormalizer::GROUPS => MapperContext::GROUPS, 22 | AbstractNormalizer::ATTRIBUTES => MapperContext::ALLOWED_ATTRIBUTES, 23 | AbstractNormalizer::IGNORED_ATTRIBUTES => MapperContext::IGNORED_ATTRIBUTES, 24 | AbstractNormalizer::OBJECT_TO_POPULATE => MapperContext::TARGET_TO_POPULATE, 25 | AbstractNormalizer::CIRCULAR_REFERENCE_LIMIT => MapperContext::CIRCULAR_REFERENCE_LIMIT, 26 | AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => MapperContext::CIRCULAR_REFERENCE_HANDLER, 27 | DateTimeNormalizer::FORMAT_KEY => MapperContext::DATETIME_FORMAT, 28 | ]; 29 | 30 | private $autoMapper; 31 | 32 | public function __construct(AutoMapperInterface $autoMapper) 33 | { 34 | $this->autoMapper = $autoMapper; 35 | } 36 | 37 | public function normalize($object, $format = null, array $context = []) 38 | { 39 | return $this->autoMapper->map($object, 'array', $this->createAutoMapperContext($context)); 40 | } 41 | 42 | public function denormalize($data, $class, $format = null, array $context = []) 43 | { 44 | return $this->autoMapper->map($data, $class, $this->createAutoMapperContext($context)); 45 | } 46 | 47 | public function supportsNormalization($data, $format = null): bool 48 | { 49 | if (!\is_object($data) || $data instanceof \stdClass) { 50 | return false; 51 | } 52 | 53 | return $this->autoMapper->hasMapper(\get_class($data), 'array'); 54 | } 55 | 56 | public function supportsDenormalization($data, $type, $format = null) 57 | { 58 | return $this->autoMapper->hasMapper('array', $type); 59 | } 60 | 61 | public function hasCacheableSupportsMethod(): bool 62 | { 63 | return true; 64 | } 65 | 66 | private function createAutoMapperContext(array $serializerContext = []): array 67 | { 68 | $context = []; 69 | 70 | foreach (self::SERIALIZER_CONTEXT_MAPPING as $serializerContextName => $autoMapperContextName) { 71 | $context[$autoMapperContextName] = $serializerContext[$serializerContextName] ?? null; 72 | unset($serializerContext[$serializerContextName]); 73 | } 74 | 75 | if (\array_key_exists(AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS, $serializerContext) && is_iterable($serializerContext[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS])) { 76 | foreach ($serializerContext[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS] as $class => $keyArgs) { 77 | foreach ($keyArgs as $key => $value) { 78 | $context[MapperContext::CONSTRUCTOR_ARGUMENTS][$class][$key] = $value; 79 | } 80 | } 81 | 82 | unset($serializerContext[AbstractNormalizer::DEFAULT_CONSTRUCTOR_ARGUMENTS]); 83 | } 84 | 85 | return $context + $serializerContext; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AutoMapper (legacy) 2 | 3 | See https://github.com/jolicode/automapper for new features and updates 🚀 4 | -------------------------------------------------------------------------------- /Transformer/AbstractArrayTransformer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | abstract class AbstractArrayTransformer implements TransformerInterface, DependentTransformerInterface 16 | { 17 | private $itemTransformer; 18 | 19 | public function __construct(TransformerInterface $itemTransformer) 20 | { 21 | $this->itemTransformer = $itemTransformer; 22 | } 23 | 24 | abstract protected function getAssignExpr(Expr $valuesVar, Expr $outputVar, Expr $loopKeyVar, bool $assignByRef): Expr; 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 30 | { 31 | $valuesVar = new Expr\Variable($uniqueVariableScope->getUniqueName('values')); 32 | $statements = [ 33 | new Stmt\Expression(new Expr\Assign($valuesVar, new Expr\Array_())), 34 | ]; 35 | 36 | $loopValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('value')); 37 | $loopKeyVar = new Expr\Variable($uniqueVariableScope->getUniqueName('key')); 38 | 39 | $assignByRef = $this->itemTransformer instanceof AssignedByReferenceTransformerInterface && $this->itemTransformer->assignByRef(); 40 | 41 | [$output, $itemStatements] = $this->itemTransformer->transform($loopValueVar, $target, $propertyMapping, $uniqueVariableScope); 42 | 43 | if ($propertyMapping->getWriteMutator() && $propertyMapping->getWriteMutator()->getType() === WriteMutator::TYPE_ADDER_AND_REMOVER) { 44 | $mappedValueVar = new Expr\Variable($uniqueVariableScope->getUniqueName('mappedValue')); 45 | $itemStatements[] = new Stmt\Expression(new Expr\Assign($mappedValueVar, $output)); 46 | $itemStatements[] = new Stmt\If_(new Expr\BinaryOp\NotIdentical(new Expr\ConstFetch(new Name('null')), $mappedValueVar), [ 47 | 'stmts' => [ 48 | new Stmt\Expression($propertyMapping->getWriteMutator()->getExpression($target, $mappedValueVar, $assignByRef)), 49 | ], 50 | ]); 51 | } else { 52 | $itemStatements[] = new Stmt\Expression($this->getAssignExpr($valuesVar, $output, $loopKeyVar, $assignByRef)); 53 | } 54 | 55 | $statements[] = new Stmt\Foreach_($input, $loopValueVar, [ 56 | 'stmts' => $itemStatements, 57 | 'keyVar' => $loopKeyVar, 58 | ]); 59 | 60 | return [$valuesVar, $statements]; 61 | } 62 | 63 | /** 64 | * {@inheritdoc} 65 | */ 66 | public function getDependencies(): array 67 | { 68 | if (!$this->itemTransformer instanceof DependentTransformerInterface) { 69 | return []; 70 | } 71 | 72 | return $this->itemTransformer->getDependencies(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Transformer/AbstractUniqueTypeTransformerFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | abstract class AbstractUniqueTypeTransformerFactory implements TransformerFactoryInterface 14 | { 15 | /** 16 | * {@inheritdoc} 17 | */ 18 | public function getTransformer(?array $sourceTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 19 | { 20 | $nbSourceTypes = $sourceTypes ? \count($sourceTypes) : 0; 21 | $nbTargetTypes = $targetTypes ? \count($targetTypes) : 0; 22 | 23 | if (0 === $nbSourceTypes || $nbSourceTypes > 1 || !$sourceTypes[0] instanceof Type) { 24 | return null; 25 | } 26 | 27 | if (0 === $nbTargetTypes || $nbTargetTypes > 1 || !$targetTypes[0] instanceof Type) { 28 | return null; 29 | } 30 | 31 | return $this->createTransformer($sourceTypes[0], $targetTypes[0], $mapperMetadata); 32 | } 33 | 34 | abstract protected function createTransformer(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface; 35 | } 36 | -------------------------------------------------------------------------------- /Transformer/ArrayTransformer.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | 13 | /** 14 | * Transformer array decorator. 15 | * 16 | * @author Joel Wurtz 17 | */ 18 | final class ArrayTransformer extends AbstractArrayTransformer 19 | { 20 | protected function getAssignExpr(Expr $valuesVar, Expr $outputVar, Expr $loopKeyVar, bool $assignByRef): Expr 21 | { 22 | if ($assignByRef) { 23 | return new Expr\AssignRef(new Expr\ArrayDimFetch($valuesVar), $outputVar); 24 | } 25 | 26 | return new Expr\Assign(new Expr\ArrayDimFetch($valuesVar), $outputVar); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Transformer/ArrayTransformerFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class ArrayTransformerFactory extends AbstractUniqueTypeTransformerFactory implements PrioritizedTransformerFactoryInterface 14 | { 15 | private $chainTransformerFactory; 16 | 17 | public function __construct(ChainTransformerFactory $chainTransformerFactory) 18 | { 19 | $this->chainTransformerFactory = $chainTransformerFactory; 20 | } 21 | 22 | /** 23 | * {@inheritdoc} 24 | */ 25 | protected function createTransformer(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 26 | { 27 | if (!$sourceType->isCollection()) { 28 | return null; 29 | } 30 | 31 | if (!$targetType->isCollection()) { 32 | return null; 33 | } 34 | 35 | if ([] === $sourceType->getCollectionValueTypes() || [] === $targetType->getCollectionValueTypes()) { 36 | return new CopyTransformer(); 37 | } 38 | 39 | $subItemTransformer = $this->chainTransformerFactory->getTransformer($sourceType->getCollectionValueTypes(), $targetType->getCollectionValueTypes(), $mapperMetadata); 40 | 41 | if (null !== $subItemTransformer) { 42 | $sourceCollectionKeyTypes = $sourceType->getCollectionKeyTypes(); 43 | $sourceCollectionKeyType = $sourceCollectionKeyTypes[0] ?? null; 44 | 45 | if ($sourceCollectionKeyType instanceof Type && Type::BUILTIN_TYPE_INT !== $sourceCollectionKeyType->getBuiltinType()) { 46 | return new DictionaryTransformer($subItemTransformer); 47 | } 48 | 49 | return new ArrayTransformer($subItemTransformer); 50 | } 51 | 52 | return null; 53 | } 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | public function getPriority(): int 59 | { 60 | return 4; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Transformer/AssignedByReferenceTransformerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface AssignedByReferenceTransformerInterface 9 | { 10 | /** 11 | * Should the resulting output be assigned by ref. 12 | */ 13 | public function assignByRef(): bool; 14 | } 15 | -------------------------------------------------------------------------------- /Transformer/BuiltinTransformer.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class BuiltinTransformer implements TransformerInterface 19 | { 20 | private const CAST_MAPPING = [ 21 | Type::BUILTIN_TYPE_BOOL => [ 22 | Type::BUILTIN_TYPE_INT => Cast\Int_::class, 23 | Type::BUILTIN_TYPE_STRING => Cast\String_::class, 24 | Type::BUILTIN_TYPE_FLOAT => Cast\Double::class, 25 | Type::BUILTIN_TYPE_ARRAY => 'toArray', 26 | Type::BUILTIN_TYPE_ITERABLE => 'toArray', 27 | ], 28 | Type::BUILTIN_TYPE_FLOAT => [ 29 | Type::BUILTIN_TYPE_STRING => Cast\String_::class, 30 | Type::BUILTIN_TYPE_INT => Cast\Int_::class, 31 | Type::BUILTIN_TYPE_BOOL => Cast\Bool_::class, 32 | Type::BUILTIN_TYPE_ARRAY => 'toArray', 33 | Type::BUILTIN_TYPE_ITERABLE => 'toArray', 34 | ], 35 | Type::BUILTIN_TYPE_INT => [ 36 | Type::BUILTIN_TYPE_FLOAT => Cast\Double::class, 37 | Type::BUILTIN_TYPE_STRING => Cast\String_::class, 38 | Type::BUILTIN_TYPE_BOOL => Cast\Bool_::class, 39 | Type::BUILTIN_TYPE_ARRAY => 'toArray', 40 | Type::BUILTIN_TYPE_ITERABLE => 'toArray', 41 | ], 42 | Type::BUILTIN_TYPE_ITERABLE => [ 43 | Type::BUILTIN_TYPE_ARRAY => 'fromIteratorToArray', 44 | ], 45 | Type::BUILTIN_TYPE_ARRAY => [], 46 | Type::BUILTIN_TYPE_STRING => [ 47 | Type::BUILTIN_TYPE_ARRAY => 'toArray', 48 | Type::BUILTIN_TYPE_ITERABLE => 'toArray', 49 | Type::BUILTIN_TYPE_FLOAT => Cast\Double::class, 50 | Type::BUILTIN_TYPE_INT => Cast\Int_::class, 51 | Type::BUILTIN_TYPE_BOOL => Cast\Bool_::class, 52 | ], 53 | Type::BUILTIN_TYPE_CALLABLE => [], 54 | Type::BUILTIN_TYPE_RESOURCE => [], 55 | ]; 56 | 57 | /** @var Type */ 58 | private $sourceType; 59 | 60 | /** @var Type[] */ 61 | private $targetTypes; 62 | 63 | public function __construct(Type $sourceType, array $targetTypes) 64 | { 65 | $this->sourceType = $sourceType; 66 | $this->targetTypes = $targetTypes; 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 73 | { 74 | $targetTypes = array_map(function (Type $type) { 75 | return $type->getBuiltinType(); 76 | }, $this->targetTypes); 77 | 78 | // Source type is in target => no cast 79 | if (\in_array($this->sourceType->getBuiltinType(), $targetTypes, true)) { 80 | return [$input, []]; 81 | } 82 | 83 | // Cast needed 84 | foreach (self::CAST_MAPPING[$this->sourceType->getBuiltinType()] as $castType => $castMethod) { 85 | if (\in_array($castType, $targetTypes, true)) { 86 | if (method_exists($this, $castMethod)) { 87 | return [$this->$castMethod($input), []]; 88 | } 89 | 90 | return [new $castMethod($input), []]; 91 | } 92 | } 93 | 94 | return [$input, []]; 95 | } 96 | 97 | private function toArray(Expr $input) 98 | { 99 | return new Expr\Array_([new Expr\ArrayItem($input)]); 100 | } 101 | 102 | private function fromIteratorToArray(Expr $input) 103 | { 104 | return new Expr\FuncCall(new Name('iterator_to_array'), [ 105 | new Arg($input), 106 | ]); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Transformer/BuiltinTransformerFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class BuiltinTransformerFactory implements TransformerFactoryInterface, PrioritizedTransformerFactoryInterface 12 | { 13 | private const BUILTIN = [ 14 | Type::BUILTIN_TYPE_BOOL, 15 | Type::BUILTIN_TYPE_CALLABLE, 16 | Type::BUILTIN_TYPE_FLOAT, 17 | Type::BUILTIN_TYPE_INT, 18 | Type::BUILTIN_TYPE_ITERABLE, 19 | Type::BUILTIN_TYPE_NULL, 20 | Type::BUILTIN_TYPE_RESOURCE, 21 | Type::BUILTIN_TYPE_STRING, 22 | ]; 23 | 24 | public function getTransformer(?array $sourceTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 25 | { 26 | $nbSourceTypes = $sourceTypes ? \count($sourceTypes) : 0; 27 | 28 | if (null === $sourceTypes || 0 === $nbSourceTypes || $nbSourceTypes > 1 || !$sourceTypes[0] instanceof Type) { 29 | return null; 30 | } 31 | 32 | $propertyType = $sourceTypes[0]; 33 | 34 | if (null !== $targetTypes && \in_array($propertyType->getBuiltinType(), self::BUILTIN, true)) { 35 | return new BuiltinTransformer($propertyType, $targetTypes); 36 | } 37 | 38 | return null; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function getPriority(): int 45 | { 46 | return 8; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Transformer/CallbackTransformer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class CallbackTransformer implements TransformerInterface 17 | { 18 | private $callbackName; 19 | 20 | public function __construct(string $callbackName) 21 | { 22 | $this->callbackName = $callbackName; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 29 | { 30 | /* 31 | * $output = $this->callbacks[$callbackName]($input); 32 | */ 33 | 34 | $arguments = [ 35 | new Arg($input), 36 | ]; 37 | if ($target instanceof Expr) { 38 | $arguments[] = new Arg($target); 39 | } 40 | 41 | return [new Expr\FuncCall( 42 | new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'callbacks'), new Scalar\String_($this->callbackName)), $arguments), 43 | [], 44 | ]; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Transformer/ChainTransformerFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class ChainTransformerFactory implements TransformerFactoryInterface 11 | { 12 | /** @var TransformerFactoryInterface[] */ 13 | private $factories = []; 14 | 15 | /** @var TransformerFactoryInterface[]|null */ 16 | private $sorted; 17 | 18 | /** 19 | * Biggest priority is MultipleTransformerFactory with 128, so default priority will be bigger in order to 20 | * be used before it, 256 should be enough. 21 | */ 22 | public function addTransformerFactory(TransformerFactoryInterface $transformerFactory, int $priority = 256): void 23 | { 24 | $this->sorted = null; 25 | 26 | if ($transformerFactory instanceof PrioritizedTransformerFactoryInterface) { 27 | $priority = $transformerFactory->getPriority(); 28 | } 29 | 30 | if (!\array_key_exists($priority, $this->factories)) { 31 | $this->factories[$priority] = []; 32 | } 33 | $this->factories[$priority][] = $transformerFactory; 34 | } 35 | 36 | public function hasTransformerFactory(TransformerFactoryInterface $transformerFactory): bool 37 | { 38 | $this->sortFactories(); 39 | 40 | $transformerFactoryClass = \get_class($transformerFactory); 41 | foreach ($this->sorted as $factory) { 42 | if (is_a($factory, $transformerFactoryClass)) { 43 | return true; 44 | } 45 | } 46 | 47 | return false; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function getTransformer(?array $sourceTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 54 | { 55 | $this->sortFactories(); 56 | 57 | foreach ($this->sorted as $factory) { 58 | $transformer = $factory->getTransformer($sourceTypes, $targetTypes, $mapperMetadata); 59 | 60 | if (null !== $transformer) { 61 | return $transformer; 62 | } 63 | } 64 | 65 | return null; 66 | } 67 | 68 | private function sortFactories(): void 69 | { 70 | if (null === $this->sorted) { 71 | $this->sorted = []; 72 | krsort($this->factories); 73 | 74 | foreach ($this->factories as $prioritisedFactories) { 75 | foreach ($prioritisedFactories as $factory) { 76 | $this->sorted[] = $factory; 77 | } 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Transformer/CopyEnumTransformer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class CopyEnumTransformer implements TransformerInterface 15 | { 16 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 17 | { 18 | return [$input, []]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Transformer/CopyTransformer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class CopyTransformer implements TransformerInterface 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 20 | { 21 | return [$input, []]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Transformer/DateTimeImmutableToMutableTransformer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class DateTimeImmutableToMutableTransformer implements TransformerInterface 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 23 | { 24 | return [ 25 | new Expr\StaticCall(new Name\FullyQualified(\DateTime::class), 'createFromFormat', [ 26 | new Arg(new String_(\DateTime::RFC3339)), 27 | new Arg(new Expr\MethodCall($input, 'format', [ 28 | new Arg(new String_(\DateTime::RFC3339)), 29 | ])), 30 | ]), 31 | [], 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Transformer/DateTimeMutableToImmutableTransformer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class DateTimeMutableToImmutableTransformer implements TransformerInterface 17 | { 18 | /** 19 | * {@inheritdoc} 20 | */ 21 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 22 | { 23 | return [ 24 | new Expr\StaticCall(new Name\FullyQualified(\DateTimeImmutable::class), 'createFromMutable', [ 25 | new Arg($input), 26 | ]), 27 | [], 28 | ]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Transformer/DateTimeToStringTransformer.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | final class DateTimeToStringTransformer implements TransformerInterface 18 | { 19 | private $format; 20 | 21 | public function __construct(string $format = \DateTimeInterface::RFC3339) 22 | { 23 | $this->format = $format; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 30 | { 31 | return [new Expr\MethodCall($input, 'format', [ 32 | new Arg( 33 | new Expr\BinaryOp\Coalesce( 34 | new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(MapperContext::DATETIME_FORMAT)), 35 | new Scalar\String_($this->format), 36 | ) 37 | ), 38 | ]), []]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Transformer/DateTimeTransformerFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class DateTimeTransformerFactory extends AbstractUniqueTypeTransformerFactory implements PrioritizedTransformerFactoryInterface 12 | { 13 | /** 14 | * {@inheritdoc} 15 | */ 16 | protected function createTransformer(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 17 | { 18 | $isSourceDate = $this->isDateTimeType($sourceType); 19 | $isTargetDate = $this->isDateTimeType($targetType); 20 | 21 | if ($isSourceDate && $isTargetDate) { 22 | return $this->createTransformerForSourceAndTarget($sourceType, $targetType); 23 | } 24 | 25 | if ($isSourceDate) { 26 | return $this->createTransformerForSource($targetType, $mapperMetadata); 27 | } 28 | 29 | if ($isTargetDate) { 30 | return $this->createTransformerForTarget($sourceType, $targetType, $mapperMetadata); 31 | } 32 | 33 | return null; 34 | } 35 | 36 | protected function createTransformerForSourceAndTarget(Type $sourceType, Type $targetType): ?TransformerInterface 37 | { 38 | $isSourceMutable = $this->isDateTimeMutable($sourceType); 39 | $isTargetMutable = $this->isDateTimeMutable($targetType); 40 | 41 | if ($isSourceMutable === $isTargetMutable) { 42 | return new CopyTransformer(); 43 | } 44 | 45 | if ($isSourceMutable) { 46 | return new DateTimeMutableToImmutableTransformer(); 47 | } 48 | 49 | return new DateTimeImmutableToMutableTransformer(); 50 | } 51 | 52 | protected function createTransformerForSource(Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 53 | { 54 | if (Type::BUILTIN_TYPE_STRING === $targetType->getBuiltinType()) { 55 | return new DateTimeToStringTransformer($mapperMetadata->getDateTimeFormat()); 56 | } 57 | 58 | return null; 59 | } 60 | 61 | protected function createTransformerForTarget(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 62 | { 63 | if (Type::BUILTIN_TYPE_STRING === $sourceType->getBuiltinType()) { 64 | return new StringToDateTimeTransformer($this->getClassName($targetType), $mapperMetadata->getDateTimeFormat()); 65 | } 66 | 67 | return null; 68 | } 69 | 70 | private function isDateTimeType(Type $type): bool 71 | { 72 | if (Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) { 73 | return false; 74 | } 75 | 76 | if (\DateTimeInterface::class !== $type->getClassName() && !is_subclass_of($type->getClassName(), \DateTimeInterface::class)) { 77 | return false; 78 | } 79 | 80 | return true; 81 | } 82 | 83 | private function getClassName(Type $type): ?string 84 | { 85 | if (\DateTimeInterface::class !== $type->getClassName()) { 86 | return \DateTimeImmutable::class; 87 | } 88 | 89 | return $type->getClassName(); 90 | } 91 | 92 | private function isDateTimeMutable(Type $type): bool 93 | { 94 | if (\DateTime::class !== $type->getClassName() && !is_subclass_of($type->getClassName(), \DateTime::class)) { 95 | return false; 96 | } 97 | 98 | return true; 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function getPriority(): int 105 | { 106 | return 16; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Transformer/DependentTransformerInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface DependentTransformerInterface 9 | { 10 | /** 11 | * Get dependencies for this transformer. 12 | * 13 | * @return MapperDependency[] 14 | */ 15 | public function getDependencies(): array; 16 | } 17 | -------------------------------------------------------------------------------- /Transformer/DictionaryTransformer.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class DictionaryTransformer extends AbstractArrayTransformer 13 | { 14 | protected function getAssignExpr(Expr $valuesVar, Expr $outputVar, Expr $loopKeyVar, bool $assignByRef): Expr 15 | { 16 | if ($assignByRef) { 17 | return new Expr\AssignRef(new Expr\ArrayDimFetch($valuesVar, $loopKeyVar), $outputVar); 18 | } 19 | 20 | return new Expr\Assign(new Expr\ArrayDimFetch($valuesVar, $loopKeyVar), $outputVar); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Transformer/EnumTransformerFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class EnumTransformerFactory extends AbstractUniqueTypeTransformerFactory implements PrioritizedTransformerFactoryInterface 12 | { 13 | protected function createTransformer(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 14 | { 15 | // source is enum, target isn't 16 | if ($this->isEnumType($sourceType, true) && !$this->isEnumType($targetType)) { 17 | return new SourceEnumTransformer(); 18 | } 19 | 20 | // target is enum, source isn't 21 | if (!$this->isEnumType($sourceType) && $this->isEnumType($targetType, true)) { 22 | return new TargetEnumTransformer($targetType->getClassName()); 23 | } 24 | 25 | // both source & target are enums 26 | if ($this->isEnumType($sourceType) && $this->isEnumType($targetType)) { 27 | return new CopyEnumTransformer(); 28 | } 29 | 30 | return null; 31 | } 32 | 33 | private function isEnumType(Type $type, bool $backed = false): bool 34 | { 35 | if (Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) { 36 | return false; 37 | } 38 | 39 | if (!is_subclass_of($type->getClassName(), \UnitEnum::class)) { 40 | return false; 41 | } 42 | 43 | if ($backed && !is_subclass_of($type->getClassName(), \BackedEnum::class)) { 44 | return false; 45 | } 46 | 47 | return true; 48 | } 49 | 50 | public function getPriority(): int 51 | { 52 | return 3; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Transformer/MapperDependency.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class MapperDependency 13 | { 14 | private $name; 15 | 16 | private $source; 17 | 18 | private $target; 19 | 20 | public function __construct(string $name, string $source, string $target) 21 | { 22 | $this->name = $name; 23 | $this->source = $source; 24 | $this->target = $target; 25 | } 26 | 27 | public function getName(): string 28 | { 29 | return $this->name; 30 | } 31 | 32 | public function getSource(): string 33 | { 34 | return $this->source; 35 | } 36 | 37 | public function getTarget(): string 38 | { 39 | return $this->target; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Transformer/MultipleTransformer.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | final class MultipleTransformer implements TransformerInterface, DependentTransformerInterface 22 | { 23 | private const CONDITION_MAPPING = [ 24 | Type::BUILTIN_TYPE_BOOL => 'is_bool', 25 | Type::BUILTIN_TYPE_INT => 'is_int', 26 | Type::BUILTIN_TYPE_FLOAT => 'is_float', 27 | Type::BUILTIN_TYPE_STRING => 'is_string', 28 | Type::BUILTIN_TYPE_NULL => 'is_null', 29 | Type::BUILTIN_TYPE_ARRAY => 'is_array', 30 | Type::BUILTIN_TYPE_OBJECT => 'is_object', 31 | Type::BUILTIN_TYPE_RESOURCE => 'is_resource', 32 | Type::BUILTIN_TYPE_CALLABLE => 'is_callable', 33 | Type::BUILTIN_TYPE_ITERABLE => 'is_iterable', 34 | ]; 35 | 36 | private $transformers = []; 37 | 38 | public function __construct(array $transformers) 39 | { 40 | $this->transformers = $transformers; 41 | } 42 | 43 | /** 44 | * {@inheritdoc} 45 | */ 46 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 47 | { 48 | $output = new Expr\Variable($uniqueVariableScope->getUniqueName('value')); 49 | $statements = [ 50 | new Stmt\Expression(new Expr\Assign($output, $input)), 51 | ]; 52 | 53 | foreach ($this->transformers as $transformerData) { 54 | $transformer = $transformerData['transformer']; 55 | $type = $transformerData['type']; 56 | 57 | [$transformerOutput, $transformerStatements] = $transformer->transform($input, $target, $propertyMapping, $uniqueVariableScope); 58 | 59 | $assignClass = ($transformer instanceof AssignedByReferenceTransformerInterface && $transformer->assignByRef()) ? Expr\AssignRef::class : Expr\Assign::class; 60 | $statements[] = new Stmt\If_( 61 | new Expr\FuncCall( 62 | new Name(self::CONDITION_MAPPING[$type->getBuiltinType()]), 63 | [ 64 | new Arg($input), 65 | ] 66 | ), 67 | [ 68 | 'stmts' => array_merge( 69 | $transformerStatements, [ 70 | new Stmt\Expression(new $assignClass($output, $transformerOutput)), 71 | ] 72 | ), 73 | ] 74 | ); 75 | } 76 | 77 | return [$output, $statements]; 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function getDependencies(): array 84 | { 85 | $dependencies = []; 86 | 87 | foreach ($this->transformers as $transformerData) { 88 | if ($transformerData['transformer'] instanceof DependentTransformerInterface) { 89 | $dependencies = array_merge($dependencies, $transformerData['transformer']->getDependencies()); 90 | } 91 | } 92 | 93 | return $dependencies; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Transformer/MultipleTransformerFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class MultipleTransformerFactory implements TransformerFactoryInterface, PrioritizedTransformerFactoryInterface 11 | { 12 | private $chainTransformerFactory; 13 | 14 | public function __construct(ChainTransformerFactory $chainTransformerFactory) 15 | { 16 | $this->chainTransformerFactory = $chainTransformerFactory; 17 | } 18 | 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function getTransformer(?array $sourceTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 23 | { 24 | if (null === $sourceTypes || \count($sourceTypes) <= 1) { 25 | return null; 26 | } 27 | 28 | $transformers = []; 29 | 30 | foreach ($sourceTypes as $sourceType) { 31 | $transformer = $this->chainTransformerFactory->getTransformer([$sourceType], $targetTypes, $mapperMetadata); 32 | 33 | if (null !== $transformer) { 34 | $transformers[] = [ 35 | 'transformer' => $transformer, 36 | 'type' => $sourceType, 37 | ]; 38 | } 39 | } 40 | 41 | if (\count($transformers) > 1) { 42 | return new MultipleTransformer($transformers); 43 | } 44 | 45 | if (\count($transformers) === 1) { 46 | return $transformers[0]['transformer']; 47 | } 48 | 49 | return null; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function getPriority(): int 56 | { 57 | return 128; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Transformer/NullableTransformer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class NullableTransformer implements TransformerInterface, DependentTransformerInterface 17 | { 18 | private $itemTransformer; 19 | private $isTargetNullable; 20 | 21 | public function __construct(TransformerInterface $itemTransformer, bool $isTargetNullable) 22 | { 23 | $this->itemTransformer = $itemTransformer; 24 | $this->isTargetNullable = $isTargetNullable; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 31 | { 32 | [$output, $itemStatements] = $this->itemTransformer->transform($input, $target, $propertyMapping, $uniqueVariableScope); 33 | 34 | $newOutput = null; 35 | $statements = []; 36 | $assignClass = ($this->itemTransformer instanceof AssignedByReferenceTransformerInterface && $this->itemTransformer->assignByRef()) ? Expr\AssignRef::class : Expr\Assign::class; 37 | 38 | if ($this->isTargetNullable) { 39 | $newOutput = new Expr\Variable($uniqueVariableScope->getUniqueName('value')); 40 | $statements[] = new Stmt\Expression(new Expr\Assign($newOutput, new Expr\ConstFetch(new Name('null')))); 41 | $itemStatements[] = new Stmt\Expression(new $assignClass($newOutput, $output)); 42 | } 43 | 44 | $statements[] = new Stmt\If_(new Expr\BinaryOp\NotIdentical(new Expr\ConstFetch(new Name('null')), $input), [ 45 | 'stmts' => $itemStatements, 46 | ]); 47 | 48 | return [$newOutput ?? $output, $statements]; 49 | } 50 | 51 | /** 52 | * {@inheritdoc} 53 | */ 54 | public function getDependencies(): array 55 | { 56 | if (!$this->itemTransformer instanceof DependentTransformerInterface) { 57 | return []; 58 | } 59 | 60 | return $this->itemTransformer->getDependencies(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Transformer/NullableTransformerFactory.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | final class NullableTransformerFactory implements TransformerFactoryInterface, PrioritizedTransformerFactoryInterface 12 | { 13 | private $chainTransformerFactory; 14 | 15 | public function __construct(ChainTransformerFactory $chainTransformerFactory) 16 | { 17 | $this->chainTransformerFactory = $chainTransformerFactory; 18 | } 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function getTransformer(?array $sourceTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 24 | { 25 | $nbSourceTypes = $sourceTypes ? \count($sourceTypes) : 0; 26 | 27 | if (null === $sourceTypes || 0 === $nbSourceTypes || $nbSourceTypes > 1) { 28 | return null; 29 | } 30 | 31 | $propertyType = $sourceTypes[0]; 32 | 33 | if (!$propertyType->isNullable()) { 34 | return null; 35 | } 36 | 37 | $isTargetNullable = false; 38 | 39 | foreach ($targetTypes as $targetType) { 40 | if ($targetType->isNullable()) { 41 | $isTargetNullable = true; 42 | 43 | break; 44 | } 45 | } 46 | 47 | $subTransformer = $this->chainTransformerFactory->getTransformer([new Type( 48 | $propertyType->getBuiltinType(), 49 | false, 50 | $propertyType->getClassName(), 51 | $propertyType->isCollection(), 52 | $propertyType->getCollectionKeyTypes(), 53 | $propertyType->getCollectionValueTypes() 54 | )], $targetTypes, $mapperMetadata); 55 | 56 | if (null === $subTransformer) { 57 | return null; 58 | } 59 | 60 | // Remove nullable property here to avoid infinite loop 61 | return new NullableTransformer($subTransformer, $isTargetNullable); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function getPriority(): int 68 | { 69 | return 64; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Transformer/ObjectTransformer.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | final class ObjectTransformer implements TransformerInterface, DependentTransformerInterface, AssignedByReferenceTransformerInterface 20 | { 21 | private $sourceType; 22 | 23 | private $targetType; 24 | 25 | public function __construct(Type $sourceType, Type $targetType) 26 | { 27 | $this->sourceType = $sourceType; 28 | $this->targetType = $targetType; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 35 | { 36 | $mapperName = $this->getDependencyName(); 37 | 38 | return [new Expr\MethodCall(new Expr\ArrayDimFetch( 39 | new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'), 40 | new Scalar\String_($mapperName) 41 | ), 'map', [ 42 | new Arg($input), 43 | new Arg(new Expr\StaticCall(new Name\FullyQualified(MapperContext::class), 'withNewContext', [ 44 | new Arg(new Expr\Variable('context')), 45 | new Arg(new Scalar\String_($propertyMapping->getProperty())), 46 | ])), 47 | ]), []]; 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function assignByRef(): bool 54 | { 55 | return true; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function getDependencies(): array 62 | { 63 | return [new MapperDependency($this->getDependencyName(), $this->getSource(), $this->getTarget())]; 64 | } 65 | 66 | private function getDependencyName(): string 67 | { 68 | return 'Mapper_' . $this->getSource() . '_' . $this->getTarget(); 69 | } 70 | 71 | private function getSource(): string 72 | { 73 | $sourceTypeName = 'array'; 74 | 75 | if (Type::BUILTIN_TYPE_OBJECT === $this->sourceType->getBuiltinType()) { 76 | $sourceTypeName = $this->sourceType->getClassName(); 77 | } 78 | 79 | return $sourceTypeName; 80 | } 81 | 82 | private function getTarget(): string 83 | { 84 | $targetTypeName = 'array'; 85 | 86 | if (Type::BUILTIN_TYPE_OBJECT === $this->targetType->getBuiltinType()) { 87 | $targetTypeName = $this->targetType->getClassName(); 88 | } 89 | 90 | return $targetTypeName; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Transformer/ObjectTransformerFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class ObjectTransformerFactory extends AbstractUniqueTypeTransformerFactory implements PrioritizedTransformerFactoryInterface 13 | { 14 | private $autoMapper; 15 | 16 | public function __construct(AutoMapperRegistryInterface $autoMapper) 17 | { 18 | $this->autoMapper = $autoMapper; 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | protected function createTransformer(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 25 | { 26 | // Only deal with source type being an object or an array that is not a collection 27 | if (!$this->isObjectType($sourceType) || !$this->isObjectType($targetType)) { 28 | return null; 29 | } 30 | 31 | $sourceTypeName = 'array'; 32 | $targetTypeName = 'array'; 33 | 34 | if (Type::BUILTIN_TYPE_OBJECT === $sourceType->getBuiltinType()) { 35 | $sourceTypeName = $sourceType->getClassName(); 36 | } 37 | 38 | if (Type::BUILTIN_TYPE_OBJECT === $targetType->getBuiltinType()) { 39 | $targetTypeName = $targetType->getClassName(); 40 | } 41 | 42 | if (null !== $sourceTypeName && null !== $targetTypeName && $this->autoMapper->hasMapper($sourceTypeName, $targetTypeName)) { 43 | return new ObjectTransformer($sourceType, $targetType); 44 | } 45 | 46 | return null; 47 | } 48 | 49 | private function isObjectType(Type $type): bool 50 | { 51 | if (!\in_array($type->getBuiltinType(), [Type::BUILTIN_TYPE_OBJECT, Type::BUILTIN_TYPE_ARRAY])) { 52 | return false; 53 | } 54 | 55 | if (Type::BUILTIN_TYPE_ARRAY === $type->getBuiltinType() && $type->isCollection()) { 56 | return false; 57 | } 58 | 59 | if (is_subclass_of($type->getClassName(), \UnitEnum::class)) { 60 | return false; 61 | } 62 | 63 | return true; 64 | } 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function getPriority(): int 70 | { 71 | return 2; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Transformer/PrioritizedTransformerFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | interface PrioritizedTransformerFactoryInterface 9 | { 10 | /** 11 | * TransformerFactory priority. 12 | */ 13 | public function getPriority(): int; 14 | } 15 | -------------------------------------------------------------------------------- /Transformer/SourceEnumTransformer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class SourceEnumTransformer implements TransformerInterface 15 | { 16 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 17 | { 18 | return [new Expr\PropertyFetch($input, 'value'), []]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Transformer/StringToDateTimeTransformer.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class StringToDateTimeTransformer implements TransformerInterface 19 | { 20 | private $className; 21 | 22 | private $format; 23 | 24 | public function __construct(string $className, string $format = \DateTimeInterface::RFC3339) 25 | { 26 | $this->className = $className; 27 | $this->format = $format; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 34 | { 35 | $className = \DateTimeInterface::class === $this->className ? \DateTimeImmutable::class : $this->className; 36 | 37 | return [new Expr\StaticCall(new Name\FullyQualified($className), 'createFromFormat', [ 38 | new Arg( 39 | new Expr\BinaryOp\Coalesce( 40 | new Expr\ArrayDimFetch(new Expr\Variable('context'), new Scalar\String_(MapperContext::DATETIME_FORMAT)), 41 | new Scalar\String_($this->format), 42 | ) 43 | ), 44 | new Arg($input), 45 | ]), []]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Transformer/StringToSymfonyUidTransformer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class StringToSymfonyUidTransformer implements TransformerInterface 17 | { 18 | private $className; 19 | 20 | public function __construct(string $className) 21 | { 22 | $this->className = $className; 23 | } 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 29 | { 30 | return [ 31 | new Expr\New_(new Name($this->className), [new Arg($input)]), 32 | [], 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Transformer/SymfonyUidCopyTransformer.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | final class SymfonyUidCopyTransformer implements TransformerInterface 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 24 | { 25 | return [ 26 | new Expr\Ternary( 27 | new Expr\Instanceof_($input, new Name(Ulid::class)), 28 | new Expr\New_(new Name(Ulid::class), [new Arg(new Expr\MethodCall($input, 'toBase32'))]), 29 | new Expr\New_(new Name(Uuid::class), [new Arg(new Expr\MethodCall($input, 'toRfc4122'))]) 30 | ), 31 | [], 32 | ]; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Transformer/SymfonyUidToStringTransformer.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | final class SymfonyUidToStringTransformer implements TransformerInterface 15 | { 16 | private $isUlid; 17 | 18 | public function __construct(bool $isUlid) 19 | { 20 | $this->isUlid = $isUlid; 21 | } 22 | 23 | /** 24 | * {@inheritdoc} 25 | */ 26 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 27 | { 28 | if ($this->isUlid) { 29 | return [ 30 | // ulid 31 | new Expr\MethodCall($input, 'toBase32'), 32 | [], 33 | ]; 34 | } 35 | 36 | return [ 37 | // uuid 38 | new Expr\MethodCall($input, 'toRfc4122'), 39 | [], 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Transformer/SymfonyUidTransformerFactory.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | final class SymfonyUidTransformerFactory extends AbstractUniqueTypeTransformerFactory implements PrioritizedTransformerFactoryInterface 14 | { 15 | /** 16 | * @var array 17 | */ 18 | private $reflectionCache = []; 19 | 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function createTransformer(Type $sourceType, Type $targetType, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 24 | { 25 | $isSourceUid = $this->isUid($sourceType); 26 | $isTargetUid = $this->isUid($targetType); 27 | 28 | if ($isSourceUid && $isTargetUid) { 29 | return new SymfonyUidCopyTransformer(); 30 | } 31 | 32 | if ($isSourceUid) { 33 | return new SymfonyUidToStringTransformer($this->reflectionCache[$sourceType->getClassName()][1]); 34 | } 35 | 36 | if ($isTargetUid) { 37 | return new StringToSymfonyUidTransformer($targetType->getClassName()); 38 | } 39 | 40 | return null; 41 | } 42 | 43 | private function isUid(Type $type): bool 44 | { 45 | if (Type::BUILTIN_TYPE_OBJECT !== $type->getBuiltinType()) { 46 | return false; 47 | } 48 | 49 | if (null === $type->getClassName()) { 50 | return false; 51 | } 52 | 53 | if (!\array_key_exists($type->getClassName(), $this->reflectionCache)) { 54 | $reflClass = new \ReflectionClass($type->getClassName()); 55 | $this->reflectionCache[$type->getClassName()] = [$reflClass->isSubclassOf(AbstractUid::class), $type->getClassName() === Ulid::class]; 56 | } 57 | 58 | return $this->reflectionCache[$type->getClassName()][0]; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function getPriority(): int 65 | { 66 | return 24; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Transformer/TargetEnumTransformer.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | final class TargetEnumTransformer implements TransformerInterface 17 | { 18 | public $targetClassName; 19 | 20 | public function __construct(string $targetClassName) 21 | { 22 | $this->targetClassName = $targetClassName; 23 | } 24 | 25 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array 26 | { 27 | return [new Expr\StaticCall(new Name\FullyQualified($this->targetClassName), 'from', [ 28 | new Arg($input), 29 | ]), []]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Transformer/TransformerFactoryInterface.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | interface TransformerFactoryInterface 14 | { 15 | /** 16 | * Get transformer to use when mapping from an array of type to another array of type. 17 | * 18 | * @param Type[] $sourceTypes 19 | * @param Type[] $targetTypes 20 | */ 21 | public function getTransformer(?array $sourceTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface; 22 | } 23 | -------------------------------------------------------------------------------- /Transformer/TransformerInterface.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | interface TransformerInterface 16 | { 17 | /** 18 | * Get AST output and expressions for transforming a property mapping given an input. 19 | * 20 | * @return [Expr, Stmt[]] First value is the output expression, second value is an array of stmt needed to get the output 21 | */ 22 | public function transform(Expr $input, Expr $target, PropertyMapping $propertyMapping, UniqueVariableScope $uniqueVariableScope): array; 23 | } 24 | -------------------------------------------------------------------------------- /Transformer/UniqueTypeTransformerFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class UniqueTypeTransformerFactory implements TransformerFactoryInterface, PrioritizedTransformerFactoryInterface 13 | { 14 | private $chainTransformerFactory; 15 | 16 | public function __construct(ChainTransformerFactory $chainTransformerFactory) 17 | { 18 | $this->chainTransformerFactory = $chainTransformerFactory; 19 | } 20 | 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | public function getTransformer(?array $sourceTypes, ?array $targetTypes, MapperMetadataInterface $mapperMetadata): ?TransformerInterface 25 | { 26 | $nbSourceTypes = $sourceTypes ? \count($sourceTypes) : 0; 27 | $nbTargetTypes = $targetTypes ? \count($targetTypes) : 0; 28 | 29 | if (null === $sourceTypes || 0 === $nbSourceTypes || $nbSourceTypes > 1) { 30 | return null; 31 | } 32 | 33 | if (null === $targetTypes || $nbTargetTypes <= 1) { 34 | return null; 35 | } 36 | 37 | foreach ($targetTypes as $targetType) { 38 | if (null === $targetType) { 39 | continue; 40 | } 41 | 42 | $transformer = $this->chainTransformerFactory->getTransformer($sourceTypes, [$targetType], $mapperMetadata); 43 | 44 | if (null !== $transformer) { 45 | return $transformer; 46 | } 47 | } 48 | 49 | return null; 50 | } 51 | 52 | /** 53 | * {@inheritdoc} 54 | */ 55 | public function getPriority(): int 56 | { 57 | return 32; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jane-php/automapper", 3 | "type": "library", 4 | "description": "Jane AutoMapper Component", 5 | "keywords": [], 6 | "homepage": "https://github.com/janephp/janephp", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Joel Wurtz", 11 | "email": "jwurtz@jolicode.com" 12 | }, 13 | { 14 | "name": "Baptiste Leduc", 15 | "email": "baptiste.leduc@gmail.com" 16 | } 17 | ], 18 | "require": { 19 | "php": "^7.3 || ^8.0", 20 | "doctrine/inflector": "^1.4 || ^2.0", 21 | "nikic/php-parser": "^4.0", 22 | "symfony/property-info": "^5.3 || ^6.0", 23 | "symfony/serializer": "^5.1 || ^6.0" 24 | }, 25 | "require-dev": { 26 | "doctrine/annotations": "~1.0", 27 | "moneyphp/money": "^3.0", 28 | "phpdocumentor/reflection-docblock": "^3.0 || ^4.0 || ^5.0", 29 | "phpunit/phpunit": "^8.0", 30 | "symfony/uid": "^5.2 || ^6.0" 31 | }, 32 | "suggest": { 33 | "doctrine/annotations": "Docblock Annotations Parser", 34 | "phpdocumentor/reflection-docblock": "Allow to extract informations from PHP Doc blocks" 35 | }, 36 | "conflict": { 37 | "symfony/serializer": "<4.2", 38 | "symfony/property-info": "5.1.6 || 5.1.7", 39 | "nikic/php-parser": "<4.0.4" 40 | }, 41 | "extra": { 42 | "branch-alias": { 43 | "dev-next": "7-dev" 44 | } 45 | }, 46 | "autoload": { 47 | "psr-4": { 48 | "Jane\\Component\\AutoMapper\\": "" 49 | }, 50 | "exclude-from-classmap": [ 51 | "/Tests/" 52 | ] 53 | }, 54 | "config": { 55 | "process-timeout": 1800, 56 | "sort-packages": true 57 | }, 58 | "minimum-stability": "dev", 59 | "prefer-stable": true 60 | } 61 | --------------------------------------------------------------------------------