Completed
Push — master ( cc3bea...d6d1d3 )
by Dana
8s
created

AnnotationService::prefixMatchesUri()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 3
nc 2
nop 1
1
<?php
2
/**
3
 * This file is part of the silex-annotation-provider package.
4
 * For the full copyright and license information, please view the LICENSE
5
 * file that was distributed with this source code.
6
 *
7
 * @license       MIT License
8
 * @copyright (c) 2014, Dana Desrosiers <[email protected]>
9
 */
10
11
namespace DDesrosiers\SilexAnnotations;
12
13
use DDesrosiers\SilexAnnotations\Annotations\Controller;
14
use DDesrosiers\SilexAnnotations\Annotations\Request;
15
use DDesrosiers\SilexAnnotations\Annotations\Route;
16
use DDesrosiers\SilexAnnotations\Annotations\RouteAnnotation;
17
use Doctrine\Common\Annotations\AnnotationReader;
18
use Doctrine\Common\Annotations\CachedReader;
19
use Doctrine\Common\Cache\Cache;
20
use Pimple\Container;
21
use ReflectionClass;
22
use ReflectionMethod;
23
use RuntimeException;
24
use Silex\Application;
25
use Silex\ControllerCollection;
26
27
/**
28
 * Class AnnotationService parses annotations on classes and converts them to
29
 * Silex routes.
30
 *
31
 * @author Dana Desrosiers <[email protected]>
32
 */
33
class AnnotationService
34
{
35
    /** @var Application */
36
    protected $app;
37
38
    /** @var AnnotationReader */
39
    protected $reader;
40
41
    /** @var Cache */
42
    protected $cache;
43
44
    /** @var bool */
45
    protected $useCache;
46
47
    const CONTROLLER_CACHE_INDEX = 'annot.controllerFiles';
48
    /**
49
     * @param Container $app
50
     */
51
    public function __construct(Container $app)
52
    {
53
        $this->app = $app;
0 ignored issues
show
Documentation Bug introduced by
$app is of type object<Pimple\Container>, but the property $app was declared to be of type object<Silex\Application>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
54
55
        if ($app->offsetExists('annot.cache')) {
56
            if ($app['annot.cache'] instanceof Cache) {
57
                $this->cache = $app['annot.cache'];
58
            } else if (is_string($app['annot.cache']) && strlen($app['annot.cache']) > 0) {
59
                $cacheClass = "Doctrine\\Common\\Cache\\".$app['annot.cache']."Cache";
60
                if (!class_exists($cacheClass)) {
61
                    throw new RuntimeException("Cache type: [$cacheClass] does not exist.  Make sure you include Doctrine cache.");
62
                }
63
64
                $this->cache = new $cacheClass();
65
            } else {
66
                throw new RuntimeException("Cache object does not implement Doctrine\\Common\\Cache\\Cache");
67
            }
68
69
            $this->reader = new CachedReader(new AnnotationReader(), $this->cache, $app['debug']);
0 ignored issues
show
Documentation Bug introduced by
It seems like new \Doctrine\Common\Ann...->cache, $app['debug']) of type object<Doctrine\Common\Annotations\CachedReader> is incompatible with the declared type object<Doctrine\Common\A...tions\AnnotationReader> of property $reader.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
70
        } else {
71
            $this->reader = new AnnotationReader();
72
        }
73
74
        $this->useCache = !$this->app['debug'] && $this->cache instanceof Cache;
75
    }
76
77
    /**
78
     * @param $dir
79
     * @return array
80
     */
81
    public function discoverControllers($dir)
82
    {
83
        $cacheKey = self::CONTROLLER_CACHE_INDEX . ".$dir";
84
85
        if ($this->useCache && $this->cache->contains($cacheKey)) {
86
            $controllerFiles = $this->cache->fetch($cacheKey);
87
        } else {
88
            $controllerFiles = $this->app['annot.controllerFinder']($this->app, $dir);
89
90
            if ($this->useCache) {
91
                $this->cache->save($cacheKey, $controllerFiles);
92
            }
93
        }
94
95
        return $controllerFiles;
96
    }
97
98
    /**
99
     * @param $controllers
100
     */
101
    public function registerControllers($controllers)
102
    {
103
        foreach ($controllers as $prefix => $controllerNames) {
104
            if (!is_array($controllerNames)) {
105
                $controllerNames = [$controllerNames];
106
            }
107
            foreach ($controllerNames as $fqcn) {
108
                if (strlen($prefix) == 0 || $this->prefixMatchesUri($prefix)) {
109
                    $this->registerController($fqcn);
110
                }
111
            }
112
        }
113
    }
114
115
    public function prefixMatchesUri($prefix)
116
    {
117
        return ($this->app->offsetExists('annot.base_uri')
118
            && strpos($_SERVER['REQUEST_URI'], $this->app['annot.base_uri'].$prefix) === 0);
119
    }
120
121
    /**
122
     * Recursively walk the file tree starting from $dir to find potential controller class files.
123
     * Returns array of fully qualified class names.
124
     * Namespace detection works with PSR-0 or PSR-4 autoloading.
125
     *
126
     * @param string  $dir
127
     * @param string  $namespace
128
     * @param array   $files
129
     * @return array
130
     */
131
    public function getFiles($dir, $namespace='', $files=array())
132
    {
133
        if ($handle = opendir($dir)) {
134
            while (false !== ($entry = readdir($handle))) {
135
                if (!in_array($entry, array('.', '..'))) {
136
                    $filePath = "$dir/$entry";
137
                    if (is_dir($filePath)) {
138
                        $subNamespace = $namespace ? $namespace."$entry\\" : '';
139
                        $files = $this->getFiles($filePath, $subNamespace, $files);
140
                    } else {
141
                        if (!$namespace) {
142
                            $namespace = $this->parseNamespace($filePath);
143
                        }
144
                        $pathInfo = pathinfo($entry);
145
                        $className = trim($namespace.$pathInfo['filename']);
146
                        if (class_exists($className)) {
147
                            $reflectionClass = new ReflectionClass($className);
148
                            $annotationClassName = "\\DDesrosiers\\SilexAnnotations\\Annotations\\Controller";
149
                            $controllerAnnotation = $this->reader->getClassAnnotation($reflectionClass, $annotationClassName);
150
151
                            if ($this->hasPrefix($controllerAnnotation)) {
0 ignored issues
show
Bug introduced by
It seems like $controllerAnnotation defined by $this->reader->getClassA..., $annotationClassName) on line 149 can also be of type object; however, DDesrosiers\SilexAnnotat...ionService::hasPrefix() does only seem to accept null|object<DDesrosiers\...Annotations\Controller>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
152
                                $files[$controllerAnnotation->getPrefix()][] = $className;
153
                            } else {
154
                                $files[] = $className;
155
                            }
156
                        }
157
                    }
158
                }
159
            }
160
            closedir($handle);
161
        }
162
163
        return $files;
164
    }
165
166
    public function hasPrefix(Controller $controllerAnnotation = null)
167
    {
168
        $hasPrefix = $controllerAnnotation instanceof Controller && strlen($controllerAnnotation->prefix) > 0;
169
170
        return $this->app->offsetExists('annot.base_uri') && $hasPrefix;
171
    }
172
173
    /**
174
     * Register the controller if a Controller annotation exists in the class doc block or $controllerAnnotation is provided.
175
     *
176
     * @param string     $controllerName
177
     */
178
    public function registerController($controllerName)
179
    {
180
        $reflectionClass = new ReflectionClass($controllerName);
181
        $annotationClassName = "\\DDesrosiers\\SilexAnnotations\\Annotations\\Controller";
182
        $controllerAnnotation = $this->reader->getClassAnnotation($reflectionClass, $annotationClassName);
183
184
        if ($controllerAnnotation instanceof Controller) {
185
            $this->app['annot.registerServiceController'](trim($controllerName, "\\"));
186
            $controllerAnnotation->process($this->app, $reflectionClass);
187
        }
188
    }
189
190
    /**
191
     * @param string $controllerName
192
     * @param boolean $isServiceController
193
     * @param boolean $newCollection
194
     * @return \Silex\ControllerCollection
195
     */
196
    public function process($controllerName, $isServiceController = true, $newCollection = false)
197
    {
198
        $this->app['annot.useServiceControllers'] = $isServiceController;
199
        $controllerCollection = $newCollection ? $this->app['controllers_factory'] : $this->app['controllers'];
200
        $reflectionClass = new ReflectionClass($controllerName);
201
202
        $this->processMethodAnnotations($reflectionClass, $controllerCollection);
203
204
        return $controllerCollection;
205
    }
206
207
    /**
208
     * @param ReflectionClass      $reflectionClass
209
     * @param ControllerCollection $controllerCollection
210
     */
211
    public function processClassAnnotations(ReflectionClass $reflectionClass, ControllerCollection $controllerCollection)
212
    {
213
        foreach ($this->reader->getClassAnnotations($reflectionClass) as $annotation) {
214
            if ($annotation instanceof RouteAnnotation) {
215
                $annotation->process($controllerCollection);
216
            }
217
        }
218
    }
219
220
    /**
221
     * @param ReflectionClass      $reflectionClass
222
     * @param ControllerCollection $controllerCollection
223
     */
224
    public function processMethodAnnotations(ReflectionClass $reflectionClass, ControllerCollection $controllerCollection)
225
    {
226
        $separator = $this->app['annot.useServiceControllers'] ? ":" : "::";
227
228
        foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $reflectionMethod) {
229
            if (!$reflectionMethod->isStatic()) {
230
                $controllerMethodName = $this->app['annot.controller_factory'](
231
                    $this->app,
232
                    $reflectionClass->name,
233
                    $reflectionMethod->name,
234
                    $separator
235
                );
236
                $methodAnnotations = $this->reader->getMethodAnnotations($reflectionMethod);
237
                foreach ($methodAnnotations as $annotation) {
238
                    if ($annotation instanceof Route) {
239
                        $annotation->process($controllerCollection, $controllerMethodName, $this->app);
240
                    } else if ($annotation instanceof Request) {
241
                        $controller = $annotation->process($controllerCollection, $controllerMethodName);
242
                        foreach ($methodAnnotations as $routeAnnotation) {
243
                            if ($routeAnnotation instanceof RouteAnnotation) {
244
                                $routeAnnotation->process($controller);
245
                            }
246
                        }
247
                    }
248
                }
249
            }
250
        }
251
    }
252
253
    /**
254
     * @return AnnotationReader
255
     */
256
    public function getReader()
257
    {
258
        return $this->reader;
259
    }
260
261
    /**
262
     * Parse the given file to find the namespace.
263
     *
264
     * @param $filePath
265
     * @return string
266
     */
267
    protected function parseNamespace($filePath)
268
    {
269
        preg_match('/namespace(.*);/', file_get_contents($filePath), $result);
270
        return isset($result[1]) ? $result[1] . "\\" : '';
271
    }
272
}
273