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\Hydra\Serializer; |
||||||
15 | |||||||
16 | use ApiPlatform\Core\Api\OperationMethodResolverInterface; |
||||||
17 | use ApiPlatform\Core\Api\OperationType; |
||||||
18 | use ApiPlatform\Core\Api\ResourceClassResolverInterface; |
||||||
19 | use ApiPlatform\Core\Api\UrlGeneratorInterface; |
||||||
20 | use ApiPlatform\Core\Documentation\Documentation; |
||||||
21 | use ApiPlatform\Core\JsonLd\ContextBuilderInterface; |
||||||
22 | use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; |
||||||
23 | use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; |
||||||
24 | use ApiPlatform\Core\Metadata\Property\PropertyMetadata; |
||||||
25 | use ApiPlatform\Core\Metadata\Property\SubresourceMetadata; |
||||||
26 | use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
||||||
27 | use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; |
||||||
28 | use ApiPlatform\Core\Operation\Factory\SubresourceOperationFactoryInterface; |
||||||
29 | use Symfony\Component\PropertyInfo\Type; |
||||||
30 | use Symfony\Component\Serializer\NameConverter\NameConverterInterface; |
||||||
31 | use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; |
||||||
32 | use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface; |
||||||
33 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface; |
||||||
34 | |||||||
35 | /** |
||||||
36 | * Creates a machine readable Hydra API documentation. |
||||||
37 | * |
||||||
38 | * @author Kévin Dunglas <[email protected]> |
||||||
39 | */ |
||||||
40 | final class DocumentationNormalizer implements NormalizerInterface, CacheableSupportsMethodInterface |
||||||
41 | { |
||||||
42 | public const FORMAT = 'jsonld'; |
||||||
43 | |||||||
44 | private $resourceMetadataFactory; |
||||||
45 | private $propertyNameCollectionFactory; |
||||||
46 | private $propertyMetadataFactory; |
||||||
47 | private $resourceClassResolver; |
||||||
48 | private $operationMethodResolver; |
||||||
49 | private $urlGenerator; |
||||||
50 | private $subresourceOperationFactory; |
||||||
51 | private $nameConverter; |
||||||
52 | |||||||
53 | public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver = null, UrlGeneratorInterface $urlGenerator, SubresourceOperationFactoryInterface $subresourceOperationFactory = null, NameConverterInterface $nameConverter = null) |
||||||
54 | { |
||||||
55 | if ($operationMethodResolver) { |
||||||
56 | @trigger_error(sprintf('Passing an instance of %s to %s() is deprecated since version 2.5 and will be removed in 3.0.', OperationMethodResolverInterface::class, __METHOD__), E_USER_DEPRECATED); |
||||||
57 | } |
||||||
58 | |||||||
59 | $this->resourceMetadataFactory = $resourceMetadataFactory; |
||||||
60 | $this->propertyNameCollectionFactory = $propertyNameCollectionFactory; |
||||||
61 | $this->propertyMetadataFactory = $propertyMetadataFactory; |
||||||
62 | $this->resourceClassResolver = $resourceClassResolver; |
||||||
63 | $this->operationMethodResolver = $operationMethodResolver; |
||||||
64 | $this->urlGenerator = $urlGenerator; |
||||||
65 | $this->subresourceOperationFactory = $subresourceOperationFactory; |
||||||
66 | $this->nameConverter = $nameConverter; |
||||||
67 | } |
||||||
68 | |||||||
69 | /** |
||||||
70 | * {@inheritdoc} |
||||||
71 | */ |
||||||
72 | public function normalize($object, $format = null, array $context = []) |
||||||
73 | { |
||||||
74 | $classes = []; |
||||||
75 | $entrypointProperties = []; |
||||||
76 | |||||||
77 | foreach ($object->getResourceNameCollection() as $resourceClass) { |
||||||
78 | $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass); |
||||||
79 | $shortName = $resourceMetadata->getShortName(); |
||||||
80 | $prefixedShortName = $resourceMetadata->getIri() ?? "#$shortName"; |
||||||
81 | |||||||
82 | $this->populateEntrypointProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties); |
||||||
0 ignored issues
–
show
Bug
introduced
by
Loading history...
|
|||||||
83 | $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context); |
||||||
0 ignored issues
–
show
It seems like
$shortName can also be of type null ; however, parameter $shortName of ApiPlatform\Core\Hydra\S...nNormalizer::getClass() does only seem to accept string , maybe add an additional type check?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
Loading history...
|
|||||||
84 | } |
||||||
85 | |||||||
86 | return $this->computeDoc($object, $this->getClasses($entrypointProperties, $classes)); |
||||||
87 | } |
||||||
88 | |||||||
89 | /** |
||||||
90 | * Populates entrypoint properties. |
||||||
91 | */ |
||||||
92 | private function populateEntrypointProperties(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName, array &$entrypointProperties) |
||||||
93 | { |
||||||
94 | $hydraCollectionOperations = $this->getHydraOperations($resourceClass, $resourceMetadata, $prefixedShortName, true); |
||||||
95 | if (empty($hydraCollectionOperations)) { |
||||||
96 | return; |
||||||
97 | } |
||||||
98 | |||||||
99 | $entrypointProperty = [ |
||||||
100 | '@type' => 'hydra:SupportedProperty', |
||||||
101 | 'hydra:property' => [ |
||||||
102 | '@id' => sprintf('#Entrypoint/%s', lcfirst($shortName)), |
||||||
103 | '@type' => 'hydra:Link', |
||||||
104 | 'domain' => '#Entrypoint', |
||||||
105 | 'rdfs:label' => "The collection of $shortName resources", |
||||||
106 | 'rdfs:range' => [ |
||||||
107 | ['@id' => 'hydra:Collection'], |
||||||
108 | [ |
||||||
109 | 'owl:equivalentClass' => [ |
||||||
110 | 'owl:onProperty' => ['@id' => 'hydra:member'], |
||||||
111 | 'owl:allValuesFrom' => ['@id' => $prefixedShortName], |
||||||
112 | ], |
||||||
113 | ], |
||||||
114 | ], |
||||||
115 | 'hydra:supportedOperation' => $hydraCollectionOperations, |
||||||
116 | ], |
||||||
117 | 'hydra:title' => "The collection of $shortName resources", |
||||||
118 | 'hydra:readable' => true, |
||||||
119 | 'hydra:writable' => false, |
||||||
120 | ]; |
||||||
121 | |||||||
122 | if ($resourceMetadata->getCollectionOperationAttribute('GET', 'deprecation_reason', null, true)) { |
||||||
123 | $entrypointProperty['owl:deprecated'] = true; |
||||||
124 | } |
||||||
125 | |||||||
126 | $entrypointProperties[] = $entrypointProperty; |
||||||
127 | } |
||||||
128 | |||||||
129 | /** |
||||||
130 | * Gets a Hydra class. |
||||||
131 | */ |
||||||
132 | private function getClass(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName, array $context): array |
||||||
133 | { |
||||||
134 | $class = [ |
||||||
135 | '@id' => $prefixedShortName, |
||||||
136 | '@type' => 'hydra:Class', |
||||||
137 | 'rdfs:label' => $shortName, |
||||||
138 | 'hydra:title' => $shortName, |
||||||
139 | 'hydra:supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context), |
||||||
140 | 'hydra:supportedOperation' => $this->getHydraOperations($resourceClass, $resourceMetadata, $prefixedShortName, false), |
||||||
141 | ]; |
||||||
142 | |||||||
143 | if (null !== $description = $resourceMetadata->getDescription()) { |
||||||
144 | $class['hydra:description'] = $description; |
||||||
145 | } |
||||||
146 | |||||||
147 | if ($resourceMetadata->getAttribute('deprecation_reason')) { |
||||||
148 | $class['owl:deprecated'] = true; |
||||||
149 | } |
||||||
150 | |||||||
151 | return $class; |
||||||
152 | } |
||||||
153 | |||||||
154 | /** |
||||||
155 | * Gets the context for the property name factory. |
||||||
156 | */ |
||||||
157 | private function getPropertyNameCollectionFactoryContext(ResourceMetadata $resourceMetadata): array |
||||||
158 | { |
||||||
159 | $attributes = $resourceMetadata->getAttributes(); |
||||||
160 | $context = []; |
||||||
161 | |||||||
162 | if (isset($attributes['normalization_context'][AbstractNormalizer::GROUPS])) { |
||||||
163 | $context['serializer_groups'] = (array) $attributes['normalization_context'][AbstractNormalizer::GROUPS]; |
||||||
164 | } |
||||||
165 | |||||||
166 | if (!isset($attributes['denormalization_context'][AbstractNormalizer::GROUPS])) { |
||||||
167 | return $context; |
||||||
168 | } |
||||||
169 | |||||||
170 | if (isset($context['serializer_groups'])) { |
||||||
171 | foreach ((array) $attributes['denormalization_context'][AbstractNormalizer::GROUPS] as $groupName) { |
||||||
172 | $context['serializer_groups'][] = $groupName; |
||||||
173 | } |
||||||
174 | |||||||
175 | return $context; |
||||||
176 | } |
||||||
177 | |||||||
178 | $context['serializer_groups'] = (array) $attributes['denormalization_context'][AbstractNormalizer::GROUPS]; |
||||||
179 | |||||||
180 | return $context; |
||||||
181 | } |
||||||
182 | |||||||
183 | /** |
||||||
184 | * Gets Hydra properties. |
||||||
185 | */ |
||||||
186 | private function getHydraProperties(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName, array $context): array |
||||||
187 | { |
||||||
188 | $classes = []; |
||||||
189 | foreach ($resourceMetadata->getCollectionOperations() as $operationName => $operation) { |
||||||
190 | $inputMetadata = $resourceMetadata->getTypedOperationAttribute(OperationType::COLLECTION, $operationName, 'input', ['class' => $resourceClass], true); |
||||||
191 | if (null !== $inputClass = $inputMetadata['class'] ?? null) { |
||||||
192 | $classes[$inputClass] = true; |
||||||
193 | } |
||||||
194 | |||||||
195 | $outputMetadata = $resourceMetadata->getTypedOperationAttribute(OperationType::COLLECTION, $operationName, 'output', ['class' => $resourceClass], true); |
||||||
196 | if (null !== $outputClass = $outputMetadata['class'] ?? null) { |
||||||
197 | $classes[$outputClass] = true; |
||||||
198 | } |
||||||
199 | } |
||||||
200 | |||||||
201 | /** @var string[] $classes */ |
||||||
202 | $classes = array_keys($classes); |
||||||
203 | $properties = []; |
||||||
204 | foreach ($classes as $class) { |
||||||
205 | foreach ($this->propertyNameCollectionFactory->create($class, $this->getPropertyNameCollectionFactoryContext($resourceMetadata)) as $propertyName) { |
||||||
206 | $propertyMetadata = $this->propertyMetadataFactory->create($class, $propertyName); |
||||||
207 | if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) { |
||||||
208 | continue; |
||||||
209 | } |
||||||
210 | |||||||
211 | if ($this->nameConverter) { |
||||||
212 | $propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context); |
||||||
0 ignored issues
–
show
The call to
Symfony\Component\Serial...rInterface::normalize() has too many arguments starting with $class .
(
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...
|
|||||||
213 | } |
||||||
214 | |||||||
215 | $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName); |
||||||
216 | } |
||||||
217 | } |
||||||
218 | |||||||
219 | return $properties; |
||||||
220 | } |
||||||
221 | |||||||
222 | /** |
||||||
223 | * Gets Hydra operations. |
||||||
224 | */ |
||||||
225 | private function getHydraOperations(string $resourceClass, ResourceMetadata $resourceMetadata, string $prefixedShortName, bool $collection): array |
||||||
226 | { |
||||||
227 | if (null === $operations = $collection ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) { |
||||||
228 | return []; |
||||||
229 | } |
||||||
230 | |||||||
231 | $hydraOperations = []; |
||||||
232 | foreach ($operations as $operationName => $operation) { |
||||||
233 | $hydraOperations[] = $this->getHydraOperation($resourceClass, $resourceMetadata, $operationName, $operation, $prefixedShortName, $collection ? OperationType::COLLECTION : OperationType::ITEM); |
||||||
234 | } |
||||||
235 | |||||||
236 | if (null !== $this->subresourceOperationFactory) { |
||||||
237 | foreach ($this->subresourceOperationFactory->create($resourceClass) as $operationId => $operation) { |
||||||
238 | $subresourceMetadata = $this->resourceMetadataFactory->create($operation['resource_class']); |
||||||
239 | $propertyMetadata = $this->propertyMetadataFactory->create(end($operation['identifiers'])[1], $operation['property']); |
||||||
240 | $hydraOperations[] = $this->getHydraOperation($resourceClass, $subresourceMetadata, $operation['route_name'], $operation, "#{$subresourceMetadata->getShortName()}", OperationType::SUBRESOURCE, $propertyMetadata->getSubresource()); |
||||||
241 | } |
||||||
242 | } |
||||||
243 | |||||||
244 | return $hydraOperations; |
||||||
245 | } |
||||||
246 | |||||||
247 | /** |
||||||
248 | * Gets and populates if applicable a Hydra operation. |
||||||
249 | * |
||||||
250 | * @param SubresourceMetadata $subresourceMetadata |
||||||
251 | */ |
||||||
252 | private function getHydraOperation(string $resourceClass, ResourceMetadata $resourceMetadata, string $operationName, array $operation, string $prefixedShortName, string $operationType, SubresourceMetadata $subresourceMetadata = null): array |
||||||
253 | { |
||||||
254 | if ($this->operationMethodResolver) { |
||||||
255 | if (OperationType::COLLECTION === $operationType) { |
||||||
256 | $method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName); |
||||||
257 | } elseif (OperationType::ITEM === $operationType) { |
||||||
258 | $method = $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName); |
||||||
259 | } else { |
||||||
260 | $method = 'GET'; |
||||||
261 | } |
||||||
262 | } else { |
||||||
263 | $method = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'method', 'GET'); |
||||||
264 | } |
||||||
265 | |||||||
266 | $hydraOperation = $operation['hydra_context'] ?? []; |
||||||
267 | if ($resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'deprecation_reason', null, true)) { |
||||||
268 | $hydraOperation['owl:deprecated'] = true; |
||||||
269 | } |
||||||
270 | |||||||
271 | $shortName = $resourceMetadata->getShortName(); |
||||||
272 | $inputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'input', ['class' => false]); |
||||||
273 | $inputClass = \array_key_exists('class', $inputMetadata) ? $inputMetadata['class'] : false; |
||||||
274 | $outputMetadata = $resourceMetadata->getTypedOperationAttribute($operationType, $operationName, 'output', ['class' => false]); |
||||||
275 | $outputClass = \array_key_exists('class', $outputMetadata) ? $outputMetadata['class'] : false; |
||||||
276 | |||||||
277 | if ('GET' === $method && OperationType::COLLECTION === $operationType) { |
||||||
278 | $hydraOperation += [ |
||||||
279 | '@type' => ['hydra:Operation', 'schema:FindAction'], |
||||||
280 | 'hydra:title' => "Retrieves the collection of $shortName resources.", |
||||||
281 | 'returns' => 'hydra:Collection', |
||||||
282 | ]; |
||||||
283 | } elseif ('GET' === $method && OperationType::SUBRESOURCE === $operationType) { |
||||||
284 | $hydraOperation += [ |
||||||
285 | '@type' => ['hydra:Operation', 'schema:FindAction'], |
||||||
286 | 'hydra:title' => $subresourceMetadata && $subresourceMetadata->isCollection() ? "Retrieves the collection of $shortName resources." : "Retrieves a $shortName resource.", |
||||||
287 | 'returns' => null === $outputClass ? 'owl:Nothing' : "#$shortName", |
||||||
288 | ]; |
||||||
289 | } elseif ('GET' === $method) { |
||||||
290 | $hydraOperation += [ |
||||||
291 | '@type' => ['hydra:Operation', 'schema:FindAction'], |
||||||
292 | 'hydra:title' => "Retrieves $shortName resource.", |
||||||
293 | 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, |
||||||
294 | ]; |
||||||
295 | } elseif ('PATCH' === $method) { |
||||||
296 | $hydraOperation += [ |
||||||
297 | '@type' => 'hydra:Operation', |
||||||
298 | 'hydra:title' => "Updates the $shortName resource.", |
||||||
299 | 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, |
||||||
300 | 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, |
||||||
301 | ]; |
||||||
302 | } elseif ('POST' === $method) { |
||||||
303 | $hydraOperation += [ |
||||||
304 | '@type' => ['hydra:Operation', 'schema:CreateAction'], |
||||||
305 | 'hydra:title' => "Creates a $shortName resource.", |
||||||
306 | 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, |
||||||
307 | 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, |
||||||
308 | ]; |
||||||
309 | } elseif ('PUT' === $method) { |
||||||
310 | $hydraOperation += [ |
||||||
311 | '@type' => ['hydra:Operation', 'schema:ReplaceAction'], |
||||||
312 | 'hydra:title' => "Replaces the $shortName resource.", |
||||||
313 | 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, |
||||||
314 | 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, |
||||||
315 | ]; |
||||||
316 | } elseif ('DELETE' === $method) { |
||||||
317 | $hydraOperation += [ |
||||||
318 | '@type' => ['hydra:Operation', 'schema:DeleteAction'], |
||||||
319 | 'hydra:title' => "Deletes the $shortName resource.", |
||||||
320 | 'returns' => 'owl:Nothing', |
||||||
321 | ]; |
||||||
322 | } |
||||||
323 | |||||||
324 | $hydraOperation['hydra:method'] ?? $hydraOperation['hydra:method'] = $method; |
||||||
325 | |||||||
326 | if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation['hydra:title'])) { |
||||||
327 | $hydraOperation['rdfs:label'] = $hydraOperation['hydra:title']; |
||||||
328 | } |
||||||
329 | |||||||
330 | ksort($hydraOperation); |
||||||
331 | |||||||
332 | return $hydraOperation; |
||||||
333 | } |
||||||
334 | |||||||
335 | /** |
||||||
336 | * Gets the range of the property. |
||||||
337 | */ |
||||||
338 | private function getRange(PropertyMetadata $propertyMetadata): ?string |
||||||
339 | { |
||||||
340 | $jsonldContext = $propertyMetadata->getAttributes()['jsonld_context'] ?? []; |
||||||
341 | |||||||
342 | if (isset($jsonldContext['@type'])) { |
||||||
343 | return $jsonldContext['@type']; |
||||||
344 | } |
||||||
345 | |||||||
346 | if (null === $type = $propertyMetadata->getType()) { |
||||||
347 | return null; |
||||||
348 | } |
||||||
349 | |||||||
350 | if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueType()) { |
||||||
351 | $type = $collectionType; |
||||||
352 | } |
||||||
353 | |||||||
354 | switch ($type->getBuiltinType()) { |
||||||
355 | case Type::BUILTIN_TYPE_STRING: |
||||||
356 | return 'xmls:string'; |
||||||
357 | case Type::BUILTIN_TYPE_INT: |
||||||
358 | return 'xmls:integer'; |
||||||
359 | case Type::BUILTIN_TYPE_FLOAT: |
||||||
360 | return 'xmls:decimal'; |
||||||
361 | case Type::BUILTIN_TYPE_BOOL: |
||||||
362 | return 'xmls:boolean'; |
||||||
363 | case Type::BUILTIN_TYPE_OBJECT: |
||||||
364 | if (null === $className = $type->getClassName()) { |
||||||
365 | return null; |
||||||
366 | } |
||||||
367 | |||||||
368 | if (is_a($className, \DateTimeInterface::class, true)) { |
||||||
369 | return 'xmls:dateTime'; |
||||||
370 | } |
||||||
371 | |||||||
372 | if ($this->resourceClassResolver->isResourceClass($className)) { |
||||||
373 | $resourceMetadata = $this->resourceMetadataFactory->create($className); |
||||||
374 | |||||||
375 | return $resourceMetadata->getIri() ?? "#{$resourceMetadata->getShortName()}"; |
||||||
376 | } |
||||||
377 | break; |
||||||
378 | } |
||||||
379 | |||||||
380 | return null; |
||||||
381 | } |
||||||
382 | |||||||
383 | /** |
||||||
384 | * Builds the classes array. |
||||||
385 | */ |
||||||
386 | private function getClasses(array $entrypointProperties, array $classes): array |
||||||
387 | { |
||||||
388 | $classes[] = [ |
||||||
389 | '@id' => '#Entrypoint', |
||||||
390 | '@type' => 'hydra:Class', |
||||||
391 | 'hydra:title' => 'The API entrypoint', |
||||||
392 | 'hydra:supportedProperty' => $entrypointProperties, |
||||||
393 | 'hydra:supportedOperation' => [ |
||||||
394 | '@type' => 'hydra:Operation', |
||||||
395 | 'hydra:method' => 'GET', |
||||||
396 | 'rdfs:label' => 'The API entrypoint.', |
||||||
397 | 'returns' => '#EntryPoint', |
||||||
398 | ], |
||||||
399 | ]; |
||||||
400 | |||||||
401 | // Constraint violation |
||||||
402 | $classes[] = [ |
||||||
403 | '@id' => '#ConstraintViolation', |
||||||
404 | '@type' => 'hydra:Class', |
||||||
405 | 'hydra:title' => 'A constraint violation', |
||||||
406 | 'hydra:supportedProperty' => [ |
||||||
407 | [ |
||||||
408 | '@type' => 'hydra:SupportedProperty', |
||||||
409 | 'hydra:property' => [ |
||||||
410 | '@id' => '#ConstraintViolation/propertyPath', |
||||||
411 | '@type' => 'rdf:Property', |
||||||
412 | 'rdfs:label' => 'propertyPath', |
||||||
413 | 'domain' => '#ConstraintViolation', |
||||||
414 | 'range' => 'xmls:string', |
||||||
415 | ], |
||||||
416 | 'hydra:title' => 'propertyPath', |
||||||
417 | 'hydra:description' => 'The property path of the violation', |
||||||
418 | 'hydra:readable' => true, |
||||||
419 | 'hydra:writable' => false, |
||||||
420 | ], |
||||||
421 | [ |
||||||
422 | '@type' => 'hydra:SupportedProperty', |
||||||
423 | 'hydra:property' => [ |
||||||
424 | '@id' => '#ConstraintViolation/message', |
||||||
425 | '@type' => 'rdf:Property', |
||||||
426 | 'rdfs:label' => 'message', |
||||||
427 | 'domain' => '#ConstraintViolation', |
||||||
428 | 'range' => 'xmls:string', |
||||||
429 | ], |
||||||
430 | 'hydra:title' => 'message', |
||||||
431 | 'hydra:description' => 'The message associated with the violation', |
||||||
432 | 'hydra:readable' => true, |
||||||
433 | 'hydra:writable' => false, |
||||||
434 | ], |
||||||
435 | ], |
||||||
436 | ]; |
||||||
437 | |||||||
438 | // Constraint violation list |
||||||
439 | $classes[] = [ |
||||||
440 | '@id' => '#ConstraintViolationList', |
||||||
441 | '@type' => 'hydra:Class', |
||||||
442 | 'subClassOf' => 'hydra:Error', |
||||||
443 | 'hydra:title' => 'A constraint violation list', |
||||||
444 | 'hydra:supportedProperty' => [ |
||||||
445 | [ |
||||||
446 | '@type' => 'hydra:SupportedProperty', |
||||||
447 | 'hydra:property' => [ |
||||||
448 | '@id' => '#ConstraintViolationList/violations', |
||||||
449 | '@type' => 'rdf:Property', |
||||||
450 | 'rdfs:label' => 'violations', |
||||||
451 | 'domain' => '#ConstraintViolationList', |
||||||
452 | 'range' => '#ConstraintViolation', |
||||||
453 | ], |
||||||
454 | 'hydra:title' => 'violations', |
||||||
455 | 'hydra:description' => 'The violations', |
||||||
456 | 'hydra:readable' => true, |
||||||
457 | 'hydra:writable' => false, |
||||||
458 | ], |
||||||
459 | ], |
||||||
460 | ]; |
||||||
461 | |||||||
462 | return $classes; |
||||||
463 | } |
||||||
464 | |||||||
465 | /** |
||||||
466 | * Gets a property definition. |
||||||
467 | */ |
||||||
468 | private function getProperty(PropertyMetadata $propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName): array |
||||||
469 | { |
||||||
470 | $propertyData = [ |
||||||
471 | '@id' => $propertyMetadata->getIri() ?? "#$shortName/$propertyName", |
||||||
472 | '@type' => false === $propertyMetadata->isReadableLink() ? 'hydra:Link' : 'rdf:Property', |
||||||
473 | 'rdfs:label' => $propertyName, |
||||||
474 | 'domain' => $prefixedShortName, |
||||||
475 | ]; |
||||||
476 | |||||||
477 | $type = $propertyMetadata->getType(); |
||||||
478 | |||||||
479 | if (null !== $type && !$type->isCollection() && (null !== $className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className)) { |
||||||
480 | $propertyData['owl:maxCardinality'] = 1; |
||||||
481 | } |
||||||
482 | |||||||
483 | $property = [ |
||||||
484 | '@type' => 'hydra:SupportedProperty', |
||||||
485 | 'hydra:property' => $propertyData, |
||||||
486 | 'hydra:title' => $propertyName, |
||||||
487 | 'hydra:required' => $propertyMetadata->isRequired(), |
||||||
488 | 'hydra:readable' => $propertyMetadata->isReadable(), |
||||||
489 | 'hydra:writable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(), |
||||||
490 | ]; |
||||||
491 | |||||||
492 | if (null !== $range = $this->getRange($propertyMetadata)) { |
||||||
493 | $property['hydra:property']['range'] = $range; |
||||||
494 | } |
||||||
495 | |||||||
496 | if (null !== $description = $propertyMetadata->getDescription()) { |
||||||
497 | $property['hydra:description'] = $description; |
||||||
498 | } |
||||||
499 | |||||||
500 | if ($propertyMetadata->getAttribute('deprecation_reason')) { |
||||||
501 | $property['owl:deprecated'] = true; |
||||||
502 | } |
||||||
503 | |||||||
504 | return $property; |
||||||
505 | } |
||||||
506 | |||||||
507 | /** |
||||||
508 | * Computes the documentation. |
||||||
509 | */ |
||||||
510 | private function computeDoc(Documentation $object, array $classes): array |
||||||
511 | { |
||||||
512 | $doc = ['@context' => $this->getContext(), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT]), '@type' => 'hydra:ApiDocumentation']; |
||||||
513 | |||||||
514 | if ('' !== $object->getTitle()) { |
||||||
515 | $doc['hydra:title'] = $object->getTitle(); |
||||||
516 | } |
||||||
517 | |||||||
518 | if ('' !== $object->getDescription()) { |
||||||
519 | $doc['hydra:description'] = $object->getDescription(); |
||||||
520 | } |
||||||
521 | |||||||
522 | $doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_entrypoint'); |
||||||
523 | $doc['hydra:supportedClass'] = $classes; |
||||||
524 | |||||||
525 | return $doc; |
||||||
526 | } |
||||||
527 | |||||||
528 | /** |
||||||
529 | * Builds the JSON-LD context for the API documentation. |
||||||
530 | */ |
||||||
531 | private function getContext(): array |
||||||
532 | { |
||||||
533 | return [ |
||||||
534 | '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#', |
||||||
535 | 'hydra' => ContextBuilderInterface::HYDRA_NS, |
||||||
536 | 'rdf' => ContextBuilderInterface::RDF_NS, |
||||||
537 | 'rdfs' => ContextBuilderInterface::RDFS_NS, |
||||||
538 | 'xmls' => ContextBuilderInterface::XML_NS, |
||||||
539 | 'owl' => ContextBuilderInterface::OWL_NS, |
||||||
540 | 'schema' => ContextBuilderInterface::SCHEMA_ORG_NS, |
||||||
541 | 'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'], |
||||||
542 | 'range' => ['@id' => 'rdfs:range', '@type' => '@id'], |
||||||
543 | 'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'], |
||||||
544 | 'expects' => ['@id' => 'hydra:expects', '@type' => '@id'], |
||||||
545 | 'returns' => ['@id' => 'hydra:returns', '@type' => '@id'], |
||||||
546 | ]; |
||||||
547 | } |
||||||
548 | |||||||
549 | /** |
||||||
550 | * {@inheritdoc} |
||||||
551 | */ |
||||||
552 | public function supportsNormalization($data, $format = null, array $context = []) |
||||||
553 | { |
||||||
554 | return self::FORMAT === $format && $data instanceof Documentation; |
||||||
555 | } |
||||||
556 | |||||||
557 | /** |
||||||
558 | * {@inheritdoc} |
||||||
559 | */ |
||||||
560 | public function hasCacheableSupportsMethod(): bool |
||||||
561 | { |
||||||
562 | return true; |
||||||
563 | } |
||||||
564 | } |
||||||
565 |