Passed
Push — master ( 81799a...502582 )
by Gennady
01:52
created

App   A

Complexity

Total Complexity 32

Size/Duplication

Total Lines 194
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 32
lcom 1
cbo 12
dl 0
loc 194
ccs 78
cts 78
cp 1
rs 9.84
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 2
A loadControllers() 0 10 3
B addRoutes() 0 41 11
A createRouteCallback() 0 7 1
A getAnnotation() 0 10 3
A getControllerFiles() 0 14 3
B createControllerInstance() 0 18 7
A createAnnotationReader() 0 13 2
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the slim-annotation-based package.
7
 *
8
 * (c) Gennady Knyazkin <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Redreams\Slim;
15
16
use Doctrine\Common\Annotations\AnnotationReader;
17
use Doctrine\Common\Annotations\AnnotationRegistry;
18
use Doctrine\Common\Annotations\CachedReader;
19
use Doctrine\Common\Annotations\Reader;
20
use Doctrine\Common\Cache\FilesystemCache;
21
use FilesystemIterator;
22
use Generator;
23
use Psr\Container\ContainerInterface;
24
use RecursiveCallbackFilterIterator;
25
use RecursiveDirectoryIterator;
26
use RecursiveIteratorIterator;
27
use Redreams\ClassFinder\ClassFinder;
28
use Redreams\Slim\Annotation\Route;
29
use Redreams\Slim\Exception\InvalidArgumentException;
30
use ReflectionClass;
31
use ReflectionMethod;
32
use ReflectionParameter;
33
use Slim\App as SlimApp;
34
use Slim\Router;
35
use SplFileInfo;
36
use function file_get_contents;
37
use function is_dir;
38
use function rtrim;
39
use function sprintf;
40
use function trim;
41
42
/**
43
 * Class App
44
 *
45
 * @author Gennady Knyazkin <[email protected]>
46
 */
47
class App extends SlimApp
48
{
49
    /**
50
     * @var Reader
51
     */
52
    protected $reader;
53
54
    /**
55
     * App constructor.
56
     *
57
     * @param string                   $controllersDir
58
     * @param array|ContainerInterface $container
59
     *
60
     * @throws InvalidArgumentException
61
     * @throws \InvalidArgumentException
62
     * @throws \Doctrine\Common\Annotations\AnnotationException
63
     * @throws \ReflectionException
64
     */
65 6
    public function __construct(string $controllersDir, $container = [])
66
    {
67 6
        parent::__construct($container);
68 6
        if (!is_dir($controllersDir)) {
69 1
            throw new InvalidArgumentException(
70 1
                sprintf('Controllers directory "%s" does not exists', $controllersDir)
71
            );
72
        }
73 5
        AnnotationRegistry::registerLoader('class_exists');
0 ignored issues
show
Deprecated Code introduced by
The method Doctrine\Common\Annotati...istry::registerLoader() has been deprecated with message: this method is deprecated and will be removed in doctrine/annotations 2.0 autoloading should be deferred to the globally registered autoloader by then. For now, use @example AnnotationRegistry::registerLoader('class_exists')

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
74 5
        $this->reader = $this->createAnnotationReader();
75 5
        $this->loadControllers($controllersDir);
76 5
    }
77
78
79
    /**
80
     * @param string $controllerDir
81
     *
82
     * @return void
83
     * @throws \InvalidArgumentException
84
     * @throws \ReflectionException
85
     */
86 5
    protected function loadControllers(string $controllerDir): void
87
    {
88
        /** @var SplFileInfo $file */
89 5
        foreach ($this->getControllerFiles($controllerDir) as $file) {
90 5
            if (($class = ClassFinder::find(file_get_contents($file->getRealPath()))) !== null) {
91 5
                $this->addRoutes($class);
92
            }
93 5
            gc_mem_caches();
94
        }
95 5
    }
96
97
    /**
98
     * @param string $class
99
     *
100
     * @return void
101
     * @throws \ReflectionException
102
     * @throws \InvalidArgumentException
103
     */
104 5
    protected function addRoutes(string $class): void
105
    {
106 5
        static $instance;
107
        /** @var Router $router */
108 5
        $router = $this->getContainer()->get('router');
109 5
        $settings = $this->getContainer()->get('settings');
110 5
        $relectionClass = new ReflectionClass($class);
111
        /** @var Route $classRoute */
112 5
        $classRoute = $this->reader->getClassAnnotation($relectionClass, Route::class);
113 5
        $pattern = '/';
114 5
        if ($classRoute !== null && !empty(trim($classRoute->getPattern(), '/'))) {
115 5
            $pattern = sprintf('/%s/', trim($classRoute->getPattern(), '/'));
116
        }
117 5
        foreach ($relectionClass->getMethods() as $reflectionMethod) {
118 5
            $methodAnnotations = $this->reader->getMethodAnnotations($reflectionMethod);
119
            /** @var Route $methodRoute */
120 5
            if ($reflectionMethod->isStatic()
121 5
                || !$reflectionMethod->isPublic()
122 5
                || ($methodRoute = $this->getAnnotation($methodAnnotations, Route::class)) === null
123
            ) {
124 5
                continue;
125
            }
126 5
            if ($instance === null) {
127 5
                $instance = $this->createControllerInstance($relectionClass);
128
            }
129 5
            $methods = $methodRoute->getMethods() ?? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
130 5
            $route = $router->map(
131 5
                $methods,
132 5
                rtrim($pattern.ltrim($methodRoute->getPattern(), '/'), '/') ?: '/',
133 5
                $this->createRouteCallback($instance, $reflectionMethod, $methodAnnotations)
134
            );
135 5
            $route->setOutputBuffering($settings['outputBuffering']);
136 5
            if ($methodRoute->getName() !== null) {
137 5
                $route->setName($methodRoute->getName());
138
            }
139
140
        }
141 5
        if ($instance !== null) {
142 5
            $instance = null;
143
        }
144 5
    }
145
146
    /**
147
     * @param                  $controllerInstance
148
     * @param ReflectionMethod $method
149
     * @param array            $annotations
150
     *
151
     * @return callable
152
     */
153 5
    protected function createRouteCallback(
154
        $controllerInstance,
155
        ReflectionMethod $method,
156
        array $annotations = []
0 ignored issues
show
Unused Code introduced by
The parameter $annotations is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
157
    ): callable {
158 5
        return [$controllerInstance, $method->getName()];
159
    }
160
161
    /**
162
     * @param array  $annotations
163
     * @param string $name
164
     *
165
     * @return object|null
166
     */
167 5
    protected function getAnnotation(array $annotations, string $name)
168
    {
169 5
        foreach ($annotations as $annotation) {
170 5
            if ($annotation instanceof $name) {
171 5
                return $annotation;
172
            }
173
        }
174
175 5
        return null;
176
    }
177
178
    /**
179
     * @param string $controllerDir
180
     *
181
     * @return Generator|SplFileInfo
182
     */
183 5
    protected function getControllerFiles(string $controllerDir)
184
    {
185 5
        $directoryIterator = new RecursiveDirectoryIterator(
186 5
            $controllerDir,
187 5
            FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS
188
        );
189
        $iterator = new RecursiveCallbackFilterIterator($directoryIterator, function (SplFileInfo $file) {
190 5
            return $file->isFile() && $file->getExtension() === 'php';
191 5
        });
192
        /** @var SplFileInfo $file */
193 5
        foreach (new RecursiveIteratorIterator($iterator) as $file) {
194 5
            yield $file;
195
        }
196 5
    }
197
198
    /**
199
     * @param ReflectionClass $relectionClass
200
     *
201
     * @return object
202
     */
203 5
    protected function createControllerInstance(ReflectionClass $relectionClass)
204
    {
205 5
        $args = [];
206 5
        $constructor = $relectionClass->getConstructor();
207 5
        if ($constructor !== null && $constructor->getNumberOfParameters() > 0) {
208
            /** @var ReflectionParameter $firstParameter */
209 5
            $firstParameter = $constructor->getParameters()[0];
210 5
            $parameterClass = $firstParameter->getClass();
211 5
            if ($parameterClass !== null
212 5
                && (($parameterClass->isInterface() && $parameterClass->getName() === ContainerInterface::class)
213 5
                    || $parameterClass->implementsInterface(ContainerInterface::class))
214
            ) {
215 5
                $args[] = $this->getContainer();
216
            }
217
        }
218
219 5
        return $relectionClass->newInstanceArgs($args);
220
    }
221
222
    /**
223
     * @return Reader
224
     * @throws \Doctrine\Common\Annotations\AnnotationException
225
     * @throws \InvalidArgumentException
226
     */
227 5
    protected function createAnnotationReader(): Reader
228
    {
229 5
        $settings = $this->getContainer()->get('settings');
230 5
        if (isset($settings['routerCacheDir'])) {
231 1
            return new CachedReader(
232 1
                new AnnotationReader(),
233 1
                new FilesystemCache($settings['routerCacheDir']),
234 1
                false
235
            );
236
        }
237
238 5
        return new AnnotationReader();
239
    }
240
}
241