1 | <?php |
||||
2 | |||||
3 | /* |
||||
4 | * This file is part of the API Platform project. |
||||
5 | * |
||||
6 | * (c) Kévin Dunglas <[email protected]> |
||||
7 | * |
||||
8 | * For the full copyright and license information, please view the LICENSE |
||||
9 | * file that was distributed with this source code. |
||||
10 | */ |
||||
11 | |||||
12 | declare(strict_types=1); |
||||
13 | |||||
14 | namespace ApiPlatform\Core\JsonSchema; |
||||
15 | |||||
16 | use ApiPlatform\Core\Api\OperationType; |
||||
17 | use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
||||
18 | use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
||||
19 | use ApiPlatform\Core\Metadata\Property\PropertyMetadata; |
||||
20 | use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
||||
21 | use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; |
||||
22 | use ApiPlatform\Core\Swagger\Serializer\DocumentationNormalizer; |
||||
23 | use Symfony\Component\PropertyInfo\Type; |
||||
24 | use Symfony\Component\Serializer\NameConverter\NameConverterInterface; |
||||
25 | use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; |
||||
26 | |||||
27 | /** |
||||
28 | * {@inheritdoc} |
||||
29 | * |
||||
30 | * @experimental |
||||
31 | * |
||||
32 | * @author Kévin Dunglas <[email protected]> |
||||
33 | */ |
||||
34 | final class SchemaFactory implements SchemaFactoryInterface |
||||
35 | { |
||||
36 | private $resourceMetadataFactory; |
||||
37 | private $propertyNameCollectionFactory; |
||||
38 | private $propertyMetadataFactory; |
||||
39 | private $typeFactory; |
||||
40 | private $nameConverter; |
||||
41 | private $distinctFormats = []; |
||||
42 | |||||
43 | public function __construct(TypeFactoryInterface $typeFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, NameConverterInterface $nameConverter = null) |
||||
44 | { |
||||
45 | $this->resourceMetadataFactory = $resourceMetadataFactory; |
||||
46 | $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
||||
47 | $this->propertyMetadataFactory = $propertyMetadataFactory; |
||||
48 | $this->nameConverter = $nameConverter; |
||||
49 | $this->typeFactory = $typeFactory; |
||||
50 | } |
||||
51 | |||||
52 | /** |
||||
53 | * When added to the list, the given format will lead to the creation of a new definition. |
||||
54 | * |
||||
55 | * @internal |
||||
56 | */ |
||||
57 | public function addDistinctFormat(string $format): void |
||||
58 | { |
||||
59 | $this->distinctFormats[$format] = true; |
||||
60 | } |
||||
61 | |||||
62 | /** |
||||
63 | * {@inheritdoc} |
||||
64 | */ |
||||
65 | public function buildSchema(string $resourceClass, string $format = 'json', bool $output = true, ?string $operationType = null, ?string $operationName = null, ?Schema $schema = null, ?array $serializerContext = null, bool $forceCollection = false): Schema |
||||
66 | { |
||||
67 | $schema = $schema ?? new Schema(); |
||||
68 | if (null === $metadata = $this->getMetadata($resourceClass, $output, $operationType, $operationName, $serializerContext)) { |
||||
69 | return $schema; |
||||
70 | } |
||||
71 | [$resourceMetadata, $serializerContext, $inputOrOutputClass] = $metadata; |
||||
72 | |||||
73 | $version = $schema->getVersion(); |
||||
74 | $definitionName = $this->buildDefinitionName($resourceClass, $format, $output, $operationType, $operationName, $serializerContext); |
||||
75 | |||||
76 | $method = (null !== $operationType && null !== $operationName) ? $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method') : 'GET'; |
||||
77 | |||||
78 | if (!$output && !\in_array($method, ['POST', 'PATCH', 'PUT'], true)) { |
||||
79 | return $schema; |
||||
80 | } |
||||
81 | |||||
82 | if (!isset($schema['$ref']) && !isset($schema['type'])) { |
||||
83 | $ref = Schema::VERSION_OPENAPI === $version ? '#/components/schemas/'.$definitionName : '#/definitions/'.$definitionName; |
||||
84 | |||||
85 | $method = null !== $operationType && null !== $operationName ? $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET') : 'GET'; |
||||
86 | if ($forceCollection || (OperationType::COLLECTION === $operationType && 'POST' !== $method)) { |
||||
87 | $schema['type'] = 'array'; |
||||
88 | $schema['items'] = ['$ref' => $ref]; |
||||
89 | } else { |
||||
90 | $schema['$ref'] = $ref; |
||||
91 | } |
||||
92 | } |
||||
93 | |||||
94 | $definitions = $schema->getDefinitions(); |
||||
95 | if (isset($definitions[$definitionName])) { |
||||
96 | // Already computed |
||||
97 | return $schema; |
||||
98 | } |
||||
99 | |||||
100 | $definition = new \ArrayObject(['type' => 'object']); |
||||
101 | $definitions[$definitionName] = $definition; |
||||
102 | if (null !== $description = $resourceMetadata->getDescription()) { |
||||
103 | $definition['description'] = $description; |
||||
104 | } |
||||
105 | // see https://github.com/json-schema-org/json-schema-spec/pull/737 |
||||
106 | if ( |
||||
107 | Schema::VERSION_SWAGGER !== $version && |
||||
108 | ( |
||||
109 | (null !== $operationType && null !== $operationName && null !== $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) || |
||||
110 | null !== $resourceMetadata->getAttribute('deprecation_reason', null) |
||||
111 | ) |
||||
112 | ) { |
||||
113 | $definition['deprecated'] = true; |
||||
114 | } |
||||
115 | // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it |
||||
116 | // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4 |
||||
117 | if (null !== $iri = $resourceMetadata->getIri()) { |
||||
118 | $definition['externalDocs'] = ['url' => $iri]; |
||||
119 | } |
||||
120 | |||||
121 | $options = isset($serializerContext[AbstractNormalizer::GROUPS]) ? ['serializer_groups' => $serializerContext[AbstractNormalizer::GROUPS]] : []; |
||||
122 | foreach ($this->propertyNameCollectionFactory->create($inputOrOutputClass, $options) as $propertyName) { |
||||
123 | $propertyMetadata = $this->propertyMetadataFactory->create($inputOrOutputClass, $propertyName); |
||||
124 | if (!$propertyMetadata->isReadable() && !$propertyMetadata->isWritable()) { |
||||
0 ignored issues
–
show
The expression
$propertyMetadata->isReadable() of type boolean|null is loosely compared to false ; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.
If an expression can have both $a = canBeFalseAndNull();
// Instead of
if ( ! $a) { }
// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
|
|||||
125 | continue; |
||||
126 | } |
||||
127 | |||||
128 | $normalizedPropertyName = $this->nameConverter ? $this->nameConverter->normalize($propertyName, $inputOrOutputClass, $format, $serializerContext) : $propertyName; |
||||
0 ignored issues
–
show
The call to
Symfony\Component\Serial...rInterface::normalize() has too many arguments starting with $inputOrOutputClass .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue. If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.
Loading history...
|
|||||
129 | if ($propertyMetadata->isRequired()) { |
||||
130 | $definition['required'][] = $normalizedPropertyName; |
||||
131 | } |
||||
132 | |||||
133 | $this->buildPropertySchema($schema, $definitionName, $normalizedPropertyName, $propertyMetadata, $serializerContext, $format); |
||||
134 | } |
||||
135 | |||||
136 | return $schema; |
||||
137 | } |
||||
138 | |||||
139 | private function buildPropertySchema(Schema $schema, string $definitionName, string $normalizedPropertyName, PropertyMetadata $propertyMetadata, array $serializerContext, string $format): void |
||||
140 | { |
||||
141 | $version = $schema->getVersion(); |
||||
142 | $swagger = false; |
||||
143 | switch ($version) { |
||||
144 | case Schema::VERSION_SWAGGER: |
||||
145 | $swagger = true; |
||||
146 | $basePropertySchemaAttribute = 'swagger_context'; |
||||
147 | break; |
||||
148 | case Schema::VERSION_OPENAPI: |
||||
149 | $basePropertySchemaAttribute = 'openapi_context'; |
||||
150 | break; |
||||
151 | default: |
||||
152 | $basePropertySchemaAttribute = 'json_schema_context'; |
||||
153 | } |
||||
154 | |||||
155 | $propertySchema = $propertyMetadata->getAttributes()[$basePropertySchemaAttribute] ?? []; |
||||
156 | if (false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) { |
||||
157 | $propertySchema['readOnly'] = true; |
||||
158 | } |
||||
159 | if (!$swagger && false === $propertyMetadata->isReadable()) { |
||||
160 | $propertySchema['writeOnly'] = true; |
||||
161 | } |
||||
162 | if (null !== $description = $propertyMetadata->getDescription()) { |
||||
163 | $propertySchema['description'] = $description; |
||||
164 | } |
||||
165 | // see https://github.com/json-schema-org/json-schema-spec/pull/737 |
||||
166 | if (!$swagger && null !== $propertyMetadata->getAttribute('deprecation_reason')) { |
||||
167 | $propertySchema['deprecated'] = true; |
||||
168 | } |
||||
169 | // externalDocs is an OpenAPI specific extension, but JSON Schema allows additional keys, so we always add it |
||||
170 | // See https://json-schema.org/latest/json-schema-core.html#rfc.section.6.4 |
||||
171 | if (null !== $iri = $propertyMetadata->getIri()) { |
||||
172 | $propertySchema['externalDocs'] = ['url' => $iri]; |
||||
173 | } |
||||
174 | |||||
175 | $valueSchema = []; |
||||
176 | if (null !== $type = $propertyMetadata->getType()) { |
||||
177 | $isCollection = $type->isCollection(); |
||||
178 | if (null === $valueType = $isCollection ? $type->getCollectionValueType() : $type) { |
||||
179 | $builtinType = 'string'; |
||||
180 | $className = null; |
||||
181 | } else { |
||||
182 | $builtinType = $valueType->getBuiltinType(); |
||||
183 | $className = $valueType->getClassName(); |
||||
184 | } |
||||
185 | |||||
186 | $valueSchema = $this->typeFactory->getType(new Type($builtinType, $type->isNullable(), $className, $isCollection), $format, $propertyMetadata->isReadableLink(), $serializerContext, $schema); |
||||
187 | } |
||||
188 | |||||
189 | $propertySchema = new \ArrayObject($propertySchema + $valueSchema); |
||||
190 | if (DocumentationNormalizer::OPENAPI_VERSION === $version) { |
||||
191 | $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema; |
||||
192 | |||||
193 | return; |
||||
194 | } |
||||
195 | |||||
196 | $schema->getDefinitions()[$definitionName]['properties'][$normalizedPropertyName] = $propertySchema; |
||||
197 | } |
||||
198 | |||||
199 | private function buildDefinitionName(string $resourceClass, string $format = 'json', bool $output = true, ?string $operationType = null, ?string $operationName = null, ?array $serializerContext = null): string |
||||
200 | { |
||||
201 | [$resourceMetadata, $serializerContext, $inputOrOutputClass] = $this->getMetadata($resourceClass, $output, $operationType, $operationName, $serializerContext); |
||||
202 | |||||
203 | $prefix = $resourceMetadata->getShortName(); |
||||
204 | if (null !== $inputOrOutputClass && $resourceClass !== $inputOrOutputClass) { |
||||
205 | $prefix .= ':'.md5($inputOrOutputClass); |
||||
206 | } |
||||
207 | |||||
208 | if (isset($this->distinctFormats[$format])) { |
||||
209 | // JSON is the default, and so isn't included in the definition name |
||||
210 | $prefix .= ':'.$format; |
||||
211 | } |
||||
212 | |||||
213 | if (isset($serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME])) { |
||||
214 | $name = sprintf('%s-%s', $prefix, $serializerContext[DocumentationNormalizer::SWAGGER_DEFINITION_NAME]); |
||||
215 | } else { |
||||
216 | $groups = (array) ($serializerContext[AbstractNormalizer::GROUPS] ?? []); |
||||
217 | $name = $groups ? sprintf('%s-%s', $prefix, implode('_', $groups)) : $prefix; |
||||
0 ignored issues
–
show
|
|||||
218 | } |
||||
219 | |||||
220 | return $name; |
||||
221 | } |
||||
222 | |||||
223 | private function getMetadata(string $resourceClass, bool $output, ?string $operationType, ?string $operationName, ?array $serializerContext): ?array |
||||
224 | { |
||||
225 | $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
||||
226 | $attribute = $output ? 'output' : 'input'; |
||||
227 | if (null === $operationType || null === $operationName) { |
||||
228 | $inputOrOutput = $resourceMetadata->getAttribute($attribute, ['class' => $resourceClass]); |
||||
229 | } else { |
||||
230 | $inputOrOutput = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, ['class' => $resourceClass], true); |
||||
231 | } |
||||
232 | |||||
233 | if (null === ($inputOrOutput['class'] ?? null)) { |
||||
234 | // input or output disabled |
||||
235 | return null; |
||||
236 | } |
||||
237 | |||||
238 | return [ |
||||
239 | $resourceMetadata, |
||||
240 | $serializerContext ?? $this->getSerializerContext($resourceMetadata, $output, $operationType, $operationName), |
||||
241 | $inputOrOutput['class'], |
||||
242 | ]; |
||||
243 | } |
||||
244 | |||||
245 | private function getSerializerContext(ResourceMetadata $resourceMetadata, bool $output, ?string $operationType, ?string $operationName): array |
||||
246 | { |
||||
247 | $attribute = $output ? 'normalization_context' : 'denormalization_context'; |
||||
248 | |||||
249 | if (null === $operationType || null === $operationName) { |
||||
250 | return $resourceMetadata->getAttribute($attribute, []); |
||||
251 | } |
||||
252 | |||||
253 | return $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, $attribute, [], true); |
||||
254 | } |
||||
255 | } |
||||
256 |
If an expression can have both
false
, andnull
as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.