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

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

15 Jul 2026 09:41PM UTC coverage: 98.788% (+0.02%) from 98.765%
29452850938

push

github

web-flow
Merge pull request #163 from wol-soft/claude/schema-error-line-numbers-b76c62

Add line/column location reporting to SchemaException

602 of 609 new or added lines in 31 files covered. (98.85%)

7010 of 7096 relevant lines covered (98.79%)

553.35 hits per line

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

98.8
/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,617✔
48
        SchemaProcessor $schemaProcessor,
49
        Schema $schema,
50
        string $propertyName,
51
        JsonSchema $propertySchema,
52
        bool $required = false,
53
    ): PropertyInterface {
54
        $json = $propertySchema->getJson();
2,617✔
55

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

70
            return $this->processReference($schemaProcessor, $schema, $propertyName, $propertySchema, $required);
530✔
71
        }
72

73
        $resolvedType = $json['type'] ?? 'any';
2,617✔
74

75
        if (is_array($resolvedType)) {
2,617✔
76
            return $this->createMultiTypeProperty(
97✔
77
                $schemaProcessor,
97✔
78
                $schema,
97✔
79
                $propertyName,
97✔
80
                $propertySchema,
97✔
81
                $resolvedType,
97✔
82
                $required,
97✔
83
            );
97✔
84
        }
85

86
        $this->checkType($resolvedType, $schema);
2,617✔
87

88
        return match ($resolvedType) {
2,617✔
89
            'object' => $this->createObjectProperty(
1,032✔
90
                $schemaProcessor,
1,032✔
91
                $schema,
1,032✔
92
                $propertyName,
1,032✔
93
                $propertySchema,
1,032✔
94
                $required,
1,032✔
95
            ),
1,032✔
96
            'base'   => $this->createBaseProperty($schemaProcessor, $schema, $propertyName, $propertySchema),
2,616✔
97
            default  => $this->createTypedProperty(
2,595✔
98
                $schemaProcessor,
2,595✔
99
                $schema,
2,595✔
100
                $propertyName,
2,595✔
101
                $propertySchema,
2,595✔
102
                $resolvedType,
2,595✔
103
                $required,
2,595✔
104
            ),
2,595✔
105
        };
106
    }
107

108
    /**
109
     * Handle a nested object property: generate the nested class, wire the outer property,
110
     * then apply universal modifiers (filter, enum, default, const) on the outer property.
111
     *
112
     * @throws SchemaException
113
     */
114
    private function createObjectProperty(
1,040✔
115
        SchemaProcessor $schemaProcessor,
116
        Schema $schema,
117
        string $propertyName,
118
        JsonSchema $propertySchema,
119
        bool $required,
120
    ): PropertyInterface {
121
        $json     = $propertySchema->getJson();
1,040✔
122
        $property = $this->buildProperty($schemaProcessor, $propertyName, null, $propertySchema, $required);
1,040✔
123

124
        $className = $schemaProcessor->getGeneratorConfiguration()->getClassNameGenerator()->getClassName(
1,040✔
125
            $propertyName,
1,040✔
126
            $propertySchema,
1,040✔
127
            false,
1,040✔
128
            $schemaProcessor->getCurrentClassName(),
1,040✔
129
        );
1,040✔
130

131
        // Strip property-level keywords before passing the schema to processSchema: these keywords
132
        // target the outer property and are handled by the universal modifiers below.
133
        $nestedJson = $json;
1,040✔
134
        unset($nestedJson['filter'], $nestedJson['enum'], $nestedJson['default']);
1,040✔
135

136
        $nestedSchema = $schemaProcessor->processSchema(
1,040✔
137
            $propertySchema->withJson($nestedJson),
1,040✔
138
            $schemaProcessor->getCurrentClassPath(),
1,040✔
139
            $className,
1,040✔
140
            $schema->getSchemaDictionary(),
1,040✔
141
        );
1,040✔
142

143
        if ($nestedSchema !== null) {
1,040✔
144
            $property->setNestedSchema($nestedSchema);
1,040✔
145
            $this->wireObjectProperty($schemaProcessor, $schema, $property, $propertySchema);
1,040✔
146
        }
147

148
        // Universal modifiers (filter, enum, default, const) run on the outer property.
149
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema, anyOnly: true);
1,040✔
150

151
        return $property;
1,038✔
152
    }
153

154
    /**
155
     * Handle a root-level schema (type=base): set up definitions, run all Draft modifiers,
156
     * then transfer any composed properties to the schema.
157
     *
158
     * @throws SchemaException
159
     */
160
    private function createBaseProperty(
2,616✔
161
        SchemaProcessor $schemaProcessor,
162
        Schema $schema,
163
        string $propertyName,
164
        JsonSchema $propertySchema,
165
    ): PropertyInterface {
166
        $schema->getSchemaDictionary()->setUpDefinitionDictionary($schemaProcessor, $schema);
2,616✔
167
        $property = new BaseProperty($propertyName, new PropertyType('object'), $propertySchema);
2,616✔
168

169
        $objectJson         = $propertySchema->getJson();
2,616✔
170
        $objectJson['type'] = 'object';
2,616✔
171
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema->withJson($objectJson));
2,616✔
172

173
        $schemaProcessor->transferComposedPropertiesToSchema($property, $schema);
2,495✔
174

175
        return $property;
2,494✔
176
    }
177

178
    /**
179
     * Handle scalar, array, and untyped properties: construct directly and run all Draft modifiers.
180
     *
181
     * @throws SchemaException
182
     */
183
    private function createTypedProperty(
2,520✔
184
        SchemaProcessor $schemaProcessor,
185
        Schema $schema,
186
        string $propertyName,
187
        JsonSchema $propertySchema,
188
        string $type,
189
        bool $required,
190
    ): PropertyInterface {
191
        $phpType  = $type !== 'any' ? TypeConverter::jsonSchemaToPHP($type) : null;
2,520✔
192
        $property = $this->buildProperty(
2,520✔
193
            $schemaProcessor,
2,520✔
194
            $propertyName,
2,520✔
195
            $phpType !== null ? new PropertyType($phpType) : null,
2,520✔
196
            $propertySchema,
2,520✔
197
            $required,
2,520✔
198
        );
2,520✔
199

200
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema);
2,518✔
201

202
        return $property;
2,439✔
203
    }
204

205
    /**
206
     * Construct a Property with the common required/readOnly/writeOnly setup.
207
     *
208
     * @throws SchemaException
209
     */
210
    private function buildProperty(
2,578✔
211
        SchemaProcessor $schemaProcessor,
212
        string $propertyName,
213
        ?PropertyType $type,
214
        JsonSchema $propertySchema,
215
        bool $required,
216
    ): Property {
217
        $json = $propertySchema->getJson();
2,578✔
218

219
        $isSchemaReadOnly = isset($json['readOnly']) && $json['readOnly'] === true;
2,578✔
220
        $isWriteOnly = isset($json['writeOnly']) && $json['writeOnly'] === true;
2,578✔
221

222
        if ($isSchemaReadOnly && $isWriteOnly) {
2,578✔
223
            throw new SchemaException(
1✔
224
                sprintf(
1✔
225
                    "Property '%s' in file '%s' cannot be both readOnly and writeOnly",
1✔
226
                    $propertyName,
1✔
227
                    $propertySchema->getFile(),
1✔
228
                ),
1✔
229
                $propertySchema,
1✔
230
            );
1✔
231
        }
232

233
        $property = (new Property($propertyName, $type, $propertySchema, $json['description'] ?? ''))
2,577✔
234
            ->setRequired($required)
2,577✔
235
            ->setReadOnly($isSchemaReadOnly || $schemaProcessor->getGeneratorConfiguration()->isImmutable())
2,577✔
236
            ->setWriteOnly($isWriteOnly);
2,577✔
237

238
        if (isset($json['$comment'])) {
2,576✔
239
            $property->setComment($json['$comment']);
3✔
240
        }
241

242
        if (isset($json['examples']) && is_array($json['examples'])) {
2,576✔
243
            $property->setExamples($json['examples']);
5✔
244
        }
245

246
        if ($required && !str_starts_with($propertyName, 'item of array ')) {
2,576✔
247
            // Compute the parent object schema pointer by stripping '<name>/properties' (last two
248
            // path segments) from the property pointer, then appending the 'required' keyword.
249
            $propertyPointer = $propertySchema->getPointer();
1,142✔
250
            $segments = $propertyPointer !== '' ? explode('/', ltrim($propertyPointer, '/')) : [];
1,142✔
251
            $parentPointer = count($segments) > 2 ? '/' . implode('/', array_slice($segments, 0, -2)) : '';
1,142✔
252
            $property->addValidator(
1,142✔
253
                (new RequiredPropertyValidator($property))->withJsonPointer($parentPointer . '/required'),
1,142✔
254
                1,
1,142✔
255
            );
1,142✔
256
        }
257

258
        $configuration = $schemaProcessor->getGeneratorConfiguration();
2,576✔
259

260
        $property
2,576✔
261
            ->addAttribute(
2,576✔
262
                new PhpAttribute(JsonPointer::class, [$propertySchema->getPointer()]),
2,576✔
263
                $configuration,
2,576✔
264
                PhpAttribute::JSON_POINTER,
2,576✔
265
            )
2,576✔
266
            ->addAttribute(
2,576✔
267
                new PhpAttribute(SchemaName::class, [$propertyName]),
2,576✔
268
                $configuration,
2,576✔
269
                PhpAttribute::SCHEMA_NAME,
2,576✔
270
            )
2,576✔
271
            ->addAttribute(
2,576✔
272
                new PhpAttribute(
2,576✔
273
                    JsonSchemaAttribute::class,
2,576✔
274
                    [empty($propertySchema->getJson()) ? '{}' : json_encode($propertySchema->getJson())],
2,576✔
275
                ),
2,576✔
276
                $configuration,
2,576✔
277
                PhpAttribute::JSON_SCHEMA,
2,576✔
278
            );
2,576✔
279

280
        if ($required) {
2,576✔
281
            $property->addAttribute(new PhpAttribute(Required::class), $configuration, PhpAttribute::REQUIRED);
1,152✔
282
        }
283

284
        if (isset($json['readOnly']) && $json['readOnly'] === true) {
2,576✔
285
            $property->addAttribute(
4✔
286
                new PhpAttribute(ReadOnlyProperty::class),
4✔
287
                $configuration,
4✔
288
                PhpAttribute::READ_WRITE_ONLY,
4✔
289
            );
4✔
290
        }
291

292
        if (isset($json['writeOnly']) && $json['writeOnly'] === true) {
2,576✔
293
            $property->addAttribute(
10✔
294
                new PhpAttribute(WriteOnlyProperty::class),
10✔
295
                $configuration,
10✔
296
                PhpAttribute::READ_WRITE_ONLY,
10✔
297
            );
10✔
298
        }
299

300
        if (isset($json['deprecated']) && $json['deprecated'] === true) {
2,576✔
301
            $property->addAttribute(new PhpAttribute(Deprecated::class), $configuration, PhpAttribute::DEPRECATED);
2✔
302
        }
303

304
        return $property;
2,576✔
305
    }
306

307
    /**
308
     * Resolve a $ref reference by looking it up in the definition dictionary.
309
     *
310
     * @throws SchemaException
311
     */
312
    private function processReference(
570✔
313
        SchemaProcessor $schemaProcessor,
314
        Schema $schema,
315
        string $propertyName,
316
        JsonSchema $propertySchema,
317
        bool $required,
318
    ): PropertyInterface {
319
        $path       = [];
570✔
320
        $reference  = $propertySchema->getJson()['$ref'];
570✔
321
        $dictionary = $schema->getSchemaDictionary();
570✔
322

323
        try {
324
            $definition = $dictionary->getDefinition($reference, $schemaProcessor, $path);
570✔
325

326
            if ($definition) {
562✔
327
                $definitionSchema = $definition->getSchema();
562✔
328

329
                if (
330
                    $schema->getClassPath() !== $definitionSchema->getClassPath() ||
562✔
331
                    $schema->getClassName() !== $definitionSchema->getClassName() ||
561✔
332
                    (
333
                        $schema->getClassName() === 'ExternalSchema' &&
562✔
334
                        $definitionSchema->getClassName() === 'ExternalSchema'
562✔
335
                    )
336
                ) {
337
                    $schema->addNamespaceTransferDecorator(
270✔
338
                        new SchemaNamespaceTransferDecorator($definitionSchema),
270✔
339
                    );
270✔
340

341
                    if ($definitionSchema->getClassName() !== 'ExternalSchema') {
270✔
342
                        $schema->addUsedClass(join('\\', array_filter([
77✔
343
                            $schemaProcessor->getGeneratorConfiguration()->getNamespacePrefix(),
77✔
344
                            $definitionSchema->getClassPath(),
77✔
345
                            $definitionSchema->getClassName(),
77✔
346
                        ])));
77✔
347
                    }
348
                }
349

350
                $property = $definition->resolveReference(
562✔
351
                    $propertyName,
562✔
352
                    implode('/', $path),
562✔
353
                    $required,
562✔
354
                    $propertySchema->getJson()['_dependencies'] ?? null,
562✔
355
                );
562✔
356

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

363
                return $property;
562✔
364
            }
365
        } catch (Exception $exception) {
8✔
366
            throw new SchemaException(
8✔
367
                "Unresolved Reference $reference in file {$propertySchema->getFile()}",
8✔
368
                null,
8✔
369
                0,
8✔
370
                $exception,
8✔
371
            );
8✔
372
        }
373

NEW
374
        throw new SchemaException(
×
NEW
375
            "Unresolved Reference $reference in file {$propertySchema->getFile()}",
×
NEW
376
            $propertySchema,
×
NEW
377
        );
×
378
    }
379

380
    /**
381
     * Resolve a $ref on a base-level schema: set up definitions, delegate to processReference,
382
     * then copy the referenced schema's properties to the parent schema.
383
     *
384
     * @throws SchemaException
385
     */
386
    private function processBaseReference(
43✔
387
        SchemaProcessor $schemaProcessor,
388
        Schema $schema,
389
        string $propertyName,
390
        JsonSchema $propertySchema,
391
        bool $required,
392
    ): PropertyInterface {
393
        $schema->getSchemaDictionary()->setUpDefinitionDictionary($schemaProcessor, $schema);
43✔
394

395
        $property = $this->processReference($schemaProcessor, $schema, $propertyName, $propertySchema, $required);
43✔
396

397
        if (!$property->getNestedSchema()) {
43✔
398
            throw new SchemaException(
1✔
399
                sprintf(
1✔
400
                    'A referenced schema on base level must provide an object definition for property %s in file %s',
1✔
401
                    $propertyName,
1✔
402
                    $propertySchema->getFile(),
1✔
403
                ),
1✔
404
                $propertySchema,
1✔
405
            );
1✔
406
        }
407

408
        foreach ($property->getNestedSchema()->getProperties() as $propertiesOfReferencedObject) {
42✔
409
            $schema->addProperty($propertiesOfReferencedObject);
42✔
410
        }
411

412
        return $property;
42✔
413
    }
414

415
    /**
416
     * Handle "type": [...] properties by processing each type through its Draft modifiers,
417
     * merging validators and decorators onto a single property, then consolidating type checks.
418
     *
419
     * @param string[] $types
420
     *
421
     * @throws SchemaException
422
     */
423
    private function createMultiTypeProperty(
97✔
424
        SchemaProcessor $schemaProcessor,
425
        Schema $schema,
426
        string $propertyName,
427
        JsonSchema $propertySchema,
428
        array $types,
429
        bool $required,
430
    ): PropertyInterface {
431
        $json     = $propertySchema->getJson();
97✔
432
        $property = $this->buildProperty($schemaProcessor, $propertyName, null, $propertySchema, $required);
97✔
433

434
        $collectedTypes   = [];
97✔
435
        $typeHints        = [];
97✔
436
        $resolvedSubCount = 0;
97✔
437
        $totalSubCount    = count($types);
97✔
438

439
        // Strip the default from sub-schemas so that default handling runs only once via the
440
        // universal DefaultValueModifier below, which already handles the multi-type case.
441
        $subJson = $json;
97✔
442
        unset($subJson['default']);
97✔
443

444
        foreach ($types as $type) {
97✔
445
            $this->checkType($type, $schema);
97✔
446

447
            $subJson['type'] = $type;
97✔
448
            $subSchema       = $propertySchema->withJson($subJson);
97✔
449

450
            // For type=object, delegate to the same object path (processSchema + wireObjectProperty).
451
            $subProperty = $type === 'object'
97✔
452
                ? $this->createObjectProperty($schemaProcessor, $schema, $propertyName, $subSchema, $required)
8✔
453
                : $this->createSubTypeProperty(
96✔
454
                    $schemaProcessor,
96✔
455
                    $schema,
96✔
456
                    $propertyName,
96✔
457
                    $subSchema,
96✔
458
                    $type,
96✔
459
                    $required,
96✔
460
                );
96✔
461

462
            $subProperty->onResolve(function () use (
97✔
463
                $property,
97✔
464
                $subProperty,
97✔
465
                $schemaProcessor,
97✔
466
                $schema,
97✔
467
                $propertySchema,
97✔
468
                $totalSubCount,
97✔
469
                &$collectedTypes,
97✔
470
                &$typeHints,
97✔
471
                &$resolvedSubCount,
97✔
472
            ): void {
97✔
473
                foreach ($subProperty->getValidators() as $validatorContainer) {
97✔
474
                    $validator = $validatorContainer->getValidator();
97✔
475

476
                    if ($validator instanceof TypeCheckInterface) {
97✔
477
                        array_push($collectedTypes, ...$validator->getTypes());
97✔
478
                        continue;
97✔
479
                    }
480

481
                    $property->addValidator(
59✔
482
                        $validator,
59✔
483
                        $validatorContainer->getPriority(),
59✔
484
                        $validatorContainer->getSourceKey(),
59✔
485
                    );
59✔
486
                }
487

488
                if ($subProperty->getDecorators()) {
97✔
489
                    $property->addDecorator(new PropertyTransferDecorator($subProperty));
34✔
490
                }
491

492
                $typeHints[] = $subProperty->getTypeHint();
97✔
493

494
                if (++$resolvedSubCount < $totalSubCount || empty($collectedTypes)) {
97✔
495
                    return;
96✔
496
                }
497

498
                $this->finalizeMultiTypeProperty(
97✔
499
                    $property,
97✔
500
                    array_unique($collectedTypes),
97✔
501
                    $typeHints,
97✔
502
                    $schemaProcessor,
97✔
503
                    $schema,
97✔
504
                    $propertySchema,
97✔
505
                );
97✔
506
            });
97✔
507
        }
508

509
        return $property;
94✔
510
    }
511

512
    /**
513
     * Build a non-object sub-property for a multi-type array, applying only type-specific
514
     * modifiers (no universal 'any' modifiers — those run once on the parent after finalization).
515
     *
516
     * @throws SchemaException
517
     */
518
    private function createSubTypeProperty(
96✔
519
        SchemaProcessor $schemaProcessor,
520
        Schema $schema,
521
        string $propertyName,
522
        JsonSchema $propertySchema,
523
        string $type,
524
        bool $required,
525
    ): Property {
526
        $subProperty = $this->buildProperty(
96✔
527
            $schemaProcessor,
96✔
528
            $propertyName,
96✔
529
            new PropertyType(TypeConverter::jsonSchemaToPHP($type)),
96✔
530
            $propertySchema,
96✔
531
            $required,
96✔
532
        );
96✔
533

534
        $this->applyModifiers($schemaProcessor, $schema, $subProperty, $propertySchema, anyOnly: false, typeOnly: true);
96✔
535

536
        return $subProperty;
96✔
537
    }
538

539
    /**
540
     * Called once all sub-properties of a multi-type property have resolved.
541
     * Adds the consolidated MultiTypeCheckValidator, sets the union PropertyType,
542
     * attaches the type-hint decorator, and runs universal modifiers.
543
     *
544
     * @param string[] $collectedTypes
545
     * @param string[] $typeHints
546
     *
547
     * @throws SchemaException
548
     */
549
    private function finalizeMultiTypeProperty(
97✔
550
        PropertyInterface $property,
551
        array $collectedTypes,
552
        array $typeHints,
553
        SchemaProcessor $schemaProcessor,
554
        Schema $schema,
555
        JsonSchema $propertySchema,
556
    ): void {
557
        $hasNull      = in_array('null', $collectedTypes, true);
97✔
558
        $nonNullTypes = array_values(array_filter(
97✔
559
            $collectedTypes,
97✔
560
            static fn(string $type): bool => $type !== 'null',
97✔
561
        ));
97✔
562

563
        $allowImplicitNull = $schemaProcessor->getGeneratorConfiguration()->isImplicitNullAllowed()
97✔
564
            && !$property->isRequired();
97✔
565

566
        $property->addValidator(
97✔
567
            (new MultiTypeCheckValidator($collectedTypes, $property, $allowImplicitNull))
97✔
568
                ->withJsonPointer($propertySchema->getPointer() . '/type'),
97✔
569
            2,
97✔
570
        );
97✔
571

572
        if ($nonNullTypes) {
97✔
573
            $property->setType(
97✔
574
                new PropertyType($nonNullTypes, $hasNull ? true : null),
97✔
575
                new PropertyType($nonNullTypes, $hasNull ? true : null),
97✔
576
            );
97✔
577
        }
578

579
        $property->addTypeHintDecorator(new TypeHintDecorator($typeHints));
97✔
580

581
        $this->applyModifiers($schemaProcessor, $schema, $property, $propertySchema, true);
97✔
582
    }
583

584
    /**
585
     * Wire the outer property for a nested object: add the type-check validator and instantiation
586
     * linkage. Schema-targeting modifiers are intentionally NOT run here because processSchema
587
     * already applied them to the nested schema.
588
     *
589
     * @throws SchemaException
590
     */
591
    private function wireObjectProperty(
1,040✔
592
        SchemaProcessor $schemaProcessor,
593
        Schema $schema,
594
        PropertyInterface $property,
595
        JsonSchema $propertySchema,
596
    ): void {
597
        (new TypeCheckModifier(TypeConverter::jsonSchemaToPHP('object')))->modify(
1,040✔
598
            $schemaProcessor,
1,040✔
599
            $schema,
1,040✔
600
            $property,
1,040✔
601
            $propertySchema,
1,040✔
602
        );
1,040✔
603

604
        (new ObjectModifier())->modify($schemaProcessor, $schema, $property, $propertySchema);
1,040✔
605
    }
606

607
    /**
608
     * Run Draft modifiers for the given property.
609
     *
610
     * By default all covered types (type-specific + 'any') run. Pass $anyOnly=true to run
611
     * only the 'any' entry (used for object outer-property universal keywords), or $typeOnly=true
612
     * to run only type-specific entries (used for multi-type sub-properties).
613
     *
614
     * @throws SchemaException
615
     */
616
    private function applyModifiers(
2,617✔
617
        SchemaProcessor $schemaProcessor,
618
        Schema $schema,
619
        PropertyInterface $property,
620
        JsonSchema $propertySchema,
621
        bool $anyOnly = false,
622
        bool $typeOnly = false,
623
    ): void {
624
        $type       = $propertySchema->getJson()['type'] ?? 'any';
2,617✔
625
        $builtDraft = $this->resolveBuiltDraft($schemaProcessor, $propertySchema);
2,617✔
626

627
        // For untyped properties ('any'), only run the 'any' entry — getCoveredTypes('any')
628
        // returns all types, which would incorrectly apply type-specific modifiers.
629
        $coveredTypes = $type === 'any'
2,617✔
630
            ? array_filter($builtDraft->getCoveredTypes('any'), static fn($t) => $t->getType() === 'any')
718✔
631
            : $builtDraft->getCoveredTypes($type);
2,617✔
632

633
        foreach ($coveredTypes as $coveredType) {
2,617✔
634
            $isAnyEntry = $coveredType->getType() === 'any';
2,617✔
635

636
            if ($anyOnly && !$isAnyEntry) {
2,617✔
637
                continue;
1,122✔
638
            }
639

640
            if ($typeOnly && $isAnyEntry) {
2,617✔
641
                continue;
96✔
642
            }
643

644
            foreach ($coveredType->getModifiers() as $modifier) {
2,617✔
645
                $countBefore = count($property->getValidators());
2,617✔
646
                $modifier->modify($schemaProcessor, $schema, $property, $propertySchema);
2,617✔
647

648
                // Tag every validator that was just added by this modifier with its schema
649
                // keyword so FilterProcessor can later classify each validator as
650
                // input-space or output-space relative to a transforming filter.
651
                // This must cover all Draft-registered validators — not only those known
652
                // to interact with filters today — because a custom Draft may register
653
                // any validator factory under any type, and the classification must work
654
                // without enumerating individual keywords.
655
                if ($modifier instanceof AbstractValidatorFactory && ($modifierKey = $modifier->getKey()) !== null) {
2,602✔
656
                    foreach (array_slice($property->getValidators(), $countBefore) as $validatorWrapper) {
2,601✔
657
                        $validatorWrapper->setSourceKey($modifierKey);
1,925✔
658
                    }
659
                }
660
            }
661
        }
662
    }
663

664
    /**
665
     * @throws SchemaException
666
     */
667
    private function checkType(mixed $type, Schema $schema): void
2,617✔
668
    {
669
        if (is_string($type)) {
2,617✔
670
            return;
2,617✔
671
        }
672

673
        throw new SchemaException(
1✔
674
            sprintf(
1✔
675
                'Invalid property type %s in file %s',
1✔
676
                $type,
1✔
677
                $schema->getJsonSchema()->getFile(),
1✔
678
            ),
1✔
679
            $schema->getJsonSchema(),
1✔
680
        );
1✔
681
    }
682

683
    private function resolveBuiltDraft(SchemaProcessor $schemaProcessor, JsonSchema $propertySchema): Draft
2,617✔
684
    {
685
        $configDraft = $schemaProcessor->getGeneratorConfiguration()->getDraft();
2,617✔
686

687
        $draft = $configDraft instanceof DraftFactoryInterface
2,617✔
688
            ? $configDraft->getDraftForSchema($propertySchema)
2,614✔
689
            : $configDraft;
3✔
690

691
        return $this->draftCache[$draft::class] ??= $draft->getDefinition()->build();
2,617✔
692
    }
693
}
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