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

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

24 Mar 2026 10:59AM UTC coverage: 98.525% (-0.06%) from 98.58%
23486015113

Pull #121

github

Enno Woortmann
Add composition required promotion and fix allOf untyped-branch type erasure

When a property is guaranteed present by composition constraints, promote
it to non-nullable in the generated type hint (nullable=false) without
changing isRequired(), so template short-circuit behaviour is preserved.

Promotion rules (via new CompositionRequiredPromotionPostProcessor):
- allOf: required in any branch → promote (all branches hold simultaneously)
- anyOf/oneOf: required in every branch → promote (matching branch always provides it)
- if/then/else: required in both then and else → promote (one always applies)

Also fixes a bug in PropertyMerger where a truly-untyped allOf branch
(no 'type' keyword) incorrectly wiped the type from a typed sibling
branch. For allOf, an untyped branch adds no type constraint.
Pull Request #121: Add composition required promotion and fix allOf untyped-branch type erasure

53 of 56 new or added lines in 3 files covered. (94.64%)

3941 of 4000 relevant lines covered (98.53%)

571.29 hits per line

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

94.0
/src/SchemaProcessor/PostProcessor/Internal/CompositionRequiredPromotionPostProcessor.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\PropertyInterface;
9
use PHPModelGenerator\Model\Property\PropertyType;
10
use PHPModelGenerator\Model\Schema;
11
use PHPModelGenerator\Model\Validator\AbstractComposedPropertyValidator;
12
use PHPModelGenerator\Model\Validator\ComposedPropertyValidator;
13
use PHPModelGenerator\Model\Validator\ConditionalPropertyValidator;
14
use PHPModelGenerator\Model\Validator\PropertyValidatorInterface;
15
use PHPModelGenerator\PropertyProcessor\ComposedValue\AllOfProcessor;
16
use PHPModelGenerator\SchemaProcessor\PostProcessor\PostProcessor;
17

18
/**
19
 * Promotes properties transferred from composition branches to non-nullable when the composition
20
 * structure guarantees the property is always present in a valid object.
21
 *
22
 * Rules:
23
 *   allOf  — property is required in any branch (all branches apply simultaneously)
24
 *   anyOf  — property is required in every branch (at least one always matches)
25
 *   oneOf  — property is required in every branch (exactly one always matches)
26
 *   if/then/else — property is required in both then and else (one always applies)
27
 *
28
 * The property's isRequired() flag is intentionally left false so the template short-circuit
29
 * (which exits early when the key is absent) continues to work correctly during construction.
30
 * Only the nullable flag on the PropertyType is changed to false.
31
 */
32
class CompositionRequiredPromotionPostProcessor extends PostProcessor
33
{
34
    public function process(Schema $schema, GeneratorConfiguration $generatorConfiguration): void
2,079✔
35
    {
36
        foreach ($schema->getBaseValidators() as $validator) {
2,079✔
37
            if (!($validator instanceof AbstractComposedPropertyValidator)) {
670✔
38
                continue;
381✔
39
            }
40

41
            foreach ($this->collectPromotablePropertyNames($validator) as $propertyName) {
341✔
42
                $this->promoteProperty($schema, $propertyName);
98✔
43
            }
44
        }
45
    }
46

47
    /**
48
     * Returns the names of all properties that are guaranteed to be present by the given validator.
49
     *
50
     * @return string[]
51
     */
52
    private function collectPromotablePropertyNames(AbstractComposedPropertyValidator $validator): array
341✔
53
    {
54
        if ($validator instanceof ConditionalPropertyValidator) {
341✔
55
            return $this->collectFromConditional($validator);
47✔
56
        }
57

58
        return $this->collectFromComposed($validator);
308✔
59
    }
60

61
    /**
62
     * For if/then/else: a property is guaranteed only when both then and else are present and
63
     * both require the property.
64
     *
65
     * @return string[]
66
     */
67
    private function collectFromConditional(ConditionalPropertyValidator $validator): array
47✔
68
    {
69
        $branches = $validator->getConditionBranches();
47✔
70

71
        if (count($branches) < 2) {
47✔
72
            return [];
8✔
73
        }
74

75
        $requiredPerBranch = array_map(
39✔
76
            static fn($branch): array =>
39✔
77
                $branch->getNestedSchema()?->getJsonSchema()->getJson()['required'] ?? [],
39✔
78
            $branches,
39✔
79
        );
39✔
80

81
        return array_values(array_intersect(...$requiredPerBranch));
39✔
82
    }
83

84
    /**
85
     * For allOf: a property is guaranteed when it is required in any branch.
86
     * For anyOf/oneOf: a property is guaranteed when it is required in every branch.
87
     *
88
     * @return string[]
89
     */
90
    private function collectFromComposed(ComposedPropertyValidator $validator): array
308✔
91
    {
92
        $branches = $validator->getComposedProperties();
308✔
93

94
        if (empty($branches)) {
308✔
NEW
95
            return [];
×
96
        }
97

98
        $requiredPerBranch = array_map(
308✔
99
            static fn($branch): array =>
308✔
100
                $branch->getNestedSchema()?->getJsonSchema()->getJson()['required'] ?? [],
308✔
101
            $branches,
308✔
102
        );
308✔
103

104
        if (is_a($validator->getCompositionProcessor(), AllOfProcessor::class, true)) {
308✔
105
            return array_values(array_unique(array_merge(...$requiredPerBranch)));
121✔
106
        }
107

108
        return array_values(array_intersect(...$requiredPerBranch));
188✔
109
    }
110

111
    /**
112
     * Strips the nullable flag from the property's type if the property is not already required
113
     * at root level and has a type that can be promoted.
114
     */
115
    private function promoteProperty(Schema $schema, string $propertyName): void
98✔
116
    {
117
        $property = null;
98✔
118

119
        foreach ($schema->getProperties() as $candidate) {
98✔
120
            if ($candidate->getName() === $propertyName) {
98✔
121
                $property = $candidate;
98✔
122
                break;
98✔
123
            }
124
        }
125

126
        if ($property === null) {
98✔
NEW
127
            return;
×
128
        }
129

130
        if ($property->isRequired()) {
98✔
131
            return;
64✔
132
        }
133

134
        $type = $property->getType();
34✔
135
        $outputType = $property->getType(true);
34✔
136

137
        if ($type === null) {
34✔
NEW
138
            return;
×
139
        }
140

141
        $property->setType(
34✔
142
            new PropertyType($type->getNames(), false),
34✔
143
            new PropertyType($outputType->getNames(), false),
34✔
144
        );
34✔
145
    }
146
}
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