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

wol-soft / php-json-schema-model-generator / 26514325359

27 May 2026 01:31PM UTC coverage: 98.865% (+0.2%) from 98.695%
26514325359

push

github

web-flow
Merge pull request #128 from wol-soft/filter-composition-ordering

Filter composition ordering

754 of 755 new or added lines in 22 files covered. (99.87%)

1 existing line in 1 file now uncovered.

5837 of 5904 relevant lines covered (98.87%)

591.82 hits per line

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

99.6
/src/Model/Validator/Factory/Composition/AbstractCompositionValidatorFactory.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\Model\Validator\Factory\Composition;
6

7
use PHPModelGenerator\Exception\Generic\DeniedPropertyException;
8
use PHPModelGenerator\Exception\SchemaException;
9
use PHPModelGenerator\Model\Property\BaseProperty;
10
use PHPModelGenerator\Model\Property\CompositionPropertyDecorator;
11
use PHPModelGenerator\Model\Property\PropertyInterface;
12
use PHPModelGenerator\Model\Property\PropertyType;
13
use PHPModelGenerator\Model\Schema;
14
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
15
use PHPModelGenerator\Model\Validator;
16
use PHPModelGenerator\Model\Validator\ComposedPropertyValidator;
17
use PHPModelGenerator\Model\Validator\Factory\AbstractValidatorFactory;
18
use PHPModelGenerator\Model\Validator\InstanceOfValidator;
19
use PHPModelGenerator\Model\Validator\PropertyValidator;
20
use PHPModelGenerator\Model\Validator\RequiredPropertyValidator;
21
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\ClearTypeHintDecorator;
22
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\CompositionTypeHintDecorator;
23
use PHPModelGenerator\PropertyProcessor\Filter\CompositionCompatibilityChecker;
24
use PHPModelGenerator\PropertyProcessor\PropertyFactory;
25
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
26
use PHPModelGenerator\Utils\TypeIntersection;
27

28
abstract class AbstractCompositionValidatorFactory extends AbstractValidatorFactory
29
{
30
    /**
31
     * Emit a generation-time warning for always-unsatisfiable composition schemas.
32
     */
33
    protected function warnIfAlwaysFalse(
32✔
34
        SchemaProcessor $schemaProcessor,
35
        PropertyInterface $property,
36
        string $reason,
37
    ): void {
38
        if ($schemaProcessor->getGeneratorConfiguration()->isOutputEnabled()) {
32✔
39
            // @codeCoverageIgnoreStart
40
            echo "Warning: always-unsatisfiable schema for property '{$property->getName()}': $reason\n";
41
            // @codeCoverageIgnoreEnd
42
        }
43
    }
44

45
    /**
46
     * Emit a warning when the composition array for the current keyword is empty.
47
     */
48
    protected function warnIfEmpty(
698✔
49
        SchemaProcessor $schemaProcessor,
50
        PropertyInterface $property,
51
        JsonSchema $propertySchema,
52
    ): void {
53
        if (
54
            empty($propertySchema->getJson()[$this->key]) &&
698✔
55
            $schemaProcessor->getGeneratorConfiguration()->isOutputEnabled()
698✔
56
        ) {
57
            // @codeCoverageIgnoreStart
58
            echo "Warning: empty composition for {$property->getName()} may lead to unexpected results\n";
59
            // @codeCoverageIgnoreEnd
60
        }
61
    }
62

63
    /**
64
     * Returns true when composition processing should be skipped for this property.
65
     *
66
     * For non-root object-typed properties, composition keywords are processed inside
67
     * the nested schema by processSchema (with the type=base path). Adding a composition
68
     * validator at the parent level would duplicate validation and inject a _Merged_ type
69
     * hint that overrides the correct nested-class type.
70
     */
71
    protected function shouldSkip(PropertyInterface $property, JsonSchema $propertySchema): bool
890✔
72
    {
73
        return !($property instanceof BaseProperty)
890✔
74
            && ($propertySchema->getJson()['type'] ?? '') === 'object';
890✔
75
    }
76

77
    /**
78
     * Check the (post-type-inheritance) composition branches for filter keywords.
79
     *
80
     * Must be called AFTER inheritPropertyType() in each modify() method. A branch that
81
     * inherits "object" from the parent is genuinely object-typed: PropertyFactory routes
82
     * it through processSchema, producing a nested class whose properties are processed
83
     * independently and are not subject to ComposedItem $value reset. branchContainsFilter()
84
     * correctly skips the properties scan for such branches.
85
     *
86
     * For "not", the value is a single branch schema (not an array); all other keywords
87
     * use an array of branches.
88
     *
89
     * TODO: filters inside composition branches cannot be correctly applied
90
     * (ComposedItem.phptpl resets $value to $originalModelData after each branch).
91
     * Proper per-branch filter chaining is deferred to a follow-up topic.
92
     *
93
     * @throws SchemaException
94
     */
95
    protected function checkForFilterInBranches(
797✔
96
        PropertyInterface $property,
97
        JsonSchema $propertySchema,
98
    ): void {
99
        $json = $propertySchema->getJson();
797✔
100

101
        if ($this->key === 'not') {
797✔
102
            $branch = $json['not'] ?? null;
103✔
103
            if (
104
                is_array($branch)
103✔
105
                && CompositionCompatibilityChecker::branchContainsFilter($branch)
103✔
106
            ) {
107
                throw new SchemaException(sprintf(
1✔
108
                    'A filter keyword inside a not composition branch is not supported'
1✔
109
                        . ' for property %s in file %s.',
1✔
110
                    $property->getName(),
1✔
111
                    $property->getJsonSchema()->getFile(),
1✔
112
                ));
1✔
113
            }
114
            return;
102✔
115
        }
116

117
        foreach ($json[$this->key] ?? [] as $index => $compositionElement) {
698✔
118
            if (
119
                is_array($compositionElement)
679✔
120
                && CompositionCompatibilityChecker::branchContainsFilter($compositionElement)
679✔
121
            ) {
122
                throw new SchemaException(sprintf(
8✔
123
                    'A filter keyword inside a %s composition branch is not supported'
8✔
124
                        . ' for property %s in file %s (branch #%d).',
8✔
125
                    $this->key,
8✔
126
                    $property->getName(),
8✔
127
                    $property->getJsonSchema()->getFile(),
8✔
128
                    $index,
8✔
129
                ));
8✔
130
            }
131
        }
132
    }
133

134
    /**
135
     * Build composition sub-properties for the current keyword's branches.
136
     *
137
     * @param bool $merged Whether to suppress CompositionTypeHintDecorators for object branches.
138
     *
139
     * @return CompositionPropertyDecorator[]
140
     *
141
     * @throws SchemaException
142
     */
143
    protected function getCompositionProperties(
788✔
144
        SchemaProcessor $schemaProcessor,
145
        Schema $schema,
146
        PropertyInterface $property,
147
        JsonSchema $propertySchema,
148
        bool $merged,
149
    ): array {
150
        $propertyFactory = new PropertyFactory();
788✔
151
        $compositionProperties = [];
788✔
152
        $json = $propertySchema->getJson()['propertySchema']->getJson();
788✔
153

154
        $property->addTypeHintDecorator(new ClearTypeHintDecorator());
788✔
155

156
        foreach ($json[$this->key] as $index => $compositionElement) {
788✔
157
            if ($compositionElement === false) {
769✔
158
                $compositionProperties[] = $this->createAlwaysFalseBranchProperty(
25✔
159
                    $schemaProcessor,
25✔
160
                    $schema,
25✔
161
                    $property,
25✔
162
                    $propertySchema->getJson()['propertySchema'],
25✔
163
                );
25✔
164
                continue;
25✔
165
            }
166

167
            if ($compositionElement === true) {
759✔
168
                $compositionProperties[] = $this->createAlwaysTrueBranchProperty(
13✔
169
                    $schemaProcessor,
13✔
170
                    $schema,
13✔
171
                    $property,
13✔
172
                    $propertySchema->getJson()['propertySchema'],
13✔
173
                );
13✔
174
                continue;
13✔
175
            }
176

177
            $compositionSchema = $propertySchema->getJson()['propertySchema']->navigate("$this->key/$index");
758✔
178

179
            $compositionProperty = new CompositionPropertyDecorator(
758✔
180
                $property->getName(),
758✔
181
                $compositionSchema,
758✔
182
                $propertyFactory->create(
758✔
183
                    $schemaProcessor,
758✔
184
                    $schema,
758✔
185
                    $property->getName(),
758✔
186
                    $compositionSchema,
758✔
187
                    $property->isRequired(),
758✔
188
                ),
758✔
189
            );
758✔
190

191
            $compositionProperty->onResolve(function () use ($compositionProperty, $property, $merged): void {
758✔
192
                $nestedSchema = $compositionProperty->getNestedSchema();
758✔
193

194
                $compositionProperty->filterValidators(
758✔
195
                    static function (Validator $validator) use ($nestedSchema): bool {
758✔
196
                        if (is_a($validator->getValidator(), RequiredPropertyValidator::class)) {
752✔
197
                            return false;
464✔
198
                        }
199
                        if (is_a($validator->getValidator(), ComposedPropertyValidator::class)) {
746✔
200
                            return false;
1✔
201
                        }
202
                        // An empty object schema ({type: object} with no declared properties)
203
                        // must accept any PHP object in composition context. The generated
204
                        // placeholder class carries no semantic constraints, so the strict
205
                        // instanceof check against it would incorrectly reject valid objects
206
                        // (e.g. a DateTime produced by a transforming filter) that are perfectly
207
                        // acceptable under the schema's actual semantics.
208
                        if (
209
                            is_a($validator->getValidator(), InstanceOfValidator::class)
746✔
210
                            && $nestedSchema !== null
746✔
211
                            && empty($nestedSchema->getProperties())
746✔
212
                        ) {
213
                            return false;
8✔
214
                        }
215
                        return true;
746✔
216
                    },
758✔
217
                );
758✔
218

219
                if (!($merged && $compositionProperty->getNestedSchema())) {
758✔
220
                    $property->addTypeHintDecorator(new CompositionTypeHintDecorator($compositionProperty));
460✔
221
                }
222
            });
758✔
223

224
            $compositionProperties[] = $compositionProperty;
758✔
225
        }
226

227
        return $compositionProperties;
788✔
228
    }
229

230
    /**
231
     * Create a composition branch for a boolean `false` schema element.
232
     *
233
     * The branch always fails when the property key is present in $modelData, so absent optional
234
     * properties are not denied. Used for false branches in allOf/anyOf/oneOf compositions.
235
     */
236
    protected function createAlwaysFalseBranchProperty(
25✔
237
        SchemaProcessor $schemaProcessor,
238
        Schema $schema,
239
        PropertyInterface $property,
240
        JsonSchema $parentSchema,
241
    ): CompositionPropertyDecorator {
242
        $propertyFactory = new PropertyFactory();
25✔
243
        $branchSchema = $parentSchema->withJson([]);
25✔
244

245
        $branchProperty = new CompositionPropertyDecorator(
25✔
246
            $property->getName(),
25✔
247
            $branchSchema,
25✔
248
            $propertyFactory->create(
25✔
249
                $schemaProcessor,
25✔
250
                $schema,
25✔
251
                $property->getName(),
25✔
252
                $branchSchema,
25✔
253
                $property->isRequired(),
25✔
254
            ),
25✔
255
        );
25✔
256

257
        $presenceCheck = "array_key_exists('" . addslashes($property->getName()) . "', \$modelData)";
25✔
258

259
        $branchProperty->onResolve(
25✔
260
            function () use ($branchProperty, $presenceCheck): void {
25✔
261
                $branchProperty->filterValidators(
25✔
262
                    static fn(Validator $validator): bool =>
25✔
263
                        !is_a($validator->getValidator(), RequiredPropertyValidator::class) &&
25✔
264
                        !is_a($validator->getValidator(), ComposedPropertyValidator::class),
25✔
265
                );
25✔
266
                $branchProperty->addValidator(
25✔
267
                    new PropertyValidator(
25✔
268
                        $branchProperty,
25✔
269
                        $presenceCheck,
25✔
270
                        DeniedPropertyException::class,
25✔
271
                    ),
25✔
272
                );
25✔
273
            },
25✔
274
        );
25✔
275

276
        return $branchProperty;
25✔
277
    }
278

279
    /**
280
     * Create a composition branch for a boolean `true` schema element.
281
     *
282
     * The branch always succeeds (no validators) and is marked as an always-true branch so that
283
     * type inference excludes it from type narrowing. Used for true branches in allOf/anyOf/oneOf.
284
     */
285
    protected function createAlwaysTrueBranchProperty(
13✔
286
        SchemaProcessor $schemaProcessor,
287
        Schema $schema,
288
        PropertyInterface $property,
289
        JsonSchema $parentSchema,
290
    ): CompositionPropertyDecorator {
291
        $propertyFactory = new PropertyFactory();
13✔
292
        $branchSchema = $parentSchema->withJson([]);
13✔
293

294
        $branchProperty = new CompositionPropertyDecorator(
13✔
295
            $property->getName(),
13✔
296
            $branchSchema,
13✔
297
            $propertyFactory->create(
13✔
298
                $schemaProcessor,
13✔
299
                $schema,
13✔
300
                $property->getName(),
13✔
301
                $branchSchema,
13✔
302
                $property->isRequired(),
13✔
303
            ),
13✔
304
        );
13✔
305

306
        $branchProperty->markAsAlwaysTrueBranch();
13✔
307

308
        $branchProperty->onResolve(function () use ($branchProperty): void {
13✔
309
            $branchProperty->filterValidators(
13✔
310
                static fn(Validator $validator): bool =>
13✔
311
                    !is_a($validator->getValidator(), RequiredPropertyValidator::class) &&
13✔
312
                    !is_a($validator->getValidator(), ComposedPropertyValidator::class),
13✔
313
            );
13✔
314
            // No validator added — true schema always succeeds.
315
            // No type hint decorator — true schema contributes no type constraint.
316
        });
13✔
317

318
        return $branchProperty;
13✔
319
    }
320

321
    /**
322
     * Inherit a parent-level type into composition branches that declare no type.
323
     */
324
    protected function inheritPropertyType(JsonSchema $propertySchema): JsonSchema
876✔
325
    {
326
        $json = $propertySchema->getJson();
876✔
327

328
        if (!isset($json['type'])) {
876✔
329
            return $propertySchema;
422✔
330
        }
331

332
        switch ($this->key) {
475✔
333
            case 'not':
475✔
334
                if (!isset($json[$this->key]['type'])) {
29✔
335
                    $json[$this->key]['type'] = $json['type'];
29✔
336
                }
337
                break;
29✔
338
            case 'if':
446✔
339
                return $this->inheritIfPropertyType($propertySchema->withJson($json));
74✔
340
            default:
341
                foreach ($json[$this->key] as &$composedElement) {
390✔
342
                    if (!is_bool($composedElement) && !isset($composedElement['type'])) {
389✔
343
                        $composedElement['type'] = $json['type'];
178✔
344
                    }
345
                }
346
        }
347

348
        return $propertySchema->withJson($json);
419✔
349
    }
350

351
    /**
352
     * Inherit the parent type into all branches of an if/then/else composition.
353
     */
354
    protected function inheritIfPropertyType(JsonSchema $propertySchema): JsonSchema
74✔
355
    {
356
        $json = $propertySchema->getJson();
74✔
357

358
        foreach (['if', 'then', 'else'] as $keyword) {
74✔
359
            if (!isset($json[$keyword]) || is_bool($json[$keyword])) {
74✔
360
                continue;
18✔
361
            }
362

363
            if (!isset($json[$keyword]['type'])) {
74✔
364
                $json[$keyword]['type'] = $json['type'];
74✔
365
            }
366
        }
367

368
        return $propertySchema->withJson($json);
74✔
369
    }
370

371
    /**
372
     * After all composition branches resolve, derive the parent property's type from the
373
     * branch types and apply it. Skips when any branch has a nested schema (object merging
374
     * is handled elsewhere).
375
     *
376
     * allOf: intersect all typed branch types — only values satisfying every branch simultaneously
377
     * are valid, so the PHP type is the intersection. Branches with no declared type impose no
378
     * constraint and are excluded from the intersection. An empty intersection (contradictory
379
     * branch types) throws SchemaException because no value can ever be valid.
380
     *
381
     * anyOf / oneOf: union of all typed branch types — at least one branch must pass, so the PHP
382
     * type is the union. An untyped branch accepts every value, making the composition satisfied
383
     * by any input; the property's type hint is removed (remains mixed) in that case.
384
     *
385
     * @param bool $isAllOf true for allOf, false for anyOf/oneOf.
386
     * @param CompositionPropertyDecorator[] $compositionProperties
387
     *
388
     * @throws SchemaException when allOf branches declare contradictory types.
389
     */
390
    protected function transferPropertyType(
673✔
391
        PropertyInterface $property,
392
        array $compositionProperties,
393
        bool $isAllOf,
394
    ): void {
395
        foreach ($compositionProperties as $compositionProperty) {
673✔
396
            if ($compositionProperty->getNestedSchema() !== null) {
673✔
397
                return;
470✔
398
            }
399
        }
400

401
        // For anyOf/oneOf: a true branch always satisfies the composition for any value,
402
        // so the property type cannot be narrowed — leave it untyped.
403
        // For allOf: exclude true branches from type computation; they contribute no constraint.
404
        $activeBranches = array_values(array_filter(
204✔
405
            $compositionProperties,
204✔
406
            static fn(CompositionPropertyDecorator $compositionProperty): bool =>
204✔
407
                !$compositionProperty->isAlwaysTrueBranch(),
204✔
408
        ));
204✔
409

410
        if (!$isAllOf && count($activeBranches) < count($compositionProperties)) {
204✔
411
            return;
8✔
412
        }
413

414

415
        $hasBranchWithRequiredProperty = array_filter(
196✔
416
            $activeBranches,
196✔
417
            static fn(CompositionPropertyDecorator $p): bool => $p->isRequired(),
196✔
418
        ) !== [];
196✔
419

420
        $hasBranchWithOptionalProperty = $isAllOf
196✔
421
            ? !$hasBranchWithRequiredProperty
57✔
422
            : array_filter(
139✔
423
                $activeBranches,
139✔
424
                static fn(CompositionPropertyDecorator $p): bool => !$p->isRequired(),
139✔
425
            ) !== [];
139✔
426

427
        if ($isAllOf) {
196✔
428
            $this->transferAllOfType($property, $compositionProperties, $hasBranchWithOptionalProperty);
57✔
429
            return;
55✔
430
        }
431

432
        $this->transferAnyOfOneOfType($property, $compositionProperties, $hasBranchWithOptionalProperty);
139✔
433
    }
434

435
    /**
436
     * Derive and apply the parent property's type using allOf intersection semantics.
437
     *
438
     * Only typed branches (those that declare a type keyword) constrain the intersection.
439
     * Untyped branches impose no type restriction and are excluded. Null is valid only when
440
     * ALL typed branches allow it. An empty non-null intersection (contradictory types) throws
441
     * SchemaException — no value can satisfy all branch type constraints simultaneously.
442
     *
443
     * @param CompositionPropertyDecorator[] $compositionProperties
444
     *
445
     * @throws SchemaException
446
     */
447
    private function transferAllOfType(
57✔
448
        PropertyInterface $property,
449
        array $compositionProperties,
450
        bool $hasBranchWithOptionalProperty,
451
    ): void {
452
        $constrainingBranches = array_values(array_filter(
57✔
453
            $compositionProperties,
57✔
454
            static fn(CompositionPropertyDecorator $p): bool => $p->getType() !== null,
57✔
455
        ));
57✔
456

457
        if (empty($constrainingBranches)) {
57✔
458
            // No typed branches — no type constraint to apply.
459
            return;
4✔
460
        }
461

462
        // Intersection of non-null type names across all typed branches.
463
        // TypeIntersection::compute handles int ⊂ float (integer is a subtype of number in JSON Schema).
464
        $nonNullSets = array_map(
53✔
465
            static fn(CompositionPropertyDecorator $p): array => array_values(array_filter(
53✔
466
                $p->getType()->getNames(),
53✔
467
                static fn(string $typeName): bool => $typeName !== 'null',
53✔
468
            )),
53✔
469
            $constrainingBranches,
53✔
470
        );
53✔
471
        $nonNullNames = array_shift($nonNullSets);
53✔
472
        foreach ($nonNullSets as $typeSet) {
53✔
473
            $nonNullNames = TypeIntersection::compute($nonNullNames, $typeSet);
33✔
474
        }
475

476
        // Null is valid in allOf only when ALL typed branches allow it.
477
        $allBranchesAllowNull = count(array_filter(
53✔
478
            $constrainingBranches,
53✔
479
            static fn(CompositionPropertyDecorator $p): bool =>
53✔
480
                in_array('null', $p->getType()->getNames(), true)
53✔
481
                || $p->getType()->isNullable() === true,
53✔
482
        )) === count($constrainingBranches);
53✔
483

484
        if (empty($nonNullNames) && !$allBranchesAllowNull) {
53✔
485
            throw new SchemaException(sprintf(
2✔
486
                "Property '%s' is defined with conflicting types in allOf composition branches"
2✔
487
                    . ' (file %s). allOf requires all constraints to hold simultaneously,'
2✔
488
                    . ' making this schema unsatisfiable.',
2✔
489
                $property->getName(),
2✔
490
                $property->getJsonSchema()->getFile(),
2✔
491
            ));
2✔
492
        }
493

494
        if (empty($nonNullNames)) {
51✔
495
            // Only null survives the intersection; the null-processor path handles pure-null types.
496
            return;
1✔
497
        }
498

499
        $nullable = ($allBranchesAllowNull || $hasBranchWithOptionalProperty) ? true : null;
50✔
500
        $property->setType(new PropertyType($nonNullNames, $nullable));
50✔
501
    }
502

503
    /**
504
     * Derive and apply the parent property's type using anyOf/oneOf union semantics.
505
     *
506
     * Branches are partitioned into three categories:
507
     * - Typed (getType() !== null): contribute their names to the union.
508
     * - Explicit null-type ({type:null}): getType() is null but typeHint contains 'null';
509
     *   contributes nullable=true to the result.
510
     * - Truly untyped ({}): getType() is null and typeHint does not contain 'null'; the branch
511
     *   accepts every value, so the composition is always satisfiable and no type hint applies.
512
     *
513
     * A truly untyped branch causes early return without setting a type (property remains mixed),
514
     * matching the behaviour of PropertyMerger::mergeNullableBranch for object-level compositions.
515
     * An explicit null-type branch ({type:null}) is NOT treated as untyped — it adds nullable=true
516
     * to the typed union rather than removing the type hint.
517
     *
518
     * @param CompositionPropertyDecorator[] $compositionProperties
519
     */
520
    private function transferAnyOfOneOfType(
139✔
521
        PropertyInterface $property,
522
        array $compositionProperties,
523
        bool $hasBranchWithOptionalProperty,
524
    ): void {
525
        $hasExplicitNullBranch = false;
139✔
526

527
        foreach ($compositionProperties as $compositionProperty) {
139✔
528
            if ($compositionProperty->getType() !== null) {
139✔
529
                continue;
115✔
530
            }
531

532
            if (str_contains($compositionProperty->getTypeHint(), 'null')) {
28✔
533
                // Explicit null-type branch ({type: null}): contributes nullable=true.
534
                $hasExplicitNullBranch = true;
2✔
535
            } else {
536
                // Truly untyped branch ({}): any value is valid, so the composition is
537
                // always satisfiable — no type hint is appropriate for this property.
538
                return;
26✔
539
            }
540
        }
541

542
        $typedBranches = array_values(array_filter(
113✔
543
            $compositionProperties,
113✔
544
            static fn(CompositionPropertyDecorator $p): bool => $p->getType() !== null,
113✔
545
        ));
113✔
546

547
        if (empty($typedBranches)) {
113✔
548
            // Only explicit null branches; no scalar type to build a union from.
549
            return;
1✔
550
        }
551

552
        $allNames = array_merge(...array_map(
112✔
553
            static fn(CompositionPropertyDecorator $p): array => $p->getType()->getNames(),
112✔
554
            $typedBranches,
112✔
555
        ));
112✔
556

557
        $hasNull = $hasExplicitNullBranch || in_array('null', $allNames, true);
112✔
558
        $nonNullNames = array_values(array_filter(
112✔
559
            array_unique($allNames),
112✔
560
            static fn(string $typeName): bool => $typeName !== 'null',
112✔
561
        ));
112✔
562

563
        if (!$nonNullNames) {
112✔
UNCOV
564
            return;
×
565
        }
566

567
        $nullable = ($hasNull || $hasBranchWithOptionalProperty) ? true : null;
112✔
568
        $property->setType(new PropertyType($nonNullNames, $nullable));
112✔
569
    }
570
}
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