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

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

01 Jun 2026 10:10AM UTC coverage: 98.714% (-0.2%) from 98.867%
26751618438

Pull #146

github

Enno Woortmann
Merge remote-tracking branch 'origin/master' into issue133
Pull Request #146: issue133

138 of 149 new or added lines in 8 files covered. (92.62%)

6063 of 6142 relevant lines covered (98.71%)

1166.02 hits per line

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

95.24
/src/Utils/PropertyAttributeSynthesizer.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\Utils;
6

7
use PHPModelGenerator\Attributes\JsonPointer;
8
use PHPModelGenerator\Attributes\JsonSchema as JsonSchemaAttribute;
9
use PHPModelGenerator\Model\Attributes\PhpAttribute;
10
use PHPModelGenerator\Model\GeneratorConfiguration;
11
use PHPModelGenerator\Model\Schema;
12
use PHPModelGenerator\Model\Validator\AbstractComposedPropertyValidator;
13
use PHPModelGenerator\Model\Validator\ConditionalPropertyValidator;
14
use PHPModelGenerator\Model\Validator\Factory\Composition\AllOfValidatorFactory;
15
use PHPModelGenerator\Model\Validator\Factory\Composition\AnyOfValidatorFactory;
16
use PHPModelGenerator\Model\Validator\Factory\Composition\IfValidatorFactory;
17
use PHPModelGenerator\Model\Validator\Factory\Composition\OneOfValidatorFactory;
18
use PHPModelGenerator\Model\Property\CompositionPropertyDecorator;
19
use PHPModelGenerator\Model\Property\PropertyInterface;
20

21
class PropertyAttributeSynthesizer
22
{
23
    private const array COMPOSITION_KEYWORDS = [
24
        AllOfValidatorFactory::class => 'allOf',
25
        AnyOfValidatorFactory::class => 'anyOf',
26
        OneOfValidatorFactory::class => 'oneOf',
27
        IfValidatorFactory::class    => 'if',
28
    ];
29

30
    public function __construct(private GeneratorConfiguration $generatorConfiguration)
5,150✔
31
    {}
5,150✔
32

33
    /**
34
     * Synthesise correct #[JsonPointer] and #[JsonSchema] attributes for each property that
35
     * was transferred from composition branches onto the outer schema.
36
     *
37
     * Called once per validator after all its branches have been resolved.
38
     *
39
     * @param array<string, true> $seenBranchPropertyNames
40
     */
41
    public function synthesiseForValidator(
750✔
42
        AbstractComposedPropertyValidator $validator,
43
        Schema $schema,
44
        array $seenBranchPropertyNames,
45
    ): void {
46
        $compositionProcessor = $validator->getCompositionProcessor();
750✔
47
        $keyword = self::COMPOSITION_KEYWORDS[$compositionProcessor];
750✔
48

49
        $isConditional = $validator instanceof ConditionalPropertyValidator;
750✔
50

51
        // For pointer synthesis: include all branches (if, then, else) so each property gets a
52
        // pointer to wherever it is defined, regardless of which branch type defines it.
53
        // For JSON schema synthesis: use condition branches (then/else) since those define the
54
        // data-shape constraints, plus the if-branch schema is added separately under 'if'.
55
        $branchesForPointers = $validator->getComposedProperties();
750✔
56
        $branchesForSchemas  = $isConditional
750✔
57
            ? $validator->getConditionBranches()
112✔
58
            : $validator->getComposedProperties();
674✔
59

60
        $ifBranch = $isConditional ? $validator->getIfBranch() : null;
750✔
61
        $thenBranch = $isConditional ? $validator->getThenBranch() : null;
750✔
62
        $elseBranch = $isConditional ? $validator->getElseBranch() : null;
750✔
63

64
        foreach (array_keys($seenBranchPropertyNames) as $propertyName) {
750✔
65
            $outerProperty = $schema->getProperty($propertyName);
744✔
66

67
            if ($outerProperty === null) {
744✔
NEW
68
                continue;
×
69
            }
70

71
            $branchPointers = [];
744✔
72
            $branchSchemas  = [];
744✔
73

74
            foreach ($branchesForPointers as $branch) {
744✔
75
                if ($branch->getNestedSchema() === null) {
744✔
NEW
76
                    continue;
×
77
                }
78

79
                foreach ($branch->getNestedSchema()->getProperties() as $branchProperty) {
744✔
80
                    if ($branchProperty->getName() === $propertyName) {
744✔
81
                        $branchPointers[] = $branchProperty->getJsonSchema()->getPointer();
744✔
82
                        break;
744✔
83
                    }
84
                }
85
            }
86

87
            foreach ($branchesForSchemas as $branch) {
744✔
88
                if ($branch->getNestedSchema() === null) {
744✔
NEW
89
                    continue;
×
90
                }
91

92
                foreach ($branch->getNestedSchema()->getProperties() as $branchProperty) {
744✔
93
                    if ($branchProperty->getName() === $propertyName) {
744✔
94
                        $branchSchemas[] = $branchProperty->getJsonSchema()->getJson();
744✔
95
                        break;
744✔
96
                    }
97
                }
98
            }
99

100
            $ifBranchJson = $ifBranch !== null
744✔
101
                ? $this->findPropertyJsonInBranch($ifBranch, $propertyName)
112✔
102
                : null;
668✔
103
            $thenBranchJson = $thenBranch !== null
744✔
104
                ? $this->findPropertyJsonInBranch($thenBranch, $propertyName)
108✔
105
                : null;
672✔
106
            $elseBranchJson = $elseBranch !== null
744✔
107
                ? $this->findPropertyJsonInBranch($elseBranch, $propertyName)
86✔
108
                : null;
686✔
109

110
            $isRootRegistered = $schema->isRootRegistered($propertyName);
744✔
111
            $rootPointer      = $isRootRegistered ? $outerProperty->getJsonSchema()->getPointer() : null;
744✔
112
            $rootLevelJson    = $isRootRegistered ? $outerProperty->getJsonSchema()->getJson() : null;
744✔
113

114
            $this->synthesiseJsonPointerAttributes(
744✔
115
                $outerProperty,
744✔
116
                $rootPointer,
744✔
117
                $branchPointers,
744✔
118
            );
744✔
119

120
            $this->synthesiseJsonSchemaAttribute(
744✔
121
                $outerProperty,
744✔
122
                $keyword,
744✔
123
                $branchSchemas,
744✔
124
                $ifBranchJson,
744✔
125
                $thenBranchJson,
744✔
126
                $elseBranchJson,
744✔
127
                $rootLevelJson,
744✔
128
            );
744✔
129
        }
130
    }
131

132
    private function synthesiseJsonPointerAttributes(
744✔
133
        PropertyInterface $property,
134
        ?string $rootPointer,
135
        array $branchPointers,
136
    ): void {
137
        if (($this->generatorConfiguration->getEnabledAttributes() & PhpAttribute::JSON_POINTER) === 0) {
744✔
NEW
138
            return;
×
139
        }
140

141
        $property->filterAttributes(
744✔
142
            static fn(PhpAttribute $attribute): bool => $attribute->getFqcn() !== JsonPointer::class,
744✔
143
        );
744✔
144

145
        if ($rootPointer !== null) {
744✔
146
            $property->addAttribute(new PhpAttribute(JsonPointer::class, [$rootPointer]));
174✔
147
        }
148

149
        foreach ($branchPointers as $pointer) {
744✔
150
            $property->addAttribute(new PhpAttribute(JsonPointer::class, [$pointer]));
744✔
151
        }
152
    }
153

154
    private function findPropertyJsonInBranch(
112✔
155
        CompositionPropertyDecorator $branch,
156
        string $propertyName,
157
    ): ?array {
158
        $nestedSchema = $branch->getNestedSchema();
112✔
159

160
        if ($nestedSchema === null) {
112✔
NEW
161
            return null;
×
162
        }
163

164
        foreach ($nestedSchema->getProperties() as $branchProperty) {
112✔
165
            if ($branchProperty->getName() === $propertyName) {
112✔
166
                return $branchProperty->getJsonSchema()->getJson();
112✔
167
            }
168
        }
169

170
        return null;
112✔
171
    }
172

173
    private function synthesiseJsonSchemaAttribute(
744✔
174
        PropertyInterface $property,
175
        string $keyword,
176
        array $branchSchemas,
177
        ?array $ifBranchJson,
178
        ?array $thenBranchJson,
179
        ?array $elseBranchJson,
180
        ?array $rootLevelJson,
181
    ): void {
182
        if (($this->generatorConfiguration->getEnabledAttributes() & PhpAttribute::JSON_SCHEMA) === 0) {
744✔
183
            return;
736✔
184
        }
185

186
        $baseJson = $rootLevelJson ?? [];
8✔
187

188
        if ($keyword === 'if') {
8✔
189
            $conditionalParts = array_filter([
2✔
190
                'if'   => $ifBranchJson,
2✔
191
                'then' => $thenBranchJson,
2✔
192
                'else' => $elseBranchJson,
2✔
193
            ]);
2✔
194
            $synthesised = array_merge($baseJson, $conditionalParts);
2✔
195
        } else {
196
            $uniqueBranchSchemas = array_values(array_unique(
6✔
197
                array_map(static fn(array $schema): string => json_encode($schema), $branchSchemas),
6✔
198
            ));
6✔
199
            $deduplicatedBranchSchemas = array_map(
6✔
200
                static fn(string $encoded): array => json_decode($encoded, true),
6✔
201
                $uniqueBranchSchemas,
6✔
202
            );
6✔
203
            $synthesised = array_merge($baseJson, [$keyword => $deduplicatedBranchSchemas]);
6✔
204
        }
205

206
        $property->filterAttributes(
8✔
207
            static fn(PhpAttribute $attribute): bool => $attribute->getFqcn() !== JsonSchemaAttribute::class,
8✔
208
        );
8✔
209

210
        $property->addAttribute(
8✔
211
            new PhpAttribute(
8✔
212
                JsonSchemaAttribute::class,
8✔
213
                [empty($synthesised) ? '{}' : json_encode($synthesised)],
8✔
214
            ),
8✔
215
        );
8✔
216
    }
217
}
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