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

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

04 Dec 2025 03:52PM UTC coverage: 98.621% (-0.04%) from 98.665%
19935096317

Pull #95

github

wol-soft
cross platform path compatibility
Pull Request #95: Improve the resolving of references

52 of 54 new or added lines in 5 files covered. (96.3%)

3 existing lines in 1 file now uncovered.

3433 of 3481 relevant lines covered (98.62%)

560.31 hits per line

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

92.16
/src/Model/SchemaDefinition/SchemaDefinitionDictionary.php
1
<?php
2

3
declare(strict_types = 1);
4

5
namespace PHPModelGenerator\Model\SchemaDefinition;
6

7
use ArrayObject;
8
use PHPModelGenerator\Exception\SchemaException;
9
use PHPModelGenerator\Model\Schema;
10
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
11

12
/**
13
 * Class SchemaDefinitionDictionary
14
 *
15
 * @package PHPModelGenerator\Model\SchemaDefinition
16
 */
17
class SchemaDefinitionDictionary extends ArrayObject
18
{
19
    /** @var Schema[] */
20
    private array $parsedExternalFileSchemas = [];
21

22
    /**
23
     * SchemaDefinitionDictionary constructor.
24
     */
25
    public function __construct(private ?JsonSchema $schema = null)
1,987✔
26
    {
27
        parent::__construct();
1,987✔
28
    }
29

30
    /**
31
     * Set up the definition directory for the schema
32
     */
33
    public function setUpDefinitionDictionary(SchemaProcessor $schemaProcessor, Schema $schema): void {
1,979✔
34
        // attach the root node to the definition dictionary
35
        $this->addDefinition('#', new SchemaDefinition($schema->getJsonSchema(), $schemaProcessor, $schema));
1,979✔
36

37
        foreach ($schema->getJsonSchema()->getJson() as $key => $propertyEntry) {
1,979✔
38
            if (!is_array($propertyEntry)) {
1,979✔
39
                continue;
1,979✔
40
            }
41

42
            // add the root nodes of the schema to resolve path references
43
            $this->addDefinition(
1,964✔
44
                $key,
1,964✔
45
                new SchemaDefinition($schema->getJsonSchema()->withJson($propertyEntry), $schemaProcessor, $schema),
1,964✔
46
            );
1,964✔
47
        }
48

49
        $this->fetchDefinitionsById($schema->getJsonSchema(), $schemaProcessor, $schema);
1,979✔
50
    }
51

52
    /**
53
     * Fetch all schema definitions with an ID for direct references
54
     */
55
    protected function fetchDefinitionsById(
1,979✔
56
        JsonSchema $jsonSchema,
57
        SchemaProcessor $schemaProcessor,
58
        Schema $schema,
59
    ): void {
60
        $json = $jsonSchema->getJson();
1,979✔
61

62
        if (isset($json['$id'])) {
1,979✔
63
            $this->addDefinition(
296✔
64
                str_starts_with($json['$id'], '#') ? $json['$id'] : "#{$json['$id']}",
296✔
65
                new SchemaDefinition($jsonSchema, $schemaProcessor, $schema),
296✔
66
            );
296✔
67
        }
68

69
        foreach ($json as $item) {
1,979✔
70
            if (!is_array($item)) {
1,979✔
71
                continue;
1,979✔
72
            }
73

74
            $this->fetchDefinitionsById($jsonSchema->withJson($item), $schemaProcessor, $schema);
1,964✔
75
        }
76
    }
77

78
    /**
79
     * Add a partial schema definition to the schema
80
     *
81
     * @return $this
82
     */
83
    public function addDefinition(string $key, SchemaDefinition $definition): self
1,979✔
84
    {
85
        if (isset($this[$key])) {
1,979✔
86
            return $this;
810✔
87
        }
88

89
        $this[$key] = $definition;
1,979✔
90

91
        return $this;
1,979✔
92
    }
93

94
    /**
95
     * @throws SchemaException
96
     */
97
    public function getDefinition(string $key, SchemaProcessor $schemaProcessor, array &$path = []): ?SchemaDefinition
528✔
98
    {
99
        if (str_starts_with($key, '#') && strpos($key, '/')) {
528✔
100
            $path = explode('/', $key);
405✔
101
            array_shift($path);
405✔
102
            $key  = array_shift($path);
405✔
103
        }
104

105
        if (!isset($this[$key])) {
528✔
106
            if (strstr($key, '#', true)) {
191✔
107
                [$jsonSchemaFile, $externalKey] = explode('#', $key);
164✔
108
            } else {
109
                $jsonSchemaFile = $key;
28✔
110
                $externalKey = '';
28✔
111
            }
112

113
            if (array_key_exists($jsonSchemaFile, $this->parsedExternalFileSchemas)) {
191✔
114
                return $this->parsedExternalFileSchemas[$jsonSchemaFile]->getSchemaDictionary()->getDefinition(
×
115
                    "#$externalKey",
×
116
                    $schemaProcessor,
×
117
                    $path,
×
118
                );
×
119
            }
120

121
            return $jsonSchemaFile
191✔
122
                ? $this->parseExternalFile($jsonSchemaFile, "#$externalKey", $schemaProcessor, $path)
191✔
123
                : null;
186✔
124
        }
125

126
        return $this[$key] ?? null;
523✔
127
    }
128

129
    /**
130
     * @throws SchemaException
131
     */
132
    private function parseExternalFile(
191✔
133
        string $jsonSchemaFile,
134
        string $externalKey,
135
        SchemaProcessor $schemaProcessor,
136
        array &$path,
137
    ): ?SchemaDefinition {
138
        $jsonSchemaFilePath = $this->getFullRefURL($jsonSchemaFile) ?: $this->getLocalRefPath($jsonSchemaFile);
191✔
139

140
        if ($jsonSchemaFilePath === null) {
191✔
141
            throw new SchemaException("Reference to non existing JSON-Schema file $jsonSchemaFile");
5✔
142
        }
143

144
        $jsonSchema = file_get_contents($jsonSchemaFilePath);
187✔
145

146
        if (!$jsonSchema || !($decodedJsonSchema = json_decode($jsonSchema, true))) {
187✔
UNCOV
147
            throw new SchemaException("Invalid JSON-Schema file $jsonSchemaFilePath");
×
148
        }
149

150
        // set up a dummy schema to fetch the definitions from the external file
151
        $schema = new Schema(
187✔
152
            '',
187✔
153
            $schemaProcessor->getCurrentClassPath(),
187✔
154
            'ExternalSchema',
187✔
155
            $externalSchema = new JsonSchema($jsonSchemaFilePath, $decodedJsonSchema),
187✔
156
            new self($externalSchema),
187✔
157
        );
187✔
158

159
        $schema->getSchemaDictionary()->setUpDefinitionDictionary($schemaProcessor, $schema);
187✔
160
        $this->parsedExternalFileSchemas[$jsonSchemaFile] = $schema;
187✔
161

162
        return $schema->getSchemaDictionary()->getDefinition($externalKey, $schemaProcessor, $path);
187✔
163
    }
164

165
    /**
166
     * Try to build a full URL to fetch the schema from utilizing the $id field of the schema
167
     */
168
    private function getFullRefURL(string $jsonSchemaFile): ?string
191✔
169
    {
170
        if (filter_var($jsonSchemaFile, FILTER_VALIDATE_URL)) {
191✔
171
            return $jsonSchemaFile;
1✔
172
        }
173

174
        if ($this->schema === null
191✔
175
            || !filter_var($this->schema->getJson()['$id'] ?? $this->schema->getFile(), FILTER_VALIDATE_URL)
191✔
176
            || ($idURL = parse_url($this->schema->getJson()['$id'] ?? $this->schema->getFile())) === false
191✔
177
        ) {
178
            return null;
188✔
179
        }
180

181
        $baseURL = $idURL['scheme'] . '://' . $idURL['host'] . (isset($idURL['port']) ? ':' . $idURL['port'] : '');
3✔
182

183
        // root relative $ref
184
        if (str_starts_with($jsonSchemaFile, '/')) {
3✔
185
            return $baseURL . $jsonSchemaFile;
1✔
186
        }
187

188
        // relative $ref against the path of $id
189
        $segments = explode('/', rtrim(dirname($idURL['path'] ?? '/'), '/') . '/' . $jsonSchemaFile);
3✔
190
        $output = [];
3✔
191

192
        foreach ($segments as $seg) {
3✔
193
            if ($seg === '' || $seg === '.') {
3✔
194
                continue;
3✔
195
            }
196
            if ($seg === '..') {
3✔
197
                array_pop($output);
1✔
198
                continue;
1✔
199
            }
200
            $output[] = $seg;
3✔
201
        }
202

203
        return $baseURL . '/' . implode('/', $output);
3✔
204
    }
205

206
    private function getLocalRefPath(string $jsonSchemaFile): ?string
188✔
207
    {
208
        $currentDir = dirname($this->schema->getFile());
188✔
209
        // windows compatibility
210
        $jsonSchemaFile = str_replace('\\', '/', $jsonSchemaFile);
188✔
211

212
        // relative paths to the current location
213
        if (!str_starts_with($jsonSchemaFile, '/')) {
188✔
214
            $candidate = $currentDir . '/' . $jsonSchemaFile;
188✔
215

216
            return file_exists($candidate) ? $candidate : null;
188✔
217
        }
218

219
        // absolute paths: traverse up to find the context root directory
220
        $relative = ltrim($jsonSchemaFile, '/');
1✔
221

222
        $dir = $currentDir;
1✔
223
        while (true) {
1✔
224
            $candidate = $dir . '/' . $relative;
1✔
225
            if (file_exists($candidate)) {
1✔
226
                return $candidate;
1✔
227
            }
228

229
            $parent = dirname($dir);
1✔
230
            if ($parent === $dir) {
1✔
NEW
UNCOV
231
                break;
×
232
            }
233
            $dir = $parent;
1✔
234
        }
235

NEW
UNCOV
236
        return null;
×
237
    }
238
}
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

© 2025 Coveralls, Inc