6
6
use DateTimeImmutable ;
7
7
use DateTimeInterface ;
8
8
use LogicException ;
9
+ use Nette \Utils \Arrays ;
9
10
use PHPStan \PhpDocParser \Ast \ConstExpr \ConstExprIntegerNode ;
10
11
use PHPStan \PhpDocParser \Ast \ConstExpr \ConstExprStringNode ;
11
12
use PHPStan \PhpDocParser \Ast \PhpDoc \ParamTagValueNode ;
12
13
use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocNode ;
13
14
use PHPStan \PhpDocParser \Ast \PhpDoc \PhpDocTagNode ;
15
+ use PHPStan \PhpDocParser \Ast \PhpDoc \TemplateTagValueNode ;
14
16
use PHPStan \PhpDocParser \Ast \PhpDoc \VarTagValueNode ;
15
17
use PHPStan \PhpDocParser \Ast \Type \ArrayShapeNode ;
16
18
use PHPStan \PhpDocParser \Ast \Type \ArrayTypeNode ;
48
50
use ShipMonk \InputMapper \Compiler \Mapper \Wrapper \MapNullable ;
49
51
use ShipMonk \InputMapper \Compiler \Mapper \Wrapper \MapOptional ;
50
52
use ShipMonk \InputMapper \Compiler \Mapper \Wrapper \ValidatedMapperCompiler ;
53
+ use ShipMonk \InputMapper \Compiler \Type \GenericTypeParameter ;
54
+ use ShipMonk \InputMapper \Compiler \Type \GenericTypeVariance ;
51
55
use ShipMonk \InputMapper \Compiler \Type \PhpDocTypeUtils ;
52
56
use ShipMonk \InputMapper \Compiler \Validator \Array \AssertListLength ;
53
57
use ShipMonk \InputMapper \Compiler \Validator \Int \AssertIntRange ;
57
61
use ShipMonk \InputMapper \Compiler \Validator \Int \AssertPositiveInt ;
58
62
use ShipMonk \InputMapper \Compiler \Validator \ValidatorCompiler ;
59
63
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 ;
60
68
use function class_exists ;
61
69
use function class_implements ;
62
70
use function class_parents ;
63
71
use function count ;
64
72
use function enum_exists ;
65
73
use function interface_exists ;
74
+ use function str_ends_with ;
66
75
use function strcasecmp ;
67
76
use function strtolower ;
68
77
use function substr ;
@@ -71,6 +80,7 @@ class DefaultMapperCompilerFactory implements MapperCompilerFactory
71
80
{
72
81
73
82
final public const DELEGATE_OBJECT_MAPPING = 'delegateObjectMapping ' ;
83
+ final public const GENERIC_PARAMETERS = 'genericParameters ' ;
74
84
75
85
/**
76
86
* @param array<class-string, callable(class-string, array<string, mixed>): MapperCompiler> $mapperCompilerFactories
@@ -102,7 +112,7 @@ public function create(TypeNode $type, array $options = []): MapperCompiler
102
112
{
103
113
if ($ type instanceof IdentifierTypeNode) {
104
114
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 ]) ) {
106
116
throw CannotCreateMapperCompilerException::fromType ($ type , 'there is no class, interface or enum with this name ' );
107
117
}
108
118
@@ -164,7 +174,7 @@ public function create(TypeNode $type, array $options = []): MapperCompiler
164
174
1 => new MapOptional ($ this ->createInner ($ type ->genericTypes [0 ], $ options )),
165
175
default => throw CannotCreateMapperCompilerException::fromType ($ type ),
166
176
},
167
- default => throw CannotCreateMapperCompilerException:: fromType ($ type ),
177
+ default => $ this -> createFromGenericType ($ type, $ options ),
168
178
},
169
179
};
170
180
}
@@ -218,6 +228,27 @@ protected function createInner(TypeNode $type, array $options): MapperCompiler
218
228
return $ this ->create ($ type , $ options );
219
229
}
220
230
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
+
221
252
/**
222
253
* @param class-string $inputClassName
223
254
* @param array<string, mixed> $options
@@ -257,8 +288,12 @@ protected function createObjectMappingByConstructorInvocation(
257
288
throw CannotCreateMapperCompilerException::fromType (new IdentifierTypeNode ($ inputClassName ), 'class has a non-public constructor ' );
258
289
}
259
290
291
+ $ genericParameters = $ this ->getGenericParameters ($ inputClassName );
292
+ $ genericParameterNames = array_column ($ genericParameters , 'name ' );
293
+ $ options [self ::GENERIC_PARAMETERS ] = array_fill_keys ($ genericParameterNames , true );
294
+
260
295
$ constructorParameterMapperCompilers = [];
261
- $ constructorParameterTypes = $ this ->getConstructorParameterTypes ($ constructor );
296
+ $ constructorParameterTypes = $ this ->getConstructorParameterTypes ($ constructor, $ genericParameterNames );
262
297
263
298
foreach ($ constructor ->getParameters () as $ parameter ) {
264
299
$ name = $ parameter ->getName ();
@@ -267,13 +302,54 @@ protected function createObjectMappingByConstructorInvocation(
267
302
}
268
303
269
304
$ 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 );
271
346
}
272
347
273
348
/**
349
+ * @param list<string> $genericParameterNames
274
350
* @return array<string, TypeNode>
275
351
*/
276
- protected function getConstructorParameterTypes (ReflectionMethod $ constructor ): array
352
+ protected function getConstructorParameterTypes (ReflectionMethod $ constructor, array $ genericParameterNames ): array
277
353
{
278
354
$ class = $ constructor ->getDeclaringClass ();
279
355
$ parameterTypes = [];
@@ -290,7 +366,7 @@ protected function getConstructorParameterTypes(ReflectionMethod $constructor):
290
366
if ($ constructorDocComment !== false ) {
291
367
foreach ($ this ->parsePhpDoc ($ constructorDocComment )->children as $ node ) {
292
368
if ($ node instanceof PhpDocTagNode && $ node ->value instanceof ParamTagValueNode) {
293
- PhpDocTypeUtils::resolve ($ node ->value ->type , $ class );
369
+ PhpDocTypeUtils::resolve ($ node ->value ->type , $ class, $ genericParameterNames );
294
370
$ parameterName = substr ($ node ->value ->parameterName , 1 );
295
371
$ parameterTypes [$ parameterName ] = $ node ->value ->type ;
296
372
}
@@ -312,7 +388,7 @@ protected function getConstructorParameterTypes(ReflectionMethod $constructor):
312
388
&& $ node ->value instanceof VarTagValueNode
313
389
&& ($ node ->value ->variableName === '' || substr ($ node ->value ->variableName , 1 ) === $ parameterName )
314
390
) {
315
- PhpDocTypeUtils::resolve ($ node ->value ->type , $ class );
391
+ PhpDocTypeUtils::resolve ($ node ->value ->type , $ class, $ genericParameterNames );
316
392
$ parameterTypes [$ parameterName ] = $ node ->value ->type ;
317
393
}
318
394
}
0 commit comments