Skip to content

Commit 1cfdf63

Browse files
committed
WIP
1 parent c7f289f commit 1cfdf63

29 files changed

+1353
-49
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace ShipMonk\InputMapper\Compiler\Mapper;
4+
5+
use ShipMonk\InputMapper\Compiler\Type\GenericTypeParameter;
6+
7+
interface GenericMapperCompiler extends MapperCompiler
8+
{
9+
10+
/**
11+
* @return list<GenericTypeParameter>
12+
*/
13+
public function getGenericParameters(): array;
14+
15+
}

src/Compiler/Mapper/Object/DelegateMapperCompiler.php

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,40 @@
22

33
namespace ShipMonk\InputMapper\Compiler\Mapper\Object;
44

5+
use Nette\Utils\Arrays;
56
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\MethodCall;
8+
use PhpParser\Node\VariadicPlaceholder;
9+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
610
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
711
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
812
use ShipMonk\InputMapper\Compiler\CompiledExpr;
913
use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler;
1014
use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder;
15+
use ShipMonk\InputMapper\Runtime\CallbackMapper;
16+
use function count;
1117

1218
class DelegateMapperCompiler implements MapperCompiler
1319
{
1420

1521
/**
16-
* @param class-string $className
22+
* @param list<MapperCompiler> $innerMapperCompilers
1723
*/
1824
public function __construct(
1925
public readonly string $className,
26+
public readonly array $innerMapperCompilers = [],
2027
)
2128
{
2229
}
2330

2431
public function compile(Expr $value, Expr $path, PhpCodeBuilder $builder): CompiledExpr
2532
{
26-
$shortName = $builder->importClass($this->className);
27-
$provider = $builder->propertyFetch($builder->var('this'), 'provider');
28-
$mapper = $builder->methodCall($provider, 'get', [$builder->classConstFetch($shortName, 'class')]);
33+
$compilerMapper = $this->compileMapperExpr($builder);
34+
$mapper = $compilerMapper->expr;
35+
$statements = $compilerMapper->statements;
2936
$mapped = $builder->methodCall($mapper, 'map', [$value, $path]);
3037

31-
return new CompiledExpr($mapped);
38+
return new CompiledExpr($mapped, $statements);
3239
}
3340

3441
public function getInputType(): TypeNode
@@ -38,7 +45,71 @@ public function getInputType(): TypeNode
3845

3946
public function getOutputType(): TypeNode
4047
{
41-
return new IdentifierTypeNode($this->className);
48+
$outputType = new IdentifierTypeNode($this->className);
49+
50+
if (count($this->innerMapperCompilers) === 0) {
51+
return $outputType;
52+
}
53+
54+
return new GenericTypeNode($outputType, Arrays::map(
55+
$this->innerMapperCompilers,
56+
static function (MapperCompiler $innerMapperCompiler): TypeNode {
57+
return $innerMapperCompiler->getOutputType();
58+
},
59+
));
60+
}
61+
62+
/**
63+
* @return list<Expr>
64+
*/
65+
private function compileInnerMappers(PhpCodeBuilder $builder): array
66+
{
67+
return Arrays::map($this->innerMapperCompilers, function (MapperCompiler $innerMapperCompiler, int $key) use ($builder): Expr {
68+
return $this->compileInnerMapper($innerMapperCompiler, $key, $builder);
69+
});
70+
}
71+
72+
private function compileInnerMapper(MapperCompiler $innerMapperCompiler, int $key, PhpCodeBuilder $builder): Expr
73+
{
74+
if ($innerMapperCompiler instanceof self && count($innerMapperCompiler->innerMapperCompilers) === 0) {
75+
$provider = $builder->propertyFetch($builder->var('this'), 'provider');
76+
$innerClassExpr = $builder->classConstFetch($builder->importClass($innerMapperCompiler->className), 'class');
77+
return $builder->methodCall($provider, 'get', [$innerClassExpr]);
78+
}
79+
80+
$innerMapperMethodName = $builder->uniqMethodName("mapInner{$key}");
81+
$innerMapperMethod = $builder->mapperMethod($innerMapperMethodName, $innerMapperCompiler)->makePrivate()->getNode();
82+
$builder->addMethod($innerMapperMethod);
83+
84+
$innerMapperMethodCallback = new MethodCall($builder->var('this'), $innerMapperMethodName, [new VariadicPlaceholder()]);
85+
return $builder->new($builder->importClass(CallbackMapper::class), [$innerMapperMethodCallback]);
86+
}
87+
88+
private function compileMapperExpr(PhpCodeBuilder $builder): CompiledExpr
89+
{
90+
foreach ($builder->getGenericParameters() as $offset => $genericParameter) {
91+
if ($this->className === $genericParameter->name) {
92+
$innerMappers = $builder->propertyFetch($builder->var('this'), 'innerMappers');
93+
$innerMapper = $builder->arrayDimFetch($innerMappers, $builder->val($offset));
94+
return new CompiledExpr($innerMapper);
95+
}
96+
}
97+
98+
$statements = [];
99+
$classNameExpr = $builder->classConstFetch($builder->importClass($this->className), 'class');
100+
$provider = $builder->propertyFetch($builder->var('this'), 'provider');
101+
$innerMappers = $this->compileInnerMappers($builder);
102+
103+
if (count($innerMappers) === 0) {
104+
$mapper = $builder->methodCall($provider, 'get', [$classNameExpr]);
105+
106+
} else {
107+
$innerMappersVarName = $builder->uniqVariableName('innerMappers');
108+
$statements[] = $builder->assign($builder->var($innerMappersVarName), $builder->val($innerMappers));
109+
$mapper = $builder->methodCall($provider, 'getGeneric', [$classNameExpr, $builder->var($innerMappersVarName)]);
110+
}
111+
112+
return new CompiledExpr($mapper, $statements);
42113
}
43114

44115
}

src/Compiler/Mapper/Object/MapObject.php

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,18 @@
33
namespace ShipMonk\InputMapper\Compiler\Mapper\Object;
44

55
use Attribute;
6+
use Nette\Utils\Arrays;
67
use PhpParser\Node\Expr;
78
use PhpParser\Node\Stmt;
9+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
810
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
911
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
1012
use ShipMonk\InputMapper\Compiler\CompiledExpr;
13+
use ShipMonk\InputMapper\Compiler\Mapper\GenericMapperCompiler;
1114
use ShipMonk\InputMapper\Compiler\Mapper\MapperCompiler;
1215
use ShipMonk\InputMapper\Compiler\Mapper\UndefinedAwareMapperCompiler;
1316
use ShipMonk\InputMapper\Compiler\Php\PhpCodeBuilder;
17+
use ShipMonk\InputMapper\Compiler\Type\GenericTypeParameter;
1418
use ShipMonk\InputMapper\Runtime\Exception\MappingFailedException;
1519
use function array_fill_keys;
1620
use function array_keys;
@@ -22,17 +26,19 @@
2226
* @template T of object
2327
*/
2428
#[Attribute(Attribute::TARGET_PARAMETER | Attribute::TARGET_PROPERTY)]
25-
class MapObject implements MapperCompiler
29+
class MapObject implements GenericMapperCompiler
2630
{
2731

2832
/**
2933
* @param class-string<T> $className
3034
* @param array<string, MapperCompiler> $constructorArgsMapperCompilers
35+
* @param list<GenericTypeParameter> $genericParameters
3136
*/
3237
public function __construct(
3338
public readonly string $className,
3439
public readonly array $constructorArgsMapperCompilers,
3540
public readonly bool $allowExtraKeys = false,
41+
public readonly array $genericParameters = [],
3642
)
3743
{
3844
}
@@ -114,7 +120,26 @@ public function getInputType(): TypeNode
114120

115121
public function getOutputType(): TypeNode
116122
{
117-
return new IdentifierTypeNode($this->className);
123+
$outputType = new IdentifierTypeNode($this->className);
124+
125+
if (count($this->genericParameters) === 0) {
126+
return $outputType;
127+
}
128+
129+
return new GenericTypeNode(
130+
$outputType,
131+
Arrays::map($this->genericParameters, static function (GenericTypeParameter $parameter): TypeNode {
132+
return new IdentifierTypeNode($parameter->name);
133+
}),
134+
);
135+
}
136+
137+
/**
138+
* @return list<GenericTypeParameter>
139+
*/
140+
public function getGenericParameters(): array
141+
{
142+
return $this->genericParameters;
118143
}
119144

120145
/**

src/Compiler/MapperFactory/DefaultMapperCompilerFactory.php

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
use DateTimeImmutable;
77
use DateTimeInterface;
88
use LogicException;
9+
use Nette\Utils\Arrays;
910
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprIntegerNode;
1011
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprStringNode;
1112
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
1213
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
1314
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
15+
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
1416
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
1517
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
1618
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
@@ -48,6 +50,8 @@
4850
use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapNullable;
4951
use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\MapOptional;
5052
use ShipMonk\InputMapper\Compiler\Mapper\Wrapper\ValidatedMapperCompiler;
53+
use ShipMonk\InputMapper\Compiler\Type\GenericTypeParameter;
54+
use ShipMonk\InputMapper\Compiler\Type\GenericTypeVariance;
5155
use ShipMonk\InputMapper\Compiler\Type\PhpDocTypeUtils;
5256
use ShipMonk\InputMapper\Compiler\Validator\Array\AssertListLength;
5357
use ShipMonk\InputMapper\Compiler\Validator\Int\AssertIntRange;
@@ -57,12 +61,17 @@
5761
use ShipMonk\InputMapper\Compiler\Validator\Int\AssertPositiveInt;
5862
use ShipMonk\InputMapper\Compiler\Validator\ValidatorCompiler;
5963
use ShipMonk\InputMapper\Runtime\Optional;
64+
use function array_column;
65+
use function array_fill_keys;
66+
use function array_keys;
67+
use function array_values;
6068
use function class_exists;
6169
use function class_implements;
6270
use function class_parents;
6371
use function count;
6472
use function enum_exists;
6573
use function interface_exists;
74+
use function str_ends_with;
6675
use function strcasecmp;
6776
use function strtolower;
6877
use function substr;
@@ -71,6 +80,7 @@ class DefaultMapperCompilerFactory implements MapperCompilerFactory
7180
{
7281

7382
final public const DELEGATE_OBJECT_MAPPING = 'delegateObjectMapping';
83+
final public const GENERIC_PARAMETERS = 'genericParameters';
7484

7585
/**
7686
* @param array<class-string, callable(class-string, array<string, mixed>): MapperCompiler> $mapperCompilerFactories
@@ -102,7 +112,7 @@ public function create(TypeNode $type, array $options = []): MapperCompiler
102112
{
103113
if ($type instanceof IdentifierTypeNode) {
104114
if (!PhpDocTypeUtils::isKeyword($type)) {
105-
if (!class_exists($type->name) && !interface_exists($type->name) && !enum_exists($type->name)) {
115+
if (!class_exists($type->name) && !interface_exists($type->name) && !enum_exists($type->name) && !isset($options[self::GENERIC_PARAMETERS][$type->name])) {
106116
throw CannotCreateMapperCompilerException::fromType($type, 'there is no class, interface or enum with this name');
107117
}
108118

@@ -164,7 +174,7 @@ public function create(TypeNode $type, array $options = []): MapperCompiler
164174
1 => new MapOptional($this->createInner($type->genericTypes[0], $options)),
165175
default => throw CannotCreateMapperCompilerException::fromType($type),
166176
},
167-
default => throw CannotCreateMapperCompilerException::fromType($type),
177+
default => $this->createFromGenericType($type, $options),
168178
},
169179
};
170180
}
@@ -218,6 +228,27 @@ protected function createInner(TypeNode $type, array $options): MapperCompiler
218228
return $this->create($type, $options);
219229
}
220230

231+
/**
232+
* @param array<string, mixed> $options
233+
*/
234+
protected function createFromGenericType(GenericTypeNode $type, array $options): MapperCompiler
235+
{
236+
$genericParameters = $this->getGenericParameters($type->type->name);
237+
238+
return new DelegateMapperCompiler(
239+
$type->type->name,
240+
Arrays::map(array_values($type->genericTypes), function (TypeNode $genericType, int $index) use ($type, $options, $genericParameters): MapperCompiler {
241+
$genericParameter = $genericParameters[$index] ?? throw CannotCreateMapperCompilerException::fromType($type, "generic parameter at index {$index} does not exist");
242+
243+
if ($genericParameter->bound !== null && !PhpDocTypeUtils::isSubTypeOf($genericType, $genericParameter->bound)) {
244+
throw CannotCreateMapperCompilerException::fromType($type, "type {$genericType} is not a subtype of {$genericParameter->bound}");
245+
}
246+
247+
return $this->createInner($genericType, $options);
248+
}),
249+
);
250+
}
251+
221252
/**
222253
* @param class-string $inputClassName
223254
* @param array<string, mixed> $options
@@ -257,8 +288,12 @@ protected function createObjectMappingByConstructorInvocation(
257288
throw CannotCreateMapperCompilerException::fromType(new IdentifierTypeNode($inputClassName), 'class has a non-public constructor');
258289
}
259290

291+
$genericParameters = $this->getGenericParameters($inputClassName);
292+
$genericParameterNames = array_column($genericParameters, 'name');
293+
$options[self::GENERIC_PARAMETERS] = array_fill_keys($genericParameterNames, true);
294+
260295
$constructorParameterMapperCompilers = [];
261-
$constructorParameterTypes = $this->getConstructorParameterTypes($constructor);
296+
$constructorParameterTypes = $this->getConstructorParameterTypes($constructor, $genericParameterNames);
262297

263298
foreach ($constructor->getParameters() as $parameter) {
264299
$name = $parameter->getName();
@@ -267,13 +302,54 @@ protected function createObjectMappingByConstructorInvocation(
267302
}
268303

269304
$allowExtraKeys = count($classReflection->getAttributes(AllowExtraKeys::class)) > 0;
270-
return new MapObject($classReflection->getName(), $constructorParameterMapperCompilers, $allowExtraKeys);
305+
return new MapObject($classReflection->getName(), $constructorParameterMapperCompilers, $allowExtraKeys, $genericParameters);
306+
}
307+
308+
/**
309+
* @param class-string $inputClassName
310+
* @return list<GenericTypeParameter>
311+
*/
312+
protected function getGenericParameters(string $inputClassName): array
313+
{
314+
$classReflection = new ReflectionClass($inputClassName);
315+
$classPhpDoc = $classReflection->getDocComment();
316+
317+
if ($classPhpDoc === false) {
318+
return [];
319+
}
320+
321+
$genericParameters = [];
322+
323+
foreach ($this->parsePhpDoc($classPhpDoc)->children as $node) {
324+
if ($node instanceof PhpDocTagNode && $node->value instanceof TemplateTagValueNode) {
325+
$variance = match (true) {
326+
str_ends_with($node->name, '-covariant') => GenericTypeVariance::Covariant,
327+
str_ends_with($node->name, '-contravariant') => GenericTypeVariance::Contravariant,
328+
default => GenericTypeVariance::Invariant,
329+
};
330+
331+
$genericParameters[$node->value->name] = new GenericTypeParameter(
332+
name: $node->value->name,
333+
variance: $variance,
334+
bound: $node->value->bound,
335+
default: $node->value->default,
336+
);
337+
}
338+
}
339+
340+
foreach ($genericParameters as $genericParameter) {
341+
PhpDocTypeUtils::resolve($genericParameter->bound, $classReflection, array_keys($genericParameters));
342+
PhpDocTypeUtils::resolve($genericParameter->default, $classReflection, array_keys($genericParameters));
343+
}
344+
345+
return array_values($genericParameters);
271346
}
272347

273348
/**
349+
* @param list<string> $genericParameterNames
274350
* @return array<string, TypeNode>
275351
*/
276-
protected function getConstructorParameterTypes(ReflectionMethod $constructor): array
352+
protected function getConstructorParameterTypes(ReflectionMethod $constructor, array $genericParameterNames): array
277353
{
278354
$class = $constructor->getDeclaringClass();
279355
$parameterTypes = [];
@@ -290,7 +366,7 @@ protected function getConstructorParameterTypes(ReflectionMethod $constructor):
290366
if ($constructorDocComment !== false) {
291367
foreach ($this->parsePhpDoc($constructorDocComment)->children as $node) {
292368
if ($node instanceof PhpDocTagNode && $node->value instanceof ParamTagValueNode) {
293-
PhpDocTypeUtils::resolve($node->value->type, $class);
369+
PhpDocTypeUtils::resolve($node->value->type, $class, $genericParameterNames);
294370
$parameterName = substr($node->value->parameterName, 1);
295371
$parameterTypes[$parameterName] = $node->value->type;
296372
}
@@ -312,7 +388,7 @@ protected function getConstructorParameterTypes(ReflectionMethod $constructor):
312388
&& $node->value instanceof VarTagValueNode
313389
&& ($node->value->variableName === '' || substr($node->value->variableName, 1) === $parameterName)
314390
) {
315-
PhpDocTypeUtils::resolve($node->value->type, $class);
391+
PhpDocTypeUtils::resolve($node->value->type, $class, $genericParameterNames);
316392
$parameterTypes[$parameterName] = $node->value->type;
317393
}
318394
}

0 commit comments

Comments
 (0)