Skip to content

feat(array-access): allow to map extra properties #251

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 2, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,8 @@ Setting this to false will make the `AutoMapper` throw an exception if the mappe

This can be useful if you want to pre generate all the mappers and have tests to ensure that all the mappers are
generated.

* `allowExtraProperties` (default: `false`)

Settings this to true will allow the mapper to map extra properties from the source object to the target object when
Copy link
Preview

Copilot AI Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typographical error: 'Settings' should be 'Setting'.

Suggested change
Settings this to true will allow the mapper to map extra properties from the source object to the target object when
Setting this to true will allow the mapper to map extra properties from the source object to the target object when

Copilot is powered by AI, so mistakes are possible. Review output carefully before use.

the source or the target implements the `ArrayAccess` interface.
1 change: 1 addition & 0 deletions src/Attribute/Mapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function __construct(
public ?bool $strictTypes = null,
public int $priority = 0,
public ?string $dateTimeFormat = null,
public ?bool $allowExtraProperties = null,
) {
}
}
4 changes: 4 additions & 0 deletions src/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public function __construct(
* The strategy to use to generate the mappers between each request.
*/
public FileReloadStrategy $reloadStrategy = FileReloadStrategy::ON_CHANGE,
/**
* Allow extra properties to be mapped when the target or the source implements ArrayAccess class.
*/
public bool $allowExtraProperties = false,
) {
}
}
1 change: 1 addition & 0 deletions src/Event/GenerateMapperEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public function __construct(
public ?ConstructorStrategy $constructorStrategy = null,
public ?bool $allowReadOnlyTargetToPopulate = null,
public ?bool $strictTypes = null,
public ?bool $allowExtraProperties = null,
) {
}
}
1 change: 1 addition & 0 deletions src/EventListener/MapperListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public function __invoke(GenerateMapperEvent $event): void
$event->constructorStrategy ??= $mapper->constructorStrategy;
$event->allowReadOnlyTargetToPopulate ??= $mapper->allowReadOnlyTargetToPopulate;
$event->strictTypes ??= $mapper->strictTypes;
$event->allowExtraProperties ??= $mapper->allowExtraProperties;
$event->mapperMetadata->dateTimeFormat = $mapper->dateTimeFormat;
}
}
24 changes: 0 additions & 24 deletions src/Extractor/FromSourceMappingExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@

namespace AutoMapper\Extractor;

use AutoMapper\Configuration;
use AutoMapper\Metadata\SourcePropertyMetadata;
use AutoMapper\Metadata\TargetPropertyMetadata;
use AutoMapper\Metadata\TypesMatching;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
use Symfony\Component\PropertyInfo\Type;

/**
Expand All @@ -24,15 +20,6 @@
*/
final class FromSourceMappingExtractor extends MappingExtractor
{
public function __construct(
Configuration $configuration,
PropertyInfoExtractorInterface $propertyInfoExtractor,
PropertyReadInfoExtractorInterface $readInfoExtractor,
PropertyWriteInfoExtractorInterface $writeInfoExtractor,
) {
parent::__construct($configuration, $propertyInfoExtractor, $readInfoExtractor, $writeInfoExtractor);
}

public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty): TypesMatching
{
$types = new TypesMatching();
Expand Down Expand Up @@ -90,15 +77,4 @@ private function transformType(string $target, ?Type $type = null): ?Type
$this->transformType($target, $collectionValueTypes[0] ?? null)
);
}

public function getWriteMutator(string $source, string $target, string $property, array $context = []): WriteMutator
{
$targetMutator = new WriteMutator(WriteMutator::TYPE_ARRAY_DIMENSION, $property, false);

if (\stdClass::class === $target) {
$targetMutator = new WriteMutator(WriteMutator::TYPE_PROPERTY, $property, false);
}

return $targetMutator;
}
}
13 changes: 0 additions & 13 deletions src/Extractor/FromTargetMappingExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,9 @@

namespace AutoMapper\Extractor;

use AutoMapper\Configuration;
use AutoMapper\Metadata\SourcePropertyMetadata;
use AutoMapper\Metadata\TargetPropertyMetadata;
use AutoMapper\Metadata\TypesMatching;
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
use Symfony\Component\PropertyInfo\Type;

/**
Expand All @@ -24,15 +20,6 @@
*/
final class FromTargetMappingExtractor extends MappingExtractor
{
public function __construct(
Configuration $configuration,
PropertyInfoExtractorInterface $propertyInfoExtractor,
PropertyReadInfoExtractorInterface $readInfoExtractor,
PropertyWriteInfoExtractorInterface $writeInfoExtractor,
) {
parent::__construct($configuration, $propertyInfoExtractor, $readInfoExtractor, $writeInfoExtractor);
}

public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty): TypesMatching
{
$types = new TypesMatching();
Expand Down
32 changes: 26 additions & 6 deletions src/Extractor/MappingExtractor.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private function getConstructorParameters(string $class): iterable
}
}

public function getReadAccessor(string $class, string $property): ?ReadAccessor
public function getReadAccessor(string $class, string $property, bool $allowExtraProperties = false): ?ReadAccessor
{
if ('array' === $class) {
return new ReadAccessor(ReadAccessor::TYPE_ARRAY_DIMENSION, $property);
Expand All @@ -83,6 +83,14 @@ public function getReadAccessor(string $class, string $property): ?ReadAccessor
$readInfo = $this->readInfoExtractor->getReadInfo($class, $property);

if (null === $readInfo) {
if ($allowExtraProperties) {
$implements = class_implements($class);

if ($implements !== false && \in_array(\ArrayAccess::class, $implements, true)) {
return new ReadAccessor(ReadAccessor::TYPE_ARRAY_ACCESS, $property);
}
}

return null;
}

Expand All @@ -101,15 +109,27 @@ public function getReadAccessor(string $class, string $property): ?ReadAccessor
);
}

public function getWriteMutator(string $source, string $target, string $property, array $context = []): ?WriteMutator
public function getWriteMutator(string $source, string $target, string $property, array $context = [], bool $allowExtraProperties = false): ?WriteMutator
{
$writeInfo = $this->writeInfoExtractor->getWriteInfo($target, $property, $context);

if (null === $writeInfo) {
return null;
}
if (null === $writeInfo || PropertyWriteInfo::TYPE_NONE === $writeInfo->getType()) {
if ('array' === $target) {
return new WriteMutator(WriteMutator::TYPE_ARRAY_DIMENSION, $property, false);
}

if (\stdClass::class === $target) {
return new WriteMutator(WriteMutator::TYPE_PROPERTY, $property, false);
}

if ($allowExtraProperties) {
$implements = class_implements($target);

if ($implements !== false && \in_array(\ArrayAccess::class, $implements, true)) {
return new WriteMutator(WriteMutator::TYPE_ARRAY_DIMENSION, $property, false);
}
}

if (PropertyWriteInfo::TYPE_NONE === $writeInfo->getType()) {
return null;
}

Expand Down
13 changes: 11 additions & 2 deletions src/Extractor/ReadAccessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ final class ReadAccessor
public const TYPE_PROPERTY = 2;
public const TYPE_ARRAY_DIMENSION = 3;
public const TYPE_SOURCE = 4;
public const TYPE_ARRAY_ACCESS = 5;

/**
* @param array<string, string> $context
Expand Down Expand Up @@ -138,7 +139,7 @@ public function getExpression(Expr\Variable $input): Expr
return new Expr\PropertyFetch($input, $this->accessor);
}

if (self::TYPE_ARRAY_DIMENSION === $this->type) {
if (self::TYPE_ARRAY_DIMENSION === $this->type || self::TYPE_ARRAY_ACCESS === $this->type) {
/*
* Use the array dim fetch to read the value
*
Expand Down Expand Up @@ -204,6 +205,10 @@ public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = fa
return new Expr\FuncCall(new Name('array_key_exists'), [new Arg(new Scalar\String_($this->accessor)), new Arg($input)]);
}

if (self::TYPE_ARRAY_ACCESS === $this->type) {
return new Expr\MethodCall($input, 'offsetExists', [new Arg(new Scalar\String_($this->accessor))]);
}

return null;
}

Expand Down Expand Up @@ -246,7 +251,7 @@ public function getIsNullExpression(Expr\Variable $input): Expr
return new Expr\BinaryOp\LogicalAnd(new Expr\BooleanNot(new Expr\Isset_([new Expr\PropertyFetch($input, $this->accessor)])), new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), new Expr\PropertyFetch($input, $this->accessor)));
}

if (self::TYPE_ARRAY_DIMENSION === $this->type) {
if (self::TYPE_ARRAY_DIMENSION === $this->type || self::TYPE_ARRAY_ACCESS === $this->type) {
/*
* Use the array dim fetch to read the value
*
Expand Down Expand Up @@ -308,6 +313,10 @@ public function getIsUndefinedExpression(Expr\Variable $input): Expr
return new Expr\BooleanNot(new Expr\FuncCall(new Name('array_key_exists'), [new Arg(new Scalar\String_($this->accessor)), new Arg($input)]));
}

if (self::TYPE_ARRAY_ACCESS === $this->type) {
return new Expr\BooleanNot(new Expr\MethodCall($input, 'offsetExists', [new Arg(new Scalar\String_($this->accessor))]));
}

throw new CompileException('Invalid accessor for read expression');
}

Expand Down
6 changes: 3 additions & 3 deletions src/Metadata/MetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
foreach ($propertyEvents as $propertyMappedEvent) {
// Create the source property metadata
if ($propertyMappedEvent->source->accessor === null) {
$propertyMappedEvent->source->accessor = $extractor->getReadAccessor($mapperMetadata->source, $propertyMappedEvent->source->property);
$propertyMappedEvent->source->accessor = $extractor->getReadAccessor($mapperMetadata->source, $propertyMappedEvent->source->property, $mapperEvent->allowExtraProperties ?? $this->configuration->allowExtraProperties);
}

if ($propertyMappedEvent->source->checkExists === null) {
Expand All @@ -239,13 +239,13 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera

// Create the target property metadata
if ($propertyMappedEvent->target->readAccessor === null) {
$propertyMappedEvent->target->readAccessor = $extractor->getReadAccessor($mapperMetadata->target, $propertyMappedEvent->target->property);
$propertyMappedEvent->target->readAccessor = $extractor->getReadAccessor($mapperMetadata->target, $propertyMappedEvent->target->property, $mapperEvent->allowExtraProperties ?? $this->configuration->allowExtraProperties);
}

if ($propertyMappedEvent->target->writeMutator === null) {
$propertyMappedEvent->target->writeMutator = $extractor->getWriteMutator($mapperMetadata->source, $mapperMetadata->target, $propertyMappedEvent->target->property, [
'enable_constructor_extraction' => false,
]);
], $mapperEvent->allowExtraProperties ?? $this->configuration->allowExtraProperties);
}

if ($propertyMappedEvent->target->parameterInConstructor === null) {
Expand Down
9 changes: 9 additions & 0 deletions tests/AutoMapperTest/ArrayAccess/expected.1.data
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
AutoMapper\Tests\AutoMapperTest\ArrayAccess\LikeArray {
storage: [
"bar" => 2
"foo" => "foo"
]
flag::STD_PROP_LIST: false
flag::ARRAY_AS_PROPS: false
iteratorClass: "ArrayIterator"
}
4 changes: 4 additions & 0 deletions tests/AutoMapperTest/ArrayAccess/expected.data
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
AutoMapper\Tests\AutoMapperTest\ArrayAccess\Foo {
+foo: "foofoo"
+bar: 10
}
26 changes: 26 additions & 0 deletions tests/AutoMapperTest/ArrayAccess/map.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Tests\AutoMapperTest\ArrayAccess;

use AutoMapper\Attribute\Mapper;
use AutoMapper\Tests\AutoMapperBuilder;

class LikeArray extends \ArrayObject
{
}

#[Mapper(source: LikeArray::class, target: LikeArray::class, allowExtraProperties: true)]
class Foo
{
public string $foo = 'foo';
public int $bar = 2;
}

return (function () {
$autoMapper = AutoMapperBuilder::buildAutoMapper();

yield $autoMapper->map(new LikeArray(['foo' => 'foofoo', 'bar' => 10]), Foo::class);
yield $autoMapper->map(new Foo(), LikeArray::class);
})();