DocumentationNormalizer::getHydraProperties()   B
last analyzed

Complexity

Conditions 9
Paths 25

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
eloc 19
nc 25
nop 5
dl 0
loc 34
rs 8.0555
c 0
b 0
f 0
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
It seems like $shortName can also be of type null; however, parameter $shortName of ApiPlatform\Core\Hydra\S...eEntrypointProperties() 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 ignore-type  annotation

82
            $this->populateEntrypointProperties($resourceClass, $resourceMetadata, /** @scrutinizer ignore-type */ $shortName, $prefixedShortName, $entrypointProperties);
Loading history...
83
            $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context);
0 ignored issues
show
Bug introduced by
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 ignore-type  annotation

83
            $classes[] = $this->getClass($resourceClass, $resourceMetadata, /** @scrutinizer ignore-type */ $shortName, $prefixedShortName, $context);
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
Unused Code introduced by
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 ignore-call  annotation

212
                    /** @scrutinizer ignore-call */ 
213
                    $propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context);

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