Skip to content

Commit 320c48c

Browse files
committed
Fix JSON Schema generation for non-resource class
1 parent 9d2019b commit 320c48c

File tree

4 files changed

+84
-36
lines changed

4 files changed

+84
-36
lines changed

SchemaFactory.php

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@
1414
namespace ApiPlatform\Core\JsonSchema;
1515

1616
use ApiPlatform\Core\Api\OperationType;
17+
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
1718
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
1819
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
1920
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
2021
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
2122
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
2223
use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer;
24+
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
2325
use Symfony\Component\PropertyInfo\Type;
2426
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
2527
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
@@ -33,20 +35,22 @@
3335
*/
3436
final class SchemaFactory implements SchemaFactoryInterface
3537
{
36-
private $resourceMetadataFactory;
38+
use ResourceClassInfoTrait;
39+
40+
private $typeFactory;
3741
private $propertyNameCollectionFactory;
3842
private $propertyMetadataFactory;
39-
private $typeFactory;
4043
private $nameConverter;
4144
private $distinctFormats = [];
4245

43-
public function __construct(TypeFactoryInterface $typeFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null)
46+
public function __construct(TypeFactoryInterface $typeFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null, ResourceClassResolverInterface $resourceClassResolver = null)
4447
{
48+
$this->typeFactory = $typeFactory;
4549
$this->resourceMetadataFactory = $resourceMetadataFactory;
4650
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
4751
$this->propertyMetadataFactory = $propertyMetadataFactory;
4852
$this->nameConverter = $nameConverter;
49-
$this->typeFactory = $typeFactory;
53+
$this->resourceClassResolver = $resourceClassResolver;
5054
}
5155

5256
/**
@@ -62,16 +66,20 @@ public function addDistinctFormat(string $format): void
6266
/**
6367
* {@inheritdoc}
6468
*/
65-
public function buildSchema(string $resourceClass, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
69+
public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema
6670
{
6771
$schema = $schema ?? new Schema();
68-
if (null === $metadata = $this->getMetadata($resourceClass, $type, $operationType, $operationName, $serializerContext)) {
72+
if (null === $metadata = $this->getMetadata($className, $type, $operationType, $operationName, $serializerContext)) {
6973
return $schema;
7074
}
7175
[$resourceMetadata, $serializerContext, $inputOrOutputClass] = $metadata;
7276

77+
if (null === $resourceMetadata && (null !== $operationType || null !== $operationName)) {
78+
throw new \LogicException('The $operationType and $operationName arguments must be null for non-resource class.');
79+
}
80+
7381
$version = $schema->getVersion();
74-
$definitionName = $this->buildDefinitionName($resourceClass, $format, $type, $operationType, $operationName, $serializerContext);
82+
$definitionName = $this->buildDefinitionName($className, $format, $type, $operationType, $operationName, $serializerContext);
7583

7684
if (null === $operationType || null === $operationName) {
7785
$method = Schema::TYPE_INPUT === $type ? 'POST' : 'GET';
@@ -103,12 +111,13 @@ public function buildSchema(string $resourceClass, string $format = 'json', stri
103111

104112
$definition = new \ArrayObject(['type' => 'object']);
105113
$definitions[$definitionName] = $definition;
106-
if (null !== $description = $resourceMetadata->getDescription()) {
114+
if (null !== $resourceMetadata && null !== $description = $resourceMetadata->getDescription()) {
107115
$definition['description'] = $description;
108116
}
109117
// see https://github.com/json-schema-org/json-schema-spec/pull/737
110118
if (
111119
Schema::VERSION_SWAGGER !== $version &&
120+
null !== $resourceMetadata &&
112121
(
113122
(null !== $operationType && null !== $operationName && null !== $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) ||
114123
null !== $resourceMetadata->getAttribute('deprecation_reason', null)
@@ -118,7 +127,7 @@ public function buildSchema(string $resourceClass, string $format = 'json', stri
118127
}
119128
// externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it
120129
// See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4
121-
if (null !== $iri = $resourceMetadata->getIri()) {
130+
if (null !== $resourceMetadata && null !== $iri = $resourceMetadata->getIri()) {
122131
$definition['externalDocs'] = ['url' => $iri];
123132
}
124133

@@ -200,12 +209,12 @@ private function buildPropertySchema(Schema $schema, string $definitionName, str
200209
$schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema;
201210
}
202211

203-
private function buildDefinitionName(string $resourceClass, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?array $serializerContext = null): string
212+
private function buildDefinitionName(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?array $serializerContext = null): string
204213
{
205-
[$resourceMetadata, $serializerContext, $inputOrOutputClass] = $this->getMetadata($resourceClass, $type, $operationType, $operationName, $serializerContext);
214+
[$resourceMetadata, $serializerContext, $inputOrOutputClass] = $this->getMetadata($className, $type, $operationType, $operationName, $serializerContext);
206215

207-
$prefix = $resourceMetadata->getShortName();
208-
if (null !== $inputOrOutputClass && $resourceClass !== $inputOrOutputClass) {
216+
$prefix = $resourceMetadata ? $resourceMetadata->getShortName() : (new \ReflectionClass($className))->getShortName();
217+
if (null !== $inputOrOutputClass && $className !== $inputOrOutputClass) {
209218
$prefix .= ':'.md5($inputOrOutputClass);
210219
}
211220

@@ -224,14 +233,22 @@ private function buildDefinitionName(string $resourceClass, string $format = 'js
224233
return $name;
225234
}
226235

227-
private function getMetadata(string $resourceClass, string $type = Schema::TYPE_OUTPUT, ?string $operationType, ?string $operationName, ?array $serializerContext): ?array
236+
private function getMetadata(string $className, string $type = Schema::TYPE_OUTPUT, ?string $operationType, ?string $operationName, ?array $serializerContext): ?array
228237
{
229-
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
238+
if (!$this->isResourceClass($className)) {
239+
return [
240+
null,
241+
$serializerContext,
242+
$className,
243+
];
244+
}
245+
246+
$resourceMetadata = $this->resourceMetadataFactory->create($className);
230247
$attribute = Schema::TYPE_OUTPUT === $type ? 'output' : 'input';
231248
if (null === $operationType || null === $operationName) {
232-
$inputOrOutput = $resourceMetadata->getAttribute($attribute, ['class' => $resourceClass]);
249+
$inputOrOutput = $resourceMetadata->getAttribute($attribute, ['class' => $className]);
233250
} else {
234-
$inputOrOutput = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, ['class' => $resourceClass], true);
251+
$inputOrOutput = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, ['class' => $className], true);
235252
}
236253

237254
if (null === ($inputOrOutput['class'] ?? null)) {

SchemaFactoryInterface.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,8 @@
1313

1414
namespace ApiPlatform\Core\JsonSchema;
1515

16-
use ApiPlatform\Core\Exception\ResourceClassNotFoundException;
17-
1816
/**
19-
* Builds a JSON Schema from an API Platform resource definition.
17+
* Factory for creating the JSON Schema document corresponding to a PHP class.
2018
*
2119
* @experimental
2220
*
@@ -25,7 +23,7 @@
2523
interface SchemaFactoryInterface
2624
{
2725
/**
28-
* @throws ResourceClassNotFoundException
26+
* Builds the JSON Schema document corresponding to the given PHP class.
2927
*/
30-
public function buildSchema(string $resourceClass, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema;
28+
public function buildSchema(string $className, string $format = 'json', string $type = Schema::TYPE_OUTPUT, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema;
3129
}

TypeFactory.php

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313

1414
namespace ApiPlatform\Core\JsonSchema;
1515

16+
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
17+
use ApiPlatform\Core\Util\ResourceClassInfoTrait;
18+
use Ramsey\Uuid\UuidInterface;
1619
use Symfony\Component\PropertyInfo\Type;
1720

1821
/**
@@ -24,21 +27,25 @@
2427
*/
2528
final class TypeFactory implements TypeFactoryInterface
2629
{
30+
use ResourceClassInfoTrait;
31+
2732
/**
2833
* @var SchemaFactoryInterface|null
2934
*/
3035
private $schemaFactory;
3136

32-
/**
33-
* Injects the JSON Schema factory to use.
34-
*/
37+
public function __construct(ResourceClassResolverInterface $resourceClassResolver = null)
38+
{
39+
$this->resourceClassResolver = $resourceClassResolver;
40+
}
41+
3542
public function setSchemaFactory(SchemaFactoryInterface $schemaFactory): void
3643
{
3744
$this->schemaFactory = $schemaFactory;
3845
}
3946

4047
/**
41-
* Gets the OpenAPI type corresponding to the given PHP type, and recursively adds needed new schema to the current schema if provided.
48+
* {@inheritdoc}
4249
*/
4350
public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, Schema $schema = null): array
4451
{
@@ -66,7 +73,7 @@ public function getType(Type $type, string $format = 'json', ?bool $readableLink
6673
}
6774

6875
/**
69-
* Gets the OpenAPI type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided.
76+
* Gets the JSON Schema document which specifies the data type corresponding to the given PHP class, and recursively adds needed new schema to the current schema if provided.
7077
*/
7178
private function getClassType(?string $className, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, ?Schema $schema = null): array
7279
{
@@ -75,20 +82,41 @@ private function getClassType(?string $className, string $format = 'json', ?bool
7582
}
7683

7784
if (is_a($className, \DateTimeInterface::class, true)) {
78-
return ['type' => 'string', 'format' => 'date-time'];
85+
return [
86+
'type' => 'string',
87+
'format' => 'date-time',
88+
];
89+
}
90+
if (is_a($className, UuidInterface::class, true)) {
91+
return [
92+
'type' => 'string',
93+
'format' => 'uuid',
94+
];
7995
}
8096

81-
if (null !== $this->schemaFactory && true === $readableLink && null !== $schema) { // Skip if $baseSchema is null (filters only support basic types)
82-
$version = $schema->getVersion();
97+
// Skip if $schema is null (filters only support basic types)
98+
if (null === $schema) {
99+
return ['type' => 'string'];
100+
}
83101

84-
$subSchema = new Schema($version);
85-
$subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema
102+
if ($this->isResourceClass($className) && true !== $readableLink) {
103+
return [
104+
'type' => 'string',
105+
'format' => 'iri-reference',
106+
];
107+
}
86108

87-
$this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, null, $subSchema, $serializerContext);
109+
$version = $schema->getVersion();
88110

89-
return ['$ref' => $subSchema['$ref']];
111+
$subSchema = new Schema($version);
112+
$subSchema->setDefinitions($schema->getDefinitions()); // Populate definitions of the main schema
113+
114+
if (null === $this->schemaFactory) {
115+
throw new \LogicException('The schema factory must be injected by calling the "setSchemaFactory" method.');
90116
}
91117

92-
return ['type' => 'string'];
118+
$subSchema = $this->schemaFactory->buildSchema($className, $format, Schema::TYPE_OUTPUT, null, null, $subSchema, $serializerContext);
119+
120+
return ['$ref' => $subSchema['$ref']];
93121
}
94122
}

TypeFactoryInterface.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,16 @@
1616
use Symfony\Component\PropertyInfo\Type;
1717

1818
/**
19-
* Gets the OpenAPI type corresponding to the given PHP type, and recursively adds needed new schema to the current schema if provided.
19+
* Factory for creating the JSON Schema document which specifies the data type corresponding to a PHP type.
20+
*
21+
* @experimental
2022
*
2123
* @author Kévin Dunglas <[email protected]>
2224
*/
2325
interface TypeFactoryInterface
2426
{
27+
/**
28+
* Gets the JSON Schema document which specifies the data type corresponding to the given PHP type, and recursively adds needed new schema to the current schema if provided.
29+
*/
2530
public function getType(Type $type, string $format = 'json', ?bool $readableLink = null, ?array $serializerContext = null, Schema $schema = null): array;
2631
}

0 commit comments

Comments
 (0)