Skip to content

Commit b6ae193

Browse files
committed
feat(array-access): allow to map extra properties
1 parent d6a0c59 commit b6ae193

File tree

13 files changed

+91
-48
lines changed

13 files changed

+91
-48
lines changed

docs/getting-started/configuration.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,8 @@ Setting this to false will make the `AutoMapper` throw an exception if the mappe
5757

5858
This can be useful if you want to pre generate all the mappers and have tests to ensure that all the mappers are
5959
generated.
60+
61+
* `allowExtraProperties` (default: `false`)
62+
63+
Settings this to true will allow the mapper to map extra properties from the source object to the target object when
64+
the source or the target implements the `ArrayAccess` interface.

src/Attribute/Mapper.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public function __construct(
2626
public ?bool $strictTypes = null,
2727
public int $priority = 0,
2828
public ?string $dateTimeFormat = null,
29+
public ?bool $allowExtraProperties = null,
2930
) {
3031
}
3132
}

src/Configuration.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public function __construct(
4646
* The strategy to use to generate the mappers between each request.
4747
*/
4848
public FileReloadStrategy $reloadStrategy = FileReloadStrategy::ON_CHANGE,
49+
/**
50+
* Allow extra properties to be mapped when the target or the source implements ArrayAccess class.
51+
*/
52+
public bool $allowExtraProperties = false,
4953
) {
5054
}
5155
}

src/Event/GenerateMapperEvent.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public function __construct(
2323
public ?ConstructorStrategy $constructorStrategy = null,
2424
public ?bool $allowReadOnlyTargetToPopulate = null,
2525
public ?bool $strictTypes = null,
26+
public ?bool $allowExtraProperties = null,
2627
) {
2728
}
2829
}

src/EventListener/MapperListener.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ public function __invoke(GenerateMapperEvent $event): void
7373
$event->constructorStrategy ??= $mapper->constructorStrategy;
7474
$event->allowReadOnlyTargetToPopulate ??= $mapper->allowReadOnlyTargetToPopulate;
7575
$event->strictTypes ??= $mapper->strictTypes;
76+
$event->allowExtraProperties ??= $mapper->allowExtraProperties;
7677
$event->mapperMetadata->dateTimeFormat = $mapper->dateTimeFormat;
7778
}
7879
}

src/Extractor/FromSourceMappingExtractor.php

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,9 @@
44

55
namespace AutoMapper\Extractor;
66

7-
use AutoMapper\Configuration;
87
use AutoMapper\Metadata\SourcePropertyMetadata;
98
use AutoMapper\Metadata\TargetPropertyMetadata;
109
use AutoMapper\Metadata\TypesMatching;
11-
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
12-
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
13-
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
1410
use Symfony\Component\PropertyInfo\Type;
1511

1612
/**
@@ -24,15 +20,6 @@
2420
*/
2521
final class FromSourceMappingExtractor extends MappingExtractor
2622
{
27-
public function __construct(
28-
Configuration $configuration,
29-
PropertyInfoExtractorInterface $propertyInfoExtractor,
30-
PropertyReadInfoExtractorInterface $readInfoExtractor,
31-
PropertyWriteInfoExtractorInterface $writeInfoExtractor,
32-
) {
33-
parent::__construct($configuration, $propertyInfoExtractor, $readInfoExtractor, $writeInfoExtractor);
34-
}
35-
3623
public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty): TypesMatching
3724
{
3825
$types = new TypesMatching();
@@ -90,15 +77,4 @@ private function transformType(string $target, ?Type $type = null): ?Type
9077
$this->transformType($target, $collectionValueTypes[0] ?? null)
9178
);
9279
}
93-
94-
public function getWriteMutator(string $source, string $target, string $property, array $context = []): WriteMutator
95-
{
96-
$targetMutator = new WriteMutator(WriteMutator::TYPE_ARRAY_DIMENSION, $property, false);
97-
98-
if (\stdClass::class === $target) {
99-
$targetMutator = new WriteMutator(WriteMutator::TYPE_PROPERTY, $property, false);
100-
}
101-
102-
return $targetMutator;
103-
}
10480
}

src/Extractor/FromTargetMappingExtractor.php

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,9 @@
44

55
namespace AutoMapper\Extractor;
66

7-
use AutoMapper\Configuration;
87
use AutoMapper\Metadata\SourcePropertyMetadata;
98
use AutoMapper\Metadata\TargetPropertyMetadata;
109
use AutoMapper\Metadata\TypesMatching;
11-
use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface;
12-
use Symfony\Component\PropertyInfo\PropertyReadInfoExtractorInterface;
13-
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
1410
use Symfony\Component\PropertyInfo\Type;
1511

1612
/**
@@ -24,15 +20,6 @@
2420
*/
2521
final class FromTargetMappingExtractor extends MappingExtractor
2622
{
27-
public function __construct(
28-
Configuration $configuration,
29-
PropertyInfoExtractorInterface $propertyInfoExtractor,
30-
PropertyReadInfoExtractorInterface $readInfoExtractor,
31-
PropertyWriteInfoExtractorInterface $writeInfoExtractor,
32-
) {
33-
parent::__construct($configuration, $propertyInfoExtractor, $readInfoExtractor, $writeInfoExtractor);
34-
}
35-
3623
public function getTypes(string $source, SourcePropertyMetadata $sourceProperty, string $target, TargetPropertyMetadata $targetProperty): TypesMatching
3724
{
3825
$types = new TypesMatching();

src/Extractor/MappingExtractor.php

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ private function getConstructorParameters(string $class): iterable
7070
}
7171
}
7272

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

8585
if (null === $readInfo) {
86+
if ($allowExtraProperties) {
87+
$implements = class_implements($class);
88+
89+
if (\in_array(\ArrayAccess::class, $implements, true)) {
90+
return new ReadAccessor(ReadAccessor::TYPE_ARRAY_ACCESS, $property);
91+
}
92+
}
93+
8694
return null;
8795
}
8896

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

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

108-
if (null === $writeInfo) {
109-
return null;
110-
}
116+
if (null === $writeInfo || PropertyWriteInfo::TYPE_NONE === $writeInfo->getType()) {
117+
if ('array' === $target) {
118+
return new WriteMutator(WriteMutator::TYPE_ARRAY_DIMENSION, $property, false);
119+
}
120+
121+
if (\stdClass::class === $target) {
122+
return new WriteMutator(WriteMutator::TYPE_PROPERTY, $property, false);
123+
}
124+
125+
if ($allowExtraProperties) {
126+
$implements = class_implements($target);
127+
128+
if (\in_array(\ArrayAccess::class, $implements, true)) {
129+
return new WriteMutator(WriteMutator::TYPE_ARRAY_DIMENSION, $property, false);
130+
}
131+
}
111132

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

src/Extractor/ReadAccessor.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ final class ReadAccessor
3030
public const TYPE_PROPERTY = 2;
3131
public const TYPE_ARRAY_DIMENSION = 3;
3232
public const TYPE_SOURCE = 4;
33+
public const TYPE_ARRAY_ACCESS = 5;
3334

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

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

208+
if (self::TYPE_ARRAY_ACCESS === $this->type) {
209+
return new Expr\MethodCall($input, 'offsetExists', [new Arg(new Scalar\String_($this->accessor))]);
210+
}
211+
207212
return null;
208213
}
209214

@@ -246,7 +251,7 @@ public function getIsNullExpression(Expr\Variable $input): Expr
246251
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)));
247252
}
248253

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

316+
if (self::TYPE_ARRAY_ACCESS === $this->type) {
317+
return new Expr\BooleanNot(new Expr\MethodCall($input, 'offsetExists', [new Arg(new Scalar\String_($this->accessor))]));
318+
}
319+
311320
throw new CompileException('Invalid accessor for read expression');
312321
}
313322

src/Metadata/MetadataFactory.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
222222
foreach ($propertyEvents as $propertyMappedEvent) {
223223
// Create the source property metadata
224224
if ($propertyMappedEvent->source->accessor === null) {
225-
$propertyMappedEvent->source->accessor = $extractor->getReadAccessor($mapperMetadata->source, $propertyMappedEvent->source->property);
225+
$propertyMappedEvent->source->accessor = $extractor->getReadAccessor($mapperMetadata->source, $propertyMappedEvent->source->property, $mapperEvent->allowExtraProperties ?? $this->configuration->allowExtraProperties);
226226
}
227227

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

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

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

251251
if ($propertyMappedEvent->target->parameterInConstructor === null) {

0 commit comments

Comments
 (0)