Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Model/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public function __construct(
protected bool $initialClass = false,
) {
$this->jsonSchema = $schema;
$this->schemaDefinitionDictionary = $dictionary ?? new SchemaDefinitionDictionary('');
$this->schemaDefinitionDictionary = $dictionary ?? new SchemaDefinitionDictionary($schema);
$this->description = $schema->getJson()['description'] ?? '';

$this->addInterface(JSONModelInterface::class);
Expand Down
26 changes: 9 additions & 17 deletions src/Model/SchemaDefinition/SchemaDefinitionDictionary.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class SchemaDefinitionDictionary extends ArrayObject
/**
* SchemaDefinitionDictionary constructor.
*/
public function __construct(private string $sourceDirectory)
public function __construct(private JsonSchema $schema)
{
parent::__construct();
}
Expand Down Expand Up @@ -129,33 +129,25 @@ public function getDefinition(string $key, SchemaProcessor $schemaProcessor, arr
/**
* @throws SchemaException
*/
protected function parseExternalFile(
private function parseExternalFile(
string $jsonSchemaFile,
string $externalKey,
SchemaProcessor $schemaProcessor,
array &$path,
): ?SchemaDefinition {
$jsonSchemaFilePath = filter_var($jsonSchemaFile, FILTER_VALIDATE_URL)
? $jsonSchemaFile
: $this->sourceDirectory . '/' . $jsonSchemaFile;

if (!filter_var($jsonSchemaFilePath, FILTER_VALIDATE_URL) && !is_file($jsonSchemaFilePath)) {
throw new SchemaException("Reference to non existing JSON-Schema file $jsonSchemaFilePath");
}

$jsonSchema = file_get_contents($jsonSchemaFilePath);

if (!$jsonSchema || !($decodedJsonSchema = json_decode($jsonSchema, true))) {
throw new SchemaException("Invalid JSON-Schema file $jsonSchemaFilePath");
}
$jsonSchema = $schemaProcessor->getSchemaProvider()->getRef(
$this->schema->getFile(),
$this->schema->getJson()['$id'] ?? null,
$jsonSchemaFile,
);

// set up a dummy schema to fetch the definitions from the external file
$schema = new Schema(
'',
$schemaProcessor->getCurrentClassPath(),
'ExternalSchema',
new JsonSchema($jsonSchemaFilePath, $decodedJsonSchema),
new self(dirname($jsonSchemaFilePath)),
$jsonSchema,
new self($jsonSchema),
);

$schema->getSchemaDictionary()->setUpDefinitionDictionary($schemaProcessor, $schema);
Expand Down
2 changes: 1 addition & 1 deletion src/ModelGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public function generateModels(SchemaProviderInterface $schemaProvider, string $

$renderQueue = new RenderQueue();
$schemaProcessor = new SchemaProcessor(
$schemaProvider->getBaseDirectory(),
$schemaProvider,
$destination,
$this->generatorConfiguration,
$renderQueue,
Expand Down
5 changes: 3 additions & 2 deletions src/SchemaProcessor/PostProcessor/EnumPostProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ public function process(Schema $schema, GeneratorConfiguration $generatorConfigu
$this->checkForExistingTransformingFilter($property);

$values = $json['enum'];
$enumSignature = ArrayHash::hash($json, ['enum', 'enum-map', '$id']);
$enumName = $json['$id'] ?? $schema->getClassName() . ucfirst($property->getName());
$enumSignature = ArrayHash::hash($json, ['enum', 'enum-map', 'title', '$id']);
$enumName = $json['title']
?? basename($json['$id'] ?? $schema->getClassName() . ucfirst($property->getName()));

if (!isset($this->generatedEnums[$enumSignature])) {
$this->generatedEnums[$enumSignature] = [
Expand Down
27 changes: 14 additions & 13 deletions src/SchemaProcessor/SchemaProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use PHPModelGenerator\PropertyProcessor\PropertyMetaDataCollection;
use PHPModelGenerator\PropertyProcessor\PropertyFactory;
use PHPModelGenerator\PropertyProcessor\PropertyProcessorFactory;
use PHPModelGenerator\SchemaProvider\SchemaProviderInterface;

/**
* Class SchemaProcessor
Expand All @@ -28,23 +29,18 @@
*/
class SchemaProcessor
{
/** @var string */
protected $currentClassPath;
/** @var string */
protected $currentClassName;
protected string $currentClassPath;
protected string $currentClassName;

/** @var Schema[] Collect processed schemas to avoid duplicated classes */
protected $processedSchema = [];
protected array $processedSchema = [];
/** @var PropertyInterface[] Collect processed schemas to avoid duplicated classes */
protected $processedMergedProperties = [];
protected array $processedMergedProperties = [];
/** @var string[] */
protected $generatedFiles = [];
protected array $generatedFiles = [];

/**
* SchemaProcessor constructor.
*/
public function __construct(
protected string $baseSource,
protected SchemaProviderInterface $schemaProvider,
protected string $destination,
protected GeneratorConfiguration $generatorConfiguration,
protected RenderQueue $renderQueue,
Expand All @@ -68,7 +64,7 @@ public function process(JsonSchema $jsonSchema): void
$jsonSchema,
$this->currentClassPath,
$this->currentClassName,
new SchemaDefinitionDictionary(dirname($jsonSchema->getFile())),
new SchemaDefinitionDictionary($jsonSchema),
true,
);
}
Expand Down Expand Up @@ -311,7 +307,7 @@ function () use ($property, $schema, $mergedPropertySchema): void {
*/
protected function setCurrentClassPath(string $jsonSchemaFile): void
{
$path = str_replace($this->baseSource, '', dirname($jsonSchemaFile));
$path = str_replace($this->schemaProvider->getBaseDirectory(), '', dirname($jsonSchemaFile));
$pieces = array_map(
static fn(string $directory): string => ucfirst(preg_replace('/\W/', '', $directory)),
explode(DIRECTORY_SEPARATOR, $path),
Expand Down Expand Up @@ -340,6 +336,11 @@ public function getGeneratorConfiguration(): GeneratorConfiguration
return $this->generatorConfiguration;
}

public function getSchemaProvider(): SchemaProviderInterface
{
return $this->schemaProvider;
}

private function getTargetFileName(string $classPath, string $className): string
{
return join(
Expand Down
2 changes: 2 additions & 0 deletions src/SchemaProvider/OpenAPIv3Provider.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
*/
class OpenAPIv3Provider implements SchemaProviderInterface
{
use RefResolverTrait;

/** @var array */
private $openAPIv3Spec;

Expand Down
2 changes: 2 additions & 0 deletions src/SchemaProvider/RecursiveDirectoryProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*/
class RecursiveDirectoryProvider implements SchemaProviderInterface
{
use RefResolverTrait;

private string $sourceDirectory;

/**
Expand Down
98 changes: 98 additions & 0 deletions src/SchemaProvider/RefResolverTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types=1);

namespace PHPModelGenerator\SchemaProvider;

use PHPModelGenerator\Exception\SchemaException;
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;

trait RefResolverTrait
{
public function getRef(string $currentFile, ?string $id, string $ref): JsonSchema
{
$jsonSchemaFilePath = $this->getFullRefURL($id ?? $currentFile, $ref)
?: $this->getLocalRefPath($currentFile, $ref);

if ($jsonSchemaFilePath === null || !($jsonSchema = file_get_contents($jsonSchemaFilePath))) {
throw new SchemaException("Reference to non existing JSON-Schema file $ref");
}

if (!($decodedJsonSchema = json_decode($jsonSchema, true))) {
throw new SchemaException("Invalid JSON-Schema file $jsonSchemaFilePath");
}

return new JsonSchema($jsonSchemaFilePath, $decodedJsonSchema);
}

/**
* Try to build a full URL to fetch the schema from utilizing the $id field of the schema
*/
private function getFullRefURL(string $id, string $ref): ?string
{
if (filter_var($ref, FILTER_VALIDATE_URL)) {
return $ref;
}

if (!filter_var($id, FILTER_VALIDATE_URL) || ($idURL = parse_url($id)) === false) {
return null;
}

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

// root relative $ref
if (str_starts_with($ref, '/')) {
return $baseURL . $ref;
}

// relative $ref against the path of $id
$segments = explode('/', rtrim(dirname($idURL['path'] ?? '/'), '/') . '/' . $ref);
$output = [];

foreach ($segments as $seg) {
if ($seg === '' || $seg === '.') {
continue;
}
if ($seg === '..') {
array_pop($output);
continue;
}
$output[] = $seg;
}

return $baseURL . '/' . implode('/', $output);
}

private function getLocalRefPath(string $currentFile, string $ref): ?string
{
$currentDir = dirname($currentFile);
// windows compatibility
$jsonSchemaFile = str_replace('\\', '/', $ref);

// relative paths to the current location
if (!str_starts_with($jsonSchemaFile, '/')) {
$candidate = $currentDir . '/' . $jsonSchemaFile;

return file_exists($candidate) ? $candidate : null;
}

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

$dir = $currentDir;
while (true) {
$candidate = $dir . '/' . $relative;
if (file_exists($candidate)) {
return $candidate;
}

$parent = dirname($dir);
if ($parent === $dir) {
break;
}
$dir = $parent;
}

return null;
}
}
11 changes: 11 additions & 0 deletions src/SchemaProvider/SchemaProviderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,15 @@ public function getSchemas(): iterable;
* Get the base directory of the provider
*/
public function getBaseDirectory(): string;

/**
* Load the content of a referenced file. You may include the RefResolverTrait which tries local and URL loading.
* If your referenced files are not easily accessible, e.g. behind a login, you need to implement the lookup yourself.
* The JsonSchema object must contain the whole referenced schema.
*
* @param string $currentFile The file containing the reference
* @param string|null $id If present, the $id field of the
* @param string $ref The $ref which should be resolved (without anchor part, anchors are resolved internally)
*/
public function getRef(string $currentFile, ?string $id, string $ref): JsonSchema;
}
10 changes: 5 additions & 5 deletions src/Utils/ClassNameGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ public function getClassName(
$className = sprintf(
$isMergeClass ? '%s_Merged_%s' : '%s_%s',
$currentClassName,
ucfirst(
isset($json['$id'])
? str_replace('#', '', $json['$id'])
: ($propertyName . ($currentClassName ? md5(json_encode($json)) : '')),
)
ucfirst(match(true) {
isset($json['title']) => $json['title'],
isset($json['$id']) => basename($json['$id']),
default => ($propertyName . ($currentClassName ? md5(json_encode($json)) : '')),
}),
);

return ucfirst(preg_replace('/\W/', '', trim($className, '_')));
Expand Down
6 changes: 3 additions & 3 deletions tests/AbstractPHPModelGeneratorTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ protected function generateClass(
$className = $this->getClassName();

if (!$originalClassNames) {
// extend the class name generator to attach a uniqid as multiple test executions use identical $id
// extend the class name generator to attach a uniqid as multiple test executions use identical title
// properties which would lead to name collisions
$generatorConfiguration->setClassNameGenerator(new class extends ClassNameGenerator {
public function getClassName(
Expand All @@ -221,12 +221,12 @@ public function getClassName(
// generate an object ID for valid JSON schema files to avoid class name collisions in the testing process
$jsonSchemaArray = json_decode($jsonSchema, true);
if ($jsonSchemaArray) {
$jsonSchemaArray['$id'] = $className;
$jsonSchemaArray['title'] = $className;

if (isset($jsonSchemaArray['components']['schemas'])) {
$counter = 0;
foreach ($jsonSchemaArray['components']['schemas'] as &$schema) {
$schema['$id'] = $className . '_' . $counter++;
$schema['title'] = $className . '_' . $counter++;
}
}

Expand Down
28 changes: 24 additions & 4 deletions tests/Objects/ReferencePropertyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -395,9 +395,9 @@ public function invalidCombinedReferenceObjectPropertyTypeDataProvider(): array
* @throws RenderException
* @throws SchemaException
*/
public function testNestedExternalReference(string $reference): void
public function testNestedExternalReference(string $id, string $reference): void
{
$className = $this->generateClassFromFileTemplate('NestedExternalReference.json', [$reference]);
$className = $this->generateClassFromFileTemplate('NestedExternalReference.json', [$id, $reference]);

$object = new $className([
'family' => [
Expand Down Expand Up @@ -426,9 +426,29 @@ public function testNestedExternalReference(string $reference): void

public function nestedReferenceProvider(): array
{
$baseURL = 'https://raw.githubusercontent.com/wol-soft/php-json-schema-model-generator/master/tests/Schema/';

return [
'Local reference' => ['../ReferencePropertyTest_external/library.json'],
'Network reference' => ['https://raw.githubusercontent.com/wol-soft/php-json-schema-model-generator/master/tests/Schema/ReferencePropertyTest_external/library.json'],
'local reference - relative' => [
'NestedExternalReference.json',
'../ReferencePropertyTest_external/library.json',
],
'local reference - context absolute' => [
'NestedExternalReference.json',
'/ReferencePropertyTest_external/library.json',
],
'network reference - full URL' => [
'NestedExternalReference.json',
$baseURL . 'ReferencePropertyTest_external/library.json',
],
'network reference - relative path to full URL $id' => [
$baseURL . 'ReferencePropertyTest/NestedExternalReference.json',
'../ReferencePropertyTest_external/library.json',
],
'network reference - absolute path to full URL $id' => [
$baseURL . 'ReferencePropertyTest/NestedExternalReference.json',
'/wol-soft/php-json-schema-model-generator/master/tests/Schema/ReferencePropertyTest_external/library.json',
],
];
}

Expand Down
Loading