• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 14222672111

02 Apr 2025 02:50PM UTC coverage: 8.514% (-0.5%) from 8.983%
14222672111

Pull #7062

github

web-flow
Merge b7033a473 into c636d980a
Pull Request #7062: fix(hydra): use correctly enable_docs

22 of 117 new or added lines in 5 files covered. (18.8%)

102 existing lines in 3 files now uncovered.

13392 of 157293 relevant lines covered (8.51%)

22.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.08
/src/Hydra/Serializer/DocumentationNormalizer.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.com>
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\Hydra\Serializer;
15

16
use ApiPlatform\Documentation\Documentation;
17
use ApiPlatform\JsonLd\ContextBuilder;
18
use ApiPlatform\JsonLd\ContextBuilderInterface;
19
use ApiPlatform\JsonLd\Serializer\HydraPrefixTrait;
20
use ApiPlatform\Metadata\ApiProperty;
21
use ApiPlatform\Metadata\ApiResource;
22
use ApiPlatform\Metadata\CollectionOperationInterface;
23
use ApiPlatform\Metadata\ErrorResource;
24
use ApiPlatform\Metadata\HttpOperation;
25
use ApiPlatform\Metadata\Operation;
26
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
27
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
28
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
29
use ApiPlatform\Metadata\Resource\ResourceMetadataCollection;
30
use ApiPlatform\Metadata\ResourceClassResolverInterface;
31
use ApiPlatform\Metadata\UrlGeneratorInterface;
32
use Symfony\Component\PropertyInfo\Type;
33
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
34
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
35
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
36

37
use const ApiPlatform\JsonLd\HYDRA_CONTEXT;
38

39
/**
40
 * Creates a machine readable Hydra API documentation.
41
 *
42
 * @author Kévin Dunglas <dunglas@gmail.com>
43
 */
44
final class DocumentationNormalizer implements NormalizerInterface
45
{
46
    use HydraPrefixTrait;
47
    public const FORMAT = 'jsonld';
48

49
    public function __construct(
50
        private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory,
51
        private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
52
        private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory,
53
        private readonly ResourceClassResolverInterface $resourceClassResolver,
54
        private readonly UrlGeneratorInterface $urlGenerator,
55
        private readonly ?NameConverterInterface $nameConverter = null,
56
        private readonly ?array $defaultContext = [],
57
        private readonly ?bool $entrypointEnabled = true,
58
        private readonly ?bool $docsEnabled = true,
59
    ) {
60
    }
2,008✔
61

62
    /**
63
     * {@inheritdoc}
64
     */
65
    public function normalize(mixed $object, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
66
    {
67
        $classes = [];
10✔
68
        $entrypointProperties = [];
10✔
69
        $hydraPrefix = $this->getHydraPrefix($context + $this->defaultContext);
10✔
70
        $resourceClasses = $this->docsEnabled ? $object->getResourceNameCollection() : [];
10✔
71

72
        foreach ($resourceClasses as $resourceClass) {
10✔
73
            $resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);
10✔
74

75
            $resourceMetadata = $resourceMetadataCollection[0];
10✔
76
            if (true === $resourceMetadata->getHideHydraOperation()) {
10✔
77
                continue;
10✔
78
            }
79

80
            $shortName = $resourceMetadata->getShortName();
10✔
81
            $prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName";
10✔
82

83
            $this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $hydraPrefix, $resourceMetadataCollection);
10✔
84
            $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix, $resourceMetadataCollection);
10✔
85
        }
86

87
        return $this->computeDoc($object, $this->getClasses($entrypointProperties, $classes, $hydraPrefix), $hydraPrefix);
10✔
88
    }
89

90
    /**
91
     * Populates entrypoint properties.
92
     */
93
    private function populateEntrypointProperties(ApiResource $resourceMetadata, string $shortName, string $prefixedShortName, array &$entrypointProperties, string $hydraPrefix, ?ResourceMetadataCollection $resourceMetadataCollection = null): void
94
    {
95
        $hydraCollectionOperations = $this->getHydraOperations(true, $resourceMetadataCollection, $hydraPrefix);
10✔
96
        if (empty($hydraCollectionOperations)) {
10✔
97
            return;
10✔
98
        }
99

100
        $entrypointProperty = [
8✔
101
            '@type' => $hydraPrefix.'SupportedProperty',
8✔
102
            $hydraPrefix.'property' => [
8✔
103
                '@id' => \sprintf('#Entrypoint/%s', lcfirst($shortName)),
8✔
104
                '@type' => $hydraPrefix.'Link',
8✔
105
                'domain' => '#Entrypoint',
8✔
106
                'owl:maxCardinality' => 1,
8✔
107
                'range' => [
8✔
108
                    ['@id' => $hydraPrefix.'Collection'],
8✔
109
                    [
8✔
110
                        'owl:equivalentClass' => [
8✔
111
                            'owl:onProperty' => ['@id' => $hydraPrefix.'member'],
8✔
112
                            'owl:allValuesFrom' => ['@id' => $prefixedShortName],
8✔
113
                        ],
8✔
114
                    ],
8✔
115
                ],
8✔
116
                $hydraPrefix.'supportedOperation' => $hydraCollectionOperations,
8✔
117
            ],
8✔
118
            $hydraPrefix.'title' => "get{$shortName}Collection",
8✔
119
            $hydraPrefix.'description' => "The collection of $shortName resources",
8✔
120
            $hydraPrefix.'readable' => true,
8✔
121
            $hydraPrefix.'writeable' => false,
8✔
122
        ];
8✔
123

124
        if ($resourceMetadata->getDeprecationReason()) {
8✔
125
            $entrypointProperty['owl:deprecated'] = true;
8✔
126
        }
127

128
        $entrypointProperties[] = $entrypointProperty;
8✔
129
    }
130

131
    /**
132
     * Gets a Hydra class.
133
     */
134
    private function getClass(string $resourceClass, ApiResource $resourceMetadata, string $shortName, string $prefixedShortName, array $context, string $hydraPrefix, ?ResourceMetadataCollection $resourceMetadataCollection = null): array
135
    {
136
        $description = $resourceMetadata->getDescription();
10✔
137
        $isDeprecated = $resourceMetadata->getDeprecationReason();
10✔
138

139
        $class = [
10✔
140
            '@id' => $prefixedShortName,
10✔
141
            '@type' => $hydraPrefix.'Class',
10✔
142
            $hydraPrefix.'title' => $shortName,
10✔
143
            $hydraPrefix.'supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix),
10✔
144
            $hydraPrefix.'supportedOperation' => $this->getHydraOperations(false, $resourceMetadataCollection, $hydraPrefix),
10✔
145
        ];
10✔
146

147
        if (null !== $description) {
10✔
148
            $class[$hydraPrefix.'description'] = $description;
10✔
149
        }
150

151
        if ($resourceMetadata instanceof ErrorResource) {
10✔
152
            $class['subClassOf'] = 'Error';
10✔
153
        }
154

155
        if ($isDeprecated) {
10✔
156
            $class['owl:deprecated'] = true;
8✔
157
        }
158

159
        return $class;
10✔
160
    }
161

162
    /**
163
     * Creates context for property metatata factories.
164
     */
165
    private function getPropertyMetadataFactoryContext(ApiResource $resourceMetadata): array
166
    {
167
        $normalizationGroups = $resourceMetadata->getNormalizationContext()[AbstractNormalizer::GROUPS] ?? null;
10✔
168
        $denormalizationGroups = $resourceMetadata->getDenormalizationContext()[AbstractNormalizer::GROUPS] ?? null;
10✔
169
        $propertyContext = [
10✔
170
            'normalization_groups' => $normalizationGroups,
10✔
171
            'denormalization_groups' => $denormalizationGroups,
10✔
172
        ];
10✔
173
        $propertyNameContext = [];
10✔
174

175
        if ($normalizationGroups) {
10✔
176
            $propertyNameContext['serializer_groups'] = $normalizationGroups;
8✔
177
        }
178

179
        if (!$denormalizationGroups) {
10✔
180
            return [$propertyNameContext, $propertyContext];
10✔
181
        }
182

183
        if (!isset($propertyNameContext['serializer_groups'])) {
8✔
184
            $propertyNameContext['serializer_groups'] = $denormalizationGroups;
×
185

UNCOV
186
            return [$propertyNameContext, $propertyContext];
×
187
        }
188

189
        foreach ($denormalizationGroups as $group) {
8✔
190
            $propertyNameContext['serializer_groups'][] = $group;
8✔
191
        }
192

193
        return [$propertyNameContext, $propertyContext];
8✔
194
    }
195

196
    /**
197
     * Gets Hydra properties.
198
     */
199
    private function getHydraProperties(string $resourceClass, ApiResource $resourceMetadata, string $shortName, string $prefixedShortName, array $context, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
200
    {
201
        $classes = [];
10✔
202

203
        $classes[$resourceClass] = true;
10✔
204
        foreach ($resourceMetadata->getOperations() as $operation) {
10✔
205
            /** @var Operation $operation */
206
            if (!$operation instanceof CollectionOperationInterface) {
10✔
207
                continue;
10✔
208
            }
209

210
            $inputMetadata = $operation->getInput();
10✔
211
            if (null !== $inputClass = $inputMetadata['class'] ?? null) {
10✔
212
                $classes[$inputClass] = true;
8✔
213
            }
214

215
            $outputMetadata = $operation->getOutput();
10✔
216
            if (null !== $outputClass = $outputMetadata['class'] ?? null) {
10✔
217
                $classes[$outputClass] = true;
8✔
218
            }
219
        }
220

221
        /** @var string[] $classes */
222
        $classes = array_keys($classes);
10✔
223
        $properties = [];
10✔
224
        [$propertyNameContext, $propertyContext] = $this->getPropertyMetadataFactoryContext($resourceMetadata);
10✔
225
        foreach ($classes as $class) {
10✔
226
            foreach ($this->propertyNameCollectionFactory->create($class, $propertyNameContext) as $propertyName) {
10✔
227
                $propertyMetadata = $this->propertyMetadataFactory->create($class, $propertyName, $propertyContext);
10✔
228

229
                if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) {
10✔
230
                    continue;
10✔
231
                }
232

233
                if ($this->nameConverter) {
10✔
234
                    $propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context);
10✔
235
                }
236

237
                if (false === $propertyMetadata->getHydra()) {
10✔
238
                    continue;
10✔
239
                }
240

241
                $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName, $hydraPrefix);
10✔
242
            }
243
        }
244

245
        return $properties;
10✔
246
    }
247

248
    /**
249
     * Gets Hydra operations.
250
     */
251
    private function getHydraOperations(bool $collection, ?ResourceMetadataCollection $resourceMetadataCollection = null, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
252
    {
253
        $hydraOperations = [];
10✔
254
        foreach ($resourceMetadataCollection as $resourceMetadata) {
10✔
255
            foreach ($resourceMetadata->getOperations() as $operation) {
10✔
256
                if (true === $operation->getHideHydraOperation()) {
10✔
257
                    continue;
10✔
258
                }
259

260
                if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
10✔
261
                    continue;
10✔
262
                }
263

264
                $hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix);
10✔
265
            }
266
        }
267

268
        return $hydraOperations;
10✔
269
    }
270

271
    /**
272
     * Gets and populates if applicable a Hydra operation.
273
     */
274
    private function getHydraOperation(HttpOperation $operation, string $prefixedShortName, string $hydraPrefix): array
275
    {
276
        $method = $operation->getMethod() ?: 'GET';
10✔
277

278
        $hydraOperation = $operation->getHydraContext() ?? [];
10✔
279
        if ($operation->getDeprecationReason()) {
10✔
280
            $hydraOperation['owl:deprecated'] = true;
8✔
281
        }
282

283
        $shortName = $operation->getShortName();
10✔
284
        $inputMetadata = $operation->getInput() ?? [];
10✔
285
        $outputMetadata = $operation->getOutput() ?? [];
10✔
286

287
        $inputClass = \array_key_exists('class', $inputMetadata) ? $inputMetadata['class'] : false;
10✔
288
        $outputClass = \array_key_exists('class', $outputMetadata) ? $outputMetadata['class'] : false;
10✔
289

290
        if ('GET' === $method && $operation instanceof CollectionOperationInterface) {
10✔
291
            $hydraOperation += [
8✔
292
                '@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
8✔
293
                $hydraPrefix.'description' => "Retrieves the collection of $shortName resources.",
8✔
294
                'returns' => null === $outputClass ? 'owl:Nothing' : $hydraPrefix.'Collection',
8✔
295
            ];
8✔
296
        } elseif ('GET' === $method) {
10✔
297
            $hydraOperation += [
10✔
298
                '@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
10✔
299
                $hydraPrefix.'description' => "Retrieves a $shortName resource.",
10✔
300
                'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
10✔
301
            ];
10✔
302
        } elseif ('PATCH' === $method) {
8✔
303
            $hydraOperation += [
8✔
304
                '@type' => $hydraPrefix.'Operation',
8✔
305
                $hydraPrefix.'description' => "Updates the $shortName resource.",
8✔
306
                'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
8✔
307
                'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
8✔
308
            ];
8✔
309

310
            if (null !== $inputClass) {
8✔
311
                $possibleValue = [];
8✔
312
                foreach ($operation->getInputFormats() as $mimeTypes) {
8✔
313
                    foreach ($mimeTypes as $mimeType) {
8✔
314
                        $possibleValue[] = $mimeType;
8✔
315
                    }
316
                }
317

318
                $hydraOperation['expectsHeader'] = [['headerName' => 'Content-Type', 'possibleValue' => $possibleValue]];
8✔
319
            }
320
        } elseif ('POST' === $method) {
8✔
321
            $hydraOperation += [
8✔
322
                '@type' => [$hydraPrefix.'Operation', 'schema:CreateAction'],
8✔
323
                $hydraPrefix.'description' => "Creates a $shortName resource.",
8✔
324
                'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
8✔
325
                'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
8✔
326
            ];
8✔
327
        } elseif ('PUT' === $method) {
8✔
328
            $hydraOperation += [
8✔
329
                '@type' => [$hydraPrefix.'Operation', 'schema:ReplaceAction'],
8✔
330
                $hydraPrefix.'description' => "Replaces the $shortName resource.",
8✔
331
                'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
8✔
332
                'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
8✔
333
            ];
8✔
334
        } elseif ('DELETE' === $method) {
8✔
335
            $hydraOperation += [
8✔
336
                '@type' => [$hydraPrefix.'Operation', 'schema:DeleteAction'],
8✔
337
                $hydraPrefix.'description' => "Deletes the $shortName resource.",
8✔
338
                'returns' => 'owl:Nothing',
8✔
339
            ];
8✔
340
        }
341

342
        $hydraOperation[$hydraPrefix.'method'] ??= $method;
10✔
343
        $hydraOperation[$hydraPrefix.'title'] ??= strtolower($method).$shortName.($operation instanceof CollectionOperationInterface ? 'Collection' : '');
10✔
344

345
        ksort($hydraOperation);
10✔
346

347
        return $hydraOperation;
10✔
348
    }
349

350
    /**
351
     * Gets the range of the property.
352
     */
353
    private function getRange(ApiProperty $propertyMetadata): array|string|null
354
    {
355
        $jsonldContext = $propertyMetadata->getJsonldContext();
10✔
356

357
        if (isset($jsonldContext['@type'])) {
10✔
358
            return $jsonldContext['@type'];
10✔
359
        }
360

361
        $builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
10✔
362
        $types = [];
10✔
363

364
        foreach ($builtInTypes as $type) {
10✔
365
            if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueTypes()[0] ?? null) {
10✔
366
                $type = $collectionType;
8✔
367
            }
368

369
            switch ($type->getBuiltinType()) {
10✔
370
                case Type::BUILTIN_TYPE_STRING:
8✔
371
                    if (!\in_array('xmls:string', $types, true)) {
10✔
372
                        $types[] = 'xmls:string';
10✔
373
                    }
374
                    break;
10✔
375
                case Type::BUILTIN_TYPE_INT:
8✔
376
                    if (!\in_array('xmls:integer', $types, true)) {
10✔
377
                        $types[] = 'xmls:integer';
10✔
378
                    }
379
                    break;
10✔
380
                case Type::BUILTIN_TYPE_FLOAT:
8✔
381
                    if (!\in_array('xmls:decimal', $types, true)) {
8✔
382
                        $types[] = 'xmls:decimal';
8✔
383
                    }
384
                    break;
8✔
385
                case Type::BUILTIN_TYPE_BOOL:
8✔
386
                    if (!\in_array('xmls:boolean', $types, true)) {
8✔
387
                        $types[] = 'xmls:boolean';
8✔
388
                    }
389
                    break;
8✔
390
                case Type::BUILTIN_TYPE_OBJECT:
8✔
391
                    if (null === $className = $type->getClassName()) {
10✔
UNCOV
392
                        continue 2;
×
393
                    }
394

395
                    if (is_a($className, \DateTimeInterface::class, true)) {
10✔
396
                        if (!\in_array('xmls:dateTime', $types, true)) {
8✔
397
                            $types[] = 'xmls:dateTime';
8✔
398
                        }
399
                        break;
8✔
400
                    }
401

402
                    if ($this->resourceClassResolver->isResourceClass($className)) {
10✔
403
                        $resourceMetadata = $this->resourceMetadataFactory->create($className);
8✔
404
                        $operation = $resourceMetadata->getOperation();
8✔
405

406
                        if (!$operation instanceof HttpOperation || !$operation->getTypes()) {
8✔
407
                            if (!\in_array("#{$operation->getShortName()}", $types, true)) {
8✔
408
                                $types[] = "#{$operation->getShortName()}";
8✔
409
                            }
410
                            break;
8✔
411
                        }
412

413
                        $types = array_unique(array_merge($types, $operation->getTypes()));
8✔
414
                        break;
8✔
415
                    }
416
            }
417
        }
418

419
        if ([] === $types) {
10✔
420
            return null;
10✔
421
        }
422

423
        return 1 === \count($types) ? $types[0] : $types;
10✔
424
    }
425

426
    private function isSingleRelation(ApiProperty $propertyMetadata): bool
427
    {
428
        $builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
10✔
429

430
        foreach ($builtInTypes as $type) {
10✔
431
            $className = $type->getClassName();
10✔
432
            if (
433
                !$type->isCollection()
10✔
434
                && null !== $className
10✔
435
                && $this->resourceClassResolver->isResourceClass($className)
10✔
436
            ) {
437
                return true;
8✔
438
            }
439
        }
440

441
        return false;
10✔
442
    }
443

444
    /**
445
     * Builds the classes array.
446
     */
447
    private function getClasses(array $entrypointProperties, array $classes, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
448
    {
449
        if ($this->entrypointEnabled) {
10✔
450
            $classes[] = [
10✔
451
                '@id' => '#Entrypoint',
10✔
452
                '@type' => $hydraPrefix.'Class',
10✔
453
                $hydraPrefix.'title' => 'Entrypoint',
10✔
454
                $hydraPrefix.'supportedProperty' => $entrypointProperties,
10✔
455
                $hydraPrefix.'supportedOperation' => [
10✔
456
                    '@type' => $hydraPrefix.'Operation',
10✔
457
                    $hydraPrefix.'method' => 'GET',
10✔
458
                    $hydraPrefix.'title' => 'index',
10✔
459
                    $hydraPrefix.'description' => 'The API Entrypoint.',
10✔
460
                    $hydraPrefix.'returns' => 'Entrypoint',
10✔
461
                ],
10✔
462
            ];
10✔
463
        }
464

465
        $classes[] = [
10✔
466
            '@id' => '#ConstraintViolationList',
10✔
467
            '@type' => $hydraPrefix.'Class',
10✔
468
            $hydraPrefix.'title' => 'ConstraintViolationList',
10✔
469
            $hydraPrefix.'description' => 'A constraint violation List.',
10✔
470
            $hydraPrefix.'supportedProperty' => [
10✔
471
                [
10✔
472
                    '@type' => $hydraPrefix.'SupportedProperty',
10✔
473
                    $hydraPrefix.'property' => [
10✔
474
                        '@id' => '#ConstraintViolationList/propertyPath',
10✔
475
                        '@type' => 'rdf:Property',
10✔
476
                        'rdfs:label' => 'propertyPath',
10✔
477
                        'domain' => '#ConstraintViolationList',
10✔
478
                        'range' => 'xmls:string',
10✔
479
                    ],
10✔
480
                    $hydraPrefix.'title' => 'propertyPath',
10✔
481
                    $hydraPrefix.'description' => 'The property path of the violation',
10✔
482
                    $hydraPrefix.'readable' => true,
10✔
483
                    $hydraPrefix.'writeable' => false,
10✔
484
                ],
10✔
485
                [
10✔
486
                    '@type' => $hydraPrefix.'SupportedProperty',
10✔
487
                    $hydraPrefix.'property' => [
10✔
488
                        '@id' => '#ConstraintViolationList/message',
10✔
489
                        '@type' => 'rdf:Property',
10✔
490
                        'rdfs:label' => 'message',
10✔
491
                        'domain' => '#ConstraintViolationList',
10✔
492
                        'range' => 'xmls:string',
10✔
493
                    ],
10✔
494
                    $hydraPrefix.'title' => 'message',
10✔
495
                    $hydraPrefix.'description' => 'The message associated with the violation',
10✔
496
                    $hydraPrefix.'readable' => true,
10✔
497
                    $hydraPrefix.'writeable' => false,
10✔
498
                ],
10✔
499
            ],
10✔
500
        ];
10✔
501

502
        return $classes;
10✔
503
    }
504

505
    /**
506
     * Gets a property definition.
507
     */
508
    private function getProperty(ApiProperty $propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName, string $hydraPrefix): array
509
    {
510
        if ($iri = $propertyMetadata->getIris()) {
10✔
511
            $iri = 1 === (is_countable($iri) ? \count($iri) : 0) ? $iri[0] : $iri;
8✔
512
        }
513

514
        if (!isset($iri)) {
10✔
515
            $iri = "#$shortName/$propertyName";
10✔
516
        }
517

518
        $propertyData = ($propertyMetadata->getJsonldContext()[$hydraPrefix.'property'] ?? []) + [
10✔
519
            '@id' => $iri,
10✔
520
            '@type' => false === $propertyMetadata->isReadableLink() ? $hydraPrefix.'Link' : 'rdf:Property',
10✔
521
            'domain' => $prefixedShortName,
10✔
522
            'label' => $propertyName,
10✔
523
        ];
10✔
524

525
        if (!isset($propertyData['owl:deprecated']) && $propertyMetadata->getDeprecationReason()) {
10✔
526
            $propertyData['owl:deprecated'] = true;
8✔
527
        }
528

529
        if (!isset($propertyData['owl:maxCardinality']) && $this->isSingleRelation($propertyMetadata)) {
10✔
530
            $propertyData['owl:maxCardinality'] = 1;
8✔
531
        }
532

533
        if (!isset($propertyData['range']) && null !== $range = $this->getRange($propertyMetadata)) {
10✔
534
            $propertyData['range'] = $range;
10✔
535
        }
536

537
        $property = [
10✔
538
            '@type' => $hydraPrefix.'SupportedProperty',
10✔
539
            $hydraPrefix.'property' => $propertyData,
10✔
540
            $hydraPrefix.'title' => $propertyName,
10✔
541
            $hydraPrefix.'required' => $propertyMetadata->isRequired() ?? false,
10✔
542
            $hydraPrefix.'readable' => $propertyMetadata->isReadable(),
10✔
543
            $hydraPrefix.'writeable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(),
10✔
544
        ];
10✔
545

546
        if (null !== $description = $propertyMetadata->getDescription()) {
10✔
547
            $property[$hydraPrefix.'description'] = $description;
10✔
548
        }
549

550
        return $property;
10✔
551
    }
552

553
    /**
554
     * Computes the documentation.
555
     */
556
    private function computeDoc(Documentation $object, array $classes, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
557
    {
558
        $doc = ['@context' => $this->getContext($hydraPrefix), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT]), '@type' => $hydraPrefix.'ApiDocumentation'];
10✔
559

560
        if ('' !== $object->getTitle()) {
10✔
561
            $doc[$hydraPrefix.'title'] = $object->getTitle();
10✔
562
        }
563

564
        if ('' !== $object->getDescription()) {
10✔
565
            $doc[$hydraPrefix.'description'] = $object->getDescription();
10✔
566
        }
567

568
        if ($this->entrypointEnabled) {
10✔
569
            $doc[$hydraPrefix.'entrypoint'] = $this->urlGenerator->generate('api_entrypoint');
10✔
570
        }
571
        $doc[$hydraPrefix.'supportedClass'] = $classes;
10✔
572

573
        return $doc;
10✔
574
    }
575

576
    /**
577
     * Builds the JSON-LD context for the API documentation.
578
     */
579
    private function getContext(string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
580
    {
581
        return [
10✔
582
            HYDRA_CONTEXT,
10✔
583
            [
10✔
584
                '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#',
10✔
585
                'hydra' => ContextBuilderInterface::HYDRA_NS,
10✔
586
                'rdf' => ContextBuilderInterface::RDF_NS,
10✔
587
                'rdfs' => ContextBuilderInterface::RDFS_NS,
10✔
588
                'xmls' => ContextBuilderInterface::XML_NS,
10✔
589
                'owl' => ContextBuilderInterface::OWL_NS,
10✔
590
                'schema' => ContextBuilderInterface::SCHEMA_ORG_NS,
10✔
591
                'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'],
10✔
592
                'range' => ['@id' => 'rdfs:range', '@type' => '@id'],
10✔
593
                'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'],
10✔
594
            ],
10✔
595
        ];
10✔
596
    }
597

598
    /**
599
     * {@inheritdoc}
600
     */
601
    public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
602
    {
603
        return self::FORMAT === $format && $data instanceof Documentation;
10✔
604
    }
605

606
    public function getSupportedTypes($format): array
607
    {
608
        return self::FORMAT === $format ? [Documentation::class => true] : [];
1,847✔
609
    }
610
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc