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

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

27 May 2026 11:00AM UTC coverage: 98.695% (+0.1%) from 98.554%
26507168724

push

github

web-flow
Merge pull request #135 from wol-soft/booleanSchemas

Boolean schemas

387 of 388 new or added lines in 16 files covered. (99.74%)

1 existing line in 1 file now uncovered.

5144 of 5212 relevant lines covered (98.7%)

633.35 hits per line

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

99.38
/src/Model/Validator/Factory/Composition/IfValidatorFactory.php
1
<?php
2

3
declare(strict_types=1);
4

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

7
use PHPModelGenerator\Exception\Generic\DeniedPropertyException;
8
use PHPModelGenerator\Exception\SchemaException;
9
use PHPModelGenerator\Model\Property\BaseProperty;
10
use PHPModelGenerator\Model\Property\CompositionPropertyDecorator;
11
use PHPModelGenerator\Model\Property\PropertyInterface;
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\ConditionalPropertyValidator;
17
use PHPModelGenerator\Model\Validator\PropertyValidator;
18
use PHPModelGenerator\Model\Validator\RequiredPropertyValidator;
19
use PHPModelGenerator\PropertyProcessor\PropertyFactory;
20
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
21
use PHPModelGenerator\Utils\RenderHelper;
22

23
class IfValidatorFactory
24
    extends AbstractCompositionValidatorFactory
25
    implements ComposedPropertiesValidatorFactoryInterface
26
{
27
    /**
28
     * @throws SchemaException
29
     */
30
    public function modify(
2,382✔
31
        SchemaProcessor $schemaProcessor,
32
        Schema $schema,
33
        PropertyInterface $property,
34
        JsonSchema $propertySchema,
35
    ): void {
36
        if (!isset($propertySchema->getJson()[$this->key]) || $this->shouldSkip($property, $propertySchema)) {
2,382✔
37
            return;
2,380✔
38
        }
39

40
        $json = $propertySchema->getJson();
89✔
41

42
        if (!isset($json['then']) && !isset($json['else'])) {
89✔
43
            throw new SchemaException(
1✔
44
                sprintf(
1✔
45
                    'Incomplete conditional composition for property %s in file %s',
1✔
46
                    $property->getName(),
1✔
47
                    $property->getJsonSchema()->getFile(),
1✔
48
                ),
1✔
49
            );
1✔
50
        }
51

52
        $json = $this->resolveBooleanBranches($json, $property, $schemaProcessor);
88✔
53

54
        if ($json === null) {
86✔
55
            return;
5✔
56
        }
57

58
        $propertySchema = $this->inheritPropertyType($propertySchema->withJson($json));
81✔
59
        $json = $propertySchema->getJson();
81✔
60

61
        $propertyFactory = new PropertyFactory();
81✔
62

63
        $onlyForDefinedValues = !($property instanceof BaseProperty)
81✔
64
            && (!$property->isRequired()
81✔
65
                && $schemaProcessor->getGeneratorConfiguration()->isImplicitNullAllowed());
81✔
66

67
        /** @var array<string, CompositionPropertyDecorator|null> $properties */
68
        $properties = [];
81✔
69

70
        foreach (['if', 'then', 'else'] as $keyword) {
81✔
71
            if (!isset($json[$keyword])) {
81✔
72
                $properties[$keyword] = null;
32✔
73
                continue;
32✔
74
            }
75

76
            if ($json[$keyword] === false) {
81✔
77
                $properties[$keyword] = $this->createAlwaysFailingBranchProperty(
10✔
78
                    $schemaProcessor,
10✔
79
                    $schema,
10✔
80
                    $property,
10✔
81
                    $propertySchema,
10✔
82
                );
10✔
83
                continue;
10✔
84
            }
85

86
            $compositionSchema = $propertySchema->navigate($keyword);
81✔
87

88
            $compositionProperty = new CompositionPropertyDecorator(
81✔
89
                $property->getName(),
81✔
90
                $compositionSchema,
81✔
91
                $propertyFactory->create(
81✔
92
                    $schemaProcessor,
81✔
93
                    $schema,
81✔
94
                    $property->getName(),
81✔
95
                    $compositionSchema,
81✔
96
                    $property->isRequired(),
81✔
97
                ),
81✔
98
            );
81✔
99

100
            $compositionProperty->onResolve(static function () use ($compositionProperty): void {
81✔
101
                $compositionProperty->filterValidators(
81✔
102
                    static fn(Validator $validator): bool =>
81✔
103
                        !is_a($validator->getValidator(), RequiredPropertyValidator::class) &&
81✔
104
                        !is_a($validator->getValidator(), ComposedPropertyValidator::class),
81✔
105
                );
81✔
106
            });
81✔
107

108
            $properties[$keyword] = $compositionProperty;
81✔
109
        }
110

111
        $property->addValidator(
81✔
112
            new ConditionalPropertyValidator(
81✔
113
                $schemaProcessor->getGeneratorConfiguration(),
81✔
114
                $property,
81✔
115
                array_values(array_filter($properties)),
81✔
116
                array_values(array_filter([$properties['then'], $properties['else']])),
81✔
117
                [
81✔
118
                    'ifProperty' => $properties['if'],
81✔
119
                    'thenProperty' => $properties['then'],
81✔
120
                    'elseProperty' => $properties['else'],
81✔
121
                    'schema' => $schema,
81✔
122
                    'generatorConfiguration' => $schemaProcessor->getGeneratorConfiguration(),
81✔
123
                    'viewHelper' => new RenderHelper($schemaProcessor->getGeneratorConfiguration()),
81✔
124
                    'onlyForDefinedValues' => $onlyForDefinedValues,
81✔
125
                ],
81✔
126
            ),
81✔
127
            100,
81✔
128
        );
81✔
129
    }
130

131
    /**
132
     * Create a composition branch that always fails, used for boolean `false` if/then/else branches.
133
     *
134
     * Unlike the allOf/anyOf/oneOf false-branch (which uses array_key_exists to guard absent
135
     * optional properties), here the outer ConditionalComposedItem template's onlyForDefinedValues
136
     * guard already prevents the entire conditional from running for absent properties. So the
137
     * branch itself just needs to always throw regardless of the value.
138
     */
139
    private function createAlwaysFailingBranchProperty(
10✔
140
        SchemaProcessor $schemaProcessor,
141
        Schema $schema,
142
        PropertyInterface $property,
143
        JsonSchema $propertySchema,
144
    ): CompositionPropertyDecorator {
145
        $propertyFactory = new PropertyFactory();
10✔
146
        $branchSchema = $propertySchema->withJson([]);
10✔
147

148
        $branchProperty = new CompositionPropertyDecorator(
10✔
149
            $property->getName(),
10✔
150
            $branchSchema,
10✔
151
            $propertyFactory->create(
10✔
152
                $schemaProcessor,
10✔
153
                $schema,
10✔
154
                $property->getName(),
10✔
155
                $branchSchema,
10✔
156
                $property->isRequired(),
10✔
157
            ),
10✔
158
        );
10✔
159

160
        $branchProperty->onResolve(function () use ($branchProperty): void {
10✔
161
            $branchProperty->filterValidators(
10✔
162
                static fn(Validator $validator): bool =>
10✔
163
                    !is_a($validator->getValidator(), RequiredPropertyValidator::class) &&
10✔
164
                    !is_a($validator->getValidator(), ComposedPropertyValidator::class),
10✔
165
            );
10✔
166
            $branchProperty->addValidator(
10✔
167
                new PropertyValidator(
10✔
168
                    $branchProperty,
10✔
169
                    'true',
10✔
170
                    DeniedPropertyException::class,
10✔
171
                ),
10✔
172
            );
10✔
173
        });
10✔
174

175
        return $branchProperty;
10✔
176
    }
177

178
    /**
179
     * Resolve boolean `if`/`then`/`else` branches into concrete schema arrays or return-null signals.
180
     *
181
     * Returns null when the entire if/then/else imposes no constraint and modify() should return
182
     * early. Returns the (possibly rewritten) $json array otherwise. Always-false branches are left
183
     * as boolean false values in the returned array so the foreach loop in modify() can handle them.
184
     *
185
     * @throws SchemaException
186
     */
187
    private function resolveBooleanBranches(
88✔
188
        array $json,
189
        PropertyInterface $property,
190
        SchemaProcessor $schemaProcessor,
191
    ): ?array {
192
        if (is_bool($json['if'])) {
88✔
193
            if ($json['if'] === false) {
21✔
194
                if (!isset($json['else'])) {
11✔
195
                    if (isset($json['then']) && $schemaProcessor->getGeneratorConfiguration()->isOutputEnabled()) {
1✔
196
                        // @codeCoverageIgnoreStart
197
                        echo "Warning: if: false for property '{$property->getName()}'"
198
                            . " — then branch will never apply (condition never matches); no constraint generated.\n";
199
                        // @codeCoverageIgnoreEnd
200
                    }
201
                    return null;
1✔
202
                }
203

204
                if ($json['else'] === true) {
10✔
205
                    return null;
1✔
206
                }
207

208
                if ($json['else'] === false) {
9✔
209
                    $this->warnIfAlwaysFalse(
5✔
210
                        $schemaProcessor,
5✔
211
                        $property,
5✔
212
                        'if: false with else: false means the composition is always unsatisfiable',
5✔
213
                    );
5✔
214
                    // Rewrite as if: {} (always passes), then: false (always fails).
215
                    // The false then-branch is handled in the foreach loop below.
216
                    $json['if'] = [];
5✔
217
                    $json['then'] = false;
5✔
218
                    unset($json['else']);
5✔
219
                    return $json;
5✔
220
                }
221

222
                // Rewrite if: false, else: X as if: {}, then: X.
223
                // An empty if schema always passes so then always applies.
224
                // The ConditionalException will say "Condition: Valid" which is accurate
225
                // for if: {} but won't mention "else"; the message still correctly names
226
                // the failing branch constraint.
227
                $json['if'] = [];
4✔
228
                $json['then'] = $json['else'];
4✔
229
                unset($json['else']);
4✔
230

231
                return $json;
4✔
232
            }
233

234
            if (!isset($json['then'])) {
10✔
235
                if (isset($json['else']) && $schemaProcessor->getGeneratorConfiguration()->isOutputEnabled()) {
1✔
236
                    // @codeCoverageIgnoreStart
237
                    echo "Warning: if: true for property '{$property->getName()}'"
238
                        . " — else branch will never apply (condition always matches); no constraint generated.\n";
239
                    // @codeCoverageIgnoreEnd
240
                }
241
                return null;
1✔
242
            }
243

244
            if ($json['then'] === true) {
9✔
NEW
245
                return null;
×
246
            }
247

248
            if ($json['then'] === false) {
9✔
249
                $this->warnIfAlwaysFalse(
5✔
250
                    $schemaProcessor,
5✔
251
                    $property,
5✔
252
                    'if: true with then: false means the composition is always unsatisfiable',
5✔
253
                );
5✔
254
            }
255

256
            // Rewrite if: true, then: Y as if: {}, then: Y (removing else — it never applies).
257
            // If then is false the false-branch is handled in the foreach loop below.
258
            $json['if'] = [];
9✔
259
            unset($json['else']);
9✔
260

261
            return $json;
9✔
262
        }
263

264
        if (isset($json['then']) && is_bool($json['then'])) {
67✔
265
            if ($json['then'] === false) {
2✔
266
                throw new SchemaException(
1✔
267
                    sprintf(
1✔
268
                        'then: false is unsatisfiable for property %s in file %s',
1✔
269
                        $property->getName(),
1✔
270
                        $property->getJsonSchema()->getFile(),
1✔
271
                    ),
1✔
272
                );
1✔
273
            }
274

275
            unset($json['then']);
1✔
276
        }
277

278
        if (isset($json['else']) && is_bool($json['else'])) {
66✔
279
            if ($json['else'] === false) {
2✔
280
                throw new SchemaException(
1✔
281
                    sprintf(
1✔
282
                        'else: false is unsatisfiable for property %s in file %s',
1✔
283
                        $property->getName(),
1✔
284
                        $property->getJsonSchema()->getFile(),
1✔
285
                    ),
1✔
286
                );
1✔
287
            }
288

289
            unset($json['else']);
1✔
290
        }
291

292
        if (!isset($json['then']) && !isset($json['else'])) {
65✔
293
            return null;
2✔
294
        }
295

296
        return $json;
63✔
297
    }
298
}
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