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

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

30 Mar 2026 09:08AM UTC coverage: 98.331% (-0.4%) from 98.717%
23736910562

Pull #123

github

Enno Woortmann
Merge remote-tracking branch 'origin/master' into drafts
Pull Request #123: Introduce Draft-based architecture: eliminate legacy processors, centralize modifier/validator registration

1374 of 1399 new or added lines in 60 files covered. (98.21%)

3 existing lines in 2 files now uncovered.

4243 of 4315 relevant lines covered (98.33%)

588.89 hits per line

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

97.83
/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(
607✔
29
        SchemaProcessor $schemaProcessor,
30
        PropertyInterface $property,
31
        JsonSchema $propertySchema,
32
    ): void {
33
        if (
34
            empty($propertySchema->getJson()[$this->key]) &&
607✔
35
            $schemaProcessor->getGeneratorConfiguration()->isOutputEnabled()
607✔
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
745✔
52
    {
53
        return !($property instanceof BaseProperty)
745✔
54
            && ($propertySchema->getJson()['type'] ?? '') === 'object';
745✔
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(
699✔
67
        SchemaProcessor $schemaProcessor,
68
        Schema $schema,
69
        PropertyInterface $property,
70
        JsonSchema $propertySchema,
71
        bool $merged,
72
    ): array {
73
        $propertyFactory = new PropertyFactory();
699✔
74
        $compositionProperties = [];
699✔
75
        $json = $propertySchema->getJson()['propertySchema']->getJson();
699✔
76

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

79
        foreach ($json[$this->key] as $compositionElement) {
699✔
80
            $compositionSchema = $propertySchema->getJson()['propertySchema']->withJson($compositionElement);
680✔
81

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

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

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

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

109
        return $compositionProperties;
699✔
110
    }
111

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

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

123
        if ($json['type'] === 'base') {
442✔
NEW
124
            $json['type'] = 'object';
×
125
        }
126

127
        switch ($this->key) {
442✔
128
            case 'not':
442✔
129
                if (!isset($json[$this->key]['type'])) {
24✔
130
                    $json[$this->key]['type'] = $json['type'];
24✔
131
                }
132
                break;
24✔
133
            case 'if':
418✔
134
                return $this->inheritIfPropertyType($propertySchema->withJson($json));
63✔
135
            default:
136
                foreach ($json[$this->key] as &$composedElement) {
373✔
137
                    if (!isset($composedElement['type'])) {
372✔
138
                        $composedElement['type'] = $json['type'];
163✔
139
                    }
140
                }
141
        }
142

143
        return $propertySchema->withJson($json);
397✔
144
    }
145

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

153
        foreach (['if', 'then', 'else'] as $keyword) {
63✔
154
            if (!isset($json[$keyword])) {
63✔
155
                continue;
14✔
156
            }
157

158
            if (!isset($json[$keyword]['type'])) {
63✔
159
                $json[$keyword]['type'] = $json['type'];
63✔
160
            }
161
        }
162

163
        return $propertySchema->withJson($json);
63✔
164
    }
165

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

184
        $allNames = array_merge(...array_map(
129✔
185
            static fn(CompositionPropertyDecorator $p): array =>
129✔
186
                $p->getType() ? $p->getType()->getNames() : [],
129✔
187
            $compositionProperties,
129✔
188
        ));
129✔
189

190
        $hasBranchWithNoType = array_filter(
129✔
191
            $compositionProperties,
129✔
192
            static fn(CompositionPropertyDecorator $p): bool => $p->getType() === null,
129✔
193
        ) !== [];
129✔
194

195
        $hasBranchWithRequiredProperty = array_filter(
129✔
196
            $compositionProperties,
129✔
197
            static fn(CompositionPropertyDecorator $p): bool => $p->isRequired(),
129✔
198
        ) !== [];
129✔
199

200
        $hasBranchWithOptionalProperty = $isAllOf
129✔
201
            ? !$hasBranchWithRequiredProperty
25✔
202
            : array_filter(
104✔
203
                $compositionProperties,
104✔
204
                static fn(CompositionPropertyDecorator $p): bool => !$p->isRequired(),
104✔
205
            ) !== [];
104✔
206

207
        $hasNull = in_array('null', $allNames, true);
129✔
208
        $nonNullNames = array_values(array_filter(
129✔
209
            array_unique($allNames),
129✔
210
            fn(string $t): bool => $t !== 'null',
129✔
211
        ));
129✔
212

213
        if (!$nonNullNames) {
129✔
NEW
214
            return;
×
215
        }
216

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

219
        $property->setType(new PropertyType($nonNullNames, $nullable));
129✔
220
    }
221
}
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