Skip to content

Commit 588da23

Browse files
authored
Better error reporting of missing 'typePropery' when using DiscriminatorMap (#257)
2 parents f1b8d9d + 91ab23c commit 588da23

File tree

9 files changed

+156
-11
lines changed

9 files changed

+156
-11
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AutoMapper\Exception;
6+
7+
/**
8+
* @author Joel Wurtz <[email protected]>
9+
*/
10+
final class CannotCreateTargetException extends RuntimeException
11+
{
12+
}

src/Generator/CreateTargetStatementsGenerator.php

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ public function __construct(
4040
* ... // create object statements
4141
* }
4242
* ```
43+
*
44+
* @return list<Stmt>
4345
*/
44-
public function generate(GeneratorMetadata $metadata, VariableRegistry $variableRegistry): Stmt
46+
public function generate(GeneratorMetadata $metadata, VariableRegistry $variableRegistry): array
4547
{
4648
$createObjectStatements = [];
4749

@@ -56,9 +58,18 @@ public function generate(GeneratorMetadata $metadata, VariableRegistry $variable
5658

5759
$createObjectStatements = array_values(array_filter($createObjectStatements));
5860

59-
return new Stmt\If_(new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), $variableRegistry->getResult()), [
60-
'stmts' => $createObjectStatements,
61-
]);
61+
if ($this->canUseTargetToPopulate($metadata)) {
62+
return [new Stmt\If_(new Expr\BinaryOp\Identical(new Expr\ConstFetch(new Name('null')), $variableRegistry->getResult()), [
63+
'stmts' => $createObjectStatements,
64+
])];
65+
}
66+
67+
return $createObjectStatements;
68+
}
69+
70+
public function canUseTargetToPopulate(GeneratorMetadata $metadata): bool
71+
{
72+
return !$this->discriminatorStatementsGeneratorTarget->supports($metadata);
6273
}
6374

6475
private function targetAsArray(GeneratorMetadata $metadata): ?Stmt
@@ -346,6 +357,10 @@ private function constructorWithoutArgument(GeneratorMetadata $metadata): ?Stmt
346357
return null;
347358
}
348359

360+
if ($metadata->mapperMetadata->targetReflectionClass?->isInstantiable() === false) {
361+
return null;
362+
}
363+
349364
return new Stmt\Expression(new Expr\Assign($metadata->variableRegistry->getResult(), new Expr\New_(new Name\FullyQualified($metadata->mapperMetadata->target))));
350365
}
351366
}

src/Generator/MapMethodStatementsGenerator.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,13 @@ public function getStatements(GeneratorMetadata $metadata): array
5252

5353
$statements = [$this->ifSourceIsNullReturnNull($metadata)];
5454
$statements = [...$statements, ...$this->handleCircularReference($metadata)];
55-
$statements = [...$statements, ...$this->initializeTargetToPopulate($metadata)];
56-
$statements = [...$statements, ...$this->initializeTargetFromProvider($metadata)];
57-
$statements[] = $this->createObjectStatementsGenerator->generate($metadata, $variableRegistry);
55+
56+
if ($this->createObjectStatementsGenerator->canUseTargetToPopulate($metadata)) {
57+
$statements = [...$statements, ...$this->initializeTargetToPopulate($metadata)];
58+
$statements = [...$statements, ...$this->initializeTargetFromProvider($metadata)];
59+
}
60+
61+
$statements = [...$statements, ...$this->createObjectStatementsGenerator->generate($metadata, $variableRegistry)];
5862

5963
$addedDependenciesStatements = $this->handleDependencies($metadata);
6064

src/Generator/Shared/DiscriminatorStatementsGenerator.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace AutoMapper\Generator\Shared;
66

7+
use AutoMapper\Exception\CannotCreateTargetException;
78
use AutoMapper\Metadata\GeneratorMetadata;
89
use AutoMapper\Transformer\TransformerInterface;
910
use PhpParser\Node\Arg;
@@ -85,7 +86,7 @@ public function createTargetStatements(GeneratorMetadata $metadata): array
8586

8687
// Generate the code that allows to put the type into the output variable,
8788
// so we are able to determine which mapper to use
88-
[$output, $createObjectStatements] = $propertyMetadata->transformer->transform(
89+
[$output, $discriminateStatements] = $propertyMetadata->transformer->transform(
8990
$fieldValueExpr,
9091
$variableRegistry->getResult(),
9192
$propertyMetadata,
@@ -94,7 +95,7 @@ public function createTargetStatements(GeneratorMetadata $metadata): array
9495
);
9596

9697
foreach ($this->classDiscriminatorResolver->discriminatorMapperNamesIndexedByTypeValue($metadata, $this->fromSource) as $typeValue => $discriminatorMapperName) {
97-
$createObjectStatements[] = new Stmt\If_(
98+
$discriminateStatements[] = new Stmt\If_(
9899
new Expr\BinaryOp\Identical(new Scalar\String_($typeValue), $output),
99100
[
100101
'stmts' => [
@@ -116,10 +117,29 @@ public function createTargetStatements(GeneratorMetadata $metadata): array
116117
);
117118
}
118119

119-
return $createObjectStatements;
120+
$isDefinedExpression = $propertyMetadata->source->accessor?->getIsDefinedExpression($variableRegistry->getSourceInput());
121+
122+
if (!$isDefinedExpression) {
123+
return $discriminateStatements;
124+
}
125+
$cannotCreateTarget = !($metadata->mapperMetadata->targetReflectionClass === null) && !$metadata->mapperMetadata->targetReflectionClass->isInstantiable();
126+
127+
$if = new Stmt\If_($isDefinedExpression, [
128+
'stmts' => $discriminateStatements,
129+
]);
130+
131+
$statements = [$if];
132+
133+
if ($cannotCreateTarget) {
134+
$statements[] = new Stmt\Expression(new Expr\Throw_(new Expr\New_(new Name(CannotCreateTargetException::class), [
135+
new Arg(new Scalar\String_('Cannot create target object, because the target is abstract or an interface, and the property "' . $propertyMetadata->source->property . '" is not defined, or the value does not match any discriminator type.')),
136+
])));
137+
}
138+
139+
return $statements;
120140
}
121141

122-
private function supports(GeneratorMetadata $metadata): bool
142+
public function supports(GeneratorMetadata $metadata): bool
123143
{
124144
if (!$this->classDiscriminatorResolver->hasClassDiscriminator($metadata, $this->fromSource)) {
125145
return false;

src/Metadata/GeneratorMetadata.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ public function hasConstructor(): bool
8989
return false;
9090
}
9191

92+
if ($constructor->getDeclaringClass()->isInterface() || $constructor->getDeclaringClass()->isAbstract()) {
93+
return false;
94+
}
95+
9296
$parameters = $constructor->getParameters();
9397
$mandatoryParameters = [];
9498

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
AutoMapper\Exception\CannotCreateTargetException {
2+
+class: "AutoMapper\Exception\CannotCreateTargetException"
3+
+message: "Cannot create target object, because the target is abstract or an interface, and the property "linkType" is not defined, or the value does not match any discriminator type."
4+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AutoMapper\Tests\AutoMapperTest\DiscriminatorMapBadConfiguration;
6+
7+
use AutoMapper\Tests\AutoMapperBuilder;
8+
use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
9+
10+
#[DiscriminatorMap('linkType', [
11+
'default' => DefaultLinkType::class,
12+
])]
13+
abstract class LinkType
14+
{
15+
}
16+
17+
class DefaultLinkType extends LinkType
18+
{
19+
public function __construct(
20+
public string $link,
21+
) {
22+
}
23+
}
24+
25+
$source = [
26+
// There is a typo on the key `type`.
27+
// It should be `linkType` instead of `type`.
28+
'type' => 'default',
29+
'link' => 'https://example.com',
30+
];
31+
32+
try {
33+
return AutoMapperBuilder::buildAutoMapper()->map($source, LinkType::class);
34+
} catch (\Throwable $th) {
35+
return $th;
36+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
AutoMapper\Tests\AutoMapperTest\DiscriminatorPopulate\LinkWrapper {
2+
+link: AutoMapper\Tests\AutoMapperTest\DiscriminatorPopulate\DefaultLinkType {
3+
+link: "https://example.com/new"
4+
+description: "description"
5+
}
6+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AutoMapper\Tests\AutoMapperTest\DiscriminatorPopulate;
6+
7+
use AutoMapper\MapperContext;
8+
use AutoMapper\Tests\AutoMapperBuilder;
9+
use Symfony\Component\Serializer\Attribute\DiscriminatorMap;
10+
11+
#[DiscriminatorMap('linkType', [
12+
'default' => DefaultLinkType::class,
13+
])]
14+
abstract class LinkType
15+
{
16+
}
17+
18+
class DefaultLinkType extends LinkType
19+
{
20+
public function __construct(
21+
public string $link,
22+
public string $description = 'default',
23+
) {
24+
}
25+
}
26+
27+
class LinkWrapper
28+
{
29+
public function __construct(
30+
public LinkType $link,
31+
) {
32+
}
33+
}
34+
35+
$source = [
36+
'link' => [
37+
'linkType' => 'default',
38+
'link' => 'https://example.com/new',
39+
],
40+
];
41+
42+
$existingData = new LinkWrapper(new DefaultLinkType('https://example.com/old', 'description'));
43+
44+
return AutoMapperBuilder::buildAutoMapper()->map($source, $existingData, [MapperContext::DEEP_TARGET_TO_POPULATE => true]);

0 commit comments

Comments
 (0)