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

api-platform / core / 13175753759

06 Feb 2025 09:30AM UTC coverage: 0.0% (-7.7%) from 7.663%
13175753759

Pull #6947

github

web-flow
Merge 432a515ad into de2d298e3
Pull Request #6947: fix(metadata): remove temporary fix for ArrayCollection

0 of 1 new or added line in 1 file covered. (0.0%)

11655 existing lines in 385 files now uncovered.

0 of 47351 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 ApiPlatform\Validator\Exception\ValidationException;
33
use Symfony\Component\PropertyInfo\Type;
34
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
35
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
36
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
37

38
use const ApiPlatform\JsonLd\HYDRA_CONTEXT;
39

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

50
    public function __construct(
51
        private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory,
52
        private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
53
        private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory,
54
        private readonly ResourceClassResolverInterface $resourceClassResolver,
55
        private readonly UrlGeneratorInterface $urlGenerator,
56
        private readonly ?NameConverterInterface $nameConverter = null,
57
        private readonly ?array $defaultContext = [],
58
    ) {
UNCOV
59
    }
×
60

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

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

73
            $resourceMetadata = $resourceMetadataCollection[0];
×
74
            if ($resourceMetadata instanceof ErrorResource && ValidationException::class === $resourceMetadata->getClass()) {
×
75
                continue;
×
76
            }
77

78
            $shortName = $resourceMetadata->getShortName();
×
79

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

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

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

98
        $entrypointProperty = [
×
99
            '@type' => $hydraPrefix.'SupportedProperty',
×
100
            $hydraPrefix.'property' => [
×
101
                '@id' => \sprintf('#Entrypoint/%s', lcfirst($shortName)),
×
102
                '@type' => $hydraPrefix.'Link',
×
103
                'domain' => '#Entrypoint',
×
104
                'rdfs:label' => "The collection of $shortName resources",
×
105
                'rdfs:range' => [
×
106
                    ['@id' => $hydraPrefix.'Collection'],
×
107
                    [
×
108
                        'owl:equivalentClass' => [
×
109
                            'owl:onProperty' => ['@id' => $hydraPrefix.'member'],
×
110
                            'owl:allValuesFrom' => ['@id' => $prefixedShortName],
×
111
                        ],
×
112
                    ],
×
113
                ],
×
114
                $hydraPrefix.'supportedOperation' => $hydraCollectionOperations,
×
115
            ],
×
116
            $hydraPrefix.'title' => "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
    {
133
        $description = $resourceMetadata->getDescription();
×
134
        $isDeprecated = $resourceMetadata->getDeprecationReason();
×
135

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

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

149
        if ($isDeprecated) {
×
150
            $class['owl:deprecated'] = true;
×
151
        }
152

153
        return $class;
×
154
    }
155

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

169
        if ($normalizationGroups) {
×
170
            $propertyNameContext['serializer_groups'] = $normalizationGroups;
×
171
        }
172

173
        if (!$denormalizationGroups) {
×
174
            return [$propertyNameContext, $propertyContext];
×
175
        }
176

177
        if (!isset($propertyNameContext['serializer_groups'])) {
×
178
            $propertyNameContext['serializer_groups'] = $denormalizationGroups;
×
179

180
            return [$propertyNameContext, $propertyContext];
×
181
        }
182

183
        foreach ($denormalizationGroups as $group) {
×
184
            $propertyNameContext['serializer_groups'][] = $group;
×
185
        }
186

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

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

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

204
            $inputMetadata = $operation->getInput();
×
205
            if (null !== $inputClass = $inputMetadata['class'] ?? null) {
×
206
                $classes[$inputClass] = true;
×
207
            }
208

209
            $outputMetadata = $operation->getOutput();
×
210
            if (null !== $outputClass = $outputMetadata['class'] ?? null) {
×
211
                $classes[$outputClass] = true;
×
212
            }
213
        }
214

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

223
                if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) {
×
224
                    continue;
×
225
                }
226

227
                if ($this->nameConverter) {
×
228
                    $propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context);
×
229
                }
230

231
                $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName, $hydraPrefix);
×
232
            }
233
        }
234

235
        return $properties;
×
236
    }
237

238
    /**
239
     * Gets Hydra operations.
240
     */
241
    private function getHydraOperations(bool $collection, ?ResourceMetadataCollection $resourceMetadataCollection = null, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
242
    {
243
        $hydraOperations = [];
×
244
        foreach ($resourceMetadataCollection as $resourceMetadata) {
×
245
            foreach ($resourceMetadata->getOperations() as $operation) {
×
246
                if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
×
247
                    continue;
×
248
                }
249
                $hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix);
×
250
            }
251
        }
252

253
        return $hydraOperations;
×
254
    }
255

256
    /**
257
     * Gets and populates if applicable a Hydra operation.
258
     */
259
    private function getHydraOperation(HttpOperation $operation, string $prefixedShortName, string $hydraPrefix): array
260
    {
261
        $method = $operation->getMethod() ?: 'GET';
×
262

263
        $hydraOperation = $operation->getHydraContext() ?? [];
×
264
        if ($operation->getDeprecationReason()) {
×
265
            $hydraOperation['owl:deprecated'] = true;
×
266
        }
267

268
        $shortName = $operation->getShortName();
×
269
        $inputMetadata = $operation->getInput() ?? [];
×
270
        $outputMetadata = $operation->getOutput() ?? [];
×
271

272
        $inputClass = \array_key_exists('class', $inputMetadata) ? $inputMetadata['class'] : false;
×
273
        $outputClass = \array_key_exists('class', $outputMetadata) ? $outputMetadata['class'] : false;
×
274

275
        if ('GET' === $method && $operation instanceof CollectionOperationInterface) {
×
276
            $hydraOperation += [
×
277
                '@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
×
278
                $hydraPrefix.'title' => "Retrieves the collection of $shortName resources.",
×
279
                'returns' => null === $outputClass ? 'owl:Nothing' : $hydraPrefix.'Collection',
×
280
            ];
×
281
        } elseif ('GET' === $method) {
×
282
            $hydraOperation += [
×
283
                '@type' => [$hydraPrefix.'Operation', 'schema:FindAction'],
×
284
                $hydraPrefix.'title' => "Retrieves a $shortName resource.",
×
285
                'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
×
286
            ];
×
287
        } elseif ('PATCH' === $method) {
×
288
            $hydraOperation += [
×
289
                '@type' => $hydraPrefix.'Operation',
×
290
                $hydraPrefix.'title' => "Updates the $shortName resource.",
×
291
                'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
×
292
                'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
×
293
            ];
×
294
        } elseif ('POST' === $method) {
×
295
            $hydraOperation += [
×
296
                '@type' => [$hydraPrefix.'Operation', 'schema:CreateAction'],
×
297
                $hydraPrefix.'title' => "Creates a $shortName resource.",
×
298
                'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
×
299
                'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
×
300
            ];
×
301
        } elseif ('PUT' === $method) {
×
302
            $hydraOperation += [
×
303
                '@type' => [$hydraPrefix.'Operation', 'schema:ReplaceAction'],
×
304
                $hydraPrefix.'title' => "Replaces the $shortName resource.",
×
305
                'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName,
×
306
                'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName,
×
307
            ];
×
308
        } elseif ('DELETE' === $method) {
×
309
            $hydraOperation += [
×
310
                '@type' => [$hydraPrefix.'Operation', 'schema:DeleteAction'],
×
311
                $hydraPrefix.'title' => "Deletes the $shortName resource.",
×
312
                'returns' => 'owl:Nothing',
×
313
            ];
×
314
        }
315

316
        $hydraOperation[$hydraPrefix.'method'] ?? $hydraOperation[$hydraPrefix.'method'] = $method;
×
317

318
        if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation[$hydraPrefix.'title'])) {
×
319
            $hydraOperation['rdfs:label'] = $hydraOperation[$hydraPrefix.'title'];
×
320
        }
321

322
        ksort($hydraOperation);
×
323

324
        return $hydraOperation;
×
325
    }
326

327
    /**
328
     * Gets the range of the property.
329
     */
330
    private function getRange(ApiProperty $propertyMetadata): array|string|null
331
    {
332
        $jsonldContext = $propertyMetadata->getJsonldContext();
×
333

334
        if (isset($jsonldContext['@type'])) {
×
335
            return $jsonldContext['@type'];
×
336
        }
337

338
        $builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
×
339
        $types = [];
×
340

341
        foreach ($builtInTypes as $type) {
×
342
            if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueTypes()[0] ?? null) {
×
343
                $type = $collectionType;
×
344
            }
345

346
            switch ($type->getBuiltinType()) {
×
UNCOV
347
                case Type::BUILTIN_TYPE_STRING:
×
348
                    if (!\in_array('xmls:string', $types, true)) {
×
349
                        $types[] = 'xmls:string';
×
350
                    }
351
                    break;
×
UNCOV
352
                case Type::BUILTIN_TYPE_INT:
×
353
                    if (!\in_array('xmls:integer', $types, true)) {
×
354
                        $types[] = 'xmls:integer';
×
355
                    }
356
                    break;
×
UNCOV
357
                case Type::BUILTIN_TYPE_FLOAT:
×
358
                    if (!\in_array('xmls:decimal', $types, true)) {
×
359
                        $types[] = 'xmls:decimal';
×
360
                    }
361
                    break;
×
UNCOV
362
                case Type::BUILTIN_TYPE_BOOL:
×
363
                    if (!\in_array('xmls:boolean', $types, true)) {
×
364
                        $types[] = 'xmls:boolean';
×
365
                    }
366
                    break;
×
UNCOV
367
                case Type::BUILTIN_TYPE_OBJECT:
×
368
                    if (null === $className = $type->getClassName()) {
×
369
                        continue 2;
×
370
                    }
371

372
                    if (is_a($className, \DateTimeInterface::class, true)) {
×
373
                        if (!\in_array('xmls:dateTime', $types, true)) {
×
374
                            $types[] = 'xmls:dateTime';
×
375
                        }
376
                        break;
×
377
                    }
378

379
                    if ($this->resourceClassResolver->isResourceClass($className)) {
×
380
                        $resourceMetadata = $this->resourceMetadataFactory->create($className);
×
381
                        $operation = $resourceMetadata->getOperation();
×
382

383
                        if (!$operation instanceof HttpOperation || !$operation->getTypes()) {
×
384
                            if (!\in_array("#{$operation->getShortName()}", $types, true)) {
×
385
                                $types[] = "#{$operation->getShortName()}";
×
386
                            }
387
                            break;
×
388
                        }
389

390
                        $types = array_unique(array_merge($types, $operation->getTypes()));
×
391
                        break;
×
392
                    }
393
            }
394
        }
395

396
        if ([] === $types) {
×
397
            return null;
×
398
        }
399

400
        return 1 === \count($types) ? $types[0] : $types;
×
401
    }
402

403
    private function isSingleRelation(ApiProperty $propertyMetadata): bool
404
    {
405
        $builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
×
406

407
        foreach ($builtInTypes as $type) {
×
408
            $className = $type->getClassName();
×
409
            if (
410
                !$type->isCollection()
×
411
                && null !== $className
×
412
                && $this->resourceClassResolver->isResourceClass($className)
×
413
            ) {
414
                return true;
×
415
            }
416
        }
417

418
        return false;
×
419
    }
420

421
    /**
422
     * Builds the classes array.
423
     */
424
    private function getClasses(array $entrypointProperties, array $classes, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
425
    {
426
        $classes[] = [
×
427
            '@id' => '#Entrypoint',
×
428
            '@type' => $hydraPrefix.'Class',
×
429
            $hydraPrefix.'title' => 'The API entrypoint',
×
430
            $hydraPrefix.'supportedProperty' => $entrypointProperties,
×
431
            $hydraPrefix.'supportedOperation' => [
×
432
                '@type' => $hydraPrefix.'Operation',
×
433
                $hydraPrefix.'method' => 'GET',
×
434
                'rdfs:label' => 'The API entrypoint.',
×
435
                'returns' => 'EntryPoint',
×
436
            ],
×
437
        ];
×
438

439
        // Constraint violation
440
        $classes[] = [
×
441
            '@id' => '#ConstraintViolation',
×
442
            '@type' => $hydraPrefix.'Class',
×
443
            $hydraPrefix.'title' => 'A constraint violation',
×
444
            $hydraPrefix.'supportedProperty' => [
×
445
                [
×
446
                    '@type' => $hydraPrefix.'SupportedProperty',
×
447
                    $hydraPrefix.'property' => [
×
448
                        '@id' => '#ConstraintViolation/propertyPath',
×
449
                        '@type' => 'rdf:Property',
×
450
                        'rdfs:label' => 'propertyPath',
×
451
                        'domain' => '#ConstraintViolation',
×
452
                        'range' => 'xmls:string',
×
453
                    ],
×
454
                    $hydraPrefix.'title' => 'propertyPath',
×
455
                    $hydraPrefix.'description' => 'The property path of the violation',
×
456
                    $hydraPrefix.'readable' => true,
×
457
                    $hydraPrefix.'writeable' => false,
×
458
                ],
×
459
                [
×
460
                    '@type' => $hydraPrefix.'SupportedProperty',
×
461
                    $hydraPrefix.'property' => [
×
462
                        '@id' => '#ConstraintViolation/message',
×
463
                        '@type' => 'rdf:Property',
×
464
                        'rdfs:label' => 'message',
×
465
                        'domain' => '#ConstraintViolation',
×
466
                        'range' => 'xmls:string',
×
467
                    ],
×
468
                    $hydraPrefix.'title' => 'message',
×
469
                    $hydraPrefix.'description' => 'The message associated with the violation',
×
470
                    $hydraPrefix.'readable' => true,
×
471
                    $hydraPrefix.'writeable' => false,
×
472
                ],
×
473
            ],
×
474
        ];
×
475

476
        // Constraint violation list
477
        $classes[] = [
×
478
            '@id' => '#ConstraintViolationList',
×
479
            '@type' => $hydraPrefix.'Class',
×
480
            'subClassOf' => $hydraPrefix.'Error',
×
481
            $hydraPrefix.'title' => 'A constraint violation list',
×
482
            $hydraPrefix.'supportedProperty' => [
×
483
                [
×
484
                    '@type' => $hydraPrefix.'SupportedProperty',
×
485
                    $hydraPrefix.'property' => [
×
486
                        '@id' => '#ConstraintViolationList/violations',
×
487
                        '@type' => 'rdf:Property',
×
488
                        'rdfs:label' => 'violations',
×
489
                        'domain' => '#ConstraintViolationList',
×
490
                        'range' => '#ConstraintViolation',
×
491
                    ],
×
492
                    $hydraPrefix.'title' => 'violations',
×
493
                    $hydraPrefix.'description' => 'The violations',
×
494
                    $hydraPrefix.'readable' => true,
×
495
                    $hydraPrefix.'writeable' => false,
×
496
                ],
×
497
            ],
×
498
        ];
×
499

500
        return $classes;
×
501
    }
502

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

512
        if (!isset($iri)) {
×
513
            $iri = "#$shortName/$propertyName";
×
514
        }
515

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

523
        if (!isset($propertyData['owl:deprecated']) && $propertyMetadata->getDeprecationReason()) {
×
524
            $propertyData['owl:deprecated'] = true;
×
525
        }
526

527
        if (!isset($propertyData['owl:maxCardinality']) && $this->isSingleRelation($propertyMetadata)) {
×
528
            $propertyData['owl:maxCardinality'] = 1;
×
529
        }
530

531
        if (!isset($propertyData['range']) && null !== $range = $this->getRange($propertyMetadata)) {
×
532
            $propertyData['range'] = $range;
×
533
        }
534

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

544
        if (null !== $description = $propertyMetadata->getDescription()) {
×
545
            $property[$hydraPrefix.'description'] = $description;
×
546
        }
547

548
        return $property;
×
549
    }
550

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

558
        if ('' !== $object->getTitle()) {
×
559
            $doc[$hydraPrefix.'title'] = $object->getTitle();
×
560
        }
561

562
        if ('' !== $object->getDescription()) {
×
563
            $doc[$hydraPrefix.'description'] = $object->getDescription();
×
564
        }
565

566
        $doc[$hydraPrefix.'entrypoint'] = $this->urlGenerator->generate('api_entrypoint');
×
567
        $doc[$hydraPrefix.'supportedClass'] = $classes;
×
568

569
        return $doc;
×
570
    }
571

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

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

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