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

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

02 Jul 2026 11:52PM UTC coverage: 98.749% (-0.003%) from 98.752%
28628957811

Pull #160

github

wol-soft
Add JSON Pointer (RFC 6901) stamping on all ValidationExceptions

Every ValidationException now carries a getJsonPointer() method (provided
by the production library) that identifies the schema keyword that rejected
the value (e.g. /properties/age/minimum, /patternProperties/^S~0~1).

Changes in the generator:
- All validator factories call ->withJsonPointer() on every validator they
  construct, threading the pointer from the JsonSchema pointer registry
- AbstractPropertyValidator gains withJsonPointer() / getJsonPointer();
  withJsonPointer() forwards resolution to the clone so the validator
  resolution chain works correctly for recursive $ref schemas
- FilterValidator overrides withJsonPointer() to propagate the pointer to
  its embedded filterValueValidator
- MediaStringModifier, EnumPostProcessor, FilterValidatorFactory, and
  ExtendObjectPropertiesMatchingPatternPropertiesPostProcessor all pass
  the appropriate pointer through FilterProcessor::process()
- AdditionalPropertiesAccessorPostProcessor resolves the primary pointer
  from the synthesised #[JsonPointer] attribute on each property
- PhpAttribute gains getArguments() to support the above at generation time
- Documentation updated; all existing pointer-assertion tests are merged
  into their sibling validation tests rather than being standalone

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Pull Request #160: Add JSON Pointer (RFC 6901) stamping on all ValidationExceptions

168 of 169 new or added lines in 30 files covered. (99.41%)

4 existing lines in 1 file now uncovered.

6473 of 6555 relevant lines covered (98.75%)

570.3 hits per line

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

96.69
/src/Model/Schema.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\Model;
6

7
use PHPModelGenerator\Attributes\Deprecated;
8
use PHPModelGenerator\Attributes\JsonPointer;
9
use PHPModelGenerator\Attributes\JsonSchema as JsonSchemaAttribute;
10
use PHPModelGenerator\Attributes\Source;
11
use PHPModelGenerator\Exception\SchemaException;
12
use PHPModelGenerator\Interfaces\JSONModelInterface;
13
use PHPModelGenerator\Model\Attributes\AttributesTrait;
14
use PHPModelGenerator\Model\Attributes\PhpAttribute;
15
use PHPModelGenerator\Model\Property\PropertyInterface;
16
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
17
use PHPModelGenerator\Model\SchemaDefinition\JsonSchemaTrait;
18
use PHPModelGenerator\Model\SchemaDefinition\SchemaDefinitionDictionary;
19
use PHPModelGenerator\Model\Validator\AbstractComposedPropertyValidator;
20
use PHPModelGenerator\Model\Validator\PropertyValidatorInterface;
21
use PHPModelGenerator\Model\Validator\SchemaDependencyValidator;
22
use PHPModelGenerator\Model\Validator\Factory\Composition\AllOfValidatorFactory;
23
use PHPModelGenerator\PropertyProcessor\Decorator\SchemaNamespaceTransferDecorator;
24
use PHPModelGenerator\SchemaProcessor\Hook\SchemaHookInterface;
25
use PHPModelGenerator\Utils\PropertyMerger;
26

27
/**
28
 * Class Schema
29
 *
30
 * @package PHPModelGenerator\Model
31
 */
32
class Schema
33
{
34
    use JsonSchemaTrait;
35
    use AttributesTrait;
36

37
    /** @var string */
38
    protected $description;
39

40
    /** @var string[] */
41
    protected $traits = [];
42
    /** @var string[] */
43
    protected $interfaces = [];
44
    /** @var PropertyInterface[] The properties which are part of the class */
45
    protected $properties = [];
46
    /** @var MethodInterface[] */
47
    protected $methods = [];
48

49
    /** @var PropertyValidatorInterface[] A Collection of validators which must be applied
50
     *                                    before adding properties to the model
51
     */
52
    protected $baseValidators = [];
53
    /** @var string[] */
54
    protected $usedClasses = [];
55
    /** @var SchemaNamespaceTransferDecorator[] */
56
    protected $namespaceTransferDecorators = [];
57
    /** @var SchemaHookInterface[] */
58
    protected $schemaHooks = [];
59

60
    protected SchemaDefinitionDictionary $schemaDefinitionDictionary;
61

62
    private int $resolvedProperties = 0;
63
    /** @var callable[] */
64
    private array $onAllPropertiesResolvedCallbacks = [];
65

66
    /** @var array<string, true> */
67
    private array $rootRegisteredPropertyNames = [];
68

69
    /** @var string[] Maps normalized attribute → raw property name; used to detect property-vs-property collisions */
70
    private array $attributeIndex = [];
71

72
    private PropertyMerger $propertyMerger;
73

74
    /**
75
     * Schema constructor.
76
     */
77
    public function __construct(
2,628✔
78
        protected string $targetFileName,
79
        protected string $classPath,
80
        protected string $className,
81
        JsonSchema $schema,
82
        ?SchemaDefinitionDictionary $dictionary = null,
83
        protected bool $initialClass = false,
84
        ?GeneratorConfiguration $generatorConfiguration = null,
85
    ) {
86
        $this->jsonSchema = $schema;
2,628✔
87
        $this->schemaDefinitionDictionary = $dictionary ?? new SchemaDefinitionDictionary($schema);
2,628✔
88
        $this->description = $schema->getJson()['description'] ?? '';
2,628✔
89
        $this->propertyMerger = new PropertyMerger($generatorConfiguration);
2,628✔
90

91
        $this
2,628✔
92
            ->addInterface(JSONModelInterface::class)
2,628✔
93
            ->addAttribute(
2,628✔
94
                new PhpAttribute(JsonPointer::class, [$schema->getPointer()]),
2,628✔
95
                $generatorConfiguration,
2,628✔
96
                PhpAttribute::JSON_POINTER,
2,628✔
97
            )
2,628✔
98
            ->addAttribute(
2,628✔
99
                new PhpAttribute(
2,628✔
100
                    JsonSchemaAttribute::class,
2,628✔
101
                    [empty($schema->getJson()) ? '{}' : json_encode($schema->getJson())],
2,628✔
102
                ),
2,628✔
103
                $generatorConfiguration,
2,628✔
104
                PhpAttribute::JSON_SCHEMA,
2,628✔
105
            )
2,628✔
106
            ->addAttribute(
2,628✔
107
                new PhpAttribute(Source::class, [$schema->getFile()]),
2,628✔
108
                $generatorConfiguration,
2,628✔
109
                PhpAttribute::SOURCE,
2,628✔
110
            );
2,628✔
111

112
        if (isset($schema->getJson()['deprecated']) && $schema->getJson()['deprecated'] === true) {
2,628✔
113
            $this->addAttribute(
2✔
114
                new PhpAttribute(Deprecated::class),
2✔
115
                $generatorConfiguration,
2✔
116
                PhpAttribute::DEPRECATED,
2✔
117
            );
2✔
118
        }
119
    }
120

121
    public function getTargetFileName(): string
2,478✔
122
    {
123
        return $this->targetFileName;
2,478✔
124
    }
125

126
    public function getClassName(): string
2,467✔
127
    {
128
        return $this->className;
2,467✔
129
    }
130

131
    public function getClassPath(): string
2,460✔
132
    {
133
        return $this->classPath;
2,460✔
134
    }
135

136
    public function getDescription(): string
2,442✔
137
    {
138
        return $this->description;
2,442✔
139
    }
140

141
    public function onAllPropertiesResolved(callable $callback): self
500✔
142
    {
143
        $this->resolvedProperties === count($this->properties)
500✔
144
            ? $callback()
497✔
145
            : $this->onAllPropertiesResolvedCallbacks[] = $callback;
×
146

147
        return $this;
497✔
148
    }
149

150
    /**
151
     * @return PropertyInterface[]
152
     */
153
    public function getProperties(): array
2,476✔
154
    {
155
        $hasSchemaDependencyValidator = static function (PropertyInterface $property): bool {
2,476✔
156
            foreach ($property->getValidators() as $validator) {
1,286✔
157
                if ($validator->getValidator() instanceof SchemaDependencyValidator) {
1,013✔
158
                    return true;
63✔
159
                }
160
            }
161

162
            return false;
1,286✔
163
        };
2,476✔
164

165
        // Order the properties to make sure properties with a SchemaDependencyValidator are validated at the beginning
166
        // of the validation process for correct exception order of the messages.
167
        // uasort preserves the string keys (property names) that getProperty() relies on;
168
        // usort would reindex to 0,1,2,... and break all subsequent name-based lookups.
169
        uasort(
2,476✔
170
            $this->properties,
2,476✔
171
            static function (
2,476✔
172
                PropertyInterface $property,
2,476✔
173
                PropertyInterface $comparedProperty,
2,476✔
174
            ) use ($hasSchemaDependencyValidator): int {
2,476✔
175
                $propertyHasSchemaDependencyValidator = $hasSchemaDependencyValidator($property);
1,286✔
176
                $comparedPropertyHasSchemaDependencyValidator = $hasSchemaDependencyValidator($comparedProperty);
1,286✔
177
                return $comparedPropertyHasSchemaDependencyValidator <=> $propertyHasSchemaDependencyValidator;
1,286✔
178
            },
2,476✔
179
        );
2,476✔
180

181
        return $this->properties;
2,476✔
182
    }
183

184
    /**
185
     * @param string|null $compositionProcessor The FQCN of the composition processor transferring this property,
186
     *                                           or null when not called from a composition context.
187
     *
188
     * @throws SchemaException
189
     */
190
    public function addProperty(PropertyInterface $property, ?string $compositionProcessor = null): self
2,451✔
191
    {
192
        if (!isset($this->properties[$property->getName()])) {
2,451✔
193
            $attribute = $property->getAttribute();
2,451✔
194

195
            $existingRawName = $this->attributeIndex[$attribute] ?? null;
2,451✔
196
            if ($existingRawName !== null && $existingRawName !== $property->getName()) {
2,451✔
197
                throw new SchemaException(
5✔
198
                    sprintf(
5✔
199
                        "Property names '%s' and '%s' both normalize to attribute '%s' in file %s",
5✔
200
                        $existingRawName,
5✔
201
                        $property->getName(),
5✔
202
                        $attribute,
5✔
203
                        $this->jsonSchema->getFile(),
5✔
204
                    ),
5✔
205
                );
5✔
206
            }
207

208
            $this->attributeIndex[$attribute] = $property->getName();
2,451✔
209
            $this->properties[$property->getName()] = $property;
2,451✔
210

211
            if ($compositionProcessor === null) {
2,451✔
212
                $this->rootRegisteredPropertyNames[$property->getName()] = true;
2,451✔
213
            }
214

215
            $property->onResolve(function (): void {
2,451✔
216
                if (++$this->resolvedProperties === count($this->properties)) {
2,403✔
217
                    foreach ($this->onAllPropertiesResolvedCallbacks as $callback) {
2,403✔
UNCOV
218
                        $callback();
×
219

UNCOV
220
                        $this->onAllPropertiesResolvedCallbacks = [];
×
221
                    }
222
                }
223
            });
2,451✔
224

225
            return $this;
2,451✔
226
        }
227

228
        $this->propertyMerger->merge(
214✔
229
            $this->properties[$property->getName()],
214✔
230
            $property,
214✔
231
            is_a($compositionProcessor, AllOfValidatorFactory::class, true),
214✔
232
            $this->isRootRegistered($property->getName()),
214✔
233
        );
214✔
234

235
        return $this;
211✔
236
    }
237

238
    public function isRootRegistered(string $name): bool
415✔
239
    {
240
        return isset($this->rootRegisteredPropertyNames[$name]);
415✔
241
    }
242

243
    public function getProperty(string $name): ?PropertyInterface
402✔
244
    {
245
        return $this->properties[$name] ?? null;
402✔
246
    }
247

248
    /**
249
     * @return PropertyValidatorInterface[]
250
     */
251
    public function getBaseValidators(): array
2,461✔
252
    {
253
        return $this->baseValidators;
2,461✔
254
    }
255

256
    /**
257
     * Get the keys of all composition base validators
258
     */
259
    public function getCompositionValidatorKeys(): array
2,461✔
260
    {
261
        $keys = [];
2,461✔
262

263
        foreach ($this->baseValidators as $key => $validator) {
2,461✔
264
            if (is_a($validator, AbstractComposedPropertyValidator::class)) {
757✔
265
                $keys[] = $key;
401✔
266
            }
267
        }
268

269
        return $keys;
2,461✔
270
    }
271

272
    public function addBaseValidator(PropertyValidatorInterface $baseValidator): self
772✔
273
    {
274
        $this->baseValidators[] = $baseValidator;
772✔
275

276
        return $this;
772✔
277
    }
278

279
    public function getSchemaDictionary(): SchemaDefinitionDictionary
2,600✔
280
    {
281
        return $this->schemaDefinitionDictionary;
2,600✔
282
    }
283

284
    /**
285
     * Add a class to the schema which is required
286
     */
287
    public function addUsedClass(string $fqcn): self
2,628✔
288
    {
289
        $this->usedClasses[] = trim($fqcn, '\\');
2,628✔
290

291
        return $this;
2,628✔
292
    }
293

294
    public function addNamespaceTransferDecorator(SchemaNamespaceTransferDecorator $decorator): self
1,068✔
295
    {
296
        $this->namespaceTransferDecorators[] = $decorator;
1,068✔
297

298
        return $this;
1,068✔
299
    }
300

301
    /**
302
     * @param Schema[] $visitedSchema
303
     *
304
     * @return string[]
305
     */
306
    public function getUsedClasses(array $visitedSchema = []): array
2,442✔
307
    {
308
        $usedClasses = $this->usedClasses;
2,442✔
309

310
        foreach ($this->namespaceTransferDecorators as $decorator) {
2,442✔
311
            $usedClasses = array_merge($usedClasses, $decorator->resolve(array_merge($visitedSchema, [$this])));
1,049✔
312
        }
313

314
        return $usedClasses;
2,442✔
315
    }
316

317
    /**
318
     * @param string $methodKey An unique key in the scope of the schema to identify the method
319
     */
320
    public function addMethod(string $methodKey, MethodInterface $method): self
1,291✔
321
    {
322
        $this->methods[$methodKey] = $method;
1,291✔
323

324
        return $this;
1,291✔
325
    }
326

327
    /**
328
     * @return MethodInterface[]
329
     */
330
    public function getMethods(): array
2,442✔
331
    {
332
        return $this->methods;
2,442✔
333
    }
334

335
    public function hasMethod(string $methodKey): bool
813✔
336
    {
337
        return isset($this->methods[$methodKey]);
813✔
338
    }
339

340
    /**
341
     * @return string[]
342
     */
343
    public function getTraits(): array
2,442✔
344
    {
345
        return $this->traits;
2,442✔
346
    }
347

348
    public function addTrait(string $trait): self
60✔
349
    {
350
        $this->traits[] = $trait;
60✔
351
        $this->addUsedClass($trait);
60✔
352

353
        return $this;
60✔
354
    }
355

356
    /**
357
     * @return string[]
358
     */
359
    public function getInterfaces(): array
2,442✔
360
    {
361
        return $this->interfaces;
2,442✔
362
    }
363

364
    public function addInterface(string $interface): self
2,628✔
365
    {
366
        $this->interfaces[] = $interface;
2,628✔
367
        $this->addUsedClass($interface);
2,628✔
368

369
        return $this;
2,628✔
370
    }
371

372
    /**
373
     * Add an additional schema hook
374
     */
375
    public function addSchemaHook(SchemaHookInterface $schemaHook): self
2,460✔
376
    {
377
        $this->schemaHooks[] = $schemaHook;
2,460✔
378

379
        return $this;
2,460✔
380
    }
381

382
    /**
383
     * @return SchemaHookInterface[]
384
     */
385
    public function getSchemaHooks(): array
2,442✔
386
    {
387
        return $this->schemaHooks;
2,442✔
388
    }
389

UNCOV
390
    public function isInitialClass(): bool
×
391
    {
UNCOV
392
        return $this->initialClass;
×
393
    }
394

395
    public function getPropertyMerger(): PropertyMerger
399✔
396
    {
397
        return $this->propertyMerger;
399✔
398
    }
399
}
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