diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index da86674..85503f0 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; @@ -126,7 +126,7 @@ class: $targetClass, // instantiate property mapping - $propertyMapping = new PropertyMapping( + $propertyMapping = new PropertyMappingMetadata( id: $id, sourceProperty: $sourcePropertyMetadata->getReadMode() !== ReadMode::None ? $sourceProperty : null, targetProperty: $targetProperty, @@ -217,7 +217,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 84f4a25..ca5f6f1 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,7 +53,7 @@ * @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. */ @@ -219,7 +219,7 @@ public function isTargetUnalterable(): bool } /** - * @return array + * @return array */ public function getPropertyMappings(): array { @@ -227,7 +227,7 @@ public function getPropertyMappings(): array } /** - * @return array + * @return array */ public function getLazyPropertyMappings(): array { @@ -235,7 +235,7 @@ public function getLazyPropertyMappings(): array } /** - * @return array + * @return array */ public function getEagerPropertyMappings(): array { @@ -243,7 +243,7 @@ public function getEagerPropertyMappings(): array } /** - * @return array + * @return array */ public function getConstructorPropertyMappings(): array { @@ -251,7 +251,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 99% rename from src/Transformer/ObjectToObjectMetadata/PropertyMapping.php rename to src/Transformer/ObjectToObjectMetadata/PropertyMappingMetadata.php index 11849a6..f059bfc 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 diff --git a/src/Transformer/Util/ReaderWriter.php b/src/Transformer/Util/ReaderWriter.php new file mode 100644 index 0000000..3fc01ea --- /dev/null +++ b/src/Transformer/Util/ReaderWriter.php @@ -0,0 +1,286 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Transformer\Util; + +use Rekalogika\Mapper\Context\Context; +use Rekalogika\Mapper\Exception\UnexpectedValueException; +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\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 Symfony\Component\PropertyAccess\PropertyAccessorInterface; + +/** + * @internal + * @todo inject logger + */ +final readonly class ReaderWriter +{ + public function __construct( + private PropertyAccessorInterface $propertyAccessor, + ) {} + + /** + * @throws UninitializedSourcePropertyException + * @throws UnableToReadException + */ + public function readSourceProperty( + object $source, + PropertyMappingMetadata $propertyMapping, + Context $context, + ): mixed { + $property = $propertyMapping->getSourceProperty(); + + if ($property === null) { + return null; + } + + if ($propertyMapping->getSourceReadVisibility() !== Visibility::Public) { + throw new UnableToReadException( + $source, + $property, + context: $context, + ); + } + + try { + $accessorName = $propertyMapping->getSourceReadName(); + $mode = $propertyMapping->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 + */ + public function readTargetProperty( + object $target, + PropertyMappingMetadata $propertyMapping, + Context $context, + ): mixed { + if ( + $propertyMapping->getTargetSetterWriteMode() === WriteMode::AdderRemover + && $propertyMapping->getTargetSetterWriteVisibility() === Visibility::Public + ) { + if ( + $propertyMapping->getTargetRemoverWriteVisibility() === Visibility::Public + ) { + $removerMethodName = $propertyMapping->getTargetRemoverWriteName(); + } else { + $removerMethodName = null; + } + + return new AdderRemoverProxy( + hostObject: $target, + getterMethodName: $propertyMapping->getTargetReadName(), + adderMethodName: $propertyMapping->getTargetSetterWriteName(), + removerMethodName: $removerMethodName, + ); + } + + if ($propertyMapping->getTargetReadVisibility() !== Visibility::Public) { + return null; + } + + try { + $accessorName = $propertyMapping->getTargetReadName(); + $readMode = $propertyMapping->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, + $propertyMapping->getTargetProperty(), + context: $context, + previous: $e, + ); + } + } + + /** + * @throws UnableToWriteException + */ + public function writeTargetProperty( + object $target, + PropertyMappingMetadata $propertyMapping, + mixed $value, + Context $context, + bool $silentOnError, + ): object { + $accessorName = $propertyMapping->getTargetSetterWriteName(); + $writeMode = $propertyMapping->getTargetSetterWriteMode(); + $visibility = $propertyMapping->getTargetSetterWriteVisibility(); + + if ( + $visibility !== Visibility::Public + || $writeMode === WriteMode::None + ) { + if ($silentOnError) { + return $target; + } + + throw new NewInstanceReturnedButCannotBeSetOnTargetException( + $target, + $propertyMapping->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 ($propertyMapping->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, + $propertyMapping->getTargetProperty(), + context: $context, + previous: $e, + ); + } + + return $target; + } +} diff --git a/src/TransformerProcessor/ObjectProcessor/ObjectProcessor.php b/src/TransformerProcessor/ObjectProcessor/ObjectProcessor.php index 0d785d5..078b231 100644 --- a/src/TransformerProcessor/ObjectProcessor/ObjectProcessor.php +++ b/src/TransformerProcessor/ObjectProcessor/ObjectProcessor.php @@ -28,7 +28,7 @@ use Rekalogika\Mapper\Transformer\Exception\UnsupportedPropertyMappingException; use Rekalogika\Mapper\Transformer\Model\ConstructorArguments; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\ObjectToObjectMetadata; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMapping; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; use Rekalogika\Mapper\TransformerProcessor\ObjectProcessorInterface; use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorFactoryInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -134,7 +134,7 @@ public function transform( // /** - * @return array + * @return array */ private function getPropertyMappings(): array { @@ -142,7 +142,7 @@ private function getPropertyMappings(): array } /** - * @return array + * @return array */ private function getLazyPropertyMappings(): array { @@ -150,7 +150,7 @@ private function getLazyPropertyMappings(): array } /** - * @return array + * @return array */ private function getEagerPropertyMappings(): array { @@ -418,7 +418,7 @@ private function generateConstructorArguments( // /** - * @param array $propertyMappings + * @param array $propertyMappings * @param array $extraTargetValues */ private function readSourceAndWriteTarget( diff --git a/src/TransformerProcessor/PropertyProcessor/CachingPropertyProcessorFactory.php b/src/TransformerProcessor/PropertyProcessor/CachingPropertyProcessorFactory.php index 8d6247d..d53741c 100644 --- a/src/TransformerProcessor/PropertyProcessor/CachingPropertyProcessorFactory.php +++ b/src/TransformerProcessor/PropertyProcessor/CachingPropertyProcessorFactory.php @@ -14,7 +14,7 @@ namespace Rekalogika\Mapper\TransformerProcessor\PropertyProcessor; use Rekalogika\Mapper\Transformer\MainTransformerAwareTrait; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMapping; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorFactoryInterface; use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorInterface; @@ -43,7 +43,7 @@ private function getDecorated(): PropertyProcessorFactoryInterface } public function getPropertyProcessor( - PropertyMapping $metadata, + PropertyMappingMetadata $metadata, ): PropertyProcessorInterface { $id = $metadata->getId(); diff --git a/src/TransformerProcessor/PropertyProcessor/DefaultPropertyProcessorFactory.php b/src/TransformerProcessor/PropertyProcessor/DefaultPropertyProcessorFactory.php index b09ebcc..5e857c5 100644 --- a/src/TransformerProcessor/PropertyProcessor/DefaultPropertyProcessorFactory.php +++ b/src/TransformerProcessor/PropertyProcessor/DefaultPropertyProcessorFactory.php @@ -16,7 +16,7 @@ use Psr\Container\ContainerInterface; use Rekalogika\Mapper\SubMapper\SubMapperFactoryInterface; use Rekalogika\Mapper\Transformer\MainTransformerAwareTrait; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMapping; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorFactoryInterface; use Rekalogika\Mapper\TransformerProcessor\PropertyProcessorInterface; use Symfony\Component\PropertyAccess\PropertyAccessorInterface; @@ -36,7 +36,7 @@ public function __construct( ) {} public function getPropertyProcessor( - PropertyMapping $metadata, + PropertyMappingMetadata $metadata, ): PropertyProcessorInterface { return new PropertyProcessor( metadata: $metadata, diff --git a/src/TransformerProcessor/PropertyProcessor/PropertyProcessor.php b/src/TransformerProcessor/PropertyProcessor/PropertyProcessor.php index d781cf7..1566312 100644 --- a/src/TransformerProcessor/PropertyProcessor/PropertyProcessor.php +++ b/src/TransformerProcessor/PropertyProcessor/PropertyProcessor.php @@ -26,7 +26,7 @@ use Rekalogika\Mapper\Transformer\Exception\UninitializedSourcePropertyException; use Rekalogika\Mapper\Transformer\Exception\UnsupportedPropertyMappingException; 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 @@ final readonly class PropertyProcessor implements PropertyProcessorInterface { public function __construct( - private PropertyMapping $metadata, + private PropertyMappingMetadata $metadata, private PropertyAccessorInterface $propertyAccessor, private MainTransformerInterface $mainTransformer, private SubMapperFactoryInterface $subMapperFactory, diff --git a/src/TransformerProcessor/PropertyProcessorFactoryInterface.php b/src/TransformerProcessor/PropertyProcessorFactoryInterface.php index 676d9f4..7e94eaa 100644 --- a/src/TransformerProcessor/PropertyProcessorFactoryInterface.php +++ b/src/TransformerProcessor/PropertyProcessorFactoryInterface.php @@ -14,7 +14,7 @@ namespace Rekalogika\Mapper\TransformerProcessor; use Rekalogika\Mapper\Transformer\MainTransformerAwareInterface; -use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMapping; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\PropertyMappingMetadata; /** * @internal @@ -22,6 +22,6 @@ interface PropertyProcessorFactoryInterface extends MainTransformerAwareInterface { public function getPropertyProcessor( - PropertyMapping $metadata, + PropertyMappingMetadata $metadata, ): PropertyProcessorInterface; }