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

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

20 Mar 2026 09:38PM UTC coverage: 98.584% (-0.1%) from 98.693%
23363658068

Pull #115

github

Enno Woortmann
Add PSR-12 code style enforcement via PHP CodeSniffer

- Add squizlabs/php_codesniffer ^4.0 as dev dependency
- Add phpcs.xml with PSR-12 baseline, disabling 4 rules:
  ScopeClosingBrace (keep compact empty bodies), ClassDeclaration
  ExtendsLine/ImplementsLine (allow extends/implements on own lines),
  and CamelCapsMethodName (allow snake_case internal utility methods)
- Apply phpcbf auto-fixes across all 108 affected source files
- Manually wrap 9 lines exceeding 120 characters
- Document phpcs check step and qlty.sh PR review URLs in CLAUDE.md
Pull Request #115: Type system (Type widening for compositions, union types)

479 of 489 new or added lines in 51 files covered. (97.96%)

1 existing line in 1 file now uncovered.

3829 of 3884 relevant lines covered (98.58%)

549.49 hits per line

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

99.09
/src/PropertyProcessor/ComposedValue/AbstractComposedValueProcessor.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\PropertyProcessor\ComposedValue;
6

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

25
/**
26
 * Class AbstractComposedValueProcessor
27
 *
28
 * @package PHPModelGenerator\PropertyProcessor\ComposedValue
29
 */
30
abstract class AbstractComposedValueProcessor extends AbstractValueProcessor
31
{
32
    private ?PropertyInterface $mergedProperty = null;
33

34
    /**
35
     * AbstractComposedValueProcessor constructor.
36
     */
37
    public function __construct(
611✔
38
        PropertyMetaDataCollection $propertyMetaDataCollection,
39
        SchemaProcessor $schemaProcessor,
40
        Schema $schema,
41
        private bool $rootLevelComposition,
42
    ) {
43
        parent::__construct($propertyMetaDataCollection, $schemaProcessor, $schema);
611✔
44
    }
45

46
    /**
47
     * @inheritdoc
48
     */
49
    protected function generateValidators(PropertyInterface $property, JsonSchema $propertySchema): void
611✔
50
    {
51
        $json = $propertySchema->getJson()['propertySchema']->getJson();
611✔
52

53
        if (
54
            empty($json[$propertySchema->getJson()['type']]) &&
611✔
55
            $this->schemaProcessor->getGeneratorConfiguration()->isOutputEnabled()
611✔
56
        ) {
57
            // @codeCoverageIgnoreStart
58
            echo "Warning: empty composition for {$property->getName()} may lead to unexpected results\n";
59
            // @codeCoverageIgnoreEnd
60
        }
61

62
        $compositionProperties = $this->getCompositionProperties($property, $propertySchema);
611✔
63

64
        $resolvedCompositions = 0;
611✔
65
        foreach ($compositionProperties as $compositionProperty) {
611✔
66
            $compositionProperty->onResolve(
593✔
67
                function () use (&$resolvedCompositions, $property, $compositionProperties, $propertySchema): void {
593✔
68
                    if (++$resolvedCompositions === count($compositionProperties)) {
593✔
69
                        $this->transferPropertyType($property, $compositionProperties);
593✔
70

71
                        $this->mergedProperty = !$this->rootLevelComposition
593✔
72
                            && $this instanceof MergedComposedPropertiesInterface
593✔
73
                                ? $this->schemaProcessor->createMergedProperty(
184✔
74
                                    $this->schema,
184✔
75
                                    $property,
184✔
76
                                    $compositionProperties,
184✔
77
                                    $propertySchema,
184✔
78
                                )
184✔
79
                                : null;
425✔
80
                    }
81
                },
593✔
82
            );
593✔
83
        }
84

85
        $availableAmount = count($compositionProperties);
611✔
86

87
        $property->addValidator(
611✔
88
            new ComposedPropertyValidator(
611✔
89
                $this->schemaProcessor->getGeneratorConfiguration(),
611✔
90
                $property,
611✔
91
                $compositionProperties,
611✔
92
                static::class,
611✔
93
                [
611✔
94
                    'compositionProperties' => $compositionProperties,
611✔
95
                    'schema' => $this->schema,
611✔
96
                    'generatorConfiguration' => $this->schemaProcessor->getGeneratorConfiguration(),
611✔
97
                    'viewHelper' => new RenderHelper($this->schemaProcessor->getGeneratorConfiguration()),
611✔
98
                    'availableAmount' => $availableAmount,
611✔
99
                    'composedValueValidation' => $this->getComposedValueValidation($availableAmount),
611✔
100
                    // if the property is a composed property the resulting value of a validation must be proposed
101
                    // to be the final value after the validations (e.g. object instantiations may be performed).
102
                    // Otherwise (eg. a NotProcessor) the value must be proposed before the validation
103
                    'postPropose' => $this instanceof ComposedPropertiesInterface,
611✔
104
                    'mergedProperty' => &$this->mergedProperty,
611✔
105
                    'onlyForDefinedValues' =>
611✔
106
                        $propertySchema->getJson()['onlyForDefinedValues']
611✔
107
                        && $this instanceof ComposedPropertiesInterface,
611✔
108
                ],
611✔
109
            ),
611✔
110
            100,
611✔
111
        );
611✔
112
    }
113

114
    /**
115
     * Set up composition properties for the given property schema
116
     *
117
     * @return CompositionPropertyDecorator[]
118
     *
119
     * @throws SchemaException
120
     */
121
    protected function getCompositionProperties(PropertyInterface $property, JsonSchema $propertySchema): array
611✔
122
    {
123
        $propertyFactory = new PropertyFactory(new PropertyProcessorFactory());
611✔
124
        $compositionProperties = [];
611✔
125
        $json = $propertySchema->getJson()['propertySchema']->getJson();
611✔
126

127
        // clear the base type of the property to keep only the types of the composition.
128
        // This avoids e.g. "array|int[]" for a property which is known to contain always an integer array
129
        $property->addTypeHintDecorator(new ClearTypeHintDecorator());
611✔
130

131
        foreach ($json[$propertySchema->getJson()['type']] as $compositionElement) {
611✔
132
            $compositionSchema = $propertySchema->getJson()['propertySchema']->withJson($compositionElement);
593✔
133

134
            $compositionProperty = new CompositionPropertyDecorator(
593✔
135
                $property->getName(),
593✔
136
                $compositionSchema,
593✔
137
                $propertyFactory
593✔
138
                    ->create(
593✔
139
                        new PropertyMetaDataCollection([$property->getName() => $property->isRequired()]),
593✔
140
                        $this->schemaProcessor,
593✔
141
                        $this->schema,
593✔
142
                        $property->getName(),
593✔
143
                        $compositionSchema,
593✔
144
                    )
593✔
145
            );
593✔
146

147
            $compositionProperty->onResolve(function () use ($compositionProperty, $property): void {
593✔
148
                $compositionProperty->filterValidators(
593✔
149
                    static fn(Validator $validator): bool =>
593✔
150
                        !is_a($validator->getValidator(), RequiredPropertyValidator::class) &&
593✔
151
                        !is_a($validator->getValidator(), ComposedPropertyValidator::class)
593✔
152
                );
593✔
153

154
                // only create a composed type hint if we aren't a AnyOf or an AllOf processor and the
155
                // compositionProperty contains no object. This results in objects being composed each separately for a
156
                // OneOf processor (e.g. string|ObjectA|ObjectB). For a merged composed property the objects are merged
157
                // together, so it results in string|MergedObject
158
                if (!($this instanceof MergedComposedPropertiesInterface && $compositionProperty->getNestedSchema())) {
593✔
159
                    $property->addTypeHintDecorator(new CompositionTypeHintDecorator($compositionProperty));
360✔
160
                }
161
            });
593✔
162

163
            $compositionProperties[] = $compositionProperty;
593✔
164
        }
165

166
        return $compositionProperties;
611✔
167
    }
168

169
    /**
170
     * Check if the provided property can inherit a single type from the composition properties.
171
     *
172
     * @param CompositionPropertyDecorator[] $compositionProperties
173
     */
174
    private function transferPropertyType(PropertyInterface $property, array $compositionProperties): void
593✔
175
    {
176
        if ($this instanceof NotProcessor) {
593✔
177
            return;
92✔
178
        }
179

180
        // Skip widening when any branch has a nested schema (object): the merged-property
181
        // mechanism creates a combined class whose name is not among the per-branch type names.
182
        foreach ($compositionProperties as $p) {
501✔
183
            if ($p->getNestedSchema() !== null) {
501✔
184
                return;
373✔
185
            }
186
        }
187

188
        // Flatten all type names from all branches. Use getNames() to handle branches that
189
        // already carry a union PropertyType (e.g. from Phase 4 or Phase 5).
190
        $allNames = array_merge(...array_map(
129✔
191
            static fn(CompositionPropertyDecorator $p): array => $p->getType() ? $p->getType()->getNames() : [],
129✔
192
            $compositionProperties,
129✔
193
        ));
129✔
194

195
        // A branch with no type contributes nothing but signals that nullable=true is required.
196
        $hasBranchWithNoType = array_filter(
129✔
197
            $compositionProperties,
129✔
198
            static fn(CompositionPropertyDecorator $p): bool => $p->getType() === null,
129✔
199
        ) !== [];
129✔
200

201
        // An optional branch (property not required in that branch) means the property can be
202
        // absent at runtime, causing the root getter to return null. This is a structural
203
        // nullable — independent of the implicit-null configuration setting.
204
        //
205
        // For oneOf/anyOf: any optional branch makes the property nullable (the branch that
206
        // omits the property can match, leaving the value as null).
207
        //
208
        // For allOf: all branches must hold simultaneously. If at least one branch marks the
209
        // property as required, the property is required overall — an optional branch in allOf
210
        // does not by itself make the property nullable. Only if NO branch requires the property
211
        // (i.e. the property is optional across all allOf branches) is it structurally nullable.
212
        $hasBranchWithRequiredProperty = array_filter(
129✔
213
            $compositionProperties,
129✔
214
            static fn(CompositionPropertyDecorator $p): bool => $p->isRequired(),
129✔
215
        ) !== [];
129✔
216
        $hasBranchWithOptionalProperty = $this instanceof AllOfProcessor
129✔
217
            ? !$hasBranchWithRequiredProperty
25✔
218
            : array_filter(
104✔
219
                $compositionProperties,
104✔
220
                static fn(CompositionPropertyDecorator $p): bool => !$p->isRequired(),
104✔
221
            ) !== [];
104✔
222

223
        // Strip 'null' → nullable flag; PropertyType constructor deduplicates the rest.
224
        $hasNull = in_array('null', $allNames, true);
129✔
225
        $nonNullNames = array_values(array_filter(
129✔
226
            array_unique($allNames),
129✔
227
            fn(string $t): bool => $t !== 'null',
129✔
228
        ));
129✔
229

230
        if (!$nonNullNames) {
129✔
NEW
231
            return;
×
232
        }
233

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

236
        $property->setType(new PropertyType($nonNullNames, $nullable));
129✔
237
    }
238

239
    /**
240
     * @param int $composedElements The amount of elements which are composed together
241
     */
242
    abstract protected function getComposedValueValidation(int $composedElements): string;
243
}
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