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

DoclerLabs / api-client-generator / 17645922910

11 Sep 2025 01:21PM UTC coverage: 86.951%. Remained the same
17645922910

Pull #126

github

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

1 of 2 new or added lines in 1 file covered. (50.0%)

1 existing line in 1 file now uncovered.

2992 of 3441 relevant lines covered (86.95%)

6.27 hits per line

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

97.95
/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
                if ($propertyField->isNullable()) {
2✔
NEW
UNCOV
324
                    $value = $this->builder->nullsafePropertyFetch($value, 'value');
×
325
                } else {
326
                    $value = $this->builder->propertyFetch($value, 'value');
2✔
327
                }
328
            } elseif ($propertyField->isArrayOfEnums() && $this->phpVersion->isEnumSupported()) {
17✔
329
                $enumField    = $propertyField->getArrayItem();
1✔
330
                $arrayMapCall = $this->builder->funcCall(
1✔
331
                    'array_map',
1✔
332
                    [
333
                        $this->builder->arrowFunction(
1✔
334
                            $this->builder->propertyFetch($this->builder->var('item'), 'value'),
1✔
335
                            [$this->builder->param('item')->setType($enumField->getPhpClassName())->getNode()],
1✔
336
                            $enumField->getType()->toPhpType(),
1✔
337
                        ),
338
                        $value,
1✔
339
                    ]
340
                );
341

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

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

358
            if ($propertyField->isOptional()) {
17✔
359
                $ifCondition = $this->builder->localMethodCall(
17✔
360
                    $this->getHasMethodName($propertyField)
17✔
361
                );
362

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

369
        return $statements;
17✔
370
    }
371
}
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