Completed
Push — 8.0 ( bf11e8...f611ca )
by David
02:23
created

ControllerAnalyzer::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 3
1
<?php
2
3
namespace Mouf\Mvc\Splash\Services;
4
5
use Doctrine\Common\Annotations\Reader;
6
use Interop\Container\ContainerInterface;
7
use Mouf\Mvc\Splash\Annotations\Action;
8
use Mouf\Mvc\Splash\Annotations\URL;
9
use Mouf\Mvc\Splash\Annotations\Delete;
10
use Mouf\Mvc\Splash\Annotations\Get;
11
use Mouf\Mvc\Splash\Annotations\Post;
12
use Mouf\Mvc\Splash\Annotations\Put;
13
use Mouf\Mvc\Splash\Annotations\Title;
14
use ReflectionMethod;
15
16
/**
17
 * This class is in charge of analyzing a controller instance and returning the routes it contains.
18
 */
19
class ControllerAnalyzer
20
{
21
    private $container;
22
23
    /**
24
     * @var ParameterFetcherRegistry
25
     */
26
    private $parameterFetcherRegistry;
27
28
    /**
29
     * @var Reader
30
     */
31
    private $annotationReader;
32
33
    /**
34
     * Initializes the registry with an array of container instances names.
35
     *
36
     * @param ParameterFetcherRegistry $parameterFetcherRegistry
37
     * @param Reader                   $annotationReader         A Doctrine annotation reader
38
     */
39
    public function __construct(ContainerInterface $container, ParameterFetcherRegistry $parameterFetcherRegistry, Reader $annotationReader)
40
    {
41
        $this->container = $container;
42
        $this->parameterFetcherRegistry = $parameterFetcherRegistry;
43
        $this->annotationReader = $annotationReader;
44
    }
45
46
    /**
47
     * Returns is a given class name is a controller or not (whether it contains @Action or @URL annotations).
48
     *
49
     * @param string $className
50
     *
51
     * @return bool
52
     */
53
    public function isController($className) : bool
54
    {
55
        $refClass = new \ReflectionClass($className);
56
57
        foreach ($refClass->getMethods() as $refMethod) {
58
            $actionAnnotation = $this->annotationReader->getMethodAnnotation($refMethod, Action::class);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $actionAnnotation is correct as $this->annotationReader-...otations\Action::class) (which targets Doctrine\Common\Annotati...::getMethodAnnotation()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
59
            if ($actionAnnotation) {
60
                return true;
61
            }
62
            $urlAnnotation = $this->annotationReader->getMethodAnnotation($refMethod, URL::class);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $urlAnnotation is correct as $this->annotationReader-...Annotations\URL::class) (which targets Doctrine\Common\Annotati...::getMethodAnnotation()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
63
            if ($urlAnnotation) {
64
                return true;
65
            }
66
        }
67
68
        return false;
69
    }
70
71
    /**
72
     * Returns an array of SplashRoute for the controller passed in parameter.
73
     *
74
     * @param object $controller
0 ignored issues
show
Documentation introduced by
There is no parameter named $controller. Did you maybe mean $controllerInstanceName?

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...
75
     *
76
     * @return SplashRoute[]
77
     *
78
     * @throws \Mouf\Mvc\Splash\Utils\SplashException
79
     */
80
    public function analyzeController(string $controllerInstanceName) : array
81
    {
82
        // Let's analyze the controller and get all the @Action annotations:
83
        $urlsList = array();
84
85
        $controller = $this->container->get($controllerInstanceName);
86
87
        $refClass = new \ReflectionClass($controller);
88
89
        foreach ($refClass->getMethods() as $refMethod) {
90
            $title = null;
91
            // Now, let's check the "Title" annotation (note: we do not support multiple title annotations for the same method)
92
            $titleAnnotation = $this->annotationReader->getMethodAnnotation($refMethod, Title::class);
93
            if ($titleAnnotation !== null) {
94
                /* @var $titleAnnotation TitleAnnotation */
95
                $title = $titleAnnotation->getTitle();
96
            }
97
98
            // First, let's check the "Action" annotation
99
            $actionAnnotation = $this->annotationReader->getMethodAnnotation($refMethod, Action::class);
100
            if ($actionAnnotation !== null) {
101
                $methodName = $refMethod->getName();
0 ignored issues
show
Bug introduced by
Consider using $refMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
102
                if ($methodName === 'index') {
103
                    $url = $controllerInstanceName.'/';
104
                } else {
105
                    $url = $controllerInstanceName.'/'.$methodName;
106
                }
107
                $parameters = $this->parameterFetcherRegistry->mapParameters($refMethod);
108
                $filters = FilterUtils::getFilters($refMethod, $this->annotationReader);
109
                $urlsList[] = new SplashRoute($url, $controllerInstanceName, $refMethod->getName(), $title, $refMethod->getDocComment(), $this->getSupportedHttpMethods($refMethod), $parameters, $filters);
0 ignored issues
show
Bug introduced by
Consider using $refMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
110
            }
111
112
            // Now, let's check the "URL" annotation (note: we support multiple URL annotations for the same method)
113
            $annotations = $this->annotationReader->getMethodAnnotations($refMethod);
114
115
            foreach ($annotations as $annotation) {
116
                if (!$annotation instanceof URL) {
117
                    continue;
118
                }
119
120
                /* @var $annotation URL */
121
                $url = $annotation->getUrl();
122
123
                // Get public properties if they exist in the URL
124
                if (preg_match_all('/[^{]*{\$this->([^\/]*)}[^{]*/', $url, $output)) {
125
                    foreach ($output[1] as $param) {
126
                        $value = $this->readPrivateProperty($controller, $param);
127
                        $url = str_replace('{$this->'.$param.'}', $value, $url);
128
                    }
129
                }
130
131
                $url = ltrim($url, '/');
132
                $parameters = $this->parameterFetcherRegistry->mapParameters($refMethod, $url);
133
                $filters = FilterUtils::getFilters($refMethod, $this->annotationReader);
134
                $urlsList[] = new SplashRoute($url, $controllerInstanceName, $refMethod->getName(), $title, $refMethod->getDocComment(), $this->getSupportedHttpMethods($refMethod), $parameters, $filters);
0 ignored issues
show
Bug introduced by
Consider using $refMethod->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
135
            }
136
        }
137
138
        return $urlsList;
139
    }
140
141
    /**
142
     * Reads a private property value.
143
     * Credit to Ocramius: https://ocramius.github.io/blog/accessing-private-php-class-members-without-reflection/.
144
     *
145
     * @param object $object
146
     * @param string $property
147
     *
148
     * @return mixed
149
     */
150
    private function &readPrivateProperty($object, string $property)
151
    {
152
        $value = &\Closure::bind(function &() use ($property) {
153
            return $this->$property;
154
        }, $object, $object)->__invoke();
155
156
        return $value;
157
    }
158
159
    /**
160
     * Returns the supported HTTP methods on this function, based on the annotations (@Get, @Post, etc...).
161
     *
162
     * @param ReflectionMethod $refMethod
163
     *
164
     * @return array
165
     */
166
    private function getSupportedHttpMethods(ReflectionMethod $refMethod) : array
167
    {
168
        $methods = array();
169
170
        if ($this->annotationReader->getMethodAnnotation($refMethod, Get::class)) {
171
            $methods[] = 'GET';
172
        }
173
        if ($this->annotationReader->getMethodAnnotation($refMethod, Post::class)) {
174
            $methods[] = 'POST';
175
        }
176
        if ($this->annotationReader->getMethodAnnotation($refMethod, Put::class)) {
177
            $methods[] = 'PUT';
178
        }
179
        if ($this->annotationReader->getMethodAnnotation($refMethod, Delete::class)) {
180
            $methods[] = 'DELETE';
181
        }
182
183
        return $methods;
184
    }
185
}
186