Completed
Push — master ( 0c3139...e3501a )
by Antoine
17s
created

src/Hydra/Serializer/DocumentationNormalizer.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
declare(strict_types=1);
13
14
namespace ApiPlatform\Core\Hydra\Serializer;
15
16
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
17
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
18
use ApiPlatform\Core\Api\UrlGeneratorInterface;
19
use ApiPlatform\Core\Documentation\Documentation;
20
use ApiPlatform\Core\JsonLd\ContextBuilderInterface;
21
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
22
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
23
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
24
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
25
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
26
use Symfony\Component\PropertyInfo\Type;
27
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
28
29
/**
30
 * Creates a machine readable Hydra API documentation.
31
 *
32
 * @author Kévin Dunglas <[email protected]>
33
 */
34
final class DocumentationNormalizer implements NormalizerInterface
35
{
36
    const FORMAT = 'jsonld';
37
38
    private $resourceMetadataFactory;
39
    private $propertyNameCollectionFactory;
40
    private $propertyMetadataFactory;
41
    private $resourceClassResolver;
42
    private $operationMethodResolver;
43
    private $urlGenerator;
44
45 View Code Duplication
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, UrlGeneratorInterface $urlGenerator)
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
46
    {
47
        $this->resourceMetadataFactory = $resourceMetadataFactory;
48
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
49
        $this->propertyMetadataFactory = $propertyMetadataFactory;
50
        $this->resourceClassResolver = $resourceClassResolver;
51
        $this->operationMethodResolver = $operationMethodResolver;
52
        $this->urlGenerator = $urlGenerator;
53
    }
54
55
    /**
56
     * {@inheritdoc}
57
     */
58
    public function normalize($object, $format = null, array $context = [])
59
    {
60
        $classes = [];
61
        $entrypointProperties = [];
62
63
        foreach ($object->getResourceNameCollection() as $resourceClass) {
64
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
65
            $shortName = $resourceMetadata->getShortName();
66
            $prefixedShortName = $resourceMetadata->getIri() ?? "#$shortName";
67
68
            $this->populateEntrypointProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties);
69
            $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName);
70
        }
71
72
        return $this->computeDoc($object, $this->getClasses($entrypointProperties, $classes));
73
    }
74
75
    /**
76
     * Populates entrypoint properties.
77
     *
78
     * @param string           $resourceClass
79
     * @param ResourceMetadata $resourceMetadata
80
     * @param string           $shortName
81
     * @param string           $prefixedShortName
82
     * @param array            $entrypointProperties
83
     */
84
    private function populateEntrypointProperties(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName, array &$entrypointProperties)
85
    {
86
        $hydraCollectionOperations = $this->getHydraOperations($resourceClass, $resourceMetadata, $prefixedShortName, true);
87
        if (empty($hydraCollectionOperations)) {
88
            return;
89
        }
90
91
        $entrypointProperties[] = [
92
            '@type' => 'hydra:SupportedProperty',
93
            'hydra:property' => [
94
                '@id' => sprintf('#Entrypoint/%s', lcfirst($shortName)),
95
                '@type' => 'hydra:Link',
96
                'domain' => '#Entrypoint',
97
                'rdfs:label' => "The collection of $shortName resources",
98
                'rdfs:range' => [
99
                    'hydra:Collection',
100
                    [
101
                        'owl:equivalentClass' => [
102
                            'owl:onProperty' => 'hydra:member',
103
                            'owl:allValuesFrom' => "#$shortName",
104
                        ],
105
                    ],
106
                ],
107
                'hydra:supportedOperation' => $hydraCollectionOperations,
108
            ],
109
            'hydra:title' => "The collection of $shortName resources",
110
            'hydra:readable' => true,
111
            'hydra:writable' => false,
112
        ];
113
    }
114
115
    /**
116
     * Gets a Hydra class.
117
     *
118
     * @param string           $resourceClass
119
     * @param ResourceMetadata $resourceMetadata
120
     * @param string           $shortName
121
     * @param string           $prefixedShortName
122
     *
123
     * @return array
124
     */
125
    private function getClass(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName): array
126
    {
127
        $class = [
128
            '@id' => $prefixedShortName,
129
            '@type' => 'hydra:Class',
130
            'rdfs:label' => $shortName,
131
            'hydra:title' => $shortName,
132
            'hydra:supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName),
133
            'hydra:supportedOperation' => $this->getHydraOperations($resourceClass, $resourceMetadata, $prefixedShortName, false),
134
        ];
135
136
        if (null !== $description = $resourceMetadata->getDescription()) {
137
            $class['hydra:description'] = $description;
138
        }
139
140
        return $class;
141
    }
142
143
    /**
144
     * Gets the context for the property name factory.
145
     *
146
     * @param ResourceMetadata $resourceMetadata
147
     *
148
     * @return array
149
     */
150
    private function getPropertyNameCollectionFactoryContext(ResourceMetadata $resourceMetadata): array
151
    {
152
        $attributes = $resourceMetadata->getAttributes();
153
        $context = [];
154
155 View Code Duplication
        if (isset($attributes['normalization_context']['groups'])) {
156
            $context['serializer_groups'] = $attributes['normalization_context']['groups'];
157
        }
158
159
        if (isset($attributes['denormalization_context']['groups'])) {
160 View Code Duplication
            if (isset($context['serializer_groups'])) {
161
                $context['serializer_groups'] += $attributes['denormalization_context']['groups'];
162
            } else {
163
                $context['serializer_groups'] = $attributes['denormalization_context']['groups'];
164
            }
165
        }
166
167
        return $context;
168
    }
169
170
    /**
171
     * Gets Hydra properties.
172
     *
173
     * @param string           $resourceClass
174
     * @param ResourceMetadata $resourceMetadata
175
     * @param string           $shortName
176
     * @param string           $prefixedShortName
177
     *
178
     * @return array
179
     */
180
    private function getHydraProperties(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName): array
181
    {
182
        $properties = [];
183
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, $this->getPropertyNameCollectionFactoryContext($resourceMetadata)) as $propertyName) {
184
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
185
            if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) {
186
                continue;
187
            }
188
189
            $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName);
190
        }
191
192
        return $properties;
193
    }
194
195
    /**
196
     * Gets Hydra operations.
197
     *
198
     * @param string           $resourceClass
199
     * @param ResourceMetadata $resourceMetadata
200
     * @param string           $prefixedShortName
201
     * @param bool             $collection
202
     *
203
     * @return array
204
     */
205
    private function getHydraOperations(string $resourceClass, ResourceMetadata $resourceMetadata, string $prefixedShortName, bool $collection): array
206
    {
207
        if (null === $operations = $collection ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
208
            return [];
209
        }
210
211
        $hydraOperations = [];
212
        foreach ($operations as $operationName => $operation) {
213
            $hydraOperations[] = $this->getHydraOperation($resourceClass, $resourceMetadata, $operationName, $operation, $prefixedShortName, $collection);
214
        }
215
216
        return $hydraOperations;
217
    }
218
219
    /**
220
     * Gets and populates if applicable a Hydra operation.
221
     *
222
     * @param string           $resourceClass
223
     * @param ResourceMetadata $resourceMetadata
224
     * @param string           $operationName
225
     * @param array            $operation
226
     * @param string           $prefixedShortName
227
     * @param bool             $collection
228
     *
229
     * @return array
230
     */
231
    private function getHydraOperation(string $resourceClass, ResourceMetadata $resourceMetadata, string $operationName, array $operation, string $prefixedShortName, bool $collection): array
232
    {
233
        if ($collection) {
234
            $method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
235
        } else {
236
            $method = $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName);
237
        }
238
239
        $hydraOperation = $operation['hydra_context'] ?? [];
240
        $shortName = $resourceMetadata->getShortName();
241
242
        if ('GET' === $method && $collection) {
243
            $hydraOperation = [
244
                '@type' => ['hydra:Operation', 'schema:FindAction'],
245
                'hydra:title' => "Retrieves the collection of $shortName resources.",
246
                'returns' => 'hydra:Collection',
247
            ] + $hydraOperation;
248 View Code Duplication
        } elseif ('GET' === $method) {
249
            $hydraOperation = [
250
                '@type' => ['hydra:Operation', 'schema:FindAction'],
251
                'hydra:title' => "Retrieves $shortName resource.",
252
                'returns' => $prefixedShortName,
253
            ] + $hydraOperation;
254
        } elseif ('POST' === $method) {
255
            $hydraOperation = [
256
                '@type' => ['hydra:Operation', 'schema:CreateAction'],
257
                'hydra:title' => "Creates a $shortName resource.",
258
                'returns' => $prefixedShortName,
259
                'expects' => $prefixedShortName,
260
            ] + $hydraOperation;
261 View Code Duplication
        } elseif ('PUT' === $method) {
0 ignored issues
show
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
262
            $hydraOperation = [
263
                '@type' => ['hydra:Operation', 'schema:ReplaceAction'],
264
                'hydra:title' => "Replaces the $shortName resource.",
265
                'returns' => $prefixedShortName,
266
                'expects' => $prefixedShortName,
267
            ] + $hydraOperation;
268
        } elseif ('DELETE' === $method) {
269
            $hydraOperation = [
270
                '@type' => ['hydra:Operation', 'schema:DeleteAction'],
271
                'hydra:title' => "Deletes the $shortName resource.",
272
                'returns' => 'owl:Nothing',
273
            ] + $hydraOperation;
274
        }
275
276
        $hydraOperation['hydra:method'] ?? $hydraOperation['hydra:method'] = $method;
277
278
        if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation['hydra:title'])) {
279
            $hydraOperation['rdfs:label'] = $hydraOperation['hydra:title'];
280
        }
281
282
        ksort($hydraOperation);
283
284
        return $hydraOperation;
285
    }
286
287
    /**
288
     * Gets the range of the property.
289
     *
290
     * @param PropertyMetadata $propertyMetadata
291
     *
292
     * @return string|null
293
     */
294
    private function getRange(PropertyMetadata $propertyMetadata)
295
    {
296
        $jsonldContext = $propertyMetadata->getAttributes()['jsonld_context'] ?? [];
297
298
        if (isset($jsonldContext['@type'])) {
299
            return $jsonldContext['@type'];
300
        }
301
302
        if (null === $type = $propertyMetadata->getType()) {
303
            return null;
304
        }
305
306
        if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueType()) {
307
            $type = $collectionType;
308
        }
309
310
        switch ($type->getBuiltinType()) {
311
            case Type::BUILTIN_TYPE_STRING:
312
                return 'xmls:string';
313
            case Type::BUILTIN_TYPE_INT:
314
                return 'xmls:integer';
315
            case Type::BUILTIN_TYPE_FLOAT:
316
                return 'xmls:decimal';
317
            case Type::BUILTIN_TYPE_BOOL:
318
                return 'xmls:boolean';
319
            case Type::BUILTIN_TYPE_OBJECT:
320
                if (null === $className = $type->getClassName()) {
321
                    return null;
322
                }
323
324
                if (is_a($className, \DateTimeInterface::class, true)) {
325
                    return 'xmls:dateTime';
326
                }
327
328
                if ($this->resourceClassResolver->isResourceClass($className)) {
329
                    $resourceMetadata = $this->resourceMetadataFactory->create($className);
330
331
                    return $resourceMetadata->getIri() ?? "#{$resourceMetadata->getShortName()}";
332
                }
333
                break;
334
        }
335
    }
336
337
    /**
338
     * Builds the classes array.
339
     *
340
     * @param array $entrypointProperties
341
     * @param array $classes
342
     *
343
     * @return array
344
     */
345
    private function getClasses(array $entrypointProperties, array $classes): array
346
    {
347
        $classes[] = [
348
            '@id' => '#Entrypoint',
349
            '@type' => 'hydra:Class',
350
            'hydra:title' => 'The API entrypoint',
351
            'hydra:supportedProperty' => $entrypointProperties,
352
            'hydra:supportedOperation' => [
353
                '@type' => 'hydra:Operation',
354
                'hydra:method' => 'GET',
355
                'rdfs:label' => 'The API entrypoint.',
356
                'returns' => '#EntryPoint',
357
            ],
358
        ];
359
360
        // Constraint violation
361
        $classes[] = [
362
            '@id' => '#ConstraintViolation',
363
            '@type' => 'hydra:Class',
364
            'hydra:title' => 'A constraint violation',
365
            'hydra:supportedProperty' => [
366
                [
367
                    '@type' => 'hydra:SupportedProperty',
368
                    'hydra:property' => [
369
                        '@id' => '#ConstraintViolation/propertyPath',
370
                        '@type' => 'rdf:Property',
371
                        'rdfs:label' => 'propertyPath',
372
                        'domain' => '#ConstraintViolation',
373
                        'range' => 'xmls:string',
374
                    ],
375
                    'hydra:title' => 'propertyPath',
376
                    'hydra:description' => 'The property path of the violation',
377
                    'hydra:readable' => true,
378
                    'hydra:writable' => false,
379
                ],
380
                [
381
                    '@type' => 'hydra:SupportedProperty',
382
                    'hydra:property' => [
383
                        '@id' => '#ConstraintViolation/message',
384
                        '@type' => 'rdf:Property',
385
                        'rdfs:label' => 'message',
386
                        'domain' => '#ConstraintViolation',
387
                        'range' => 'xmls:string',
388
                    ],
389
                    'hydra:title' => 'message',
390
                    'hydra:description' => 'The message associated with the violation',
391
                    'hydra:readable' => true,
392
                    'hydra:writable' => false,
393
                ],
394
            ],
395
        ];
396
397
        // Constraint violation list
398
        $classes[] = [
399
            '@id' => '#ConstraintViolationList',
400
            '@type' => 'hydra:Class',
401
            'subClassOf' => 'hydra:Error',
402
            'hydra:title' => 'A constraint violation list',
403
            'hydra:supportedProperty' => [
404
                [
405
                    '@type' => 'hydra:SupportedProperty',
406
                    'hydra:property' => [
407
                        '@id' => '#ConstraintViolationList/violations',
408
                        '@type' => 'rdf:Property',
409
                        'rdfs:label' => 'violations',
410
                        'domain' => '#ConstraintViolationList',
411
                        'range' => '#ConstraintViolation',
412
                    ],
413
                    'hydra:title' => 'violations',
414
                    'hydra:description' => 'The violations',
415
                    'hydra:readable' => true,
416
                    'hydra:writable' => false,
417
                ],
418
            ],
419
        ];
420
421
        return $classes;
422
    }
423
424
    /**
425
     * Gets a property definition.
426
     *
427
     * @param PropertyMetadata $propertyMetadata
428
     * @param string           $propertyName
429
     * @param string           $prefixedShortName
430
     * @param string           $shortName
431
     *
432
     * @return array
433
     */
434
    private function getProperty(PropertyMetadata $propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName): array
435
    {
436
        $propertyData = [
437
            '@id' => $propertyMetadata->getIri() ?? "#$shortName/$propertyName",
438
            '@type' => $propertyMetadata->isReadableLink() ? 'rdf:Property' : 'hydra:Link',
439
            'rdfs:label' => $propertyName,
440
            'domain' => $prefixedShortName,
441
        ];
442
443
        $type = $propertyMetadata->getType();
444
445
        if (null !== $type && !$type->isCollection() && (null !== $className = $type->getClassName()) && $this->resourceClassResolver->isResourceClass($className)) {
446
            $propertyData['owl:maxCardinality'] = 1;
447
        }
448
449
        $property = [
450
            '@type' => 'hydra:SupportedProperty',
451
            'hydra:property' => $propertyData,
452
            'hydra:title' => $propertyName,
453
            'hydra:required' => $propertyMetadata->isRequired(),
454
            'hydra:readable' => $propertyMetadata->isReadable(),
455
            'hydra:writable' => $propertyMetadata->isWritable(),
456
        ];
457
458
        if (null !== $range = $this->getRange($propertyMetadata)) {
459
            $property['hydra:property']['range'] = $range;
460
        }
461
462
        if (null !== $description = $propertyMetadata->getDescription()) {
463
            $property['hydra:description'] = $description;
464
        }
465
466
        return $property;
467
    }
468
469
    /**
470
     * Computes the documentation.
471
     *
472
     * @param Documentation $object
473
     * @param array         $classes
474
     *
475
     * @return array
476
     */
477
    private function computeDoc(Documentation $object, array $classes): array
478
    {
479
        $doc = ['@context' => $this->getContext(), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT]), '@type' => 'hydra:ApiDocumentation'];
480
481
        if ('' !== $object->getTitle()) {
482
            $doc['hydra:title'] = $object->getTitle();
483
        }
484
485
        if ('' !== $object->getDescription()) {
486
            $doc['hydra:description'] = $object->getDescription();
487
        }
488
489
        $doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_entrypoint');
490
        $doc['hydra:supportedClass'] = $classes;
491
492
        return $doc;
493
    }
494
495
    /**
496
     * Builds the JSON-LD context for the API documentation.
497
     *
498
     * @return array
499
     */
500
    private function getContext(): array
501
    {
502
        return [
503
            '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#',
504
            'hydra' => ContextBuilderInterface::HYDRA_NS,
505
            'rdf' => ContextBuilderInterface::RDF_NS,
506
            'rdfs' => ContextBuilderInterface::RDFS_NS,
507
            'xmls' => ContextBuilderInterface::XML_NS,
508
            'owl' => ContextBuilderInterface::OWL_NS,
509
            'schema' => ContextBuilderInterface::SCHEMA_ORG_NS,
510
            'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'],
511
            'range' => ['@id' => 'rdfs:range', '@type' => '@id'],
512
            'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'],
513
            'expects' => ['@id' => 'hydra:expects', '@type' => '@id'],
514
            'returns' => ['@id' => 'hydra:returns', '@type' => '@id'],
515
        ];
516
    }
517
518
    /**
519
     * {@inheritdoc}
520
     */
521
    public function supportsNormalization($data, $format = null, array $context = [])
522
    {
523
        return self::FORMAT === $format && $data instanceof Documentation;
524
    }
525
}
526