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