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

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

28 Jul 2026 06:05AM UTC coverage: 98.735% (+0.001%) from 98.734%
30333648898

Pull #180

github

claude
Exclude internal bookkeeping properties from composition branch default helpers

setupBranchDefaultHelpers() iterated a composition branch's nested schema
properties without excluding internal ones, so an internal bookkeeping
property with a non-null default (eg. _skipNotProvidedPropertiesMap, added
whenever serialization is enabled) was misread as real branch data and
leaked into componentDefaultValueMap and propertyAccessors of the
generated _getModifiedValues_* helper, even though no getter is ever
generated for it.

Fixes #179.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QL8i9sBrFNCQv4WdGwxfup
Pull Request #180: Exclude internal bookkeeping properties from composition branch default helpers

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

6 existing lines in 3 files now uncovered.

7178 of 7270 relevant lines covered (98.73%)

573.98 hits per line

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

98.94
/src/PropertyProcessor/PropertyFactory.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\PropertyProcessor;
6

7
use PHPModelGenerator\Attributes\Deprecated;
8
use PHPModelGenerator\Attributes\JsonPointer;
9
use PHPModelGenerator\Attributes\JsonSchema as JsonSchemaAttribute;
10
use PHPModelGenerator\Attributes\ReadOnlyProperty;
11
use PHPModelGenerator\Attributes\Required;
12
use PHPModelGenerator\Attributes\SchemaName;
13
use PHPModelGenerator\Attributes\WriteOnlyProperty;
14
use Exception;
15
use PHPModelGenerator\Draft\Draft;
16
use PHPModelGenerator\Draft\DraftFactoryInterface;
17
use PHPModelGenerator\Draft\Modifier\ObjectType\ObjectModifier;
18
use PHPModelGenerator\Model\Validator\Factory\AbstractValidatorFactory;
19
use PHPModelGenerator\Draft\Modifier\TypeCheckModifier;
20
use PHPModelGenerator\Exception\SchemaException;
21
use PHPModelGenerator\Model\Attributes\PhpAttribute;
22
use PHPModelGenerator\Model\Property\BaseProperty;
23
use PHPModelGenerator\Model\Property\Property;
24
use PHPModelGenerator\Model\Property\PropertyInterface;
25
use PHPModelGenerator\Model\Property\PropertyType;
26
use PHPModelGenerator\Model\Schema;
27
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
28
use PHPModelGenerator\Model\Validator\MultiTypeCheckValidator;
29
use PHPModelGenerator\Model\Validator\RequiredPropertyValidator;
30
use PHPModelGenerator\Model\Validator\TypeCheckInterface;
31
use PHPModelGenerator\PropertyProcessor\Decorator\Property\PropertyTransferDecorator;
32
use PHPModelGenerator\PropertyProcessor\Decorator\SchemaNamespaceTransferDecorator;
33
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintDecorator;
34
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
35
use PHPModelGenerator\Utils\TypeConverter;
36

37
class PropertyFactory
38
{
39
    /** @var Draft[] Keyed by draft class name */
40
    private array $draftCache = [];
41

42
    /**
43
     * Create a property, applying all applicable Draft modifiers.
44
     *
45
     * @throws SchemaException
46
     */
47
    public function create(
2,658✔
48
        SchemaProcessor $schemaProcessor,
49
        Schema $schema,
50
        string $propertyName,
51
        JsonSchema $propertySchema,
52
        bool $required = false,
53
        bool $isArrayItem = false,
54
    ): PropertyInterface {
55
        $json = $propertySchema->getJson();
2,658✔
56

57
        // $ref: replace the property entirely via the definition dictionary.
58
        // This is a schema-identity primitive — it cannot be a Draft modifier because
59
        // ModifierInterface::modify returns void and cannot replace the property object.
60
        if (isset($json['$ref'])) {
2,658✔
61
            if (isset($json['type']) && $json['type'] === 'base') {
570✔
62
                return $this->processBaseReference(
43✔
63
                    $schemaProcessor,
43✔
64
                    $schema,
43✔
65
                    $propertyName,
43✔
66
                    $propertySchema,
43✔
67
                    $required,
43✔
68
                    $isArrayItem,
43✔
69
                );
43✔
70
            }
71

72
            return $this->processReference(
530✔
73
                $schemaProcessor,
530✔
74
                $schema,
530✔
75
                $propertyName,
530✔
76
                $propertySchema,
530✔
77
                $required,
530✔
78
                $isArrayItem,
530✔
79
            );
530✔
80
        }
81

82
        $resolvedType = $json['type'] ?? 'any';
2,658✔
83

84
        if (is_array($resolvedType)) {
2,658✔
85
            return $this->createMultiTypeProperty(
98✔
86
                $schemaProcessor,
98✔
87
                $schema,
98✔
88
                $propertyName,
98✔
89
                $propertySchema,
98✔
90
                $resolvedType,
98✔
91
                $required,
98✔
92
                $isArrayItem,
98✔
93
            );
98✔
94
        }
95

96
        $this->checkType($resolvedType, $schema);
2,658✔
97

98
        return match ($resolvedType) {
2,658✔
99
            'object' => $this->createObjectProperty(
1,055✔
100
                $schemaProcessor,
1,055✔
101
                $schema,
1,055✔
102
                $propertyName,
1,055✔
103
                $propertySchema,
1,055✔
104
                $required,
1,055✔
105
                $isArrayItem,
1,055✔
106
            ),
1,055✔
107
            'base'   => $this->createBaseProperty($schemaProcessor, $schema, $propertyName, $propertySchema),
2,657✔
108
            default  => $this->createTypedProperty(
2,636✔
109
                $schemaProcessor,
2,636✔
110
                $schema,
2,636✔
111
                $propertyName,
2,636✔
112
                $propertySchema,
2,636✔
113
                $resolvedType,
2,636✔
114
                $required,
2,636✔
115
                $isArrayItem,
2,636✔
116
            ),
2,636✔
117
        };
118
    }
119

120
    /**
121
     * Handle a nested object property: generate the nested class, wire the outer property,
122
     * then apply universal modifiers (filter, enum, default, const) on the outer property.
123
     *
124
     * @throws SchemaException
125
     */
126
    private function createObjectProperty(
1,064✔
127
        SchemaProcessor $schemaProcessor,
128
        Schema $schema,
129
        string $propertyName,
130
        JsonSchema $propertySchema,
131
        bool $required,
132
        bool $isArrayItem = false,
133
    ): PropertyInterface {
134
        $json     = $propertySchema->getJson();
1,064✔
135
        $property = $this->buildProperty(
1,064✔
136
            $schemaProcessor,
1,064✔
137
            $propertyName,
1,064✔
138
            null,
1,064✔
139
            $propertySchema,
1,064✔
140
            $required,
1,064✔
141
            $isArrayItem,
1,064✔
142
        );
1,064✔
143

144
        $className = $schemaProcessor->getGeneratorConfiguration()->getClassNameGenerator()->getClassName(
1,064✔
145
            $propertyName,
1,064✔
146
            $propertySchema,
1,064✔
147
            false,
1,064✔
148
            $schemaProcessor->getCurrentClassName(),
1,064✔
149
        );
1,064✔
150

151
        // Strip property-level keywords before passing the schema to processSchema: these keywords
152
        // target the outer property and are handled by the universal modifiers below.
153
        $nestedJson = $json;
1,064✔
154
        unset($nestedJson['filter'], $nestedJson['enum'], $nestedJson['default']);
1,064✔
155

156
        $nestedSchema = $schemaProcessor->processSchema(
1,064✔
157
            $propertySchema->withJson($nestedJson),
1,064✔
158
            $schemaProcessor->getCurrentClassPath(),
1,064✔
159
            $className,
1,064✔
160
            $schema->getSchemaDictionary(),
1,064✔
161
        );
1,064✔
162

163
        if ($nestedSchema !== null) {
1,064✔
164
            $property->setNestedSchema($nestedSchema);
1,064✔
165
            $this->wireObjectProperty($schemaProcessor, $schema, $property, $propertySchema);
1,064✔
166
        }
167

168
        // Universal modifiers (filter, enum, default, const) run on the outer property.
169
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema, anyOnly: true);
1,064✔
170

171
        return $property;
1,062✔
172
    }
173

174
    /**
175
     * Handle a root-level schema (type=base): set up definitions, run all Draft modifiers,
176
     * then transfer any composed properties to the schema.
177
     *
178
     * @throws SchemaException
179
     */
180
    private function createBaseProperty(
2,657✔
181
        SchemaProcessor $schemaProcessor,
182
        Schema $schema,
183
        string $propertyName,
184
        JsonSchema $propertySchema,
185
    ): PropertyInterface {
186
        $schema->getSchemaDictionary()->setUpDefinitionDictionary($schemaProcessor, $schema);
2,657✔
187
        $property = new BaseProperty($propertyName, new PropertyType('object'), $propertySchema);
2,657✔
188

189
        $objectJson         = $propertySchema->getJson();
2,657✔
190
        $objectJson['type'] = 'object';
2,657✔
191
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema->withJson($objectJson));
2,657✔
192

193
        $schemaProcessor->transferComposedPropertiesToSchema($property, $schema);
2,534✔
194

195
        return $property;
2,533✔
196
    }
197

198
    /**
199
     * Handle scalar, array, and untyped properties: construct directly and run all Draft modifiers.
200
     *
201
     * @throws SchemaException
202
     */
203
    private function createTypedProperty(
2,561✔
204
        SchemaProcessor $schemaProcessor,
205
        Schema $schema,
206
        string $propertyName,
207
        JsonSchema $propertySchema,
208
        string $type,
209
        bool $required,
210
        bool $isArrayItem = false,
211
    ): PropertyInterface {
212
        $phpType  = $type !== 'any' ? TypeConverter::jsonSchemaToPHP($type) : null;
2,561✔
213
        $property = $this->buildProperty(
2,561✔
214
            $schemaProcessor,
2,561✔
215
            $propertyName,
2,561✔
216
            $phpType !== null ? new PropertyType($phpType) : null,
2,561✔
217
            $propertySchema,
2,561✔
218
            $required,
2,561✔
219
            $isArrayItem,
2,561✔
220
        );
2,561✔
221

222
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema);
2,559✔
223

224
        return $property;
2,478✔
225
    }
226

227
    /**
228
     * Construct a Property with the common required/readOnly/writeOnly setup.
229
     *
230
     * @throws SchemaException
231
     */
232
    private function buildProperty(
2,619✔
233
        SchemaProcessor $schemaProcessor,
234
        string $propertyName,
235
        ?PropertyType $type,
236
        JsonSchema $propertySchema,
237
        bool $required,
238
        bool $isArrayItem = false,
239
    ): Property {
240
        $json = $propertySchema->getJson();
2,619✔
241

242
        $isSchemaReadOnly = isset($json['readOnly']) && $json['readOnly'] === true;
2,619✔
243
        $isWriteOnly = isset($json['writeOnly']) && $json['writeOnly'] === true;
2,619✔
244

245
        if ($isSchemaReadOnly && $isWriteOnly) {
2,619✔
246
            throw new SchemaException(
1✔
247
                sprintf(
1✔
248
                    "Property '%s' in file '%s' cannot be both readOnly and writeOnly",
1✔
249
                    $propertyName,
1✔
250
                    $propertySchema->getFile(),
1✔
251
                ),
1✔
252
                $propertySchema,
1✔
253
            );
1✔
254
        }
255

256
        $property = (new Property($propertyName, $type, $propertySchema, $json['description'] ?? ''))
2,618✔
257
            ->setRequired($required)
2,618✔
258
            ->setArrayItem($isArrayItem)
2,618✔
259
            ->setReadOnly($isSchemaReadOnly || $schemaProcessor->getGeneratorConfiguration()->isImmutable())
2,618✔
260
            ->setWriteOnly($isWriteOnly);
2,618✔
261

262
        if (isset($json['$comment'])) {
2,617✔
263
            $property->setComment($json['$comment']);
7✔
264
        }
265

266
        if (isset($json['examples']) && is_array($json['examples'])) {
2,617✔
267
            $property->setExamples($json['examples']);
9✔
268
        }
269

270
        if ($required && !$isArrayItem) {
2,617✔
271
            // Compute the parent object schema pointer by stripping '<name>/properties' (last two
272
            // path segments) from the property pointer, then appending the 'required' keyword.
273
            $propertyPointer = $propertySchema->getPointer();
1,175✔
274
            $segments = $propertyPointer !== '' ? explode('/', ltrim($propertyPointer, '/')) : [];
1,175✔
275
            $parentPointer = count($segments) > 2 ? '/' . implode('/', array_slice($segments, 0, -2)) : '';
1,175✔
276
            $property->addValidator(
1,175✔
277
                (new RequiredPropertyValidator($property))->withJsonPointer($parentPointer . '/required'),
1,175✔
278
                1,
1,175✔
279
            );
1,175✔
280
        }
281

282
        $configuration = $schemaProcessor->getGeneratorConfiguration();
2,617✔
283

284
        $property
2,617✔
285
            ->addAttribute(
2,617✔
286
                new PhpAttribute(JsonPointer::class, [$propertySchema->getPointer()]),
2,617✔
287
                $configuration,
2,617✔
288
                PhpAttribute::JSON_POINTER,
2,617✔
289
            )
2,617✔
290
            ->addAttribute(
2,617✔
291
                new PhpAttribute(SchemaName::class, [$propertyName]),
2,617✔
292
                $configuration,
2,617✔
293
                PhpAttribute::SCHEMA_NAME,
2,617✔
294
            )
2,617✔
295
            ->addAttribute(
2,617✔
296
                new PhpAttribute(
2,617✔
297
                    JsonSchemaAttribute::class,
2,617✔
298
                    [empty($propertySchema->getJson()) ? '{}' : json_encode($propertySchema->getJson())],
2,617✔
299
                ),
2,617✔
300
                $configuration,
2,617✔
301
                PhpAttribute::JSON_SCHEMA,
2,617✔
302
            );
2,617✔
303

304
        if ($required) {
2,617✔
305
            $property->addAttribute(new PhpAttribute(Required::class), $configuration, PhpAttribute::REQUIRED);
1,175✔
306
        }
307

308
        if (isset($json['readOnly']) && $json['readOnly'] === true) {
2,617✔
309
            $property->addAttribute(
4✔
310
                new PhpAttribute(ReadOnlyProperty::class),
4✔
311
                $configuration,
4✔
312
                PhpAttribute::READ_WRITE_ONLY,
4✔
313
            );
4✔
314
        }
315

316
        if (isset($json['writeOnly']) && $json['writeOnly'] === true) {
2,617✔
317
            $property->addAttribute(
14✔
318
                new PhpAttribute(WriteOnlyProperty::class),
14✔
319
                $configuration,
14✔
320
                PhpAttribute::READ_WRITE_ONLY,
14✔
321
            );
14✔
322
        }
323

324
        if (isset($json['deprecated']) && $json['deprecated'] === true) {
2,617✔
325
            $property->addAttribute(new PhpAttribute(Deprecated::class), $configuration, PhpAttribute::DEPRECATED);
2✔
326
        }
327

328
        return $property;
2,617✔
329
    }
330

331
    /**
332
     * Resolve a $ref reference by looking it up in the definition dictionary.
333
     *
334
     * @throws SchemaException
335
     */
336
    private function processReference(
570✔
337
        SchemaProcessor $schemaProcessor,
338
        Schema $schema,
339
        string $propertyName,
340
        JsonSchema $propertySchema,
341
        bool $required,
342
        bool $isArrayItem = false,
343
    ): PropertyInterface {
344
        $path       = [];
570✔
345
        $reference  = $propertySchema->getJson()['$ref'];
570✔
346
        $dictionary = $schema->getSchemaDictionary();
570✔
347

348
        try {
349
            $definition = $dictionary->getDefinition($reference, $schemaProcessor, $path);
570✔
350

351
            if ($definition) {
562✔
352
                $definitionSchema = $definition->getSchema();
562✔
353

354
                if (
355
                    $schema->getClassPath() !== $definitionSchema->getClassPath() ||
562✔
356
                    $schema->getClassName() !== $definitionSchema->getClassName() ||
561✔
357
                    (
358
                        $schema->getClassName() === 'ExternalSchema' &&
562✔
359
                        $definitionSchema->getClassName() === 'ExternalSchema'
562✔
360
                    )
361
                ) {
362
                    $schema->addNamespaceTransferDecorator(
270✔
363
                        new SchemaNamespaceTransferDecorator($definitionSchema),
270✔
364
                    );
270✔
365

366
                    if ($definitionSchema->getClassName() !== 'ExternalSchema') {
270✔
367
                        $schema->addUsedClass(join('\\', array_filter([
77✔
368
                            $schemaProcessor->getGeneratorConfiguration()->getNamespacePrefix(),
77✔
369
                            $definitionSchema->getClassPath(),
77✔
370
                            $definitionSchema->getClassName(),
77✔
371
                        ])));
77✔
372
                    }
373
                }
374

375
                $property = $definition->resolveReference(
562✔
376
                    $propertyName,
562✔
377
                    implode('/', $path),
562✔
378
                    $required,
562✔
379
                    $propertySchema->getJson()['_dependencies'] ?? null,
562✔
380
                    $isArrayItem,
562✔
381
                );
562✔
382

383
                // Use the reference site's pointer (where $ref appears in the schema) rather
384
                // than the definition's pointer. This is always meaningful and consistent with
385
                // how inline properties work — both show WHERE IN THE SCHEMA the property is
386
                // defined, not where the resolved type lives.
387
                $property->overrideJsonPointer(new PhpAttribute(JsonPointer::class, [$propertySchema->getPointer()]));
562✔
388

389
                return $property;
562✔
390
            }
391
        } catch (Exception $exception) {
8✔
392
            throw new SchemaException(
8✔
393
                "Unresolved Reference $reference in file {$propertySchema->getFile()}",
8✔
394
                null,
8✔
395
                0,
8✔
396
                $exception,
8✔
397
            );
8✔
398
        }
399

UNCOV
400
        throw new SchemaException(
×
UNCOV
401
            "Unresolved Reference $reference in file {$propertySchema->getFile()}",
×
UNCOV
402
            $propertySchema,
×
UNCOV
403
        );
×
404
    }
405

406
    /**
407
     * Resolve a $ref on a base-level schema: set up definitions, delegate to processReference,
408
     * then copy the referenced schema's properties to the parent schema.
409
     *
410
     * @throws SchemaException
411
     */
412
    private function processBaseReference(
43✔
413
        SchemaProcessor $schemaProcessor,
414
        Schema $schema,
415
        string $propertyName,
416
        JsonSchema $propertySchema,
417
        bool $required,
418
        bool $isArrayItem = false,
419
    ): PropertyInterface {
420
        $schema->getSchemaDictionary()->setUpDefinitionDictionary($schemaProcessor, $schema);
43✔
421

422
        $property = $this->processReference(
43✔
423
            $schemaProcessor,
43✔
424
            $schema,
43✔
425
            $propertyName,
43✔
426
            $propertySchema,
43✔
427
            $required,
43✔
428
            $isArrayItem,
43✔
429
        );
43✔
430

431
        if (!$property->getNestedSchema()) {
43✔
432
            throw new SchemaException(
1✔
433
                sprintf(
1✔
434
                    'A referenced schema on base level must provide an object definition for property %s in file %s',
1✔
435
                    $propertyName,
1✔
436
                    $propertySchema->getFile(),
1✔
437
                ),
1✔
438
                $propertySchema,
1✔
439
            );
1✔
440
        }
441

442
        foreach ($property->getNestedSchema()->getProperties() as $propertiesOfReferencedObject) {
42✔
443
            $schema->addProperty($propertiesOfReferencedObject);
42✔
444
        }
445

446
        return $property;
42✔
447
    }
448

449
    /**
450
     * Handle "type": [...] properties by processing each type through its Draft modifiers,
451
     * merging validators and decorators onto a single property, then consolidating type checks.
452
     *
453
     * @param string[] $types
454
     *
455
     * @throws SchemaException
456
     */
457
    private function createMultiTypeProperty(
98✔
458
        SchemaProcessor $schemaProcessor,
459
        Schema $schema,
460
        string $propertyName,
461
        JsonSchema $propertySchema,
462
        array $types,
463
        bool $required,
464
        bool $isArrayItem = false,
465
    ): PropertyInterface {
466
        $json     = $propertySchema->getJson();
98✔
467
        $property = $this->buildProperty(
98✔
468
            $schemaProcessor,
98✔
469
            $propertyName,
98✔
470
            null,
98✔
471
            $propertySchema,
98✔
472
            $required,
98✔
473
            $isArrayItem,
98✔
474
        );
98✔
475

476
        $collectedTypes   = [];
98✔
477
        $typeHints        = [];
98✔
478
        $resolvedSubCount = 0;
98✔
479
        $totalSubCount    = count($types);
98✔
480

481
        // Strip the default from sub-schemas so that default handling runs only once via the
482
        // universal DefaultValueModifier below, which already handles the multi-type case.
483
        $subJson = $json;
98✔
484
        unset($subJson['default']);
98✔
485

486
        foreach ($types as $type) {
98✔
487
            $this->checkType($type, $schema);
98✔
488

489
            $subJson['type'] = $type;
98✔
490
            $subSchema       = $propertySchema->withJson($subJson);
98✔
491

492
            // For type=object, delegate to the same object path (processSchema + wireObjectProperty).
493
            $subProperty = $type === 'object'
98✔
494
                ? $this->createObjectProperty(
9✔
495
                    $schemaProcessor,
9✔
496
                    $schema,
9✔
497
                    $propertyName,
9✔
498
                    $subSchema,
9✔
499
                    $required,
9✔
500
                    $isArrayItem,
9✔
501
                )
9✔
502
                : $this->createSubTypeProperty(
97✔
503
                    $schemaProcessor,
97✔
504
                    $schema,
97✔
505
                    $propertyName,
97✔
506
                    $subSchema,
97✔
507
                    $type,
97✔
508
                    $required,
97✔
509
                    $isArrayItem,
97✔
510
                );
97✔
511

512
            $subProperty->onResolve(function () use (
98✔
513
                $property,
98✔
514
                $subProperty,
98✔
515
                $schemaProcessor,
98✔
516
                $schema,
98✔
517
                $propertySchema,
98✔
518
                $totalSubCount,
98✔
519
                &$collectedTypes,
98✔
520
                &$typeHints,
98✔
521
                &$resolvedSubCount,
98✔
522
            ): void {
98✔
523
                foreach ($subProperty->getValidators() as $validatorContainer) {
98✔
524
                    $validator = $validatorContainer->getValidator();
98✔
525

526
                    if ($validator instanceof TypeCheckInterface) {
98✔
527
                        array_push($collectedTypes, ...$validator->getTypes());
98✔
528
                        continue;
98✔
529
                    }
530

531
                    $property->addValidator(
60✔
532
                        $validator,
60✔
533
                        $validatorContainer->getPriority(),
60✔
534
                        $validatorContainer->getSourceKey(),
60✔
535
                    );
60✔
536
                }
537

538
                if ($subProperty->getDecorators()) {
98✔
539
                    $property->addDecorator(new PropertyTransferDecorator($subProperty));
35✔
540
                }
541

542
                $typeHints[] = $subProperty->getTypeHint();
98✔
543

544
                if (++$resolvedSubCount < $totalSubCount || empty($collectedTypes)) {
98✔
545
                    return;
97✔
546
                }
547

548
                $this->finalizeMultiTypeProperty(
98✔
549
                    $property,
98✔
550
                    array_unique($collectedTypes),
98✔
551
                    $typeHints,
98✔
552
                    $schemaProcessor,
98✔
553
                    $schema,
98✔
554
                    $propertySchema,
98✔
555
                );
98✔
556
            });
98✔
557
        }
558

559
        return $property;
95✔
560
    }
561

562
    /**
563
     * Build a non-object sub-property for a multi-type array, applying only type-specific
564
     * modifiers (no universal 'any' modifiers — those run once on the parent after finalization).
565
     *
566
     * @throws SchemaException
567
     */
568
    private function createSubTypeProperty(
97✔
569
        SchemaProcessor $schemaProcessor,
570
        Schema $schema,
571
        string $propertyName,
572
        JsonSchema $propertySchema,
573
        string $type,
574
        bool $required,
575
        bool $isArrayItem = false,
576
    ): Property {
577
        $subProperty = $this->buildProperty(
97✔
578
            $schemaProcessor,
97✔
579
            $propertyName,
97✔
580
            new PropertyType(TypeConverter::jsonSchemaToPHP($type)),
97✔
581
            $propertySchema,
97✔
582
            $required,
97✔
583
            $isArrayItem,
97✔
584
        );
97✔
585

586
        $this->applyModifiers($schemaProcessor, $schema, $subProperty, $propertySchema, anyOnly: false, typeOnly: true);
97✔
587

588
        return $subProperty;
97✔
589
    }
590

591
    /**
592
     * Called once all sub-properties of a multi-type property have resolved.
593
     * Adds the consolidated MultiTypeCheckValidator, sets the union PropertyType,
594
     * attaches the type-hint decorator, and runs universal modifiers.
595
     *
596
     * @param string[] $collectedTypes
597
     * @param string[] $typeHints
598
     *
599
     * @throws SchemaException
600
     */
601
    private function finalizeMultiTypeProperty(
98✔
602
        PropertyInterface $property,
603
        array $collectedTypes,
604
        array $typeHints,
605
        SchemaProcessor $schemaProcessor,
606
        Schema $schema,
607
        JsonSchema $propertySchema,
608
    ): void {
609
        $hasNull      = in_array('null', $collectedTypes, true);
98✔
610
        $nonNullTypes = array_values(array_filter(
98✔
611
            $collectedTypes,
98✔
612
            static fn(string $type): bool => $type !== 'null',
98✔
613
        ));
98✔
614

615
        $allowImplicitNull = $schemaProcessor->getGeneratorConfiguration()->isImplicitNullAllowed()
98✔
616
            && !$property->isRequired();
98✔
617

618
        $property->addValidator(
98✔
619
            (new MultiTypeCheckValidator($collectedTypes, $property, $allowImplicitNull))
98✔
620
                ->withJsonPointer($propertySchema->getPointer() . '/type'),
98✔
621
            2,
98✔
622
        );
98✔
623

624
        if ($nonNullTypes) {
98✔
625
            $property->setType(
98✔
626
                new PropertyType($nonNullTypes, $hasNull ? true : null),
98✔
627
                new PropertyType($nonNullTypes, $hasNull ? true : null),
98✔
628
            );
98✔
629
        }
630

631
        $property->addTypeHintDecorator(new TypeHintDecorator($typeHints));
98✔
632

633
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema, true);
98✔
634
    }
635

636
    /**
637
     * Wire the outer property for a nested object: add the type-check validator and instantiation
638
     * linkage. Schema-targeting modifiers are intentionally NOT run here because processSchema
639
     * already applied them to the nested schema.
640
     *
641
     * @throws SchemaException
642
     */
643
    private function wireObjectProperty(
1,064✔
644
        SchemaProcessor $schemaProcessor,
645
        Schema $schema,
646
        PropertyInterface $property,
647
        JsonSchema $propertySchema,
648
    ): void {
649
        (new TypeCheckModifier(TypeConverter::jsonSchemaToPHP('object')))->modify(
1,064✔
650
            $schemaProcessor,
1,064✔
651
            $schema,
1,064✔
652
            $property,
1,064✔
653
            $propertySchema,
1,064✔
654
        );
1,064✔
655

656
        (new ObjectModifier())->modify($schemaProcessor, $schema, $property, $propertySchema);
1,064✔
657
    }
658

659
    /**
660
     * Run Draft modifiers for the given property.
661
     *
662
     * By default all covered types (type-specific + 'any') run. Pass $anyOnly=true to run
663
     * only the 'any' entry (used for object outer-property universal keywords), or $typeOnly=true
664
     * to run only type-specific entries (used for multi-type sub-properties).
665
     *
666
     * @throws SchemaException
667
     */
668
    private function applyModifiers(
2,658✔
669
        SchemaProcessor $schemaProcessor,
670
        Schema $schema,
671
        PropertyInterface $property,
672
        JsonSchema $propertySchema,
673
        bool $anyOnly = false,
674
        bool $typeOnly = false,
675
    ): void {
676
        $type       = $propertySchema->getJson()['type'] ?? 'any';
2,658✔
677
        $builtDraft = $this->resolveBuiltDraft($schemaProcessor, $propertySchema);
2,658✔
678

679
        // For untyped properties ('any'), only run the 'any' entry — getCoveredTypes('any')
680
        // returns all types, which would incorrectly apply type-specific modifiers.
681
        $coveredTypes = $type === 'any'
2,658✔
682
            ? array_filter($builtDraft->getCoveredTypes('any'), static fn($t) => $t->getType() === 'any')
749✔
683
            : $builtDraft->getCoveredTypes($type);
2,658✔
684

685
        foreach ($coveredTypes as $coveredType) {
2,658✔
686
            $isAnyEntry = $coveredType->getType() === 'any';
2,658✔
687

688
            if ($anyOnly && !$isAnyEntry) {
2,658✔
689
                continue;
1,146✔
690
            }
691

692
            if ($typeOnly && $isAnyEntry) {
2,658✔
693
                continue;
97✔
694
            }
695

696
            foreach ($coveredType->getModifiers() as $modifier) {
2,658✔
697
                $countBefore = count($property->getValidators());
2,658✔
698
                $modifier->modify($schemaProcessor, $schema, $property, $propertySchema);
2,658✔
699

700
                // Tag every validator that was just added by this modifier with its schema
701
                // keyword so FilterProcessor can later classify each validator as
702
                // input-space or output-space relative to a transforming filter.
703
                // This must cover all Draft-registered validators — not only those known
704
                // to interact with filters today — because a custom Draft may register
705
                // any validator factory under any type, and the classification must work
706
                // without enumerating individual keywords.
707
                if ($modifier instanceof AbstractValidatorFactory && ($modifierKey = $modifier->getKey()) !== null) {
2,643✔
708
                    foreach (array_slice($property->getValidators(), $countBefore) as $validatorWrapper) {
2,642✔
709
                        $validatorWrapper->setSourceKey($modifierKey);
1,961✔
710
                    }
711
                }
712
            }
713
        }
714
    }
715

716
    /**
717
     * @throws SchemaException
718
     */
719
    private function checkType(mixed $type, Schema $schema): void
2,658✔
720
    {
721
        if (is_string($type)) {
2,658✔
722
            return;
2,658✔
723
        }
724

725
        throw new SchemaException(
1✔
726
            sprintf(
1✔
727
                'Invalid property type %s in file %s',
1✔
728
                $type,
1✔
729
                $schema->getJsonSchema()->getFile(),
1✔
730
            ),
1✔
731
            $schema->getJsonSchema(),
1✔
732
        );
1✔
733
    }
734

735
    private function resolveBuiltDraft(SchemaProcessor $schemaProcessor, JsonSchema $propertySchema): Draft
2,658✔
736
    {
737
        $configDraft = $schemaProcessor->getGeneratorConfiguration()->getDraft();
2,658✔
738

739
        $draft = $configDraft instanceof DraftFactoryInterface
2,658✔
740
            ? $configDraft->getDraftForSchema($propertySchema)
2,655✔
741
            : $configDraft;
3✔
742

743
        return $this->draftCache[$draft::class] ??= $draft->getDefinition()->build();
2,658✔
744
    }
745
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc