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); |
||||
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 |