diff --git a/src/AutoMapper.php b/src/AutoMapper.php index c5915e92..d6b13a3f 100644 --- a/src/AutoMapper.php +++ b/src/AutoMapper.php @@ -152,7 +152,7 @@ public function bindTransformerFactory(TransformerFactoryInterface $transformerF } public static function create( - bool $private = true, + bool $mapPrivateProperties = false, ClassLoaderInterface $loader = null, AdvancedNameConverterInterface $nameConverter = null, string $classPrefix = 'Mapper_', @@ -171,19 +171,9 @@ public static function create( )); } - $flags = ReflectionExtractor::ALLOW_PUBLIC; + $flags = ReflectionExtractor::ALLOW_PUBLIC | ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE; - if ($private) { - $flags |= ReflectionExtractor::ALLOW_PROTECTED | ReflectionExtractor::ALLOW_PRIVATE; - } - - $reflectionExtractor = new ReflectionExtractor( - null, - null, - null, - true, - $flags - ); + $reflectionExtractor = new ReflectionExtractor(accessFlags: $flags); $phpDocExtractor = new PhpDocExtractor(); $propertyInfoExtractor = new PropertyInfoExtractor( @@ -226,7 +216,8 @@ public static function create( $fromTargetMappingExtractor, $classPrefix, $attributeChecking, - $dateTimeFormat + $dateTimeFormat, + $mapPrivateProperties )) : new self($loader, $transformerFactory); $transformerFactory->addTransformerFactory(new MultipleTransformerFactory($transformerFactory)); diff --git a/src/Extractor/FromSourceMappingExtractor.php b/src/Extractor/FromSourceMappingExtractor.php index d4114330..0deb292e 100644 --- a/src/Extractor/FromSourceMappingExtractor.php +++ b/src/Extractor/FromSourceMappingExtractor.php @@ -8,6 +8,7 @@ use AutoMapper\MapperMetadataInterface; use AutoMapper\Transformer\TransformerFactoryInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfo; use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; use Symfony\Component\PropertyInfo\Type; @@ -85,7 +86,8 @@ public function getPropertiesMapping(MapperMetadataInterface $mapperMetadata): a $this->getGroups($mapperMetadata->getTarget(), $property), $this->getMaxDepth($mapperMetadata->getSource(), $property), $this->isIgnoredProperty($mapperMetadata->getSource(), $property), - $this->isIgnoredProperty($mapperMetadata->getTarget(), $property) + $this->isIgnoredProperty($mapperMetadata->getTarget(), $property), + PropertyReadInfo::VISIBILITY_PUBLIC === $this->readInfoExtractor->getReadInfo($mapperMetadata->getSource(), $property)?->getVisibility() ?? true, ); } diff --git a/src/Extractor/FromTargetMappingExtractor.php b/src/Extractor/FromTargetMappingExtractor.php index fd953c06..29d67cd7 100644 --- a/src/Extractor/FromTargetMappingExtractor.php +++ b/src/Extractor/FromTargetMappingExtractor.php @@ -8,6 +8,7 @@ use AutoMapper\MapperMetadataInterface; use AutoMapper\Transformer\TransformerFactoryInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfo; use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; use Symfony\Component\PropertyInfo\PropertyWriteInfo; use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; @@ -84,7 +85,8 @@ public function getPropertiesMapping(MapperMetadataInterface $mapperMetadata): a $this->getGroups($mapperMetadata->getTarget(), $property), $this->getMaxDepth($mapperMetadata->getTarget(), $property), $this->isIgnoredProperty($mapperMetadata->getSource(), $property), - $this->isIgnoredProperty($mapperMetadata->getTarget(), $property) + $this->isIgnoredProperty($mapperMetadata->getTarget(), $property), + PropertyReadInfo::VISIBILITY_PUBLIC === $this->readInfoExtractor->getReadInfo($mapperMetadata->getSource(), $property)?->getVisibility() ?? true, ); } diff --git a/src/Extractor/MapToContextPropertyInfoExtractorDecorator.php b/src/Extractor/MapToContextPropertyInfoExtractorDecorator.php index 8ffa37db..9a5df36e 100644 --- a/src/Extractor/MapToContextPropertyInfoExtractorDecorator.php +++ b/src/Extractor/MapToContextPropertyInfoExtractorDecorator.php @@ -5,21 +5,31 @@ namespace AutoMapper\Extractor; use AutoMapper\Attribute\MapToContext; +use Symfony\Component\PropertyInfo\Extractor\ConstructorArgumentTypeExtractorInterface; use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyInitializableExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyReadInfo; use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; +use Symfony\Component\PropertyInfo\PropertyWriteInfo; +use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface; -final readonly class MapToContextPropertyInfoExtractorDecorator implements PropertyAccessExtractorInterface, PropertyReadInfoExtractorInterface +final readonly class MapToContextPropertyInfoExtractorDecorator implements PropertyListExtractorInterface, PropertyTypeExtractorInterface, PropertyAccessExtractorInterface, PropertyInitializableExtractorInterface, PropertyReadInfoExtractorInterface, PropertyWriteInfoExtractorInterface, ConstructorArgumentTypeExtractorInterface { public function __construct( - private PropertyReadInfoExtractorInterface&PropertyAccessExtractorInterface $propertyReadInfoExtractor + private ReflectionExtractor $decorated ) { } public function getReadInfo(string $class, string $property, array $context = []): ?PropertyReadInfo { - $readInfo = $this->propertyReadInfoExtractor->getReadInfo($class, $property, $context); + $readInfo = $this->decorated->getReadInfo($class, $property, $context); + + if ($class === 'array') { + return $readInfo; + } if (null === $readInfo || $readInfo->getType() === PropertyReadInfo::TYPE_PROPERTY && PropertyReadInfo::VISIBILITY_PUBLIC !== $readInfo->getVisibility()) { $reflClass = new \ReflectionClass($class); @@ -56,7 +66,7 @@ public function isReadable(string $class, string $property, array $context = []) public function isWritable(string $class, string $property, array $context = []): bool { - return $this->propertyReadInfoExtractor->isWritable($class, $property, $context); + return $this->decorated->isWritable($class, $property, $context); } private function camelize(string $string): string @@ -91,4 +101,29 @@ private function isAllowedProperty(string $class, string $property, bool $writeA return false; } + + public function getTypesFromConstructor(string $class, string $property): ?array + { + return $this->decorated->getTypesFromConstructor($class, $property); + } + + public function isInitializable(string $class, string $property, array $context = []): ?bool + { + return $this->decorated->isInitializable($class, $property, $context); + } + + public function getProperties(string $class, array $context = []) + { + return $this->decorated->getProperties($class, $context); + } + + public function getTypes(string $class, string $property, array $context = []) + { + return $this->decorated->getTypes($class, $property, $context); + } + + public function getWriteInfo(string $class, string $property, array $context = []): ?PropertyWriteInfo + { + return $this->decorated->getWriteInfo($class, $property, $context); + } } diff --git a/src/Extractor/MappingExtractor.php b/src/Extractor/MappingExtractor.php index 72e750aa..13d4a2cd 100644 --- a/src/Extractor/MappingExtractor.php +++ b/src/Extractor/MappingExtractor.php @@ -22,7 +22,7 @@ abstract class MappingExtractor implements MappingExtractorInterface { public function __construct( protected readonly PropertyInfoExtractorInterface $propertyInfoExtractor, - private readonly PropertyReadInfoExtractorInterface $readInfoExtractor, + protected readonly PropertyReadInfoExtractorInterface $readInfoExtractor, protected readonly PropertyWriteInfoExtractorInterface $writeInfoExtractor, protected readonly TransformerFactoryInterface $transformerFactory, private readonly ?ClassMetadataFactoryInterface $classMetadataFactory = null, @@ -47,7 +47,8 @@ public function getReadAccessor(string $source, string $target, string $property $type, $readInfo->getName(), $source, - PropertyReadInfo::VISIBILITY_PUBLIC !== $readInfo->getVisibility() + PropertyReadInfo::VISIBILITY_PUBLIC !== $readInfo->getVisibility(), + $property ); } diff --git a/src/Extractor/PropertyMapping.php b/src/Extractor/PropertyMapping.php index d9231d04..b4212d4a 100644 --- a/src/Extractor/PropertyMapping.php +++ b/src/Extractor/PropertyMapping.php @@ -24,12 +24,15 @@ public function __construct( public readonly ?array $targetGroups = null, public readonly ?int $maxDepth = null, public readonly bool $sourceIgnored = false, - public readonly bool $targetIgnored = false + public readonly bool $targetIgnored = false, + public readonly bool $isPublic = false, ) { } - public function shouldIgnoreProperty(): bool + public function shouldIgnoreProperty(bool $shouldMapPrivateProperties = true): bool { - return $this->sourceIgnored || $this->targetIgnored; + return $this->sourceIgnored + || $this->targetIgnored + || !($shouldMapPrivateProperties || $this->isPublic); } } diff --git a/src/Extractor/ReadAccessor.php b/src/Extractor/ReadAccessor.php index 44b66eee..da026a63 100644 --- a/src/Extractor/ReadAccessor.php +++ b/src/Extractor/ReadAccessor.php @@ -28,10 +28,11 @@ final class ReadAccessor public function __construct( private readonly int $type, - private readonly string $name, + private readonly string $accessor, private readonly ?string $sourceClass = null, - private readonly bool $private = false) - { + private readonly bool $private = false, + private readonly ?string $name = null, // will be the name of the property if different from accessor + ) { if (self::TYPE_METHOD === $this->type && null === $this->sourceClass) { throw new \InvalidArgumentException('Source class must be provided when using "method" type.'); } @@ -48,7 +49,7 @@ public function getExpression(Expr\Variable $input): Expr $methodCallArguments = []; if (\PHP_VERSION_ID >= 80000 && class_exists($this->sourceClass)) { - $parameters = (new \ReflectionMethod($this->sourceClass, $this->name))->getParameters(); + $parameters = (new \ReflectionMethod($this->sourceClass, $this->accessor))->getParameters(); foreach ($parameters as $parameter) { if ($attribute = ($parameter->getAttributes(MapToContext::class)[0] ?? null)) { @@ -72,7 +73,7 @@ public function getExpression(Expr\Variable $input): Expr [ new Arg( new Scalar\String_( - "Parameter \"\${$parameter->getName()}\" of method \"{$this->sourceClass}\"::\"{$this->name}()\" is configured to be mapped to context but no value was found in the context." + "Parameter \"\${$parameter->getName()}\" of method \"{$this->sourceClass}\"::\"{$this->accessor}()\" is configured to be mapped to context but no value was found in the context." ) ), ] @@ -81,29 +82,38 @@ public function getExpression(Expr\Variable $input): Expr ) ); } elseif (!$parameter->isDefaultValueAvailable()) { - throw new \InvalidArgumentException("Accessors method \"{$this->sourceClass}\"::\"{$this->name}()\" parameters must have either a default value or the #[MapToContext] attribute."); + throw new \InvalidArgumentException("Accessors method \"{$this->sourceClass}\"::\"{$this->accessor}()\" parameters must have either a default value or the #[MapToContext] attribute."); } } } - return new Expr\MethodCall($input, $this->name, $methodCallArguments); + if ($this->private) { + return new Expr\FuncCall( + new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->name ?? $this->accessor)), + [ + new Arg($input), + ] + ); + } + + return new Expr\MethodCall($input, $this->accessor, $methodCallArguments); } if (self::TYPE_PROPERTY === $this->type) { if ($this->private) { return new Expr\FuncCall( - new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->name)), + new Expr\ArrayDimFetch(new Expr\PropertyFetch(new Expr\Variable('this'), 'extractCallbacks'), new Scalar\String_($this->accessor)), [ new Arg($input), ] ); } - return new Expr\PropertyFetch($input, $this->name); + return new Expr\PropertyFetch($input, $this->accessor); } if (self::TYPE_ARRAY_DIMENSION === $this->type) { - return new Expr\ArrayDimFetch($input, new Scalar\String_($this->name)); + return new Expr\ArrayDimFetch($input, new Scalar\String_($this->accessor)); } if (self::TYPE_SOURCE === $this->type) { @@ -118,19 +128,25 @@ public function getExpression(Expr\Variable $input): Expr */ public function getExtractCallback(string $className): ?Expr { - if (self::TYPE_PROPERTY !== $this->type || !$this->private) { + if (!\in_array($this->type, [self::TYPE_PROPERTY, self::TYPE_METHOD]) || !$this->private) { return null; } return new Expr\StaticCall(new Name\FullyQualified(\Closure::class), 'bind', [ - new Arg(new Expr\Closure([ - 'params' => [ - new Param(new Expr\Variable('object')), - ], - 'stmts' => [ - new Stmt\Return_(new Expr\PropertyFetch(new Expr\Variable('object'), $this->name)), - ], - ])), + new Arg( + new Expr\Closure([ + 'params' => [ + new Param(new Expr\Variable('object')), + ], + 'stmts' => [ + new Stmt\Return_( + $this->type === self::TYPE_PROPERTY + ? new Expr\PropertyFetch(new Expr\Variable('object'), $this->accessor) + : new Expr\MethodCall(new Expr\Variable('object'), $this->accessor) + ), + ], + ]) + ), new Arg(new Expr\ConstFetch(new Name('null'))), new Arg(new Scalar\String_($className)), ]); diff --git a/src/Extractor/SourceTargetMappingExtractor.php b/src/Extractor/SourceTargetMappingExtractor.php index 25da31c5..172c5a60 100644 --- a/src/Extractor/SourceTargetMappingExtractor.php +++ b/src/Extractor/SourceTargetMappingExtractor.php @@ -5,6 +5,7 @@ namespace AutoMapper\Extractor; use AutoMapper\MapperMetadataInterface; +use Symfony\Component\PropertyInfo\PropertyReadInfo; /** * Extracts mapping between two objects, only gives properties that have the same name. @@ -77,7 +78,8 @@ public function getPropertiesMapping(MapperMetadataInterface $mapperMetadata): a $this->getGroups($mapperMetadata->getTarget(), $property), $maxDepth, $this->isIgnoredProperty($mapperMetadata->getSource(), $property), - $this->isIgnoredProperty($mapperMetadata->getTarget(), $property) + $this->isIgnoredProperty($mapperMetadata->getTarget(), $property), + PropertyReadInfo::VISIBILITY_PUBLIC === $this->readInfoExtractor->getReadInfo($mapperMetadata->getSource(), $property)?->getVisibility() ?? true, ); } } diff --git a/src/Generator/Generator.php b/src/Generator/Generator.php index 9922fefe..74917125 100644 --- a/src/Generator/Generator.php +++ b/src/Generator/Generator.php @@ -154,7 +154,7 @@ public function generate(MapperGeneratorMetadataInterface $mapperGeneratorMetada $duplicatedStatements = []; $setterStatements = []; foreach ($propertiesMapping as $propertyMapping) { - if ($propertyMapping->shouldIgnoreProperty()) { + if ($propertyMapping->shouldIgnoreProperty($mapperGeneratorMetadata->shouldMapPrivateProperties())) { continue; } diff --git a/src/MapperGeneratorMetadataFactory.php b/src/MapperGeneratorMetadataFactory.php index f4dcb415..8a35da41 100644 --- a/src/MapperGeneratorMetadataFactory.php +++ b/src/MapperGeneratorMetadataFactory.php @@ -22,6 +22,7 @@ public function __construct( private string $classPrefix = 'Mapper_', private bool $attributeChecking = true, private string $dateTimeFormat = \DateTime::RFC3339, + private bool $mapPrivateProperties = true, ) { } @@ -40,7 +41,7 @@ public function create(MapperGeneratorMetadataRegistryInterface $autoMapperRegis $extractor = $this->fromSourcePropertiesMappingExtractor; } - $mapperMetadata = new MapperMetadata($autoMapperRegister, $extractor, $source, $target, $this->isReadOnly($target), $this->classPrefix); + $mapperMetadata = new MapperMetadata($autoMapperRegister, $extractor, $source, $target, $this->isReadOnly($target), $this->mapPrivateProperties, $this->classPrefix); $mapperMetadata->setAttributeChecking($this->attributeChecking); $mapperMetadata->setDateTimeFormat($this->dateTimeFormat); diff --git a/src/MapperGeneratorMetadataInterface.php b/src/MapperGeneratorMetadataInterface.php index c1858048..a811c9d8 100644 --- a/src/MapperGeneratorMetadataInterface.php +++ b/src/MapperGeneratorMetadataInterface.php @@ -54,4 +54,9 @@ public function isTargetCloneable(): bool; * If not the case, allow to not generate code about circular references */ public function canHaveCircularReference(): bool; + + /** + * Whether we should map private properties and methods. + */ + public function shouldMapPrivateProperties(): bool; } diff --git a/src/MapperMetadata.php b/src/MapperMetadata.php index 9d2b460b..5f5fce1d 100644 --- a/src/MapperMetadata.php +++ b/src/MapperMetadata.php @@ -35,6 +35,7 @@ public function __construct( private readonly string $source, private readonly string $target, private readonly bool $isTargetReadOnlyClass, + private readonly bool $mapPrivateProperties, private readonly string $classPrefix = 'Mapper_', ) { $this->isConstructorAllowed = true; @@ -266,4 +267,9 @@ public function isTargetReadOnlyClass(): bool { return $this->isTargetReadOnlyClass; } + + public function shouldMapPrivateProperties(): bool + { + return $this->mapPrivateProperties; + } } diff --git a/tests/AutoMapperBaseTest.php b/tests/AutoMapperBaseTest.php index cba14d63..1d82d4b7 100644 --- a/tests/AutoMapperBaseTest.php +++ b/tests/AutoMapperBaseTest.php @@ -33,7 +33,7 @@ protected function setUp(): void $this->buildAutoMapper(); } - protected function buildAutoMapper(bool $allowReadOnlyTargetToPopulate = false): AutoMapper + protected function buildAutoMapper(bool $allowReadOnlyTargetToPopulate = false, bool $mapPrivatePropertiesAndMethod = false, string $classPrefix = 'Mapper_'): AutoMapper { $fs = new Filesystem(); $fs->remove(__DIR__ . '/cache/'); @@ -45,6 +45,6 @@ protected function buildAutoMapper(bool $allowReadOnlyTargetToPopulate = false): $allowReadOnlyTargetToPopulate ), __DIR__ . '/cache'); - return $this->autoMapper = AutoMapper::create(true, $this->loader); + return $this->autoMapper = AutoMapper::create($mapPrivatePropertiesAndMethod, $this->loader, classPrefix: $classPrefix); } } diff --git a/tests/AutoMapperTest.php b/tests/AutoMapperTest.php index f2a4b8c5..1568b9c3 100644 --- a/tests/AutoMapperTest.php +++ b/tests/AutoMapperTest.php @@ -17,6 +17,7 @@ use AutoMapper\Tests\Fixtures\AddressType; use AutoMapper\Tests\Fixtures\AddressWithEnum; use AutoMapper\Tests\Fixtures\ClassWithMapToContextAttribute; +use AutoMapper\Tests\Fixtures\ClassWithPrivateProperty; use AutoMapper\Tests\Fixtures\Fish; use AutoMapper\Tests\Fixtures\ObjectWithDateTime; use AutoMapper\Tests\Fixtures\Order; @@ -34,6 +35,8 @@ class AutoMapperTest extends AutoMapperBaseTest { public function testAutoMapping(): void { + $this->buildAutoMapper(mapPrivatePropertiesAndMethod: true); + $userMetadata = $this->autoMapper->getMetadata(Fixtures\User::class, Fixtures\UserDTO::class); $userMetadata->forMember('yearOfBirth', function (Fixtures\User $user) { return ((int) date('Y')) - ((int) $user->age); @@ -66,6 +69,8 @@ public function testAutoMapping(): void public function testAutoMapperFromArray(): void { + $this->buildAutoMapper(mapPrivatePropertiesAndMethod: true); + $user = [ 'id' => 1, 'address' => [ @@ -87,6 +92,8 @@ public function testAutoMapperFromArray(): void public function testAutoMapperFromArrayCustomDateTime(): void { + $this->buildAutoMapper(mapPrivatePropertiesAndMethod: true, classPrefix: 'CustomDateTime_'); + $dateTime = \DateTime::createFromFormat(\DateTime::RFC3339, '1987-04-30T06:00:00Z'); $customFormat = 'U'; $user = [ @@ -97,12 +104,11 @@ public function testAutoMapperFromArrayCustomDateTime(): void 'createdAt' => $dateTime->format($customFormat), ]; - $autoMapper = AutoMapper::create(true, $this->loader, null, 'CustomDateTime_'); - $configuration = $autoMapper->getMetadata('array', Fixtures\UserDTO::class); + $configuration = $this->autoMapper->getMetadata('array', Fixtures\UserDTO::class); $configuration->setDateTimeFormat($customFormat); /** @var Fixtures\UserDTO $userDto */ - $userDto = $autoMapper->map($user, Fixtures\UserDTO::class); + $userDto = $this->autoMapper->map($user, Fixtures\UserDTO::class); self::assertInstanceOf(Fixtures\UserDTO::class, $userDto); self::assertEquals($dateTime->format($customFormat), $userDto->createdAt->format($customFormat)); @@ -126,6 +132,8 @@ public function testAutoMapperToArray(): void public function testAutoMapperFromStdObject(): void { + $this->buildAutoMapper(mapPrivatePropertiesAndMethod: true); + $user = new \stdClass(); $user->id = 1; @@ -164,16 +172,17 @@ public function testAutoMapperStdObjectToStdObject(): void public function testNotReadable(): void { - $autoMapper = AutoMapper::create(false, $this->loader, null, 'NotReadable_'); + $this->buildAutoMapper(classPrefix: 'CustomDateTime_'); + $address = new Fixtures\Address(); $address->setCity('test'); - $addressArray = $autoMapper->map($address, 'array'); + $addressArray = $this->autoMapper->map($address, 'array'); self::assertIsArray($addressArray); self::assertArrayNotHasKey('city', $addressArray); - $addressMapped = $autoMapper->map($address, Fixtures\Address::class); + $addressMapped = $this->autoMapper->map($address, Fixtures\Address::class); self::assertInstanceOf(Fixtures\Address::class, $addressMapped); @@ -187,11 +196,12 @@ public function testNotReadable(): void public function testNoTypes(): void { - $autoMapper = AutoMapper::create(false, $this->loader, null, 'NotReadable_'); + $this->buildAutoMapper(classPrefix: 'NotReadable_'); + $address = new Fixtures\AddressNoTypes(); $address->city = 'test'; - $addressArray = $autoMapper->map($address, 'array'); + $addressArray = $this->autoMapper->map($address, 'array'); self::assertIsArray($addressArray); self::assertArrayHasKey('city', $addressArray); @@ -349,6 +359,8 @@ public function testCircularReferenceArray(): void public function testPrivate(): void { + $this->buildAutoMapper(mapPrivatePropertiesAndMethod: true); + $user = new Fixtures\PrivateUser(10, 'foo', 'bar'); /** @var Fixtures\PrivateUserDTO $userDto */ $userDto = $this->autoMapper->map($user, Fixtures\PrivateUserDTO::class); @@ -361,7 +373,7 @@ public function testPrivate(): void public function testConstructor(): void { - $autoMapper = AutoMapper::create(false, $this->loader); + $autoMapper = AutoMapper::create(loader: $this->loader); $user = new Fixtures\UserDTO(); $user->id = 10; @@ -379,8 +391,9 @@ public function testConstructor(): void public function testConstructorNotAllowed(): void { - $autoMapper = AutoMapper::create(true, $this->loader, null, 'NotAllowedMapper_'); - $configuration = $autoMapper->getMetadata(Fixtures\UserDTO::class, Fixtures\UserConstructorDTO::class); + $this->buildAutoMapper(classPrefix: 'NotAllowedMapper_'); + + $configuration = $this->autoMapper->getMetadata(Fixtures\UserDTO::class, Fixtures\UserConstructorDTO::class); $configuration->setConstructorAllowed(false); $user = new Fixtures\UserDTO(); @@ -389,7 +402,7 @@ public function testConstructorNotAllowed(): void $user->age = 3; /** @var Fixtures\UserConstructorDTO $userDto */ - $userDto = $autoMapper->map($user, Fixtures\UserConstructorDTO::class); + $userDto = $this->autoMapper->map($user, Fixtures\UserConstructorDTO::class); self::assertInstanceOf(Fixtures\UserConstructorDTO::class, $userDto); self::assertSame('10', $userDto->getId()); @@ -590,6 +603,8 @@ public function testIgnoredAttributes(): void public function testMappingWithTargetObjectWithNoObjectToPopulate(): void { + $this->buildAutoMapper(mapPrivatePropertiesAndMethod: true); + $configurationUser = $this->autoMapper->getMetadata(Fixtures\User::class, Fixtures\UserDTOMerged::class); $configurationUser->forMember('properties', function (Fixtures\User $user, Fixtures\UserDTOMerged $target) { return array_merge($target->getProperties(), [ @@ -671,7 +686,7 @@ public function denormalize(string $propertyName, ?string $class = null, ?string }; } - $autoMapper = AutoMapper::create(true, null, $nameConverter, 'Mapper2_'); + $autoMapper = AutoMapper::create(loader: $this->loader, nameConverter: $nameConverter, classPrefix: 'Mapper2_'); $user = new Fixtures\User(1, 'yolo', '13'); $userArray = $autoMapper->map($user, 'array'); @@ -699,6 +714,8 @@ public function testDefaultArguments(): void public function testDiscriminator(): void { + $this->buildAutoMapper(classPrefix: 'Discriminator'); + $data = [ 'type' => 'cat', ]; @@ -727,7 +744,7 @@ public function testNoAutoRegister(): void { self::expectException(NoMappingFoundException::class); - $automapper = AutoMapper::create(false, null, null, 'Mapper_', true, false); + $automapper = AutoMapper::create(autoRegister: false); $automapper->getMapper(Fixtures\User::class, Fixtures\UserDTO::class); } @@ -745,6 +762,8 @@ public function testWithMixedArray(): void public function testCustomTransformerFromArrayToObject(): void { + $this->buildAutoMapper(mapPrivatePropertiesAndMethod: true); + $this->autoMapper->bindTransformerFactory(new MoneyTransformerFactory()); $data = [ @@ -794,9 +813,6 @@ public function testCustomTransformerFromObjectToObject(): void self::assertEquals('EUR', $newOrder->price->getCurrency()->getCode()); } - /** - * @requires PHP >= 7.4 - */ public function testIssue425(): void { $data = [1, 2, 3, 4, 5]; @@ -924,6 +940,8 @@ public function testSkipNullValues(): void public function testAdderAndRemoverWithClass() { + $this->buildAutoMapper(mapPrivatePropertiesAndMethod: true); + $petOwner = [ 'pets' => [ ['type' => 'cat', 'name' => 'FĂ©lix'], @@ -943,6 +961,8 @@ public function testAdderAndRemoverWithClass() public function testAdderAndRemoverWithInstance() { + $this->buildAutoMapper(mapPrivatePropertiesAndMethod: true); + $fish = new Fish(); $fish->name = 'Nemo'; $fish->type = 'fish'; @@ -1016,11 +1036,10 @@ public function testPartialConstructorWithTargetToPopulate(): void self::assertEquals(37, $target->age); } - /** - * @requires PHP 8.1 - */ public function testEnum(): void { + $this->buildAutoMapper(mapPrivatePropertiesAndMethod: true); + // enum source $address = new AddressWithEnum(); $address->setType(AddressType::APARTMENT); @@ -1043,9 +1062,6 @@ public function testEnum(): void self::assertEquals($address->getType(), $copyAddress->getType()); } - /** - * @requires PHP 8.2 - */ public function testTargetReadonlyClass(): void { $data = ['city' => 'Nantes']; @@ -1055,9 +1071,6 @@ public function testTargetReadonlyClass(): void $this->autoMapper->map($data, $toPopulate); } - /** - * @requires PHP 8.2 - */ public function testTargetReadonlyClassSkippedContext(): void { $data = ['city' => 'Nantes']; @@ -1069,9 +1082,6 @@ public function testTargetReadonlyClassSkippedContext(): void self::assertEquals('city', $toPopulate->city); } - /** - * @requires PHP 8.2 - */ public function testTargetReadonlyClassAllowed(): void { $this->buildAutoMapper(true); @@ -1086,13 +1096,11 @@ public function testTargetReadonlyClassAllowed(): void } /** - * @requires PHP 8.1 - * * @dataProvider provideReadonly */ public function testReadonly(string $addressWithReadonlyClass): void { - $this->buildAutoMapper(true); + $this->buildAutoMapper(allowReadOnlyTargetToPopulate: true, mapPrivatePropertiesAndMethod: true); $address = new Address(); $address->setCity('city'); @@ -1156,9 +1164,6 @@ public function testDateTimeFormatCanBeConfiguredFromContext(): void ); } - /** - * @requires PHP 8.0 - */ public function testMapToContextAttribute(): void { self::assertSame( @@ -1174,4 +1179,32 @@ public function testMapToContextAttribute(): void ) ); } + + public function testMapClassWithPrivateProperty(): void + { + $this->buildAutoMapper(mapPrivatePropertiesAndMethod: true); + + self::assertSame( + ['foo' => 'foo', 'bar' => 'bar'], + $this->autoMapper->map(new ClassWithPrivateProperty('foo'), 'array') + ); + self::assertEquals( + new ClassWithPrivateProperty('foo'), + $this->autoMapper->map(['foo' => 'foo'], ClassWithPrivateProperty::class) + ); + } + + /** + * Generated mapper will be different from what "testMapClassWithPrivateProperty" generates, + * hence the duplicated class, to avoid any conflict with autloading. + */ + public function testItCanDisablePrivatePropertiesMapping(): void + { + $this->buildAutoMapper(classPrefix: 'DontMapPrivate_'); + + self::assertSame( + [], + $this->autoMapper->map(new ClassWithPrivateProperty('foo'), 'array') + ); + } } diff --git a/tests/Extractor/FromSourceMappingExtractorTest.php b/tests/Extractor/FromSourceMappingExtractorTest.php index 6fecfce7..2cabe51e 100644 --- a/tests/Extractor/FromSourceMappingExtractorTest.php +++ b/tests/Extractor/FromSourceMappingExtractorTest.php @@ -78,7 +78,7 @@ private function fromSourceMappingExtractorBootstrap(bool $private = true): void public function testWithTargetAsArray(): void { $userReflection = new \ReflectionClass(Fixtures\User::class); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, Fixtures\User::class, 'array', false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, source: Fixtures\User::class, target: 'array', isTargetReadOnlyClass: false, mapPrivateProperties: true); $sourcePropertiesMapping = $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); self::assertCount(\count($userReflection->getProperties()), $sourcePropertiesMapping); @@ -91,7 +91,7 @@ public function testWithTargetAsArray(): void public function testWithTargetAsStdClass(): void { $userReflection = new \ReflectionClass(Fixtures\User::class); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, Fixtures\User::class, 'stdClass', false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, source: Fixtures\User::class, target: 'stdClass', isTargetReadOnlyClass: false, mapPrivateProperties: true); $sourcePropertiesMapping = $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); self::assertCount(\count($userReflection->getProperties()), $sourcePropertiesMapping); @@ -103,7 +103,7 @@ public function testWithTargetAsStdClass(): void public function testWithSourceAsEmpty(): void { - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, Fixtures\Empty_::class, 'array', false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, source: Fixtures\Empty_::class, target: 'array', isTargetReadOnlyClass: false, mapPrivateProperties: true); $sourcePropertiesMapping = $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); self::assertCount(0, $sourcePropertiesMapping); @@ -112,12 +112,12 @@ public function testWithSourceAsEmpty(): void public function testWithSourceAsPrivate(): void { $privateReflection = new \ReflectionClass(Fixtures\Private_::class); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, Fixtures\Private_::class, 'array', false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, source: Fixtures\Private_::class, target: 'array', isTargetReadOnlyClass: false, mapPrivateProperties: true); $sourcePropertiesMapping = $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); self::assertCount(\count($privateReflection->getProperties()), $sourcePropertiesMapping); $this->fromSourceMappingExtractorBootstrap(false); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, Fixtures\Private_::class, 'array', false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, source: Fixtures\Private_::class, target: 'array', isTargetReadOnlyClass: false, mapPrivateProperties: true); $sourcePropertiesMapping = $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); self::assertCount(0, $sourcePropertiesMapping); } @@ -127,7 +127,7 @@ public function testWithSourceAsArray(): void self::expectException(InvalidMappingException::class); self::expectExceptionMessage('Only array or stdClass are accepted as a target'); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, 'array', Fixtures\User::class, false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, source: 'array', target: Fixtures\User::class, isTargetReadOnlyClass: false, mapPrivateProperties: true); $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); } @@ -136,7 +136,7 @@ public function testWithSourceAsStdClass(): void self::expectException(InvalidMappingException::class); self::expectExceptionMessage('Only array or stdClass are accepted as a target'); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, 'stdClass', Fixtures\User::class, false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromSourceMappingExtractor, source: 'stdClass', target: Fixtures\User::class, isTargetReadOnlyClass: false, mapPrivateProperties: true); $this->fromSourceMappingExtractor->getPropertiesMapping($mapperMetadata); } } diff --git a/tests/Extractor/FromTargetMappingExtractorTest.php b/tests/Extractor/FromTargetMappingExtractorTest.php index a400c302..0faea684 100644 --- a/tests/Extractor/FromTargetMappingExtractorTest.php +++ b/tests/Extractor/FromTargetMappingExtractorTest.php @@ -78,7 +78,7 @@ private function fromTargetMappingExtractorBootstrap(bool $private = true): void public function testWithSourceAsArray(): void { $userReflection = new \ReflectionClass(Fixtures\User::class); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, 'array', Fixtures\User::class, false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, source: 'array', target: Fixtures\User::class, isTargetReadOnlyClass: false, mapPrivateProperties: true); $targetPropertiesMapping = $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); self::assertCount(\count($userReflection->getProperties()), $targetPropertiesMapping); @@ -90,7 +90,7 @@ public function testWithSourceAsArray(): void public function testWithSourceAsStdClass(): void { $userReflection = new \ReflectionClass(Fixtures\User::class); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, 'stdClass', Fixtures\User::class, false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, source: 'stdClass', target: Fixtures\User::class, isTargetReadOnlyClass: false, mapPrivateProperties: true); $targetPropertiesMapping = $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); self::assertCount(\count($userReflection->getProperties()), $targetPropertiesMapping); @@ -101,7 +101,7 @@ public function testWithSourceAsStdClass(): void public function testWithTargetAsEmpty(): void { - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, 'array', Fixtures\Empty_::class, false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, source: 'array', target: Fixtures\Empty_::class, isTargetReadOnlyClass: false, mapPrivateProperties: true); $targetPropertiesMapping = $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); self::assertCount(0, $targetPropertiesMapping); @@ -110,12 +110,12 @@ public function testWithTargetAsEmpty(): void public function testWithTargetAsPrivate(): void { $privateReflection = new \ReflectionClass(Fixtures\Private_::class); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, 'array', Fixtures\Private_::class, false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, source: 'array', target: Fixtures\Private_::class, isTargetReadOnlyClass: false, mapPrivateProperties: true); $targetPropertiesMapping = $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); self::assertCount(\count($privateReflection->getProperties()), $targetPropertiesMapping); $this->fromTargetMappingExtractorBootstrap(false); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, 'array', Fixtures\Private_::class, false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, source: 'array', target: Fixtures\Private_::class, isTargetReadOnlyClass: false, mapPrivateProperties: true); $targetPropertiesMapping = $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); self::assertCount(0, $targetPropertiesMapping); } @@ -125,7 +125,7 @@ public function testWithTargetAsArray(): void self::expectException(InvalidMappingException::class); self::expectExceptionMessage('Only array or stdClass are accepted as a source'); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, Fixtures\User::class, 'array', false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, source: Fixtures\User::class, target: 'array', isTargetReadOnlyClass: false, mapPrivateProperties: true); $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); } @@ -134,7 +134,7 @@ public function testWithTargetAsStdClass(): void self::expectException(InvalidMappingException::class); self::expectExceptionMessage('Only array or stdClass are accepted as a source'); - $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, Fixtures\User::class, 'stdClass', false); + $mapperMetadata = new MapperMetadata($this->autoMapper, $this->fromTargetMappingExtractor, source: Fixtures\User::class, target: 'stdClass', isTargetReadOnlyClass: false, mapPrivateProperties: true); $this->fromTargetMappingExtractor->getPropertiesMapping($mapperMetadata); } } diff --git a/tests/Fixtures/ClassWithPrivateProperty.php b/tests/Fixtures/ClassWithPrivateProperty.php new file mode 100644 index 00000000..9132246b --- /dev/null +++ b/tests/Fixtures/ClassWithPrivateProperty.php @@ -0,0 +1,17 @@ +buildAutoMapper(mapPrivatePropertiesAndMethod: true); $this->normalizer = new AutoMapperNormalizer($this->autoMapper); }