diff --git a/src/Transformer/Implementation/ObjectToObjectTransformer.php b/src/Transformer/Implementation/ObjectToObjectTransformer.php index 8bd70c5..7689db8 100644 --- a/src/Transformer/Implementation/ObjectToObjectTransformer.php +++ b/src/Transformer/Implementation/ObjectToObjectTransformer.php @@ -388,19 +388,14 @@ private function readSourcePropertyAndWriteTargetProperty( PropertyMapping $propertyMapping, Context $context ): void { - $targetWriteMode = $propertyMapping->getTargetWriteMode(); - $targetWriteVisibility = $propertyMapping->getTargetWriteVisibility(); + $targetSetterWriteMode = $propertyMapping->getTargetSetterWriteMode(); + $targetSetterWriteVisibility = $propertyMapping->getTargetSetterWriteVisibility(); - if ( - in_array($targetWriteMode, [ - WriteMode::None, - WriteMode::Constructor, - ], true) - ) { + if ($targetSetterWriteMode === WriteMode::None) { return; } - if ($targetWriteVisibility !== Visibility::Public) { + if ($targetSetterWriteVisibility !== Visibility::Public) { return; } diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index 8b0f3d1..207e2fc 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php @@ -153,8 +153,10 @@ public function createObjectToObjectMetadata( ->getReadInfo($sourceClass, $sourceProperty); $targetReadInfo = $this->propertyReadInfoExtractor ->getReadInfo($targetClass, $targetProperty); - $targetWriteInfo = $this->propertyWriteInfoExtractor - ->getWriteInfo($targetClass, $targetProperty); + $targetConstructorWriteInfo = $this + ->getConstructorWriteInfo($targetClass, $targetProperty); + $targetSetterWriteInfo = $this + ->getSetterWriteInfo($targetClass, $targetProperty); // process source read mode @@ -216,45 +218,59 @@ public function createObjectToObjectMetadata( }; } - // process target write mode + // skip if target is not writable + + if ($targetConstructorWriteInfo === null && $targetSetterWriteInfo === null) { + continue; + } + + // process target constructor write info if ( - $targetWriteInfo === null + $targetConstructorWriteInfo === null + || $targetConstructorWriteInfo->getType() !== PropertyWriteInfo::TYPE_CONSTRUCTOR ) { - continue; - } elseif ($targetWriteInfo->getType() === PropertyWriteInfo::TYPE_ADDER_AND_REMOVER) { - $targetWriteMode = WriteMode::AdderRemover; - $targetWriteName = $targetWriteInfo->getAdderInfo()->getName(); - $targetWriteVisibility = match ($targetWriteInfo->getAdderInfo()->getVisibility()) { + $targetConstructorWriteMode = WriteMode::None; + $targetConstructorWriteName = null; + } else { + $targetConstructorWriteMode = WriteMode::Constructor; + $targetConstructorWriteName = $targetConstructorWriteInfo->getName(); + } + + // process target setter write mode + + if ($targetSetterWriteInfo === null) { + $targetSetterWriteMode = WriteMode::None; + $targetSetterWriteName = null; + $targetSetterWriteVisibility = Visibility::None; + } elseif ($targetSetterWriteInfo->getType() === PropertyWriteInfo::TYPE_ADDER_AND_REMOVER) { + $targetSetterWriteMode = WriteMode::AdderRemover; + $targetSetterWriteName = $targetSetterWriteInfo->getAdderInfo()->getName(); + $targetSetterWriteVisibility = match ($targetSetterWriteInfo->getAdderInfo()->getVisibility()) { PropertyWriteInfo::VISIBILITY_PUBLIC => Visibility::Public, PropertyWriteInfo::VISIBILITY_PROTECTED => Visibility::Protected, PropertyWriteInfo::VISIBILITY_PRIVATE => Visibility::Private, default => Visibility::None, }; - } elseif ($targetWriteInfo->getType() === PropertyWriteInfo::TYPE_CONSTRUCTOR) { - $targetWriteMode = WriteMode::Constructor; - $targetWriteName = $targetWriteInfo->getName(); - $targetWriteVisibility = Visibility::None; } else { - $targetWriteMode = match ($targetWriteInfo->getType()) { + $targetSetterWriteMode = match ($targetSetterWriteInfo->getType()) { PropertyWriteInfo::TYPE_METHOD => WriteMode::Method, PropertyWriteInfo::TYPE_PROPERTY => WriteMode::Property, default => WriteMode::None, }; - if ($targetWriteMode === WriteMode::None) { + if ($targetSetterWriteMode === WriteMode::None) { if ($targetAllowsDynamicProperties && $targetReadInfo === null) { - $targetWriteMode = WriteMode::DynamicProperty; - $targetWriteName = $targetProperty; - $targetWriteVisibility = Visibility::Public; + $targetSetterWriteMode = WriteMode::DynamicProperty; + $targetSetterWriteName = $targetProperty; + $targetSetterWriteVisibility = Visibility::Public; } else { - $effectivePropertiesToMap[] = $targetProperty; - - continue; + $targetSetterWriteName = null; + $targetSetterWriteVisibility = Visibility::None; } } else { - $targetWriteName = $targetWriteInfo->getName(); - $targetWriteVisibility = match ($targetWriteInfo->getVisibility()) { + $targetSetterWriteName = $targetSetterWriteInfo->getName(); + $targetSetterWriteVisibility = match ($targetSetterWriteInfo->getVisibility()) { PropertyWriteInfo::VISIBILITY_PUBLIC => Visibility::Public, PropertyWriteInfo::VISIBILITY_PROTECTED => Visibility::Protected, PropertyWriteInfo::VISIBILITY_PRIVATE => Visibility::Private, @@ -343,9 +359,11 @@ public function createObjectToObjectMetadata( targetReadMode: $targetReadMode, targetReadName: $targetReadName, targetReadVisibility: $targetReadVisibility, - targetWriteMode: $targetWriteMode, - targetWriteName: $targetWriteName, - targetWriteVisibility: $targetWriteVisibility, + targetSetterWriteMode: $targetSetterWriteMode, + targetSetterWriteName: $targetSetterWriteName, + targetSetterWriteVisibility: $targetSetterWriteVisibility, + targetConstructorWriteMode: $targetConstructorWriteMode, + targetConstructorWriteName: $targetConstructorWriteName, targetScalarType: $targetPropertyScalarType, propertyMapper: $serviceMethodSpecification, sourceLazy: $sourceLazy, @@ -469,4 +487,36 @@ private function allowsDynamicProperties(\ReflectionClass $class): bool return false; } + + private function getConstructorWriteInfo( + string $class, + string $property, + ): ?PropertyWriteInfo { + $writeInfo = $this->propertyWriteInfoExtractor + ->getWriteInfo($class, $property, [ + 'enable_getter_setter_extraction' => false, + 'enable_magic_methods_extraction' => false, + 'enable_adder_remover_extraction' => false, + ]); + + if ($writeInfo === null) { + return null; + } + + if ($writeInfo->getType() === PropertyWriteInfo::TYPE_CONSTRUCTOR) { + return $writeInfo; + } + + return null; + } + + private function getSetterWriteInfo( + string $class, + string $property, + ): ?PropertyWriteInfo { + return $this->propertyWriteInfoExtractor + ->getWriteInfo($class, $property, [ + 'enable_constructor_extraction' => false, + ]); + } } diff --git a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php index 90020bb..a1018fb 100644 --- a/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php +++ b/src/Transformer/ObjectToObjectMetadata/ObjectToObjectMetadata.php @@ -79,9 +79,11 @@ public function __construct( $propertyPropertyMappings = []; foreach ($allPropertyMappings as $propertyMapping) { - if ($propertyMapping->getTargetWriteMode() === WriteMode::Constructor) { + if ($propertyMapping->getTargetConstructorWriteMode() === WriteMode::Constructor) { $constructorPropertyMappings[] = $propertyMapping; - } else { + } + + if ($propertyMapping->getTargetSetterWriteMode() !== WriteMode::None) { $propertyPropertyMappings[] = $propertyMapping; if ($propertyMapping->isSourceLazy()) { diff --git a/src/Transformer/ObjectToObjectMetadata/PropertyMapping.php b/src/Transformer/ObjectToObjectMetadata/PropertyMapping.php index b868577..2e498e3 100644 --- a/src/Transformer/ObjectToObjectMetadata/PropertyMapping.php +++ b/src/Transformer/ObjectToObjectMetadata/PropertyMapping.php @@ -49,9 +49,11 @@ public function __construct( private ReadMode $targetReadMode, private ?string $targetReadName, private Visibility $targetReadVisibility, - private WriteMode $targetWriteMode, - private ?string $targetWriteName, - private Visibility $targetWriteVisibility, + private WriteMode $targetSetterWriteMode, + private ?string $targetSetterWriteName, + private Visibility $targetSetterWriteVisibility, + private WriteMode $targetConstructorWriteMode, + private ?string $targetConstructorWriteName, private ?string $targetScalarType, private ?ServiceMethodSpecification $propertyMapper, private bool $sourceLazy, @@ -134,14 +136,29 @@ public function getTargetReadName(): ?string return $this->targetReadName; } - public function getTargetWriteMode(): WriteMode + public function getTargetSetterWriteMode(): WriteMode { - return $this->targetWriteMode; + return $this->targetSetterWriteMode; } - public function getTargetWriteName(): ?string + public function getTargetSetterWriteName(): ?string { - return $this->targetWriteName; + return $this->targetSetterWriteName; + } + + public function getTargetSetterWriteVisibility(): Visibility + { + return $this->targetSetterWriteVisibility; + } + + public function getTargetConstructorWriteMode(): WriteMode + { + return $this->targetConstructorWriteMode; + } + + public function getTargetConstructorWriteName(): ?string + { + return $this->targetConstructorWriteName; } public function getSourceReadVisibility(): Visibility @@ -154,11 +171,6 @@ public function getTargetReadVisibility(): Visibility return $this->targetReadVisibility; } - public function getTargetWriteVisibility(): Visibility - { - return $this->targetWriteVisibility; - } - public function isSourceLazy(): bool { return $this->sourceLazy; diff --git a/src/Transformer/Util/ReaderWriter.php b/src/Transformer/Util/ReaderWriter.php index 5e1422f..7b03470 100644 --- a/src/Transformer/Util/ReaderWriter.php +++ b/src/Transformer/Util/ReaderWriter.php @@ -97,12 +97,12 @@ public function readTargetProperty( Context $context ): mixed { if ( - $propertyMapping->getTargetWriteMode() === WriteMode::AdderRemover - && $propertyMapping->getTargetWriteVisibility() === Visibility::Public + $propertyMapping->getTargetSetterWriteMode() === WriteMode::AdderRemover + && $propertyMapping->getTargetSetterWriteVisibility() === Visibility::Public ) { return new AdderRemoverProxy( $target, - $propertyMapping->getTargetWriteName(), + $propertyMapping->getTargetSetterWriteName(), null ); } @@ -157,13 +157,13 @@ public function writeTargetProperty( mixed $value, Context $context ): void { - if ($propertyMapping->getTargetWriteVisibility() !== Visibility::Public) { + if ($propertyMapping->getTargetSetterWriteVisibility() !== Visibility::Public) { return; } try { - $accessorName = $propertyMapping->getTargetWriteName(); - $writeMode = $propertyMapping->getTargetWriteMode(); + $accessorName = $propertyMapping->getTargetSetterWriteName(); + $writeMode = $propertyMapping->getTargetSetterWriteMode(); if ($writeMode === WriteMode::Property) { $target->{$accessorName} = $value; diff --git a/templates/data_collector.html.twig b/templates/data_collector.html.twig index 558355b..79f326b 100644 --- a/templates/data_collector.html.twig +++ b/templates/data_collector.html.twig @@ -321,7 +321,7 @@ {% for mapping in metadata.constructorPropertyMappings %} - {{ _self.render_o2o_property_mapping(mapping, helper) }} + {{ _self.render_o2o_constructor_mapping(mapping, helper) }} {% endfor %} {% endif %} @@ -357,6 +357,38 @@ {% endmacro %} +{% macro render_o2o_constructor_mapping(mapping, helper) %} + + + {{ mapping.targetProperty }} + + + {{ helper.typeToHtml(mapping.sourceTypes)|raw }} + + + {{ helper.typeToHtml(mapping.targetTypes)|raw }} + + + {{ _self.render_o2o_property_mapping_read_mode(mapping.sourceReadMode, mapping.sourceReadName, mapping.sourceReadVisibility) }} + + + N/A + + + {{ _self.render_o2o_property_mapping_write_mode(mapping.targetConstructorWriteMode, mapping.targetConstructorWriteName, null) }} + + + {% if mapping.targetScalarType %} + {{ mapping.targetScalarType }} + {% endif %} + + {% if mapping.targetCanAcceptNull %} + nullable + {% endif %} + + +{% endmacro %} + {% macro render_o2o_property_mapping(mapping, helper) %} @@ -375,7 +407,7 @@ {{ _self.render_o2o_property_mapping_read_mode(mapping.targetReadMode, mapping.targetReadName, mapping.targetReadVisibility) }} - {{ _self.render_o2o_property_mapping_write_mode(mapping.targetWriteMode, mapping.targetWriteName, mapping.targetWriteVisibility) }} + {{ _self.render_o2o_property_mapping_write_mode(mapping.targetSetterWriteMode, mapping.targetSetterWriteName, mapping.targetSetterWriteVisibility) }} {% if mapping.targetScalarType %} @@ -404,13 +436,13 @@ {% macro render_o2o_property_mapping_write_mode(mode, name, visibility) %} {%- if mode == constant('Rekalogika\\Mapper\\Transformer\\ObjectToObjectMetadata\\WriteMode::Method') -%} - ->{{ name }}() + ->{{ name }}(...) {%- elseif mode == constant('Rekalogika\\Mapper\\Transformer\\ObjectToObjectMetadata\\WriteMode::Property') -%} ->{{ name }} {%- elseif mode == constant('Rekalogika\\Mapper\\Transformer\\ObjectToObjectMetadata\\WriteMode::DynamicProperty') -%} ->{{ name }} {%- elseif mode == constant('Rekalogika\\Mapper\\Transformer\\ObjectToObjectMetadata\\WriteMode::AdderRemover') -%} - ->{{ name }}() + ->{{ name }}(...) {%- elseif mode == constant('Rekalogika\\Mapper\\Transformer\\ObjectToObjectMetadata\\WriteMode::Constructor') -%} ->__construct(...) {%- else -%} diff --git a/tests/Fixtures/PropertyInSetterAndConstructor/ChildObject.php b/tests/Fixtures/PropertyInSetterAndConstructor/ChildObject.php new file mode 100644 index 0000000..3942ddc --- /dev/null +++ b/tests/Fixtures/PropertyInSetterAndConstructor/ChildObject.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\PropertyInSetterAndConstructor; + +class ChildObject +{ + public function __construct( + private string $a, + ) { + } + + public function getA(): string + { + return $this->a; + } + + public function setA(string $a): void + { + $this->a = $a; + } +} diff --git a/tests/Fixtures/PropertyInSetterAndConstructor/ChildObjectDto.php b/tests/Fixtures/PropertyInSetterAndConstructor/ChildObjectDto.php new file mode 100644 index 0000000..e4da05d --- /dev/null +++ b/tests/Fixtures/PropertyInSetterAndConstructor/ChildObjectDto.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\PropertyInSetterAndConstructor; + +/** @psalm-suppress MissingConstructor */ +class ChildObjectDto +{ + public string $a; +} diff --git a/tests/Fixtures/PropertyInSetterAndConstructor/ParentObject.php b/tests/Fixtures/PropertyInSetterAndConstructor/ParentObject.php new file mode 100644 index 0000000..51c7e2d --- /dev/null +++ b/tests/Fixtures/PropertyInSetterAndConstructor/ParentObject.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\PropertyInSetterAndConstructor; + +class ParentObject +{ + public function __construct( + private string $name, + private ChildObject $child, + ) { + } + + public function getChild(): ChildObject + { + return $this->child; + } + + public function setChild(ChildObject $child): void + { + $this->child = $child; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): void + { + $this->name = $name; + } +} diff --git a/tests/Fixtures/PropertyInSetterAndConstructor/ParentObjectDto.php b/tests/Fixtures/PropertyInSetterAndConstructor/ParentObjectDto.php new file mode 100644 index 0000000..44c9fc1 --- /dev/null +++ b/tests/Fixtures/PropertyInSetterAndConstructor/ParentObjectDto.php @@ -0,0 +1,21 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\Fixtures\PropertyInSetterAndConstructor; + +/** @psalm-suppress MissingConstructor */ +class ParentObjectDto +{ + public string $name; + public ChildObjectDto $child; +} diff --git a/tests/IntegrationTest/PropertyInSetterAndConstructorTest.php b/tests/IntegrationTest/PropertyInSetterAndConstructorTest.php new file mode 100644 index 0000000..def2cda --- /dev/null +++ b/tests/IntegrationTest/PropertyInSetterAndConstructorTest.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Tests\IntegrationTest; + +use Rekalogika\Mapper\Tests\Common\FrameworkTestCase; +use Rekalogika\Mapper\Tests\Fixtures\PropertyInSetterAndConstructor\ChildObject; +use Rekalogika\Mapper\Tests\Fixtures\PropertyInSetterAndConstructor\ChildObjectDto; +use Rekalogika\Mapper\Tests\Fixtures\PropertyInSetterAndConstructor\ParentObject; +use Rekalogika\Mapper\Tests\Fixtures\PropertyInSetterAndConstructor\ParentObjectDto; + +class PropertyInSetterAndConstructorTest extends FrameworkTestCase +{ + public function testIssue57(): void + { + $dto = new ParentObjectDto(); + $dto->name = 'dto-name'; + $dto->child = new ChildObjectDto(); + $dto->child->a = 'dto-a'; + + $entity = $this->mapper->map($dto, ParentObject::class); + + $this->assertSame('dto-name', $entity->getName()); + $this->assertSame('dto-a', $entity->getChild()->getA()); + } + + public function testIssue57Preinitialized(): void + { + $dto = new ParentObjectDto(); + $dto->name = 'dto-name'; + $dto->child = new ChildObjectDto(); + $dto->child->a = 'dto-a'; + + $entity = new ParentObject('entity-name', new ChildObject('entity-a')); + $this->mapper->map($dto, $entity); + + $this->assertSame('dto-name', $entity->getName()); + $this->assertSame('dto-a', $entity->getChild()->getA()); + } +}