Skip to content

Commit b911c3a

Browse files
authored
Add support for DiscriminatorMap with interface (#242)
2 parents 567af53 + b41faa8 commit b911c3a

File tree

15 files changed

+263
-13
lines changed

15 files changed

+263
-13
lines changed

castor.php

Lines changed: 23 additions & 0 deletions
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+
}
Lines changed: 97 additions & 0 deletions
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+
}

src/Extractor/ReadAccessor.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ public function getExtractIsUndefinedCallback(string $className): ?Expr
429429
*/
430430
public function getTypes(string $class): ?array
431431
{
432-
if (self::TYPE_METHOD === $this->type && class_exists($class)) {
432+
if (self::TYPE_METHOD === $this->type && (class_exists($class) || interface_exists($class))) {
433433
try {
434434
$reflectionMethod = new \ReflectionMethod($class, $this->accessor);
435435

@@ -455,7 +455,7 @@ public function getTypes(string $class): ?array
455455
}
456456
}
457457

458-
if (self::TYPE_PROPERTY === $this->type && class_exists($class)) {
458+
if (self::TYPE_PROPERTY === $this->type && (class_exists($class) || interface_exists($class))) {
459459
try {
460460
$reflectionProperty = new \ReflectionProperty($class, $this->accessor);
461461

src/Extractor/WriteMutator.php

Lines changed: 2 additions & 2 deletions
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

src/Generator/Shared/DiscriminatorStatementsGenerator.php

Lines changed: 25 additions & 3 deletions
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;
@@ -54,11 +53,34 @@ public function createTargetStatements(GeneratorMetadata $metadata): array
5453
$fieldValueExpr = $propertyMetadata->source->accessor?->getExpression($variableRegistry->getSourceInput());
5554

5655
if (null === $fieldValueExpr) {
57-
if (!($propertyMetadata->transformer instanceof AllowNullValueTransformerInterface)) {
56+
if (!$this->fromSource) {
5857
return [];
5958
}
6059

61-
$fieldValueExpr = new Expr\ConstFetch(new Name('null'));
60+
$createObjectStatements = [];
61+
62+
// This means we cannot get type from the source, so we get it from the classname
63+
foreach ($this->classDiscriminatorResolver->discriminatorMapperNames($metadata, $this->fromSource) as $className => $discriminatorMapperName) {
64+
$createObjectStatements[] = new Stmt\If_(new Expr\Instanceof_(new Expr\Variable('value'), new Name($className)), [
65+
'stmts' => [
66+
new Stmt\Return_(
67+
new Expr\MethodCall(
68+
new Expr\ArrayDimFetch(
69+
new Expr\PropertyFetch(new Expr\Variable('this'), 'mappers'),
70+
new Scalar\String_($discriminatorMapperName)
71+
),
72+
'map',
73+
[
74+
new Arg($variableRegistry->getSourceInput()),
75+
new Arg(new Expr\Variable('context')),
76+
]
77+
)
78+
),
79+
],
80+
]);
81+
}
82+
83+
return $createObjectStatements;
6284
}
6385

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

src/Metadata/MapperMetadata.php

Lines changed: 2 additions & 2 deletions
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 {

src/Metadata/MetadataFactory.php

Lines changed: 9 additions & 1 deletion
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\MetadataAwareNameConverter;
@@ -287,7 +289,12 @@ private function createGeneratorMetadata(MapperMetadata $mapperMetadata): Genera
287289

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

293300
if ($targetPropertyMetadata->writeMutator === null && $targetPropertyMetadata->parameterInConstructor === null) {
@@ -349,6 +356,7 @@ public static function create(
349356
$eventDispatcher->addListener(PropertyMetadataEvent::class, new SerializerMaxDepthListener($classMetadataFactory));
350357
$eventDispatcher->addListener(PropertyMetadataEvent::class, new SerializerGroupListener($classMetadataFactory));
351358
$eventDispatcher->addListener(PropertyMetadataEvent::class, new SerializerIgnoreListener($classMetadataFactory));
359+
$eventDispatcher->addListener(GenerateMapperEvent::class, new ClassDiscriminatorListener(new ClassDiscriminatorFromClassMetadata($classMetadataFactory)));
352360
} elseif (null !== $nameConverter) {
353361
$eventDispatcher->addListener(PropertyMetadataEvent::class, new AdvancedNameConverterListener($nameConverter));
354362
}

src/Normalizer/AutoMapperNormalizer.php

Lines changed: 1 addition & 1 deletion
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

src/Symfony/Bundle/DataCollector/MetadataCollector.php

Lines changed: 1 addition & 1 deletion
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) {

src/Transformer/ObjectTransformerFactory.php

Lines changed: 1 addition & 1 deletion
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

0 commit comments

Comments
 (0)