Completed
Pull Request — master (#904)
by Antoine
03:21
created

ApiLoader::computeSubcollectionOperations()   C

Complexity

Conditions 8
Paths 13

Size

Total Lines 68
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 68
c 0
b 0
f 0
rs 6.5974
cc 8
eloc 39
nc 13
nop 4

How to fix   Long Method   

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\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 SUBCOLLECTION_SUFFIX = '_get_subcollection';
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
        if (null === $this->propertyNameCollectionFactory || null === $this->propertyMetadataFactory) {
63
            @trigger_error('Missing dependencies to compute subcollection operations.', E_USER_DEPRECATED);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
64
        }
65
    }
66
67
    /**
68
     * {@inheritdoc}
69
     */
70
    public function load($data, $type = null)
71
    {
72
        $routeCollection = new RouteCollection();
73
74
        $this->loadExternalFiles($routeCollection);
75
76
        foreach ($this->resourceNameCollectionFactory->create() as $resourceClass) {
77
            $resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
78
            $resourceShortName = $resourceMetadata->getShortName();
79
80
            if (null === $resourceShortName) {
81
                throw new InvalidResourceException(sprintf('Resource %s has no short name defined.', $resourceClass));
82
            }
83
84 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...
85
                foreach ($collectionOperations as $operationName => $operation) {
86
                    $this->addRoute($routeCollection, $resourceClass, $operationName, $operation, $resourceShortName, OperationType::COLLECTION);
87
                }
88
            }
89
90 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...
91
                foreach ($itemOperations as $operationName => $operation) {
92
                    $this->addRoute($routeCollection, $resourceClass, $operationName, $operation, $resourceShortName, OperationType::ITEM);
93
                }
94
            }
95
96
            $this->computeSubcollectionOperations($routeCollection, $resourceClass);
97
        }
98
99
        return $routeCollection;
100
    }
101
102
    /**
103
     * Transforms a given string to a tableized, pluralized string
104
     *
105
     * @param string  $name usually a ResourceMetadata shortname
106
     * @return string A string that is a part of the route name
107
     */
108
    private function routeNameResolver(string $name): string {
109
        return Inflector::pluralize(Inflector::tableize($name));
110
    }
111
112
    /**
113
     * Handles subcollection 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 string          $rootResourceClass null on the first iteration, it then keeps track of the parent operation
119
     */
120
    private function computeSubcollectionOperations(RouteCollection $routeCollection, string $resourceClass, $rootResourceClass = null, $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->hasSubcollection()) {
134
                continue;
135
            }
136
137
            $subcollection = $propertyMetadata->getType()->isCollection() ? $propertyMetadata->getType()->getCollectionValueType()->getClassName() : $propertyMetadata->getType()->getClassName();
138
139
            $propertyName = $this->routeNameResolver($property);
140
141
            $operation = [
142
                'property' => $property,
143
            ];
144
145
            if (null === $parentOperation) {
146
                $rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass);
147
                $rootShortname = $rootResourceMetadata->getShortName();
148
                $resourceRouteName = $this->routeNameResolver($rootShortname);
149
150
                $operation['identifiers'] = [['id', $rootResourceClass]];
151
                $operation['route_name'] = sprintf('%s%s_%s%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $propertyName, self::SUBCOLLECTION_SUFFIX);
152
153
                $operation['path'] = $this->operationPathResolver->resolveOperationPath($rootShortname, $operation, OperationType::SUBCOLLECTION);
154
            } else {
155
                $identifier = $parentOperation['property'];
156
157
                $operation['identifiers'] = $parentOperation['identifiers'];
158
                $operation['identifiers'][] = [$identifier, $resourceClass];
159
                $operation['route_name'] = str_replace(self::SUBCOLLECTION_SUFFIX, "_$propertyName".self::SUBCOLLECTION_SUFFIX, $parentOperation['route_name']);
160
161
                $operation['path'] = $this->operationPathResolver->resolveOperationPath($parentOperation['path'], $operation, OperationType::SUBCOLLECTION);
162
            }
163
164
            $route = new Route(
165
                $operation['path'],
166
                [
167
                    '_controller' => self::DEFAULT_ACTION_PATTERN.'get_subcollection',
168
                    '_format' => null,
169
                    '_api_resource_class' => $subcollection,
170
                    '_api_subcollection_operation_name' => 'get',
171
                    '_api_subcollection_context' => [
172
                        'property' => $operation['property'],
173
                        'identifiers' => $operation['identifiers'],
174
                    ],
175
                ],
176
                [],
177
                [],
178
                '',
179
                [],
180
                ['GET']
181
            );
182
183
            $routeCollection->add($operation['route_name'], $route);
184
185
            $this->computeSubcollectionOperations($routeCollection, $subcollection, $rootResourceClass, $operation);
186
        }
187
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192
    public function supports($resource, $type = null)
193
    {
194
        return 'api_platform' === $type;
195
    }
196
197
    /**
198
     * Load external files.
199
     *
200
     * @param RouteCollection $routeCollection
201
     */
202
    private function loadExternalFiles(RouteCollection $routeCollection)
203
    {
204
        $routeCollection->addCollection($this->fileLoader->load('api.xml'));
205
206
        if (isset($this->formats['jsonld'])) {
207
            $routeCollection->addCollection($this->fileLoader->load('jsonld.xml'));
208
        }
209
    }
210
211
    /**
212
     * Creates and adds a route for the given operation to the route collection.
213
     *
214
     * @param RouteCollection $routeCollection
215
     * @param string          $resourceClass
216
     * @param string          $operationName
217
     * @param array           $operation
218
     * @param string          $resourceShortName
219
     * @param bool            $collection
0 ignored issues
show
Documentation introduced by
There is no parameter named $collection. Did you maybe mean $routeCollection?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function. It has, however, found a similar but not annotated parameter which might be a good fit.

Consider the following example. The parameter $ireland is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $ireland
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was changed, but the annotation was not.

Loading history...
220
     *
221
     * @throws RuntimeException
222
     */
223
    private function addRoute(RouteCollection $routeCollection, string $resourceClass, string $operationName, array $operation, string $resourceShortName, string $operationType)
224
    {
225
        if (isset($operation['route_name'])) {
226
            return;
227
        }
228
229
        if (!isset($operation['method'])) {
230
            throw new RuntimeException('Either a "route_name" or a "method" operation attribute must exist.');
231
        }
232
233
        $controller = $operation['controller'] ?? null;
234
        $actionName = sprintf('%s_%s', strtolower($operation['method']), $operationType);
235
236
        if (null === $controller) {
237
            $controller = self::DEFAULT_ACTION_PATTERN.$actionName;
238
239
            if (!$this->container->has($controller)) {
240
                throw new RuntimeException(sprintf('There is no builtin action for the %s %s operation. You need to define the controller yourself.', $operationType, $operation['method']));
241
            }
242
        }
243
244
        if ($operationName !== strtolower($operation['method'])) {
245
            $actionName = sprintf('%s_%s', $operationName, $operationType);
246
        }
247
248
        $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType);
249
250
        $resourceRouteName = $this->routeNameResolver($resourceShortName);
251
        $routeName = sprintf('%s%s_%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $actionName);
252
253
        $route = new Route(
254
            $path,
255
            [
256
                '_controller' => $controller,
257
                '_format' => null,
258
                '_api_resource_class' => $resourceClass,
259
                sprintf('_api_%s_operation_name', $operationType) => $operationName,
260
            ],
261
            [],
262
            [],
263
            '',
264
            [],
265
            [$operation['method']]
266
        );
267
268
        $routeCollection->add($routeName, $route);
269
    }
270
}
271