From ecfc40957c5ee46d5ac7b313645511a06075b453 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:04:56 +0700 Subject: [PATCH 1/5] refactor: separate dynamic properties determination to dedicated class --- CHANGELOG.md | 1 + .../Util/ClassMetadataFactory.php | 19 ++------- .../Util/DynamicPropertiesDeterminer.php | 39 +++++++++++++++++++ .../Util/PropertyMetadataFactory.php | 28 ++++++++----- .../Util/PropertyMetadataFactoryInterface.php | 1 - .../Util/PropertyPathMetadataFactory.php | 1 - src/Util/ClassUtil.php | 20 ++++++++++ 7 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 src/Transformer/ObjectToObjectMetadata/Implementation/Util/DynamicPropertiesDeterminer.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 73757db..9436410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ * test: test different setter return types * feat: support for immutable adder and remover * feat: optional second argument for getting the existing target value +* refactor: separate dynamic properties determination to dedicated class ## 1.8.0 diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/Util/ClassMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/Util/ClassMetadataFactory.php index 5f49b94..31ece90 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/Util/ClassMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/Util/ClassMetadataFactory.php @@ -30,6 +30,7 @@ public function __construct( private EagerPropertiesResolverInterface $eagerPropertiesResolver, private PropertyListExtractorInterface $propertyListExtractor, private PropertyWriteInfoExtractorInterface $propertyWriteInfoExtractor, + private DynamicPropertiesDeterminer $dynamicPropertiesDeterminer, ) {} /** @@ -41,11 +42,11 @@ public function createClassMetadata(string $class): ClassMetadata $reflection = new \ReflectionClass($class); $hasReadableDynamicProperties = - $this->allowsDynamicProperties($reflection) + $this->dynamicPropertiesDeterminer->allowsDynamicProperties($class) || method_exists($class, '__get'); $hasWritableDynamicProperties = - $this->allowsDynamicProperties($reflection) + $this->dynamicPropertiesDeterminer->allowsDynamicProperties($class) || method_exists($class, '__set'); $internal = $reflection->isInternal(); @@ -81,20 +82,6 @@ class: $class, ); } - /** - * @param \ReflectionClass $class - */ - private function allowsDynamicProperties(\ReflectionClass $class): bool - { - do { - if ($class->getAttributes(\AllowDynamicProperties::class) !== []) { - return true; - } - } while ($class = $class->getParentClass()); - - return false; - } - /** * @param class-string $class * @param list $attributes diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/Util/DynamicPropertiesDeterminer.php b/src/Transformer/ObjectToObjectMetadata/Implementation/Util/DynamicPropertiesDeterminer.php new file mode 100644 index 0000000..8fc6606 --- /dev/null +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/Util/DynamicPropertiesDeterminer.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +namespace Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\Util; + +use Rekalogika\Mapper\Util\ClassUtil; + +/** + * @internal + */ +final class DynamicPropertiesDeterminer +{ + /** + * @var array + */ + private array $cache = []; + + /** + * @param class-string $class + */ + public function allowsDynamicProperties(string $class): bool + { + if (isset($this->cache[$class])) { + return $this->cache[$class]; + } + + return $this->cache[$class] = ClassUtil::allowsDynamicProperties($class); + } +} diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyMetadataFactory.php index 84ac16e..1f87640 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyMetadataFactory.php @@ -40,6 +40,7 @@ public function __construct( private PropertyWriteInfoExtractorInterface $propertyWriteInfoExtractor, private PropertyTypeExtractorInterface $propertyTypeExtractor, private TypeResolverInterface $typeResolver, + private DynamicPropertiesDeterminer $dynamicPropertiesDeterminer, ) { $this->propertyPathMetadataFactory = new PropertyPathMetadataFactory( propertyTypeExtractor: $propertyTypeExtractor, @@ -55,15 +56,14 @@ public function __construct( public function createPropertyMetadata( string $class, string $property, - bool $allowsDynamicProperties, ): PropertyMetadata { + // property path if ($this->isPropertyPath($property)) { return $this->propertyPathMetadataFactory->createPropertyMetadata( class: $class, property: $property, - allowsDynamicProperties: $allowsDynamicProperties, ); } @@ -79,9 +79,9 @@ class: $class, ->getConstructorPropertyWriteInfo($class, $property); [$readMode, $readName, $readVisibility] = $this->processPropertyReadInfo( - readInfo: $readInfo, + class: $class, property: $property, - allowsDynamicProperties: $allowsDynamicProperties, + readInfo: $readInfo, ); [$constructorWriteMode, $constructorWriteName] = @@ -110,10 +110,10 @@ class: $class, $replaceable, ] = $this->processPropertyWriteInfo( + class: $class, + property: $property, readInfo: $readInfo, writeInfo: $writeInfo, - property: $property, - allowsDynamicProperties: $allowsDynamicProperties, ); [$types, $scalarType, $nullable] = @@ -158,13 +158,17 @@ class: $class, } /** + * @param class-string $class * @return array{ReadMode,?string,Visibility} */ private function processPropertyReadInfo( - ?PropertyReadInfo $readInfo, + string $class, string $property, - bool $allowsDynamicProperties, + ?PropertyReadInfo $readInfo, ): array { + $allowsDynamicProperties = $this->dynamicPropertiesDeterminer + ->allowsDynamicProperties($class); + if ($readInfo === null) { // if source allows dynamic properties, including stdClass if ($allowsDynamicProperties) { @@ -217,14 +221,18 @@ private function processConstructorWriteInfo( } /** + * @param class-string $class * @return array{WriteMode,?string,Visibility,?string,Visibility,bool} */ private function processPropertyWriteInfo( + string $class, + string $property, ?PropertyReadInfo $readInfo, ?PropertyWriteInfo $writeInfo, - string $property, - bool $allowsDynamicProperties, ): array { + $allowsDynamicProperties = $this->dynamicPropertiesDeterminer + ->allowsDynamicProperties($class); + $removerWriteName = null; $removerWriteVisibility = Visibility::None; diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyMetadataFactoryInterface.php b/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyMetadataFactoryInterface.php index caf38a6..f169704 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyMetadataFactoryInterface.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyMetadataFactoryInterface.php @@ -27,6 +27,5 @@ interface PropertyMetadataFactoryInterface public function createPropertyMetadata( string $class, string $property, - bool $allowsDynamicProperties, ): PropertyMetadata; } diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyPathMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyPathMetadataFactory.php index c1dea9d..eb55d48 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyPathMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/Util/PropertyPathMetadataFactory.php @@ -41,7 +41,6 @@ public function __construct( public function createPropertyMetadata( string $class, string $property, - bool $allowsDynamicProperties, ): PropertyMetadata { $propertyPathObject = new PropertyPath($property); diff --git a/src/Util/ClassUtil.php b/src/Util/ClassUtil.php index ca66825..80d782b 100644 --- a/src/Util/ClassUtil.php +++ b/src/Util/ClassUtil.php @@ -354,4 +354,24 @@ private static function getAttributesFromMethod( return $attributes; } + + /** + * @param class-string $class + */ + public static function allowsDynamicProperties(string $class): bool + { + if (is_a($class, \stdClass::class, true)) { + return true; + } + + $class = new \ReflectionClass($class); + + do { + if ($class->getAttributes(\AllowDynamicProperties::class) !== []) { + return true; + } + } while ($class = $class->getParentClass()); + + return false; + } } From 426e2fac9e17008064559d196af2411f7936c884 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:06:41 +0700 Subject: [PATCH 2/5] fix test --- tests/src/IntegrationTest/MapPropertyPathTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/src/IntegrationTest/MapPropertyPathTest.php b/tests/src/IntegrationTest/MapPropertyPathTest.php index 9f34d71..cce79d4 100644 --- a/tests/src/IntegrationTest/MapPropertyPathTest.php +++ b/tests/src/IntegrationTest/MapPropertyPathTest.php @@ -68,7 +68,7 @@ public function testPropertyPathMetadataFactory( $library->addShelf($shelf); $metadata = $propertyPathAwarePropertyTypeExtractor - ->createPropertyMetadata($class, $path, false); + ->createPropertyMetadata($class, $path); $types = $metadata->getTypes(); $attributes = $metadata->getAttributes(); From 6840273a553f7c1e691222093c177b3f6a1f65db Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:10:38 +0700 Subject: [PATCH 3/5] add some tests --- .../IntegrationTest/DynamicPropertyTest.php | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/src/IntegrationTest/DynamicPropertyTest.php b/tests/src/IntegrationTest/DynamicPropertyTest.php index a3ac7d2..e66cdf4 100644 --- a/tests/src/IntegrationTest/DynamicPropertyTest.php +++ b/tests/src/IntegrationTest/DynamicPropertyTest.php @@ -21,9 +21,70 @@ use Rekalogika\Mapper\Tests\Fixtures\DynamicProperty\ObjectWithNonNullPropertyThatCannotBeCastFromNull; use Rekalogika\Mapper\Tests\Fixtures\Scalar\ObjectWithScalarProperties; use Rekalogika\Mapper\Tests\Fixtures\ScalarDto\ObjectWithScalarPropertiesDto; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\Util\DynamicPropertiesDeterminer; class DynamicPropertyTest extends FrameworkTestCase { + /** + * @param class-string $class + * @dataProvider provideDynamicPropertiesDetermination + */ + public function testDynamicPropertiesDetermination( + string $class, + bool $expected, + ): void { + $determiner = new DynamicPropertiesDeterminer(); + $actual = $determiner->allowsDynamicProperties($class); + + $this->assertEquals($expected, $actual); + } + + /** + * @return iterable + */ + public static function provideDynamicPropertiesDetermination(): iterable + { + yield 'stdClass' => [ + \stdClass::class, + true, + ]; + + yield 'ObjectExtendingStdClass' => [ + ObjectExtendingStdClass::class, + true, + ]; + + yield 'ObjectWithScalarProperties' => [ + ObjectWithScalarProperties::class, + false, + ]; + + yield 'ObjectWithScalarPropertiesDto' => [ + ObjectWithScalarPropertiesDto::class, + false, + ]; + + yield 'AnotherObjectExtendingStdClass' => [ + AnotherObjectExtendingStdClass::class, + true, + ]; + + yield 'ObjectExtendingStdClassWithExplicitScalarProperties' => [ + ObjectExtendingStdClassWithExplicitScalarProperties::class, + true, + ]; + + yield 'ObjectExtendingStdClassWithProperties' => [ + ObjectExtendingStdClassWithProperties::class, + true, + ]; + + yield 'ObjectWithNonNullPropertyThatCannotBeCastFromNull' => [ + ObjectWithNonNullPropertyThatCannotBeCastFromNull::class, + false, + ]; + } + // from stdclass to object public function testStdClassToObject(): void From f1cfc30050f5f0214b89036d992a648737e651b1 Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:12:40 +0700 Subject: [PATCH 4/5] fix --- .../Implementation/ObjectToObjectMetadataFactory.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php index 557a346..bc3d23e 100644 --- a/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php +++ b/src/Transformer/ObjectToObjectMetadata/Implementation/ObjectToObjectMetadataFactory.php @@ -27,6 +27,7 @@ use Rekalogika\Mapper\Transformer\Exception\SourceClassNotInInheritanceMapException; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\Util\ClassMetadataFactory; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\Util\ClassMetadataFactoryInterface; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\Util\DynamicPropertiesDeterminer; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\Util\PropertyMappingResolver; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\Util\PropertyMetadataFactory; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\Util\PropertyMetadataFactoryInterface; @@ -62,17 +63,21 @@ public function __construct( private ProxyFactoryInterface $proxyFactory, TypeResolverInterface $typeResolver, ) { + $dynamicPropertiesDeterminer = new DynamicPropertiesDeterminer(); + $this->propertyMetadataFactory = new PropertyMetadataFactory( propertyReadInfoExtractor: $propertyReadInfoExtractor, propertyWriteInfoExtractor: $propertyWriteInfoExtractor, propertyTypeExtractor: $propertyTypeExtractor, typeResolver: $typeResolver, + dynamicPropertiesDeterminer: $dynamicPropertiesDeterminer, ); $this->classMetadataFactory = new ClassMetadataFactory( eagerPropertiesResolver: $eagerPropertiesResolver, propertyListExtractor: $propertyListExtractor, propertyWriteInfoExtractor: $propertyWriteInfoExtractor, + dynamicPropertiesDeterminer: $dynamicPropertiesDeterminer, ); $this->propertyMappingResolver = new PropertyMappingResolver( @@ -190,14 +195,12 @@ public function createObjectToObjectMetadata( ->createPropertyMetadata( class: $sourceClass, property: $sourceProperty, - allowsDynamicProperties: $sourceClassMetadata->hasReadableDynamicProperties(), ); $targetPropertyMetadata = $this->propertyMetadataFactory ->createPropertyMetadata( class: $targetClass, property: $targetProperty, - allowsDynamicProperties: $targetClassMetadata->hasWritableDynamicProperties(), ); // determine if source property is lazy From f1a285ea4135bf09bfe744eeebca0effa177477f Mon Sep 17 00:00:00 2001 From: Priyadi Iman Nurcahyo <1102197+priyadi@users.noreply.github.com> Date: Sun, 29 Sep 2024 11:15:46 +0700 Subject: [PATCH 5/5] fix test --- tests/src/IntegrationTest/ValueObjectTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/src/IntegrationTest/ValueObjectTest.php b/tests/src/IntegrationTest/ValueObjectTest.php index 43e85e2..122d5a8 100644 --- a/tests/src/IntegrationTest/ValueObjectTest.php +++ b/tests/src/IntegrationTest/ValueObjectTest.php @@ -21,6 +21,7 @@ use Rekalogika\Mapper\Tests\Fixtures\WitherMethod\ObjectWithImmutableSetter; use Rekalogika\Mapper\Transformer\EagerPropertiesResolver\EagerPropertiesResolverInterface; use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\Util\ClassMetadataFactory; +use Rekalogika\Mapper\Transformer\ObjectToObjectMetadata\Implementation\Util\DynamicPropertiesDeterminer; use Symfony\Component\Clock\DatePoint; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; @@ -77,6 +78,7 @@ public function testValueObject(string $class, bool $isValueObject): void eagerPropertiesResolver: $eagerPropertiesResolver, propertyWriteInfoExtractor: $propertyWriteInfoExtractor, propertyListExtractor: $propertyListExtractor, + dynamicPropertiesDeterminer: new DynamicPropertiesDeterminer(), ); $classMetadata = $classMetadataFactory->createClassMetadata($class);