Completed
Push — master ( 1e4134...246601 )
by Christian
02:12 queued 11s
created

RestActionReader::generateUrlParts()   B

Complexity

Conditions 11
Paths 11

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 11.0207

Importance

Changes 0
Metric Value
dl 0
loc 34
ccs 17
cts 18
cp 0.9444
rs 7.3166
c 0
b 0
f 0
cc 11
nc 11
nop 3
crap 11.0207

How to fix   Complexity   

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 FOSRestBundle package.
5
 *
6
 * (c) FriendsOfSymfony <http://friendsofsymfony.github.com/>
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 FOS\RestBundle\Routing\Loader\Reader;
13
14
@trigger_error(sprintf('The %s\RestActionReader class is deprecated since FOSRestBundle 2.8.', __NAMESPACE__), 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...
15
16
use Doctrine\Common\Annotations\Reader;
17
use FOS\RestBundle\Controller\Annotations\Route as RouteAnnotation;
18
use FOS\RestBundle\Inflector\InflectorInterface;
19
use FOS\RestBundle\Request\ParamFetcherInterface;
20
use FOS\RestBundle\Request\ParamReaderInterface;
21
use FOS\RestBundle\Routing\RestRouteCollection;
22
use Psr\Http\Message\MessageInterface;
23
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
24
use Symfony\Component\HttpFoundation\Request;
25
use Symfony\Component\HttpFoundation\Session\SessionInterface;
26
use Symfony\Component\Routing\Route;
27
use Symfony\Component\Security\Core\User\UserInterface;
28
use Symfony\Component\Validator\ConstraintViolationListInterface;
29
30
/**
31
 * REST controller actions reader.
32
 *
33
 * @author Konstantin Kudryashov <[email protected]>
34
 *
35
 * @deprecated since 2.8
36
 */
37
class RestActionReader
38
{
39
    const COLLECTION_ROUTE_PREFIX = 'c';
40
41
    private $annotationReader;
42
    private $paramReader;
43
    private $inflector;
44
    private $formats;
45
    private $includeFormat;
46
    private $routePrefix;
47
    private $namePrefix;
48
    private $versions;
49
    private $pluralize;
50
    private $parents = [];
51
    private $availableHTTPMethods = [
52
        'get',
53
        'post',
54
        'put',
55
        'patch',
56
        'delete',
57
        'link',
58
        'unlink',
59
        'head',
60
        'options',
61
        'mkcol',
62
        'propfind',
63
        'proppatch',
64
        'move',
65
        'copy',
66
        'lock',
67
        'unlock',
68
    ];
69
    private $availableConventionalActions = ['new', 'edit', 'remove'];
70
    private $hasMethodPrefix;
71
72 19
    public function __construct(Reader $annotationReader, ParamReaderInterface $paramReader, InflectorInterface $inflector, bool $includeFormat, array $formats = [], bool $hasMethodPrefix = true)
73
    {
74 19
        $this->annotationReader = $annotationReader;
75 19
        $this->paramReader = $paramReader;
76 19
        $this->inflector = $inflector;
77 19
        $this->includeFormat = $includeFormat;
78 19
        $this->formats = $formats;
79 19
        $this->hasMethodPrefix = $hasMethodPrefix;
80 19
    }
81
82 2
    public function setRoutePrefix(?string $prefix = null): void
83
    {
84 2
        $this->routePrefix = $prefix;
85 2
    }
86
87 2
    public function getRoutePrefix(): ?string
88
    {
89 2
        return $this->routePrefix;
90
    }
91
92 2
    public function setNamePrefix(?string $prefix = null): void
93
    {
94 2
        $this->namePrefix = $prefix;
95 2
    }
96
97
    public function getNamePrefix(): string
98
    {
99
        return $this->namePrefix;
100
    }
101
102
    /**
103
     * @param string[]|string|null $versions
104
     */
105 2
    public function setVersions($versions = null): void
106
    {
107 2
        $this->versions = (array) $versions;
108 2
    }
109
110
    /**
111
     * @return string[]|null
112
     */
113
    public function getVersions(): ?array
114
    {
115
        return $this->versions;
116
    }
117
118 2
    public function setPluralize(?bool $pluralize): void
119
    {
120 2
        $this->pluralize = $pluralize;
121 2
    }
122
123
    /**
124
     * @return bool|null
125
     */
126
    public function getPluralize(): ?bool
127
    {
128
        return $this->pluralize;
129
    }
130
131
    /**
132
     * @param string[] $parents Array of parent resources names
133
     */
134 2
    public function setParents(array $parents): void
135
    {
136 2
        $this->parents = $parents;
137 2
    }
138
139
    /**
140
     * @return string[]
141
     */
142
    public function getParents(): array
143
    {
144
        return $this->parents;
145
    }
146
147
    /**
148
     * @param string[] $resource
149
     *
150
     * @throws \InvalidArgumentException
151
     */
152 2
    public function read(RestRouteCollection $collection, \ReflectionMethod $method, array $resource): void
153
    {
154
        // check that every route parent has non-empty singular name
155 2
        foreach ($this->parents as $parent) {
156
            if (empty($parent) || '/' === substr($parent, -1)) {
157
                throw new \InvalidArgumentException('Every parent controller must have `get{SINGULAR}Action(\$id)` method where {SINGULAR} is a singular form of associated object');
158
            }
159
        }
160
161
        // if method is not readable - skip
162 2
        if (!$this->isMethodReadable($method)) {
163
            return;
164
        }
165
166
        // if we can't get http-method and resources from method name - skip
167 2
        $httpMethodAndResources = $this->getHttpMethodAndResourcesFromMethod($method, $resource);
168 2
        if (!$httpMethodAndResources) {
169
            return;
170
        }
171
172 2
        list($httpMethod, $resources, $isCollection, $isInflectable) = $httpMethodAndResources;
173 2
        $arguments = $this->getMethodArguments($method);
174
175
        // if we have only 1 resource & 1 argument passed, then it's object call, so
176
        // we can set collection singular name
177 2
        if (1 === count($resources) && 1 === count($arguments) - count($this->parents)) {
178 1
            $collection->setSingularName($resources[0]);
179
        }
180
181
        // if we have parents passed - merge them with own resource names
182 2
        if (count($this->parents)) {
183
            $resources = array_merge($this->parents, $resources);
184
        }
185
186 2
        if (empty($resources)) {
187
            $resources[] = null;
188
        }
189
190 2
        $routeName = $httpMethod.$this->generateRouteName($resources);
191 2
        $urlParts = $this->generateUrlParts($resources, $arguments, $httpMethod);
192
193
        // if passed method is not valid HTTP method then it's either
194
        // a hypertext driver, a custom object (PUT) or collection (GET)
195
        // method
196 2
        if (!in_array($httpMethod, $this->availableHTTPMethods)) {
197
            $urlParts[] = $httpMethod;
198
            $httpMethod = $this->getCustomHttpMethod($httpMethod, $resources, $arguments);
199
        }
200
201
        // generated parameters
202 2
        $routeName = strtolower($routeName);
203 2
        $path = implode('/', $urlParts);
204 2
        $defaults = ['_controller' => $method->getName()];
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
205 2
        $requirements = [];
206 2
        $options = [];
207 2
        $host = '';
208 2
        $versionCondition = $this->getVersionCondition();
209 2
        $versionRequirement = $this->getVersionRequirement();
210
211 2
        $annotations = $this->readRouteAnnotation($method);
212 2
        if (!empty($annotations)) {
213
            foreach ($annotations as $annotation) {
214
                $path = implode('/', $urlParts);
215
                $defaults = ['_controller' => $method->getName()];
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
216
                $requirements = [];
217
                $options = [];
218
                $methods = explode('|', $httpMethod);
219
220
                $annoRequirements = $annotation->getRequirements();
221
                $annoMethods = $annotation->getMethods();
222
223
                if (!empty($annoMethods)) {
224
                    $methods = $annoMethods;
225
                }
226
227
                $path = null !== $annotation->getPath() ? $this->routePrefix.$annotation->getPath() : $path;
228
                $requirements = array_merge($requirements, $annoRequirements);
229
                $options = array_merge($options, $annotation->getOptions());
230
                $defaults = array_merge($defaults, $annotation->getDefaults());
231
                $host = $annotation->getHost();
232
                $schemes = $annotation->getSchemes();
233
234
                if ($this->hasVersionPlaceholder($path)) {
235
                    $combinedCondition = $annotation->getCondition();
236
                    $requirements = array_merge($versionRequirement, $requirements);
237
                } else {
238
                    $combinedCondition = $this->combineConditions($versionCondition, $annotation->getCondition());
239
                }
240
241
                $this->includeFormatIfNeeded($path, $requirements);
242
243
                // add route to collection
244
                $route = new Route(
245
                    $path,
246
                    $defaults,
247
                    $requirements,
248
                    $options,
249
                    $host,
250
                    $schemes,
251
                    $methods,
252
                    $combinedCondition
253
                );
254
                $this->addRoute($collection, $routeName, $route, $isCollection, $isInflectable, $annotation);
255
            }
256
        } else {
257 2
            if ($this->hasVersionPlaceholder($path)) {
258
                $versionCondition = null;
259
                $requirements = $versionRequirement;
260
            }
261
262 2
            $this->includeFormatIfNeeded($path, $requirements);
263
264 2
            $methods = explode('|', strtoupper($httpMethod));
265
266
            // add route to collection
267 2
            $route = new Route(
268 2
                $path,
269
                $defaults,
270
                $requirements,
271
                $options,
272
                $host,
273 2
                [],
274
                $methods,
275
                $versionCondition
276
            );
277 2
            $this->addRoute($collection, $routeName, $route, $isCollection, $isInflectable);
278
        }
279 2
    }
280
281 2
    private function getVersionCondition(): ?string
282
    {
283 2
        if (empty($this->versions)) {
284 2
            return null;
285
        }
286
287
        return sprintf("request.attributes.get('version') in ['%s']", implode("', '", $this->versions));
288
    }
289
290
    private function combineConditions(?string $conditionOne, ?string $conditionTwo): ?string
291
    {
292
        if (null === $conditionOne) {
293
            return $conditionTwo;
294
        }
295
296
        if (null === $conditionTwo) {
297
            return $conditionOne;
298
        }
299
300
        return sprintf('(%s) and (%s)', $conditionOne, $conditionTwo);
301
    }
302
303 2
    private function getVersionRequirement(): array
304
    {
305 2
        if (empty($this->versions)) {
306 2
            return [];
307
        }
308
309
        return ['version' => implode('|', $this->versions)];
310
    }
311
312 2
    private function hasVersionPlaceholder(string $path): bool
313
    {
314 2
        return false !== strpos($path, '{version}');
315
    }
316
317 2
    private function includeFormatIfNeeded(string &$path, array &$requirements): void
318
    {
319 2
        if (true === $this->includeFormat) {
320 2
            $path .= '.{_format}';
321
322 2
            if (!isset($requirements['_format']) && !empty($this->formats)) {
323
                $requirements['_format'] = implode('|', array_keys($this->formats));
324
            }
325
        }
326 2
    }
327
328 2
    private function isMethodReadable(\ReflectionMethod $method): bool
329
    {
330
        // if method starts with _ - skip
331 2
        if ('_' === substr($method->getName(), 0, 1)) {
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
332
            return false;
333
        }
334
335 2
        $hasNoRouteMethod = (bool) $this->readMethodAnnotation($method, 'NoRoute');
336 2
        $hasNoRouteClass = (bool) $this->readClassAnnotation($method->getDeclaringClass(), 'NoRoute');
337
338 2
        $hasNoRoute = $hasNoRouteMethod || $hasNoRouteClass;
339
        // since NoRoute extends Route we need to exclude all the method NoRoute annotations
340 2
        $hasRoute = (bool) $this->readMethodAnnotation($method, 'Route') && !$hasNoRouteMethod;
341
342
        // if method has NoRoute annotation and does not have Route annotation - skip
343 2
        if ($hasNoRoute && !$hasRoute) {
344
            return false;
345
        }
346
347 2
        return true;
348
    }
349
350
    /**
351
     * @param string[] $resource
352
     */
353 2
    private function getHttpMethodAndResourcesFromMethod(\ReflectionMethod $method, array $resource): ?array
354
    {
355
        // if method doesn't match regex - skip
356 2
        if (!preg_match('/([a-z][_a-z0-9]+)(.*)Action/', $method->getName(), $matches)) {
0 ignored issues
show
Bug introduced by
Consider using $method->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
357
            return null;
358
        }
359
360 2
        $httpMethod = strtolower($matches[1]);
361 2
        $resources = preg_split(
362 2
            '/([A-Z][^A-Z]*)/',
363 2
            $matches[2],
364 2
            -1,
365 2
            PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE
366
        );
367 2
        $isCollection = false;
368 2
        $isInflectable = true;
369
370 2
        if (0 === strpos($httpMethod, self::COLLECTION_ROUTE_PREFIX)
371 2
            && in_array(substr($httpMethod, 1), $this->availableHTTPMethods)
372
        ) {
373 1
            $isCollection = true;
374 1
            $httpMethod = substr($httpMethod, 1);
375 2
        } elseif ('options' === $httpMethod) {
376
            $isCollection = true;
377
        }
378
379 2
        if ($isCollection && !empty($resource)) {
380 1
            $resourcePluralized = $this->generateResourceName(end($resource));
0 ignored issues
show
Security Bug introduced by
It seems like end($resource) targeting end() can also be of type false; however, FOS\RestBundle\Routing\L...:generateResourceName() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
381 1
            $isInflectable = ($resourcePluralized != $resource[count($resource) - 1]);
382 1
            $resource[count($resource) - 1] = $resourcePluralized;
383
        }
384
385 2
        $resources = array_merge($resource, $resources);
386
387 2
        return [$httpMethod, $resources, $isCollection, $isInflectable];
388
    }
389
390
    /**
391
     * @return \ReflectionParameter[]
392
     */
393 2
    private function getMethodArguments(\ReflectionMethod $method): array
394
    {
395
        // ignore all query params
396 2
        $params = $this->paramReader->getParamsFromMethod($method);
397
398
        // check if a parameter is coming from the request body
399 2
        $ignoreParameters = [];
400 2
        if (class_exists(ParamConverter::class)) {
401
            $ignoreParameters = array_map(function ($annotation) {
402
                return
403 1
                    $annotation instanceof ParamConverter &&
404 1
                    'fos_rest.request_body' === $annotation->getConverter()
405 1
                        ? $annotation->getName() : null;
406 2
            }, $this->annotationReader->getMethodAnnotations($method));
407
        }
408
409
        // ignore several type hinted arguments
410
        $ignoreClasses = [
411 2
            ConstraintViolationListInterface::class,
412
            MessageInterface::class,
413
            ParamConverter::class,
414
            ParamFetcherInterface::class,
415
            Request::class,
416
            SessionInterface::class,
417
            UserInterface::class,
418
        ];
419
420 2
        $arguments = [];
421 2
        foreach ($method->getParameters() as $argument) {
422 2
            if (isset($params[$argument->getName()])) {
0 ignored issues
show
Bug introduced by
Consider using $argument->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
423
                continue;
424
            }
425
426 2
            $argumentClass = $argument->getClass();
427 2
            if ($argumentClass) {
428 2
                $className = $argumentClass->getName();
0 ignored issues
show
Bug introduced by
Consider using $argumentClass->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
429 2
                foreach ($ignoreClasses as $class) {
430 2
                    if ($className === $class || is_subclass_of($className, $class)) {
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if $class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
431 1
                        continue 2;
432
                    }
433
                }
434
            }
435
436 2
            if (in_array($argument->getName(), $ignoreParameters, true)) {
0 ignored issues
show
Bug introduced by
Consider using $argument->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
437 1
                continue;
438
            }
439
440 1
            $arguments[] = $argument;
441
        }
442
443 2
        return $arguments;
444
    }
445
446 2
    private function generateResourceName(string $resource): string
447
    {
448 2
        if (false === $this->pluralize) {
449
            return $resource;
450
        }
451
452 2
        return $this->inflector->pluralize($resource);
453
    }
454
455
    /**
456
     * @param string[] $resources
457
     */
458 2
    private function generateRouteName(array $resources): string
459
    {
460 2
        $routeName = '';
461 2
        foreach ($resources as $resource) {
462 2
            if (null !== $resource) {
463 2
                $routeName .= '_'.basename($resource);
464
            }
465
        }
466
467 2
        return $routeName;
468
    }
469
470
    /**
471
     * @param string[]               $resources
472
     * @param \ReflectionParameter[] $arguments
473
     */
474 2
    private function generateUrlParts(array $resources, array $arguments, string $httpMethod): array
475
    {
476 2
        $urlParts = [];
477 2
        foreach ($resources as $i => $resource) {
478
            // if we already added all parent routes paths to URL & we have
479
            // prefix - add it
480 2
            if (!empty($this->routePrefix) && $i === count($this->parents)) {
481
                $urlParts[] = $this->routePrefix;
482
            }
483
484
            // if we have argument for current resource, then it's object.
485
            // otherwise - it's collection
486 2
            if (isset($arguments[$i])) {
487 1
                if (null !== $resource) {
488 1
                    $urlParts[] =
489 1
                        strtolower($this->generateResourceName($resource))
490 1
                        .'/{'.$arguments[$i]->getName().'}';
0 ignored issues
show
Bug introduced by
Consider using $arguments[$i]->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
491
                } else {
492 1
                    $urlParts[] = '{'.$arguments[$i]->getName().'}';
0 ignored issues
show
Bug introduced by
Consider using $arguments[$i]->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
493
                }
494 2
            } elseif (null !== $resource) {
495 2
                if ((0 === count($arguments) && !in_array($httpMethod, $this->availableHTTPMethods))
496 2
                    || 'new' === $httpMethod
497 2
                    || 'post' === $httpMethod
498
                ) {
499 2
                    $urlParts[] = $this->generateResourceName(strtolower($resource));
500
                } else {
501 1
                    $urlParts[] = strtolower($resource);
502
                }
503
            }
504
        }
505
506 2
        return $urlParts;
507
    }
508
509
    /**
510
     * @param string[]               $resources
511
     * @param \ReflectionParameter[] $arguments
512
     */
513
    private function getCustomHttpMethod(string $httpMethod, array $resources, array $arguments): string
514
    {
515
        if (in_array($httpMethod, $this->availableConventionalActions)) {
516
            // allow hypertext as the engine of application state
517
            // through conventional GET actions
518
            return 'get';
519
        }
520
521
        if (count($arguments) < count($resources)) {
522
            // resource collection
523
            return 'get';
524
        }
525
526
        // custom object
527
        return 'patch';
528
    }
529
530
    /**
531
     * @return RouteAnnotation[]
532
     */
533 2
    private function readRouteAnnotation(\ReflectionMethod $reflectionMethod): array
534
    {
535 2
        $annotations = [];
536
537 2
        if ($newAnnotations = $this->readMethodAnnotations($reflectionMethod, 'Route')) {
538
            $annotations = array_merge($annotations, $newAnnotations);
539
        }
540
541 2
        return $annotations;
542
    }
543
544 2
    private function readClassAnnotation(\ReflectionClass $reflectionClass, string $annotationName): ?RouteAnnotation
545
    {
546 2
        $annotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
547
548 2
        if ($annotation = $this->annotationReader->getClassAnnotation($reflectionClass, $annotationClass)) {
549
            return $annotation;
550
        }
551
552 2
        return null;
553
    }
554
555 2
    private function readMethodAnnotation(\ReflectionMethod $reflectionMethod, string $annotationName): ?RouteAnnotation
556
    {
557 2
        $annotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
558
559 2
        if ($annotation = $this->annotationReader->getMethodAnnotation($reflectionMethod, $annotationClass)) {
560
            return $annotation;
561
        }
562
563 2
        return null;
564
    }
565
566
    /**
567
     * @return RouteAnnotation[]
568
     */
569 2
    private function readMethodAnnotations(\ReflectionMethod $reflectionMethod, string $annotationName): array
570
    {
571 2
        $annotations = [];
572 2
        $annotationClass = "FOS\\RestBundle\\Controller\\Annotations\\$annotationName";
573
574 2
        if ($annotations_new = $this->annotationReader->getMethodAnnotations($reflectionMethod)) {
575 1
            foreach ($annotations_new as $annotation) {
576 1
                if ($annotation instanceof $annotationClass) {
577
                    $annotations[] = $annotation;
578
                }
579
            }
580
        }
581
582 2
        return $annotations;
583
    }
584
585 2
    private function addRoute(RestRouteCollection $collection, string $routeName, Route $route, bool $isCollection, bool $isInflectable, RouteAnnotation $annotation = null): void
586
    {
587 2
        if ($annotation && null !== $annotation->getName()) {
588
            $options = $annotation->getOptions();
589
590
            if (false === $this->hasMethodPrefix || (isset($options['method_prefix']) && false === $options['method_prefix'])) {
591
                $routeName = $annotation->getName();
592
            } else {
593
                $routeName .= $annotation->getName();
594
            }
595
        }
596
597 2
        $fullRouteName = $this->namePrefix.$routeName;
598
599 2
        if ($isCollection && !$isInflectable) {
600
            $collection->add($this->namePrefix.self::COLLECTION_ROUTE_PREFIX.$routeName, $route);
601
            if (!$collection->get($fullRouteName)) {
602
                $collection->add($fullRouteName, clone $route);
603
            }
604
        } else {
605 2
            $collection->add($fullRouteName, $route);
606
        }
607 2
    }
608
}
609