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

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

03 Jun 2026 08:37PM UTC coverage: 98.72% (+0.006%) from 98.714%
26912113495

push

github

wol-soft
Merge remote-tracking branch 'origin/master'

157 of 168 new or added lines in 9 files covered. (93.45%)

42 existing lines in 5 files now uncovered.

6172 of 6252 relevant lines covered (98.72%)

574.49 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,575✔
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,575✔
87
        $this->schemaDefinitionDictionary = $dictionary ?? new SchemaDefinitionDictionary($schema);
2,575✔
88
        $this->description = $schema->getJson()['description'] ?? '';
2,575✔
89
        $this->propertyMerger = new PropertyMerger($generatorConfiguration);
2,575✔
90

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

112
        if (isset($schema->getJson()['deprecated']) && $schema->getJson()['deprecated'] === true) {
2,575✔
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,434✔
122
    {
123
        return $this->targetFileName;
2,434✔
124
    }
125

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

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

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

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

147
        return $this;
468✔
148
    }
149

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

162
            return false;
1,254✔
163
        };
2,432✔
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
        usort(
2,432✔
168
            $this->properties,
2,432✔
169
            static function (
2,432✔
170
                PropertyInterface $property,
2,432✔
171
                PropertyInterface $comparedProperty,
2,432✔
172
            ) use ($hasSchemaDependencyValidator): int {
2,432✔
173
                $propertyHasSchemaDependencyValidator = $hasSchemaDependencyValidator($property);
1,254✔
174
                $comparedPropertyHasSchemaDependencyValidator = $hasSchemaDependencyValidator($comparedProperty);
1,254✔
175
                return $comparedPropertyHasSchemaDependencyValidator <=> $propertyHasSchemaDependencyValidator;
1,254✔
176
            },
2,432✔
177
        );
2,432✔
178

179
        return $this->properties;
2,432✔
180
    }
181

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

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

206
            $this->attributeIndex[$attribute] = $property->getName();
2,407✔
207
            $this->properties[$property->getName()] = $property;
2,407✔
208

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

213
            $property->onResolve(function (): void {
2,407✔
214
                if (++$this->resolvedProperties === count($this->properties)) {
2,359✔
215
                    foreach ($this->onAllPropertiesResolvedCallbacks as $callback) {
2,359✔
UNCOV
216
                        $callback();
×
217

UNCOV
218
                        $this->onAllPropertiesResolvedCallbacks = [];
×
219
                    }
220
                }
221
            });
2,407✔
222

223
            return $this;
2,407✔
224
        }
225

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

233
        return $this;
187✔
234
    }
235

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

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

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

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

261
        foreach ($this->baseValidators as $key => $validator) {
2,425✔
262
            if (is_a($validator, AbstractComposedPropertyValidator::class)) {
729✔
263
                $keys[] = $key;
379✔
264
            }
265
        }
266

267
        return $keys;
2,425✔
268
    }
269

270
    public function addBaseValidator(PropertyValidatorInterface $baseValidator): self
736✔
271
    {
272
        $this->baseValidators[] = $baseValidator;
736✔
273

274
        return $this;
736✔
275
    }
276

277
    public function getSchemaDictionary(): SchemaDefinitionDictionary
2,556✔
278
    {
279
        return $this->schemaDefinitionDictionary;
2,556✔
280
    }
281

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

289
        return $this;
2,575✔
290
    }
291

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

296
        return $this;
1,036✔
297
    }
298

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

308
        foreach ($this->namespaceTransferDecorators as $decorator) {
2,408✔
309
            $usedClasses = array_merge($usedClasses, $decorator->resolve(array_merge($visitedSchema, [$this])));
1,025✔
310
        }
311

312
        return $usedClasses;
2,408✔
313
    }
314

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

322
        return $this;
1,269✔
323
    }
324

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

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

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

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

351
        return $this;
60✔
352
    }
353

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

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

367
        return $this;
2,575✔
368
    }
369

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

377
        return $this;
2,424✔
378
    }
379

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

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

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