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

DoclerLabs / api-client-generator / 17646029044

11 Sep 2025 01:25PM UTC coverage: 86.973% (+0.02%) from 86.951%
17646029044

Pull #126

github

web-flow
Merge b491228da into b0b1dbeb8
Pull Request #126: Update SchemaGenerator.php - Fix issue with a nullable Enum

3 of 3 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

2991 of 3439 relevant lines covered (86.97%)

6.28 hits per line

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

98.45
/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();
17✔
31
        foreach ($compositeFields as $field) {
17✔
32
            if ($field->isObject() && !$field->isFreeFormObject()) {
17✔
33
                $this->generateSchema($field, $fileRegistry);
17✔
34
            }
35
        }
36
    }
17✔
37

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

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

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

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

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

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

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

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

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

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

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

105
        return $statements;
17✔
106
    }
107

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

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

125
        return $statements;
17✔
126
    }
127

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

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

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

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

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

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

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

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

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

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

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

211
        return $statements;
17✔
212
    }
213

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

221
        return $statements;
17✔
222
    }
223

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

233
        return $statements;
17✔
234
    }
235

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

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

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

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

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

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

278
    private function collectSerializationFields(Field $root, Variable $arrayVariable): array
279
    {
280
        $statements = [];
17✔
281
        foreach ($root->getObjectProperties() as $propertyField) {
17✔
282
            $value = $this->builder->localPropertyFetch($propertyField->getPhpVariableName());
17✔
283
            if ($propertyField->isComposite()) {
17✔
284
                $methodCall = $this->builder->methodCall($value, 'toArray');
14✔
285
                if ($propertyField->isNullable()) {
14✔
286
                    if ($this->phpVersion->isNullSafeSupported()) {
12✔
287
                        $value = $this->builder->nullsafeMethodCall($value, 'toArray');
4✔
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;
14✔
297
                }
298
            } elseif ($propertyField->isDate()) {
17✔
299
                $methodCall = $this->builder->methodCall(
14✔
300
                    $value,
301
                    'format',
14✔
302
                    [$this->builder->constFetch('DATE_RFC3339')]
14✔
303
                );
304

305
                if ($propertyField->isNullable()) {
14✔
306
                    if ($this->phpVersion->isNullSafeSupported()) {
12✔
307
                        $value = $this->builder->nullsafeMethodCall(
4✔
308
                            $value,
309
                            'format',
4✔
310
                            [$this->builder->constFetch('DATE_RFC3339')]
4✔
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;
14✔
321
                }
322
            } elseif ($propertyField->isEnum() && $this->phpVersion->isEnumSupported()) {
17✔
323
                $value = $propertyField->isNullable() ? $this->builder->nullsafePropertyFetch($value, 'value') : $this->builder->propertyFetch($value, 'value');
2✔
324
            } elseif ($propertyField->isArrayOfEnums() && $this->phpVersion->isEnumSupported()) {
17✔
325
                $enumField    = $propertyField->getArrayItem();
1✔
326
                $arrayMapCall = $this->builder->funcCall(
1✔
327
                    'array_map',
1✔
328
                    [
329
                        $this->builder->arrowFunction(
1✔
330
                            $this->builder->propertyFetch($this->builder->var('item'), 'value'),
1✔
331
                            [$this->builder->param('item')->setType($enumField->getPhpClassName())->getNode()],
1✔
332
                            $enumField->getType()->toPhpType(),
1✔
333
                        ),
334
                        $value,
1✔
335
                    ]
336
                );
337

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

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

354
            if ($propertyField->isOptional()) {
17✔
355
                $ifCondition = $this->builder->localMethodCall(
17✔
356
                    $this->getHasMethodName($propertyField)
17✔
357
                );
358

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

365
        return $statements;
17✔
366
    }
367
}
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