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

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

17 Apr 2026 02:06PM UTC coverage: 98.486% (-0.2%) from 98.654%
24569325082

Pull #125

github

wol-soft
additional test cases
Pull Request #125: attributes

1812 of 1837 new or added lines in 80 files covered. (98.64%)

1 existing line in 1 file now uncovered.

4554 of 4624 relevant lines covered (98.49%)

610.41 hits per line

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

98.89
/src/Model/Validator/Factory/Composition/AbstractCompositionValidatorFactory.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\Model\Validator\Factory\Composition;
6

7
use PHPModelGenerator\Exception\SchemaException;
8
use PHPModelGenerator\Model\Property\BaseProperty;
9
use PHPModelGenerator\Model\Property\CompositionPropertyDecorator;
10
use PHPModelGenerator\Model\Property\PropertyInterface;
11
use PHPModelGenerator\Model\Property\PropertyType;
12
use PHPModelGenerator\Model\Schema;
13
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
14
use PHPModelGenerator\Model\Validator;
15
use PHPModelGenerator\Model\Validator\ComposedPropertyValidator;
16
use PHPModelGenerator\Model\Validator\Factory\AbstractValidatorFactory;
17
use PHPModelGenerator\Model\Validator\RequiredPropertyValidator;
18
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\ClearTypeHintDecorator;
19
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\CompositionTypeHintDecorator;
20
use PHPModelGenerator\PropertyProcessor\PropertyFactory;
21
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
22

23
abstract class AbstractCompositionValidatorFactory extends AbstractValidatorFactory
24
{
25
    /**
26
     * Emit a warning when the composition array for the current keyword is empty.
27
     */
28
    protected function warnIfEmpty(
609✔
29
        SchemaProcessor $schemaProcessor,
30
        PropertyInterface $property,
31
        JsonSchema $propertySchema,
32
    ): void {
33
        if (
34
            empty($propertySchema->getJson()[$this->key]) &&
609✔
35
            $schemaProcessor->getGeneratorConfiguration()->isOutputEnabled()
609✔
36
        ) {
37
            // @codeCoverageIgnoreStart
38
            echo "Warning: empty composition for {$property->getName()} may lead to unexpected results\n";
39
            // @codeCoverageIgnoreEnd
40
        }
41
    }
42

43
    /**
44
     * Returns true when composition processing should be skipped for this property.
45
     *
46
     * For non-root object-typed properties, composition keywords are processed inside
47
     * the nested schema by processSchema (with the type=base path). Adding a composition
48
     * validator at the parent level would duplicate validation and inject a _Merged_ type
49
     * hint that overrides the correct nested-class type.
50
     */
51
    protected function shouldSkip(PropertyInterface $property, JsonSchema $propertySchema): bool
749✔
52
    {
53
        return !($property instanceof BaseProperty)
749✔
54
            && ($propertySchema->getJson()['type'] ?? '') === 'object';
749✔
55
    }
56

57
    /**
58
     * Build composition sub-properties for the current keyword's branches.
59
     *
60
     * @param bool $merged Whether to suppress CompositionTypeHintDecorators for object branches.
61
     *
62
     * @return CompositionPropertyDecorator[]
63
     *
64
     * @throws SchemaException
65
     */
66
    protected function getCompositionProperties(
703✔
67
        SchemaProcessor $schemaProcessor,
68
        Schema $schema,
69
        PropertyInterface $property,
70
        JsonSchema $propertySchema,
71
        bool $merged,
72
    ): array {
73
        $propertyFactory = new PropertyFactory();
703✔
74
        $compositionProperties = [];
703✔
75
        $json = $propertySchema->getJson()['propertySchema']->getJson();
703✔
76

77
        $property->addTypeHintDecorator(new ClearTypeHintDecorator());
703✔
78

79
        foreach ($json[$this->key] as $index => $compositionElement) {
703✔
80
            $compositionSchema = $propertySchema->getJson()['propertySchema']->navigate("$this->key/$index");
684✔
81

82
            $compositionProperty = new CompositionPropertyDecorator(
684✔
83
                $property->getName(),
684✔
84
                $compositionSchema,
684✔
85
                $propertyFactory->create(
684✔
86
                    $schemaProcessor,
684✔
87
                    $schema,
684✔
88
                    $property->getName(),
684✔
89
                    $compositionSchema,
684✔
90
                    $property->isRequired(),
684✔
91
                ),
684✔
92
            );
684✔
93

94
            $compositionProperty->onResolve(function () use ($compositionProperty, $property, $merged): void {
684✔
95
                $compositionProperty->filterValidators(
684✔
96
                    static fn(Validator $validator): bool =>
684✔
97
                        !is_a($validator->getValidator(), RequiredPropertyValidator::class) &&
684✔
98
                        !is_a($validator->getValidator(), ComposedPropertyValidator::class),
684✔
99
                );
684✔
100

101
                if (!($merged && $compositionProperty->getNestedSchema())) {
684✔
102
                    $property->addTypeHintDecorator(new CompositionTypeHintDecorator($compositionProperty));
393✔
103
                }
104
            });
684✔
105

106
            $compositionProperties[] = $compositionProperty;
684✔
107
        }
108

109
        return $compositionProperties;
703✔
110
    }
111

112
    /**
113
     * Inherit a parent-level type into composition branches that declare no type.
114
     */
115
    protected function inheritPropertyType(JsonSchema $propertySchema): JsonSchema
748✔
116
    {
117
        $json = $propertySchema->getJson();
748✔
118

119
        if (!isset($json['type'])) {
748✔
120
            return $propertySchema;
323✔
121
        }
122

123
        switch ($this->key) {
446✔
124
            case 'not':
446✔
125
                if (!isset($json[$this->key]['type'])) {
26✔
126
                    $json[$this->key]['type'] = $json['type'];
26✔
127
                }
128
                break;
26✔
129
            case 'if':
420✔
130
                return $this->inheritIfPropertyType($propertySchema->withJson($json));
63✔
131
            default:
132
                foreach ($json[$this->key] as &$composedElement) {
375✔
133
                    if (!isset($composedElement['type'])) {
374✔
134
                        $composedElement['type'] = $json['type'];
163✔
135
                    }
136
                }
137
        }
138

139
        return $propertySchema->withJson($json);
401✔
140
    }
141

142
    /**
143
     * Inherit the parent type into all branches of an if/then/else composition.
144
     */
145
    protected function inheritIfPropertyType(JsonSchema $propertySchema): JsonSchema
63✔
146
    {
147
        $json = $propertySchema->getJson();
63✔
148

149
        foreach (['if', 'then', 'else'] as $keyword) {
63✔
150
            if (!isset($json[$keyword])) {
63✔
151
                continue;
14✔
152
            }
153

154
            if (!isset($json[$keyword]['type'])) {
63✔
155
                $json[$keyword]['type'] = $json['type'];
63✔
156
            }
157
        }
158

159
        return $propertySchema->withJson($json);
63✔
160
    }
161

162
    /**
163
     * After all composition branches resolve, attempt to widen the parent property's type
164
     * to cover all branch types. Skips for branches with nested schemas.
165
     *
166
     * @param bool $isAllOf Whether allOf semantics apply (affects nullable detection).
167
     * @param CompositionPropertyDecorator[] $compositionProperties
168
     */
169
    protected function transferPropertyType(
590✔
170
        PropertyInterface $property,
171
        array $compositionProperties,
172
        bool $isAllOf,
173
    ): void {
174
        foreach ($compositionProperties as $compositionProperty) {
590✔
175
            if ($compositionProperty->getNestedSchema() !== null) {
590✔
176
                return;
462✔
177
            }
178
        }
179

180
        $allNames = array_merge(...array_map(
129✔
181
            static fn(CompositionPropertyDecorator $p): array =>
129✔
182
                $p->getType() ? $p->getType()->getNames() : [],
129✔
183
            $compositionProperties,
129✔
184
        ));
129✔
185

186
        $hasBranchWithNoType = array_filter(
129✔
187
            $compositionProperties,
129✔
188
            static fn(CompositionPropertyDecorator $p): bool => $p->getType() === null,
129✔
189
        ) !== [];
129✔
190

191
        $hasBranchWithRequiredProperty = array_filter(
129✔
192
            $compositionProperties,
129✔
193
            static fn(CompositionPropertyDecorator $p): bool => $p->isRequired(),
129✔
194
        ) !== [];
129✔
195

196
        $hasBranchWithOptionalProperty = $isAllOf
129✔
197
            ? !$hasBranchWithRequiredProperty
25✔
198
            : array_filter(
104✔
199
                $compositionProperties,
104✔
200
                static fn(CompositionPropertyDecorator $p): bool => !$p->isRequired(),
104✔
201
            ) !== [];
104✔
202

203
        $hasNull = in_array('null', $allNames, true);
129✔
204
        $nonNullNames = array_values(array_filter(
129✔
205
            array_unique($allNames),
129✔
206
            fn(string $t): bool => $t !== 'null',
129✔
207
        ));
129✔
208

209
        if (!$nonNullNames) {
129✔
NEW
210
            return;
×
211
        }
212

213
        $nullable = ($hasNull || $hasBranchWithNoType || $hasBranchWithOptionalProperty) ? true : null;
129✔
214

215
        $property->setType(new PropertyType($nonNullNames, $nullable));
129✔
216
    }
217
}
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