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

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

23 Mar 2026 08:31PM UTC coverage: 98.58% (-0.1%) from 98.693%
23458676927

Pull #120

github

web-flow
Merge abb6f5013 into d14ae3d85
Pull Request #120: Fix/issue 98

568 of 579 new or added lines in 54 files covered. (98.1%)

3889 of 3945 relevant lines covered (98.58%)

553.63 hits per line

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

99.15
/src/PropertyProcessor/Property/BaseProcessor.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\PropertyProcessor\Property;
6

7
use PHPMicroTemplate\Exception\FileSystemException;
8
use PHPMicroTemplate\Exception\SyntaxErrorException;
9
use PHPMicroTemplate\Exception\UndefinedSymbolException;
10
use PHPModelGenerator\Exception\Object\MaxPropertiesException;
11
use PHPModelGenerator\Exception\Object\MinPropertiesException;
12
use PHPModelGenerator\Exception\SchemaException;
13
use PHPModelGenerator\Model\Property\BaseProperty;
14
use PHPModelGenerator\Model\Property\CompositionPropertyDecorator;
15
use PHPModelGenerator\Model\Property\Property;
16
use PHPModelGenerator\Model\Property\PropertyInterface;
17
use PHPModelGenerator\Model\Property\PropertyType;
18
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
19
use PHPModelGenerator\Model\Validator;
20
use PHPModelGenerator\Model\Validator\AbstractComposedPropertyValidator;
21
use PHPModelGenerator\Exception\Generic\DeniedPropertyException;
22
use PHPModelGenerator\Model\Validator\AdditionalPropertiesValidator;
23
use PHPModelGenerator\Model\Validator\ComposedPropertyValidator;
24
use PHPModelGenerator\Model\Validator\ConditionalPropertyValidator;
25
use PHPModelGenerator\Model\Validator\NoAdditionalPropertiesValidator;
26
use PHPModelGenerator\Model\Validator\PatternPropertiesValidator;
27
use PHPModelGenerator\Model\Validator\PropertyNamesValidator;
28
use PHPModelGenerator\Model\Validator\PropertyTemplateValidator;
29
use PHPModelGenerator\Model\Validator\PropertyValidator;
30
use PHPModelGenerator\PropertyProcessor\ComposedValue\AllOfProcessor;
31
use PHPModelGenerator\PropertyProcessor\ComposedValue\ComposedPropertiesInterface;
32
use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection;
33
use PHPModelGenerator\PropertyProcessor\PropertyFactory;
34
use PHPModelGenerator\PropertyProcessor\PropertyProcessorFactory;
35

36
/**
37
 * Class BaseObjectProcessor
38
 *
39
 * @package PHPModelGenerator\PropertyProcessor\Property
40
 */
41
class BaseProcessor extends AbstractPropertyProcessor
42
{
43
    protected const TYPE = 'object';
44

45
    private const string COUNT_PROPERTIES =
46
        'count(
47
            array_unique(
48
                array_merge(
49
                    array_keys($this->_rawModelDataInput),
50
                    array_keys($modelData),
51
                )
52
            ),
53
        )';
54

55
    /**
56
     * @inheritdoc
57
     *
58
     * @throws FileSystemException
59
     * @throws SchemaException
60
     * @throws SyntaxErrorException
61
     * @throws UndefinedSymbolException
62
     */
63
    public function process(string $propertyName, JsonSchema $propertySchema): PropertyInterface
2,085✔
64
    {
65
        $this->schema
2,085✔
66
            ->getSchemaDictionary()
2,085✔
67
            ->setUpDefinitionDictionary($this->schemaProcessor, $this->schema);
2,085✔
68

69
        // create a property which is used to gather composed properties validators.
70
        $property = new BaseProperty($propertyName, new PropertyType(static::TYPE), $propertySchema);
2,085✔
71
        $this->generateValidators($property, $propertySchema);
2,085✔
72

73
        $this->addPropertiesToSchema($propertySchema);
2,084✔
74
        $this->transferComposedPropertiesToSchema($property);
2,026✔
75

76
        $this->addPropertyNamesValidator($propertySchema);
2,025✔
77
        $this->addPatternPropertiesValidator($propertySchema);
2,024✔
78
        $this->addAdditionalPropertiesValidator($propertySchema);
2,023✔
79

80
        $this->addMinPropertiesValidator($propertyName, $propertySchema);
2,023✔
81
        $this->addMaxPropertiesValidator($propertyName, $propertySchema);
2,023✔
82

83
        return $property;
2,023✔
84
    }
85

86
    /**
87
     * Add a validator to check all provided property names
88
     *
89
     * @throws SchemaException
90
     * @throws FileSystemException
91
     * @throws SyntaxErrorException
92
     * @throws UndefinedSymbolException
93
     */
94
    protected function addPropertyNamesValidator(JsonSchema $propertySchema): void
2,025✔
95
    {
96
        if (!isset($propertySchema->getJson()['propertyNames'])) {
2,025✔
97
            return;
1,981✔
98
        }
99

100
        $this->schema->addBaseValidator(
44✔
101
            new PropertyNamesValidator(
44✔
102
                $this->schemaProcessor,
44✔
103
                $this->schema,
44✔
104
                $propertySchema->withJson($propertySchema->getJson()['propertyNames']),
44✔
105
            )
44✔
106
        );
44✔
107
    }
108

109
    /**
110
     * Add an object validator to specify constraints for properties which are not defined in the schema
111
     *
112
     * @throws FileSystemException
113
     * @throws SchemaException
114
     * @throws SyntaxErrorException
115
     * @throws UndefinedSymbolException
116
     */
117
    protected function addAdditionalPropertiesValidator(JsonSchema $propertySchema): void
2,023✔
118
    {
119
        $json = $propertySchema->getJson();
2,023✔
120

121
        if (
122
            !isset($json['additionalProperties']) &&
2,023✔
123
            $this->schemaProcessor->getGeneratorConfiguration()->denyAdditionalProperties()
2,023✔
124
        ) {
125
            $json['additionalProperties'] = false;
5✔
126
        }
127

128
        if (!isset($json['additionalProperties']) || $json['additionalProperties'] === true) {
2,023✔
129
            return;
1,945✔
130
        }
131

132
        if (!is_bool($json['additionalProperties'])) {
253✔
133
            $this->schema->addBaseValidator(
88✔
134
                new AdditionalPropertiesValidator(
88✔
135
                    $this->schemaProcessor,
88✔
136
                    $this->schema,
88✔
137
                    $propertySchema,
88✔
138
                )
88✔
139
            );
88✔
140

141
            return;
88✔
142
        }
143

144
        $this->schema->addBaseValidator(
165✔
145
            new NoAdditionalPropertiesValidator(
165✔
146
                new Property($this->schema->getClassName(), null, $propertySchema),
165✔
147
                $json,
165✔
148
            )
165✔
149
        );
165✔
150
    }
151

152
    /**
153
     * @throws SchemaException
154
     */
155
    protected function addPatternPropertiesValidator(JsonSchema $propertySchema): void
2,024✔
156
    {
157
        $json = $propertySchema->getJson();
2,024✔
158

159
        if (!isset($json['patternProperties'])) {
2,024✔
160
            return;
1,968✔
161
        }
162

163
        foreach ($json['patternProperties'] as $pattern => $schema) {
60✔
164
            $escapedPattern = addcslashes((string) $pattern, '/');
60✔
165

166
            if (@preg_match("/$escapedPattern/", '') === false) {
60✔
167
                throw new SchemaException(
1✔
168
                    "Invalid pattern '$pattern' for pattern property in file {$propertySchema->getFile()}",
1✔
169
                );
1✔
170
            }
171

172
            $validator = new PatternPropertiesValidator(
59✔
173
                $this->schemaProcessor,
59✔
174
                $this->schema,
59✔
175
                $pattern,
59✔
176
                $propertySchema->withJson($schema),
59✔
177
            );
59✔
178

179
            $this->schema->addBaseValidator($validator);
59✔
180
        }
181
    }
182

183
    /**
184
     * Add an object validator to limit the amount of provided properties
185
     *
186
     * @throws SchemaException
187
     */
188
    protected function addMaxPropertiesValidator(string $propertyName, JsonSchema $propertySchema): void
2,023✔
189
    {
190
        $json = $propertySchema->getJson();
2,023✔
191

192
        if (!isset($json['maxProperties'])) {
2,023✔
193
            return;
1,990✔
194
        }
195

196
        $this->schema->addBaseValidator(
43✔
197
            new PropertyValidator(
43✔
198
                new Property($propertyName, null, $propertySchema),
43✔
199
                sprintf(
43✔
200
                    '%s > %d',
43✔
201
                    self::COUNT_PROPERTIES,
43✔
202
                    $json['maxProperties'],
43✔
203
                ),
43✔
204
                MaxPropertiesException::class,
43✔
205
                [$json['maxProperties']],
43✔
206
            )
43✔
207
        );
43✔
208
    }
209

210
    /**
211
     * Add an object validator to force at least the defined amount of properties to be provided
212
     *
213
     * @throws SchemaException
214
     */
215
    protected function addMinPropertiesValidator(string $propertyName, JsonSchema $propertySchema): void
2,023✔
216
    {
217
        $json = $propertySchema->getJson();
2,023✔
218

219
        if (!isset($json['minProperties'])) {
2,023✔
220
            return;
2,008✔
221
        }
222

223
        $this->schema->addBaseValidator(
25✔
224
            new PropertyValidator(
25✔
225
                new Property($propertyName, null, $propertySchema),
25✔
226
                sprintf(
25✔
227
                    '%s < %d',
25✔
228
                    self::COUNT_PROPERTIES,
25✔
229
                    $json['minProperties'],
25✔
230
                ),
25✔
231
                MinPropertiesException::class,
25✔
232
                [$json['minProperties']],
25✔
233
            )
25✔
234
        );
25✔
235
    }
236

237
    /**
238
     * Add the properties defined in the JSON schema to the current schema model
239
     *
240
     * @throws SchemaException
241
     */
242
    protected function addPropertiesToSchema(JsonSchema $propertySchema): void
2,084✔
243
    {
244
        $json = $propertySchema->getJson();
2,084✔
245

246
        $propertyFactory = new PropertyFactory(new PropertyProcessorFactory());
2,084✔
247
        $propertyMetaDataCollection = new PropertyMetaDataCollection(
2,084✔
248
            $json['required'] ?? [],
2,084✔
249
            $json['dependencies'] ?? [],
2,084✔
250
        );
2,084✔
251

252
        $json['properties'] ??= [];
2,084✔
253
        // setup empty properties for required properties which aren't defined in the properties section of the schema
254
        $json['properties'] += array_fill_keys(
2,084✔
255
            array_diff($json['required'] ?? [], array_keys($json['properties'])),
2,084✔
256
            [],
2,084✔
257
        );
2,084✔
258

259
        foreach ($json['properties'] as $propertyName => $propertyStructure) {
2,084✔
260
            if ($propertyStructure === false) {
2,017✔
261
                if (in_array($propertyName, $json['required'] ?? [], true)) {
13✔
262
                    throw new SchemaException(
1✔
263
                        sprintf(
1✔
264
                            "Property '%s' is denied (schema false) but also listed as required in file %s",
1✔
265
                            $propertyName,
1✔
266
                            $propertySchema->getFile(),
1✔
267
                        ),
1✔
268
                    );
1✔
269
                }
270

271
                $this->schema->addBaseValidator(
12✔
272
                    new PropertyValidator(
12✔
273
                        new Property($propertyName, null, $propertySchema->withJson([])),
12✔
274
                        "array_key_exists('" . addslashes($propertyName) . "', \$modelData)",
12✔
275
                        DeniedPropertyException::class,
12✔
276
                    )
12✔
277
                );
12✔
278
                continue;
12✔
279
            }
280

281
            $this->schema->addProperty(
2,016✔
282
                $propertyFactory->create(
2,016✔
283
                    $propertyMetaDataCollection,
2,016✔
284
                    $this->schemaProcessor,
2,016✔
285
                    $this->schema,
2,016✔
286
                    (string) $propertyName,
2,016✔
287
                    $propertySchema->withJson($propertyStructure),
2,016✔
288
                )
2,016✔
289
            );
2,016✔
290
        }
291
    }
292

293
    /**
294
     * Transfer properties of composed properties to the current schema to offer a complete model including all
295
     * composed properties.
296
     *
297
     * @throws SchemaException
298
     */
299
    protected function transferComposedPropertiesToSchema(PropertyInterface $property): void
2,026✔
300
    {
301
        foreach ($property->getValidators() as $validator) {
2,026✔
302
            $validator = $validator->getValidator();
2,026✔
303

304
            if (!is_a($validator, AbstractComposedPropertyValidator::class)) {
2,026✔
305
                continue;
2,026✔
306
            }
307

308
            // If the transferred validator of the composed property is also a composed property strip the nested
309
            // composition validations from the added validator. The nested composition will be validated in the object
310
            // generated for the nested composition which will be executed via an instantiation. Consequently, the
311
            // validation must not be executed in the outer composition.
312
            $this->schema->addBaseValidator(
289✔
313
                ($validator instanceof ComposedPropertyValidator)
289✔
314
                    ? $validator->withoutNestedCompositionValidation()
265✔
315
                    : $validator,
289✔
316
            );
289✔
317

318
            if (!is_a($validator->getCompositionProcessor(), ComposedPropertiesInterface::class, true)) {
289✔
319
                continue;
×
320
            }
321

322
            $branchesForValidator = $validator instanceof ConditionalPropertyValidator
289✔
323
                ? $validator->getConditionBranches()
38✔
324
                : $validator->getComposedProperties();
265✔
325

326
            $totalBranches = count($branchesForValidator);
289✔
327
            $resolvedPropertiesCallbacks = 0;
289✔
328
            $seenBranchPropertyNames = [];
289✔
329

330
            foreach ($validator->getComposedProperties() as $composedProperty) {
289✔
331
                $composedProperty->onResolve(function () use (
289✔
332
                    $composedProperty,
289✔
333
                    $property,
289✔
334
                    $validator,
289✔
335
                    $branchesForValidator,
289✔
336
                    $totalBranches,
289✔
337
                    &$resolvedPropertiesCallbacks,
289✔
338
                    &$seenBranchPropertyNames,
289✔
339
                ): void {
289✔
340
                    if (!$composedProperty->getNestedSchema()) {
289✔
341
                        throw new SchemaException(
1✔
342
                            sprintf(
1✔
343
                                "No nested schema for composed property %s in file %s found",
1✔
344
                                $property->getName(),
1✔
345
                                $property->getJsonSchema()->getFile(),
1✔
346
                            )
1✔
347
                        );
1✔
348
                    }
349

350
                    $isBranchForValidator = in_array($composedProperty, $branchesForValidator, true);
288✔
351

352
                    $composedProperty->getNestedSchema()->onAllPropertiesResolved(
288✔
353
                        function () use (
288✔
354
                            $composedProperty,
288✔
355
                            $validator,
288✔
356
                            $isBranchForValidator,
288✔
357
                            $totalBranches,
288✔
358
                            &$resolvedPropertiesCallbacks,
288✔
359
                            &$seenBranchPropertyNames,
288✔
360
                        ): void {
288✔
361
                            foreach ($composedProperty->getNestedSchema()->getProperties() as $branchProperty) {
288✔
362
                                $this->schema->addProperty(
286✔
363
                                    $this->cloneTransferredProperty(
286✔
364
                                        $branchProperty,
286✔
365
                                        $composedProperty,
286✔
366
                                        $validator,
286✔
367
                                    ),
286✔
368
                                    $validator->getCompositionProcessor(),
286✔
369
                                );
286✔
370

371
                                $composedProperty->appendAffectedObjectProperty($branchProperty);
284✔
372
                                $seenBranchPropertyNames[$branchProperty->getName()] = true;
284✔
373
                            }
374

375
                            if ($isBranchForValidator && ++$resolvedPropertiesCallbacks === $totalBranches) {
286✔
376
                                foreach (array_keys($seenBranchPropertyNames) as $branchPropertyName) {
285✔
377
                                    $this->schema->getPropertyMerger()->checkForTotalConflict(
283✔
378
                                        $branchPropertyName,
283✔
379
                                        $totalBranches,
283✔
380
                                    );
283✔
381
                                }
382
                            }
383
                        },
288✔
384
                    );
288✔
385
                });
289✔
386
            }
387
        }
388
    }
389

390
    /**
391
     * Clone the provided property to transfer it to a schema. Sets the nullability and required flag based on the
392
     * composition processor used to set up the composition. Widens the type to mixed when the property is exclusive
393
     * to one anyOf/oneOf branch and at least one other branch allows additional properties, preventing TypeError when
394
     * raw input values of an arbitrary type are stored in the property slot.
395
     */
396
    private function cloneTransferredProperty(
286✔
397
        PropertyInterface $property,
398
        CompositionPropertyDecorator $sourceBranch,
399
        AbstractComposedPropertyValidator $validator,
400
    ): PropertyInterface {
401
        $compositionProcessor = $validator->getCompositionProcessor();
286✔
402

403
        $transferredProperty = (clone $property)
286✔
404
            ->filterValidators(static fn(Validator $validator): bool =>
286✔
405
                is_a($validator->getValidator(), PropertyTemplateValidator::class));
286✔
406

407
        if (!is_a($compositionProcessor, AllOfProcessor::class, true)) {
286✔
408
            $transferredProperty->setRequired(false);
188✔
409

410
            if ($transferredProperty->getType()) {
188✔
411
                $transferredProperty->setType(
167✔
412
                    new PropertyType($transferredProperty->getType()->getNames(), true),
167✔
413
                    new PropertyType($transferredProperty->getType(true)->getNames(), true),
167✔
414
                );
167✔
415
            }
416

417
            $wideningBranches = $validator instanceof ConditionalPropertyValidator
188✔
418
                ? $validator->getConditionBranches()
38✔
419
                : $validator->getComposedProperties();
150✔
420

421
            if ($this->exclusiveBranchPropertyNeedsWidening($property->getName(), $sourceBranch, $wideningBranches)) {
188✔
422
                $transferredProperty->setType(null, null, reset: true);
151✔
423
            }
424
        }
425

426
        return $transferredProperty;
286✔
427
    }
428

429
    /**
430
     * Returns true when the property named $propertyName is exclusive to $sourceBranch and at least
431
     * one other anyOf/oneOf branch allows additional properties (i.e. does NOT declare
432
     * additionalProperties: false). In that case the property slot can receive an arbitrarily-typed
433
     * raw input value from a non-matching branch, so the native type hint must be removed.
434
     *
435
     * Returns false when the property appears in another branch too (Schema::addProperty handles
436
     * that via type merging) or when all other branches have additionalProperties: false (making
437
     * the property mutually exclusive with the other branches' properties).
438
     *
439
     * @param CompositionPropertyDecorator[] $allBranches
440
     */
441
    private function exclusiveBranchPropertyNeedsWidening(
188✔
442
        string $propertyName,
443
        CompositionPropertyDecorator $sourceBranch,
444
        array $allBranches,
445
    ): bool {
446
        // Pass 1: if any other branch defines the same property, Phase 6 handles the type
447
        // merging via Schema::addProperty — widening to mixed is not needed here.
448
        foreach ($allBranches as $branch) {
188✔
449
            if ($branch === $sourceBranch) {
188✔
450
                continue;
188✔
451
            }
452

453
            $branchPropertyNames = $branch->getNestedSchema()
187✔
454
                ? array_map(
187✔
455
                    static fn(PropertyInterface $p): string => $p->getName(),
187✔
456
                    $branch->getNestedSchema()->getProperties(),
187✔
457
                )
187✔
NEW
458
                : [];
×
459

460
            if (in_array($propertyName, $branchPropertyNames, true)) {
187✔
461
                return false;
69✔
462
            }
463
        }
464

465
        // Pass 2: the property is exclusive to $sourceBranch. Widening is needed when at
466
        // least one other branch allows additional properties — an arbitrary input value can
467
        // then land in this slot when that branch is the one that matched.
468
        foreach ($allBranches as $branch) {
153✔
469
            if ($branch === $sourceBranch) {
153✔
470
                continue;
120✔
471
            }
472

473
            if (($branch->getBranchSchema()->getJson()['additionalProperties'] ?? true) !== false) {
152✔
474
                return true;
151✔
475
            }
476
        }
477

478
        // All other branches have additionalProperties:false — no arbitrary value can arrive.
479
        return false;
8✔
480
    }
481
}
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