57
57
use ShipMonk \InputMapper \Compiler \Validator \Int \AssertPositiveInt ;
58
58
use ShipMonk \InputMapper \Compiler \Validator \ValidatorCompiler ;
59
59
use ShipMonk \InputMapper \Runtime \Optional ;
60
+ use function array_column ;
61
+ use function array_fill_keys ;
60
62
use function class_exists ;
61
63
use function class_implements ;
62
64
use function class_parents ;
63
65
use function count ;
64
- use function enum_exists ;
65
66
use function interface_exists ;
67
+ use function is_array ;
66
68
use function strcasecmp ;
67
69
use function strtolower ;
68
70
use function substr ;
@@ -71,6 +73,7 @@ class DefaultMapperCompilerFactory implements MapperCompilerFactory
71
73
{
72
74
73
75
final public const DELEGATE_OBJECT_MAPPING = 'delegateObjectMapping ' ;
76
+ final public const GENERIC_PARAMETERS = 'genericParameters ' ;
74
77
75
78
/**
76
79
* @param array<class-string, callable(class-string, array<string, mixed>): MapperCompiler> $mapperCompilerFactories
@@ -102,13 +105,21 @@ public function create(TypeNode $type, array $options = []): MapperCompiler
102
105
{
103
106
if ($ type instanceof IdentifierTypeNode) {
104
107
if (!PhpDocTypeUtils::isKeyword ($ type )) {
105
- if (!class_exists ($ type ->name ) && !interface_exists ($ type ->name ) && !enum_exists ($ type ->name )) {
108
+ if (isset ($ options [self ::DELEGATE_OBJECT_MAPPING ]) && $ options [self ::DELEGATE_OBJECT_MAPPING ] === true ) {
109
+ if (!class_exists ($ type ->name ) && !interface_exists ($ type ->name )) {
110
+ if (!isset ($ options [self ::GENERIC_PARAMETERS ]) || !is_array ($ options [self ::GENERIC_PARAMETERS ]) || !isset ($ options [self ::GENERIC_PARAMETERS ][$ type ->name ])) {
111
+ throw CannotCreateMapperCompilerException::fromType ($ type , 'there is no class, interface or enum with this name ' );
112
+ }
113
+ }
114
+
115
+ return new DelegateMapperCompiler ($ type ->name );
116
+ }
117
+
118
+ if (!class_exists ($ type ->name ) && !interface_exists ($ type ->name )) {
106
119
throw CannotCreateMapperCompilerException::fromType ($ type , 'there is no class, interface or enum with this name ' );
107
120
}
108
121
109
- return isset ($ options [self ::DELEGATE_OBJECT_MAPPING ]) && $ options [self ::DELEGATE_OBJECT_MAPPING ] === true
110
- ? new DelegateMapperCompiler ($ type ->name )
111
- : $ this ->createObjectMapperCompiler ($ type ->name , $ options );
122
+ return $ this ->createObjectMapperCompiler ($ type ->name , $ options );
112
123
}
113
124
114
125
return match (strtolower ($ type ->name )) {
@@ -164,7 +175,7 @@ public function create(TypeNode $type, array $options = []): MapperCompiler
164
175
1 => new MapOptional ($ this ->createInner ($ type ->genericTypes [0 ], $ options )),
165
176
default => throw CannotCreateMapperCompilerException::fromType ($ type ),
166
177
},
167
- default => throw CannotCreateMapperCompilerException:: fromType ($ type ),
178
+ default => $ this -> createFromGenericType ($ type, $ options ),
168
179
},
169
180
};
170
181
}
@@ -218,13 +229,45 @@ protected function createInner(TypeNode $type, array $options): MapperCompiler
218
229
return $ this ->create ($ type , $ options );
219
230
}
220
231
232
+ /**
233
+ * @param array<string, mixed> $options
234
+ */
235
+ protected function createFromGenericType (GenericTypeNode $ type , array $ options ): MapperCompiler
236
+ {
237
+ if (!class_exists ($ type ->type ->name ) && !interface_exists ($ type ->type ->name )) {
238
+ throw CannotCreateMapperCompilerException::fromType ($ type , 'there is no class or interface with this name ' );
239
+ }
240
+
241
+ $ genericParameters = PhpDocTypeUtils::getGenericTypeDefinition ($ type ->type )->parameters ;
242
+ $ innerMapperCompilers = [];
243
+
244
+ foreach ($ type ->genericTypes as $ index => $ genericType ) {
245
+ $ genericParameter = $ genericParameters [$ index ] ?? throw CannotCreateMapperCompilerException::fromType ($ type , "generic parameter at index {$ index } does not exist " );
246
+
247
+ if ($ genericParameter ->bound !== null && !PhpDocTypeUtils::isSubTypeOf ($ genericType , $ genericParameter ->bound )) {
248
+ throw CannotCreateMapperCompilerException::fromType ($ type , "type {$ genericType } is not a subtype of {$ genericParameter ->bound }" );
249
+ }
250
+
251
+ $ innerMapperCompilers [] = $ this ->createInner ($ genericType , $ options );
252
+ }
253
+
254
+ return new DelegateMapperCompiler ($ type ->type ->name , $ innerMapperCompilers );
255
+ }
256
+
221
257
/**
222
258
* @param class-string $inputClassName
223
259
* @param array<string, mixed> $options
224
260
*/
225
261
protected function createObjectMapperCompiler (string $ inputClassName , array $ options ): MapperCompiler
226
262
{
227
- $ classLikeNames = [$ inputClassName => true , ...class_parents ($ inputClassName ), ...class_implements ($ inputClassName )];
263
+ $ classParents = class_parents ($ inputClassName );
264
+ $ classImplements = class_implements ($ inputClassName );
265
+
266
+ if ($ classParents === false || $ classImplements === false ) {
267
+ throw new LogicException ("Unable to get class parents or implements for ' $ inputClassName'. " );
268
+ }
269
+
270
+ $ classLikeNames = [$ inputClassName => true , ...$ classParents , ...$ classImplements ];
228
271
229
272
foreach ($ classLikeNames as $ classLikeName => $ _ ) {
230
273
if (isset ($ this ->mapperCompilerFactories [$ classLikeName ])) {
@@ -245,20 +288,24 @@ protected function createObjectMappingByConstructorInvocation(
245
288
array $ options ,
246
289
): MapperCompiler
247
290
{
291
+ $ inputType = new IdentifierTypeNode ($ inputClassName );
248
292
$ classReflection = new ReflectionClass ($ inputClassName );
249
-
250
293
$ constructor = $ classReflection ->getConstructor ();
251
294
252
295
if ($ constructor === null ) {
253
- throw CannotCreateMapperCompilerException::fromType (new IdentifierTypeNode ( $ inputClassName ) , 'class has no constructor ' );
296
+ throw CannotCreateMapperCompilerException::fromType ($ inputType , 'class has no constructor ' );
254
297
}
255
298
256
299
if (!$ constructor ->isPublic ()) {
257
- throw CannotCreateMapperCompilerException::fromType (new IdentifierTypeNode ( $ inputClassName ) , 'class has a non-public constructor ' );
300
+ throw CannotCreateMapperCompilerException::fromType ($ inputType , 'class has a non-public constructor ' );
258
301
}
259
302
303
+ $ genericParameters = PhpDocTypeUtils::getGenericTypeDefinition ($ inputType )->parameters ;
304
+ $ genericParameterNames = array_column ($ genericParameters , 'name ' );
305
+ $ options [self ::GENERIC_PARAMETERS ] = array_fill_keys ($ genericParameterNames , true );
306
+
260
307
$ constructorParameterMapperCompilers = [];
261
- $ constructorParameterTypes = $ this ->getConstructorParameterTypes ($ constructor );
308
+ $ constructorParameterTypes = $ this ->getConstructorParameterTypes ($ constructor, $ genericParameterNames );
262
309
263
310
foreach ($ constructor ->getParameters () as $ parameter ) {
264
311
$ name = $ parameter ->getName ();
@@ -267,13 +314,14 @@ protected function createObjectMappingByConstructorInvocation(
267
314
}
268
315
269
316
$ allowExtraKeys = count ($ classReflection ->getAttributes (AllowExtraKeys::class)) > 0 ;
270
- return new MapObject ($ classReflection ->getName (), $ constructorParameterMapperCompilers , $ allowExtraKeys );
317
+ return new MapObject ($ classReflection ->getName (), $ constructorParameterMapperCompilers , $ allowExtraKeys, $ genericParameters );
271
318
}
272
319
273
320
/**
321
+ * @param list<string> $genericParameterNames
274
322
* @return array<string, TypeNode>
275
323
*/
276
- protected function getConstructorParameterTypes (ReflectionMethod $ constructor ): array
324
+ protected function getConstructorParameterTypes (ReflectionMethod $ constructor, array $ genericParameterNames ): array
277
325
{
278
326
$ class = $ constructor ->getDeclaringClass ();
279
327
$ parameterTypes = [];
@@ -290,7 +338,7 @@ protected function getConstructorParameterTypes(ReflectionMethod $constructor):
290
338
if ($ constructorDocComment !== false ) {
291
339
foreach ($ this ->parsePhpDoc ($ constructorDocComment )->children as $ node ) {
292
340
if ($ node instanceof PhpDocTagNode && $ node ->value instanceof ParamTagValueNode) {
293
- PhpDocTypeUtils::resolve ($ node ->value ->type , $ class );
341
+ PhpDocTypeUtils::resolve ($ node ->value ->type , $ class, $ genericParameterNames );
294
342
$ parameterName = substr ($ node ->value ->parameterName , 1 );
295
343
$ parameterTypes [$ parameterName ] = $ node ->value ->type ;
296
344
}
@@ -312,7 +360,7 @@ protected function getConstructorParameterTypes(ReflectionMethod $constructor):
312
360
&& $ node ->value instanceof VarTagValueNode
313
361
&& ($ node ->value ->variableName === '' || substr ($ node ->value ->variableName , 1 ) === $ parameterName )
314
362
) {
315
- PhpDocTypeUtils::resolve ($ node ->value ->type , $ class );
363
+ PhpDocTypeUtils::resolve ($ node ->value ->type , $ class, $ genericParameterNames );
316
364
$ parameterTypes [$ parameterName ] = $ node ->value ->type ;
317
365
}
318
366
}
0 commit comments