Skip to content

Commit 0ef4f61

Browse files
authored
Mapping to generic input classes (#46)
1 parent e2c2149 commit 0ef4f61

37 files changed

+1947
-192
lines changed

phpstan.neon.dist

+6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ parameters:
3434
forbidCheckedExceptionInCallable:
3535
immediatelyCalledCallables:
3636
'ShipMonkTests\InputMapper\InputMapperTestCase::assertException': 2
37+
allowedCheckedExceptionCallables:
38+
'ShipMonk\InputMapper\Runtime\CallbackMapper::__construct': 0
3739

3840
ignoreErrors:
3941
-
@@ -53,3 +55,7 @@ parameters:
5355
message: "#^Method ShipMonkTests\\\\InputMapper\\\\Compiler\\\\Validator\\\\Array\\\\Data\\\\ListItemValidatorWithMultipleValidatorsMapper\\:\\:map\\(\\) should return list\\<int\\<1, max\\>\\> but returns list\\<int\\>\\.$#"
5456
count: 1
5557
path: tests/Compiler/Validator/Array/Data/ListItemValidatorWithMultipleValidatorsMapper.php
58+
-
59+
message: "#^Throwing checked exception ShipMonk\\\\InputMapper\\\\Runtime\\\\Exception\\\\MappingFailedException in first-class-callable!$#"
60+
count: 1
61+
path: tests/Compiler/Mapper/Object/Data/DelegateToIntCollectionMapper.php
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

+82-6
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,76 @@ 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+
$innerMappers = [];
68+
69+
foreach ($this->innerMapperCompilers as $key => $innerMapperCompiler) {
70+
$innerMappers[] = $this->compileInnerMapper($innerMapperCompiler, $key, $builder);
71+
}
72+
73+
return $innerMappers;
74+
}
75+
76+
private function compileInnerMapper(MapperCompiler $innerMapperCompiler, int $key, PhpCodeBuilder $builder): Expr
77+
{
78+
if ($innerMapperCompiler instanceof self && count($innerMapperCompiler->innerMapperCompilers) === 0) {
79+
$provider = $builder->propertyFetch($builder->var('this'), 'provider');
80+
$innerClassExpr = $builder->classConstFetch($builder->importClass($innerMapperCompiler->className), 'class');
81+
return $builder->methodCall($provider, 'get', [$innerClassExpr]);
82+
}
83+
84+
$innerMapperMethodName = $builder->uniqMethodName("mapInner{$key}");
85+
$innerMapperMethod = $builder->mapperMethod($innerMapperMethodName, $innerMapperCompiler)->makePrivate()->getNode();
86+
$builder->addMethod($innerMapperMethod);
87+
88+
$innerMapperMethodCallback = new MethodCall($builder->var('this'), $innerMapperMethodName, [new VariadicPlaceholder()]);
89+
return $builder->new($builder->importClass(CallbackMapper::class), [$innerMapperMethodCallback]);
90+
}
91+
92+
private function compileMapperExpr(PhpCodeBuilder $builder): CompiledExpr
93+
{
94+
foreach ($builder->getGenericParameters() as $offset => $genericParameter) {
95+
if ($this->className === $genericParameter->name) {
96+
$innerMappers = $builder->propertyFetch($builder->var('this'), 'innerMappers');
97+
$innerMapper = $builder->arrayDimFetch($innerMappers, $builder->val($offset));
98+
return new CompiledExpr($innerMapper);
99+
}
100+
}
101+
102+
$statements = [];
103+
$classNameExpr = $builder->classConstFetch($builder->importClass($this->className), 'class');
104+
$provider = $builder->propertyFetch($builder->var('this'), 'provider');
105+
$innerMappers = $this->compileInnerMappers($builder);
106+
107+
if (count($innerMappers) > 0) {
108+
$innerMappersVarName = $builder->uniqVariableName('innerMappers');
109+
$statements[] = $builder->assign($builder->var($innerMappersVarName), $builder->val($innerMappers));
110+
$getArguments = [$classNameExpr, $builder->var($innerMappersVarName)];
111+
112+
} else {
113+
$getArguments = [$classNameExpr];
114+
}
115+
116+
$mapper = $builder->methodCall($provider, 'get', $getArguments);
117+
return new CompiledExpr($mapper, $statements);
42118
}
43119

44120
}

src/Compiler/Mapper/Object/MapObject.php

+27-2
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
/**

0 commit comments

Comments
 (0)