Completed
Pull Request — master (#904)
by Antoine
05:58
created

ApiLoader::load()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 31
Code Lines 16

Duplication

Lines 10
Ratio 32.26 %

Importance

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