• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

DoclerLabs / api-client-generator / 27691612716

17 Jun 2026 01:14PM UTC coverage: 86.919% (+0.07%) from 86.854%
27691612716

Pull #143

github

web-flow
Merge b33042ad1 into 185d12b92
Pull Request #143: fix: respect discriminator mapping in response body mappers

80 of 83 new or added lines in 2 files covered. (96.39%)

3535 of 4067 relevant lines covered (86.92%)

7.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.7
/src/Generator/SchemaMapperGenerator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace DoclerLabs\ApiClientGenerator\Generator;
6

7
use DateTimeImmutable;
8
use DoclerLabs\ApiClientException\UnexpectedResponseBodyException;
9
use DoclerLabs\ApiClientGenerator\Ast\Builder\ParameterBuilder;
10
use DoclerLabs\ApiClientGenerator\Ast\ParameterNode;
11
use DoclerLabs\ApiClientGenerator\Entity\Field;
12
use DoclerLabs\ApiClientGenerator\Input\Specification;
13
use DoclerLabs\ApiClientGenerator\Naming\SchemaMapperNaming;
14
use DoclerLabs\ApiClientGenerator\Output\Php\PhpFileCollection;
15
use PhpParser\Node\Expr;
16
use PhpParser\Node\Expr\Variable;
17
use PhpParser\Node\Stmt;
18
use PhpParser\Node\Stmt\Case_;
19
use PhpParser\Node\Stmt\ClassMethod;
20

21
class SchemaMapperGenerator extends MutatorAccessorClassGeneratorAbstract
22
{
23
    public const NAMESPACE_SUBPATH = '\\Schema\\Mapper';
24

25
    public const SUBDIRECTORY = 'Schema/Mapper/';
26

27
    private array $mapMethodThrownExceptions;
28

29
    public function generate(Specification $specification, PhpFileCollection $fileRegistry): void
30
    {
31
        foreach ($specification->getCompositeResponseFields() as $field) {
26✔
32
            /** @var Field $field */
33
            $this->generateMapper($fileRegistry, $field);
26✔
34
        }
35
    }
36

37
    protected function generateMapper(PhpFileCollection $fileRegistry, Field $root): void
38
    {
39
        $this->mapMethodThrownExceptions = [];
26✔
40

41
        $className    = SchemaMapperNaming::getClassName($root);
26✔
42
        $classBuilder = $this->builder
26✔
43
            ->class($className)
26✔
44
            ->implement('SchemaMapperInterface')
26✔
45
            ->addStmts($this->generateProperties($root))
26✔
46
            ->addStmt($this->generateConstructor($root))
26✔
47
            ->addStmt($this->generateMap($root));
26✔
48

49
        $this->registerFile($fileRegistry, $classBuilder, self::SUBDIRECTORY, self::NAMESPACE_SUBPATH);
26✔
50
    }
51

52
    protected function generateProperties(Field $root): array
53
    {
54
        if ($this->phpVersion->isConstructorPropertyPromotionSupported()) {
26✔
55
            return [];
16✔
56
        }
57

58
        $properties = [];
10✔
59
        if ($root->isObject()) {
10✔
60
            $alreadyInjected = [];
10✔
61
            foreach ($root->getObjectProperties() as $child) {
10✔
62
                if ($child->isComposite()) {
9✔
63
                    $childClassName = SchemaMapperNaming::getClassName($child);
7✔
64
                    if (!isset($alreadyInjected[$childClassName])) {
7✔
65
                        $propertyName = SchemaMapperNaming::getPropertyName($child);
7✔
66
                        $properties[] = $this->builder->localProperty($propertyName, $childClassName, $childClassName, readonly: true);
7✔
67

68
                        $alreadyInjected[$childClassName] = true;
7✔
69
                    }
70
                }
71
            }
72
        }
73

74
        if ($root->isArrayOfObjects()) {
10✔
75
            $child = $root->getArrayItem();
3✔
76
            if ($child !== null && $child->isComposite()) {
3✔
77
                $propertyName   = SchemaMapperNaming::getPropertyName($child);
3✔
78
                $childClassName = SchemaMapperNaming::getClassName($child);
3✔
79
                $properties[]   = $this->builder->localProperty(
3✔
80
                    $propertyName,
3✔
81
                    $childClassName,
3✔
82
                    $childClassName,
3✔
83
                    readonly: true
3✔
84
                );
3✔
85
            }
86
        }
87

88
        return $properties;
10✔
89
    }
90

91
    protected function generateConstructor(Field $root): ?ClassMethod
92
    {
93
        $params     = [];
26✔
94
        $paramInits = [];
26✔
95

96
        if ($root->isObject()) {
26✔
97
            $alreadyInjected = [];
26✔
98
            foreach ($root->getObjectProperties() as $child) {
26✔
99
                if ($child->isComposite()) {
23✔
100
                    $childClassName = SchemaMapperNaming::getClassName($child);
18✔
101
                    if (!isset($alreadyInjected[$childClassName])) {
18✔
102
                        $propertyName = SchemaMapperNaming::getPropertyName($child);
18✔
103
                        $params[]     = $this->builder
18✔
104
                            ->param($propertyName)
18✔
105
                            ->setType($childClassName);
18✔
106

107
                        $paramInits[] = $this->builder->assign(
18✔
108
                            $this->builder->localPropertyFetch($propertyName),
18✔
109
                            $this->builder->var($propertyName)
18✔
110
                        );
18✔
111
                        $alreadyInjected[$childClassName] = true;
18✔
112
                    }
113
                }
114
            }
115
        }
116

117
        if ($root->isArrayOfObjects()) {
26✔
118
            $child = $root->getArrayItem();
5✔
119
            if ($child !== null && $child->isComposite()) {
5✔
120
                $propertyName = SchemaMapperNaming::getPropertyName($child);
5✔
121
                $params[]     = $this->builder
5✔
122
                    ->param($propertyName)
5✔
123
                    ->setType(SchemaMapperNaming::getClassName($child));
5✔
124

125
                $paramInits[] = $this->builder->assign(
5✔
126
                    $this->builder->localPropertyFetch($propertyName),
5✔
127
                    $this->builder->var($propertyName)
5✔
128
                );
5✔
129
            }
130
        }
131

132
        if (empty($params)) {
26✔
133
            return null;
26✔
134
        }
135

136
        if ($this->phpVersion->isConstructorPropertyPromotionSupported()) {
18✔
137
            foreach ($params as $param) {
11✔
138
                $param->makePrivate();
11✔
139
            }
140
        }
141
        if ($this->phpVersion->isReadonlyPropertySupported()) {
18✔
142
            foreach ($params as $param) {
6✔
143
                $param->makeReadonly();
6✔
144
            }
145
        }
146

147
        $params = array_map(
18✔
148
            static fn (ParameterBuilder $param): ParameterNode => $param->getNode(),
18✔
149
            $params
18✔
150
        );
18✔
151

152
        $constructor = $this->builder
18✔
153
            ->method('__construct')
18✔
154
            ->makePublic()
18✔
155
            ->addParams($params)
18✔
156
            ->composeDocBlock($params);
18✔
157

158
        if (!$this->phpVersion->isConstructorPropertyPromotionSupported()) {
18✔
159
            $constructor->addStmts($paramInits);
7✔
160
        }
161

162
        return $constructor->getNode();
18✔
163
    }
164

165
    protected function generateMap(Field $root): ClassMethod
166
    {
167
        $statements = [];
26✔
168
        $builder    = $this->builder->param('payload');
26✔
169
        $builder->setType('array');
26✔
170
        $payloadParam = $builder->getNode();
26✔
171

172
        $payloadVariable = $this->builder->var('payload');
26✔
173
        $this->addImport(
26✔
174
            sprintf(
26✔
175
                '%s%s\\%s',
26✔
176
                $this->baseNamespace,
26✔
177
                SchemaGenerator::NAMESPACE_SUBPATH,
26✔
178
                $root->getPhpClassName()
26✔
179
            )
26✔
180
        );
26✔
181

182
        if ($root->isFreeFormObject()) {
26✔
183
            $statements[] = $this->generateMapStatementForFreeFormObject($root, $payloadVariable);
5✔
184
        } elseif ($root->isObject()) {
23✔
185
            $statements = array_merge(
23✔
186
                $statements,
23✔
187
                $this->generateMapStatementsForObject($root, $payloadVariable)
23✔
188
            );
23✔
189
        }
190

191
        if ($root->isArrayOfObjects()) {
26✔
192
            $statements = array_merge(
5✔
193
                $statements,
5✔
194
                $this->generateMapStatementsForArrayOfObjects($root, $payloadVariable)
5✔
195
            );
5✔
196
        }
197

198
        return $this->builder
26✔
199
            ->method('toSchema')
26✔
200
            ->makePublic()
26✔
201
            ->addParam($payloadParam)
26✔
202
            ->addStmts($statements)
26✔
203
            ->setReturnType($root->getPhpTypeHint())
26✔
204
            ->composeDocBlock(
26✔
205
                [$payloadParam],
26✔
206
                $root->getPhpDocType(false),
26✔
207
                array_keys($this->mapMethodThrownExceptions)
26✔
208
            )
26✔
209
            ->getNode();
26✔
210
    }
211

212
    protected function generateMapStatementsForArrayOfObjects(
213
        Field $field,
214
        Variable $payloadVariable
215
    ): array {
216
        $itemsVar     = $this->builder->var('items');
5✔
217
        $statements[] = $this->builder->assign($itemsVar, $this->builder->array([]));
5✔
218

219
        $payloadItemVariable = $this->builder->var('payloadItem');
5✔
220
        $itemMapper          = $this->builder->localPropertyFetch(
5✔
221
            SchemaMapperNaming::getPropertyName($field->getArrayItem())
5✔
222
        );
5✔
223
        $itemMapperCall      = $this->builder->methodCall($itemMapper, 'toSchema', [$payloadItemVariable]);
5✔
224
        $foreachStatements[] = $this->builder->appendToArray($itemsVar, $itemMapperCall);
5✔
225

226
        $statements[] = $this->builder->foreach($payloadVariable, $payloadItemVariable, $foreachStatements);
5✔
227

228
        $statements[] = $this->builder->return(
5✔
229
            $this->builder->new(
5✔
230
                $field->getPhpClassName(),
5✔
231
                [$this->builder->argument($itemsVar, false, true)]
5✔
232
            )
5✔
233
        );
5✔
234

235
        return $statements;
5✔
236
    }
237

238
    protected function generateMapStatementForFreeFormObject(Field $root, Variable $payloadVariable): Stmt
239
    {
240
        $schemaInit = $this->builder->new($root->getPhpClassName(), [$payloadVariable]);
5✔
241

242
        return $this->builder->return($schemaInit);
5✔
243
    }
244

245
    protected function hasComposite(array $fields): bool
246
    {
247
        foreach ($fields as $field) {
20✔
248
            if ($field->isComposite()) {
20✔
249
                return true;
18✔
250
            }
251
        }
252

253
        return false;
19✔
254
    }
255

256
    protected function generateMapStatementsForObject(Field $root, Variable $payloadVariable): array
257
    {
258
        $statements = [];
23✔
259

260
        $requiredFields        = [];
23✔
261
        $requiredItemsNames    = [];
23✔
262
        $requiredResponseItems = [];
23✔
263
        $optionalFields        = [];
23✔
264
        $optionalResponseItems = [];
23✔
265
        foreach ($root->getObjectProperties() as $property) {
23✔
266
            if ($property->isRequired()) {
23✔
267
                $requiredFields[]        = $property;
20✔
268
                $requiredItemName        = $this->builder->val($property->getName());
20✔
269
                $requiredItemsNames[]    = $requiredItemName;
20✔
270
                $requiredResponseItems[] = $this->builder->getArrayItem($payloadVariable, $requiredItemName);
20✔
271
            } else {
272
                $optionalFields[] = $property;
20✔
273
                if ($root->hasOneOf() || $root->hasAnyOf()) {
20✔
274
                    $optionalResponseItems[] = $payloadVariable;
10✔
275
                } else {
276
                    $optionalResponseItems[] = $this->builder->getArrayItem(
19✔
277
                        $payloadVariable,
19✔
278
                        $this->builder->val($property->getName())
19✔
279
                    );
19✔
280
                }
281
            }
282
        }
283

284
        if (!empty($requiredFields)) {
23✔
285
            $unexpectedResponseBodyException = 'UnexpectedResponseBodyException';
20✔
286
            $this->addImport(UnexpectedResponseBodyException::class);
20✔
287

288
            $missingFieldsVariable  = $this->builder->var('missingFields');
20✔
289
            $missingFieldsArrayKeys = $this->builder->funcCall('array_keys', [$payloadVariable]);
20✔
290
            $missingFieldsArrayDiff = $this->builder->funcCall(
20✔
291
                'array_diff',
20✔
292
                [$this->builder->array($requiredItemsNames), $missingFieldsArrayKeys]
20✔
293
            );
20✔
294
            $missingFieldsImplode = $this->builder->funcCall(
20✔
295
                'implode',
20✔
296
                [$this->builder->val(', '), $missingFieldsArrayDiff]
20✔
297
            );
20✔
298

299
            $statements[] = $this->builder->expr(
20✔
300
                $this->builder->assign($missingFieldsVariable, $missingFieldsImplode)
20✔
301
            );
20✔
302

303
            $requiredFieldsIfCondition = $this->builder->not(
20✔
304
                $this->builder->funcCall('empty', [$missingFieldsVariable])
20✔
305
            );
20✔
306

307
            $exceptionMsg = sprintf(
20✔
308
                'Required attributes for `%s` missing in the response body: ',
20✔
309
                $root->getPhpClassName()
20✔
310
            );
20✔
311

312
            $requiredFieldsIfStatements[] = $this->builder->throw(
20✔
313
                $unexpectedResponseBodyException,
20✔
314
                $this->builder->concat($this->builder->val($exceptionMsg), $missingFieldsVariable)
20✔
315
            );
20✔
316

317
            $statements[] = $this->builder->if($requiredFieldsIfCondition, $requiredFieldsIfStatements);
20✔
318

319
            $this->mapMethodThrownExceptions[$unexpectedResponseBodyException] = true;
20✔
320
        }
321

322
        $requiredVars = [];
23✔
323
        foreach ($requiredFields as $i => $field) {
23✔
324
            /** @var Field $field */
325
            if ($field->isComposite()) {
20✔
326
                if ($field->isNullable()) {
3✔
327
                    $requiredVars[] = $this->builder->ternary(
3✔
328
                        $this->builder->notEquals($requiredResponseItems[$i], $this->builder->val(null)),
3✔
329
                        $this->builder->methodCall(
3✔
330
                            $this->builder->localPropertyFetch(SchemaMapperNaming::getPropertyName($field)),
3✔
331
                            'toSchema',
3✔
332
                            [$requiredResponseItems[$i]]
3✔
333
                        ),
3✔
334
                        $this->builder->val(null)
3✔
335
                    );
3✔
336
                } else {
337
                    $requiredVars[] = $this->builder->methodCall(
3✔
338
                        $this->builder->localPropertyFetch(SchemaMapperNaming::getPropertyName($field)),
3✔
339
                        'toSchema',
3✔
340
                        [$requiredResponseItems[$i]]
3✔
341
                    );
3✔
342
                }
343
            } elseif ($field->isDate()) {
20✔
344
                $this->addImport(DateTimeImmutable::class);
6✔
345
                $newDateTime = $this->builder->new('DateTimeImmutable', [$requiredResponseItems[$i]]);
6✔
346
                if ($field->isNullable()) {
6✔
347
                    $requiredVars[] = $this->builder->ternary(
3✔
348
                        $this->builder->notEquals($requiredResponseItems[$i], $this->builder->val(null)),
3✔
349
                        $newDateTime,
3✔
350
                        $this->builder->val(null)
3✔
351
                    );
3✔
352
                } else {
353
                    $requiredVars[] = $newDateTime;
6✔
354
                }
355
            } elseif ($field->isEnum() && $this->phpVersion->isEnumSupported()) {
20✔
356
                $this->addImport($this->fqdn($this->withSubNamespace(SchemaGenerator::NAMESPACE_SUBPATH), $field->getPhpClassName()));
5✔
357
                $newEnum = $this->builder->staticCall($field->getPhpClassName(), 'from', [$requiredResponseItems[$i]]);
5✔
358
                if ($field->isNullable()) {
5✔
359
                    $requiredVars[] = $this->builder->ternary(
×
360
                        $this->builder->notEquals($requiredResponseItems[$i], $this->builder->val(null)),
×
361
                        $newEnum,
×
362
                        $this->builder->val(null)
×
363
                    );
×
364
                } else {
365
                    $requiredVars[] = $newEnum;
5✔
366
                }
367
            } elseif ($field->isArrayOfEnums() && $this->phpVersion->isEnumSupported()) {
20✔
368
                $enumField = $field->getArrayItem();
1✔
369
                $this->addImport($this->fqdn($this->withSubNamespace(SchemaGenerator::NAMESPACE_SUBPATH), $enumField->getPhpClassName()));
1✔
370

371
                $arrowFunctionParam = $this->builder->param('item')->setType($enumField->getType()->toPhpType())->getNode();
1✔
372
                $arrayMapCall       = $this->builder->funcCall(
1✔
373
                    'array_map',
1✔
374
                    [
1✔
375
                        $this->builder->arrowFunction(
1✔
376
                            $this->builder->staticCall($enumField->getPhpClassName(), 'from', [$this->builder->var('item')]),
1✔
377
                            [$arrowFunctionParam],
1✔
378
                            $enumField->getPhpClassName(),
1✔
379
                        ),
1✔
380
                        $requiredResponseItems[$i],
1✔
381
                    ]
1✔
382
                );
1✔
383
                $requiredVars[] = $field->isNullable()
1✔
384
                    ? $this->builder->ternary(
1✔
385
                        $this->builder->notEquals($requiredResponseItems[$i], $this->builder->val(null)),
1✔
386
                        $arrayMapCall,
1✔
387
                        $this->builder->val(null)
1✔
388
                    )
1✔
389
                    : $arrayMapCall;
1✔
390
            } else {
391
                $requiredVars[] = $requiredResponseItems[$i];
19✔
392
            }
393
        }
394
        $schemaInit = $this->builder->new($root->getPhpClassName(), $requiredVars);
23✔
395

396
        if (!empty($optionalFields)) {
23✔
397
            $schemaVar    = $this->builder->var('schema');
20✔
398
            $matchesVar   = $this->builder->var('matches');
20✔
399
            $statements[] = $this->builder->assign($schemaVar, $schemaInit);
20✔
400

401
            $tryCatchStatements = [];
20✔
402
            foreach ($optionalFields as $i => $field) {
20✔
403
                /** @var Field $field */
404
                if ($field->isComposite()) {
20✔
405
                    $mapper = $this->builder->localPropertyFetch(SchemaMapperNaming::getPropertyName($field));
18✔
406
                    if ($field->isNullable()) {
18✔
407
                        $optionalVar = $this->builder->ternary(
3✔
408
                            $this->builder->notEquals($optionalResponseItems[$i], $this->builder->val(null)),
3✔
409
                            $this->builder->methodCall(
3✔
410
                                $mapper,
3✔
411
                                'toSchema',
3✔
412
                                [$optionalResponseItems[$i]]
3✔
413
                            ),
3✔
414
                            $this->builder->val(null)
3✔
415
                        );
3✔
416
                    } else {
417
                        $optionalVar = $this->builder->methodCall(
18✔
418
                            $mapper,
18✔
419
                            'toSchema',
18✔
420
                            [$optionalResponseItems[$i]]
18✔
421
                        );
18✔
422
                    }
423
                } elseif ($field->isDate()) {
19✔
424
                    $this->addImport(DateTimeImmutable::class);
5✔
425
                    if ($field->isNullable()) {
5✔
426
                        $optionalVar = $this->builder->ternary(
3✔
427
                            $this->builder->notEquals($optionalResponseItems[$i], $this->builder->val(null)),
3✔
428
                            $this->builder->new('DateTimeImmutable', [$optionalResponseItems[$i]]),
3✔
429
                            $this->builder->val(null)
3✔
430
                        );
3✔
431
                    } else {
432
                        $optionalVar = $this->builder->new('DateTimeImmutable', [$optionalResponseItems[$i]]);
5✔
433
                    }
434
                } elseif ($field->isEnum() && $this->phpVersion->isEnumSupported()) {
19✔
435
                    $this->addImport($this->fqdn($this->withSubNamespace(SchemaGenerator::NAMESPACE_SUBPATH), $field->getPhpClassName()));
1✔
436
                    $optionalVar = $this->builder->staticCall($field->getPhpClassName(), 'from', [$optionalResponseItems[$i]]);
1✔
437
                } elseif ($field->isArrayOfEnums() && $this->phpVersion->isEnumSupported()) {
19✔
438
                    $enumField = $field->getArrayItem();
1✔
439
                    $this->addImport($this->fqdn($this->withSubNamespace(SchemaGenerator::NAMESPACE_SUBPATH), $enumField->getPhpClassName()));
1✔
440

441
                    $arrowFunctionParam = $this->builder->param('item')->setType($enumField->getType()->toPhpType())->getNode();
1✔
442
                    $arrayMapCall       = $this->builder->funcCall(
1✔
443
                        'array_map',
1✔
444
                        [
1✔
445
                            $this->builder->arrowFunction(
1✔
446
                                $this->builder->staticCall($enumField->getPhpClassName(), 'from', [$this->builder->var('item')]),
1✔
447
                                [$arrowFunctionParam],
1✔
448
                                $enumField->getPhpClassName(),
1✔
449
                            ),
1✔
450
                            $optionalResponseItems[$i],
1✔
451
                        ]
1✔
452
                    );
1✔
453
                    $optionalVar = $field->isNullable()
1✔
454
                        ? $this->builder->ternary(
1✔
455
                            $this->builder->notEquals($optionalResponseItems[$i], $this->builder->val(null)),
1✔
456
                            $arrayMapCall,
1✔
457
                            $this->builder->val(null)
1✔
458
                        )
1✔
459
                        : $arrayMapCall;
1✔
460
                } else {
461
                    $optionalVar = $optionalResponseItems[$i];
18✔
462
                }
463

464
                if ($root->hasOneOf() || $root->hasAnyOf()) {
20✔
465
                    $tryStatements = [
10✔
466
                        $this->builder->expr(
10✔
467
                            $this->builder->methodCall(
10✔
468
                                $schemaVar,
10✔
469
                                $this->getSetMethodName($field),
10✔
470
                                [$optionalVar]
10✔
471
                            )
10✔
472
                        ),
10✔
473
                    ];
10✔
474

475
                    $tryStatements[] = $this->builder->expr($this->builder->assign(
10✔
476
                        $this->builder->var('matches'),
10✔
477
                        $this->builder->operation($this->builder->var('matches'), '+', $this->builder->val(1))
10✔
478
                    ));
10✔
479

480
                    $this->addImport(UnexpectedResponseBodyException::class);
10✔
481
                    $catchStatement = $this->builder->catch(
10✔
482
                        [$this->builder->className('UnexpectedResponseBodyException')],
10✔
483
                        $this->builder->var('exception'),
10✔
484
                        []
10✔
485
                    );
10✔
486
                    $tryCatchStatements[] = $this->builder->tryCatch($tryStatements, [$catchStatement]);
10✔
487
                } else {
488
                    $ifCondition = $field->isNullable()
19✔
489
                        ? $this->builder->funcCall('array_key_exists', [$field->getName(), $payloadVariable])
7✔
490
                        : $this->builder->funcCall('isset', [$optionalResponseItems[$i]]);
19✔
491

492
                    $ifStmt = $this->builder->expr(
19✔
493
                        $this->builder->methodCall(
19✔
494
                            $schemaVar,
19✔
495
                            $this->getSetMethodName($field),
19✔
496
                            [$optionalVar]
19✔
497
                        )
19✔
498
                    );
19✔
499

500
                    $statements[] = $this->builder->if($ifCondition, [$ifStmt]);
19✔
501
                }
502
            }
503

504
            if ($root->hasOneOf() || $root->hasAnyOf()) {
20✔
505
                if ($root->getDiscriminator()) {
10✔
506
                    $statements[] = $this->generateDiscriminatorStatement($root, $payloadVariable);
4✔
507
                } else {
508
                    $statements[] = $this->builder->assign($matchesVar, $this->builder->val(0));
6✔
509

510
                    $statements = [...$statements, ...$tryCatchStatements];
6✔
511

512
                    if ($root->hasAnyOf()) {
6✔
513
                        $statements[] = $this->builder->if(
3✔
514
                            $this->builder->equals($matchesVar, $this->builder->val(0)),
3✔
515
                            [
3✔
516
                                $this->builder->throw(
3✔
517
                                    'UnexpectedResponseBodyException'
3✔
518
                                ),
3✔
519
                            ]
3✔
520
                        );
3✔
521
                    }
522
                    if ($root->hasOneOf()) {
6✔
523
                        $statements[] = $this->builder->if(
3✔
524
                            $this->builder->notEquals($matchesVar, $this->builder->val(1)),
3✔
525
                            [
3✔
526
                                $this->builder->throw(
3✔
527
                                    'UnexpectedResponseBodyException'
3✔
528
                                ),
3✔
529
                            ]
3✔
530
                        );
3✔
531
                    }
532
                    $this->mapMethodThrownExceptions['UnexpectedResponseBodyException'] = true;
6✔
533
                }
534
            }
535

536
            if (!$this->hasComposite($optionalFields)) {
20✔
537
                $this->addImport(UnexpectedResponseBodyException::class);
19✔
538
                $statements[] = $this->builder->if(
19✔
539
                    $this->builder->funcCall('empty', [$this->builder->methodCall($schemaVar, 'toArray')]),
19✔
540
                    [
19✔
541
                        $this->builder->throw(
19✔
542
                            'UnexpectedResponseBodyException'
19✔
543
                        ),
19✔
544
                    ]
19✔
545
                );
19✔
546
                $this->mapMethodThrownExceptions['UnexpectedResponseBodyException'] = true;
19✔
547
            }
548

549
            $statements[] = $this->builder->return($schemaVar);
20✔
550
        } else {
551
            $statements[] = $this->builder->return($schemaInit);
13✔
552
        }
553

554
        return $statements;
23✔
555
    }
556

557
    private function generateDiscriminatorStatement(Field $root, Variable $payloadVariable): Stmt
558
    {
559
        $discriminator = $root->getDiscriminator();
4✔
560

561
        /** @phpstan-ignore-next-line */
562
        $propertyName = $discriminator->propertyName;
4✔
563

564
        $ifCondition = $this->builder->funcCall('array_key_exists', [
4✔
565
            $propertyName,
4✔
566
            $payloadVariable,
4✔
567
        ]);
4✔
568

569
        $payloadDiscriminator = $this->builder->getArrayItem(
4✔
570
            $payloadVariable,
4✔
571
            $this->builder->val($propertyName)
4✔
572
        );
4✔
573

574
        $fallbackStatements = $this->generateDiscriminatorFallbackStatements($payloadDiscriminator, $payloadVariable);
4✔
575

576
        /** @phpstan-ignore-next-line */
577
        $mapping = $discriminator->mapping ?? [];
4✔
578
        $cases   = $this->generateDiscriminatorMappingCases($root, $mapping, $payloadVariable);
4✔
579

580
        if ($cases === []) {
4✔
581
            return $this->builder->if($ifCondition, $fallbackStatements);
1✔
582
        }
583

584
        $defaultStatements = [...$fallbackStatements, $this->builder->break()];
3✔
585
        $cases[]           = $this->builder->default(...$defaultStatements);
3✔
586

587
        return $this->builder->if($ifCondition, [$this->builder->switch($payloadDiscriminator, ...$cases)]);
3✔
588
    }
589

590
    /**
591
     * @return Stmt[]
592
     */
593
    private function generateDiscriminatorFallbackStatements(Expr $payloadDiscriminator, Variable $payloadVariable): array
594
    {
595
        $assignMethodName = $this->builder->expr(
4✔
596
            $this->builder->assign(
4✔
597
                $this->builder->var('methodName'),
4✔
598
                $this->builder->concat(
4✔
599
                    $this->builder->val('set'),
4✔
600
                    $this->builder->funcCall('ucfirst', [$payloadDiscriminator])
4✔
601
                )
4✔
602
            )
4✔
603
        );
4✔
604

605
        $assignMapperName = $this->builder->expr(
4✔
606
            $this->builder->assign(
4✔
607
                $this->builder->var('mapperName'),
4✔
608
                $this->builder->concat(
4✔
609
                    $payloadDiscriminator,
4✔
610
                    $this->builder->val('Mapper')
4✔
611
                )
4✔
612
            )
4✔
613
        );
4✔
614

615
        $schemaMethodCall = $this->builder->expr(
4✔
616
            $this->builder->methodCall(
4✔
617
                $this->builder->var('schema'),
4✔
618
                '$methodName',
4✔
619
                [
4✔
620
                    $this->builder->methodCall(
4✔
621
                        $this->builder->localPropertyFetch('$mapperName'),
4✔
622
                        'toSchema',
4✔
623
                        [$payloadVariable]
4✔
624
                    ),
4✔
625
                ]
4✔
626
            )
4✔
627
        );
4✔
628

629
        return [$assignMethodName, $assignMapperName, $schemaMethodCall];
4✔
630
    }
631

632
    /**
633
     * @param string[] $mapping
634
     *
635
     * @return Case_[]
636
     */
637
    private function generateDiscriminatorMappingCases(Field $root, array $mapping, Variable $payloadVariable): array
638
    {
639
        $childrenByClassName = [];
4✔
640
        foreach ($root->getObjectProperties() as $child) {
4✔
641
            if ($child->isComposite()) {
4✔
642
                $childrenByClassName[$child->getPhpClassName()] = $child;
4✔
643
            }
644
        }
645

646
        $cases = [];
4✔
647
        foreach ($mapping as $discriminatorValue => $reference) {
4✔
648
            $schemaName = $this->resolveSchemaNameFromReference($reference);
3✔
649
            if (!isset($childrenByClassName[$schemaName])) {
3✔
NEW
650
                continue;
×
651
            }
652

653
            $child   = $childrenByClassName[$schemaName];
3✔
654
            $cases[] = $this->builder->case(
3✔
655
                $this->builder->val((string)$discriminatorValue),
3✔
656
                $this->builder->expr(
3✔
657
                    $this->builder->methodCall(
3✔
658
                        $this->builder->var('schema'),
3✔
659
                        $this->getSetMethodName($child),
3✔
660
                        [
3✔
661
                            $this->builder->methodCall(
3✔
662
                                $this->builder->localPropertyFetch(SchemaMapperNaming::getPropertyName($child)),
3✔
663
                                'toSchema',
3✔
664
                                [$payloadVariable]
3✔
665
                            ),
3✔
666
                        ]
3✔
667
                    )
3✔
668
                ),
3✔
669
                $this->builder->break()
3✔
670
            );
3✔
671
        }
672

673
        return $cases;
4✔
674
    }
675

676
    private function resolveSchemaNameFromReference(string $reference): string
677
    {
678
        $segments = explode('/', $reference);
3✔
679

680
        return (string)end($segments);
3✔
681
    }
682
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc