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
7 changes: 5 additions & 2 deletions docs/source/complexTypes/object.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,15 @@ Naming
Naming of classes
^^^^^^^^^^^^^^^^^

If the given main object in a JSON-Schema file contains a `$id` the id will be used as class name. Otherwise the name of the file will be used.
If the given main object in a JSON-Schema file contains a `title`, the title will be used as class name.
Otherwise, if an `$id` is present, the basename of the $id and as a last fallback the name of the file will be used.

Naming of nested classes
^^^^^^^^^^^^^^^^^^^^^^^^

For the class name of a nested class the `$id` property of the nested object is used. If the id property isn't present the property key will be prefixed with the parent class. If an object `Person` has a nested object `car` without a `$id` the class for car will be named **Person_Car**.
For the class name of a nested class the `title` property (fallback to `$id`) of the nested object is used.
If neither the title nor the $id property is present the property key will be prefixed with the parent class.
If an object `Person` has a nested object `car` without a `title` and an `$id` the class for car will be named **Person_Car**.

Property Name Normalization
^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
10 changes: 10 additions & 0 deletions docs/source/generic/references.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,20 @@ Supported reference types
* relative reference based on the location on the file system to a complete file (example: `"$ref": "./../modules/myObject.json"`)
* relative reference based on the location on the file system to an object by id (example: `"$ref": "./../modules/myObject.json#IdOfMyObject"`)
* relative reference based on the location on the file system to an object by path (example: `"$ref": "./../modules/myObject.json#/definitions/myObject"`)
* absolute reference based on the location on the file system to a complete file (example: `"$ref": "/modules/myObject.json"`)
* absolute reference based on the location on the file system to an object by id (example: `"$ref": "/modules/myObject.json#IdOfMyObject"`)
* absolute reference based on the location on the file system to an object by path (example: `"$ref": "/modules/myObject.json#/definitions/myObject"`)
* network reference to a complete file (example: `"$ref": "https://my.domain.com/schema/modules/myObject.json"`)
* network reference to an object by id (example: `"$ref": "https://my.domain.com/schema/modules/myObject.json#IdOfMyObject"`)
* network reference to an object by path (example: `"$ref": "https://my.domain.com/schema/modules/myObject.json#/definitions/myObject"`)

If an `$id` is present in the schema, the `$ref` will be resolved relative to the `$id` (except the `$ref` already is an absolute reference, e.g. a full URL).
The behaviour of `$ref` resolving can be overwritten by implementing a custom **SchemaProviderInterface**, for example when you want to use network references behind an authorization.

.. note::

For absolute local references, the default implementation traverses up the directory tree until it finds a matching file to find the project root

Object reference
----------------

Expand Down
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
Loading