Completed
Push — master ( 18b7e7...c1f85f )
by Kévin
03:05
created

DocumentationNormalizer   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 477
Duplicated Lines 4.19 %

Coupling/Cohesion

Components 1
Dependencies 10

Importance

Changes 0
Metric Value
wmc 53
lcom 1
cbo 10
dl 20
loc 477
rs 7.4757
c 0
b 0
f 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A populateEntrypointProperties() 0 22 2
A getClass() 0 17 2
A getHydraProperties() 0 14 4
A getHydraOperations() 0 13 4
C getHydraOperation() 12 53 10
A getPropertyNameCollectionFactoryContext() 8 19 4
A normalize() 0 16 2
C getRange() 0 46 13
B getClasses() 0 78 1
B getProperty() 0 26 4
A computeDoc() 0 17 3
A getContext() 0 16 1
A supportsNormalization() 0 4 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like DocumentationNormalizer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DocumentationNormalizer, and based on these observations, apply Extract Interface, too.

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
namespace ApiPlatform\Core\Hydra\Serializer;
13
14
use ApiPlatform\Core\Api\OperationMethodResolverInterface;
15
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
16
use ApiPlatform\Core\Api\UrlGeneratorInterface;
17
use ApiPlatform\Core\Documentation\Documentation;
18
use ApiPlatform\Core\JsonLd\ContextBuilderInterface;
19
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
20
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
21
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
22
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
23
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
24
use Symfony\Component\PropertyInfo\Type;
25
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
26
27
/**
28
 * Creates a machine readable Hydra API documentation.
29
 *
30
 * @author Kévin Dunglas <[email protected]>
31
 */
32
final class DocumentationNormalizer implements NormalizerInterface
33
{
34
    const FORMAT = 'jsonld';
35
36
    private $resourceMetadataFactory;
37
    private $propertyNameCollectionFactory;
38
    private $propertyMetadataFactory;
39
    private $resourceClassResolver;
40
    private $operationMethodResolver;
41
    private $urlGenerator;
42
43
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, UrlGeneratorInterface $urlGenerator)
44
    {
45
        $this->resourceMetadataFactory = $resourceMetadataFactory;
46
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
47
        $this->propertyMetadataFactory = $propertyMetadataFactory;
48
        $this->resourceClassResolver = $resourceClassResolver;
49
        $this->operationMethodResolver = $operationMethodResolver;
50
        $this->urlGenerator = $urlGenerator;
51
    }
52
53
    /**
54
     * {@inheritdoc}
55
     */
56
    public function normalize($object, $format = null, array $context = [])
57
    {
58
        $classes = [];
59
        $entrypointProperties = [];
60
61
        foreach ($object->getResourceNameCollection() as $resourceClass) {
62
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
63
            $shortName = $resourceMetadata->getShortName();
64
            $prefixedShortName = $resourceMetadata->getIri() ?? "#$shortName";
65
66
            $this->populateEntrypointProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties);
67
            $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName);
68
        }
69
70
        return $this->computeDoc($object, $this->getClasses($entrypointProperties, $classes));
71
    }
72
73
    /**
74
     * Populates entrypoint properties.
75
     *
76
     * @param string           $resourceClass
77
     * @param ResourceMetadata $resourceMetadata
78
     * @param string           $shortName
79
     * @param string           $prefixedShortName
80
     * @param array            $entrypointProperties
81
     */
82
    private function populateEntrypointProperties(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName, array &$entrypointProperties)
83
    {
84
        $hydraCollectionOperations = $this->getHydraOperations($resourceClass, $resourceMetadata, $prefixedShortName, true);
85
        if (empty($hydraCollectionOperations)) {
86
            return;
87
        }
88
89
        $entrypointProperties[] = [
90
            '@type' => 'hydra:SupportedProperty',
91
            'hydra:property' => [
92
                '@id' => sprintf('#Entrypoint/%s', lcfirst($shortName)),
93
                '@type' => 'hydra:Link',
94
                'domain' => '#Entrypoint',
95
                'rdfs:label' => "The collection of $shortName resources",
96
                'range' => 'hydra:PagedCollection',
97
                'hydra:supportedOperation' => $hydraCollectionOperations,
98
            ],
99
            'hydra:title' => "The collection of $shortName resources",
100
            'hydra:readable' => true,
101
            'hydra:writable' => false,
102
        ];
103
    }
104
105
    /**
106
     * Gets a Hydra class.
107
     *
108
     * @param string           $resourceClass
109
     * @param ResourceMetadata $resourceMetadata
110
     * @param string           $shortName
111
     * @param string           $prefixedShortName
112
     *
113
     * @return array
114
     */
115
    private function getClass(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName): array
116
    {
117
        $class = [
118
            '@id' => $prefixedShortName,
119
            '@type' => 'hydra:Class',
120
            'rdfs:label' => $shortName,
121
            'hydra:title' => $shortName,
122
            'hydra:supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName),
123
            'hydra:supportedOperation' => $this->getHydraOperations($resourceClass, $resourceMetadata, $prefixedShortName, false),
124
        ];
125
126
        if (null !== $description = $resourceMetadata->getDescription()) {
127
            $class['hydra:description'] = $description;
128
        }
129
130
        return $class;
131
    }
132
133
    /**
134
     * Gets the context for the property name factory.
135
     *
136
     * @param ResourceMetadata $resourceMetadata
137
     *
138
     * @return array
139
     */
140
    private function getPropertyNameCollectionFactoryContext(ResourceMetadata $resourceMetadata): array
141
    {
142
        $attributes = $resourceMetadata->getAttributes();
143
        $context = [];
144
145 View Code Duplication
        if (isset($attributes['normalization_context']['groups'])) {
0 ignored issues
show
Duplication introduced by
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...
146
            $context['serializer_groups'] = $attributes['normalization_context']['groups'];
147
        }
148
149
        if (isset($attributes['denormalization_context']['groups'])) {
150 View Code Duplication
            if (isset($context['serializer_groups'])) {
0 ignored issues
show
Duplication introduced by
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...
151
                $context['serializer_groups'] += $attributes['denormalization_context']['groups'];
152
            } else {
153
                $context['serializer_groups'] = $attributes['denormalization_context']['groups'];
154
            }
155
        }
156
157
        return $context;
158
    }
159
160
    /**
161
     * Gets Hydra properties.
162
     *
163
     * @param string           $resourceClass
164
     * @param ResourceMetadata $resourceMetadata
165
     * @param string           $shortName
166
     * @param string           $prefixedShortName
167
     *
168
     * @return array
169
     */
170
    private function getHydraProperties(string $resourceClass, ResourceMetadata $resourceMetadata, string $shortName, string $prefixedShortName): array
171
    {
172
        $properties = [];
173
        foreach ($this->propertyNameCollectionFactory->create($resourceClass, $this->getPropertyNameCollectionFactoryContext($resourceMetadata)) as $propertyName) {
174
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
175
            if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) {
176
                continue;
177
            }
178
179
            $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName);
180
        }
181
182
        return $properties;
183
    }
184
185
    /**
186
     * Gets Hydra operations.
187
     *
188
     * @param string           $resourceClass
189
     * @param ResourceMetadata $resourceMetadata
190
     * @param string           $prefixedShortName
191
     * @param bool             $collection
192
     *
193
     * @return array
194
     */
195
    private function getHydraOperations(string $resourceClass, ResourceMetadata $resourceMetadata, string $prefixedShortName, bool $collection): array
196
    {
197
        if (null === $operations = $collection ? $resourceMetadata->getCollectionOperations() : $resourceMetadata->getItemOperations()) {
198
            return [];
199
        }
200
201
        $hydraOperations = [];
202
        foreach ($operations as $operationName => $operation) {
203
            $hydraOperations[] = $this->getHydraOperation($resourceClass, $resourceMetadata, $operationName, $operation, $prefixedShortName, $collection);
204
        }
205
206
        return $hydraOperations;
207
    }
208
209
    /**
210
     * Gets and populates if applicable a Hydra operation.
211
     *
212
     * @param string           $resourceClass
213
     * @param ResourceMetadata $resourceMetadata
214
     * @param string           $operationName
215
     * @param array            $operation
216
     * @param string           $prefixedShortName
217
     * @param bool             $collection
218
     *
219
     * @return array
220
     */
221
    private function getHydraOperation(string $resourceClass, ResourceMetadata $resourceMetadata, string $operationName, array $operation, string $prefixedShortName, bool $collection): array
222
    {
223
        if ($collection) {
224
            $method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
225
        } else {
226
            $method = $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName);
227
        }
228
229
        $hydraOperation = $operation['hydra_context'] ?? [];
230
        $shortName = $resourceMetadata->getShortName();
231
232
        if ('GET' === $method && $collection) {
233
            $hydraOperation = [
234
                'hydra:title' => "Retrieves the collection of $shortName resources.",
235
                'returns' => 'hydra:PagedCollection',
236
            ] + $hydraOperation;
237 View Code Duplication
        } elseif ('GET' === $method) {
0 ignored issues
show
Duplication introduced by
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...
238
            $hydraOperation = [
239
                'hydra:title' => "Retrieves $shortName resource.",
240
                'returns' => $prefixedShortName,
241
            ] + $hydraOperation;
242
        } elseif ('POST' === $method) {
243
            $hydraOperation = [
244
                '@type' => 'hydra:CreateResourceOperation',
245
                'hydra:title' => "Creates a $shortName resource.",
246
                'returns' => $prefixedShortName,
247
                'expects' => $prefixedShortName,
248
            ] + $hydraOperation;
249
        } elseif ('PUT' === $method) {
250
            $hydraOperation = [
251
                '@type' => 'hydra:ReplaceResourceOperation',
252
                'hydra:title' => "Replaces the $shortName resource.",
253
                'returns' => $prefixedShortName,
254
                'expects' => $prefixedShortName,
255
            ] + $hydraOperation;
256 View Code Duplication
        } elseif ('DELETE' === $method) {
0 ignored issues
show
Duplication introduced by
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...
257
            $hydraOperation = [
258
                'hydra:title' => "Deletes the $shortName resource.",
259
                'returns' => 'owl:Nothing',
260
            ] + $hydraOperation;
261
        }
262
263
        $hydraOperation['@type'] ?? $hydraOperation['@type'] = 'hydra:Operation';
264
        $hydraOperation['hydra:method'] ?? $hydraOperation['hydra:method'] = $method;
265
266
        if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation['hydra:title'])) {
267
            $hydraOperation['rdfs:label'] = $hydraOperation['hydra:title'];
268
        }
269
270
        ksort($hydraOperation);
271
272
        return $hydraOperation;
273
    }
274
275
    /**
276
     * Gets the range of the property.
277
     *
278
     * @param PropertyMetadata $propertyMetadata
279
     *
280
     * @return string|null
281
     */
282
    private function getRange(PropertyMetadata $propertyMetadata)
283
    {
284
        $jsonldContext = $propertyMetadata->getAttributes()['jsonld_context'] ?? [];
285
286
        if (isset($jsonldContext['@type'])) {
287
            return $jsonldContext['@type'];
288
        }
289
290
        if (null === $type = $propertyMetadata->getType()) {
291
            return;
292
        }
293
294
        if ($type->isCollection() && null !== $collectionType = $type->getCollectionValueType()) {
295
            $type = $collectionType;
296
        }
297
298
        switch ($type->getBuiltinType()) {
299
            case Type::BUILTIN_TYPE_STRING:
300
                return 'xmls:string';
301
302
            case Type::BUILTIN_TYPE_INT:
303
                return 'xmls:integer';
304
305
            case Type::BUILTIN_TYPE_FLOAT:
306
                return 'xmls:number';
307
308
            case Type::BUILTIN_TYPE_BOOL:
309
                return 'xmls:boolean';
310
311
            case Type::BUILTIN_TYPE_OBJECT:
312
                if (null === $className = $type->getClassName()) {
313
                    return;
314
                }
315
316
                if (is_a($className, \DateTimeInterface::class, true)) {
317
                    return 'xmls:dateTime';
318
                }
319
320
                if ($this->resourceClassResolver->isResourceClass($className)) {
321
                    $resourceMetadata = $this->resourceMetadataFactory->create($className);
322
323
                    return $resourceMetadata->getIri() ?? "#{$resourceMetadata->getShortName()}";
324
                }
325
                break;
326
        }
327
    }
328
329
    /**
330
     * Builds the classes array.
331
     *
332
     * @param array $entrypointProperties
333
     * @param array $classes
334
     *
335
     * @return array
336
     */
337
    private function getClasses(array $entrypointProperties, array $classes): array
338
    {
339
        $classes[] = [
340
            '@id' => '#Entrypoint',
341
            '@type' => 'hydra:Class',
342
            'hydra:title' => 'The API entrypoint',
343
            'hydra:supportedProperty' => $entrypointProperties,
344
            'hydra:supportedOperation' => [
345
                '@type' => 'hydra:Operation',
346
                'hydra:method' => 'GET',
347
                'rdfs:label' => 'The API entrypoint.',
348
                'returns' => '#EntryPoint',
349
            ],
350
        ];
351
352
        // Constraint violation
353
        $classes[] = [
354
            '@id' => '#ConstraintViolation',
355
            '@type' => 'hydra:Class',
356
            'hydra:title' => 'A constraint violation',
357
            'hydra:supportedProperty' => [
358
                [
359
                    '@type' => 'hydra:SupportedProperty',
360
                    'hydra:property' => [
361
                        '@id' => '#ConstraintViolation/propertyPath',
362
                        '@type' => 'rdf:Property',
363
                        'rdfs:label' => 'propertyPath',
364
                        'domain' => '#ConstraintViolation',
365
                        'range' => 'xmls:string',
366
                    ],
367
                    'hydra:title' => 'propertyPath',
368
                    'hydra:description' => 'The property path of the violation',
369
                    'hydra:readable' => true,
370
                    'hydra:writable' => false,
371
                ],
372
                [
373
                    '@type' => 'hydra:SupportedProperty',
374
                    'hydra:property' => [
375
                        '@id' => '#ConstraintViolation/message',
376
                        '@type' => 'rdf:Property',
377
                        'rdfs:label' => 'message',
378
                        'domain' => '#ConstraintViolation',
379
                        'range' => 'xmls:string',
380
                    ],
381
                    'hydra:title' => 'message',
382
                    'hydra:description' => 'The message associated with the violation',
383
                    'hydra:readable' => true,
384
                    'hydra:writable' => false,
385
                ],
386
            ],
387
        ];
388
389
        // Constraint violation list
390
        $classes[] = [
391
            '@id' => '#ConstraintViolationList',
392
            '@type' => 'hydra:Class',
393
            'subClassOf' => 'hydra:Error',
394
            'hydra:title' => 'A constraint violation list',
395
            'hydra:supportedProperty' => [
396
                [
397
                    '@type' => 'hydra:SupportedProperty',
398
                    'hydra:property' => [
399
                        '@id' => '#ConstraintViolationList/violation',
400
                        '@type' => 'rdf:Property',
401
                        'rdfs:label' => 'violation',
402
                        'domain' => '#ConstraintViolationList',
403
                        'range' => '#ConstraintViolation',
404
                    ],
405
                    'hydra:title' => 'violation',
406
                    'hydra:description' => 'The violations',
407
                    'hydra:readable' => true,
408
                    'hydra:writable' => false,
409
                ],
410
            ],
411
        ];
412
413
        return $classes;
414
    }
415
416
    /**
417
     * Gets a property definition.
418
     *
419
     * @param PropertyMetadata $propertyMetadata
420
     * @param string           $propertyName
421
     * @param string           $prefixedShortName
422
     * @param string           $shortName
423
     *
424
     * @return array
425
     */
426
    private function getProperty(PropertyMetadata $propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName): array
427
    {
428
        $property = [
429
            '@type' => 'hydra:SupportedProperty',
430
            'hydra:property' => [
431
                '@id' => $propertyMetadata->getIri() ?? "#$shortName/$propertyName",
432
                '@type' => $propertyMetadata->isReadableLink() ? 'rdf:Property' : 'hydra:Link',
433
                'rdfs:label' => $propertyName,
434
                'domain' => $prefixedShortName,
435
            ],
436
            'hydra:title' => $propertyName,
437
            'hydra:required' => $propertyMetadata->isRequired(),
438
            'hydra:readable' => $propertyMetadata->isReadable(),
439
            'hydra:writable' => $propertyMetadata->isWritable(),
440
        ];
441
442
        if (null !== $range = $this->getRange($propertyMetadata)) {
443
            $property['hydra:property']['range'] = $range;
444
        }
445
446
        if (null !== $description = $propertyMetadata->getDescription()) {
447
            $property['hydra:description'] = $description;
448
        }
449
450
        return $property;
451
    }
452
453
    /**
454
     * Computes the documentation.
455
     *
456
     * @param Documentation $object
457
     * @param array         $classes
458
     *
459
     * @return array
460
     */
461
    private function computeDoc(Documentation $object, array $classes): array
462
    {
463
        $doc = ['@context' => $this->getContext(), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT])];
464
465
        if ('' !== $object->getTitle()) {
466
            $doc['hydra:title'] = $object->getTitle();
467
        }
468
469
        if ('' !== $object->getDescription()) {
470
            $doc['hydra:description'] = $object->getDescription();
471
        }
472
473
        $doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_entrypoint');
474
        $doc['hydra:supportedClass'] = $classes;
475
476
        return $doc;
477
    }
478
479
    /**
480
     * Builds the JSON-LD context for the API documentation.
481
     *
482
     * @return array
483
     */
484
    private function getContext(): array
485
    {
486
        return [
487
            '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#',
488
            'hydra' => ContextBuilderInterface::HYDRA_NS,
489
            'rdf' => ContextBuilderInterface::RDF_NS,
490
            'rdfs' => ContextBuilderInterface::RDFS_NS,
491
            'xmls' => ContextBuilderInterface::XML_NS,
492
            'owl' => ContextBuilderInterface::OWL_NS,
493
            'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'],
494
            'range' => ['@id' => 'rdfs:range', '@type' => '@id'],
495
            'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'],
496
            'expects' => ['@id' => 'hydra:expects', '@type' => '@id'],
497
            'returns' => ['@id' => 'hydra:returns', '@type' => '@id'],
498
        ];
499
    }
500
501
    /**
502
     * {@inheritdoc}
503
     */
504
    public function supportsNormalization($data, $format = null, array $context = [])
505
    {
506
        return self::FORMAT === $format && $data instanceof Documentation;
507
    }
508
}
509