├── CHANGELOG.md ├── Exception ├── MappingException.php ├── ExceptionInterface.php ├── MappingTransformException.php ├── RuntimeException.php └── NoSuchPropertyException.php ├── Metadata ├── Mapping.php ├── ObjectMapperMetadataFactoryInterface.php └── ReflectionObjectMapperMetadataFactory.php ├── README.md ├── ConditionCallableInterface.php ├── TransformCallableInterface.php ├── Condition └── TargetClass.php ├── composer.json ├── LICENSE ├── ObjectMapperInterface.php ├── Attribute └── Map.php └── ObjectMapper.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | 7.3 5 | --- 6 | 7 | * Add the component as experimental 8 | -------------------------------------------------------------------------------- /Exception/MappingException.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 Symfony\Component\ObjectMapper\Exception; 13 | 14 | /** 15 | * @experimental 16 | * 17 | * @author Antoine Bluchet 18 | */ 19 | class MappingException extends RuntimeException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/ExceptionInterface.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 Symfony\Component\ObjectMapper\Exception; 13 | 14 | /** 15 | * @experimental 16 | * 17 | * @author Antoine Bluchet 18 | */ 19 | interface ExceptionInterface extends \Throwable 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/MappingTransformException.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 Symfony\Component\ObjectMapper\Exception; 13 | 14 | /** 15 | * @experimental 16 | * 17 | * @author Antoine Bluchet 18 | */ 19 | final class MappingTransformException extends RuntimeException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/RuntimeException.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 Symfony\Component\ObjectMapper\Exception; 13 | 14 | /** 15 | * @experimental 16 | * 17 | * @author Antoine Bluchet 18 | */ 19 | class RuntimeException extends \RuntimeException implements ExceptionInterface 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Exception/NoSuchPropertyException.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 Symfony\Component\ObjectMapper\Exception; 13 | 14 | /** 15 | * Thrown when a property cannot be found. 16 | * 17 | * @author Antoine Bluchet 18 | */ 19 | class NoSuchPropertyException extends MappingException 20 | { 21 | } 22 | -------------------------------------------------------------------------------- /Metadata/Mapping.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 Symfony\Component\ObjectMapper\Metadata; 13 | 14 | use Symfony\Component\ObjectMapper\Attribute\Map; 15 | 16 | /** 17 | * Configures a class or a property to map to. 18 | * 19 | * @internal 20 | * 21 | * @author Antoine Bluchet 22 | */ 23 | final class Mapping extends Map 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /Metadata/ObjectMapperMetadataFactoryInterface.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 Symfony\Component\ObjectMapper\Metadata; 13 | 14 | /** 15 | * Factory to create Mapper metadata. 16 | * 17 | * @experimental 18 | * 19 | * @author Antoine Bluchet 20 | */ 21 | interface ObjectMapperMetadataFactoryInterface 22 | { 23 | /** 24 | * @param array $context 25 | * 26 | * @return list 27 | */ 28 | public function create(object $object, ?string $property = null, array $context = []): array; 29 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Object Mapper Component 2 | ======================= 3 | 4 | The Object Mapper component allows you to map an object to another object, 5 | facilitating the mapping using attributes. 6 | 7 | **This Component is experimental**. 8 | [Experimental features](https://symfony.com/doc/current/contributing/code/experimental.html) 9 | are not covered by Symfony's 10 | [Backward Compatibility Promise](https://symfony.com/doc/current/contributing/code/bc.html). 11 | 12 | Resources 13 | --------- 14 | 15 | * [Documentation](https://symfony.com/doc/current/object_mapper.html) 16 | * [Contributing](https://symfony.com/doc/current/contributing/index.html) 17 | * [Report issues](https://github.com/symfony/symfony/issues) and 18 | [send Pull Requests](https://github.com/symfony/symfony/pulls) 19 | in the [main Symfony repository](https://github.com/symfony/symfony) 20 | -------------------------------------------------------------------------------- /ConditionCallableInterface.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 Symfony\Component\ObjectMapper; 13 | 14 | /** 15 | * Service used by "Map::if". 16 | * 17 | * @template T of object 18 | * @template T2 of object 19 | * 20 | * @experimental 21 | * 22 | * {@see Symfony\Component\ObjectMapper\Attribute\Map} 23 | */ 24 | interface ConditionCallableInterface 25 | { 26 | /** 27 | * @param mixed $value The value being mapped 28 | * @param T $source The object we're working on 29 | * @param T2|null $target The target we're mapping to 30 | */ 31 | public function __invoke(mixed $value, object $source, ?object $target): bool; 32 | } 33 | -------------------------------------------------------------------------------- /TransformCallableInterface.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 Symfony\Component\ObjectMapper; 13 | 14 | /** 15 | * Service used by "Map::transform". 16 | * 17 | * @template T of object 18 | * @template T2 of object 19 | * 20 | * @experimental 21 | * 22 | * {@see Symfony\Component\ObjectMapper\Attribute\Map} 23 | */ 24 | interface TransformCallableInterface 25 | { 26 | /** 27 | * @param mixed $value The value being mapped 28 | * @param T $source The object we're working on 29 | * @param T2|null $target The target we're mapping to 30 | */ 31 | public function __invoke(mixed $value, object $source, ?object $target): mixed; 32 | } 33 | -------------------------------------------------------------------------------- /Condition/TargetClass.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 Symfony\Component\ObjectMapper\Condition; 13 | 14 | use Symfony\Component\ObjectMapper\ConditionCallableInterface; 15 | 16 | /** 17 | * @template T of object 18 | * 19 | * @implements ConditionCallableInterface 20 | */ 21 | final class TargetClass implements ConditionCallableInterface 22 | { 23 | /** 24 | * @param class-string $className 25 | */ 26 | public function __construct(private readonly string $className) 27 | { 28 | } 29 | 30 | public function __invoke(mixed $value, object $source, ?object $target): bool 31 | { 32 | return $target instanceof $this->className; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "symfony/object-mapper", 3 | "type": "library", 4 | "description": "Provides a way to map an object to another object", 5 | "keywords": [], 6 | "homepage": "https://symfony.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Fabien Potencier", 11 | "email": "fabien@symfony.com" 12 | }, 13 | { 14 | "name": "Symfony Community", 15 | "homepage": "https://symfony.com/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=8.2", 20 | "psr/container": "^2.0" 21 | }, 22 | "require-dev": { 23 | "symfony/property-access": "^7.2", 24 | "symfony/var-exporter": "^7.2" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Symfony\\Component\\ObjectMapper\\": "" 29 | }, 30 | "exclude-from-classmap": [ 31 | "/Tests/" 32 | ] 33 | }, 34 | "conflict": { 35 | "symfony/property-access": "<7.2" 36 | }, 37 | "minimum-stability": "dev" 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025-present Fabien Potencier 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 | -------------------------------------------------------------------------------- /ObjectMapperInterface.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 Symfony\Component\ObjectMapper; 13 | 14 | use Symfony\Component\ObjectMapper\Exception\MappingException; 15 | use Symfony\Component\ObjectMapper\Exception\MappingTransformException; 16 | use Symfony\Component\ObjectMapper\Exception\NoSuchPropertyException; 17 | 18 | /** 19 | * Object to object mapper. 20 | * 21 | * @experimental 22 | * 23 | * @author Antoine Bluchet 24 | */ 25 | interface ObjectMapperInterface 26 | { 27 | /** 28 | * @template T of object 29 | * 30 | * @param object $source The object to map from 31 | * @param T|class-string|null $target The object or class to map to 32 | * 33 | * @return T 34 | * 35 | * @throws MappingException When the mapping configuration is wrong 36 | * @throws MappingTransformException When a transformation on an object does not return an object 37 | * @throws NoSuchPropertyException When a property does not exist 38 | */ 39 | public function map(object $source, object|string|null $target = null): object; 40 | } 41 | -------------------------------------------------------------------------------- /Metadata/ReflectionObjectMapperMetadataFactory.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 Symfony\Component\ObjectMapper\Metadata; 13 | 14 | use Symfony\Component\ObjectMapper\Attribute\Map; 15 | use Symfony\Component\ObjectMapper\Exception\MappingException; 16 | 17 | /** 18 | * @internal 19 | * 20 | * @author Antoine Bluchet 21 | */ 22 | final class ReflectionObjectMapperMetadataFactory implements ObjectMapperMetadataFactoryInterface 23 | { 24 | public function create(object $object, ?string $property = null, array $context = []): array 25 | { 26 | try { 27 | $refl = new \ReflectionClass($object); 28 | $mapTo = []; 29 | foreach (($property ? $refl->getProperty($property) : $refl)->getAttributes(Map::class, \ReflectionAttribute::IS_INSTANCEOF) as $mapAttribute) { 30 | $map = $mapAttribute->newInstance(); 31 | $mapTo[] = new Mapping($map->target, $map->source, $map->if, $map->transform); 32 | } 33 | 34 | return $mapTo; 35 | } catch (\ReflectionException $e) { 36 | throw new MappingException($e->getMessage(), $e->getCode(), $e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Attribute/Map.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 Symfony\Component\ObjectMapper\Attribute; 13 | 14 | /** 15 | * Configures a class or a property to map to. 16 | * 17 | * @experimental 18 | * 19 | * @author Antoine Bluchet 20 | */ 21 | #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::TARGET_PROPERTY | \Attribute::IS_REPEATABLE)] 22 | class Map 23 | { 24 | /** 25 | * @param string|class-string|null $source The property or the class to map from 26 | * @param string|class-string|null $target The property or the class to map to 27 | * @param string|bool|callable(mixed, object): bool|null $if A boolean, a service id or a callable that instructs whether to map 28 | * @param (string|callable(mixed, object, ?object): mixed)|(string|callable(mixed, object, ?object): mixed)[]|null $transform A service id or a callable that transforms the value during mapping 29 | */ 30 | public function __construct( 31 | public readonly ?string $target = null, 32 | public readonly ?string $source = null, 33 | public readonly mixed $if = null, 34 | public readonly mixed $transform = null, 35 | ) { 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /ObjectMapper.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 Symfony\Component\ObjectMapper; 13 | 14 | use Psr\Container\ContainerInterface; 15 | use Symfony\Component\ObjectMapper\Exception\MappingException; 16 | use Symfony\Component\ObjectMapper\Exception\MappingTransformException; 17 | use Symfony\Component\ObjectMapper\Exception\NoSuchPropertyException; 18 | use Symfony\Component\ObjectMapper\Metadata\Mapping; 19 | use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface; 20 | use Symfony\Component\ObjectMapper\Metadata\ReflectionObjectMapperMetadataFactory; 21 | use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException as PropertyAccessorNoSuchPropertyException; 22 | use Symfony\Component\PropertyAccess\PropertyAccessorInterface; 23 | use Symfony\Component\VarExporter\LazyObjectInterface; 24 | 25 | /** 26 | * Object to object mapper. 27 | * 28 | * @experimental 29 | * 30 | * @author Antoine Bluchet 31 | */ 32 | final class ObjectMapper implements ObjectMapperInterface 33 | { 34 | /** 35 | * Tracks recursive references. 36 | */ 37 | private ?\SplObjectStorage $objectMap = null; 38 | 39 | public function __construct( 40 | private readonly ObjectMapperMetadataFactoryInterface $metadataFactory = new ReflectionObjectMapperMetadataFactory(), 41 | private readonly ?PropertyAccessorInterface $propertyAccessor = null, 42 | private readonly ?ContainerInterface $transformCallableLocator = null, 43 | private readonly ?ContainerInterface $conditionCallableLocator = null, 44 | ) { 45 | } 46 | 47 | public function map(object $source, object|string|null $target = null): object 48 | { 49 | $objectMapInitialized = false; 50 | if (null === $this->objectMap) { 51 | $this->objectMap = new \SplObjectStorage(); 52 | $objectMapInitialized = true; 53 | } 54 | 55 | $metadata = $this->metadataFactory->create($source); 56 | $map = $this->getMapTarget($metadata, null, $source, null); 57 | $target ??= $map?->target; 58 | $mappingToObject = \is_object($target); 59 | 60 | if (!$target) { 61 | throw new MappingException(\sprintf('Mapping target not found for source "%s".', get_debug_type($source))); 62 | } 63 | 64 | if (\is_string($target) && !class_exists($target)) { 65 | throw new MappingException(\sprintf('Mapping target class "%s" does not exist for source "%s".', $target, get_debug_type($source))); 66 | } 67 | 68 | try { 69 | $targetRefl = new \ReflectionClass($target); 70 | } catch (\ReflectionException $e) { 71 | throw new MappingException($e->getMessage(), $e->getCode(), $e); 72 | } 73 | 74 | $mappedTarget = $mappingToObject ? $target : $targetRefl->newInstanceWithoutConstructor(); 75 | 76 | if (!$metadata && $targetMetadata = $this->metadataFactory->create($mappedTarget)) { 77 | $metadata = $targetMetadata; 78 | $map = $this->getMapTarget($metadata, null, $source, null); 79 | } 80 | 81 | if ($map && $map->transform) { 82 | $mappedTarget = $this->applyTransforms($map, $mappedTarget, $source, null); 83 | 84 | if (!\is_object($mappedTarget)) { 85 | throw new MappingTransformException(\sprintf('Cannot map "%s" to a non-object target of type "%s".', get_debug_type($source), get_debug_type($mappedTarget))); 86 | } 87 | } 88 | 89 | if (!is_a($mappedTarget, $targetRefl->getName(), false)) { 90 | throw new MappingException(\sprintf('Expected the mapped object to be an instance of "%s" but got "%s".', $targetRefl->getName(), get_debug_type($mappedTarget))); 91 | } 92 | 93 | $this->objectMap[$source] = $mappedTarget; 94 | $ctorArguments = []; 95 | $targetConstructor = $targetRefl->getConstructor(); 96 | foreach ($targetConstructor?->getParameters() ?? [] as $parameter) { 97 | $parameterName = $parameter->getName(); 98 | 99 | if ($targetRefl->hasProperty($parameterName)) { 100 | $property = $targetRefl->getProperty($parameterName); 101 | 102 | if ($property->isReadOnly() && $property->isInitialized($mappedTarget)) { 103 | continue; 104 | } 105 | } 106 | 107 | if ($this->isReadable($source, $parameterName)) { 108 | $ctorArguments[$parameterName] = $this->getRawValue($source, $parameterName); 109 | } else { 110 | $ctorArguments[$parameterName] = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; 111 | } 112 | } 113 | 114 | $readMetadataFrom = $source; 115 | $refl = $this->getSourceReflectionClass($source) ?? $targetRefl; 116 | 117 | // When source contains no metadata, we read metadata on the target instead 118 | if ($refl === $targetRefl) { 119 | $readMetadataFrom = $mappedTarget; 120 | } 121 | 122 | $mapToProperties = []; 123 | foreach ($refl->getProperties() as $property) { 124 | if ($property->isStatic()) { 125 | continue; 126 | } 127 | 128 | $propertyName = $property->getName(); 129 | $mappings = $this->metadataFactory->create($readMetadataFrom, $propertyName); 130 | foreach ($mappings as $mapping) { 131 | $sourcePropertyName = $propertyName; 132 | if ($mapping->source && (!$refl->hasProperty($propertyName) || !isset($source->$propertyName))) { 133 | $sourcePropertyName = $mapping->source; 134 | } 135 | 136 | if (false === $if = $mapping->if) { 137 | continue; 138 | } 139 | 140 | $value = $this->getRawValue($source, $sourcePropertyName); 141 | if ($if && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source, $mappedTarget)) { 142 | continue; 143 | } 144 | 145 | $targetPropertyName = $mapping->target ?? $propertyName; 146 | if (!$targetRefl->hasProperty($targetPropertyName)) { 147 | continue; 148 | } 149 | 150 | $value = $this->getSourceValue($source, $mappedTarget, $value, $this->objectMap, $mapping); 151 | $this->storeValue($targetPropertyName, $mapToProperties, $ctorArguments, $value); 152 | } 153 | 154 | if (!$mappings && $targetRefl->hasProperty($propertyName)) { 155 | $sourceProperty = $refl->getProperty($propertyName); 156 | if ($refl->isInstance($source) && !$sourceProperty->isInitialized($source)) { 157 | continue; 158 | } 159 | 160 | $value = $this->getSourceValue($source, $mappedTarget, $this->getRawValue($source, $propertyName), $this->objectMap); 161 | $this->storeValue($propertyName, $mapToProperties, $ctorArguments, $value); 162 | } 163 | } 164 | 165 | if (!$mappingToObject && !$map?->transform && $targetConstructor) { 166 | try { 167 | $mappedTarget->__construct(...$ctorArguments); 168 | } catch (\ReflectionException $e) { 169 | throw new MappingException($e->getMessage(), $e->getCode(), $e); 170 | } 171 | } 172 | 173 | if ($mappingToObject && $ctorArguments) { 174 | foreach ($ctorArguments as $property => $value) { 175 | if ($this->propertyIsMappable($refl, $property) && $this->propertyIsMappable($targetRefl, $property)) { 176 | $mapToProperties[$property] = $value; 177 | } 178 | } 179 | } 180 | 181 | foreach ($mapToProperties as $property => $value) { 182 | $this->propertyAccessor ? $this->propertyAccessor->setValue($mappedTarget, $property, $value) : ($mappedTarget->{$property} = $value); 183 | } 184 | 185 | if ($objectMapInitialized) { 186 | $this->objectMap = null; 187 | } 188 | 189 | return $mappedTarget; 190 | } 191 | 192 | private function isReadable(object $source, string $propertyName): bool 193 | { 194 | if ($this->propertyAccessor) { 195 | return $this->propertyAccessor->isReadable($source, $propertyName); 196 | } 197 | 198 | if (!property_exists($source, $propertyName) && !isset($source->{$propertyName})) { 199 | return false; 200 | } 201 | 202 | return true; 203 | } 204 | 205 | private function getRawValue(object $source, string $propertyName): mixed 206 | { 207 | if ($this->propertyAccessor) { 208 | try { 209 | return $this->propertyAccessor->getValue($source, $propertyName); 210 | } catch (PropertyAccessorNoSuchPropertyException $e) { 211 | throw new NoSuchPropertyException($e->getMessage(), $e->getCode(), $e); 212 | } 213 | } 214 | 215 | if (!property_exists($source, $propertyName) && !isset($source->{$propertyName})) { 216 | throw new NoSuchPropertyException(\sprintf('The property "%s" does not exist on "%s".', $propertyName, get_debug_type($source))); 217 | } 218 | 219 | return $source->{$propertyName}; 220 | } 221 | 222 | private function getSourceValue(object $source, object $target, mixed $value, \SplObjectStorage $objectMap, ?Mapping $mapping = null): mixed 223 | { 224 | if ($mapping?->transform) { 225 | $value = $this->applyTransforms($mapping, $value, $source, $target); 226 | } 227 | 228 | if ( 229 | \is_object($value) 230 | && ($innerMetadata = $this->metadataFactory->create($value)) 231 | && ($mapTo = $this->getMapTarget($innerMetadata, $value, $source, $target)) 232 | && (\is_string($mapTo->target) && class_exists($mapTo->target)) 233 | ) { 234 | $value = $this->applyTransforms($mapTo, $value, $source, $target); 235 | 236 | if ($value === $source) { 237 | $value = $target; 238 | } elseif ($objectMap->offsetExists($value)) { 239 | $value = $objectMap[$value]; 240 | } else { 241 | $value = $this->map($value, $mapTo->target); 242 | } 243 | } 244 | 245 | return $value; 246 | } 247 | 248 | /** 249 | * Store the value either the constructor arguments or as a property to be mapped. 250 | * 251 | * @param array $mapToProperties 252 | * @param array $ctorArguments 253 | */ 254 | private function storeValue(string $propertyName, array &$mapToProperties, array &$ctorArguments, mixed $value): void 255 | { 256 | if (\array_key_exists($propertyName, $ctorArguments)) { 257 | $ctorArguments[$propertyName] = $value; 258 | 259 | return; 260 | } 261 | 262 | $mapToProperties[$propertyName] = $value; 263 | } 264 | 265 | /** 266 | * @param callable(): mixed $fn 267 | */ 268 | private function call(callable $fn, mixed $value, object $source, ?object $target = null): mixed 269 | { 270 | if (\is_string($fn)) { 271 | return \call_user_func($fn, $value); 272 | } 273 | 274 | return $fn($value, $source, $target); 275 | } 276 | 277 | /** 278 | * @param Mapping[] $metadata 279 | */ 280 | private function getMapTarget(array $metadata, mixed $value, object $source, ?object $target): ?Mapping 281 | { 282 | $mapTo = null; 283 | foreach ($metadata as $mapAttribute) { 284 | if (($if = $mapAttribute->if) && ($fn = $this->getCallable($if, $this->conditionCallableLocator)) && !$this->call($fn, $value, $source, $target)) { 285 | continue; 286 | } 287 | 288 | $mapTo = $mapAttribute; 289 | } 290 | 291 | return $mapTo; 292 | } 293 | 294 | private function applyTransforms(Mapping $map, mixed $value, object $source, ?object $target): mixed 295 | { 296 | if (!$transforms = $map->transform) { 297 | return $value; 298 | } 299 | 300 | if (\is_callable($transforms)) { 301 | $transforms = [$transforms]; 302 | } elseif (!\is_array($transforms)) { 303 | $transforms = [$transforms]; 304 | } 305 | 306 | foreach ($transforms as $transform) { 307 | if ($fn = $this->getCallable($transform, $this->transformCallableLocator)) { 308 | $value = $this->call($fn, $value, $source, $target); 309 | } 310 | } 311 | 312 | return $value; 313 | } 314 | 315 | /** 316 | * @param (string|callable(mixed $value, object $object): mixed) $fn 317 | */ 318 | private function getCallable(string|callable $fn, ?ContainerInterface $locator = null): ?callable 319 | { 320 | if (\is_callable($fn)) { 321 | return $fn; 322 | } 323 | 324 | if ($locator?->has($fn)) { 325 | return $locator->get($fn); 326 | } 327 | 328 | return null; 329 | } 330 | 331 | /** 332 | * @return ?\ReflectionClass 333 | */ 334 | private function getSourceReflectionClass(object $source): ?\ReflectionClass 335 | { 336 | $metadata = $this->metadataFactory->create($source); 337 | try { 338 | $refl = new \ReflectionClass($source); 339 | } catch (\ReflectionException $e) { 340 | throw new MappingException($e->getMessage(), $e->getCode(), $e); 341 | } 342 | 343 | if ($source instanceof LazyObjectInterface) { 344 | $source->initializeLazyObject(); 345 | } elseif (\PHP_VERSION_ID >= 80400 && $refl->isUninitializedLazyObject($source)) { 346 | $refl->initializeLazyObject($source); 347 | } 348 | 349 | if ($metadata) { 350 | return $refl; 351 | } 352 | 353 | foreach ($refl->getProperties() as $property) { 354 | if ($this->metadataFactory->create($source, $property->getName())) { 355 | return $refl; 356 | } 357 | } 358 | 359 | return null; 360 | } 361 | 362 | private function propertyIsMappable(\ReflectionClass $targetRefl, int|string $property): bool 363 | { 364 | return $targetRefl->hasProperty($property) && $targetRefl->getProperty($property)->isPublic(); 365 | } 366 | } 367 | --------------------------------------------------------------------------------