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

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

24 Mar 2026 12:42PM UTC coverage: 98.575% (-0.005%) from 98.58%
23489999208

Pull #121

github

Enno Woortmann
Strengthen ComposedRequiredPromotion test coverage

- Assert specific composition exception types (AllOfException,
  AnyOfException, OneOfException, ConditionalException) in CanBeOmitted
  tests instead of the generic Exception base class
- Assert assertInstanceOf on the collected error in all three
  implicit-null error-collection tests; add allOf and oneOf variants
  of that test (previously only anyOf was covered)
- assertNullableStringProperty/assertNullableIntProperty now
  unconditionally assert null is present: composition-transferred
  non-promoted properties are always nullable regardless of implicitNull,
  and this is now verified in both modes rather than only under
  implicitNull=true
- Add testMultiplePropertiesOnlySomePromoted: two transferred properties
  where only one meets the promotion condition
- Cover three defensive guard paths in the post processor with new
  schemas and tests: AnyOfEmptyComposition (empty branches array),
  AnyOfRequiredOnlyBranches (required name absent from schema properties),
  and AnyOfUntypedPropertyInBranches (getType() returns null, no promotion)
Pull Request #121: Add composition required promotion and fix allOf untyped-branch type erasure

55 of 56 new or added lines in 3 files covered. (98.21%)

3943 of 4000 relevant lines covered (98.58%)

573.62 hits per line

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

98.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,086✔
35
    {
36
        foreach ($schema->getBaseValidators() as $validator) {
2,086✔
37
            if (!($validator instanceof AbstractComposedPropertyValidator)) {
677✔
38
                continue;
382✔
39
            }
40

41
            foreach ($this->collectPromotablePropertyNames($validator) as $propertyName) {
348✔
42
                $this->promoteProperty($schema, $propertyName);
104✔
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
348✔
53
    {
54
        if ($validator instanceof ConditionalPropertyValidator) {
348✔
55
            return $this->collectFromConditional($validator);
47✔
56
        }
57

58
        return $this->collectFromComposed($validator);
315✔
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
315✔
91
    {
92
        $branches = $validator->getComposedProperties();
315✔
93

94
        if (empty($branches)) {
315✔
95
            return [];
1✔
96
        }
97

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

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

108
        return array_values(array_intersect(...$requiredPerBranch));
193✔
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
104✔
116
    {
117
        $property = null;
104✔
118

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

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

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

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

137
        if ($type === null) {
39✔
138
            return;
2✔
139
        }
140

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