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

DoclerLabs / api-client-generator / 9254068657

27 May 2024 11:30AM UTC coverage: 86.981% (-1.4%) from 88.428%
9254068657

push

github

web-flow
Merge pull request #112 from DoclerLabs/php81

php 8.1 features

106 of 172 new or added lines in 20 files covered. (61.63%)

4 existing lines in 2 files now uncovered.

2913 of 3349 relevant lines covered (86.98%)

4.92 hits per line

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

96.5
/src/Generator/RequestGenerator.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace DoclerLabs\ApiClientGenerator\Generator;
6

7
use DateTimeInterface;
8
use DoclerLabs\ApiClientGenerator\Ast\Builder\CodeBuilder;
9
use DoclerLabs\ApiClientGenerator\Ast\Builder\ParameterBuilder;
10
use DoclerLabs\ApiClientGenerator\Ast\ParameterNode;
11
use DoclerLabs\ApiClientGenerator\Ast\PhpVersion;
12
use DoclerLabs\ApiClientGenerator\Entity\Field;
13
use DoclerLabs\ApiClientGenerator\Entity\Operation;
14
use DoclerLabs\ApiClientGenerator\Entity\Request;
15
use DoclerLabs\ApiClientGenerator\Generator\Security\SecurityStrategyInterface;
16
use DoclerLabs\ApiClientGenerator\Input\InvalidSpecificationException;
17
use DoclerLabs\ApiClientGenerator\Input\Specification;
18
use DoclerLabs\ApiClientGenerator\Naming\CopiedNamespace;
19
use DoclerLabs\ApiClientGenerator\Naming\RequestNaming;
20
use DoclerLabs\ApiClientGenerator\Output\Copy\Schema\SerializableInterface;
21
use DoclerLabs\ApiClientGenerator\Output\Php\PhpFileCollection;
22
use PhpParser\Node\Expr\FuncCall;
23
use PhpParser\Node\Stmt\ClassMethod;
24

25
class RequestGenerator extends MutatorAccessorClassGeneratorAbstract
26
{
27
    public const NAMESPACE_SUBPATH = '\\Request';
28

29
    public const SUBDIRECTORY = 'Request/';
30

31
    /** @var SecurityStrategyInterface[] */
32
    private array $securityStrategies;
33

34
    public function __construct(
35
        string $baseNamespace,
36
        CodeBuilder $builder,
37
        PhpVersion $phpVersion,
38
        SecurityStrategyInterface ...$securityStrategies
39
    ) {
40
        parent::__construct($baseNamespace, $builder, $phpVersion);
22✔
41

42
        $this->securityStrategies = $securityStrategies;
22✔
43
    }
22✔
44

45
    public function generate(Specification $specification, PhpFileCollection $fileRegistry): void
46
    {
47
        foreach ($specification->getOperations() as $operation) {
22✔
48
            $this->generateRequest($fileRegistry, $operation, $specification);
22✔
49
        }
50
    }
22✔
51

52
    protected function generateRequest(
53
        PhpFileCollection $fileRegistry,
54
        Operation $operation,
55
        Specification $specification
56
    ): void {
57
        $className = RequestNaming::getClassName($operation);
22✔
58
        $request   = $operation->request;
22✔
59

60
        $classBuilder = $this
22✔
61
            ->builder
22✔
62
            ->class($className)
22✔
63
            ->implement('RequestInterface')
22✔
64
            ->addStmts($this->generateEnums($request))
22✔
65
            ->addStmts($this->generateProperties($request, $operation, $specification))
22✔
66
            ->addStmt($this->generateConstructor($request, $operation, $specification))
22✔
67
            ->addStmt($this->generateGetContentType())
22✔
68
            ->addStmts($this->generateSetters($request))
22✔
69
            ->addStmt($this->generateGetMethod($request))
22✔
70
            ->addStmt($this->generateGetRoute($request))
22✔
71
            ->addStmts($this->generateGetParametersMethods($request, $operation, $specification));
22✔
72

73
        foreach ($this->securityStrategies as $securityStrategy) {
22✔
74
            $this->getImports()->append($securityStrategy->getImports($this->baseNamespace));
22✔
75
        }
76

77
        $this->registerFile($fileRegistry, $classBuilder, self::SUBDIRECTORY, self::NAMESPACE_SUBPATH);
22✔
78
    }
22✔
79

80
    protected function generateEnums(Request $request): array
81
    {
82
        $statements = [];
22✔
83
        foreach ($request->fields as $field) {
22✔
84
            foreach ($this->generateEnumStatements($field) as $statement) {
22✔
85
                $statements[] = $statement;
11✔
86
            }
87
        }
88

89
        return $statements;
22✔
90
    }
91

92
    protected function generateProperties(Request $request, Operation $operation, Specification $specification): array
93
    {
94
        $statements = [];
22✔
95

96
        foreach ($request->fields as $field) {
22✔
97
            if ($field->isComposite()) {
22✔
98
                $this->addImport(
22✔
99
                    sprintf(
22✔
100
                        '%s%s\\%s',
22✔
101
                        $this->baseNamespace,
22✔
102
                        SchemaGenerator::NAMESPACE_SUBPATH,
22✔
103
                        $field->getPhpClassName()
22✔
104
                    )
105
                );
106
            }
107
            if ($field->isDate()) {
22✔
108
                $this->addImport(DateTimeInterface::class);
3✔
109
            } elseif ($field->isEnum() && $this->phpVersion->isEnumSupported()) {
22✔
110
                $this->addImport($this->fqdn($this->withSubNamespace(SchemaGenerator::NAMESPACE_SUBPATH), $field->getPhpClassName()));
5✔
111
            }
112
            if (
113
                $field->isRequired()
22✔
114
                && $this->phpVersion->isConstructorPropertyPromotionSupported()
22✔
115
            ) {
116
                continue;
10✔
117
            }
118

119
            $statements[] = $this->generateProperty($field);
22✔
120
        }
121

122
        $default = null;
22✔
123
        if (count($request->bodyContentTypes) < 2) {
22✔
124
            $default = $this->builder->val($request->bodyContentTypes[0] ?? '');
22✔
125
        }
126
        $statements[] = $this->builder->localProperty('contentType', 'string', 'string', false, $default);
22✔
127

128
        foreach ($this->securityStrategies as $securityStrategy) {
22✔
129
            array_push($statements, ...$securityStrategy->getProperties($operation, $specification));
22✔
130
        }
131

132
        return $statements;
22✔
133
    }
134

135
    protected function generateConstructor(
136
        Request $request,
137
        Operation $operation,
138
        Specification $specification
139
    ): ?ClassMethod {
140
        $params      = [];
22✔
141
        $paramInits  = [];
22✔
142
        $validations = [];
22✔
143
        foreach ($request->fields as $field) {
22✔
144
            if ($field->isRequired()) {
22✔
145
                array_push($validations, ...$this->generateValidationStmts($field));
16✔
146

147
                $param = $this
16✔
148
                    ->builder
16✔
149
                    ->param($field->getPhpVariableName())
16✔
150
                    ->setType($field->getPhpTypeHint(), $field->isNullable());
16✔
151

152
                if (($default = $field->getDefault()) !== null) {
16✔
153
                    if ($field->isEnum() && $this->phpVersion->isEnumSupported() && (
12✔
154
                        is_string($default)
4✔
155
                        || is_integer($default)
12✔
156
                    )) {
157
                        $param->setDefault($this->builder->classConstFetch(
4✔
158
                            $field->getPhpClassName(),
4✔
159
                            EnumGenerator::getCaseName((string)$default)
4✔
160
                        ));
161
                    } else {
162
                        $param->setDefault($field->getDefault());
8✔
163
                    }
164
                }
165

166
                $params[] = $param;
16✔
167

168
                $paramInits[] = $this->builder->assign(
16✔
169
                    $this->builder->localPropertyFetch($field->getPhpVariableName()),
16✔
170
                    $this->builder->var($field->getPhpVariableName())
16✔
171
                );
172
            }
173
        }
174

175
        foreach ($this->securityStrategies as $securityStrategy) {
22✔
176
            array_push($params, ...$securityStrategy->getConstructorParams($operation, $specification));
22✔
177
            array_push($paramInits, ...$securityStrategy->getConstructorParamInits($operation, $specification));
22✔
178
        }
179

180
        if (count($request->bodyContentTypes) > 1) {
22✔
181
            $contentTypeVariableName = 'contentType';
1✔
182

183
            $params[] = $this->builder->param($contentTypeVariableName)->setType('string');
1✔
184

185
            $paramInits[] = $this->builder->assign(
1✔
186
                $this->builder->localPropertyFetch($contentTypeVariableName),
1✔
187
                $this->builder->var($contentTypeVariableName)
1✔
188
            );
189
        }
190

191
        if (empty($params)) {
22✔
192
            return null;
1✔
193
        }
194

195
        if ($this->phpVersion->isConstructorPropertyPromotionSupported()) {
22✔
196
            foreach ($params as $param) {
14✔
197
                $param->makePrivate();
14✔
198
            }
199
        }
200
        if ($this->phpVersion->isReadonlyPropertySupported()) {
22✔
201
            foreach ($params as $param) {
7✔
202
                $param->makeReadonly();
7✔
203
            }
204
        }
205

206
        $params = array_map(
22✔
207
            static fn (ParameterBuilder $param): ParameterNode => $param->getNode(),
22✔
208
            $params
209
        );
210

211
        $params = $this->sortParameters(...$params);
22✔
212

213
        $constructor = $this->builder
22✔
214
            ->method('__construct')
22✔
215
            ->makePublic()
22✔
216
            ->addParams($params)
22✔
217
            ->addStmts($validations)
22✔
218
            ->composeDocBlock($params);
22✔
219

220
        if (!$this->phpVersion->isConstructorPropertyPromotionSupported()) {
22✔
221
            $constructor->addStmts($paramInits);
8✔
222
        }
223

224
        return $constructor->getNode();
22✔
225
    }
226

227
    private function generateSetters(Request $request): array
228
    {
229
        $statements = [];
22✔
230
        foreach ($request->fields as $field) {
22✔
231
            if ($field->isRequired()) {
22✔
232
                continue;
16✔
233
            }
234
            if ($field->isNullable()) {
22✔
235
                throw new InvalidSpecificationException('Nullable optional parameter is not supported');
×
236
            }
237
            $statements[] = $this->generateSet($field);
22✔
238
        }
239

240
        return $statements;
22✔
241
    }
242

243
    private function generateGetContentType(): ClassMethod
244
    {
245
        $return     = $this->builder->return($this->builder->localPropertyFetch('contentType'));
22✔
246
        $returnType = 'string';
22✔
247

248
        return $this
249
            ->builder
22✔
250
            ->method('getContentType')
22✔
251
            ->makePublic()
22✔
252
            ->addStmt($return)
22✔
253
            ->setReturnType($returnType)
22✔
254
            ->composeDocBlock([], $returnType)
22✔
255
            ->getNode();
22✔
256
    }
257

258
    private function generateGetMethod(Request $request): ClassMethod
259
    {
260
        $return     = $this->builder->return($this->builder->val($request->method));
22✔
261
        $returnType = 'string';
22✔
262

263
        return $this
264
            ->builder
22✔
265
            ->method('getMethod')
22✔
266
            ->makePublic()
22✔
267
            ->addStmt($return)
22✔
268
            ->setReturnType($returnType)
22✔
269
            ->composeDocBlock([], $returnType)
22✔
270
            ->getNode();
22✔
271
    }
272

273
    private function generateGetRoute(Request $request): ClassMethod
274
    {
275
        $values     = [];
22✔
276
        $returnType = 'string';
22✔
277

278
        foreach ($request->fields->getPathFields() as $field) {
22✔
279
            $key          = sprintf('{%s}', $field->getName());
4✔
280
            $values[$key] = $this->builder->localPropertyFetch($field->getPhpVariableName());
4✔
281
        }
282

283
        if (empty($values)) {
22✔
284
            $return = $this->builder->return($this->builder->val($request->path));
19✔
285

286
            return $this
287
                ->builder
19✔
288
                ->method('getRoute')
19✔
289
                ->makePublic()
19✔
290
                ->addStmt($return)
19✔
291
                ->setReturnType($returnType)
19✔
292
                ->composeDocBlock([], $returnType)
19✔
293
                ->getNode();
19✔
294
        }
295

296
        $map    = $this->builder->array($values);
4✔
297
        $return = $this->builder->return(
4✔
298
            $this->builder->funcCall('strtr', [$this->builder->val($request->path), $map])
4✔
299
        );
300

301
        return $this
302
            ->builder
4✔
303
            ->method('getRoute')
4✔
304
            ->makePublic()
4✔
305
            ->addStmt($return)
4✔
306
            ->setReturnType($returnType)
4✔
307
            ->composeDocBlock([], $returnType)
4✔
308
            ->getNode();
4✔
309
    }
310

311
    private function generateGetParametersMethods(
312
        Request $request,
313
        Operation $operation,
314
        Specification $specification
315
    ): array {
316
        $methods = [];
22✔
317
        $fields  = $request->fields;
22✔
318

319
        $securityQueryFields = $this->getSecurityQueryParameters($operation, $specification);
22✔
320
        $securityCookies     = $this->getSecurityCookies($operation, $specification);
22✔
321

322
        $methods[] = $this->generateGetParametersMethod('getQueryParameters', $fields->getQueryFields(), $securityQueryFields);
22✔
323
        $methods[] = $this->generateGetRawParametersMethod('getRawQueryParameters', $fields->getQueryFields(), $securityQueryFields);
22✔
324
        $methods[] = $this->generateGetParametersMethod('getCookies', $fields->getCookieFields(), $securityCookies);
22✔
325
        $methods[] = $this->generateGetHeadersMethod($request, $fields->getHeaderFields(), $operation, $specification);
22✔
326
        $methods[] = $this->generateGetBody($fields->getBody());
22✔
327

328
        return $methods;
22✔
329
    }
330

331
    private function generateGetParametersMethod(string $methodName, array $fields, array $securityFields): ClassMethod
332
    {
333
        $returnVal  = $this->builder->array([]);
22✔
334
        $fieldsArr  = $this->generateFieldsArray($fields);
22✔
335
        $returnType = 'array';
22✔
336

337
        if (!empty($fieldsArr)) {
22✔
338
            if (empty($securityFields)) {
22✔
339
                $returnVal = $this->generateParametersFromFields($fieldsArr);
10✔
340
            } else {
341
                $returnVal = $this->builder->funcCall(
12✔
342
                    'array_merge',
12✔
343
                    [
344
                        $this->generateParametersFromFields($fieldsArr),
12✔
345
                        $securityFields,
12✔
346
                    ]
347
                );
348
            }
349
        } else {
350
            $returnVal = $this->builder->array($securityFields);
19✔
351
        }
352

353
        return $this
354
            ->builder
22✔
355
            ->method($methodName)
22✔
356
            ->makePublic()
22✔
357
            ->addStmt($this->builder->return($returnVal))
22✔
358
            ->setReturnType($returnType)
22✔
359
            ->composeDocBlock([], $returnType)
22✔
360
            ->getNode();
22✔
361
    }
362

363
    private function generateGetRawParametersMethod(string $methodName, array $fields, array $securityFields): ClassMethod
364
    {
365
        $fieldsArr  = $this->generateFieldsArray($fields);
22✔
366
        $returnType = 'array';
22✔
367

368
        return $this
369
            ->builder
22✔
370
            ->method($methodName)
22✔
371
            ->makePublic()
22✔
372
            ->addStmt($this->builder->return($this->builder->array(array_merge($fieldsArr, $securityFields))))
22✔
373
            ->setReturnType($returnType)
22✔
374
            ->composeDocBlock([], $returnType)
22✔
375
            ->getNode();
22✔
376
    }
377

378
    private function generateFieldsArray(array $fields): array
379
    {
380
        $fieldsArr = [];
22✔
381
        foreach ($fields as $field) {
22✔
382
            /** @var Field $field */
383
            $fieldsArr[$field->getName()] = $this->builder->localPropertyFetch($field->getPhpVariableName());
22✔
384

385
            if ($this->phpVersion->isEnumSupported()) {
22✔
386
                if ($field->isEnum()) {
7✔
387
                    if ($field->isNullable() || $field->isOptional()) {
1✔
388
                        $fieldsArr[$field->getName()] = $this->builder->nullsafePropertyFetch($fieldsArr[$field->getName()], 'value');
1✔
389
                    } else {
390
                        $fieldsArr[$field->getName()] = $this->builder->propertyFetch($fieldsArr[$field->getName()], 'value');
1✔
391
                    }
392
                } elseif ($field->isArrayOfEnums()) {
7✔
NEW
393
                    $enumField = $field->getArrayItem();
×
NEW
394
                    $this->addImport($this->fqdn($this->withSubNamespace(SchemaGenerator::NAMESPACE_SUBPATH), $enumField->getPhpClassName()));
×
NEW
395
                    $fieldsArr[$field->getName()] = $this->builder->funcCall(
×
NEW
396
                        'array_map',
×
397
                        [
NEW
398
                            $this->builder->arrowFunction(
×
NEW
399
                                $this->builder->propertyFetch($this->builder->var('enum'), 'value'),
×
NEW
400
                                [$this->builder->param('enum')->setType($enumField->getPhpClassName())->getNode()],
×
NEW
401
                                $enumField->getType()->toPhpType()
×
402
                            ),
NEW
403
                            $fieldsArr[$field->getName()],
×
404
                        ]
405
                    );
406
                }
407
            }
408
        }
409

410
        return $fieldsArr;
22✔
411
    }
412

413
    private function generateGetBody(?Field $body): ClassMethod
414
    {
415
        if ($body !== null) {
22✔
416
            $returnType = $body->getPhpTypeHint();
16✔
417

418
            return $this
419
                ->builder
16✔
420
                ->method('getBody')
16✔
421
                ->makePublic()
16✔
422
                ->addStmt($this->builder->return($this->builder->localPropertyFetch($body->getPhpVariableName())))
16✔
423
                ->composeDocBlock([], $returnType)
16✔
424
                ->getNode();
16✔
425
        }
426

427
        return $this
428
            ->builder
7✔
429
            ->method('getBody')
7✔
430
            ->makePublic()
7✔
431
            ->addStmt($this->builder->return($this->builder->val(null)))
7✔
432
            ->getNode();
7✔
433
    }
434

435
    private function generateGetHeadersMethod(
436
        Request $request,
437
        array $fields,
438
        Operation $operation,
439
        Specification $specification
440
    ): ClassMethod {
441
        $stmts   = $this->getSecurityHeadersStmts($operation, $specification);
22✔
442
        $headers = $this->getSecurityHeaders($operation, $specification);
22✔
443
        if (!empty($request->bodyContentTypes)) {
22✔
444
            $headers['Content-Type'] = $this->builder->localPropertyFetch('contentType');
16✔
445
        }
446
        $returnVal  = $this->builder->array($headers);
22✔
447
        $fieldsArr  = [];
22✔
448
        $returnType = 'array';
22✔
449
        foreach ($fields as $field) {
22✔
450
            $fieldsArr[$field->getName()] = $this->builder->localPropertyFetch($field->getPhpVariableName());
16✔
451
        }
452

453
        if (!empty($fieldsArr)) {
22✔
454
            $returnVal = $this->builder->funcCall(
16✔
455
                'array_merge',
16✔
456
                [$returnVal, $this->generateParametersFromFields($fieldsArr)]
16✔
457
            );
458
        }
459

460
        return $this
461
            ->builder
22✔
462
            ->method('getHeaders')
22✔
463
            ->makePublic()
22✔
464
            ->addStmts($stmts)
22✔
465
            ->addStmt($this->builder->return($returnVal))
22✔
466
            ->setReturnType($returnType)
22✔
467
            ->composeDocBlock([], $returnType)
22✔
468
            ->getNode();
22✔
469
    }
470

471
    private function getSecurityHeadersStmts(Operation $operation, Specification $specification): array
472
    {
473
        $stmts = [];
22✔
474

475
        foreach ($this->securityStrategies as $securityStrategy) {
22✔
476
            $stmts = array_merge($stmts, $securityStrategy->getSecurityHeadersStmts($operation, $specification));
22✔
477
        }
478

479
        return $stmts;
22✔
480
    }
481

482
    private function getSecurityHeaders(Operation $operation, Specification $specification): array
483
    {
484
        $headers = [];
22✔
485

486
        foreach ($this->securityStrategies as $securityStrategy) {
22✔
487
            $headers += $securityStrategy->getSecurityHeaders($operation, $specification);
22✔
488
        }
489

490
        return $headers;
22✔
491
    }
492

493
    private function getSecurityCookies(Operation $operation, Specification $specification): array
494
    {
495
        $cookies = [];
22✔
496

497
        foreach ($this->securityStrategies as $securityStrategy) {
22✔
498
            $cookies += $securityStrategy->getSecurityCookies($operation, $specification);
22✔
499
        }
500

501
        return $cookies;
22✔
502
    }
503

504
    private function getSecurityQueryParameters(Operation $operation, Specification $specification): array
505
    {
506
        $queryParameters = [];
22✔
507

508
        foreach ($this->securityStrategies as $securityStrategy) {
22✔
509
            $queryParameters += $securityStrategy->getSecurityQueryParameters($operation, $specification);
22✔
510
        }
511

512
        return $queryParameters;
22✔
513
    }
514

515
    private function generateParametersFromFields(array $fields): FuncCall
516
    {
517
        $filterCallbackBody = $this->builder->return(
22✔
518
            $this->builder->notEquals($this->builder->val(null), $this->builder->var('value'))
22✔
519
        );
520

521
        $filterCallback = $this->builder->closure(
22✔
522
            [$filterCallbackBody],
22✔
523
            [$this->builder->param('value')->getNode()]
22✔
524
        );
525

526
        $filter = $this->builder->funcCall(
22✔
527
            'array_filter',
22✔
528
            [$this->builder->array($fields), $filterCallback]
22✔
529
        );
530

531
        $this->addImport(CopiedNamespace::getImport($this->baseNamespace, SerializableInterface::class));
22✔
532
        $closureVariable = $this->builder->var('value');
22✔
533
        $closureBody     = $this->builder->return(
22✔
534
            $this->builder->ternary(
22✔
535
                $this->builder->instanceOf(
22✔
536
                    $closureVariable,
537
                    $this->builder->className('SerializableInterface')
22✔
538
                ),
539
                $this->builder->methodCall(
22✔
540
                    $closureVariable,
541
                    'toArray'
22✔
542
                ),
543
                $closureVariable
544
            )
545
        );
546

547
        return $this->builder->funcCall(
22✔
548
            'array_map',
22✔
549
            [
550
                $this->builder->closure(
22✔
551
                    [$closureBody],
22✔
552
                    [$this->builder->param('value')->getNode()]
22✔
553
                ),
554
                $filter,
22✔
555
            ]
556
        );
557
    }
558

559
    private function sortParameters(ParameterNode ...$parameterNodes): array
560
    {
561
        usort(
22✔
562
            $parameterNodes,
563
            static fn (ParameterNode $paramA, ParameterNode $paramB) => $paramA->default <=> $paramB->default
22✔
564
        );
22✔
565

566
        return $parameterNodes;
22✔
567
    }
568
}
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

© 2025 Coveralls, Inc