Skip to content

Commit ae959e0

Browse files
committed
wip
1 parent be78e12 commit ae959e0

File tree

10 files changed

+322
-94
lines changed

10 files changed

+322
-94
lines changed

src/Event/GenerateMapperEvent.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ final class GenerateMapperEvent
1818
public function __construct(
1919
public readonly MapperMetadata $mapperMetadata,
2020
public array $properties = [],
21-
public ?string $provider = null,
21+
public null|string|array $provider = null,
2222
public ?bool $checkAttributes = null,
2323
public ?ConstructorStrategy $constructorStrategy = null,
2424
public ?bool $allowReadOnlyTargetToPopulate = null,
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
namespace AutoMapper\EventListener\ObjectMapper;
4+
5+
use AutoMapper\Event\GenerateMapperEvent;
6+
use AutoMapper\Event\PropertyMetadataEvent;
7+
use AutoMapper\Event\SourcePropertyMetadata;
8+
use AutoMapper\Event\TargetPropertyMetadata;
9+
use AutoMapper\Exception\BadMapDefinitionException;
10+
use AutoMapper\Transformer\CallableTransformer;
11+
use AutoMapper\Transformer\ExpressionLanguageTransformer;
12+
use AutoMapper\Transformer\TransformerInterface;
13+
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
14+
use Symfony\Component\ExpressionLanguage\SyntaxError;
15+
use Symfony\Component\ObjectMapper\Attribute\Map;
16+
17+
final readonly class MapClassListener
18+
{
19+
public function __construct(
20+
private ExpressionLanguage $expressionLanguage,
21+
) {
22+
}
23+
24+
public function __invoke(GenerateMapperEvent $event): void
25+
{
26+
// only handle class to class mapping
27+
if (!$event->mapperMetadata->sourceReflectionClass || !$event->mapperMetadata->targetReflectionClass) {
28+
return;
29+
}
30+
31+
$mapAttribute = null;
32+
$reflectionClass = null;
33+
$isSource = false;
34+
35+
foreach ($event->mapperMetadata->sourceReflectionClass->getAttributes(Map::class) as $sourceAttribute) {
36+
/** @var Map $attribute */
37+
$attribute = $sourceAttribute->newInstance();
38+
39+
if (!$attribute->target || $attribute->target === $event->mapperMetadata->target) {
40+
$mapAttribute = $attribute;
41+
$reflectionClass = $event->mapperMetadata->sourceReflectionClass;
42+
$isSource = true;
43+
break;
44+
}
45+
}
46+
47+
if (!$mapAttribute) {
48+
foreach ($event->mapperMetadata->targetReflectionClass->getAttributes(Map::class) as $targetAttribute) {
49+
/** @var Map $attribute */
50+
$attribute = $targetAttribute->newInstance();
51+
52+
if (!$attribute->source || $attribute->source === $event->mapperMetadata->source) {
53+
$mapAttribute = $attribute;
54+
$reflectionClass = $event->mapperMetadata->targetReflectionClass;
55+
break;
56+
}
57+
}
58+
}
59+
60+
if (!$mapAttribute || !$reflectionClass) {
61+
return;
62+
}
63+
64+
// get all properties
65+
$properties = [];
66+
67+
foreach ($reflectionClass->getProperties() as $property) {
68+
foreach ($property->getAttributes(Map::class) as $propertyAttribute) {
69+
/** @var Map $attribute */
70+
$attribute = $propertyAttribute->newInstance();
71+
$propertyMetadata = new PropertyMetadataEvent(
72+
/**
73+
* public ?string $if = null,// @TODO
74+
*/
75+
$event->mapperMetadata,
76+
new SourcePropertyMetadata($isSource ? $property->getName() : ($attribute->source ?? $property->getName())),
77+
new TargetPropertyMetadata($isSource ? ($attribute->target ?? $property->getName()) : $property->getName()),
78+
transformer: $this->getTransformerFromMapAttribute($reflectionClass->getName(), $attribute, $isSource),
79+
if: $attribute->if,
80+
);
81+
82+
$properties[] = $propertyMetadata;
83+
}
84+
}
85+
86+
$event->properties = $properties;
87+
88+
if ($mapAttribute->transform) {
89+
$event->provider = $mapAttribute->transform;
90+
}
91+
}
92+
93+
protected function getTransformerFromMapAttribute(string $class, Map $attribute, bool $fromSource = true): ?TransformerInterface
94+
{
95+
$transformer = null;
96+
97+
if ($attribute->transform !== null) {
98+
$callableName = null;
99+
$transformerCallable = $attribute->transform;
100+
101+
if ($transformerCallable instanceof \Closure) {
102+
// This is not supported because we cannot generate code from a closure
103+
// However this should never be possible since attributes does not allow to pass a closure
104+
// Let's keep this check for future proof
105+
throw new BadMapDefinitionException('Closure transformer is not supported.');
106+
}
107+
108+
if (\is_callable($transformerCallable, false, $callableName)) {
109+
$transformer = new CallableTransformer($callableName);
110+
} elseif (\is_string($transformerCallable) && method_exists($class, $transformerCallable)) {
111+
$reflMethod = new \ReflectionMethod($class, $transformerCallable);
112+
113+
if ($reflMethod->isStatic()) {
114+
$transformer = new CallableTransformer($class . '::' . $transformerCallable);
115+
} else {
116+
$transformer = new CallableTransformer($transformerCallable, $fromSource, !$fromSource);
117+
}
118+
} elseif (\is_string($transformerCallable)) {
119+
try {
120+
$expression = $this->expressionLanguage->compile($transformerCallable, ['value' => 'source', 'context']);
121+
} catch (SyntaxError $e) {
122+
throw new BadMapDefinitionException(\sprintf('Transformer "%s" targeted by %s transformer on class "%s" is not valid.', $transformerCallable, $attribute::class, $class), 0, $e);
123+
}
124+
125+
$transformer = new ExpressionLanguageTransformer($expression);
126+
} else {
127+
throw new BadMapDefinitionException(\sprintf('Callable "%s" targeted by %s transformer on class "%s" is not valid.', json_encode($transformerCallable), $attribute::class, $class));
128+
}
129+
}
130+
131+
return $transformer;
132+
}
133+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace AutoMapper\EventListener\ObjectMapper;
4+
5+
final readonly class MapPropertyListener
6+
{
7+
}

src/Generator/MapMethodStatementsGenerator.php

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

55
namespace AutoMapper\Generator;
66

7+
use AutoMapper\Exception\CompileException;
78
use AutoMapper\Exception\ReadOnlyTargetException;
89
use AutoMapper\Generator\Shared\CachedReflectionStatementsGenerator;
910
use AutoMapper\Generator\Shared\DiscriminatorStatementsGenerator;
@@ -294,35 +295,70 @@ private function initializeTargetFromProvider(GeneratorMetadata $metadata): arra
294295
}
295296

296297
$variableRegistry = $metadata->variableRegistry;
298+
$statements = [];
299+
300+
if (is_array($metadata->provider) || is_callable($metadata->provider)) {
301+
$callableName = null;
302+
303+
if (!is_callable($metadata->provider, false, $callableName)) {
304+
return [];
305+
}
306+
307+
/*
308+
* Get result from callable if available
309+
*
310+
* ```php
311+
* $result ??= callable(Target::class, $value, $context, $this->getTargetIdentifiers($value));
312+
* ```
313+
*/
314+
$statements[] = new Stmt\Expression(
315+
new Expr\AssignOp\Coalesce(
316+
$variableRegistry->getResult(),
317+
new Expr\FuncCall(
318+
new Name($callableName), [
319+
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
320+
new Arg($variableRegistry->getSourceInput()),
321+
new Arg($variableRegistry->getContext()),
322+
new Arg(new Expr\MethodCall(new Expr\Variable('this'), 'getTargetIdentifiers', [
323+
new Arg(new Expr\Variable('value')),
324+
])),
325+
]),
326+
)
327+
);
328+
} else {
329+
330+
/*
331+
* Get result from provider if available
332+
*
333+
* ```php
334+
* $result ??= $this->providerRegistry->getProvider($metadata->provider)->provide($source, $context);
335+
* ```
336+
*/
337+
$statements[] = new Stmt\Expression(
338+
new Expr\AssignOp\Coalesce(
339+
$variableRegistry->getResult(),
340+
new Expr\MethodCall(new Expr\MethodCall(new Expr\PropertyFetch(new Expr\Variable('this'), 'providerRegistry'), 'getProvider', [
341+
new Arg(new Scalar\String_($metadata->provider)),
342+
]), 'provide', [
343+
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
344+
new Arg($variableRegistry->getSourceInput()),
345+
new Arg($variableRegistry->getContext()),
346+
new Arg(new Expr\MethodCall(new Expr\Variable('this'), 'getTargetIdentifiers', [
347+
new Arg(new Expr\Variable('value')),
348+
])),
349+
]),
350+
)
351+
);
352+
}
297353

298354
/*
299-
* Get result from provider if available
300-
*
301-
* ```php
302-
* $result ??= $this->providerRegistry->getProvider($metadata->provider)->provide($source, $context);
355+
* Return early if the result is an EarlyReturn instance
303356
*
304357
* if ($result instanceof EarlyReturn) {
305358
* return $result->value;
306359
* }
307360
* ```
308361
*/
309-
$statements = [];
310-
$statements[] = new Stmt\Expression(
311-
new Expr\AssignOp\Coalesce(
312-
$variableRegistry->getResult(),
313-
new Expr\MethodCall(new Expr\MethodCall(new Expr\PropertyFetch(new Expr\Variable('this'), 'providerRegistry'), 'getProvider', [
314-
new Arg(new Scalar\String_($metadata->provider)),
315-
]), 'provide', [
316-
new Arg(new Scalar\String_($metadata->mapperMetadata->target)),
317-
new Arg($variableRegistry->getSourceInput()),
318-
new Arg($variableRegistry->getContext()),
319-
new Arg(new Expr\MethodCall(new Expr\Variable('this'), 'getTargetIdentifiers', [
320-
new Arg(new Expr\Variable('value')),
321-
])),
322-
]),
323-
)
324-
);
325-
326362
$statements[] = new Stmt\If_(
327363
new Expr\Instanceof_($variableRegistry->getResult(), new Name(EarlyReturn::class)),
328364
[

src/Generator/PropertyConditionsGenerator.php

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,12 @@ private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $
246246
}
247247

248248
$callableName = null;
249+
$value = $metadata->variableRegistry->getSourceInput();
250+
251+
// use read accessor
252+
if ($propertyMetadata->source->accessor !== null) {
253+
$value = $propertyMetadata->source->accessor->getExpression($metadata->variableRegistry->getSourceInput());
254+
}
249255

250256
if (\is_callable($propertyMetadata->if, false, $callableName)) {
251257
if (\function_exists($callableName)) {
@@ -257,7 +263,7 @@ private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $
257263
return new Expr\FuncCall(
258264
new Name($callableName),
259265
[
260-
new Arg(new Expr\Variable('value')),
266+
new Arg($value),
261267
]
262268
);
263269
}
@@ -270,7 +276,7 @@ private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $
270276
return new Expr\FuncCall(
271277
new Name($callableName),
272278
[
273-
new Arg(new Expr\Variable('value')),
279+
new Arg($value),
274280
new Arg(new Expr\Variable('context')),
275281
]
276282
);
@@ -284,17 +290,17 @@ private function customCondition(GeneratorMetadata $metadata, PropertyMetadata $
284290
new Name\FullyQualified($metadata->mapperMetadata->source),
285291
$propertyMetadata->if,
286292
[
287-
new Arg(new Expr\Variable('value')),
293+
new Arg($value),
288294
new Arg(new Expr\Variable('context')),
289295
]
290296
);
291297
}
292298

293299
return new Expr\MethodCall(
294-
new Expr\Variable('value'),
300+
$metadata->variableRegistry->getSourceInput(),
295301
$propertyMetadata->if,
296302
[
297-
new Arg(new Expr\Variable('value')),
303+
new Arg($value),
298304
new Arg(new Expr\Variable('context')),
299305
]
300306
);

src/Metadata/GeneratorMetadata.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public function __construct(
2626
public readonly ConstructorStrategy $constructorStrategy = ConstructorStrategy::AUTO,
2727
public readonly bool $allowReadOnlyTargetToPopulate = false,
2828
public readonly bool $strictTypes = false,
29-
public readonly ?string $provider = null,
29+
public readonly null|string|array $provider = null,
3030
) {
3131
$this->variableRegistry = new VariableRegistry();
3232
}

src/Metadata/MetadataFactory.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use AutoMapper\EventListener\MapProviderListener;
1717
use AutoMapper\EventListener\MapToContextListener;
1818
use AutoMapper\EventListener\MapToListener;
19+
use AutoMapper\EventListener\ObjectMapper\MapClassListener;
1920
use AutoMapper\EventListener\Symfony\ClassDiscriminatorListener;
2021
use AutoMapper\EventListener\Symfony\NameConverterListener;
2122
use AutoMapper\EventListener\Symfony\SerializerGroupListener;
@@ -51,6 +52,7 @@
5152
use Symfony\Component\EventDispatcher\EventDispatcher;
5253
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
5354
use Symfony\Component\ExpressionLanguage\ExpressionLanguage;
55+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
5456
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
5557
use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
5658
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
@@ -394,6 +396,10 @@ public static function create(
394396
$eventDispatcher->addListener(GenerateMapperEvent::class, new MapperListener());
395397
$eventDispatcher->addListener(GenerateMapperEvent::class, new MapProviderListener());
396398

399+
if (interface_exists(ObjectMapperInterface::class)) {
400+
$eventDispatcher->addListener(GenerateMapperEvent::class, new MapClassListener($expressionLanguage));
401+
}
402+
397403
// Create transformer factories
398404
$factories = [
399405
new DoctrineCollectionTransformerFactory(),

0 commit comments

Comments
 (0)