Skip to content

Commit f698d45

Browse files
committed
#95: Expose a $ref resolve method for the SchemaProvider to be able to load schemas from inaccessible locations (e.g. login, weird folder structure etc.)
1 parent 0b76442 commit f698d45

File tree

9 files changed

+149
-104
lines changed

9 files changed

+149
-104
lines changed

src/Model/Schema.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function __construct(
6565
protected bool $initialClass = false,
6666
) {
6767
$this->jsonSchema = $schema;
68-
$this->schemaDefinitionDictionary = $dictionary ?? new SchemaDefinitionDictionary();
68+
$this->schemaDefinitionDictionary = $dictionary ?? new SchemaDefinitionDictionary($schema);
6969
$this->description = $schema->getJson()['description'] ?? '';
7070

7171
$this->addInterface(JSONModelInterface::class);

src/Model/SchemaDefinition/SchemaDefinitionDictionary.php

Lines changed: 8 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class SchemaDefinitionDictionary extends ArrayObject
2222
/**
2323
* SchemaDefinitionDictionary constructor.
2424
*/
25-
public function __construct(private ?JsonSchema $schema = null)
25+
public function __construct(private JsonSchema $schema)
2626
{
2727
parent::__construct();
2828
}
@@ -135,104 +135,24 @@ private function parseExternalFile(
135135
SchemaProcessor $schemaProcessor,
136136
array &$path,
137137
): ?SchemaDefinition {
138-
$jsonSchemaFilePath = $this->getFullRefURL($jsonSchemaFile) ?: $this->getLocalRefPath($jsonSchemaFile);
139-
140-
if ($jsonSchemaFilePath === null) {
141-
throw new SchemaException("Reference to non existing JSON-Schema file $jsonSchemaFile");
142-
}
143-
144-
$jsonSchema = file_get_contents($jsonSchemaFilePath);
145-
146-
if (!$jsonSchema || !($decodedJsonSchema = json_decode($jsonSchema, true))) {
147-
throw new SchemaException("Invalid JSON-Schema file $jsonSchemaFilePath");
148-
}
138+
$jsonSchema = $schemaProcessor->getSchemaProvider()->getRef(
139+
$this->schema->getFile(),
140+
$this->schema->getJson()['$id'] ?? null,
141+
$jsonSchemaFile,
142+
);
149143

150144
// set up a dummy schema to fetch the definitions from the external file
151145
$schema = new Schema(
152146
'',
153147
$schemaProcessor->getCurrentClassPath(),
154148
'ExternalSchema',
155-
$externalSchema = new JsonSchema($jsonSchemaFilePath, $decodedJsonSchema),
156-
new self($externalSchema),
149+
$jsonSchema,
150+
new self($jsonSchema),
157151
);
158152

159153
$schema->getSchemaDictionary()->setUpDefinitionDictionary($schemaProcessor, $schema);
160154
$this->parsedExternalFileSchemas[$jsonSchemaFile] = $schema;
161155

162156
return $schema->getSchemaDictionary()->getDefinition($externalKey, $schemaProcessor, $path);
163157
}
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
169-
{
170-
if (filter_var($jsonSchemaFile, FILTER_VALIDATE_URL)) {
171-
return $jsonSchemaFile;
172-
}
173-
174-
if ($this->schema === null
175-
|| !filter_var($this->schema->getJson()['$id'] ?? $this->schema->getFile(), FILTER_VALIDATE_URL)
176-
|| ($idURL = parse_url($this->schema->getJson()['$id'] ?? $this->schema->getFile())) === false
177-
) {
178-
return null;
179-
}
180-
181-
$baseURL = $idURL['scheme'] . '://' . $idURL['host'] . (isset($idURL['port']) ? ':' . $idURL['port'] : '');
182-
183-
// root relative $ref
184-
if (str_starts_with($jsonSchemaFile, '/')) {
185-
return $baseURL . $jsonSchemaFile;
186-
}
187-
188-
// relative $ref against the path of $id
189-
$segments = explode('/', rtrim(dirname($idURL['path'] ?? '/'), '/') . '/' . $jsonSchemaFile);
190-
$output = [];
191-
192-
foreach ($segments as $seg) {
193-
if ($seg === '' || $seg === '.') {
194-
continue;
195-
}
196-
if ($seg === '..') {
197-
array_pop($output);
198-
continue;
199-
}
200-
$output[] = $seg;
201-
}
202-
203-
return $baseURL . '/' . implode('/', $output);
204-
}
205-
206-
private function getLocalRefPath(string $jsonSchemaFile): ?string
207-
{
208-
$currentDir = dirname($this->schema->getFile());
209-
// windows compatibility
210-
$jsonSchemaFile = str_replace('\\', '/', $jsonSchemaFile);
211-
212-
// relative paths to the current location
213-
if (!str_starts_with($jsonSchemaFile, '/')) {
214-
$candidate = $currentDir . '/' . $jsonSchemaFile;
215-
216-
return file_exists($candidate) ? $candidate : null;
217-
}
218-
219-
// absolute paths: traverse up to find the context root directory
220-
$relative = ltrim($jsonSchemaFile, '/');
221-
222-
$dir = $currentDir;
223-
while (true) {
224-
$candidate = $dir . '/' . $relative;
225-
if (file_exists($candidate)) {
226-
return $candidate;
227-
}
228-
229-
$parent = dirname($dir);
230-
if ($parent === $dir) {
231-
break;
232-
}
233-
$dir = $parent;
234-
}
235-
236-
return null;
237-
}
238158
}

src/ModelGenerator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public function generateModels(SchemaProviderInterface $schemaProvider, string $
105105

106106
$renderQueue = new RenderQueue();
107107
$schemaProcessor = new SchemaProcessor(
108-
$schemaProvider->getBaseDirectory(),
108+
$schemaProvider,
109109
$destination,
110110
$this->generatorConfiguration,
111111
$renderQueue,

src/SchemaProcessor/SchemaProcessor.php

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection;
2121
use PHPModelGenerator\PropertyProcessor\PropertyFactory;
2222
use PHPModelGenerator\PropertyProcessor\PropertyProcessorFactory;
23+
use PHPModelGenerator\SchemaProvider\SchemaProviderInterface;
2324

2425
/**
2526
* Class SchemaProcessor
@@ -28,23 +29,18 @@
2829
*/
2930
class SchemaProcessor
3031
{
31-
/** @var string */
32-
protected $currentClassPath;
33-
/** @var string */
34-
protected $currentClassName;
32+
protected string $currentClassPath;
33+
protected string $currentClassName;
3534

3635
/** @var Schema[] Collect processed schemas to avoid duplicated classes */
37-
protected $processedSchema = [];
36+
protected array $processedSchema = [];
3837
/** @var PropertyInterface[] Collect processed schemas to avoid duplicated classes */
39-
protected $processedMergedProperties = [];
38+
protected array $processedMergedProperties = [];
4039
/** @var string[] */
41-
protected $generatedFiles = [];
40+
protected array $generatedFiles = [];
4241

43-
/**
44-
* SchemaProcessor constructor.
45-
*/
4642
public function __construct(
47-
protected string $baseSource,
43+
protected SchemaProviderInterface $schemaProvider,
4844
protected string $destination,
4945
protected GeneratorConfiguration $generatorConfiguration,
5046
protected RenderQueue $renderQueue,
@@ -311,7 +307,7 @@ function () use ($property, $schema, $mergedPropertySchema): void {
311307
*/
312308
protected function setCurrentClassPath(string $jsonSchemaFile): void
313309
{
314-
$path = str_replace($this->baseSource, '', dirname($jsonSchemaFile));
310+
$path = str_replace($this->schemaProvider->getBaseDirectory(), '', dirname($jsonSchemaFile));
315311
$pieces = array_map(
316312
static fn(string $directory): string => ucfirst(preg_replace('/\W/', '', $directory)),
317313
explode(DIRECTORY_SEPARATOR, $path),
@@ -340,6 +336,11 @@ public function getGeneratorConfiguration(): GeneratorConfiguration
340336
return $this->generatorConfiguration;
341337
}
342338

339+
public function getSchemaProvider(): SchemaProviderInterface
340+
{
341+
return $this->schemaProvider;
342+
}
343+
343344
private function getTargetFileName(string $classPath, string $className): string
344345
{
345346
return join(

src/SchemaProvider/OpenAPIv3Provider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
*/
1515
class OpenAPIv3Provider implements SchemaProviderInterface
1616
{
17+
use RefResolverTrait;
18+
1719
/** @var array */
1820
private $openAPIv3Spec;
1921

src/SchemaProvider/RecursiveDirectoryProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
class RecursiveDirectoryProvider implements SchemaProviderInterface
2020
{
21+
use RefResolverTrait;
22+
2123
private string $sourceDirectory;
2224

2325
/**
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PHPModelGenerator\SchemaProvider;
6+
7+
use PHPModelGenerator\Exception\SchemaException;
8+
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
9+
10+
trait RefResolverTrait
11+
{
12+
public function getRef(string $currentFile, ?string $id, string $ref): JsonSchema
13+
{
14+
$jsonSchemaFilePath = $this->getFullRefURL($id ?? $currentFile, $ref)
15+
?: $this->getLocalRefPath($currentFile, $ref);
16+
17+
if ($jsonSchemaFilePath === null || !($jsonSchema = file_get_contents($jsonSchemaFilePath))) {
18+
throw new SchemaException("Reference to non existing JSON-Schema file $ref");
19+
}
20+
21+
if (!($decodedJsonSchema = json_decode($jsonSchema, true))) {
22+
throw new SchemaException("Invalid JSON-Schema file $jsonSchemaFilePath");
23+
}
24+
25+
return new JsonSchema($jsonSchemaFilePath, $decodedJsonSchema);
26+
}
27+
28+
/**
29+
* Try to build a full URL to fetch the schema from utilizing the $id field of the schema
30+
*/
31+
private function getFullRefURL(string $id, string $ref): ?string
32+
{
33+
if (filter_var($ref, FILTER_VALIDATE_URL)) {
34+
return $ref;
35+
}
36+
37+
if (!filter_var($id, FILTER_VALIDATE_URL) || ($idURL = parse_url($id)) === false) {
38+
return null;
39+
}
40+
41+
$baseURL = $idURL['scheme'] . '://' . $idURL['host'] . (isset($idURL['port']) ? ':' . $idURL['port'] : '');
42+
43+
// root relative $ref
44+
if (str_starts_with($ref, '/')) {
45+
return $baseURL . $ref;
46+
}
47+
48+
// relative $ref against the path of $id
49+
$segments = explode('/', rtrim(dirname($idURL['path'] ?? '/'), '/') . '/' . $ref);
50+
$output = [];
51+
52+
foreach ($segments as $seg) {
53+
if ($seg === '' || $seg === '.') {
54+
continue;
55+
}
56+
if ($seg === '..') {
57+
array_pop($output);
58+
continue;
59+
}
60+
$output[] = $seg;
61+
}
62+
63+
return $baseURL . '/' . implode('/', $output);
64+
}
65+
66+
private function getLocalRefPath(string $currentFile, string $ref): ?string
67+
{
68+
$currentDir = dirname($currentFile);
69+
// windows compatibility
70+
$jsonSchemaFile = str_replace('\\', '/', $ref);
71+
72+
// relative paths to the current location
73+
if (!str_starts_with($jsonSchemaFile, '/')) {
74+
$candidate = $currentDir . '/' . $jsonSchemaFile;
75+
76+
return file_exists($candidate) ? $candidate : null;
77+
}
78+
79+
// absolute paths: traverse up to find the context root directory
80+
$relative = ltrim($jsonSchemaFile, '/');
81+
82+
$dir = $currentDir;
83+
while (true) {
84+
$candidate = $dir . '/' . $relative;
85+
if (file_exists($candidate)) {
86+
return $candidate;
87+
}
88+
89+
$parent = dirname($dir);
90+
if ($parent === $dir) {
91+
break;
92+
}
93+
$dir = $parent;
94+
}
95+
96+
return null;
97+
}
98+
}

src/SchemaProvider/SchemaProviderInterface.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,15 @@ public function getSchemas(): iterable;
2525
* Get the base directory of the provider
2626
*/
2727
public function getBaseDirectory(): string;
28+
29+
/**
30+
* Load the content of a referenced file. You may include the RefResolverTrait which tries local and URL loading.
31+
* If your referenced files are not easily accessible, e.g. behind a login, you need to implement the lookup yourself.
32+
* The JsonSchema object must contain the whole referenced schema.
33+
*
34+
* @param string $currentFile The file containing the reference
35+
* @param string|null $id If present, the $id field of the
36+
* @param string $ref The $ref which should be resolved (without anchor part, anchors are resolved internally)
37+
*/
38+
public function getRef(string $currentFile, ?string $id, string $ref): JsonSchema;
2839
}

tests/PropertyProcessor/PropertyProcessorFactoryTest.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use PHPModelGenerator\PropertyProcessor\PropertyProcessorFactory;
2020
use PHPModelGenerator\SchemaProcessor\RenderQueue;
2121
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
22+
use PHPModelGenerator\SchemaProvider\RecursiveDirectoryProvider;
2223
use PHPUnit\Framework\TestCase;
2324

2425
/**
@@ -40,7 +41,12 @@ public function testGetPropertyProcessor(string $type, string $expectedClass): v
4041
$propertyProcessor = $propertyProcessorFactory->getProcessor(
4142
$type,
4243
new PropertyMetaDataCollection(),
43-
new SchemaProcessor('', '', new GeneratorConfiguration(), new RenderQueue()),
44+
new SchemaProcessor(
45+
new RecursiveDirectoryProvider(__DIR__),
46+
'',
47+
new GeneratorConfiguration(),
48+
new RenderQueue(),
49+
),
4450
new Schema('', '', '', new JsonSchema('', [])),
4551
);
4652

@@ -76,7 +82,12 @@ public function testGetInvalidPropertyProcessorThrowsAnException(): void
7682
$propertyProcessorFactory->getProcessor(
7783
'Hello',
7884
new PropertyMetaDataCollection(),
79-
new SchemaProcessor('', '', new GeneratorConfiguration(), new RenderQueue()),
85+
new SchemaProcessor(
86+
new RecursiveDirectoryProvider(__DIR__),
87+
'',
88+
new GeneratorConfiguration(),
89+
new RenderQueue(),
90+
),
8091
new Schema('', '', '', new JsonSchema('', [])),
8192
);
8293
}

0 commit comments

Comments
 (0)