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()) {
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...
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
                $operation['path'] = $this->operationPathResolver->resolveOperationPath($rootShortname, $operation, OperationType::SUBRESOURCE);
159
            } else {
160
                $operation['identifiers'] = $parentOperation['identifiers'];
161
                $operation['identifiers'][] = [$parentOperation['property'], $resourceClass];
162
                $operation['route_name'] = str_replace(self::SUBRESOURCE_SUFFIX, "_$propertyName".self::SUBRESOURCE_SUFFIX, $parentOperation['route_name']);
163
                $operation['path'] = $this->operationPathResolver->resolveOperationPath($parentOperation['path'], $operation, OperationType::SUBRESOURCE);
164
            }
165
166
            $route = new Route(
167
                $operation['path'],
168
                [
169
                    '_controller' => self::DEFAULT_ACTION_PATTERN.'get_subresource',
170
                    '_format' => null,
171
                    '_api_resource_class' => $subresource,
172
                    '_api_subresource_operation_name' => 'get',
173
                    '_api_subresource_context' => [
174
                        'property' => $operation['property'],
175
                        'identifiers' => $operation['identifiers'],
176
                        'collection' => $isCollection,
177
                    ],
178
                ],
179
                [],
180
                [],
181
                '',
182
                [],
183
                ['GET']
184
            );
185
186
            $routeCollection->add($operation['route_name'], $route);
187
188
            $this->computeSubresourceOperations($routeCollection, $subresource, $rootResourceClass, $operation);
189
        }
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195
    public function supports($resource, $type = null)
196
    {
197
        return 'api_platform' === $type;
198
    }
199
200
    /**
201
     * Load external files.
202
     *
203
     * @param RouteCollection $routeCollection
204
     */
205
    private function loadExternalFiles(RouteCollection $routeCollection)
206
    {
207
        $routeCollection->addCollection($this->fileLoader->load('api.xml'));
208
209
        if (isset($this->formats['jsonld'])) {
210
            $routeCollection->addCollection($this->fileLoader->load('jsonld.xml'));
211
        }
212
    }
213
214
    /**
215
     * Creates and adds a route for the given operation to the route collection.
216
     *
217
     * @param RouteCollection $routeCollection
218
     * @param string          $resourceClass
219
     * @param string          $operationName
220
     * @param array           $operation
221
     * @param string          $resourceShortName
222
     * @param OperationType   $operationType
223
     *
224
     * @throws RuntimeException
225
     */
226
    private function addRoute(RouteCollection $routeCollection, string $resourceClass, string $operationName, array $operation, string $resourceShortName, string $operationType)
227
    {
228
        if (isset($operation['route_name'])) {
229
            return;
230
        }
231
232
        if (!isset($operation['method'])) {
233
            throw new RuntimeException('Either a "route_name" or a "method" operation attribute must exist.');
234
        }
235
236
        $controller = $operation['controller'] ?? null;
237
        $actionName = sprintf('%s_%s', strtolower($operation['method']), $operationType);
238
239
        if (null === $controller) {
240
            $controller = self::DEFAULT_ACTION_PATTERN.$actionName;
241
242
            if (!$this->container->has($controller)) {
243
                throw new RuntimeException(sprintf('There is no builtin action for the %s %s operation. You need to define the controller yourself.', $operationType, $operation['method']));
244
            }
245
        }
246
247
        if ($operationName !== strtolower($operation['method'])) {
248
            $actionName = sprintf('%s_%s', $operationName, $operationType);
249
        }
250
251
        $path = $this->operationPathResolver->resolveOperationPath($resourceShortName, $operation, $operationType);
0 ignored issues
show
Bug introduced by
It seems like $operationType defined by parameter $operationType on line 226 can also be of type object<ApiPlatform\Core\Api\OperationType>; however, ApiPlatform\Core\PathRes...:resolveOperationPath() does only seem to accept string|boolean, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
252
253
        $resourceRouteName = $this->routeNameResolver($resourceShortName);
254
        $routeName = sprintf('%s%s_%s', self::ROUTE_NAME_PREFIX, $resourceRouteName, $actionName);
255
256
        $route = new Route(
257
            $path,
258
            [
259
                '_controller' => $controller,
260
                '_format' => null,
261
                '_api_resource_class' => $resourceClass,
262
                sprintf('_api_%s_operation_name', $operationType) => $operationName,
263
            ],
264
            [],
265
            [],
266
            '',
267
            [],
268
            [$operation['method']]
269
        );
270
271
        $routeCollection->add($routeName, $route);
272
    }
273
}
274