Skip to content

Commit 6b0926a

Browse files
committed
feat(nested): split dot properties for nested
1 parent d029a85 commit 6b0926a

4 files changed

Lines changed: 201 additions & 28 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Add a castor task to serve the symfony app in tests for debugging purpose.
1414
- [GH#304](https://github.com/jolicode/automapper/pull/304) ; Allow to override source and/or target property type
1515
- Add support for static callable in attribute transformer
16+
- Initial support for nested properties
1617

1718
### Changed
1819
- [BC Break] `PropertyTransformerSupportInterface` does not use a `TypesMatching` anymore, you can get the type directly from `SourcePropertyMetadata` or `TargetPropertyMetadata`.

src/Extractor/MappingExtractor.php

Lines changed: 143 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface;
1313
use Symfony\Component\PropertyInfo\PropertyWriteInfo;
1414
use Symfony\Component\PropertyInfo\PropertyWriteInfoExtractorInterface;
15+
use Symfony\Component\TypeInfo\Type;
16+
use Symfony\Component\TypeInfo\TypeIdentifier;
1517

1618
/**
1719
* @internal
@@ -74,6 +76,142 @@ private function getConstructorParameters(string $class): iterable
7476
}
7577

7678
public function getReadAccessor(string $class, string $property, bool $allowExtraProperties = false): ?ReadAccessorInterface
79+
{
80+
// split to first dot for nested properties
81+
$exploded = explode('.', $property, 2);
82+
83+
if (2 === \count($exploded)) {
84+
[$parent, $child] = $exploded;
85+
} else {
86+
return $this->doGetReadAccessor($class, $property, $allowExtraProperties);
87+
}
88+
89+
if ('array' === $class) {
90+
$type = Type::array();
91+
} elseif (\stdClass::class === $class) {
92+
$type = Type::object(\stdClass::class);
93+
} else {
94+
$type = $this->sourceTypeExtractor->getType($class, $parent);
95+
}
96+
97+
$parentAccessor = $this->doGetReadAccessor($class, $parent, $allowExtraProperties);
98+
99+
if ($type instanceof Type\ObjectType) {
100+
/** @var class-string $childClass */
101+
$childClass = $type->getClassName();
102+
$childAccessor = $this->getReadAccessor($childClass, $child, $allowExtraProperties);
103+
} elseif ($type?->isIdentifiedBy(TypeIdentifier::ARRAY)) {
104+
$childAccessor = $this->getReadAccessor('array', $child, $allowExtraProperties);
105+
} else {
106+
return null;
107+
}
108+
109+
if (null === $parentAccessor || null === $childAccessor) {
110+
return null;
111+
}
112+
113+
return new NestedReadAccessor($parentAccessor, $childAccessor);
114+
}
115+
116+
public function getWriteMutator(string $source, string $target, string $property, array $context = [], bool $allowExtraProperties = false): ?WriteMutatorInterface
117+
{
118+
// split to last dot for nested properties
119+
$lastDotPosition = strrpos($property, '.');
120+
121+
if (false === $lastDotPosition) {
122+
return $this->doGetWriteMutator($target, $property, $context, $allowExtraProperties);
123+
}
124+
125+
$parent = substr($property, 0, $lastDotPosition);
126+
$child = substr($property, $lastDotPosition + 1);
127+
$accessor = $this->getReadAccessor($target, $parent, $allowExtraProperties);
128+
129+
if ($target === 'array') {
130+
$lastAccessorType = Type::array();
131+
} elseif (\stdClass::class === $target) {
132+
$lastAccessorType = Type::object(\stdClass::class);
133+
} else {
134+
$lastAccessorType = $this->findLastTypeForAccessor($target, $parent);
135+
}
136+
137+
if (null === $lastAccessorType) {
138+
return null;
139+
}
140+
141+
if ($lastAccessorType instanceof Type\ObjectType) {
142+
/** @var class-string|'array' $targetClass */
143+
$targetClass = $lastAccessorType->getClassName();
144+
} elseif ($lastAccessorType->isIdentifiedBy(TypeIdentifier::ARRAY)) {
145+
$targetClass = 'array';
146+
} else {
147+
return null;
148+
}
149+
150+
$mutator = $this->doGetWriteMutator($targetClass, $child, $context, $allowExtraProperties);
151+
152+
if (null === $accessor || null === $mutator) {
153+
return null;
154+
}
155+
156+
return new NestedWriteMutator($accessor, $mutator);
157+
}
158+
159+
public function getCheckExists(string $class, string $property): bool
160+
{
161+
if ('array' === $class || \stdClass::class === $class) {
162+
return true;
163+
}
164+
165+
return false;
166+
}
167+
168+
public function getGroups(string $class, string $property): ?array
169+
{
170+
return null;
171+
}
172+
173+
public function getDateTimeFormat(PropertyMetadataEvent $propertyMetadataEvent): string
174+
{
175+
if (null !== $propertyMetadataEvent->dateTimeFormat) {
176+
return $propertyMetadataEvent->dateTimeFormat;
177+
}
178+
179+
if (null !== $propertyMetadataEvent->mapperMetadata->dateTimeFormat) {
180+
return $propertyMetadataEvent->mapperMetadata->dateTimeFormat;
181+
}
182+
183+
return $this->configuration->dateTimeFormat;
184+
}
185+
186+
/**
187+
* @param class-string $source
188+
*/
189+
private function findLastTypeForAccessor(string $source, string $property): ?Type
190+
{
191+
$exploded = explode('.', $property, 2);
192+
193+
if (2 === \count($exploded)) {
194+
[$parent, $child] = $exploded;
195+
} else {
196+
return $this->sourceTypeExtractor->getType($source, $property);
197+
}
198+
199+
$parentType = $this->sourceTypeExtractor->getType($source, $parent);
200+
201+
if ($parentType instanceof Type\ObjectType) {
202+
/** @var class-string $className */
203+
$className = $parentType->getClassName();
204+
205+
return $this->findLastTypeForAccessor($className, $child);
206+
}
207+
208+
return $parentType;
209+
}
210+
211+
/**
212+
* @param class-string|'array' $class
213+
*/
214+
private function doGetReadAccessor(string $class, string $property, bool $allowExtraProperties = false): ?ReadAccessorInterface
77215
{
78216
if ('array' === $class) {
79217
return new ArrayReadAccessor($property);
@@ -107,7 +245,11 @@ public function getReadAccessor(string $class, string $property, bool $allowExtr
107245
);
108246
}
109247

110-
public function getWriteMutator(string $source, string $target, string $property, array $context = [], bool $allowExtraProperties = false): ?WriteMutatorInterface
248+
/**
249+
* @param class-string|'array' $target
250+
* @param array<string, mixed> $context
251+
*/
252+
private function doGetWriteMutator(string $target, string $property, array $context = [], bool $allowExtraProperties = false): ?WriteMutatorInterface
111253
{
112254
$writeInfo = $this->writeInfoExtractor->getWriteInfo($target, $property, $context);
113255

@@ -147,31 +289,4 @@ public function getWriteMutator(string $source, string $target, string $property
147289

148290
return new PropertyWriteMutator($writeInfo->getName(), PropertyReadInfo::VISIBILITY_PUBLIC !== $writeInfo->getVisibility());
149291
}
150-
151-
public function getCheckExists(string $class, string $property): bool
152-
{
153-
if ('array' === $class || \stdClass::class === $class) {
154-
return true;
155-
}
156-
157-
return false;
158-
}
159-
160-
public function getGroups(string $class, string $property): ?array
161-
{
162-
return null;
163-
}
164-
165-
public function getDateTimeFormat(PropertyMetadataEvent $propertyMetadataEvent): string
166-
{
167-
if (null !== $propertyMetadataEvent->dateTimeFormat) {
168-
return $propertyMetadataEvent->dateTimeFormat;
169-
}
170-
171-
if (null !== $propertyMetadataEvent->mapperMetadata->dateTimeFormat) {
172-
return $propertyMetadataEvent->mapperMetadata->dateTimeFormat;
173-
}
174-
175-
return $this->configuration->dateTimeFormat;
176-
}
177292
}

src/Extractor/MappingExtractorInterface.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,16 @@ public function getCheckExists(string $class, string $property): bool;
4646

4747
/**
4848
* Extracts read accessor for a given source, target and property.
49+
*
50+
* @param class-string|'array' $class
4951
*/
5052
public function getReadAccessor(string $class, string $property): ?ReadAccessorInterface;
5153

5254
/**
5355
* Extracts write mutator for a given source, target and property.
5456
*
57+
* @param class-string|'array' $source
58+
* @param class-string|'array' $target
5559
* @param array<string, mixed> $context
5660
*/
5761
public function getWriteMutator(string $source, string $target, string $property, array $context = []): ?WriteMutatorInterface;
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AutoMapper\Extractor;
6+
7+
use PhpParser\Node\Expr;
8+
9+
final readonly class NestedReadAccessor implements ReadAccessorInterface
10+
{
11+
public function __construct(
12+
public ReadAccessorInterface $parent,
13+
public ReadAccessorInterface $child,
14+
) {
15+
}
16+
17+
public function getExpression(Expr $input, bool $target = false): Expr
18+
{
19+
$parentExpr = $this->parent->getExpression($input, $target);
20+
21+
return $this->child->getExpression($parentExpr, $target);
22+
}
23+
24+
public function getIsDefinedExpression(Expr\Variable $input, bool $nullable = false, bool $target = false): ?Expr
25+
{
26+
return null;
27+
}
28+
29+
public function getIsNullExpression(Expr\Variable $input, bool $target = false): Expr
30+
{
31+
return $this->parent->getIsNullExpression($input, $target);
32+
}
33+
34+
public function getIsUndefinedExpression(Expr\Variable $input, bool $target = false): Expr
35+
{
36+
return $this->parent->getIsNullExpression($input, $target);
37+
}
38+
39+
public function getExtractCallback(string $className): ?Expr
40+
{
41+
return null;
42+
}
43+
44+
public function getExtractIsNullCallback(string $className): ?Expr
45+
{
46+
return null;
47+
}
48+
49+
public function getExtractIsUndefinedCallback(string $className): ?Expr
50+
{
51+
return null;
52+
}
53+
}

0 commit comments

Comments
 (0)