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

api-platform / core / 13200284839

07 Feb 2025 12:56PM UTC coverage: 0.0% (-8.2%) from 8.164%
13200284839

Pull #6952

github

web-flow
Merge 519fbf8cc into 62377f880
Pull Request #6952: fix: errors retrieval and documentation

0 of 206 new or added lines in 17 files covered. (0.0%)

10757 existing lines in 366 files now uncovered.

0 of 47781 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/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
    ) {
UNCOV
58
    }
×
59

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

UNCOV
69
        foreach ($object->getResourceNameCollection() as $resourceClass) {
×
UNCOV
70
            $resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);
×
71

UNCOV
72
            $resourceMetadata = $resourceMetadataCollection[0];
×
UNCOV
73
            if (true === $resourceMetadata->getHideHydraOperation()) {
×
UNCOV
74
                continue;
×
75
            }
76

UNCOV
77
            $shortName = $resourceMetadata->getShortName();
×
UNCOV
78
            $prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName";
×
79

UNCOV
80
            $this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $hydraPrefix, $resourceMetadataCollection);
×
UNCOV
81
            $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix, $resourceMetadataCollection);
×
82
        }
83

UNCOV
84
        return $this->computeDoc($object, $this->getClasses($entrypointProperties, $classes, $hydraPrefix), $hydraPrefix);
×
85
    }
86

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

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

121
        if ($resourceMetadata->getDeprecationReason()) {
×
122
            $entrypointProperty['owl:deprecated'] = true;
×
123
        }
124

125
        $entrypointProperties[] = $entrypointProperty;
×
126
    }
127

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

UNCOV
136
        $class = [
×
UNCOV
137
            '@id' => $prefixedShortName,
×
UNCOV
138
            '@type' => $hydraPrefix.'Class',
×
UNCOV
139
            $hydraPrefix.'title' => $shortName,
×
UNCOV
140
            $hydraPrefix.'supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix),
×
UNCOV
141
            $hydraPrefix.'supportedOperation' => $this->getHydraOperations(false, $resourceMetadataCollection, $hydraPrefix),
×
UNCOV
142
        ];
×
143

UNCOV
144
        if (null !== $description) {
×
UNCOV
145
            $class[$hydraPrefix.'description'] = $description;
×
146
        }
147

UNCOV
148
        if ($resourceMetadata instanceof ErrorResource) {
×
UNCOV
149
            $class['subClassOf'] = 'Error';
×
150
        }
151

UNCOV
152
        if ($isDeprecated) {
×
153
            $class['owl:deprecated'] = true;
×
154
        }
155

UNCOV
156
        return $class;
×
157
    }
158

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

UNCOV
172
        if ($normalizationGroups) {
×
173
            $propertyNameContext['serializer_groups'] = $normalizationGroups;
×
174
        }
175

UNCOV
176
        if (!$denormalizationGroups) {
×
UNCOV
177
            return [$propertyNameContext, $propertyContext];
×
178
        }
179

180
        if (!isset($propertyNameContext['serializer_groups'])) {
×
181
            $propertyNameContext['serializer_groups'] = $denormalizationGroups;
×
182

183
            return [$propertyNameContext, $propertyContext];
×
184
        }
185

186
        foreach ($denormalizationGroups as $group) {
×
187
            $propertyNameContext['serializer_groups'][] = $group;
×
188
        }
189

190
        return [$propertyNameContext, $propertyContext];
×
191
    }
192

193
    /**
194
     * Gets Hydra properties.
195
     */
196
    private function getHydraProperties(string $resourceClass, ApiResource $resourceMetadata, string $shortName, string $prefixedShortName, array $context, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
197
    {
UNCOV
198
        $classes = [];
×
199

UNCOV
200
        $classes[$resourceClass] = true;
×
UNCOV
201
        foreach ($resourceMetadata->getOperations() as $operation) {
×
202
            /** @var Operation $operation */
UNCOV
203
            if (!$operation instanceof CollectionOperationInterface) {
×
UNCOV
204
                continue;
×
205
            }
206

UNCOV
207
            $inputMetadata = $operation->getInput();
×
UNCOV
208
            if (null !== $inputClass = $inputMetadata['class'] ?? null) {
×
209
                $classes[$inputClass] = true;
×
210
            }
211

UNCOV
212
            $outputMetadata = $operation->getOutput();
×
UNCOV
213
            if (null !== $outputClass = $outputMetadata['class'] ?? null) {
×
214
                $classes[$outputClass] = true;
×
215
            }
216
        }
217

218
        /** @var string[] $classes */
UNCOV
219
        $classes = array_keys($classes);
×
UNCOV
220
        $properties = [];
×
UNCOV
221
        [$propertyNameContext, $propertyContext] = $this->getPropertyMetadataFactoryContext($resourceMetadata);
×
UNCOV
222
        foreach ($classes as $class) {
×
UNCOV
223
            foreach ($this->propertyNameCollectionFactory->create($class, $propertyNameContext) as $propertyName) {
×
UNCOV
224
                $propertyMetadata = $this->propertyMetadataFactory->create($class, $propertyName, $propertyContext);
×
225

UNCOV
226
                if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) {
×
UNCOV
227
                    continue;
×
228
                }
229

UNCOV
230
                if ($this->nameConverter) {
×
UNCOV
231
                    $propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context);
×
232
                }
233

UNCOV
234
                if (false === $propertyMetadata->getHydra()) {
×
UNCOV
235
                    continue;
×
236
                }
237

UNCOV
238
                $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName, $hydraPrefix);
×
239
            }
240
        }
241

UNCOV
242
        return $properties;
×
243
    }
244

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

UNCOV
257
                if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
×
UNCOV
258
                    continue;
×
259
                }
260

UNCOV
261
                $hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix);
×
262
            }
263
        }
264

UNCOV
265
        return $hydraOperations;
×
266
    }
267

268
    /**
269
     * Gets and populates if applicable a Hydra operation.
270
     */
271
    private function getHydraOperation(HttpOperation $operation, string $prefixedShortName, string $hydraPrefix): array
272
    {
UNCOV
273
        $method = $operation->getMethod() ?: 'GET';
×
274

UNCOV
275
        $hydraOperation = $operation->getHydraContext() ?? [];
×
UNCOV
276
        if ($operation->getDeprecationReason()) {
×
277
            $hydraOperation['owl:deprecated'] = true;
×
278
        }
279

UNCOV
280
        $shortName = $operation->getShortName();
×
UNCOV
281
        $inputMetadata = $operation->getInput() ?? [];
×
UNCOV
282
        $outputMetadata = $operation->getOutput() ?? [];
×
283

UNCOV
284
        $inputClass = \array_key_exists('class', $inputMetadata) ? $inputMetadata['class'] : false;
×
UNCOV
285
        $outputClass = \array_key_exists('class', $outputMetadata) ? $outputMetadata['class'] : false;
×
286

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

307
            if (null !== $inputClass) {
×
308
                $possibleValue = [];
×
309
                foreach ($operation->getInputFormats() as $mimeTypes) {
×
310
                    foreach ($mimeTypes as $mimeType) {
×
311
                        $possibleValue[] = $mimeType;
×
312
                    }
313
                }
314

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

UNCOV
339
        $hydraOperation[$hydraPrefix.'method'] ??= $method;
×
UNCOV
340
        $hydraOperation[$hydraPrefix.'title'] ??= strtolower($method).$shortName.($operation instanceof CollectionOperationInterface ? 'Collection' : '');
×
341

UNCOV
342
        ksort($hydraOperation);
×
343

UNCOV
344
        return $hydraOperation;
×
345
    }
346

347
    /**
348
     * Gets the range of the property.
349
     */
350
    private function getRange(ApiProperty $propertyMetadata): array|string|null
351
    {
UNCOV
352
        $jsonldContext = $propertyMetadata->getJsonldContext();
×
353

UNCOV
354
        if (isset($jsonldContext['@type'])) {
×
UNCOV
355
            return $jsonldContext['@type'];
×
356
        }
357

UNCOV
358
        $builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
×
UNCOV
359
        $types = [];
×
360

UNCOV
361
        foreach ($builtInTypes as $type) {
×
UNCOV
362
            if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueTypes()[0] ?? null) {
×
363
                $type = $collectionType;
×
364
            }
365

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

UNCOV
392
                    if (is_a($className, \DateTimeInterface::class, true)) {
×
393
                        if (!\in_array('xmls:dateTime', $types, true)) {
×
394
                            $types[] = 'xmls:dateTime';
×
395
                        }
396
                        break;
×
397
                    }
398

UNCOV
399
                    if ($this->resourceClassResolver->isResourceClass($className)) {
×
400
                        $resourceMetadata = $this->resourceMetadataFactory->create($className);
×
401
                        $operation = $resourceMetadata->getOperation();
×
402

403
                        if (!$operation instanceof HttpOperation || !$operation->getTypes()) {
×
404
                            if (!\in_array("#{$operation->getShortName()}", $types, true)) {
×
405
                                $types[] = "#{$operation->getShortName()}";
×
406
                            }
407
                            break;
×
408
                        }
409

410
                        $types = array_unique(array_merge($types, $operation->getTypes()));
×
411
                        break;
×
412
                    }
413
            }
414
        }
415

UNCOV
416
        if ([] === $types) {
×
UNCOV
417
            return null;
×
418
        }
419

UNCOV
420
        return 1 === \count($types) ? $types[0] : $types;
×
421
    }
422

423
    private function isSingleRelation(ApiProperty $propertyMetadata): bool
424
    {
UNCOV
425
        $builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
×
426

UNCOV
427
        foreach ($builtInTypes as $type) {
×
UNCOV
428
            $className = $type->getClassName();
×
429
            if (
UNCOV
430
                !$type->isCollection()
×
UNCOV
431
                && null !== $className
×
UNCOV
432
                && $this->resourceClassResolver->isResourceClass($className)
×
433
            ) {
434
                return true;
×
435
            }
436
        }
437

UNCOV
438
        return false;
×
439
    }
440

441
    /**
442
     * Builds the classes array.
443
     */
444
    private function getClasses(array $entrypointProperties, array $classes, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
445
    {
UNCOV
446
        $classes[] = [
×
UNCOV
447
            '@id' => '#Entrypoint',
×
UNCOV
448
            '@type' => $hydraPrefix.'Class',
×
UNCOV
449
            $hydraPrefix.'title' => 'Entrypoint',
×
UNCOV
450
            $hydraPrefix.'supportedProperty' => $entrypointProperties,
×
UNCOV
451
            $hydraPrefix.'supportedOperation' => [
×
UNCOV
452
                '@type' => $hydraPrefix.'Operation',
×
UNCOV
453
                $hydraPrefix.'method' => 'GET',
×
UNCOV
454
                $hydraPrefix.'title' => 'index',
×
UNCOV
455
                $hydraPrefix.'description' => 'The API Entrypoint.',
×
UNCOV
456
                $hydraPrefix.'returns' => 'Entrypoint',
×
UNCOV
457
            ],
×
UNCOV
458
        ];
×
459

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

UNCOV
497
        return $classes;
×
498
    }
499

500
    /**
501
     * Gets a property definition.
502
     */
503
    private function getProperty(ApiProperty $propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName, string $hydraPrefix): array
504
    {
UNCOV
505
        if ($iri = $propertyMetadata->getIris()) {
×
506
            $iri = 1 === (is_countable($iri) ? \count($iri) : 0) ? $iri[0] : $iri;
×
507
        }
508

UNCOV
509
        if (!isset($iri)) {
×
UNCOV
510
            $iri = "#$shortName/$propertyName";
×
511
        }
512

UNCOV
513
        $propertyData = ($propertyMetadata->getJsonldContext()[$hydraPrefix.'property'] ?? []) + [
×
UNCOV
514
            '@id' => $iri,
×
UNCOV
515
            '@type' => false === $propertyMetadata->isReadableLink() ? $hydraPrefix.'Link' : 'rdf:Property',
×
UNCOV
516
            'domain' => $prefixedShortName,
×
UNCOV
517
            'label' => $propertyName,
×
UNCOV
518
        ];
×
519

UNCOV
520
        if (!isset($propertyData['owl:deprecated']) && $propertyMetadata->getDeprecationReason()) {
×
521
            $propertyData['owl:deprecated'] = true;
×
522
        }
523

UNCOV
524
        if (!isset($propertyData['owl:maxCardinality']) && $this->isSingleRelation($propertyMetadata)) {
×
525
            $propertyData['owl:maxCardinality'] = 1;
×
526
        }
527

UNCOV
528
        if (!isset($propertyData['range']) && null !== $range = $this->getRange($propertyMetadata)) {
×
UNCOV
529
            $propertyData['range'] = $range;
×
530
        }
531

UNCOV
532
        $property = [
×
UNCOV
533
            '@type' => $hydraPrefix.'SupportedProperty',
×
UNCOV
534
            $hydraPrefix.'property' => $propertyData,
×
UNCOV
535
            $hydraPrefix.'title' => $propertyName,
×
UNCOV
536
            $hydraPrefix.'required' => $propertyMetadata->isRequired() ?? false,
×
UNCOV
537
            $hydraPrefix.'readable' => $propertyMetadata->isReadable(),
×
UNCOV
538
            $hydraPrefix.'writeable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(),
×
UNCOV
539
        ];
×
540

UNCOV
541
        if (null !== $description = $propertyMetadata->getDescription()) {
×
UNCOV
542
            $property[$hydraPrefix.'description'] = $description;
×
543
        }
544

UNCOV
545
        return $property;
×
546
    }
547

548
    /**
549
     * Computes the documentation.
550
     */
551
    private function computeDoc(Documentation $object, array $classes, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
552
    {
UNCOV
553
        $doc = ['@context' => $this->getContext($hydraPrefix), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT]), '@type' => $hydraPrefix.'ApiDocumentation'];
×
554

UNCOV
555
        if ('' !== $object->getTitle()) {
×
UNCOV
556
            $doc[$hydraPrefix.'title'] = $object->getTitle();
×
557
        }
558

UNCOV
559
        if ('' !== $object->getDescription()) {
×
UNCOV
560
            $doc[$hydraPrefix.'description'] = $object->getDescription();
×
561
        }
562

UNCOV
563
        $doc[$hydraPrefix.'entrypoint'] = $this->urlGenerator->generate('api_entrypoint');
×
UNCOV
564
        $doc[$hydraPrefix.'supportedClass'] = $classes;
×
565

UNCOV
566
        return $doc;
×
567
    }
568

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

591
    /**
592
     * {@inheritdoc}
593
     */
594
    public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
595
    {
UNCOV
596
        return self::FORMAT === $format && $data instanceof Documentation;
×
597
    }
598

599
    public function getSupportedTypes($format): array
600
    {
UNCOV
601
        return self::FORMAT === $format ? [Documentation::class => true] : [];
×
602
    }
603
}
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

© 2025 Coveralls, Inc