Skip to content

Commit 567af53

Browse files
authored
feat(array-access): allow to map extra properties (#251)
Replace #144 Basically this allow to map to or from an object implementing the ArrayAccess interface
2 parents 40a53e8 + 3d348d9 commit 567af53

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 ($implements !== false && \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 ($implements !== false && \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
@@ -223,7 +223,7 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
223223
foreach ($propertyEvents as $propertyMappedEvent) {
224224
// Create the source property metadata
225225
if ($propertyMappedEvent->source->accessor === null) {
226-
$propertyMappedEvent->source->accessor = $extractor->getReadAccessor($mapperMetadata->source, $propertyMappedEvent->source->property);
226+
$propertyMappedEvent->source->accessor = $extractor->getReadAccessor($mapperMetadata->source, $propertyMappedEvent->source->property, $mapperEvent->allowExtraProperties ?? $this->configuration->allowExtraProperties);
227227
}
228228

229229
if ($propertyMappedEvent->source->checkExists === null) {
@@ -240,13 +240,13 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
240240

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

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

252252
if ($propertyMappedEvent->target->parameterInConstructor === null) {
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
AutoMapper\Tests\AutoMapperTest\ArrayAccess\LikeArray {
2+
storage: [
3+
"bar" => 2
4+
"foo" => "foo"
5+
]
6+
flag::STD_PROP_LIST: false
7+
flag::ARRAY_AS_PROPS: false
8+
iteratorClass: "ArrayIterator"
9+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
AutoMapper\Tests\AutoMapperTest\ArrayAccess\Foo {
2+
+foo: "foofoo"
3+
+bar: 10
4+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AutoMapper\Tests\AutoMapperTest\ArrayAccess;
6+
7+
use AutoMapper\Attribute\Mapper;
8+
use AutoMapper\Tests\AutoMapperBuilder;
9+
10+
class LikeArray extends \ArrayObject
11+
{
12+
}
13+
14+
#[Mapper(source: LikeArray::class, target: LikeArray::class, allowExtraProperties: true)]
15+
class Foo
16+
{
17+
public string $foo = 'foo';
18+
public int $bar = 2;
19+
}
20+
21+
return (function () {
22+
$autoMapper = AutoMapperBuilder::buildAutoMapper();
23+
24+
yield $autoMapper->map(new LikeArray(['foo' => 'foofoo', 'bar' => 10]), Foo::class);
25+
yield $autoMapper->map(new Foo(), LikeArray::class);
26+
})();

0 commit comments

Comments
 (0)