diff --git a/CHANGELOG.md b/CHANGELOG.md index a20f0ed4..27c4068d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 1.11.1 + +* refactor: refactor `ObjectToObjectTransformer` for future optimization + ## 1.11.0 * refactor: spin off `resolveTargetClass()` to separate class diff --git a/config/services.php b/config/services.php index 7f8fa97b..17699984 100644 --- a/config/services.php +++ b/config/services.php @@ -65,6 +65,9 @@ use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\CachingObjectToObjectMetadataFactory; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\ObjectToObjectMetadataFactory; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\ProxyResolvingObjectToObjectMetadataFactory; +use Rekalogika\Mapper\TransformerProcessor\ObjectProcessor\DefaultObjectProcessorFactory; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessor\CachingPropertyProcessorFactory; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessor\DefaultPropertyProcessorFactory; use Rekalogika\Mapper\TransformerRegistry\Implementation\CachingTransformerRegistry; use Rekalogika\Mapper\TransformerRegistry\Implementation\TransformerRegistry; use Rekalogika\Mapper\TypeResolver\Implementation\CachingTypeResolver; @@ -227,10 +230,7 @@ ->set(ObjectToObjectTransformer::class) ->args([ '$objectToObjectMetadataFactory' => service('rekalogika.mapper.object_to_object_metadata_factory'), - '$propertyMapperLocator' => tagged_locator('rekalogika.mapper.property_mapper'), - '$subMapperFactory' => service('rekalogika.mapper.sub_mapper.factory'), - '$proxyFactory' => service('rekalogika.mapper.proxy.factory'), - '$propertyAccessor' => service(PropertyAccessorInterface::class), + '$objectProcessorFactory' => service('rekalogika.mapper.transformer_processor.object_processor_factory'), ]) ->tag('rekalogika.mapper.transformer', ['priority' => -900]); @@ -273,10 +273,7 @@ ) ->args([ '$objectToObjectMetadataFactory' => service('rekalogika.mapper.object_to_object_metadata_factory'), - '$propertyMapperLocator' => tagged_locator('rekalogika.mapper.property_mapper'), - '$subMapperFactory' => service('rekalogika.mapper.sub_mapper.factory'), - '$proxyFactory' => service('rekalogika.mapper.proxy.factory'), - '$propertyAccessor' => service(PropertyAccessorInterface::class), + '$objectProcessorFactory' => service('rekalogika.mapper.transformer_processor.object_processor_factory'), ]); # mapping cache warmer @@ -382,6 +379,44 @@ service($createCache($services, 'transformer_registry')), ]); + # transformer processor, object processor factory + + $services + ->set( + 'rekalogika.mapper.transformer_processor.object_processor_factory', + DefaultObjectProcessorFactory::class, + ) + ->args([ + '$propertyMapperLocator' => tagged_locator('rekalogika.mapper.property_mapper'), + '$subMapperFactory' => service('rekalogika.mapper.sub_mapper.factory'), + '$proxyFactory' => service('rekalogika.mapper.proxy.factory'), + '$propertyAccessor' => service(PropertyAccessorInterface::class), + '$propertyProcessorFactory' => service('rekalogika.mapper.transformer_processor.property_processor_factory'), + ]); + + # transformer processor, property processor factory + + $services + ->set( + 'rekalogika.mapper.transformer_processor.property_processor_factory', + DefaultPropertyProcessorFactory::class, + ) + ->args([ + '$propertyAccessor' => service(PropertyAccessorInterface::class), + '$subMapperFactory' => service('rekalogika.mapper.sub_mapper.factory'), + '$propertyMapperLocator' => tagged_locator('rekalogika.mapper.property_mapper'), + ]); + + $services + ->set( + 'rekalogika.mapper.transformer_processor.property_processor_factory.caching', + CachingPropertyProcessorFactory::class, + ) + ->decorate('rekalogika.mapper.transformer_processor.property_processor_factory') + ->args([ + service('.inner'), + ]); + # sub mapper $services diff --git a/src/MapperFactory.php b/src/MapperFactory.php index b9493329..02cf4251 100644 --- a/src/MapperFactory.php +++ b/src/MapperFactory.php @@ -67,6 +67,11 @@ use Rekalogika\Mapper\Transformer\MetadataUtil\MetadataUtilLocator; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadataFactoryInterface; use Rekalogika\Mapper\Transformer\TransformerInterface; +use Rekalogika\Mapper\TransformerProcessor\ObjectProcessor\DefaultObjectProcessorFactory; +use Rekalogika\Mapper\TransformerProcessor\ObjectProcessorFactoryInterface; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessor\CachingPropertyProcessorFactory; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessor\DefaultPropertyProcessorFactory; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorFactoryInterface; use Rekalogika\Mapper\TransformerRegistry\Implementation\TransformerRegistry; use Rekalogika\Mapper\TransformerRegistry\TransformerRegistryInterface; use Rekalogika\Mapper\TypeResolver\Implementation\CachingTypeResolver; @@ -197,6 +202,10 @@ class MapperFactory private ?ProxyFactoryInterface $proxyFactory = null; + private ?ObjectProcessorFactoryInterface $objectProcessorFactory = null; + + private ?PropertyProcessorFactoryInterface $propertyProcessorFactory = null; + private ?MappingCommand $mappingCommand = null; private ?TryCommand $tryCommand = null; @@ -403,17 +412,10 @@ protected function getNullTransformer(): TransformerInterface protected function getObjectToObjectTransformer(): TransformerInterface { - if (null === $this->objectToObjectTransformer) { - $this->objectToObjectTransformer = new ObjectToObjectTransformer( - objectToObjectMetadataFactory: $this->getObjectToObjectMetadataFactory(), - propertyMapperLocator: $this->getPropertyMapperLocator(), - subMapperFactory: $this->getSubMapperFactory(), - proxyFactory: $this->getProxyFactory(), - propertyAccessor: $this->getPropertyAccessor(), - ); - } - - return $this->objectToObjectTransformer; + return $this->objectToObjectTransformer ??= new ObjectToObjectTransformer( + $this->getObjectToObjectMetadataFactory(), + $this->getObjectProcessorFactory(), + ); } protected function getObjectToStringTransformer(): TransformerInterface @@ -899,6 +901,32 @@ protected function getProxyFactory(): ProxyFactoryInterface return $this->proxyFactory; } + // + // transformer processor + // + + protected function getObjectProcessorFactory(): ObjectProcessorFactoryInterface + { + return $this->objectProcessorFactory ??= new DefaultObjectProcessorFactory( + propertyMapperLocator: $this->getPropertyMapperLocator(), + subMapperFactory: $this->getSubMapperFactory(), + proxyFactory: $this->getProxyFactory(), + propertyAccessor: $this->getPropertyAccessor(), + propertyProcessorFactory: $this->getPropertyProcessorFactory(), + ); + } + + protected function getPropertyProcessorFactory(): PropertyProcessorFactoryInterface + { + return $this->propertyProcessorFactory ??= new CachingPropertyProcessorFactory( + new DefaultPropertyProcessorFactory( + propertyAccessor: $this->getPropertyAccessor(), + subMapperFactory: $this->getSubMapperFactory(), + propertyMapperLocator: $this->getPropertyMapperLocator(), + ), + ); + } + // // command // diff --git a/src/Transformer/Implementation/ObjectToObjectTransformer.php b/src/Transformer/Implementation/ObjectToObjectTransformer.php index 2dc8917f..04ce5bfe 100644 --- a/src/Transformer/Implementation/ObjectToObjectTransformer.php +++ b/src/Transformer/Implementation/ObjectToObjectTransformer.php @@ -13,40 +13,19 @@ namespace Rekalogika\Mapper\Transformer\Implementation; -use Psr\Container\ContainerInterface; use Rekalogika\Mapper\CacheWarmer\WarmableMainTransformerInterface; use Rekalogika\Mapper\CacheWarmer\WarmableObjectToObjectMetadataFactoryInterface; use Rekalogika\Mapper\CacheWarmer\WarmableTransformerInterface; use Rekalogika\Mapper\Context\Context; -use Rekalogika\Mapper\Context\ExtraTargetValues; -use Rekalogika\Mapper\Context\MapperOptions; use Rekalogika\Mapper\Exception\InvalidArgumentException; -use Rekalogika\Mapper\ObjectCache\ObjectCache; -use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; -use Rekalogika\Mapper\ServiceMethod\ServiceMethodRunner; -use Rekalogika\Mapper\ServiceMethod\ServiceMethodSpecification; -use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface; -use Rekalogika\Mapper\Transformer\Exception\ClassNotInstantiableException; -use Rekalogika\Mapper\Transformer\Exception\ExtraTargetPropertyNotFoundException; -use Rekalogika\Mapper\Transformer\Exception\InstantiationFailureException; use Rekalogika\Mapper\Transformer\Exception\NotAClassException; -use Rekalogika\Mapper\Transformer\Exception\UninitializedSourcePropertyException; -use Rekalogika\Mapper\Transformer\Exception\UnsupportedPropertyMappingException; use Rekalogika\Mapper\Transformer\MainTransformerAwareInterface; use Rekalogika\Mapper\Transformer\MainTransformerAwareTrait; -use Rekalogika\Mapper\Transformer\Model\AdderRemoverProxy; -use Rekalogika\Mapper\Transformer\Model\ConstructorArguments; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadataFactoryInterface; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMapping; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ReadMode; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\WriteMode; use Rekalogika\Mapper\Transformer\TransformerInterface; use Rekalogika\Mapper\Transformer\TypeMapping; -use Rekalogika\Mapper\Transformer\Util\ReaderWriter; +use Rekalogika\Mapper\TransformerProcessor\ObjectProcessorFactoryInterface; use Rekalogika\Mapper\Util\TypeFactory; -use Rekalogika\Mapper\Util\TypeGuesser; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\Type; final class ObjectToObjectTransformer implements @@ -56,16 +35,18 @@ final class ObjectToObjectTransformer implements { use MainTransformerAwareTrait; - private ReaderWriter $readerWriter; + private ?ObjectProcessorFactoryInterface $objectProcessorFactoryWithMainTransformer = null; public function __construct( - private ObjectToObjectMetadataFactoryInterface $objectToObjectMetadataFactory, - private ContainerInterface $propertyMapperLocator, - private SubMapperFactoryInterface $subMapperFactory, - private ProxyFactoryInterface $proxyFactory, - PropertyAccessorInterface $propertyAccessor, - ) { - $this->readerWriter = new ReaderWriter($propertyAccessor); + private readonly ObjectToObjectMetadataFactoryInterface $objectToObjectMetadataFactory, + private readonly ObjectProcessorFactoryInterface $objectProcessorFactory, + ) {} + + private function getObjectProcessorFactory(): ObjectProcessorFactoryInterface + { + return $this->objectProcessorFactoryWithMainTransformer + ??= $this->objectProcessorFactory + ->withMainTransformer($this->getMainTransformer()); } #[\Override] @@ -115,690 +96,22 @@ public function transform( $objectToObjectMetadata = $this->objectToObjectMetadataFactory ->createObjectToObjectMetadata($sourceClass, $targetClass); - // disregard target if target is read only or target value reading is - // disabled - - if ( - $objectToObjectMetadata->isTargetUnalterable() - || $context(MapperOptions::class)?->readTargetValue !== true - ) { - $target = null; - } - - // get extra target values - - $extraTargetValues = $this->getExtraTargetValues( - objectToObjectMetadata: $objectToObjectMetadata, - context: $context, - ); - - // initialize target if target is null - - if (null === $target) { - $canUseTargetProxy = $objectToObjectMetadata->canUseTargetProxy() - && $context(MapperOptions::class)?->lazyLoading; - - if ($canUseTargetProxy) { - $target = $this->instantiateTargetProxy( - source: $source, - objectToObjectMetadata: $objectToObjectMetadata, - extraTargetValues: $extraTargetValues, - context: $context, - ); - } else { - $target = $this->instantiateRealTarget( - source: $source, - objectToObjectMetadata: $objectToObjectMetadata, - extraTargetValues: $extraTargetValues, - context: $context, - ); - } - } else { - $canUseTargetProxy = false; - - if (!\is_object($target)) { - throw new InvalidArgumentException(\sprintf('The target must be an object, "%s" given.', get_debug_type($target)), context: $context); - } - } - - // save object to cache - - $context(ObjectCache::class)?->saveTarget( - source: $source, - targetType: $targetType, - target: $target, - ); - - // map properties if it is not a proxy - - if (!$canUseTargetProxy) { - // map dynamic properties if both are stdClass or allow dynamic - // properties - - if ( - $objectToObjectMetadata->sourceAllowsDynamicProperties() - && $objectToObjectMetadata->targetAllowsDynamicProperties() - ) { - $this->mapDynamicProperties( - source: $source, - target: $target, - objectToObjectMetadata: $objectToObjectMetadata, - context: $context, - ); - } - - $target = $this->readSourceAndWriteTarget( - source: $source, - target: $target, - propertyMappings: $objectToObjectMetadata->getPropertyMappings(), - extraTargetValues: $extraTargetValues, - context: $context, - ); - } - - return $target; - } - - /** - * @return array - */ - private function getExtraTargetValues( - ObjectToObjectMetadata $objectToObjectMetadata, - Context $context, - ): array { - $extraTargetValues = $context(ExtraTargetValues::class) - ?->getArgumentsForClass($objectToObjectMetadata->getAllTargetClasses()) - ?? []; - - $allPropertyMappings = $objectToObjectMetadata->getPropertyMappings(); - - foreach (array_keys($extraTargetValues) as $property) { - if (!isset($allPropertyMappings[$property])) { - throw new ExtraTargetPropertyNotFoundException( - class: $objectToObjectMetadata->getTargetClass(), - property: $property, - context: $context, - ); - } - } - - return $extraTargetValues; - } - - /** - * @param array $extraTargetValues - */ - private function instantiateRealTarget( - object $source, - ObjectToObjectMetadata $objectToObjectMetadata, - array $extraTargetValues, - Context $context, - ): object { - $targetClass = $objectToObjectMetadata->getTargetClass(); - - // check if class is valid & instantiable - - if (!$objectToObjectMetadata->isInstantiable()) { - throw new ClassNotInstantiableException($targetClass, context: $context); - } - - $constructorArguments = $this->generateConstructorArguments( - source: $source, - objectToObjectMetadata: $objectToObjectMetadata, - extraTargetValues: $extraTargetValues, - context: $context, - ); - - try { - $reflectionClass = new \ReflectionClass($targetClass); - - return $reflectionClass - ->newInstanceArgs($constructorArguments->getArguments()); - } catch (\TypeError | \ReflectionException $e) { - throw new InstantiationFailureException( - source: $source, - targetClass: $targetClass, - constructorArguments: $constructorArguments->getArguments(), - unsetSourceProperties: $constructorArguments->getUnsetSourceProperties(), - previous: $e, - context: $context, - ); - } - } - - /** - * @param array $extraTargetValues - */ - private function instantiateTargetProxy( - object $source, - ObjectToObjectMetadata $objectToObjectMetadata, - array $extraTargetValues, - Context $context, - ): object { - $targetClass = $objectToObjectMetadata->getTargetClass(); - - // check if class is valid & instantiable - - if (!$objectToObjectMetadata->isInstantiable()) { - throw new ClassNotInstantiableException($targetClass, context: $context); - } - - // create proxy initializer. this initializer will be executed when the - // proxy is first accessed - - $initializer = function (object $target) use ( - $source, - $objectToObjectMetadata, - $context, - $extraTargetValues, - ): void { - // if the constructor is lazy, run it here - - if (!$objectToObjectMetadata->constructorIsEager()) { - $target = $this->runConstructorManually( - source: $source, - target: $target, - objectToObjectMetadata: $objectToObjectMetadata, - extraTargetValues: $extraTargetValues, - context: $context, - ); - } - - // map lazy properties - - $this->readSourceAndWriteTarget( - source: $source, - target: $target, - propertyMappings: $objectToObjectMetadata->getLazyPropertyMappings(), - extraTargetValues: $extraTargetValues, - context: $context, - ); - }; - - // instantiate the proxy - - $target = $this->proxyFactory->createProxy( - $targetClass, - $initializer, - $objectToObjectMetadata->getTargetProxySkippedProperties(), - ); - - // if the constructor is eager, run it here - - if ($objectToObjectMetadata->constructorIsEager()) { - $target = $this->runConstructorManually( - source: $source, - target: $target, - objectToObjectMetadata: $objectToObjectMetadata, - extraTargetValues: $extraTargetValues, - context: $context, - ); - } - - // map eager properties - - $target = $this->readSourceAndWriteTarget( - source: $source, - target: $target, - propertyMappings: $objectToObjectMetadata->getEagerPropertyMappings(), - extraTargetValues: $extraTargetValues, - context: $context, - ); - - return $target; - } + // type checking - /** - * @param array $extraTargetValues - */ - private function runConstructorManually( - object $source, - object $target, - ObjectToObjectMetadata $objectToObjectMetadata, - array $extraTargetValues, - Context $context, - ): object { - if (!method_exists($target, '__construct')) { - return $target; + if ($target !== null && !\is_object($target)) { + throw new InvalidArgumentException(\sprintf('The target must be an object, "%s" given.', get_debug_type($target)), context: $context); } - $constructorArguments = $this->generateConstructorArguments( - source: $source, - objectToObjectMetadata: $objectToObjectMetadata, - extraTargetValues: $extraTargetValues, - context: $context, - ); - - $arguments = $constructorArguments->getArguments(); - - try { - /** - * @psalm-suppress DirectConstructorCall - * @psalm-suppress MixedMethodCall - */ - $target->__construct(...$arguments); - } catch (\TypeError | \ReflectionException $e) { - throw new InstantiationFailureException( - source: $source, - targetClass: $target::class, - constructorArguments: $constructorArguments->getArguments(), - unsetSourceProperties: $constructorArguments->getUnsetSourceProperties(), - previous: $e, - context: $context, - ); - } - - return $target; - } - - /** - * @param array $extraTargetValues - */ - private function generateConstructorArguments( - object $source, - ObjectToObjectMetadata $objectToObjectMetadata, - array $extraTargetValues, - Context $context, - ): ConstructorArguments { - $constructorPropertyMappings = $objectToObjectMetadata->getConstructorPropertyMappings(); - - $constructorArguments = new ConstructorArguments(); - - // add arguments from property mappings - - foreach ($constructorPropertyMappings as $propertyMapping) { - try { - /** @var mixed $targetPropertyValue */ - [$targetPropertyValue,] = $this->transformValue( - propertyMapping: $propertyMapping, - source: $source, - target: null, - mandatory: $propertyMapping->isTargetConstructorMandatory(), - context: $context, - ); - - if ($propertyMapping->isTargetConstructorVariadic()) { - if ( - !\is_array($targetPropertyValue) - && !$targetPropertyValue instanceof \Traversable - ) { - $targetPropertyValue = [$targetPropertyValue]; - } - - $constructorArguments->addVariadicArgument($targetPropertyValue); - } else { - $constructorArguments->addArgument( - $propertyMapping->getTargetProperty(), - $targetPropertyValue, - ); - } - } catch (UninitializedSourcePropertyException $e) { - $sourceProperty = $e->getPropertyName(); - $constructorArguments->addUnsetSourceProperty($sourceProperty); - - continue; - } catch (UnsupportedPropertyMappingException) { - continue; - } - } - - // add arguments from extra target values - - /** @var mixed $value */ - foreach ($extraTargetValues as $property => $value) { - // skip if there is no constructor property mapping for this - if (!isset($constructorPropertyMappings[$property])) { - continue; - } - - $constructorArguments->addArgument($property, $value); - } - - return $constructorArguments; - } - - /** - * @param array $propertyMappings - * @param array $extraTargetValues - */ - private function readSourceAndWriteTarget( - object $source, - object $target, - array $propertyMappings, - array $extraTargetValues, - Context $context, - ): object { - foreach ($propertyMappings as $propertyMapping) { - $target = $this->readSourcePropertyAndWriteTargetProperty( - source: $source, - target: $target, - propertyMapping: $propertyMapping, - context: $context, - ); - } - - // process extra target values - - /** @var mixed $value */ - foreach ($extraTargetValues as $property => $value) { - if (!isset($propertyMappings[$property])) { - continue; - } - - $propertyMapping = $propertyMappings[$property]; - - $target = $this->readerWriter->writeTargetProperty( - target: $target, - propertyMapping: $propertyMapping, - value: $value, - context: $context, - silentOnError: true, - ); - } - - return $target; - } - - /** - * @return object The target object after writing the property, can be of a - * different instance but should be of the same class - */ - private function readSourcePropertyAndWriteTargetProperty( - object $source, - object $target, - PropertyMapping $propertyMapping, - Context $context, - ): object { - if ( - $propertyMapping->getTargetReadMode() === ReadMode::None - && $propertyMapping->getTargetSetterWriteMode() === WriteMode::None - ) { - return $target; - } - - try { - /** @var mixed $targetPropertyValue */ - [$targetPropertyValue, $isChanged] = $this->transformValue( - propertyMapping: $propertyMapping, - source: $source, - target: $target, - mandatory: false, - context: $context, - ); - } catch (UninitializedSourcePropertyException | UnsupportedPropertyMappingException) { - return $target; - } - - // write - - if ( - $isChanged - || $propertyMapping->getTargetSetterWriteMode() === WriteMode::DynamicProperty - ) { - if ($targetPropertyValue instanceof AdderRemoverProxy) { - $target = $targetPropertyValue->getHostObject(); - } - - return $this->readerWriter->writeTargetProperty( - target: $target, - propertyMapping: $propertyMapping, - value: $targetPropertyValue, - context: $context, - silentOnError: false, - ); - } - - return $target; - } - - /** - * @param object|null $target Target is null if the transformation is for a - * constructor argument - * @return array{mixed,bool} The target value after transformation and whether the value differs from before transformation - */ - private function transformValue( - PropertyMapping $propertyMapping, - object $source, - ?object $target, - bool $mandatory, - Context $context, - ): mixed { - // if a custom property mapper is set, then use it + // transform - if (($serviceMethodSpecification = $propertyMapping->getPropertyMapper()) !== null) { - /** @psalm-suppress MixedReturnStatement */ - return $this->transformValueUsingPropertyMapper( - propertyMapping: $propertyMapping, - serviceMethodSpecification: $serviceMethodSpecification, + return $this->getObjectProcessorFactory() + ->getObjectProcessor($objectToObjectMetadata) + ->transform( source: $source, target: $target, + targetType: $targetType, context: $context, ); - } - - // if source property name is null, continue. there is nothing to - // transform - - $sourceProperty = $propertyMapping->getSourceProperty(); - - if ($sourceProperty === null) { - throw new UnsupportedPropertyMappingException(); - } - - // get the value of the source property - - try { - /** @var mixed */ - $sourcePropertyValue = $this->readerWriter - ->readSourceProperty($source, $propertyMapping, $context); - } catch (UninitializedSourcePropertyException $e) { - if (!$mandatory) { - throw $e; - } - - $sourcePropertyValue = null; - } - - // short circuit. optimization for transformation between scalar and - // null, so that we don't have to go through the main transformer for - // this common task. - - if ($context(MapperOptions::class)?->objectToObjectScalarShortCircuit === true) { - // if source is null & target accepts null, we set the - // target to null - - if ($propertyMapping->targetCanAcceptNull() && $sourcePropertyValue === null) { - return [null, true]; - } - - // if the the source is null or scalar, and the target is a scalar - - $targetScalarType = $propertyMapping->getTargetScalarType(); - - if ($targetScalarType !== null) { - if ($sourcePropertyValue === null) { - $result = match ($targetScalarType) { - 'int' => 0, - 'float' => 0.0, - 'string' => '', - 'bool' => false, - 'null' => null, - }; - - return [$result, true]; - } elseif (\is_scalar($sourcePropertyValue)) { - $result = match ($targetScalarType) { - 'int' => (int) $sourcePropertyValue, - 'float' => (float) $sourcePropertyValue, - 'string' => (string) $sourcePropertyValue, - 'bool' => (bool) $sourcePropertyValue, - 'null' => null, - }; - - return [$result, true]; - } - } - } - - // get the value of the target property if the target is an object and - // target value reading is enabled - - if ( - \is_object($target) - && $context(MapperOptions::class)?->readTargetValue - ) { - // if this is for a property mapping, not a constructor argument - - /** @var mixed */ - $targetPropertyValue = $this->readerWriter->readTargetProperty( - $target, - $propertyMapping, - $context, - ); - } else { - // if this is for a constructor argument, we don't have an existing - // value - - $targetPropertyValue = null; - } - - // if we get an AdderRemoverProxy, change the target type - - $targetTypes = $propertyMapping->getTargetTypes(); - - if ($targetPropertyValue instanceof AdderRemoverProxy) { - $key = $targetTypes[0]->getCollectionKeyTypes(); - $value = $targetTypes[0]->getCollectionValueTypes(); - - $targetTypes = [ - TypeFactory::objectWithKeyValue( - \ArrayAccess::class, - $key[0], - $value[0], - ), - ]; - } - - // guess source type, and get the compatible type from metadata, so - // we can preserve generics information - - $guessedSourceType = TypeGuesser::guessTypeFromVariable($sourcePropertyValue); - $sourceType = $propertyMapping->getCompatibleSourceType($guessedSourceType) - ?? $guessedSourceType; - - // add attributes to context - - $sourceAttributes = $propertyMapping->getSourceAttributes(); - $context = $context->with($sourceAttributes); - - $targetAttributes = $propertyMapping->getTargetAttributes(); - $context = $context->with($targetAttributes); - - // transform the value - - /** @var mixed */ - $originalTargetPropertyValue = $targetPropertyValue; - - /** @var mixed */ - $targetPropertyValue = $this->getMainTransformer()->transform( - source: $sourcePropertyValue, - target: $targetPropertyValue, - sourceType: $sourceType, - targetTypes: $targetTypes, - context: $context, - path: $propertyMapping->getTargetProperty(), - ); - - return [ - $targetPropertyValue, - $targetPropertyValue !== $originalTargetPropertyValue, - ]; - } - - /** - * @return array{mixed,bool} The target value after transformation and whether the value differs from before transformation - */ - private function transformValueUsingPropertyMapper( - PropertyMapping $propertyMapping, - ServiceMethodSpecification $serviceMethodSpecification, - object $source, - ?object $target, - Context $context, - ): array { - if ($target === null) { - $targetPropertyValue = null; - } else { - /** @var mixed */ - $targetPropertyValue = $this->readerWriter->readTargetProperty( - $target, - $propertyMapping, - $context, - ); - } - - $serviceMethodRunner = ServiceMethodRunner::create( - serviceLocator: $this->propertyMapperLocator, - mainTransformer: $this->getMainTransformer(), - subMapperFactory: $this->subMapperFactory, - ); - - /** @var mixed */ - $result = $serviceMethodRunner->runPropertyMapper( - serviceMethodSpecification: $serviceMethodSpecification, - source: $source, - target: $target, - targetPropertyValue: $targetPropertyValue, - targetType: null, - context: $context, - ); - - return [$result, $result !== $targetPropertyValue]; - } - - private function mapDynamicProperties( - object $source, - object $target, - ObjectToObjectMetadata $objectToObjectMetadata, - Context $context, - ): void { - $sourceProperties = $objectToObjectMetadata->getSourceProperties(); - - /** @var mixed $sourcePropertyValue */ - foreach (get_object_vars($source) as $sourceProperty => $sourcePropertyValue) { - if (!\in_array($sourceProperty, $sourceProperties, true)) { - try { - if (isset($target->{$sourceProperty})) { - /** @psalm-suppress MixedAssignment */ - $currentTargetPropertyValue = $target->{$sourceProperty}; - } else { - $currentTargetPropertyValue = null; - } - - - if ( - $currentTargetPropertyValue === null - || \is_scalar($currentTargetPropertyValue) - ) { - /** @psalm-suppress MixedAssignment */ - $targetPropertyValue = $sourcePropertyValue; - } else { - /** @var mixed */ - $targetPropertyValue = $this->getMainTransformer()->transform( - source: $sourcePropertyValue, - target: $currentTargetPropertyValue, - sourceType: null, - targetTypes: [], - context: $context, - path: $sourceProperty, - ); - } - } catch (\Throwable) { - $targetPropertyValue = null; - } - - $target->{$sourceProperty} = $targetPropertyValue; - } - } } #[\Override] diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index 5537998b..5ae8e99e 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php @@ -28,7 +28,7 @@ use Rekalogika\Mapper\Transformer\MetadataUtil\TargetClassResolverInterface; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadataFactoryInterface; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMapping; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ReadMode; use Rekalogika\Mapper\Util\ClassUtil; @@ -120,9 +120,14 @@ class: $targetClass, $sourceAttributes = new SourcePropertyAttributes($sourcePropertyMetadata->getAttributes()); $targetAttributes = new TargetPropertyAttributes($targetPropertyMetadata->getAttributes()); + // generate id + + $propertyMappingMetadataId = hash('sha256', $sourceClass . $sourceProperty . $targetClass . $targetProperty); + // instantiate property mapping - $propertyMapping = new PropertyMapping( + $propertyMapping = new PropertyMappingMetadata( + id: $propertyMappingMetadataId, sourceProperty: $sourcePropertyMetadata->getReadMode() !== ReadMode::None ? $sourceProperty : null, targetProperty: $targetProperty, sourceTypes: $sourcePropertyMetadata->getTypes(), @@ -158,7 +163,10 @@ class: $targetClass, $effectivePropertiesToMap[] = $targetProperty; } + $metadataId = hash('sha256', $sourceClass . $targetClass); + $objectToObjectMetadata = new ObjectToObjectMetadata( + id: $metadataId, sourceClass: $sourceClass, targetClass: $targetClass, providedTargetClass: $providedTargetClass, @@ -212,7 +220,7 @@ class: $targetClass, /** * @param class-string $targetClass * @param list $eagerProperties - * @param array $constructorPropertyMappings + * @param array $constructorPropertyMappings * @return array{array,bool} */ private function getProxyParameters( diff --git a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php index 84f4a252..82196489 100644 --- a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php +++ b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php @@ -24,27 +24,27 @@ final readonly class ObjectToObjectMetadata { /** - * @var array + * @var array */ private array $allPropertyMappings; /** - * @var array + * @var array */ private array $propertyMappings; /** - * @var array + * @var array */ private array $constructorPropertyMappings; /** - * @var array + * @var array */ private array $lazyPropertyMappings; /** - * @var array + * @var array */ private array $eagerPropertyMappings; @@ -53,11 +53,12 @@ * @param class-string $targetClass Effective target class after resolving inheritance map * @param class-string $providedTargetClass * @param list $allTargetClasses - * @param list $allPropertyMappings + * @param list $allPropertyMappings * @param array $targetProxySkippedProperties * @param list $sourceProperties List of the source properties. Used by `ObjectToObjectTransformer` to determine if a property is a dynamic property. A property not listed here is considered dynamic. */ public function __construct( + private string $id, private string $sourceClass, private string $targetClass, private string $providedTargetClass, @@ -123,6 +124,7 @@ public function withTargetProxy( bool $constructorIsEager, ): self { return new self( + id: $this->id, sourceClass: $this->sourceClass, targetClass: $this->targetClass, providedTargetClass: $this->providedTargetClass, @@ -149,6 +151,7 @@ public function withReasonCannotUseProxy( string $reason, ): self { return new self( + id: $this->id, sourceClass: $this->sourceClass, targetClass: $this->targetClass, providedTargetClass: $this->providedTargetClass, @@ -171,6 +174,11 @@ public function withReasonCannotUseProxy( ); } + public function getId(): string + { + return $this->id; + } + /** * @return class-string */ @@ -219,7 +227,7 @@ public function isTargetUnalterable(): bool } /** - * @return array + * @return array */ public function getPropertyMappings(): array { @@ -227,7 +235,7 @@ public function getPropertyMappings(): array } /** - * @return array + * @return array */ public function getLazyPropertyMappings(): array { @@ -235,7 +243,7 @@ public function getLazyPropertyMappings(): array } /** - * @return array + * @return array */ public function getEagerPropertyMappings(): array { @@ -243,7 +251,7 @@ public function getEagerPropertyMappings(): array } /** - * @return array + * @return array */ public function getConstructorPropertyMappings(): array { @@ -251,7 +259,7 @@ public function getConstructorPropertyMappings(): array } /** - * @return array + * @return array */ public function getAllPropertyMappings(): array { diff --git a/src/Transformer/ObjectToObjectMetadata/PropertyMapping.php b/src/Transformer/ObjectToObjectMetadata/PropertyMappingMetadata.php similarity index 96% rename from src/Transformer/ObjectToObjectMetadata/PropertyMapping.php rename to src/Transformer/ObjectToObjectMetadata/PropertyMappingMetadata.php index f9a8f317..f059bfc2 100644 --- a/src/Transformer/ObjectToObjectMetadata/PropertyMapping.php +++ b/src/Transformer/ObjectToObjectMetadata/PropertyMappingMetadata.php @@ -23,7 +23,7 @@ * @immutable * @internal */ -final readonly class PropertyMapping +final readonly class PropertyMappingMetadata { /** * @var array $sourceTypes @@ -41,6 +41,7 @@ * @param 'int'|'float'|'string'|'bool'|'null'|null $targetScalarType */ public function __construct( + private string $id, private ?string $sourceProperty, private string $targetProperty, array $sourceTypes, @@ -75,6 +76,11 @@ public function __construct( $this->targetTypes = array_values($targetTypes); } + public function getId(): string + { + return $this->id; + } + public function getCompatibleSourceType(Type $type): ?Type { foreach ($this->sourceTypes as $sourceType) { @@ -117,6 +123,11 @@ public function getPropertyMapper(): ?ServiceMethodSpecification return $this->propertyMapper; } + public function hasPropertyMapper(): bool + { + return $this->propertyMapper !== null; + } + /** * If set, set the property directly, without delegating to the main * transformer diff --git a/src/Transformer/Util/ReaderWriter.php b/src/Transformer/Util/ReaderWriter.php index 16bdb78e..3fc01ea1 100644 --- a/src/Transformer/Util/ReaderWriter.php +++ b/src/Transformer/Util/ReaderWriter.php @@ -20,7 +20,7 @@ use Rekalogika\Mapper\Transformer\Exception\UnableToWriteException; use Rekalogika\Mapper\Transformer\Exception\UninitializedSourcePropertyException; use Rekalogika\Mapper\Transformer\Model\AdderRemoverProxy; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMapping; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ReadMode; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Visibility; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\WriteMode; @@ -42,7 +42,7 @@ public function __construct( */ public function readSourceProperty( object $source, - PropertyMapping $propertyMapping, + PropertyMappingMetadata $propertyMapping, Context $context, ): mixed { $property = $propertyMapping->getSourceProperty(); @@ -125,7 +125,7 @@ public function readSourceProperty( */ public function readTargetProperty( object $target, - PropertyMapping $propertyMapping, + PropertyMappingMetadata $propertyMapping, Context $context, ): mixed { if ( @@ -197,7 +197,7 @@ public function readTargetProperty( */ public function writeTargetProperty( object $target, - PropertyMapping $propertyMapping, + PropertyMappingMetadata $propertyMapping, mixed $value, Context $context, bool $silentOnError, diff --git a/src/TransformerProcessor/ObjectProcessor/DefaultObjectProcessorFactory.php b/src/TransformerProcessor/ObjectProcessor/DefaultObjectProcessorFactory.php new file mode 100644 index 00000000..fe96d09c --- /dev/null +++ b/src/TransformerProcessor/ObjectProcessor/DefaultObjectProcessorFactory.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\TransformerProcessor\ObjectProcessor; + +use Psr\Container\ContainerInterface; +use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; +use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface; +use Rekalogika\Mapper\Transformer\MainTransformerAwareTrait; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; +use Rekalogika\Mapper\TransformerProcessor\ObjectProcessorFactoryInterface; +use Rekalogika\Mapper\TransformerProcessor\ObjectProcessorInterface; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorFactoryInterface; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +/** + * @internal + */ +final class DefaultObjectProcessorFactory implements ObjectProcessorFactoryInterface +{ + use MainTransformerAwareTrait; + + public function __construct( + private readonly ContainerInterface $propertyMapperLocator, + private readonly SubMapperFactoryInterface $subMapperFactory, + private readonly ProxyFactoryInterface $proxyFactory, + private readonly PropertyAccessorInterface $propertyAccessor, + private readonly PropertyProcessorFactoryInterface $propertyProcessorFactory, + ) {} + + public function getObjectProcessor( + ObjectToObjectMetadata $metadata, + ): ObjectProcessorInterface { + return new ObjectProcessor( + metadata: $metadata, + mainTransformer: $this->getMainTransformer(), + propertyMapperLocator: $this->propertyMapperLocator, + subMapperFactory: $this->subMapperFactory, + proxyFactory: $this->proxyFactory, + propertyAccessor: $this->propertyAccessor, + propertyProcessorFactory: $this->propertyProcessorFactory + ->withMainTransformer($this->getMainTransformer()), + ); + } +} diff --git a/src/TransformerProcessor/ObjectProcessor/ObjectProcessor.php b/src/TransformerProcessor/ObjectProcessor/ObjectProcessor.php new file mode 100644 index 00000000..078b231f --- /dev/null +++ b/src/TransformerProcessor/ObjectProcessor/ObjectProcessor.php @@ -0,0 +1,508 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\TransformerProcessor\ObjectProcessor; + +use Psr\Container\ContainerInterface; +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Context\ExtraTargetValues; +use Rekalogika\Mapper\Context\MapperOptions; +use Rekalogika\Mapper\MainTransformer\MainTransformerInterface; +use Rekalogika\Mapper\ObjectCache\ObjectCache; +use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; +use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface; +use Rekalogika\Mapper\Transformer\Exception\ClassNotInstantiableException; +use Rekalogika\Mapper\Transformer\Exception\ExtraTargetPropertyNotFoundException; +use Rekalogika\Mapper\Transformer\Exception\InstantiationFailureException; +use Rekalogika\Mapper\Transformer\Exception\UninitializedSourcePropertyException; +use Rekalogika\Mapper\Transformer\Exception\UnsupportedPropertyMappingException; +use Rekalogika\Mapper\Transformer\Model\ConstructorArguments; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; +use Rekalogika\Mapper\TransformerProcessor\ObjectProcessorInterface; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorFactoryInterface; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * @internal + */ +final readonly class ObjectProcessor implements ObjectProcessorInterface +{ + public function __construct( + private ObjectToObjectMetadata $metadata, + private MainTransformerInterface $mainTransformer, + private ContainerInterface $propertyMapperLocator, + private SubMapperFactoryInterface $subMapperFactory, + private ProxyFactoryInterface $proxyFactory, + private PropertyAccessorInterface $propertyAccessor, + private PropertyProcessorFactoryInterface $propertyProcessorFactory, + ) {} + + public function transform( + object $source, + ?object $target, + Type $targetType, + Context $context, + ): object { + // disregard target if target is read only or target value reading is + // disabled + + if ( + $this->metadata->isTargetUnalterable() + || $context(MapperOptions::class)?->readTargetValue !== true + ) { + $target = null; + } + + // get extra target values + + $extraTargetValues = $this->getExtraTargetValues($context); + + // initialize target if target is null + + if (null === $target) { + $canUseTargetProxy = $this->metadata->canUseTargetProxy() + && $context(MapperOptions::class)?->lazyLoading; + + if ($canUseTargetProxy) { + $target = $this->instantiateTargetProxy( + source: $source, + extraTargetValues: $extraTargetValues, + context: $context, + ); + } else { + $target = $this->instantiateRealTarget( + source: $source, + extraTargetValues: $extraTargetValues, + context: $context, + ); + } + } else { + $canUseTargetProxy = false; + } + + // save object to cache + + $context(ObjectCache::class)?->saveTarget( + source: $source, + targetType: $targetType, + target: $target, + ); + + // map properties if it is not a proxy + + if (!$canUseTargetProxy) { + // map dynamic properties if both are stdClass or allow dynamic + // properties + + if ( + $this->metadata->sourceAllowsDynamicProperties() + && $this->metadata->targetAllowsDynamicProperties() + ) { + $this->mapDynamicProperties( + source: $source, + target: $target, + context: $context, + ); + } + + $target = $this->readSourceAndWriteTarget( + source: $source, + target: $target, + propertyMappings: $this->getPropertyMappings(), + extraTargetValues: $extraTargetValues, + context: $context, + ); + } + + return $target; + } + + // + // property mappings getters + // + + /** + * @return array + */ + private function getPropertyMappings(): array + { + return $this->metadata->getPropertyMappings(); + } + + /** + * @return array + */ + private function getLazyPropertyMappings(): array + { + return $this->metadata->getLazyPropertyMappings(); + } + + /** + * @return array + */ + private function getEagerPropertyMappings(): array + { + return $this->metadata->getEagerPropertyMappings(); + } + + // + // extra target values + // + + /** + * @return array + */ + private function getExtraTargetValues(Context $context): array + { + $extraTargetValues = $context(ExtraTargetValues::class) + ?->getArgumentsForClass($this->metadata->getAllTargetClasses()) + ?? []; + + $allPropertyMappings = $this->metadata->getPropertyMappings(); + + foreach (array_keys($extraTargetValues) as $property) { + if (!isset($allPropertyMappings[$property])) { + throw new ExtraTargetPropertyNotFoundException( + class: $this->metadata->getTargetClass(), + property: $property, + context: $context, + ); + } + } + + return $extraTargetValues; + } + + // + // instantiation + // + + /** + * @param array $extraTargetValues + */ + private function instantiateRealTarget( + object $source, + array $extraTargetValues, + Context $context, + ): object { + $targetClass = $this->metadata->getTargetClass(); + + // check if class is valid & instantiable + + if (!$this->metadata->isInstantiable()) { + throw new ClassNotInstantiableException($targetClass, context: $context); + } + + $constructorArguments = $this->generateConstructorArguments( + source: $source, + extraTargetValues: $extraTargetValues, + context: $context, + ); + + try { + $reflectionClass = new \ReflectionClass($targetClass); + + return $reflectionClass + ->newInstanceArgs($constructorArguments->getArguments()); + } catch (\TypeError | \ReflectionException $e) { + throw new InstantiationFailureException( + source: $source, + targetClass: $targetClass, + constructorArguments: $constructorArguments->getArguments(), + unsetSourceProperties: $constructorArguments->getUnsetSourceProperties(), + previous: $e, + context: $context, + ); + } + } + + /** + * @param array $extraTargetValues + */ + private function instantiateTargetProxy( + object $source, + array $extraTargetValues, + Context $context, + ): object { + $targetClass = $this->metadata->getTargetClass(); + + // check if class is valid & instantiable + + if (!$this->metadata->isInstantiable()) { + throw new ClassNotInstantiableException($targetClass, context: $context); + } + + // create proxy initializer. this initializer will be executed when the + // proxy is first accessed + + $initializer = function (object $target) use ( + $source, + $context, + $extraTargetValues, + ): void { + // if the constructor is lazy, run it here + + if (!$this->metadata->constructorIsEager()) { + $target = $this->runConstructorManually( + source: $source, + target: $target, + extraTargetValues: $extraTargetValues, + context: $context, + ); + } + + // map lazy properties + + $this->readSourceAndWriteTarget( + source: $source, + target: $target, + propertyMappings: $this->getLazyPropertyMappings(), + extraTargetValues: $extraTargetValues, + context: $context, + ); + }; + + // instantiate the proxy + + $target = $this->proxyFactory->createProxy( + class: $targetClass, + initializer: $initializer, + eagerProperties: $this->metadata->getTargetProxySkippedProperties(), + ); + + // if the constructor is eager, run it here + + if ($this->metadata->constructorIsEager()) { + $target = $this->runConstructorManually( + source: $source, + target: $target, + extraTargetValues: $extraTargetValues, + context: $context, + ); + } + + // map eager properties + + $target = $this->readSourceAndWriteTarget( + source: $source, + target: $target, + propertyMappings: $this->getEagerPropertyMappings(), + extraTargetValues: $extraTargetValues, + context: $context, + ); + + return $target; + } + + /** + * @param array $extraTargetValues + */ + private function runConstructorManually( + object $source, + object $target, + array $extraTargetValues, + Context $context, + ): object { + if (!method_exists($target, '__construct')) { + return $target; + } + + $constructorArguments = $this->generateConstructorArguments( + source: $source, + extraTargetValues: $extraTargetValues, + context: $context, + ); + + $arguments = $constructorArguments->getArguments(); + + try { + /** + * @psalm-suppress DirectConstructorCall + * @psalm-suppress MixedMethodCall + */ + $target->__construct(...$arguments); + } catch (\TypeError | \ReflectionException $e) { + throw new InstantiationFailureException( + source: $source, + targetClass: $target::class, + constructorArguments: $constructorArguments->getArguments(), + unsetSourceProperties: $constructorArguments->getUnsetSourceProperties(), + previous: $e, + context: $context, + ); + } + + return $target; + } + + /** + * @param array $extraTargetValues + */ + private function generateConstructorArguments( + object $source, + array $extraTargetValues, + Context $context, + ): ConstructorArguments { + $constructorPropertyMappings = $this->metadata->getConstructorPropertyMappings(); + + $constructorArguments = new ConstructorArguments(); + + // add arguments from property mappings + + foreach ($constructorPropertyMappings as $propertyMapping) { + try { + /** @var mixed $targetPropertyValue */ + [$targetPropertyValue,] = $this->propertyProcessorFactory + ->getPropertyProcessor($propertyMapping) + ->transformValue( + source: $source, + target: null, + mandatory: $propertyMapping->isTargetConstructorMandatory(), + context: $context, + ); + + if ($propertyMapping->isTargetConstructorVariadic()) { + if ( + !\is_array($targetPropertyValue) + && !$targetPropertyValue instanceof \Traversable + ) { + $targetPropertyValue = [$targetPropertyValue]; + } + + $constructorArguments->addVariadicArgument($targetPropertyValue); + } else { + $constructorArguments->addArgument( + $propertyMapping->getTargetProperty(), + $targetPropertyValue, + ); + } + } catch (UninitializedSourcePropertyException $e) { + $sourceProperty = $e->getPropertyName(); + $constructorArguments->addUnsetSourceProperty($sourceProperty); + + continue; + } catch (UnsupportedPropertyMappingException) { + continue; + } + } + + // add arguments from extra target values + + /** @var mixed $value */ + foreach ($extraTargetValues as $property => $value) { + // skip if there is no constructor property mapping for this + if (!isset($constructorPropertyMappings[$property])) { + continue; + } + + $constructorArguments->addArgument($property, $value); + } + + return $constructorArguments; + } + + // + // properties mapping + // + + /** + * @param array $propertyMappings + * @param array $extraTargetValues + */ + private function readSourceAndWriteTarget( + object $source, + object $target, + array $propertyMappings, + array $extraTargetValues, + Context $context, + ): object { + foreach ($propertyMappings as $propertyMapping) { + $target = $this->propertyProcessorFactory + ->getPropertyProcessor($propertyMapping) + ->readSourcePropertyAndWriteTargetProperty( + source: $source, + target: $target, + context: $context, + ); + } + + // process extra target values + + /** @var mixed $value */ + foreach ($extraTargetValues as $property => $value) { + if (!isset($propertyMappings[$property])) { + continue; + } + + $propertyMapping = $propertyMappings[$property]; + + $target = $this->propertyProcessorFactory + ->getPropertyProcessor($propertyMapping) + ->writeTargetProperty( + target: $target, + value: $value, + context: $context, + silentOnError: true, + ); + } + + return $target; + } + + private function mapDynamicProperties( + object $source, + object $target, + Context $context, + ): void { + $sourceProperties = $this->metadata->getSourceProperties(); + + /** @var mixed $sourcePropertyValue */ + foreach (get_object_vars($source) as $sourceProperty => $sourcePropertyValue) { + if (!\in_array($sourceProperty, $sourceProperties, true)) { + try { + if (isset($target->{$sourceProperty})) { + /** @psalm-suppress MixedAssignment */ + $currentTargetPropertyValue = $target->{$sourceProperty}; + } else { + $currentTargetPropertyValue = null; + } + + + if ( + $currentTargetPropertyValue === null + || \is_scalar($currentTargetPropertyValue) + ) { + /** @psalm-suppress MixedAssignment */ + $targetPropertyValue = $sourcePropertyValue; + } else { + /** @var mixed */ + $targetPropertyValue = $this->mainTransformer->transform( + source: $sourcePropertyValue, + target: $currentTargetPropertyValue, + sourceType: null, + targetTypes: [], + context: $context, + path: $sourceProperty, + ); + } + } catch (\Throwable) { + $targetPropertyValue = null; + } + + $target->{$sourceProperty} = $targetPropertyValue; + } + } + } +} diff --git a/src/TransformerProcessor/ObjectProcessorFactoryInterface.php b/src/TransformerProcessor/ObjectProcessorFactoryInterface.php new file mode 100644 index 00000000..13a2fd8d --- /dev/null +++ b/src/TransformerProcessor/ObjectProcessorFactoryInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\TransformerProcessor; + +use Rekalogika\Mapper\Transformer\MainTransformerAwareInterface; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; + +/** + * @internal + */ +interface ObjectProcessorFactoryInterface extends MainTransformerAwareInterface +{ + public function getObjectProcessor( + ObjectToObjectMetadata $metadata, + ): ObjectProcessorInterface; +} diff --git a/src/TransformerProcessor/ObjectProcessorInterface.php b/src/TransformerProcessor/ObjectProcessorInterface.php new file mode 100644 index 00000000..382894cc --- /dev/null +++ b/src/TransformerProcessor/ObjectProcessorInterface.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\TransformerProcessor; + +use Rekalogika\Mapper\Context\Context; +use Symfony\Component\PropertyInfo\Type; + +/** + * @internal + */ +interface ObjectProcessorInterface +{ + public function transform( + object $source, + ?object $target, + Type $targetType, + Context $context, + ): object; +} diff --git a/src/TransformerProcessor/PropertyProcessor/CachingPropertyProcessorFactory.php b/src/TransformerProcessor/PropertyProcessor/CachingPropertyProcessorFactory.php new file mode 100644 index 00000000..d53741c9 --- /dev/null +++ b/src/TransformerProcessor/PropertyProcessor/CachingPropertyProcessorFactory.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\TransformerProcessor\PropertyProcessor; + +use Rekalogika\Mapper\Transformer\MainTransformerAwareTrait; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorFactoryInterface; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorInterface; + +/** + * @internal + */ +final class CachingPropertyProcessorFactory implements PropertyProcessorFactoryInterface +{ + use MainTransformerAwareTrait; + + /** + * @var array $cache + */ + private array $cache = []; + + private ?PropertyProcessorFactoryInterface $decoratedWithMainTransformer = null; + + public function __construct( + private readonly PropertyProcessorFactoryInterface $decorated, + ) {} + + private function getDecorated(): PropertyProcessorFactoryInterface + { + return $this->decoratedWithMainTransformer ??= $this->decorated + ->withMainTransformer($this->getMainTransformer()); + } + + public function getPropertyProcessor( + PropertyMappingMetadata $metadata, + ): PropertyProcessorInterface { + $id = $metadata->getId(); + + return $this->cache[$id] ??= $this->getDecorated() + ->getPropertyProcessor($metadata); + } +} diff --git a/src/TransformerProcessor/PropertyProcessor/DefaultPropertyProcessorFactory.php b/src/TransformerProcessor/PropertyProcessor/DefaultPropertyProcessorFactory.php new file mode 100644 index 00000000..5e857c5c --- /dev/null +++ b/src/TransformerProcessor/PropertyProcessor/DefaultPropertyProcessorFactory.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\TransformerProcessor\PropertyProcessor; + +use Psr\Container\ContainerInterface; +use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface; +use Rekalogika\Mapper\Transformer\MainTransformerAwareTrait; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorFactoryInterface; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorInterface; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +/** + * @internal + */ +final class DefaultPropertyProcessorFactory implements + PropertyProcessorFactoryInterface +{ + use MainTransformerAwareTrait; + + public function __construct( + private readonly PropertyAccessorInterface $propertyAccessor, + private readonly SubMapperFactoryInterface $subMapperFactory, + private readonly ContainerInterface $propertyMapperLocator, + ) {} + + public function getPropertyProcessor( + PropertyMappingMetadata $metadata, + ): PropertyProcessorInterface { + return new PropertyProcessor( + metadata: $metadata, + propertyAccessor: $this->propertyAccessor, + mainTransformer: $this->getMainTransformer(), + subMapperFactory: $this->subMapperFactory, + propertyMapperLocator: $this->propertyMapperLocator, + ); + } +} diff --git a/src/TransformerProcessor/PropertyProcessor/PropertyProcessor.php b/src/TransformerProcessor/PropertyProcessor/PropertyProcessor.php new file mode 100644 index 00000000..1566312b --- /dev/null +++ b/src/TransformerProcessor/PropertyProcessor/PropertyProcessor.php @@ -0,0 +1,550 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\TransformerProcessor\PropertyProcessor; + +use Psr\Container\ContainerInterface; +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Context\MapperOptions; +use Rekalogika\Mapper\Exception\UnexpectedValueException; +use Rekalogika\Mapper\MainTransformer\MainTransformerInterface; +use Rekalogika\Mapper\ServiceMethod\ServiceMethodRunner; +use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface; +use Rekalogika\Mapper\Transformer\Exception\NewInstanceReturnedButCannotBeSetOnTargetException; +use Rekalogika\Mapper\Transformer\Exception\UnableToReadException; +use Rekalogika\Mapper\Transformer\Exception\UnableToWriteException; +use Rekalogika\Mapper\Transformer\Exception\UninitializedSourcePropertyException; +use Rekalogika\Mapper\Transformer\Exception\UnsupportedPropertyMappingException; +use Rekalogika\Mapper\Transformer\Model\AdderRemoverProxy; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ReadMode; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Visibility; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\WriteMode; +use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorInterface; +use Rekalogika\Mapper\Util\TypeFactory; +use Rekalogika\Mapper\Util\TypeGuesser; +use Symfony\Component\PropertyAccess\PropertyAccessorInterface; +use Symfony\Component\PropertyInfo\Type; + +/** + * @internal + */ +final readonly class PropertyProcessor implements PropertyProcessorInterface +{ + public function __construct( + private PropertyMappingMetadata $metadata, + private PropertyAccessorInterface $propertyAccessor, + private MainTransformerInterface $mainTransformer, + private SubMapperFactoryInterface $subMapperFactory, + private ContainerInterface $propertyMapperLocator, + ) {} + + /** + * @return object The target object after writing the property, can be of a + * different instance but should be of the same class + */ + public function readSourcePropertyAndWriteTargetProperty( + object $source, + object $target, + Context $context, + ): object { + if ( + $this->metadata->getTargetReadMode() === ReadMode::None + && $this->metadata->getTargetSetterWriteMode() === WriteMode::None + ) { + return $target; + } + + try { + /** @var mixed $targetPropertyValue */ + [$targetPropertyValue, $isChanged] = $this->transformValue( + source: $source, + target: $target, + mandatory: false, + context: $context, + ); + } catch (UninitializedSourcePropertyException | UnsupportedPropertyMappingException) { + return $target; + } + + // write + + if ( + $isChanged + || $this->metadata->getTargetSetterWriteMode() === WriteMode::DynamicProperty + ) { + if ($targetPropertyValue instanceof AdderRemoverProxy) { + $target = $targetPropertyValue->getHostObject(); + } + + return $this->writeTargetProperty( + target: $target, + value: $targetPropertyValue, + context: $context, + silentOnError: false, + ); + } + + return $target; + } + + /** + * @param object|null $target Target is null if the transformation is for a + * constructor argument + * @return array{mixed,bool} The target value after transformation and whether the value differs from before transformation + */ + public function transformValue( + object $source, + ?object $target, + bool $mandatory, + Context $context, + ): mixed { + // if a custom property mapper is set, then use it + + if ($this->metadata->hasPropertyMapper()) { + /** @psalm-suppress MixedReturnStatement */ + return $this->transformValueUsingPropertyMapper( + source: $source, + target: $target, + context: $context, + ); + } + + // if source property name is null, continue. there is nothing to + // transform + + $sourceProperty = $this->metadata->getSourceProperty(); + + if ($sourceProperty === null) { + throw new UnsupportedPropertyMappingException(); + } + + // get the value of the source property + + try { + /** @var mixed */ + $sourcePropertyValue = $this->readSourceProperty( + source: $source, + context: $context, + ); + } catch (UninitializedSourcePropertyException $e) { + if (!$mandatory) { + throw $e; + } + + $sourcePropertyValue = null; + } + + // short circuit. optimization for transformation between scalar and + // null, so that we don't have to go through the main transformer for + // this common task. + + if ($context(MapperOptions::class)?->objectToObjectScalarShortCircuit === true) { + // if source is null & target accepts null, we set the + // target to null + + if ($this->metadata->targetCanAcceptNull() && $sourcePropertyValue === null) { + return [null, true]; + } + + // if the the source is null or scalar, and the target is a scalar + + $targetScalarType = $this->metadata->getTargetScalarType(); + + if ($targetScalarType !== null) { + if ($sourcePropertyValue === null) { + $result = match ($targetScalarType) { + 'int' => 0, + 'float' => 0.0, + 'string' => '', + 'bool' => false, + 'null' => null, + }; + + return [$result, true]; + } elseif (\is_scalar($sourcePropertyValue)) { + $result = match ($targetScalarType) { + 'int' => (int) $sourcePropertyValue, + 'float' => (float) $sourcePropertyValue, + 'string' => (string) $sourcePropertyValue, + 'bool' => (bool) $sourcePropertyValue, + 'null' => null, + }; + + return [$result, true]; + } + } + } + + // get the value of the target property if the target is an object and + // target value reading is enabled + + if ( + \is_object($target) + && $context(MapperOptions::class)?->readTargetValue + ) { + // if this is for a property mapping, not a constructor argument + + /** @var mixed */ + $targetPropertyValue = $this->readTargetProperty( + target: $target, + context: $context, + ); + } else { + // if this is for a constructor argument, we don't have an existing + // value + + $targetPropertyValue = null; + } + + // if we get an AdderRemoverProxy, change the target type + + $targetTypes = $this->metadata->getTargetTypes(); + + if ($targetPropertyValue instanceof AdderRemoverProxy) { + $key = $targetTypes[0]->getCollectionKeyTypes(); + $value = $targetTypes[0]->getCollectionValueTypes(); + + $targetTypes = [ + TypeFactory::objectWithKeyValue( + \ArrayAccess::class, + $key[0], + $value[0], + ), + ]; + } + + // guess source type, and get the compatible type from metadata, so + // we can preserve generics information + + $guessedSourceType = TypeGuesser::guessTypeFromVariable($sourcePropertyValue); + $sourceType = $this->metadata->getCompatibleSourceType($guessedSourceType) + ?? $guessedSourceType; + + // add attributes to context + + $sourceAttributes = $this->metadata->getSourceAttributes(); + $context = $context->with($sourceAttributes); + + $targetAttributes = $this->metadata->getTargetAttributes(); + $context = $context->with($targetAttributes); + + // transform the value + + /** @var mixed */ + $originalTargetPropertyValue = $targetPropertyValue; + + /** @var mixed */ + $targetPropertyValue = $this->mainTransformer->transform( + source: $sourcePropertyValue, + target: $targetPropertyValue, + sourceType: $sourceType, + targetTypes: $targetTypes, + context: $context, + path: $this->metadata->getTargetProperty(), + ); + + return [ + $targetPropertyValue, + $targetPropertyValue !== $originalTargetPropertyValue, + ]; + } + + /** + * @return array{mixed,bool} The target value after transformation and whether the value differs from before transformation + */ + private function transformValueUsingPropertyMapper( + object $source, + ?object $target, + Context $context, + ): array { + $serviceMethodSpecification = $this->metadata->getPropertyMapper(); + + if ($serviceMethodSpecification === null) { + throw new UnexpectedValueException('PropertyMapper is null', context: $context); + } + + if ($target === null) { + $targetPropertyValue = null; + } else { + /** @var mixed */ + $targetPropertyValue = $this->readTargetProperty( + $target, + $context, + ); + } + + $serviceMethodRunner = ServiceMethodRunner::create( + serviceLocator: $this->propertyMapperLocator, + mainTransformer: $this->mainTransformer, + subMapperFactory: $this->subMapperFactory, + ); + + /** @var mixed */ + $result = $serviceMethodRunner->runPropertyMapper( + serviceMethodSpecification: $serviceMethodSpecification, + source: $source, + target: $target, + targetPropertyValue: $targetPropertyValue, + targetType: null, + context: $context, + ); + + return [$result, $result !== $targetPropertyValue]; + } + + /** + * @throws UninitializedSourcePropertyException + * @throws UnableToReadException + */ + private function readSourceProperty( + object $source, + Context $context, + ): mixed { + $property = $this->metadata->getSourceProperty(); + + if ($property === null) { + return null; + } + + if ($this->metadata->getSourceReadVisibility() !== Visibility::Public) { + throw new UnableToReadException( + $source, + $property, + context: $context, + ); + } + + try { + $accessorName = $this->metadata->getSourceReadName(); + $mode = $this->metadata->getSourceReadMode(); + + if ($accessorName === null) { + throw new UnexpectedValueException('AccessorName is null', context: $context); + } + + if ($mode === ReadMode::Property) { + return $source->{$accessorName}; + } elseif ($mode === ReadMode::Method) { + /** @psalm-suppress MixedMethodCall */ + return $source->{$accessorName}(); + } elseif ($mode === ReadMode::PropertyPath) { + return $this->propertyAccessor + ->getValue($source, $accessorName); + } elseif ($mode === ReadMode::DynamicProperty) { + $errorHandler = static function ( + int $errno, + string $errstr, + string $errfile, + int $errline, + ) use ($accessorName): bool { + if (str_starts_with($errstr, 'Undefined property')) { + restore_error_handler(); + throw new UninitializedSourcePropertyException($accessorName); + } + + return false; + }; + + set_error_handler($errorHandler); + /** @var mixed */ + $result = $source->{$accessorName}; + restore_error_handler(); + + return $result; + } + + return null; + } catch (\Error $e) { + $message = $e->getMessage(); + + if ( + str_contains($message, 'must not be accessed before initialization') + || str_contains($message, 'Cannot access uninitialized non-nullable property') + ) { + throw new UninitializedSourcePropertyException($property); + } + + throw new UnableToReadException( + $source, + $property, + context: $context, + previous: $e, + ); + } catch (\BadMethodCallException) { + throw new UninitializedSourcePropertyException($property); + } + } + + /** + * @throws UnableToReadException + */ + private function readTargetProperty( + object $target, + Context $context, + ): mixed { + if ( + $this->metadata->getTargetSetterWriteMode() === WriteMode::AdderRemover + && $this->metadata->getTargetSetterWriteVisibility() === Visibility::Public + ) { + if ( + $this->metadata->getTargetRemoverWriteVisibility() === Visibility::Public + ) { + $removerMethodName = $this->metadata->getTargetRemoverWriteName(); + } else { + $removerMethodName = null; + } + + return new AdderRemoverProxy( + hostObject: $target, + getterMethodName: $this->metadata->getTargetReadName(), + adderMethodName: $this->metadata->getTargetSetterWriteName(), + removerMethodName: $removerMethodName, + ); + } + + if ($this->metadata->getTargetReadVisibility() !== Visibility::Public) { + return null; + } + + try { + $accessorName = $this->metadata->getTargetReadName(); + $readMode = $this->metadata->getTargetReadMode(); + + if ($accessorName === null) { + throw new UnexpectedValueException('AccessorName is null', context: $context); + } + + if ($readMode === ReadMode::Property) { + return $target->{$accessorName}; + } elseif ($readMode === ReadMode::Method) { + /** @psalm-suppress MixedMethodCall */ + return $target->{$accessorName}(); + } elseif ($readMode === ReadMode::PropertyPath) { + return $this->propertyAccessor + ->getValue($target, $accessorName); + } elseif ($readMode === ReadMode::DynamicProperty) { + return $target->{$accessorName} ?? null; + } + + return null; + } catch (\Error $e) { + $message = $e->getMessage(); + + if ( + str_contains($message, 'must not be accessed before initialization') + || str_contains($message, 'Cannot access uninitialized non-nullable property') + ) { + return null; + } + + throw new UnableToReadException( + $target, + $this->metadata->getTargetProperty(), + context: $context, + previous: $e, + ); + } + } + + /** + * @throws UnableToWriteException + */ + public function writeTargetProperty( + object $target, + mixed $value, + Context $context, + bool $silentOnError, + ): object { + $accessorName = $this->metadata->getTargetSetterWriteName(); + $writeMode = $this->metadata->getTargetSetterWriteMode(); + $visibility = $this->metadata->getTargetSetterWriteVisibility(); + + if ( + $visibility !== Visibility::Public + || $writeMode === WriteMode::None + ) { + if ($silentOnError) { + return $target; + } + + throw new NewInstanceReturnedButCannotBeSetOnTargetException( + $target, + $this->metadata->getTargetProperty(), + context: $context, + ); + } + + if ($accessorName === null) { + throw new UnexpectedValueException('AccessorName is null', context: $context); + } + + try { + if ($writeMode === WriteMode::Property) { + $target->{$accessorName} = $value; + } elseif ($writeMode === WriteMode::Method) { + if ($this->metadata->isTargetSetterVariadic()) { + if (!\is_array($value) && !$value instanceof \Traversable) { + $value = [$value]; + } + + /** @psalm-suppress MixedArgument */ + $value = iterator_to_array($value); + + /** + * @psalm-suppress MixedMethodCall + * @var mixed + */ + $result = $target->{$accessorName}(...$value); + } else { + /** + * @psalm-suppress MixedMethodCall + * @var mixed + */ + $result = $target->{$accessorName}($value); + } + + // if the setter returns the a value with the same type as the + // target object, we assume that the setter method is a fluent + // interface or an immutable setter, and we return the result + + if ( + \is_object($result) && is_a($result, $target::class, true) + ) { + return $result; + } + } elseif ($writeMode === WriteMode::AdderRemover) { + // noop + } elseif ($writeMode === WriteMode::PropertyPath) { + // PropertyAccessor might modify the target object + $temporaryTarget = $target; + + $this->propertyAccessor + ->setValue($temporaryTarget, $accessorName, $value); + } elseif ($writeMode === WriteMode::DynamicProperty) { + $target->{$accessorName} = $value; + } + } catch (\BadMethodCallException) { + return $target; + } catch (\Throwable $e) { + throw new UnableToWriteException( + $target, + $this->metadata->getTargetProperty(), + context: $context, + previous: $e, + ); + } + + return $target; + } +} diff --git a/src/TransformerProcessor/PropertyProcessorFactoryInterface.php b/src/TransformerProcessor/PropertyProcessorFactoryInterface.php new file mode 100644 index 00000000..7e94eaa5 --- /dev/null +++ b/src/TransformerProcessor/PropertyProcessorFactoryInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\TransformerProcessor; + +use Rekalogika\Mapper\Transformer\MainTransformerAwareInterface; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; + +/** + * @internal + */ +interface PropertyProcessorFactoryInterface extends MainTransformerAwareInterface +{ + public function getPropertyProcessor( + PropertyMappingMetadata $metadata, + ): PropertyProcessorInterface; +} diff --git a/src/TransformerProcessor/PropertyProcessorInterface.php b/src/TransformerProcessor/PropertyProcessorInterface.php new file mode 100644 index 00000000..11af0569 --- /dev/null +++ b/src/TransformerProcessor/PropertyProcessorInterface.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\TransformerProcessor; + +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Transformer\Exception\UnableToWriteException; + +/** + * @internal + */ +interface PropertyProcessorInterface +{ + public function readSourcePropertyAndWriteTargetProperty( + object $source, + object $target, + Context $context, + ): object; + + /** + * @param object|null $target Target is null if the transformation is for a + * constructor argument + * @return array{mixed,bool} The target value after transformation and whether the value differs from before transformation + */ + public function transformValue( + object $source, + ?object $target, + bool $mandatory, + Context $context, + ): mixed; + + /** + * @throws UnableToWriteException + */ + public function writeTargetProperty( + object $target, + mixed $value, + Context $context, + bool $silentOnError, + ): object; +}