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

ApiLoader::addRoute()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 47
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 47
rs 8.5125
c 0
b 0
f 0
cc 6
eloc 25
nc 7
nop 6
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
        if (null === $this->propertyNameCollectionFactory || null === $this->propertyMetadataFactory) {
63
            @trigger_error('Not injecting "PropertyNameCollectionFactoryInterface" and "PropertyMetadataFactoryInterface" instances in '.__CLASS__.' is deprecrated since API Platform 2.3 and will not be possible anymore in API Platform 3', 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->computeSubresourceOperations($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
     *
107
     * @return string A string that is a part of the route name
108
     */
109
    private function routeNameResolver(string $name, bool $pluralize = true): string
110
    {
111
        $name = Inflector::tableize($name);
112
113
        return $pluralize ? Inflector::pluralize($name) : $name;
114
    }
115
116
    /**
117
     * Handles subresource operations recursively and declare their corresponding routes.
118
     *
119
     * @param RouteCollection $routeCollection
120
     * @param string          $resourceClass
121
     * @param string          $rootResourceClass null on the first iteration, it then keeps track of the origin resource class
122
     * @param string          $rootResourceClass null on the first iteration, it then keeps track of the parent operation
123
     */
124
    private function computeSubresourceOperations(RouteCollection $routeCollection, string $resourceClass, $rootResourceClass = null, $parentOperation = null)
125
    {
126
        if (null === $this->propertyNameCollectionFactory || null === $this->propertyMetadataFactory) {
127
            return;
128
        }
129
130
        if (null === $rootResourceClass) {
131
            $rootResourceClass = $resourceClass;
132
        }
133
134
        foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $property) {
135
            $propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $property);
136
137
            if (!$propertyMetadata->hasSubresource()) {
138
                continue;
139
            }
140
141
            $isCollection = $propertyMetadata->getType()->isCollection();
142
            $subresource = $isCollection ? $propertyMetadata->getType()->getCollectionValueType()->getClassName() : $propertyMetadata->getType()->getClassName();
143
144
            $propertyName = $this->routeNameResolver($property, $isCollection);
145
146
            $operation = [
147
                'property' => $property,
148
                'collection' => $isCollection,
149
            ];
150
151
            if (null === $parentOperation) {
152
                $rootResourceMetadata = $this->resourceMetadataFactory->create($rootResourceClass);
153
                $rootShortname = $rootResourceMetadata->getShortName();
154
                $resourceRouteName = $this->routeNameResolver($rootShortname);
155
156
                $operation['identifiers'] = [['id', $rootResourceClass]];
157
                $operation['route_name'] = sprintf('%s%s_%s%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $propertyName, self::SUBRESOURCE_SUFFIX);
158
159
                $operation['path'] = $this->operationPathResolver->resolveOperationPath($rootShortname, $operation, OperationType::SUBRESOURCE);
160
            } else {
161
                $identifier = $parentOperation['property'];
162
163
                $operation['identifiers'] = $parentOperation['identifiers'];
164
                $operation['identifiers'][] = [$identifier, $resourceClass];
165
                $operation['route_name'] = str_replace(self::SUBRESOURCE_SUFFIX, "_$propertyName".self::SUBRESOURCE_SUFFIX, $parentOperation['route_name']);
166
167
                $operation['path'] = $this->operationPathResolver->resolveOperationPath($parentOperation['path'], $operation, OperationType::SUBRESOURCE);
168
            }
169
170
            $route = new Route(
171
                $operation['path'],
172
                [
173
                    '_controller' => self::DEFAULT_ACTION_PATTERN.'get_subresource',
174
                    '_format' => null,
175
                    '_api_resource_class' => $subresource,
176
                    '_api_subresource_operation_name' => 'get',
177
                    '_api_subresource_context' => [
178
                        'property' => $operation['property'],
179
                        'identifiers' => $operation['identifiers'],
180
                        'collection' => $isCollection,
181
                    ],
182
                ],
183
                [],
184
                [],
185
                '',
186
                [],
187
                ['GET']
188
            );
189
190
            $routeCollection->add($operation['route_name'], $route);
191
192
            $this->computeSubresourceOperations($routeCollection, $subresource, $rootResourceClass, $operation);
193
        }
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function supports($resource, $type = null)
200
    {
201
        return 'api_platform' === $type;
202
    }
203
204
    /**
205
     * Load external files.
206
     *
207
     * @param RouteCollection $routeCollection
208
     */
209
    private function loadExternalFiles(RouteCollection $routeCollection)
210
    {
211
        $routeCollection->addCollection($this->fileLoader->load('api.xml'));
212
213
        if (isset($this->formats['jsonld'])) {
214
            $routeCollection->addCollection($this->fileLoader->load('jsonld.xml'));
215
        }
216
    }
217
218
    /**
219
     * Creates and adds a route for the given operation to the route collection.
220
     *
221
     * @param RouteCollection $routeCollection
222
     * @param string          $resourceClass
223
     * @param string          $operationName
224
     * @param array           $operation
225
     * @param string          $resourceShortName
226
     * @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...
227
     *
228
     * @throws RuntimeException
229
     */
230
    private function addRoute(RouteCollection $routeCollection, string $resourceClass, string $operationName, array $operation, string $resourceShortName, string $operationType)
231
    {
232
        if (isset($operation['route_name'])) {
233
            return;
234
        }
235
236
        if (!isset($operation['method'])) {
237
            throw new RuntimeException('Either a "route_name" or a "method" operation attribute must exist.');
238
        }
239
240
        $controller = $operation['controller'] ?? null;
241
        $actionName = sprintf('%s_%s', strtolower($operation['method']), $operationType);
242
243
        if (null === $controller) {
244
            $controller = self::DEFAULT_ACTION_PATTERN.$actionName;
245
246
            if (!$this->container->has($controller)) {
247
                throw new RuntimeException(sprintf('There is no builtin action for the %s %s operation. You need to define the controller yourself.', $operationType, $operation['method']));
248
            }
249
        }
250
251
        if ($operationName !== strtolower($operation['method'])) {
252
            $actionName = sprintf('%s_%s', $operationName, $operationType);
253
        }
254
255
        $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType);
256
257
        $resourceRouteName = $this->routeNameResolver($resourceShortName);
258
        $routeName = sprintf('%s%s_%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $actionName);
259
260
        $route = new Route(
261
            $path,
262
            [
263
                '_controller' => $controller,
264
                '_format' => null,
265
                '_api_resource_class' => $resourceClass,
266
                sprintf('_api_%s_operation_name', $operationType) => $operationName,
267
            ],
268
            [],
269
            [],
270
            '',
271
            [],
272
            [$operation['method']]
273
        );
274
275
        $routeCollection->add($routeName, $route);
276
    }
277
}
278