Skip to content

Commit 2ca95e9

Browse files
committed
feat(discriminator): better handling of discriminator
1 parent f388df0 commit 2ca95e9

File tree

11 files changed

+167
-21
lines changed

11 files changed

+167
-21
lines changed

Diff for: castor.php

+23
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,26 @@ function qa_phpstan(bool $generateBaseline = false)
3333

3434
phpstan($params, '1.11.1');
3535
}
36+
37+
#[AsTask('mapper', namespace: 'debug', description: 'Debug a mapper', aliases: ['debug'])]
38+
function debug_mapper(string $source, string $target)
39+
{
40+
require_once __DIR__ . '/vendor/autoload.php';
41+
42+
$automapper = AutoMapper\AutoMapper::create();
43+
// get private property loader value
44+
$loader = new ReflectionProperty($automapper, 'classLoader');
45+
$loader = $loader->getValue($automapper);
46+
47+
// get metadata factory
48+
$metadataFactory = new ReflectionProperty($loader, 'metadataFactory');
49+
$metadataFactory = $metadataFactory->getValue($loader);
50+
51+
$command = new AutoMapper\Symfony\Bundle\Command\DebugMapperCommand($metadataFactory);
52+
$input = new Symfony\Component\Console\Input\ArrayInput([
53+
'source' => $source,
54+
'target' => $target,
55+
]);
56+
57+
$command->run($input, \Castor\output());
58+
}
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AutoMapper\EventListener\Symfony;
6+
7+
use AutoMapper\Event\GenerateMapperEvent;
8+
use AutoMapper\Event\PropertyMetadataEvent;
9+
use AutoMapper\Event\SourcePropertyMetadata;
10+
use AutoMapper\Event\TargetPropertyMetadata;
11+
use AutoMapper\Transformer\FixedValueTransformer;
12+
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorMapping;
13+
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorResolverInterface;
14+
15+
/**
16+
* @internal
17+
*/
18+
final readonly class ClassDiscriminatorListener
19+
{
20+
public function __construct(
21+
private ClassDiscriminatorResolverInterface $classDiscriminator,
22+
) {
23+
}
24+
25+
public function __invoke(GenerateMapperEvent $event): void
26+
{
27+
$classDiscriminatorMappingSource = $this->getMappingForClass($event->mapperMetadata->source);
28+
$classDiscriminatorMappingTarget = $this->getMappingForClass($event->mapperMetadata->target);
29+
30+
if ($classDiscriminatorMappingSource) {
31+
$sourceType = null;
32+
33+
foreach ($classDiscriminatorMappingSource->getTypesMapping() as $type => $class) {
34+
if ($class === $event->mapperMetadata->source) {
35+
$sourceType = $type;
36+
break;
37+
}
38+
}
39+
40+
$property = $classDiscriminatorMappingSource->getTypeProperty();
41+
$sourceProperty = new SourcePropertyMetadata($property);
42+
$targetProperty = new TargetPropertyMetadata($property);
43+
44+
$event->properties[$property] = new PropertyMetadataEvent(
45+
mapperMetadata: $event->mapperMetadata,
46+
source: $sourceProperty,
47+
target: $targetProperty,
48+
transformer: $sourceType ? new FixedValueTransformer($sourceType) : null,
49+
);
50+
}
51+
52+
if ($classDiscriminatorMappingTarget) {
53+
$property = $classDiscriminatorMappingTarget->getTypeProperty();
54+
$sourceProperty = new SourcePropertyMetadata($property);
55+
$targetProperty = new TargetPropertyMetadata($property);
56+
57+
$event->properties[$property] = new PropertyMetadataEvent(
58+
mapperMetadata: $event->mapperMetadata,
59+
source: $sourceProperty,
60+
target: $targetProperty,
61+
);
62+
}
63+
}
64+
65+
private function getMappingForClass(string $class): ?ClassDiscriminatorMapping
66+
{
67+
if (!class_exists($class) && !interface_exists($class)) {
68+
return null;
69+
}
70+
71+
$mapping = $this->classDiscriminator->getMappingForClass($class);
72+
73+
if ($mapping) {
74+
return $mapping;
75+
}
76+
77+
$reflectionClass = new \ReflectionClass($class);
78+
79+
// Include metadata from the parent class
80+
if ($parent = $reflectionClass->getParentClass()) {
81+
$mapping = $this->getMappingForClass($parent->name);
82+
83+
if ($mapping) {
84+
return $mapping;
85+
}
86+
}
87+
88+
// Include metadata from all implemented interfaces
89+
foreach ($reflectionClass->getInterfaces() as $interface) {
90+
if ($mapping = $this->getMappingForClass($interface->name)) {
91+
return $mapping;
92+
}
93+
}
94+
95+
return null;
96+
}
97+
}

Diff for: src/Extractor/ReadAccessor.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ public function getExtractIsUndefinedCallback(string $className): ?Expr
420420
*/
421421
public function getTypes(string $class): ?array
422422
{
423-
if (self::TYPE_METHOD === $this->type && class_exists($class)) {
423+
if (self::TYPE_METHOD === $this->type && (class_exists($class) || interface_exists($class))) {
424424
try {
425425
$reflectionMethod = new \ReflectionMethod($class, $this->accessor);
426426

@@ -446,7 +446,7 @@ public function getTypes(string $class): ?array
446446
}
447447
}
448448

449-
if (self::TYPE_PROPERTY === $this->type && class_exists($class)) {
449+
if (self::TYPE_PROPERTY === $this->type && (class_exists($class) || interface_exists($class))) {
450450
try {
451451
$reflectionProperty = new \ReflectionProperty($class, $this->accessor);
452452

Diff for: src/Extractor/WriteMutator.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public function getHydrateCallback(string $className): ?Expr
136136
*/
137137
public function getTypes(string $target): ?array
138138
{
139-
if (self::TYPE_METHOD === $this->type && class_exists($target)) {
139+
if (self::TYPE_METHOD === $this->type && (class_exists($target) || interface_exists($target))) {
140140
try {
141141
$reflectionMethod = new \ReflectionMethod($target, $this->property);
142142

@@ -169,7 +169,7 @@ public function getTypes(string $target): ?array
169169
}
170170
}
171171

172-
if (self::TYPE_PROPERTY === $this->type && class_exists($target)) {
172+
if (self::TYPE_PROPERTY === $this->type && (class_exists($target) || interface_exists($target))) {
173173
try {
174174
$reflectionProperty = new \ReflectionProperty($target, $this->property);
175175

Diff for: src/Generator/Shared/DiscriminatorStatementsGenerator.php

+23-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
namespace AutoMapper\Generator\Shared;
66

77
use AutoMapper\Metadata\GeneratorMetadata;
8-
use AutoMapper\Transformer\AllowNullValueTransformerInterface;
98
use AutoMapper\Transformer\TransformerInterface;
109
use PhpParser\Node\Arg;
1110
use PhpParser\Node\Expr;
@@ -53,12 +52,31 @@ public function createTargetStatements(GeneratorMetadata $metadata): array
5352
$variableRegistry = $metadata->variableRegistry;
5453
$fieldValueExpr = $propertyMetadata->source->accessor?->getExpression($variableRegistry->getSourceInput());
5554

56-
if (null === $fieldValueExpr) {
57-
if (!($propertyMetadata->transformer instanceof AllowNullValueTransformerInterface)) {
58-
return [];
55+
if (null === $fieldValueExpr && $this->fromSource) {
56+
$createObjectStatements = [];
57+
58+
// This means we cannot get type from the source, so we get it from the classname
59+
foreach ($this->classDiscriminatorResolver->discriminatorMapperNames($metadata, $this->fromSource) as $className => $discriminatorMapperName) {
60+
$createObjectStatements[] = new Stmt\If_(new Expr\Instanceof_(new Expr\Variable('value'), new Name($className)), [
61+
'stmts' => [
62+
new Stmt\Return_(
63+
new Expr\MethodCall(
64+
new Expr\ArrayDimFetch(
65+
new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'),
66+
new Scalar\String_($discriminatorMapperName)
67+
),
68+
'map',
69+
[
70+
new Arg($variableRegistry->getSourceInput()),
71+
new Arg(new Expr\Variable('context')),
72+
]
73+
)
74+
),
75+
],
76+
]);
5977
}
6078

61-
$fieldValueExpr = new Expr\ConstFetch(new Name('null'));
79+
return $createObjectStatements;
6280
}
6381

6482
// Generate the code that allows to put the type into the output variable,

Diff for: src/Metadata/MapperMetadata.php

+2-2
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ public function __construct(
2828
private string $classPrefix = 'Mapper_',
2929
public ?string $dateTimeFormat = null,
3030
) {
31-
if (class_exists($this->source) && $this->source !== \stdClass::class) {
31+
if ((class_exists($this->source) || interface_exists($this->source)) && $this->source !== \stdClass::class) {
3232
$reflectionSource = new \ReflectionClass($this->source);
3333
$this->sourceReflectionClass = $reflectionSource;
3434
} else {
3535
$this->sourceReflectionClass = null;
3636
}
3737

38-
if (class_exists($this->target) && $this->target !== \stdClass::class) {
38+
if ((class_exists($this->target) || interface_exists($this->target)) && $this->target !== \stdClass::class) {
3939
$reflectionTarget = new \ReflectionClass($this->target);
4040
$this->targetReflectionClass = $reflectionTarget;
4141
} else {

Diff for: src/Metadata/MetadataFactory.php

+9-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use AutoMapper\EventListener\MapToContextListener;
1616
use AutoMapper\EventListener\MapToListener;
1717
use AutoMapper\EventListener\Symfony\AdvancedNameConverterListener;
18+
use AutoMapper\EventListener\Symfony\ClassDiscriminatorListener;
1819
use AutoMapper\EventListener\Symfony\SerializerGroupListener;
1920
use AutoMapper\EventListener\Symfony\SerializerIgnoreListener;
2021
use AutoMapper\EventListener\Symfony\SerializerMaxDepthListener;
@@ -49,6 +50,7 @@
4950
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
5051
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
5152
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
53+
use Symfony\Component\Serializer\Mapping\ClassDiscriminatorFromClassMetadata;
5254
use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactory;
5355
use Symfony\Component\Serializer\NameConverter\AdvancedNameConverterInterface;
5456
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
@@ -286,7 +288,12 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
286288

287289
if ($sourcePropertyMetadata->accessor === null && !($propertyMappedEvent->transformer instanceof AllowNullValueTransformerInterface)) {
288290
$propertyMappedEvent->ignored = true;
289-
$propertyMappedEvent->ignoreReason = 'Property cannot be read from source, and the attached transformer require a value.';
291+
292+
if ($propertyMappedEvent->transformer === null) {
293+
$propertyMappedEvent->ignoreReason = 'Property cannot be read from source.';
294+
} else {
295+
$propertyMappedEvent->ignoreReason = 'Property cannot be read from source, and the attached transformer `' . $propertyMappedEvent->transformer::class . '` require a value.';
296+
}
290297
}
291298

292299
if ($targetPropertyMetadata->writeMutator === null && $targetPropertyMetadata->parameterInConstructor === null) {
@@ -351,6 +358,7 @@ public static function create(
351358
$eventDispatcher->addListener(PropertyMetadataEvent::class, new SerializerMaxDepthListener($classMetadataFactory));
352359
$eventDispatcher->addListener(PropertyMetadataEvent::class, new SerializerGroupListener($classMetadataFactory));
353360
$eventDispatcher->addListener(PropertyMetadataEvent::class, new SerializerIgnoreListener($classMetadataFactory));
361+
$eventDispatcher->addListener(GenerateMapperEvent::class, new ClassDiscriminatorListener(new ClassDiscriminatorFromClassMetadata($classMetadataFactory)));
354362
}
355363

356364
$eventDispatcher->addListener(PropertyMetadataEvent::class, new MapToContextListener($reflectionExtractor));

Diff for: src/Normalizer/AutoMapperNormalizer.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ public function supportsNormalization(mixed $data, ?string $format = null, array
104104
*/
105105
public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool
106106
{
107-
if (!class_exists($type)) {
107+
if (!class_exists($type) && !interface_exists($type)) {
108108
return false;
109109
}
110110

Diff for: src/Symfony/Bundle/DataCollector/MetadataCollector.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep
3838
foreach ($this->metadataFactory->listMetadata() as $metadata) {
3939
$fileCode = null;
4040

41-
if (class_exists($metadata->mapperMetadata->className)) {
41+
if (class_exists($metadata->mapperMetadata->className) || interface_exists($metadata->mapperMetadata->className)) {
4242
$reflectionClass = new \ReflectionClass($metadata->mapperMetadata->className);
4343

4444
if (($fileName = $reflectionClass->getFileName()) !== false && ($content = @file_get_contents($fileName)) !== false) {

Diff for: src/Transformer/ObjectTransformerFactory.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ private function isObjectType(Type $type): bool
6262
return true;
6363
}
6464

65-
if (!class_exists($class)) {
65+
if (!class_exists($class) && !interface_exists($class)) {
6666
return false;
6767
}
6868

Diff for: tests/AutoMapperTest.php

+6-6
Original file line numberDiff line numberDiff line change
@@ -1217,23 +1217,23 @@ public function testRealClassName(): void
12171217
self::assertEquals('Mapper_AutoMapper_Tests_Fixtures_Proxy_array', $mapper::class);
12181218
}
12191219

1220-
public function testDiscriminatorMapAndInterface(): void
1220+
public function testDiscriminatorMapAndInterface3(): void
12211221
{
12221222
if (!class_exists(ClassDiscriminatorFromClassMetadata::class)) {
12231223
self::markTestSkipped('Symfony Serializer is required to run this test.');
12241224
}
12251225

1226-
$this->buildAutoMapper(mapPrivatePropertiesAndMethod: true);
1226+
$autoMapper = AutoMapperBuilder::buildAutoMapper(mapPrivatePropertiesAndMethod: true);
12271227

12281228
$typeA = new Fixtures\DiscriminatorMapAndInterface\TypeA('my name');
12291229
$something = new Fixtures\DiscriminatorMapAndInterface\Something($typeA);
12301230

1231-
$mapped = $this->autoMapper->map($something, 'array');
1231+
$mapped = $autoMapper->map($something, 'array');
12321232

12331233
$expected = [
12341234
'myInterface' => [
1235-
'type' => 'type_a',
12361235
'name' => 'my name',
1236+
'type' => 'type_a',
12371237
],
12381238
];
12391239
self::assertSame($expected, $mapped);
@@ -1245,7 +1245,7 @@ public function testDiscriminatorMapAndInterface2(): void
12451245
self::markTestSkipped('Symfony Serializer is required to run this test.');
12461246
}
12471247

1248-
$this->buildAutoMapper(classPrefix: 'Discriminator2');
1248+
$autoMapper = AutoMapperBuilder::buildAutoMapper(mapPrivatePropertiesAndMethod: true);
12491249

12501250
$something = [
12511251
'myInterface' => [
@@ -1254,7 +1254,7 @@ public function testDiscriminatorMapAndInterface2(): void
12541254
],
12551255
];
12561256

1257-
$mapped = $this->autoMapper->map($something, Fixtures\DiscriminatorMapAndInterface\Something::class);
1257+
$mapped = $autoMapper->map($something, Fixtures\DiscriminatorMapAndInterface\Something::class);
12581258

12591259
self::assertInstanceOf(Fixtures\DiscriminatorMapAndInterface\Something::class, $mapped);
12601260
self::assertInstanceOf(Fixtures\DiscriminatorMapAndInterface\TypeA::class, $mapped->myInterface);

0 commit comments

Comments
 (0)