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

DoclerLabs / api-client-generator / 17945326072

23 Sep 2025 11:53AM UTC coverage: 86.981% (+0.03%) from 86.951%
17945326072

push

github

vsouz4
release fix nullable enum

2993 of 3441 relevant lines covered (86.98%)

6.37 hits per line

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

98.46
/src/Generator/SchemaGenerator.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\ParameterBuilder;
9
use DoclerLabs\ApiClientGenerator\Ast\ParameterNode;
10
use DoclerLabs\ApiClientGenerator\Entity\Field;
11
use DoclerLabs\ApiClientGenerator\Entity\FieldType;
12
use DoclerLabs\ApiClientGenerator\Input\Specification;
13
use DoclerLabs\ApiClientGenerator\Output\Php\PhpFileCollection;
14
use JsonSerializable;
15
use PhpParser\Node\Expr\Variable;
16
use PhpParser\Node\Stmt;
17
use PhpParser\Node\Stmt\ClassMethod;
18
use UnexpectedValueException;
19

20
class SchemaGenerator extends MutatorAccessorClassGeneratorAbstract
21
{
22
    public const SUBDIRECTORY = 'Schema/';
23

24
    public const NAMESPACE_SUBPATH = '\\Schema';
25

26
    private const OPTIONAL_CHANGED_FIELDS_PROPERTY_NAME = 'optionalPropertyChanged';
27

28
    public function generate(Specification $specification, PhpFileCollection $fileRegistry): void
29
    {
30
        $compositeFields = $specification->getCompositeFields()->getUniqueByPhpClassName();
19✔
31
        foreach ($compositeFields as $field) {
19✔
32
            if ($field->isObject() && !$field->isFreeFormObject()) {
19✔
33
                $this->generateSchema($field, $fileRegistry);
19✔
34
            }
35
        }
36
    }
19✔
37

38
    protected function generateSchema(Field $root, PhpFileCollection $fileRegistry): void
39
    {
40
        $this->addImport(JsonSerializable::class);
19✔
41

42
        $className = $root->getPhpClassName();
19✔
43

44
        $classBuilder = $this
19✔
45
            ->builder
19✔
46
            ->class($className)
19✔
47
            ->implement('SerializableInterface', 'JsonSerializable')
19✔
48
            ->addStmts($this->generateEnumConsts($root))
19✔
49
            ->addStmts($this->generateProperties($root))
19✔
50
            ->addStmt($this->generateOptionalChangedFieldsProperty($root))
19✔
51
            ->addStmt($this->generateConstructor($root))
19✔
52
            ->addStmts($this->generateSetMethods($root))
19✔
53
            ->addStmts($this->generateHasMethods($root))
19✔
54
            ->addStmts($this->generateGetMethods($root))
19✔
55
            ->addStmt($this->generateToArray($root))
19✔
56
            ->addStmt($this->generateJsonSerialize());
19✔
57

58
        $this->registerFile($fileRegistry, $classBuilder, self::SUBDIRECTORY, self::NAMESPACE_SUBPATH);
19✔
59
    }
19✔
60

61
    private function generateOptionalChangedFieldsProperty(Field $root): ?Stmt
62
    {
63
        $optionalProperties = [];
19✔
64

65
        foreach ($root->getObjectProperties() as $propertyField) {
19✔
66
            if ($propertyField->isOptional()) {
19✔
67
                if ($propertyField->getPhpVariableName() === self::OPTIONAL_CHANGED_FIELDS_PROPERTY_NAME) {
19✔
68
                    throw new UnexpectedValueException('Property "' . self::OPTIONAL_CHANGED_FIELDS_PROPERTY_NAME . '" not supported!');
×
69
                }
70

71
                if ($propertyField->isNullable()) {
19✔
72
                    trigger_error('Property "' . $propertyField->getName() . '" is nullable and optional, that might be a sign of a bad api design', E_USER_WARNING);
19✔
73
                }
74
                $optionalProperties[] = $propertyField;
19✔
75
            }
76
        }
77

78
        if (empty($optionalProperties)) {
19✔
79
            return null;
×
80
        }
81

82
        $propertyArrayValues = [];
19✔
83
        foreach ($optionalProperties as $optionalProperty) {
19✔
84
            $propertyArrayValues[$optionalProperty->getPhpVariableName()] = $this->builder->val(false);
19✔
85
        }
86

87
        return $this->builder->localProperty(
19✔
88
            self::OPTIONAL_CHANGED_FIELDS_PROPERTY_NAME,
19✔
89
            'array',
19✔
90
            'array',
19✔
91
            false,
19✔
92
            $this->builder->array($propertyArrayValues)
19✔
93
        );
94
    }
95

96
    protected function generateEnumConsts(Field $root): array
97
    {
98
        $statements = [];
19✔
99
        foreach ($root->getObjectProperties() as $propertyField) {
19✔
100
            foreach ($this->generateEnumStatements($propertyField) as $statement) {
19✔
101
                $statements[] = $statement;
13✔
102
            }
103
        }
104

105
        return $statements;
19✔
106
    }
107

108
    protected function generateProperties(Field $root): array
109
    {
110
        $statements = [];
19✔
111
        foreach ($root->getObjectProperties() as $propertyField) {
19✔
112
            if ($propertyField->isDate()) {
19✔
113
                $this->addImport(DateTimeInterface::class);
16✔
114
            }
115
            if (
116
                $propertyField->isRequired()
19✔
117
                && $this->phpVersion->isConstructorPropertyPromotionSupported()
19✔
118
            ) {
119
                continue;
7✔
120
            }
121

122
            $statements[] = $this->generateProperty($propertyField);
19✔
123
        }
124

125
        return $statements;
19✔
126
    }
127

128
    protected function generateConstructor(Field $root): ?ClassMethod
129
    {
130
        $params             = [];
19✔
131
        $validations        = [];
19✔
132
        $paramsInit         = [];
19✔
133
        $paramsDoc          = [];
19✔
134
        $thrownExceptionMap = [];
19✔
135

136
        foreach ($root->getObjectProperties() as $propertyField) {
19✔
137
            if ($propertyField->isRequired()) {
19✔
138
                $validationStmts = $this->generateValidationStmts($propertyField);
19✔
139
                array_push($validations, ...$validationStmts);
19✔
140
                if (!empty($validationStmts)) {
19✔
141
                    $thrownExceptionMap['RequestValidationException'] = true;
14✔
142
                }
143
                $params[] = $this->builder
19✔
144
                    ->param($propertyField->getPhpVariableName())
19✔
145
                    ->setType($propertyField->getPhpTypeHint(), $propertyField->isNullable());
19✔
146

147
                $paramsInit[] = $this->builder->assign(
19✔
148
                    $this->builder->localPropertyFetch($propertyField->getPhpVariableName()),
19✔
149
                    $this->builder->var($propertyField->getPhpVariableName())
19✔
150
                );
151

152
                $paramsDoc[] = $this->builder
19✔
153
                    ->param($propertyField->getPhpVariableName())
19✔
154
                    ->setType($propertyField->getPhpTypeHint(), $propertyField->isNullable())
19✔
155
                    ->setDocBlockType($propertyField->getPhpDocType($propertyField->isNullable()))
19✔
156
                    ->getNode();
19✔
157
            }
158
        }
159
        if (empty($params)) {
19✔
160
            return null;
16✔
161
        }
162

163
        if ($this->phpVersion->isConstructorPropertyPromotionSupported()) {
19✔
164
            foreach ($params as $param) {
7✔
165
                $param->makePrivate();
7✔
166
            }
167
        }
168
        if ($this->phpVersion->isReadonlyPropertySupported()) {
19✔
169
            foreach ($params as $param) {
5✔
170
                $param->makeReadonly();
5✔
171
            }
172
        }
173

174
        $params = array_map(
19✔
175
            static fn (ParameterBuilder $param): ParameterNode => $param->getNode(),
19✔
176
            $params
177
        );
178

179
        $constructor = $this
19✔
180
            ->builder
19✔
181
            ->method('__construct')
19✔
182
            ->makePublic()
19✔
183
            ->addParams($params)
19✔
184
            ->addStmts($validations)
19✔
185
            ->composeDocBlock($paramsDoc, '', array_keys($thrownExceptionMap));
19✔
186

187
        if (!$this->phpVersion->isConstructorPropertyPromotionSupported()) {
19✔
188
            $constructor->addStmts($paramsInit);
12✔
189
        }
190

191
        return $constructor->getNode();
19✔
192
    }
193

194
    protected function generateSetMethods(Field $root): array
195
    {
196
        $statements = [];
19✔
197
        foreach ($root->getObjectProperties() as $propertyField) {
19✔
198
            if ($propertyField->isOptional()) {
19✔
199
                $changedFieldSetter = $this->builder->assign(
19✔
200
                    $this->builder->getArrayItem(
19✔
201
                        $this->builder->localPropertyFetch(self::OPTIONAL_CHANGED_FIELDS_PROPERTY_NAME),
19✔
202
                        $this->builder->val($propertyField->getPhpVariableName())
19✔
203
                    ),
204
                    $this->builder->val(true)
19✔
205
                );
206

207
                $statements[] = $this->generateSet($propertyField, [$changedFieldSetter]);
19✔
208
            }
209
        }
210

211
        return $statements;
19✔
212
    }
213

214
    protected function generateGetMethods(Field $root): array
215
    {
216
        $statements = [];
19✔
217
        foreach ($root->getObjectProperties() as $propertyField) {
19✔
218
            $statements[] = $this->generateGet($propertyField);
19✔
219
        }
220

221
        return $statements;
19✔
222
    }
223

224
    protected function generateHasMethods(Field $root): array
225
    {
226
        $statements = [];
19✔
227
        foreach ($root->getObjectProperties() as $propertyField) {
19✔
228
            if ($propertyField->isOptional()) {
19✔
229
                $statements[] = $this->generateHas($propertyField);
19✔
230
            }
231
        }
232

233
        return $statements;
19✔
234
    }
235

236
    protected function generateToArray(Field $root): ClassMethod
237
    {
238
        $statements    = [];
19✔
239
        $arrayVariable = $this->builder->var('fields');
19✔
240
        $initialValue  = $this->builder->val([]);
19✔
241

242
        $statements[] = $this->builder->assign($arrayVariable, $initialValue);
19✔
243
        $statements   = array_merge($statements, $this->collectSerializationFields($root, $arrayVariable));
19✔
244
        $statements[] = $this->builder->return($arrayVariable);
19✔
245

246
        $returnType = FieldType::PHP_TYPE_ARRAY;
19✔
247

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

258
    private function generateHas(Field $field): ClassMethod
259
    {
260
        $return = $this->builder->return(
19✔
261
            $this->builder->getArrayItem(
19✔
262
                $this->builder->localPropertyFetch(
19✔
263
                    self::OPTIONAL_CHANGED_FIELDS_PROPERTY_NAME
19✔
264
                ),
265
                $this->builder->val($field->getPhpVariableName())
19✔
266
            )
267
        );
268

269
        return $this->builder
19✔
270
            ->method($this->getHasMethodName($field))
19✔
271
            ->makePublic()
19✔
272
            ->addStmt($return)
19✔
273
            ->setReturnType('bool')
19✔
274
            ->composeDocBlock([], 'bool')
19✔
275
            ->getNode();
19✔
276
    }
277

278
    private function collectSerializationFields(Field $root, Variable $arrayVariable): array
279
    {
280
        $statements = [];
19✔
281
        foreach ($root->getObjectProperties() as $propertyField) {
19✔
282
            $value = $this->builder->localPropertyFetch($propertyField->getPhpVariableName());
19✔
283
            if ($propertyField->isComposite()) {
19✔
284
                $methodCall = $this->builder->methodCall($value, 'toArray');
16✔
285
                if ($propertyField->isNullable()) {
16✔
286
                    if ($this->phpVersion->isNullSafeSupported()) {
14✔
287
                        $value = $this->builder->nullsafeMethodCall($value, 'toArray');
6✔
288
                    } else {
289
                        $value = $this->builder->ternary(
8✔
290
                            $this->builder->notEquals($value, $this->builder->val(null)),
8✔
291
                            $methodCall,
292
                            $this->builder->val(null)
8✔
293
                        );
294
                    }
295
                } else {
296
                    $value = $methodCall;
16✔
297
                }
298
            } elseif ($propertyField->isDate()) {
19✔
299
                $methodCall = $this->builder->methodCall(
16✔
300
                    $value,
301
                    'format',
16✔
302
                    [$this->builder->constFetch('DATE_RFC3339')]
16✔
303
                );
304

305
                if ($propertyField->isNullable()) {
16✔
306
                    if ($this->phpVersion->isNullSafeSupported()) {
14✔
307
                        $value = $this->builder->nullsafeMethodCall(
6✔
308
                            $value,
309
                            'format',
6✔
310
                            [$this->builder->constFetch('DATE_RFC3339')]
6✔
311
                        );
312
                    } else {
313
                        $value = $this->builder->ternary(
8✔
314
                            $this->builder->notEquals($value, $this->builder->val(null)),
8✔
315
                            $methodCall,
316
                            $this->builder->val(null)
8✔
317
                        );
318
                    }
319
                } else {
320
                    $value = $methodCall;
16✔
321
                }
322
            } elseif ($propertyField->isEnum() && $this->phpVersion->isEnumSupported()) {
19✔
323
                $value = $propertyField->isNullable()
4✔
324
                    ? $this->builder->nullsafePropertyFetch($value, 'value')
3✔
325
                    : $this->builder->propertyFetch($value, 'value');
4✔
326
            } elseif ($propertyField->isArrayOfEnums() && $this->phpVersion->isEnumSupported()) {
19✔
327
                $enumField    = $propertyField->getArrayItem();
1✔
328
                $arrayMapCall = $this->builder->funcCall(
1✔
329
                    'array_map',
1✔
330
                    [
331
                        $this->builder->arrowFunction(
1✔
332
                            $this->builder->propertyFetch($this->builder->var('item'), 'value'),
1✔
333
                            [$this->builder->param('item')->setType($enumField->getPhpClassName())->getNode()],
1✔
334
                            $enumField->getType()->toPhpType(),
1✔
335
                        ),
336
                        $value,
1✔
337
                    ]
338
                );
339

340
                $value = $propertyField->isNullable()
1✔
341
                    ? $this->builder->ternary(
1✔
342
                        $this->builder->notEquals($value, $this->builder->val(null)),
1✔
343
                        $arrayMapCall,
344
                        $this->builder->val(null)
1✔
345
                    )
346
                    : $arrayMapCall;
1✔
347
            }
348

349
            $fieldName = $this->builder->val($propertyField->getName());
19✔
350
            if ($root->hasOneOf()) {
19✔
351
                $assignStatement = $this->builder->expr($this->builder->assign($arrayVariable, $value));
×
352
            } else {
353
                $assignStatement = $this->builder->appendToAssociativeArray($arrayVariable, $fieldName, $value);
19✔
354
            }
355

356
            if ($propertyField->isOptional()) {
19✔
357
                $ifCondition = $this->builder->localMethodCall(
19✔
358
                    $this->getHasMethodName($propertyField)
19✔
359
                );
360

361
                $statements[] = $this->builder->if($ifCondition, [$assignStatement]);
19✔
362
            } else {
363
                $statements[] = $assignStatement;
19✔
364
            }
365
        }
366

367
        return $statements;
19✔
368
    }
369
}
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