Completed
Pull Request — master (#904)
by Antoine
04:19 queued 01:13
created

ApiLoader   B

Complexity

Total Complexity 27

Size/Duplication

Total Lines 234
Duplicated Lines 4.27 %

Coupling/Cohesion

Components 1
Dependencies 17

Importance

Changes 0
Metric Value
wmc 27
lcom 1
cbo 17
dl 10
loc 234
rs 7.8571
c 0
b 0
f 0

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
C load() 10 31 7
A routeNameResolver() 0 6 2
C computeSubresourceOperations() 0 67 8
A supports() 0 4 1
A loadExternalFiles() 0 8 2
B addRoute() 0 47 6

How to fix   Duplicated Code   

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:

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\Bridge\Symfony\Routing;
13
14
use ApiPlatform\Core\Api\OperationType;
15
use ApiPlatform\Core\Exception\InvalidResourceException;
16
use ApiPlatform\Core\Exception\RuntimeException;
17
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
18
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
19
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
20
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
21
use ApiPlatform\Core\PathResolver\OperationPathResolverInterface;
22
use Doctrine\Common\Inflector\Inflector;
23
use Symfony\Component\Config\FileLocator;
24
use Symfony\Component\Config\Loader\Loader;
25
use Symfony\Component\DependencyInjection\ContainerInterface;
26
use Symfony\Component\HttpKernel\KernelInterface;
27
use Symfony\Component\Routing\Loader\XmlFileLoader;
28
use Symfony\Component\Routing\Route;
29
use Symfony\Component\Routing\RouteCollection;
30
31
/**
32
 * Loads Resources.
33
 *
34
 * @author Kévin Dunglas <[email protected]>
35
 */
36
final class ApiLoader extends Loader
37
{
38
    const ROUTE_NAME_PREFIX = 'api_';
39
    const DEFAULT_ACTION_PATTERN = 'api_platform.action.';
40
    const SUBRESOURCE_SUFFIX = '_get_subresource';
41
42
    private $fileLoader;
43
    private $propertyNameCollectionFactory;
44
    private $propertyMetadataFactory;
45
    private $resourceNameCollectionFactory;
46
    private $resourceMetadataFactory;
47
    private $operationPathResolver;
48
    private $container;
49
    private $formats;
50
51
    public function __construct(KernelInterface $kernel, ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, OperationPathResolverInterface $operationPathResolver, ContainerInterface $container, array $formats, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory = null, PropertyMetadataFactoryInterface $propertyMetadataFactory = null)
52
    {
53
        $this->fileLoader = new XmlFileLoader(new FileLocator($kernel->locateResource('@ApiPlatformBundle/Resources/config/routing')));
54
        $this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
55
        $this->resourceMetadataFactory = $resourceMetadataFactory;
56
        $this->operationPathResolver = $operationPathResolver;
57
        $this->container = $container;
58
        $this->formats = $formats;
59
        $this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
60
        $this->propertyMetadataFactory = $propertyMetadataFactory;
61
    }
62
63
    /**
64
     * {@inheritdoc}
65
     */
66
    public function load($data, $type = null)
67
    {
68
        $routeCollection = new RouteCollection();
69
70
        $this->loadExternalFiles($routeCollection);
71
72
        foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
73
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
74
            $resourceShortName = $resourceMetadata->getShortName();
75
76
            if (null === $resourceShortName) {
77
                throw new InvalidResourceException(sprintf('Resource %s has no short name defined.', $resourceClass));
78
            }
79
80 View Code Duplication
            if (null !== $collectionOperations = $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...
81
                foreach ($collectionOperations as $operationName => $operation) {
82
                    $this->addRoute($routeCollection, $resourceClass, $operationName, $operation, $resourceShortName, OperationType::COLLECTION);
83
                }
84
            }
85
86 View Code Duplication
            if (null !== $itemOperations = $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...
87
                foreach ($itemOperations as $operationName => $operation) {
88
                    $this->addRoute($routeCollection, $resourceClass, $operationName, $operation, $resourceShortName, OperationType::ITEM);
89
                }
90
            }
91
92
            $this->computeSubresourceOperations($routeCollection, $resourceClass);
93
        }
94
95
        return $routeCollection;
96
    }
97
98
    /**
99
     * Transforms a given string to a tableized, pluralized string.
100
     *
101
     * @param string $name usually a ResourceMetadata shortname
102
     *
103
     * @return string A string that is a part of the route name
104
     */
105
    private function routeNameResolver(string $name, bool $pluralize = true): string
106
    {
107
        $name = Inflector::tableize($name);
108
109
        return $pluralize ? Inflector::pluralize($name) : $name;
110
    }
111
112
    /**
113
     * Handles subresource operations recursively and declare their corresponding routes.
114
     *
115
     * @param RouteCollection $routeCollection
116
     * @param string          $resourceClass
117
     * @param string          $rootResourceClass null on the first iteration, it then keeps track of the origin resource class
118
     * @param array           $parentOperation   the previous call operation
119
     */
120
    private function computeSubresourceOperations(RouteCollection $routeCollection, string $resourceClass, string $rootResourceClass = null, array $parentOperation = null)
121
    {
122
        if (null === $this->propertyNameCollectionFactory || null === $this->propertyMetadataFactory) {
123
            return;
124
        }
125
126
        if (null === $rootResourceClass) {
127
            $rootResourceClass = $resourceClass;
128
        }
129
130
        foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
131
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
132
133
            if (!$propertyMetadata->hasSubresource()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $propertyMetadata->hasSubresource() of type null|boolean is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
134
                continue;
135
            }
136
137
            $isCollection = $propertyMetadata->getType()->isCollection();
138
            $subresource = $isCollection ? $propertyMetadata->getType()->getCollectionValueType()->getClassName() : $propertyMetadata->getType()->getClassName();
139
140
            $propertyName = $this->routeNameResolver($property, $isCollection);
141
142
            $operation = [
143
                'property' => $property,
144
                'collection' => $isCollection,
145
            ];
146
147
            if (null === $parentOperation) {
148
                $rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass);
149
                $rootShortname = $rootResourceMetadata->getShortName();
150
                $resourceRouteName = $this->routeNameResolver($rootShortname);
151
152
                $operation['identifiers'] = [['id', $rootResourceClass]];
153
                $operation['route_name'] = sprintf('%s%s_%s%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $propertyName, self::SUBRESOURCE_SUFFIX);
154
                $operation['path'] = $this->operationPathResolver->resolveOperationPath($rootShortname, $operation, OperationType::SUBRESOURCE);
155
            } else {
156
                $operation['identifiers'] = $parentOperation['identifiers'];
157
                $operation['identifiers'][] = [$parentOperation['property'], $resourceClass];
158
                $operation['route_name'] = str_replace(self::SUBRESOURCE_SUFFIX, "_$propertyName".self::SUBRESOURCE_SUFFIX, $parentOperation['route_name']);
159
                $operation['path'] = $this->operationPathResolver->resolveOperationPath($parentOperation['path'], $operation, OperationType::SUBRESOURCE);
160
            }
161
162
            $route = new Route(
163
                $operation['path'],
164
                [
165
                    '_controller' => self::DEFAULT_ACTION_PATTERN.'get_subresource',
166
                    '_format' => null,
167
                    '_api_resource_class' => $subresource,
168
                    '_api_subresource_operation_name' => 'get',
169
                    '_api_subresource_context' => [
170
                        'property' => $operation['property'],
171
                        'identifiers' => $operation['identifiers'],
172
                        'collection' => $isCollection,
173
                    ],
174
                ],
175
                [],
176
                [],
177
                '',
178
                [],
179
                ['GET']
180
            );
181
182
            $routeCollection->add($operation['route_name'], $route);
183
184
            $this->computeSubresourceOperations($routeCollection, $subresource, $rootResourceClass, $operation);
185
        }
186
    }
187
188
    /**
189
     * {@inheritdoc}
190
     */
191
    public function supports($resource, $type = null)
192
    {
193
        return 'api_platform' === $type;
194
    }
195
196
    /**
197
     * Load external files.
198
     *
199
     * @param RouteCollection $routeCollection
200
     */
201
    private function loadExternalFiles(RouteCollection $routeCollection)
202
    {
203
        $routeCollection->addCollection($this->fileLoader->load('api.xml'));
204
205
        if (isset($this->formats['jsonld'])) {
206
            $routeCollection->addCollection($this->fileLoader->load('jsonld.xml'));
207
        }
208
    }
209
210
    /**
211
     * Creates and adds a route for the given operation to the route collection.
212
     *
213
     * @param RouteCollection $routeCollection
214
     * @param string          $resourceClass
215
     * @param string          $operationName
216
     * @param array           $operation
217
     * @param string          $resourceShortName
218
     * @param string          $operationType
219
     *
220
     * @throws RuntimeException
221
     */
222
    private function addRoute(RouteCollection $routeCollection, string $resourceClass, string $operationName, array $operation, string $resourceShortName, string $operationType)
223
    {
224
        if (isset($operation['route_name'])) {
225
            return;
226
        }
227
228
        if (!isset($operation['method'])) {
229
            throw new RuntimeException('Either a "route_name" or a "method" operation attribute must exist.');
230
        }
231
232
        $controller = $operation['controller'] ?? null;
233
        $actionName = sprintf('%s_%s', strtolower($operation['method']), $operationType);
234
235
        if (null === $controller) {
236
            $controller = self::DEFAULT_ACTION_PATTERN.$actionName;
237
238
            if (!$this->container->has($controller)) {
239
                throw new RuntimeException(sprintf('There is no builtin action for the %s %s operation. You need to define the controller yourself.', $operationType, $operation['method']));
240
            }
241
        }
242
243
        if ($operationName !== strtolower($operation['method'])) {
244
            $actionName = sprintf('%s_%s', $operationName, $operationType);
245
        }
246
247
        $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType);
248
249
        $resourceRouteName = $this->routeNameResolver($resourceShortName);
250
        $routeName = sprintf('%s%s_%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $actionName);
251
252
        $route = new Route(
253
            $path,
254
            [
255
                '_controller' => $controller,
256
                '_format' => null,
257
                '_api_resource_class' => $resourceClass,
258
                sprintf('_api_%s_operation_name', $operationType) => $operationName,
259
            ],
260
            [],
261
            [],
262
            '',
263
            [],
264
            [$operation['method']]
265
        );
266
267
        $routeCollection->add($routeName, $route);
268
    }
269
}
270