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

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

07 Jul 2026 09:31AM UTC coverage: 98.076%. First build
28856540538

Pull #157

github

wol-soft
Warn and skip on unevaluatedProperties dead cells + edge-case tests

UnevaluatedPropertiesValidatorFactory now consolidates all four
sibling shapes that leave the unevaluatedProperties bucket
permanently empty into a single deadCodeReason() helper:

- additionalProperties: true — every extra flows to the model but
  the accumulator does not credit it, so the validator would still
  fire and defeat the intent of `additionalProperties: true`.
- additionalProperties: {schema} — every extra is claimed and
  validated by additionalProperties; the unevaluated set is empty.
- additionalProperties: false — every extra is rejected before the
  post-composition phase, so the unevaluated validator never runs.
- denyAdditionalProperties() generator flag with additionalProperties
  absent — synthesises the same false shape at configuration time.

Each cell emits a distinct warning via the existing echo channel
so build-output greps can identify the specific dead shape, and
skips validator emission entirely.

Tests added:

- Object-side dead-code data provider covering all four shapes plus
  the denyAdditionalProperties() variant. Rows pass the
  GeneratorConfiguration directly (setOutputEnabled(true) on the
  base, chained with setDenyAdditionalProperties(true) on the deny
  row) so the test signature drops the closure/fallback boilerplate.
- testAdditionalFalseRejectsExtrasEvenWhenUnevaluatedSchemaWouldAccept
  proves the factory suppressed the validator: an extra that would
  satisfy the unevaluated integer schema is still rejected because
  additionalProperties: false runs first.
- testContradictoryInnerSchemaThrowsSchemaExceptionPointingAtFile
  pins that an unevaluatedProperties schema with contradictory allOf
  types surfaces via the existing "conflicting types" SchemaException
  path, with the file identifier preserved.
- testPropertyNamesRejectionPrecedesUnevaluated data-provider variant
  covering both direct-exception and error-collection modes. The
  re... (continued)
Pull Request #157: Unevaluated properties

712 of 774 new or added lines in 38 files covered. (91.99%)

7237 of 7379 relevant lines covered (98.08%)

563.49 hits per line

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

93.24
/src/SchemaProcessor/PostProcessor/Internal/CompositionValidationPostProcessor.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\SchemaProcessor\PostProcessor\Internal;
6

7
use PHPModelGenerator\Model\GeneratorConfiguration;
8
use PHPModelGenerator\Model\Property\Property;
9
use PHPModelGenerator\Model\Property\PropertyInterface;
10
use PHPModelGenerator\Model\Property\PropertyType;
11
use PHPModelGenerator\Model\Schema;
12
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
13
use PHPModelGenerator\Model\Validator\AbstractComposedPropertyValidator;
14
use PHPModelGenerator\SchemaProcessor\Hook\SetterBeforeValidationHookInterface;
15
use PHPModelGenerator\SchemaProcessor\PostProcessor\PostProcessor;
16
use PHPModelGenerator\SchemaProcessor\PostProcessor\RenderedMethod;
17
use PHPModelGenerator\Utils\RenderHelper;
18

19
/**
20
 * Class CompositionValidationPostProcessor
21
 *
22
 * The CompositionValidationPostProcessor adds methods to models which require composition validations on object level
23
 * to validate the compositions.
24
 *
25
 * Additionally extends setter methods to also validate compositions if the updated property is part of a composition
26
 *
27
 * @package PHPModelGenerator\SchemaProcessor\PostProcessor\Internal
28
 */
29
class CompositionValidationPostProcessor extends PostProcessor
30
{
31
    public function process(Schema $schema, GeneratorConfiguration $generatorConfiguration): void
2,583✔
32
    {
33
        $compositionValidatorKeys = $schema->getCompositionValidatorKeys();
2,583✔
34

35
        if (empty($compositionValidatorKeys)) {
2,583✔
36
            return;
2,581✔
37
        }
38

39
        $validatorPropertyMap = $this->generateValidatorPropertyMap($schema);
436✔
40

41
        $this->addValidationMethods($schema, $generatorConfiguration, $compositionValidatorKeys);
436✔
42

43
        // if the generator is immutable no validation on value updates are required
44
        if ($generatorConfiguration->isImmutable() || empty($validatorPropertyMap)) {
436✔
45
            return;
372✔
46
        }
47

48
        $this->addValidationCallsToSetterMethods($schema, $validatorPropertyMap);
64✔
49
    }
50

51
    /**
52
     * Set up a map containing the properties and the corresponding composition validators which must be checked when
53
     * the property is updated
54
     */
55
    private function generateValidatorPropertyMap(Schema $schema): array
436✔
56
    {
57
        $validatorPropertyMap = [];
436✔
58

59
        // get all base validators which are composed value validators and set up a map of affected object properties
60
        foreach ($schema->getBaseValidators() as $validatorIndex => $validator) {
436✔
61
            if (!is_a($validator, AbstractComposedPropertyValidator::class)) {
436✔
62
                continue;
29✔
63
            }
64

65
            foreach ($validator->getComposedProperties() as $composedProperty) {
436✔
66
                if ($composedProperty->getNestedSchema() === null) {
434✔
67
                    // Harvest inline branch declared property names so setter-side revalidation
68
                    // fires when those properties change. Without this, updating a property that
69
                    // only an inline branch's `properties` declares would leave _compositionEvaluations
70
                    // stale.
NEW
71
                    foreach ($composedProperty->getBranchDeclaredPropertyNames() as $propertyName) {
×
NEW
72
                        if (!isset($validatorPropertyMap[$propertyName])) {
×
NEW
73
                            $validatorPropertyMap[$propertyName] = [];
×
74
                        }
NEW
75
                        $validatorPropertyMap[$propertyName][] = $validatorIndex;
×
76
                    }
77
                    continue;
×
78
                }
79

80
                foreach ($composedProperty->getNestedSchema()->getProperties() as $property) {
434✔
81
                    if (!isset($validatorPropertyMap[$property->getName()])) {
431✔
82
                        $validatorPropertyMap[$property->getName()] = [];
431✔
83
                    }
84

85
                    $validatorPropertyMap[$property->getName()][] = $validatorIndex;
431✔
86
                }
87
            }
88
        }
89

90
        if (!empty($validatorPropertyMap)) {
436✔
91
            $schema->addProperty(
431✔
92
                (new Property(
431✔
93
                    'propertyValidationState',
431✔
94
                    new PropertyType('array'),
431✔
95
                    new JsonSchema(__FILE__, []),
431✔
96
                    'Track the internal validation state of composed validations',
431✔
97
                ))
431✔
98
                    ->setInternal(true)
431✔
99
                    ->setDefaultValue(
431✔
100
                        array_fill_keys(
431✔
101
                            array_unique(
431✔
102
                                array_merge(...array_values($validatorPropertyMap)),
431✔
103
                            ),
431✔
104
                            [],
431✔
105
                        )
431✔
106
                    ),
431✔
107
            );
431✔
108
        }
109

110
        return $validatorPropertyMap;
436✔
111
    }
112

113
    /**
114
     * @param int[] $compositionValidatorKeys
115
     */
116
    private function addValidationMethods(
436✔
117
        Schema $schema,
118
        GeneratorConfiguration $generatorConfiguration,
119
        array $compositionValidatorKeys,
120
    ): void {
121
        foreach ($compositionValidatorKeys as $validatorIndex) {
436✔
122
            /** @var AbstractComposedPropertyValidator $compositionValidator */
123
            $compositionValidator = $schema->getBaseValidators()[$validatorIndex];
436✔
124

125
            $compositionValidator->setScope($schema);
436✔
126

127
            $schema->addMethod(
436✔
128
                "_validateComposition_$validatorIndex",
436✔
129
                new RenderedMethod(
436✔
130
                    $schema,
436✔
131
                    $generatorConfiguration,
436✔
132
                    'CompositionValidation.phptpl',
436✔
133
                    [
436✔
134
                        'validator' => $compositionValidator,
436✔
135
                        'schema' => $schema,
436✔
136
                        'index' => $validatorIndex,
436✔
137
                        'viewHelper' => new RenderHelper($generatorConfiguration),
436✔
138
                    ],
436✔
139
                )
436✔
140
            );
436✔
141
        }
142
    }
143

144
    /**
145
     * Add internal calls to validation methods to the setters which are part of a composition validation. The
146
     * validation methods will validate the state of all compositions when the value is updated.
147
     */
148
    private function addValidationCallsToSetterMethods(Schema $schema, array $validatorPropertyMap): void
64✔
149
    {
150
        $schema->addSchemaHook(new class ($validatorPropertyMap) implements SetterBeforeValidationHookInterface {
64✔
151
            public function __construct(protected array $validatorPropertyMap)
152
            {}
64✔
153

154
            public function getCode(PropertyInterface $property, bool $batchUpdate = false): string
155
            {
156
                return join(
64✔
157
                    "\n",
64✔
158
                    array_map(
64✔
159
                        static fn(int $validatorIndex): string =>
64✔
160
                            sprintf('$this->_validateComposition_%s($modelData);', $validatorIndex),
64✔
161
                        array_unique($this->validatorPropertyMap[$property->getName()] ?? []),
64✔
162
                    )
64✔
163
                );
64✔
164
            }
165
        });
64✔
166
    }
167
}
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