From 9c9de1e8092a0275b617bf93f8171db4410e6427 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Wed, 23 Oct 2024 21:40:59 +0700 Subject: [PATCH] refactor: remove property processor for easier future optimization --- CHANGELOG.md | 1 + config/services.php | 26 - src/MapperFactory.php | 17 - .../DefaultObjectProcessorFactory.php | 4 - .../ObjectProcessor/ObjectProcessor.php | 571 +++++++++++++++++- .../CachingPropertyProcessorFactory.php | 53 -- .../DefaultPropertyProcessorFactory.php | 49 -- .../PropertyProcessor/PropertyProcessor.php | 550 ----------------- .../PropertyProcessorFactoryInterface.php | 27 - .../PropertyProcessorInterface.php | 51 -- 10 files changed, 547 insertions(+), 802 deletions(-) delete mode 100644 src/TransformerProcessor/PropertyProcessor/CachingPropertyProcessorFactory.php delete mode 100644 src/TransformerProcessor/PropertyProcessor/DefaultPropertyProcessorFactory.php delete mode 100644 src/TransformerProcessor/PropertyProcessor/PropertyProcessor.php delete mode 100644 src/TransformerProcessor/PropertyProcessorFactoryInterface.php delete mode 100644 src/TransformerProcessor/PropertyProcessorInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c4366f..bb83d6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 1.13.2 * feat: `Map` now accepts false, for the more expressive `#[Map(false)]` +* refactor: remove property processor for easier future optimization ## 1.13.1 diff --git a/config/services.php b/config/services.php index 1769998..8bf37de 100644 --- a/config/services.php +++ b/config/services.php @@ -66,8 +66,6 @@ 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; @@ -391,30 +389,6 @@ '$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 diff --git a/src/MapperFactory.php b/src/MapperFactory.php index 02cf425..d8e894b 100644 --- a/src/MapperFactory.php +++ b/src/MapperFactory.php @@ -69,9 +69,6 @@ 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; @@ -204,8 +201,6 @@ class MapperFactory private ?ObjectProcessorFactoryInterface $objectProcessorFactory = null; - private ?PropertyProcessorFactoryInterface $propertyProcessorFactory = null; - private ?MappingCommand $mappingCommand = null; private ?TryCommand $tryCommand = null; @@ -912,18 +907,6 @@ protected function getObjectProcessorFactory(): ObjectProcessorFactoryInterface 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(), - ), ); } diff --git a/src/TransformerProcessor/ObjectProcessor/DefaultObjectProcessorFactory.php b/src/TransformerProcessor/ObjectProcessor/DefaultObjectProcessorFactory.php index fe96d09..427e6aa 100644 --- a/src/TransformerProcessor/ObjectProcessor/DefaultObjectProcessorFactory.php +++ b/src/TransformerProcessor/ObjectProcessor/DefaultObjectProcessorFactory.php @@ -20,7 +20,6 @@ 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; /** @@ -35,7 +34,6 @@ public function __construct( private readonly SubMapperFactoryInterface $subMapperFactory, private readonly ProxyFactoryInterface $proxyFactory, private readonly PropertyAccessorInterface $propertyAccessor, - private readonly PropertyProcessorFactoryInterface $propertyProcessorFactory, ) {} public function getObjectProcessor( @@ -48,8 +46,6 @@ public function getObjectProcessor( 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 index fe3196c..c04b63c 100644 --- a/src/TransformerProcessor/ObjectProcessor/ObjectProcessor.php +++ b/src/TransformerProcessor/ObjectProcessor/ObjectProcessor.php @@ -17,20 +17,30 @@ use Rekalogika\Mapper\Context\Context; use Rekalogika\Mapper\Context\ExtraTargetValues; use Rekalogika\Mapper\Context\MapperOptions; +use Rekalogika\Mapper\Exception\UnexpectedValueException; use Rekalogika\Mapper\MainTransformer\MainTransformerInterface; use Rekalogika\Mapper\ObjectCache\ObjectCache; use Rekalogika\Mapper\Proxy\ProxyFactoryInterface; +use Rekalogika\Mapper\ServiceMethod\ServiceMethodRunner; 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\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\Model\ConstructorArguments; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; 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\ObjectProcessorInterface; -use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorFactoryInterface; +use Rekalogika\Mapper\Util\TypeFactory; +use Rekalogika\Mapper\Util\TypeGuesser; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; use Symfony\Component\PropertyInfo\Type; @@ -46,7 +56,6 @@ public function __construct( private SubMapperFactoryInterface $subMapperFactory, private ProxyFactoryInterface $proxyFactory, private PropertyAccessorInterface $propertyAccessor, - private PropertyProcessorFactoryInterface $propertyProcessorFactory, ) {} public function transform( @@ -375,14 +384,13 @@ private function generateConstructorArguments( foreach ($constructorPropertyMappings as $propertyMapping) { try { /** @var mixed $targetPropertyValue */ - [$targetPropertyValue,] = $this->propertyProcessorFactory - ->getPropertyProcessor($propertyMapping) - ->transformValue( - source: $source, - target: null, - mandatory: $propertyMapping->isTargetConstructorMandatory(), - context: $context, - ); + [$targetPropertyValue,] = $this->transformValue( + metadata: $propertyMapping, + source: $source, + target: null, + mandatory: $propertyMapping->isTargetConstructorMandatory(), + context: $context, + ); if ($propertyMapping->isTargetConstructorVariadic()) { if ( @@ -451,13 +459,12 @@ private function readSourceAndWriteTarget( continue; } - $target = $this->propertyProcessorFactory - ->getPropertyProcessor($propertyMapping) - ->readSourcePropertyAndWriteTargetProperty( - source: $source, - target: $target, - context: $context, - ); + $target = $this->readSourcePropertyAndWriteTargetProperty( + metadata: $propertyMapping, + source: $source, + target: $target, + context: $context, + ); } // process extra target values @@ -474,14 +481,13 @@ private function readSourceAndWriteTarget( $propertyMapping = $propertyMappings[$property]; - $target = $this->propertyProcessorFactory - ->getPropertyProcessor($propertyMapping) - ->writeTargetProperty( - target: $target, - value: $value, - context: $context, - silentOnError: true, - ); + $target = $this->writeTargetProperty( + metadata: $propertyMapping, + target: $target, + value: $value, + context: $context, + silentOnError: true, + ); } return $target; @@ -541,4 +547,519 @@ private function mapDynamicProperties( $target->{$sourceProperty} = $targetPropertyValue; } } + + // + // property processor + // + + /** + * @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( + PropertyMappingMetadata $metadata, + object $source, + object $target, + Context $context, + ): object { + if ( + $metadata->getTargetReadMode() === ReadMode::None + && $metadata->getTargetSetterWriteMode() === WriteMode::None + ) { + return $target; + } + + try { + /** @var mixed $targetPropertyValue */ + [$targetPropertyValue, $isChanged] = $this->transformValue( + metadata: $metadata, + source: $source, + target: $target, + mandatory: false, + context: $context, + ); + } catch (UninitializedSourcePropertyException | UnsupportedPropertyMappingException) { + return $target; + } + + // write + + if ( + $isChanged + || $metadata->getTargetSetterWriteMode() === WriteMode::DynamicProperty + ) { + if ($targetPropertyValue instanceof AdderRemoverProxy) { + $target = $targetPropertyValue->getHostObject(); + } + + return $this->writeTargetProperty( + metadata: $metadata, + 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( + PropertyMappingMetadata $metadata, + object $source, + ?object $target, + bool $mandatory, + Context $context, + ): mixed { + // if a custom property mapper is set, then use it + + if ($metadata->hasPropertyMapper()) { + /** @psalm-suppress MixedReturnStatement */ + return $this->transformValueUsingPropertyMapper( + metadata: $metadata, + source: $source, + target: $target, + context: $context, + ); + } + + // if source property name is null, continue. there is nothing to + // transform + + $sourceProperty = $metadata->getSourceProperty(); + + if ($sourceProperty === null) { + throw new UnsupportedPropertyMappingException(); + } + + // get the value of the source property + + try { + /** @var mixed */ + $sourcePropertyValue = $this->readSourceProperty( + metadata: $metadata, + 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 ($metadata->targetCanAcceptNull() && $sourcePropertyValue === null) { + return [null, true]; + } + + // if the the source is null or scalar, and the target is a scalar + + $targetScalarType = $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( + metadata: $metadata, + 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 = $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 = $metadata->getCompatibleSourceType($guessedSourceType) + ?? $guessedSourceType; + + // add attributes to context + + $sourceAttributes = $metadata->getSourceAttributes(); + $context = $context->with($sourceAttributes); + + $targetAttributes = $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: $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( + PropertyMappingMetadata $metadata, + object $source, + ?object $target, + Context $context, + ): array { + $serviceMethodSpecification = $metadata->getPropertyMapper(); + + if ($serviceMethodSpecification === null) { + throw new UnexpectedValueException('PropertyMapper is null', context: $context); + } + + if ($target === null) { + $targetPropertyValue = null; + } else { + /** @var mixed */ + $targetPropertyValue = $this->readTargetProperty( + metadata: $metadata, + target: $target, + context: $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( + PropertyMappingMetadata $metadata, + object $source, + Context $context, + ): mixed { + $property = $metadata->getSourceProperty(); + + if ($property === null) { + return null; + } + + if ($metadata->getSourceReadVisibility() !== Visibility::Public) { + throw new UnableToReadException( + source: $source, + property: $property, + context: $context, + ); + } + + try { + $accessorName = $metadata->getSourceReadName(); + $mode = $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: $source, + property: $property, + context: $context, + previous: $e, + ); + } catch (\BadMethodCallException) { + throw new UninitializedSourcePropertyException($property); + } + } + + /** + * @throws UnableToReadException + */ + private function readTargetProperty( + PropertyMappingMetadata $metadata, + object $target, + Context $context, + ): mixed { + if ( + $metadata->getTargetSetterWriteMode() === WriteMode::AdderRemover + && $metadata->getTargetSetterWriteVisibility() === Visibility::Public + ) { + if ( + $metadata->getTargetRemoverWriteVisibility() === Visibility::Public + ) { + $removerMethodName = $metadata->getTargetRemoverWriteName(); + } else { + $removerMethodName = null; + } + + return new AdderRemoverProxy( + hostObject: $target, + getterMethodName: $metadata->getTargetReadName(), + adderMethodName: $metadata->getTargetSetterWriteName(), + removerMethodName: $removerMethodName, + ); + } + + if ($metadata->getTargetReadVisibility() !== Visibility::Public) { + return null; + } + + try { + $accessorName = $metadata->getTargetReadName(); + $readMode = $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( + source: $target, + property: $metadata->getTargetProperty(), + context: $context, + previous: $e, + ); + } + } + + /** + * @throws UnableToWriteException + */ + public function writeTargetProperty( + PropertyMappingMetadata $metadata, + object $target, + mixed $value, + Context $context, + bool $silentOnError, + ): object { + $accessorName = $metadata->getTargetSetterWriteName(); + $writeMode = $metadata->getTargetSetterWriteMode(); + $visibility = $metadata->getTargetSetterWriteVisibility(); + + if ( + $visibility !== Visibility::Public + || $writeMode === WriteMode::None + ) { + if ($silentOnError) { + return $target; + } + + throw new NewInstanceReturnedButCannotBeSetOnTargetException( + target: $target, + propertyName: $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 ($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: $target, + propertyName: $metadata->getTargetProperty(), + context: $context, + previous: $e, + ); + } + + return $target; + } } diff --git a/src/TransformerProcessor/PropertyProcessor/CachingPropertyProcessorFactory.php b/src/TransformerProcessor/PropertyProcessor/CachingPropertyProcessorFactory.php deleted file mode 100644 index d53741c..0000000 --- a/src/TransformerProcessor/PropertyProcessor/CachingPropertyProcessorFactory.php +++ /dev/null @@ -1,53 +0,0 @@ - - * - * 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 deleted file mode 100644 index 5e857c5..0000000 --- a/src/TransformerProcessor/PropertyProcessor/DefaultPropertyProcessorFactory.php +++ /dev/null @@ -1,49 +0,0 @@ - - * - * 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 deleted file mode 100644 index 1566312..0000000 --- a/src/TransformerProcessor/PropertyProcessor/PropertyProcessor.php +++ /dev/null @@ -1,550 +0,0 @@ - - * - * 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 deleted file mode 100644 index 7e94eaa..0000000 --- a/src/TransformerProcessor/PropertyProcessorFactoryInterface.php +++ /dev/null @@ -1,27 +0,0 @@ - - * - * 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 deleted file mode 100644 index 11af056..0000000 --- a/src/TransformerProcessor/PropertyProcessorInterface.php +++ /dev/null @@ -1,51 +0,0 @@ - - * - * 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; -}