Completed
Branch master (44e5fc)
by Kévin
10:15 queued 02:00
created

DocumentationNormalizer::getRange()   C

Complexity

Conditions 12
Paths 19

Size

Total Lines 41
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 41
rs 5.1612
c 0
b 0
f 0
cc 12
eloc 25
nc 19
nop 1

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 View Code Duplication
    public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ResourceClassResolverInterface $resourceClassResolver, OperationMethodResolverInterface $operationMethodResolver, UrlGeneratorInterface $urlGenerator)
1 ignored issue
show
Duplication introduced by
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...
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
64
            $shortName = $resourceMetadata->getShortName();
65
            $prefixedShortName = ($iri = $resourceMetadata->getIri()) ? $iri : '#'.$shortName;
66
67
            $collectionOperations = [];
68 View Code Duplication
            if ($itemOperations = $resourceMetadata->getCollectionOperations()) {
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...
69
                foreach ($itemOperations as $operationName => $collectionOperation) {
70
                    $collectionOperations[] = $this->getHydraOperation($resourceClass, $resourceMetadata, $operationName, $collectionOperation, $prefixedShortName, true);
71
                }
72
            }
73
74
            if (!empty($collectionOperations)) {
75
                $entrypointProperties[] = [
76
                    '@type' => 'hydra:SupportedProperty',
77
                    'hydra:property' => [
78
                        '@id' => sprintf('#Entrypoint/%s', lcfirst($shortName)),
79
                        '@type' => 'hydra:Link',
80
                        'domain' => '#Entrypoint',
81
                        'rdfs:label' => sprintf('The collection of %s resources', $shortName),
82
                        'range' => 'hydra:PagedCollection',
83
                        'hydra:supportedOperation' => $collectionOperations,
84
                    ],
85
                    'hydra:title' => sprintf('The collection of %s resources', $shortName),
86
                    'hydra:readable' => true,
87
                    'hydra:writable' => false,
88
                ];
89
            }
90
91
            $class = [
92
                '@id' => $prefixedShortName,
93
                '@type' => 'hydra:Class',
94
                'rdfs:label' => $shortName,
95
                'hydra:title' => $shortName,
96
            ];
97
98
            if ($description = $resourceMetadata->getDescription()) {
99
                $class['hydra:description'] = $description;
100
            }
101
102
            $attributes = $resourceMetadata->getAttributes();
103
            $context = [];
104
            $properties = [];
105
106 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...
107
                $context['serializer_groups'] = $attributes['normalization_context']['groups'];
108
            }
109
110 View Code Duplication
            if (isset($attributes['denormalization_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...
111
                $context['serializer_groups'] = isset($context['serializer_groups']) ? array_merge($context['serializer_groups'], $attributes['denormalization_context']['groups']) : $context['serializer_groups'];
112
            }
113
114
            foreach ($this->propertyNameCollectionFactory->create($resourceClass, $context) as $propertyName) {
115
                $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
116
                if (true === $propertyMetadata->isIdentifier() && false === $propertyMetadata->isWritable()) {
117
                    continue;
118
                }
119
120
                $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName);
121
            }
122
123
            $class['hydra:supportedProperty'] = $properties;
124
125
            $itemOperations = [];
126
127 View Code Duplication
            if ($operations = $resourceMetadata->getItemOperations()) {
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...
128
                foreach ($operations as $operationName => $itemOperation) {
129
                    $itemOperations[] = $this->getHydraOperation($resourceClass, $resourceMetadata, $operationName, $itemOperation, $prefixedShortName, false);
130
                }
131
            }
132
133
            $class['hydra:supportedOperation'] = $itemOperations;
134
            $classes[] = $class;
135
        }
136
137
        $classes = $this->getClasses($entrypointProperties, $classes);
138
139
        return $this->computeDoc($object, $classes);
140
    }
141
142
    /**
143
     * Gets and populates if applicable a Hydra operation.
144
     */
145
    private function getHydraOperation(string $resourceClass, ResourceMetadata $resourceMetadata, string $operationName, array $operation, string $prefixedShortName, bool $collection) : array
146
    {
147
        if ($collection) {
148
            $method = $this->operationMethodResolver->getCollectionOperationMethod($resourceClass, $operationName);
149
        } else {
150
            $method = $this->operationMethodResolver->getItemOperationMethod($resourceClass, $operationName);
151
        }
152
153
        $hydraOperation = $operation['hydra_context'] ?? [];
154
        $shortName = $resourceMetadata->getShortName();
155
156
        switch ($method) {
157
            case 'GET':
1 ignored issue
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
158
                if ($collection) {
159
                    if (!isset($hydraOperation['hydra:title'])) {
160
                        $hydraOperation['hydra:title'] = sprintf('Retrieves the collection of %s resources.', $shortName);
161
                    }
162
163
                    if (!isset($hydraOperation['returns'])) {
164
                        $hydraOperation['returns'] = 'hydra:PagedCollection';
165
                    }
166
                } else {
167
                    if (!isset($hydraOperation['hydra:title'])) {
168
                        $hydraOperation['hydra:title'] = sprintf('Retrieves %s resource.', $shortName);
169
                    }
170
                }
171
            break;
172
173 View Code Duplication
            case 'POST':
1 ignored issue
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
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...
174
                if (!isset($hydraOperation['@type'])) {
175
                    $hydraOperation['@type'] = 'hydra:CreateResourceOperation';
176
                }
177
178
                if (!isset($hydraOperation['hydra:title'])) {
179
                    $hydraOperation['hydra:title'] = sprintf('Creates a %s resource.', $shortName);
180
                }
181
            break;
182
183 View Code Duplication
            case 'PUT':
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...
184
                if (!isset($hydraOperation['@type'])) {
185
                    $hydraOperation['@type'] = 'hydra:ReplaceResourceOperation';
186
                }
187
188
                if (!isset($hydraOperation['hydra:title'])) {
189
                    $hydraOperation['hydra:title'] = sprintf('Replaces the %s resource.', $shortName);
190
                }
191
                break;
192
193 View Code Duplication
            case 'DELETE':
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...
194
                if (!isset($hydraOperation['hydra:title'])) {
195
                    $hydraOperation['hydra:title'] = sprintf('Deletes the %s resource.', $shortName);
196
                }
197
198
                if (!isset($hydraOperation['returns'])) {
199
                    $hydraOperation['returns'] = 'owl:Nothing';
200
                }
201
            break;
202
        }
203
204 View Code Duplication
        if (!isset($hydraOperation['returns']) &&
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...
205
            (
206
                ('GET' === $method && !$collection) ||
207
                'POST' === $method ||
208
                'PUT' === $method
209
            )
210
        ) {
211
            $hydraOperation['returns'] = $prefixedShortName;
212
        }
213
214 View Code Duplication
        if (!isset($hydraOperation['expects']) &&
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...
215
            ('POST' === $method || 'PUT' === $method)) {
216
            $hydraOperation['expects'] = $prefixedShortName;
217
        }
218
219
        if (!isset($hydraOperation['@type'])) {
220
            $hydraOperation['@type'] = 'hydra:Operation';
221
        }
222
223
        if (!isset($hydraOperation['hydra:method'])) {
224
            $hydraOperation['hydra:method'] = $method;
225
        }
226
227
        if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation['hydra:title'])) {
228
            $hydraOperation['rdfs:label'] = $hydraOperation['hydra:title'];
229
        }
230
231
        ksort($hydraOperation);
232
233
        return $hydraOperation;
234
    }
235
236
    /**
237
     * Gets the range of the property.
238
     *
239
     * @param PropertyMetadata $propertyMetadata
240
     *
241
     * @return string|null
242
     */
243
    private function getRange(PropertyMetadata $propertyMetadata)
244
    {
245
        $type = $propertyMetadata->getType();
246
        if (!$type) {
247
            return;
248
        }
249
250
        if ($type->isCollection() && $collectionType = $type->getCollectionValueType()) {
251
            $type = $collectionType;
252
        }
253
254
        switch ($type->getBuiltinType()) {
255
            case Type::BUILTIN_TYPE_STRING:
256
                return 'xmls:string';
257
258
            case Type::BUILTIN_TYPE_INT:
259
                return 'xmls:integer';
260
261
            case Type::BUILTIN_TYPE_FLOAT:
262
                return 'xmls:number';
263
264
            case Type::BUILTIN_TYPE_BOOL:
265
                return 'xmls:boolean';
266
267
            case Type::BUILTIN_TYPE_OBJECT:
268
                $className = $type->getClassName();
269
270
                if (null !== $className) {
271
                    $reflection = new \ReflectionClass($className);
272
                    if ($reflection->implementsInterface(\DateTimeInterface::class)) {
273
                        return 'xmls:dateTime';
274
                    }
275
276
                    $className = $type->getClassName();
277
                    if ($this->resourceClassResolver->isResourceClass($className)) {
278
                        return sprintf('#%s', $this->resourceMetadataFactory->create($className)->getShortName());
279
                    }
280
                }
281
                break;
282
        }
283
    }
284
285
    /*
286
     * Builds the classes array.
287
     */
288
    private function getClasses(array $entrypointProperties, array $classes) : array
289
    {
290
        $classes[] = [
291
                '@id' => '#Entrypoint',
292
                '@type' => 'hydra:Class',
293
                'hydra:title' => 'The API entrypoint',
294
                'hydra:supportedProperty' => $entrypointProperties,
295
                'hydra:supportedOperation' => [
296
                    '@type' => 'hydra:Operation',
297
                    'hydra:method' => 'GET',
298
                    'rdfs:label' => 'The API entrypoint.',
299
                    'returns' => '#EntryPoint',
300
                ],
301
            ];
302
303
        // Constraint violation
304
        $classes[] = [
305
            '@id' => '#ConstraintViolation',
306
            '@type' => 'hydra:Class',
307
            'hydra:title' => 'A constraint violation',
308
            'hydra:supportedProperty' => [
309
                [
310
                    '@type' => 'hydra:SupportedProperty',
311
                    'hydra:property' => [
312
                        '@id' => '#ConstraintViolation/propertyPath',
313
                        '@type' => 'rdf:Property',
314
                        'rdfs:label' => 'propertyPath',
315
                        'domain' => '#ConstraintViolation',
316
                        'range' => 'xmls:string',
317
                    ],
318
                    'hydra:title' => 'propertyPath',
319
                    'hydra:description' => 'The property path of the violation',
320
                    'hydra:readable' => true,
321
                    'hydra:writable' => false,
322
                ],
323
                [
324
                    '@type' => 'hydra:SupportedProperty',
325
                    'hydra:property' => [
326
                        '@id' => '#ConstraintViolation/message',
327
                        '@type' => 'rdf:Property',
328
                        'rdfs:label' => 'message',
329
                        'domain' => '#ConstraintViolation',
330
                        'range' => 'xmls:string',
331
                    ],
332
                    'hydra:title' => 'message',
333
                    'hydra:description' => 'The message associated with the violation',
334
                    'hydra:readable' => true,
335
                    'hydra:writable' => false,
336
                ],
337
            ],
338
        ];
339
340
        // Constraint violation list
341
        $classes[] = [
342
            '@id' => '#ConstraintViolationList',
343
            '@type' => 'hydra:Class',
344
            'subClassOf' => 'hydra:Error',
345
            'hydra:title' => 'A constraint violation list',
346
            'hydra:supportedProperty' => [
347
                [
348
                    '@type' => 'hydra:SupportedProperty',
349
                    'hydra:property' => [
350
                        '@id' => '#ConstraintViolationList/violation',
351
                        '@type' => 'rdf:Property',
352
                        'rdfs:label' => 'violation',
353
                        'domain' => '#ConstraintViolationList',
354
                        'range' => '#ConstraintViolation',
355
                    ],
356
                    'hydra:title' => 'violation',
357
                    'hydra:description' => 'The violations',
358
                    'hydra:readable' => true,
359
                    'hydra:writable' => false,
360
                ],
361
            ],
362
        ];
363
364
        return $classes;
365
    }
366
367
    private function getProperty(PropertyMetadata $propertyMetadata, string $propertyName, string $prefixedShortName, string $shortName): array
368
    {
369
        $type = $propertyMetadata->isReadableLink() ? 'rdf:Property' : 'Hydra:Link';
370
        $property = [
371
            '@type' => 'hydra:SupportedProperty',
372
            'hydra:property' => [
373
                '@id' => ($iri = $propertyMetadata->getIri()) ? $iri : sprintf('#%s/%s', $shortName, $propertyName),
374
                '@type' => $type,
375
                'rdfs:label' => $propertyName,
376
                'domain' => $prefixedShortName,
377
            ],
378
            'hydra:title' => $propertyName,
379
            'hydra:required' => $propertyMetadata->isRequired(),
380
            'hydra:readable' => $propertyMetadata->isReadable(),
381
            'hydra:writable' => $propertyMetadata->isWritable(),
382
        ];
383
384
        if ($range = $this->getRange($propertyMetadata)) {
385
            $property['hydra:property']['range'] = $range;
386
        }
387
388
        if ($description = $propertyMetadata->getDescription()) {
389
            $property['hydra:description'] = $description;
390
        }
391
392
        return $property;
393
    }
394
395
    private function computeDoc(Documentation $object, array $classes): array
396
    {
397
        $doc = ['@context' => $this->getContext(), '@id' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT])];
398
399
        if ('' !== $object->getTitle()) {
400
            $doc['hydra:title'] = $object->getTitle();
401
        }
402
403
        if ('' !== $object->getDescription()) {
404
            $doc['hydra:description'] = $object->getDescription();
405
        }
406
407
        $doc['hydra:entrypoint'] = $this->urlGenerator->generate('api_entrypoint');
408
        $doc['hydra:supportedClass'] = $classes;
409
410
        return $doc;
411
    }
412
413
    /**
414
     * Builds the JSON-LD context for the API documentation.
415
     *
416
     * @return array
417
     */
418
    private function getContext() : array
419
    {
420
        return [
421
            '@vocab' => $this->urlGenerator->generate('api_doc', ['_format' => self::FORMAT], UrlGeneratorInterface::ABS_URL).'#',
422
            'hydra' => ContextBuilderInterface::HYDRA_NS,
423
            'rdf' => ContextBuilderInterface::RDF_NS,
424
            'rdfs' => ContextBuilderInterface::RDFS_NS,
425
            'xmls' => ContextBuilderInterface::XML_NS,
426
            'owl' => ContextBuilderInterface::OWL_NS,
427
            'domain' => ['@id' => 'rdfs:domain', '@type' => '@id'],
428
            'range' => ['@id' => 'rdfs:range', '@type' => '@id'],
429
            'subClassOf' => ['@id' => 'rdfs:subClassOf', '@type' => '@id'],
430
            'expects' => ['@id' => 'hydra:expects', '@type' => '@id'],
431
            'returns' => ['@id' => 'hydra:returns', '@type' => '@id'],
432
        ];
433
    }
434
435
    /**
436
     * {@inheritdoc}
437
     */
438
    public function supportsNormalization($data, $format = null, array $context = [])
439
    {
440
        return self::FORMAT === $format && $data instanceof Documentation;
441
    }
442
}
443