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

api-platform / core / 14836358929

05 May 2025 12:24PM UTC coverage: 8.396% (-15.0%) from 23.443%
14836358929

push

github

soyuka
test: property info deprecation

0 of 300 new or added lines in 4 files covered. (0.0%)

2655 existing lines in 165 files now uncovered.

13444 of 160118 relevant lines covered (8.4%)

22.88 hits per line

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

86.81
/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\Metadata\Util\TypeHelper;
33
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;
34
use Symfony\Component\PropertyInfo\Type as LegacyType;
35
use Symfony\Component\Serializer\NameConverter\NameConverterInterface;
36
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
37
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
38
use Symfony\Component\TypeInfo\Type;
39
use Symfony\Component\TypeInfo\Type\CollectionType;
40
use Symfony\Component\TypeInfo\Type\ObjectType;
41
use Symfony\Component\TypeInfo\TypeIdentifier;
42

43
use const ApiPlatform\JsonLd\HYDRA_CONTEXT;
44

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

55
    public function __construct(
56
        private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory,
57
        private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
58
        private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory,
59
        private readonly ResourceClassResolverInterface $resourceClassResolver,
60
        private readonly UrlGeneratorInterface $urlGenerator,
61
        private readonly ?NameConverterInterface $nameConverter = null,
62
        private readonly ?array $defaultContext = [],
63
        private readonly ?bool $entrypointEnabled = true,
64
    ) {
65
    }
2,021✔
66

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

76
        foreach ($object->getResourceNameCollection() as $resourceClass) {
10✔
77
            $resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass);
10✔
78

79
            $resourceMetadata = $resourceMetadataCollection[0];
10✔
80
            if (true === $resourceMetadata->getHideHydraOperation()) {
10✔
81
                continue;
10✔
82
            }
83

84
            $shortName = $resourceMetadata->getShortName();
10✔
85
            $prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName";
10✔
86

87
            $this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $hydraPrefix, $resourceMetadataCollection);
10✔
88
            $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix, $resourceMetadataCollection);
10✔
89
        }
90

91
        return $this->computeDoc($object, $this->getClasses($entrypointProperties, $classes, $hydraPrefix), $hydraPrefix);
10✔
92
    }
93

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

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

UNCOV
128
        if ($resourceMetadata->getDeprecationReason()) {
8✔
UNCOV
129
            $entrypointProperty['owl:deprecated'] = true;
8✔
130
        }
131

UNCOV
132
        $entrypointProperties[] = $entrypointProperty;
8✔
133
    }
134

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

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

151
        if (null !== $description) {
10✔
152
            $class[$hydraPrefix.'description'] = $description;
10✔
153
        }
154

155
        if ($resourceMetadata instanceof ErrorResource) {
10✔
156
            $class['subClassOf'] = 'Error';
10✔
157
        }
158

159
        if ($isDeprecated) {
10✔
UNCOV
160
            $class['owl:deprecated'] = true;
8✔
161
        }
162

163
        return $class;
10✔
164
    }
165

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

179
        if ($normalizationGroups) {
10✔
UNCOV
180
            $propertyNameContext['serializer_groups'] = $normalizationGroups;
8✔
181
        }
182

183
        if (!$denormalizationGroups) {
10✔
184
            return [$propertyNameContext, $propertyContext];
10✔
185
        }
186

UNCOV
187
        if (!isset($propertyNameContext['serializer_groups'])) {
8✔
UNCOV
188
            $propertyNameContext['serializer_groups'] = $denormalizationGroups;
×
189

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

UNCOV
193
        foreach ($denormalizationGroups as $group) {
8✔
UNCOV
194
            $propertyNameContext['serializer_groups'][] = $group;
8✔
195
        }
196

UNCOV
197
        return [$propertyNameContext, $propertyContext];
8✔
198
    }
199

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

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

214
            $inputMetadata = $operation->getInput();
10✔
215
            if (null !== $inputClass = $inputMetadata['class'] ?? null) {
10✔
UNCOV
216
                $classes[$inputClass] = true;
8✔
217
            }
218

219
            $outputMetadata = $operation->getOutput();
10✔
220
            if (null !== $outputClass = $outputMetadata['class'] ?? null) {
10✔
UNCOV
221
                $classes[$outputClass] = true;
8✔
222
            }
223
        }
224

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

233
                if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) {
10✔
234
                    continue;
10✔
235
                }
236

237
                if ($this->nameConverter) {
10✔
238
                    $propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context);
10✔
239
                }
240

241
                if (false === $propertyMetadata->getHydra()) {
10✔
242
                    continue;
10✔
243
                }
244

245
                $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName, $hydraPrefix);
10✔
246
            }
247
        }
248

249
        return $properties;
10✔
250
    }
251

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

264
                if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) {
10✔
265
                    continue;
10✔
266
                }
267

268
                $hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix);
10✔
269
            }
270
        }
271

272
        return $hydraOperations;
10✔
273
    }
274

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

282
        $hydraOperation = $operation->getHydraContext() ?? [];
10✔
283
        if ($operation->getDeprecationReason()) {
10✔
UNCOV
284
            $hydraOperation['owl:deprecated'] = true;
8✔
285
        }
286

287
        $shortName = $operation->getShortName();
10✔
288
        $inputMetadata = $operation->getInput() ?? [];
10✔
289
        $outputMetadata = $operation->getOutput() ?? [];
10✔
290

291
        $inputClass = \array_key_exists('class', $inputMetadata) ? $inputMetadata['class'] : false;
10✔
292
        $outputClass = \array_key_exists('class', $outputMetadata) ? $outputMetadata['class'] : false;
10✔
293

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

UNCOV
314
            if (null !== $inputClass) {
8✔
UNCOV
315
                $possibleValue = [];
8✔
UNCOV
316
                foreach ($operation->getInputFormats() as $mimeTypes) {
8✔
UNCOV
317
                    foreach ($mimeTypes as $mimeType) {
8✔
UNCOV
318
                        $possibleValue[] = $mimeType;
8✔
319
                    }
320
                }
321

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

346
        $hydraOperation[$hydraPrefix.'method'] ??= $method;
10✔
347
        $hydraOperation[$hydraPrefix.'title'] ??= strtolower($method).$shortName.($operation instanceof CollectionOperationInterface ? 'Collection' : '');
10✔
348

349
        ksort($hydraOperation);
10✔
350

351
        return $hydraOperation;
10✔
352
    }
353

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

361
        if (isset($jsonldContext['@type'])) {
10✔
362
            return $jsonldContext['@type'];
10✔
363
        }
364

365
        $types = [];
10✔
366

367
        if (method_exists(PropertyInfoExtractor::class, 'getType')) {
10✔
368
            $nativeType = $propertyMetadata->getNativeType();
10✔
369
            if (null === $nativeType) {
10✔
UNCOV
370
                return null;
8✔
371
            }
372

373
            if ($nativeType->isSatisfiedBy(fn ($t) => $t instanceof CollectionType)) {
10✔
374
                $nativeType = TypeHelper::getCollectionValueType($nativeType);
10✔
375
            }
376

377
            // Check for specific types after potentially unwrapping the collection
378
            if (null === $nativeType) {
10✔
UNCOV
379
                return null; // Should not happen if collection had a value type, but safety check
×
380
            }
381

382
            if ($nativeType->isIdentifiedBy(TypeIdentifier::STRING)) {
10✔
383
                $types[] = 'xmls:string';
10✔
384
            }
385

386
            if ($nativeType->isIdentifiedBy(TypeIdentifier::INT)) {
10✔
387
                $types[] = 'xmls:integer';
10✔
388
            }
389

390
            if ($nativeType->isIdentifiedBy(TypeIdentifier::FLOAT)) {
10✔
391
                $types[] = 'xmls:decimal';
8✔
392
            }
393

394
            if ($nativeType->isIdentifiedBy(TypeIdentifier::BOOL)) {
10✔
UNCOV
395
                $types[] = 'xmls:boolean';
8✔
396
            }
397

398
            if ($nativeType->isIdentifiedBy(\DateTimeInterface::class)) {
10✔
UNCOV
399
                $types[] = 'xmls:dateTime';
8✔
400
            }
401

402
            /** @var class-string|null $className */
403
            $className = null;
10✔
404

405
            $typeIsResourceClass = function (Type $type) use (&$className): bool {
10✔
406
                return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
10✔
407
            };
10✔
408

409
            if ($nativeType->isSatisfiedBy($typeIsResourceClass) && $className) {
10✔
UNCOV
410
                $resourceMetadata = $this->resourceMetadataFactory->create($className);
8✔
UNCOV
411
                $operation = $resourceMetadata->getOperation();
8✔
412

UNCOV
413
                if (!$operation instanceof HttpOperation || !$operation->getTypes()) {
8✔
UNCOV
414
                    if (!\in_array("#{$operation->getShortName()}", $types, true)) {
8✔
UNCOV
415
                        $types[] = "#{$operation->getShortName()}";
8✔
416
                    }
417
                } else {
UNCOV
418
                    $types = array_unique(array_merge($types, $operation->getTypes()));
8✔
419
                }
420
            }
421
        // TODO: remove in 5.x
422
        } else {
UNCOV
423
            $builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
×
424

UNCOV
425
            foreach ($builtInTypes as $type) {
×
UNCOV
426
                if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueTypes()[0] ?? null) {
×
UNCOV
427
                    $type = $collectionType;
×
428
                }
429

UNCOV
430
                switch ($type->getBuiltinType()) {
×
UNCOV
431
                    case LegacyType::BUILTIN_TYPE_STRING:
×
UNCOV
432
                        if (!\in_array('xmls:string', $types, true)) {
×
UNCOV
433
                            $types[] = 'xmls:string';
×
434
                        }
435
                        break;
×
UNCOV
436
                    case LegacyType::BUILTIN_TYPE_INT:
×
437
                        if (!\in_array('xmls:integer', $types, true)) {
×
438
                            $types[] = 'xmls:integer';
×
439
                        }
UNCOV
440
                        break;
×
UNCOV
441
                    case LegacyType::BUILTIN_TYPE_FLOAT:
×
442
                        if (!\in_array('xmls:decimal', $types, true)) {
×
443
                            $types[] = 'xmls:decimal';
×
444
                        }
445
                        break;
×
UNCOV
446
                    case LegacyType::BUILTIN_TYPE_BOOL:
×
447
                        if (!\in_array('xmls:boolean', $types, true)) {
×
448
                            $types[] = 'xmls:boolean';
×
449
                        }
450
                        break;
×
UNCOV
451
                    case LegacyType::BUILTIN_TYPE_OBJECT:
×
452
                        if (null === $className = $type->getClassName()) {
×
453
                            continue 2;
×
454
                        }
455

UNCOV
456
                        if (is_a($className, \DateTimeInterface::class, true)) {
×
457
                            if (!\in_array('xmls:dateTime', $types, true)) {
×
458
                                $types[] = 'xmls:dateTime';
×
459
                            }
460
                            break;
×
461
                        }
462

463
                        if ($this->resourceClassResolver->isResourceClass($className)) {
×
464
                            $resourceMetadata = $this->resourceMetadataFactory->create($className);
×
465
                            $operation = $resourceMetadata->getOperation();
×
466

UNCOV
467
                            if (!$operation instanceof HttpOperation || !$operation->getTypes()) {
×
468
                                if (!\in_array("#{$operation->getShortName()}", $types, true)) {
×
469
                                    $types[] = "#{$operation->getShortName()}";
×
470
                                }
UNCOV
471
                                break;
×
472
                            }
473

UNCOV
474
                            $types = array_unique(array_merge($types, $operation->getTypes()));
×
475
                            break;
×
476
                        }
477
                }
478
            }
479
        }
480

481
        if ([] === $types) {
10✔
482
            return null;
10✔
483
        }
484

485
        $types = array_unique($types);
10✔
486

487
        return 1 === \count($types) ? $types[0] : $types;
10✔
488
    }
489

490
    private function isSingleRelation(ApiProperty $propertyMetadata): bool
491
    {
492
        if (method_exists(PropertyInfoExtractor::class, 'getType')) {
10✔
493
            $nativeType = $propertyMetadata->getNativeType();
10✔
494
            if (null === $nativeType) {
10✔
UNCOV
495
                return false;
8✔
496
            }
497

498
            if ($nativeType instanceof CollectionType) {
10✔
499
                return false;
10✔
500
            }
501

502
            $typeIsResourceClass = function (Type $type) use (&$className): bool {
10✔
503
                return $type instanceof ObjectType && $this->resourceClassResolver->isResourceClass($className = $type->getClassName());
10✔
504
            };
10✔
505

506
            return $nativeType->isSatisfiedBy($typeIsResourceClass);
10✔
507
        }
508

509
        // TODO: remove in 5.x
UNCOV
510
        $builtInTypes = $propertyMetadata->getBuiltinTypes() ?? [];
×
511

UNCOV
512
        foreach ($builtInTypes as $type) {
×
UNCOV
513
            $className = $type->getClassName();
×
514
            if (
UNCOV
515
                !$type->isCollection()
×
UNCOV
516
                && null !== $className
×
UNCOV
517
                && $this->resourceClassResolver->isResourceClass($className)
×
518
            ) {
UNCOV
519
                return true;
×
520
            }
521
        }
522

UNCOV
523
        return false;
×
524
    }
525

526
    /**
527
     * Builds the classes array.
528
     */
529
    private function getClasses(array $entrypointProperties, array $classes, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
530
    {
531
        if ($this->entrypointEnabled) {
10✔
532
            $classes[] = [
10✔
533
                '@id' => '#Entrypoint',
10✔
534
                '@type' => $hydraPrefix.'Class',
10✔
535
                $hydraPrefix.'title' => 'Entrypoint',
10✔
536
                $hydraPrefix.'supportedProperty' => $entrypointProperties,
10✔
537
                $hydraPrefix.'supportedOperation' => [
10✔
538
                    '@type' => $hydraPrefix.'Operation',
10✔
539
                    $hydraPrefix.'method' => 'GET',
10✔
540
                    $hydraPrefix.'title' => 'index',
10✔
541
                    $hydraPrefix.'description' => 'The API Entrypoint.',
10✔
542
                    $hydraPrefix.'returns' => 'Entrypoint',
10✔
543
                ],
10✔
544
            ];
10✔
545
        }
546

547
        $classes[] = [
10✔
548
            '@id' => '#ConstraintViolationList',
10✔
549
            '@type' => $hydraPrefix.'Class',
10✔
550
            $hydraPrefix.'title' => 'ConstraintViolationList',
10✔
551
            $hydraPrefix.'description' => 'A constraint violation List.',
10✔
552
            $hydraPrefix.'supportedProperty' => [
10✔
553
                [
10✔
554
                    '@type' => $hydraPrefix.'SupportedProperty',
10✔
555
                    $hydraPrefix.'property' => [
10✔
556
                        '@id' => '#ConstraintViolationList/propertyPath',
10✔
557
                        '@type' => 'rdf:Property',
10✔
558
                        'rdfs:label' => 'propertyPath',
10✔
559
                        'domain' => '#ConstraintViolationList',
10✔
560
                        'range' => 'xmls:string',
10✔
561
                    ],
10✔
562
                    $hydraPrefix.'title' => 'propertyPath',
10✔
563
                    $hydraPrefix.'description' => 'The property path of the violation',
10✔
564
                    $hydraPrefix.'readable' => true,
10✔
565
                    $hydraPrefix.'writeable' => false,
10✔
566
                ],
10✔
567
                [
10✔
568
                    '@type' => $hydraPrefix.'SupportedProperty',
10✔
569
                    $hydraPrefix.'property' => [
10✔
570
                        '@id' => '#ConstraintViolationList/message',
10✔
571
                        '@type' => 'rdf:Property',
10✔
572
                        'rdfs:label' => 'message',
10✔
573
                        'domain' => '#ConstraintViolationList',
10✔
574
                        'range' => 'xmls:string',
10✔
575
                    ],
10✔
576
                    $hydraPrefix.'title' => 'message',
10✔
577
                    $hydraPrefix.'description' => 'The message associated with the violation',
10✔
578
                    $hydraPrefix.'readable' => true,
10✔
579
                    $hydraPrefix.'writeable' => false,
10✔
580
                ],
10✔
581
            ],
10✔
582
        ];
10✔
583

584
        return $classes;
10✔
585
    }
586

587
    /**
588
     * Gets a property definition.
589
     */
590
    private function getProperty(ApiProperty $propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName, string $hydraPrefix): array
591
    {
592
        if ($iri = $propertyMetadata->getIris()) {
10✔
UNCOV
593
            $iri = 1 === (is_countable($iri) ? \count($iri) : 0) ? $iri[0] : $iri;
8✔
594
        }
595

596
        if (!isset($iri)) {
10✔
597
            $iri = "#$shortName/$propertyName";
10✔
598
        }
599

600
        $propertyData = ($propertyMetadata->getJsonldContext()[$hydraPrefix.'property'] ?? []) + [
10✔
601
            '@id' => $iri,
10✔
602
            '@type' => false === $propertyMetadata->isReadableLink() ? $hydraPrefix.'Link' : 'rdf:Property',
10✔
603
            'domain' => $prefixedShortName,
10✔
604
            'label' => $propertyName,
10✔
605
        ];
10✔
606

607
        if (!isset($propertyData['owl:deprecated']) && $propertyMetadata->getDeprecationReason()) {
10✔
UNCOV
608
            $propertyData['owl:deprecated'] = true;
8✔
609
        }
610

611
        if (!isset($propertyData['owl:maxCardinality']) && $this->isSingleRelation($propertyMetadata)) {
10✔
UNCOV
612
            $propertyData['owl:maxCardinality'] = 1;
8✔
613
        }
614

615
        if (!isset($propertyData['range']) && null !== $range = $this->getRange($propertyMetadata)) {
10✔
616
            $propertyData['range'] = $range;
10✔
617
        }
618

619
        $property = [
10✔
620
            '@type' => $hydraPrefix.'SupportedProperty',
10✔
621
            $hydraPrefix.'property' => $propertyData,
10✔
622
            $hydraPrefix.'title' => $propertyName,
10✔
623
            $hydraPrefix.'required' => $propertyMetadata->isRequired() ?? false,
10✔
624
            $hydraPrefix.'readable' => $propertyMetadata->isReadable(),
10✔
625
            $hydraPrefix.'writeable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(),
10✔
626
        ];
10✔
627

628
        if (null !== $description = $propertyMetadata->getDescription()) {
10✔
629
            $property[$hydraPrefix.'description'] = $description;
10✔
630
        }
631

632
        return $property;
10✔
633
    }
634

635
    /**
636
     * Computes the documentation.
637
     */
638
    private function computeDoc(Documentation $object, array $classes, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
639
    {
640
        $doc = ['@context' => $this->getContext($hydraPrefix), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT]), '@type' => $hydraPrefix.'ApiDocumentation'];
10✔
641

642
        if ('' !== $object->getTitle()) {
10✔
643
            $doc[$hydraPrefix.'title'] = $object->getTitle();
10✔
644
        }
645

646
        if ('' !== $object->getDescription()) {
10✔
647
            $doc[$hydraPrefix.'description'] = $object->getDescription();
10✔
648
        }
649

650
        if ($this->entrypointEnabled) {
10✔
651
            $doc[$hydraPrefix.'entrypoint'] = $this->urlGenerator->generate('api_entrypoint');
10✔
652
        }
653
        $doc[$hydraPrefix.'supportedClass'] = $classes;
10✔
654

655
        return $doc;
10✔
656
    }
657

658
    /**
659
     * Builds the JSON-LD context for the API documentation.
660
     */
661
    private function getContext(string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array
662
    {
663
        return [
10✔
664
            HYDRA_CONTEXT,
10✔
665
            [
10✔
666
                '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#',
10✔
667
                'hydra' => ContextBuilderInterface::HYDRA_NS,
10✔
668
                'rdf' => ContextBuilderInterface::RDF_NS,
10✔
669
                'rdfs' => ContextBuilderInterface::RDFS_NS,
10✔
670
                'xmls' => ContextBuilderInterface::XML_NS,
10✔
671
                'owl' => ContextBuilderInterface::OWL_NS,
10✔
672
                'schema' => ContextBuilderInterface::SCHEMA_ORG_NS,
10✔
673
                'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'],
10✔
674
                'range' => ['@id' => 'rdfs:range', '@type' => '@id'],
10✔
675
                'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'],
10✔
676
            ],
10✔
677
        ];
10✔
678
    }
679

680
    /**
681
     * {@inheritdoc}
682
     */
683
    public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool
684
    {
685
        return self::FORMAT === $format && $data instanceof Documentation;
10✔
686
    }
687

688
    public function getSupportedTypes($format): array
689
    {
690
        return self::FORMAT === $format ? [Documentation::class => true] : [];
1,854✔
691
    }
692
}
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