Skip to content

Commit d5880d2

Browse files
committed
Implement external Schema $ref resolving
1 parent c622865 commit d5880d2

File tree

13 files changed

+329
-87
lines changed

13 files changed

+329
-87
lines changed

src/Generator.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public function generateModels(string $source, string $destination): array
6363
$schemaProcessor = new SchemaProcessor($source, $destination, $this->generatorConfiguration, $renderProxy);
6464

6565
foreach ($this->getSchemaFiles($source) as $jsonSchemaFile) {
66-
$schemaProcessor->process($jsonSchemaFile);
66+
$schemaProcessor->process($jsonSchemaFile, $source);
6767
}
6868

6969
// render all collected classes

src/Model/RenderJob.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ public function render(string $destination, GeneratorConfiguration $generatorCon
5555
if (!file_put_contents($this->fileName, $class)) {
5656
// @codeCoverageIgnoreStart
5757
throw new FileSystemException("Can't write class $this->classPath\\$this->className");
58-
// @codeCoverageIgnoreEno
58+
// @codeCoverageIgnoreEnd
5959
}
6060

6161
if ($generatorConfiguration->isOutputEnabled()) {
6262
// @codeCoverageIgnoreStart
6363
echo "Rendered class $this->className\n";
64-
// @codeCoverageIgnoreEno
64+
// @codeCoverageIgnoreEnd
6565
}
6666
}
6767

src/Model/SchemaDefinition/SchemaDefinitionDictionary.php

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace PHPModelGenerator\Model\SchemaDefinition;
66

77
use ArrayObject;
8+
use PHPModelGenerator\Exception\SchemaException;
89
use PHPModelGenerator\Model\Schema;
910
use PHPModelGenerator\SchemaProcessor\SchemaProcessor;
1011

@@ -15,15 +16,35 @@
1516
*/
1617
class SchemaDefinitionDictionary extends ArrayObject
1718
{
19+
/** @var string */
20+
private $sourceDirectory;
21+
/** @var Schema[] */
22+
private $parsedExternalFileSchemas = [];
23+
24+
/**
25+
* SchemaDefinitionDictionary constructor.
26+
*
27+
* @param string $sourceDirectory
28+
*/
29+
public function __construct(string $sourceDirectory)
30+
{
31+
parent::__construct();
32+
33+
$this->sourceDirectory = $sourceDirectory;
34+
}
35+
1836
/**
1937
* Set up the definition directory for the schema
2038
*
2139
* @param array $propertyData
2240
* @param SchemaProcessor $schemaProcessor
2341
* @param Schema $schema
2442
*/
25-
public function setUpDefinitionDictionary(array $propertyData, SchemaProcessor $schemaProcessor, Schema $schema)
26-
{
43+
public function setUpDefinitionDictionary(
44+
array $propertyData,
45+
SchemaProcessor $schemaProcessor,
46+
Schema $schema
47+
): void {
2748
foreach ($propertyData as $key => $propertyEntry) {
2849
if (!is_array($propertyEntry)) {
2950
continue;
@@ -43,7 +64,7 @@ public function setUpDefinitionDictionary(array $propertyData, SchemaProcessor $
4364
* @param SchemaProcessor $schemaProcessor
4465
* @param Schema $schema
4566
*/
46-
protected function fetchDefinitionsById(array $propertyData, SchemaProcessor $schemaProcessor, Schema $schema)
67+
protected function fetchDefinitionsById(array $propertyData, SchemaProcessor $schemaProcessor, Schema $schema): void
4768
{
4869
if (isset($propertyData['$id'])) {
4970
$this->addDefinition($propertyData['$id'], new SchemaDefinition($propertyData, $schemaProcessor, $schema));
@@ -78,12 +99,72 @@ public function addDefinition(string $key, SchemaDefinition $definition): self
7899
}
79100

80101
/**
81-
* @param string $key
102+
* @param string $key
103+
* @param SchemaProcessor $schemaProcessor
104+
* @param array $path
82105
*
83106
* @return SchemaDefinition
107+
*
108+
* @throws SchemaException
84109
*/
85-
public function getDefinition(string $key): ?SchemaDefinition
110+
public function getDefinition(string $key, SchemaProcessor $schemaProcessor, array &$path = []): ?SchemaDefinition
86111
{
112+
if (strpos($key, '#') === 0 && strpos($key, '/')) {
113+
$path = explode('/', $key);
114+
array_shift($path);
115+
$key = array_shift($path);
116+
}
117+
118+
if (!isset($this[$key]) && strstr($key, '#', true)) {
119+
[$jsonSchemaFile, $externalKey] = explode('#', $key);
120+
$jsonSchemaFile = $this->sourceDirectory . DIRECTORY_SEPARATOR . $jsonSchemaFile;
121+
122+
if (array_key_exists($jsonSchemaFile, $this->parsedExternalFileSchemas)) {
123+
return $this->parsedExternalFileSchemas[$jsonSchemaFile]->getSchemaDictionary()->getDefinition(
124+
"#$externalKey",
125+
$schemaProcessor,
126+
$path
127+
);
128+
}
129+
130+
return $jsonSchemaFile
131+
? $this->parseExternalFile($jsonSchemaFile, "#$externalKey", $schemaProcessor, $path)
132+
: null;
133+
}
134+
87135
return $this[$key] ?? null;
88136
}
137+
138+
/**
139+
* @param string $jsonSchemaFile
140+
* @param string $externalKey
141+
* @param SchemaProcessor $schemaProcessor
142+
* @param array $path
143+
*
144+
* @return SchemaDefinition|null
145+
*
146+
* @throws SchemaException
147+
*/
148+
protected function parseExternalFile(
149+
string $jsonSchemaFile,
150+
string $externalKey,
151+
SchemaProcessor $schemaProcessor,
152+
array &$path
153+
): ?SchemaDefinition {
154+
if (!is_file($jsonSchemaFile)) {
155+
return null;
156+
}
157+
158+
$jsonSchema = file_get_contents($jsonSchemaFile);
159+
160+
if (!$jsonSchema || !($jsonSchema = json_decode($jsonSchema, true))) {
161+
throw new SchemaException("Invalid JSON-Schema file $jsonSchemaFile");
162+
}
163+
164+
$schema = new Schema(new self(dirname($jsonSchemaFile)));
165+
$schema->getSchemaDictionary()->setUpDefinitionDictionary($jsonSchema, $schemaProcessor, $schema);
166+
$this->parsedExternalFileSchemas[$jsonSchemaFile] = $schema;
167+
168+
return $schema->getSchemaDictionary()->getDefinition($externalKey, $schemaProcessor, $path);
169+
}
89170
}

src/PropertyProcessor/Property/ReferenceProcessor.php

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use Exception;
88
use PHPModelGenerator\Exception\SchemaException;
99
use PHPModelGenerator\Model\Property\PropertyInterface;
10-
use PHPModelGenerator\Model\SchemaDefinition\SchemaDefinition;
1110

1211
/**
1312
* Class ConstProcessor
@@ -23,46 +22,11 @@ class ReferenceProcessor extends AbstractTypedValueProcessor
2322
*/
2423
public function process(string $propertyName, array $propertyData): PropertyInterface
2524
{
25+
$path = [];
2626
$reference = $propertyData['$ref'];
2727
$dictionary = $this->schema->getSchemaDictionary();
28+
$definition = $dictionary->getDefinition($reference, $this->schemaProcessor, $path);
2829

29-
if ($dictionary->getDefinition($reference)) {
30-
return $this->resolveDefinition($propertyName, $dictionary->getDefinition($reference), $reference);
31-
}
32-
33-
if (strpos($reference, '#') === 0 && strpos($reference, '/')) {
34-
$path = explode('/', $reference);
35-
array_shift($path);
36-
37-
return $this->resolveDefinition(
38-
$propertyName,
39-
$dictionary->getDefinition(array_shift($path)),
40-
$reference,
41-
$path
42-
);
43-
}
44-
45-
throw new SchemaException("Unresolved Reference: $reference");
46-
}
47-
48-
/**
49-
* Resolve a given definition into a Property
50-
*
51-
* @param string $propertyName
52-
* @param SchemaDefinition|null $definition
53-
* @param string $reference
54-
* @param array $path
55-
*
56-
* @return PropertyInterface
57-
*
58-
* @throws SchemaException
59-
*/
60-
protected function resolveDefinition(
61-
string $propertyName,
62-
?SchemaDefinition $definition,
63-
string $reference,
64-
array $path = []
65-
): PropertyInterface {
6630
if ($definition) {
6731
try {
6832
return $definition->resolveReference($propertyName, $path, $this->propertyCollectionProcessor);

src/SchemaProcessor/SchemaProcessor.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,11 @@ public function __construct(
6161
* Process a given json schema file
6262
*
6363
* @param string $jsonSchemaFile
64+
* @param string $schemaSourceDirectory
6465
*
6566
* @throws SchemaException
6667
*/
67-
public function process(string $jsonSchemaFile): void
68+
public function process(string $jsonSchemaFile, string $schemaSourceDirectory): void
6869
{
6970
$jsonSchema = file_get_contents($jsonSchemaFile);
7071

@@ -79,7 +80,7 @@ public function process(string $jsonSchemaFile): void
7980
$jsonSchema,
8081
$this->currentClassPath,
8182
$this->currentClassName,
82-
new SchemaDefinitionDictionary()
83+
new SchemaDefinitionDictionary($schemaSourceDirectory)
8384
);
8485
}
8586

@@ -102,13 +103,12 @@ public function processSchema(
102103
string $classPath,
103104
string $className,
104105
SchemaDefinitionDictionary $dictionary
105-
): Schema {
106+
): ?Schema {
106107
if ((!isset($jsonSchema['type']) || $jsonSchema['type'] !== 'object') &&
107108
!array_intersect(array_keys($jsonSchema), ['anyOf', 'allOf', 'oneOf'])
108109
) {
109-
throw new SchemaException(
110-
"JSON-Schema doesn't provide an object or a composition " . $jsonSchema['id'] ?? ''
111-
);
110+
// skip the JSON schema as neither an object nor a composition is defined on the root level
111+
return null;
112112
}
113113

114114
return $this->generateModel($classPath, $className, $jsonSchema, $dictionary);
@@ -154,7 +154,7 @@ protected function generateModel(
154154
if ($this->generatorConfiguration->isOutputEnabled()) {
155155
// @codeCoverageIgnoreStart
156156
echo "Generated class $className\n";
157-
// @codeCoverageIgnoreEno
157+
// @codeCoverageIgnoreEnd
158158
}
159159

160160
$this->generatedFiles[] = $fileName;

tests/AbstractPHPModelGeneratorTest.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
*/
2020
abstract class AbstractPHPModelGeneratorTest extends TestCase
2121
{
22+
protected const EXTERNAL_JSON_DIRECTORIES = [];
23+
2224
private $names = [];
2325

2426
private $generatedFiles = [];
@@ -75,6 +77,24 @@ public function tearDown(): void
7577
$this->generatedFiles = [];
7678
}
7779

80+
/**
81+
* Copy given external JSON schema files into the tmp directory to make them available during model generation
82+
*/
83+
private function copyExternalJSON(): void
84+
{
85+
$baseDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'PHPModelGeneratorTest' . DIRECTORY_SEPARATOR;
86+
$copyBaseDir = __DIR__ . "/Schema/{$this->getStaticClassName()}/";
87+
88+
foreach (static::EXTERNAL_JSON_DIRECTORIES as $directory) {
89+
$di = new RecursiveDirectoryIterator($copyBaseDir . $directory, FilesystemIterator::SKIP_DOTS);
90+
91+
foreach (new RecursiveIteratorIterator($di, RecursiveIteratorIterator::CHILD_FIRST) as $file) {
92+
@mkdir($baseDir . dirname(str_replace($copyBaseDir, '', $file)), 0777, true);
93+
@copy($file, $baseDir . str_replace($copyBaseDir, '', $file));
94+
}
95+
}
96+
}
97+
7898
/**
7999
* Generate an object from a given JSON schema file and return the FQCN
80100
*
@@ -166,6 +186,7 @@ public function generateObject(string $jsonSchema, GeneratorConfiguration $gener
166186
}
167187

168188
file_put_contents($baseDir . DIRECTORY_SEPARATOR . $className . '.json', $jsonSchema);
189+
$this->copyExternalJSON();
169190

170191
$generatedFiles = (new Generator($generatorConfiguration))->generateModels(
171192
$baseDir,

tests/Basic/BasicSchemaGenerationTest.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,6 @@ public function testInvalidJsonSchemaFileThrowsAnException(): void
9494
$this->generateObjectFromFile('InvalidJSONSchema.json');
9595
}
9696

97-
public function testJsonSchemaWithoutObjectSpecificationThrowsAnException(): void
98-
{
99-
$this->expectException(SchemaException::class);
100-
$this->expectExceptionMessageRegExp('/^JSON-Schema doesn\'t provide an object(.*)$/');
101-
102-
$this->generateObjectFromFile('JSONSchemaWithoutObject.json');
103-
}
104-
10597
public function testJsonSchemaWithInvalidPropertyTypeThrowsAnException(): void
10698
{
10799
$this->expectException(SchemaException::class);

0 commit comments

Comments
 (0)