App::createControllerInstance()   B
last analyzed

Complexity

Conditions 7
Paths 3

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 11
cts 11
cp 1
rs 8.8333
c 0
b 0
f 0
cc 7
nc 3
nop 1
crap 7
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 ReflectionParameter;
32
use Slim\App as SlimApp;
33
use Slim\Interfaces\RouteInterface;
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
                [$instance, $reflectionMethod->getName()]
134
            );
135 5
            $route->setOutputBuffering($settings['outputBuffering']);
136 5
            if ($methodRoute->getName() !== null) {
137 5
                $route->setName($methodRoute->getName());
138
            }
139 5
            $this->handleActionAnnotations($methodAnnotations, $route);
140
141
        }
142 5
        if ($instance !== null) {
143 5
            $instance = null;
144
        }
145 5
    }
146
147
    /**
148
     * @param array  $annotations
149
     * @param string $name
150
     *
151
     * @return object|null
152
     */
153 5
    protected function getAnnotation(array $annotations, string $name)
154
    {
155 5
        foreach ($annotations as $annotation) {
156 5
            if ($annotation instanceof $name) {
157 5
                return $annotation;
158
            }
159
        }
160
161 5
        return null;
162
    }
163
164
    /**
165
     * @param string $controllerDir
166
     *
167
     * @return Generator|SplFileInfo
168
     */
169 5
    protected function getControllerFiles(string $controllerDir)
170
    {
171 5
        $directoryIterator = new RecursiveDirectoryIterator(
172 5
            $controllerDir,
173 5
            FilesystemIterator::SKIP_DOTS | FilesystemIterator::FOLLOW_SYMLINKS
174
        );
175
        $iterator = new RecursiveCallbackFilterIterator($directoryIterator, function (SplFileInfo $file) {
176 5
            return $file->isDir() || ($file->isFile() && $file->getExtension() === 'php');
177 5
        });
178
        /** @var SplFileInfo $file */
179 5
        foreach (new RecursiveIteratorIterator($iterator) as $file) {
180 5
            yield $file;
181
        }
182 5
    }
183
184
    /**
185
     * @param ReflectionClass $relectionClass
186
     *
187
     * @return object
188
     */
189 5
    protected function createControllerInstance(ReflectionClass $relectionClass)
190
    {
191 5
        $args = [];
192 5
        $constructor = $relectionClass->getConstructor();
193 5
        if ($constructor !== null && $constructor->getNumberOfParameters() > 0) {
194
            /** @var ReflectionParameter $firstParameter */
195 5
            $firstParameter = $constructor->getParameters()[0];
196 5
            $parameterClass = $firstParameter->getClass();
197 5
            if ($parameterClass !== null
198 5
                && (($parameterClass->isInterface() && $parameterClass->getName() === ContainerInterface::class)
199 5
                    || $parameterClass->implementsInterface(ContainerInterface::class))
200
            ) {
201 5
                $args[] = $this->getContainer();
202
            }
203
        }
204
205 5
        return $relectionClass->newInstanceArgs($args);
206
    }
207
208
    /**
209
     * @return Reader
210
     * @throws \Doctrine\Common\Annotations\AnnotationException
211
     * @throws \InvalidArgumentException
212
     */
213 5
    protected function createAnnotationReader(): Reader
214
    {
215 5
        $settings = $this->getContainer()->get('settings');
216 5
        if (isset($settings['routerCacheDir'])) {
217 1
            return new CachedReader(
218 1
                new AnnotationReader(),
219 1
                new FilesystemCache($settings['routerCacheDir']),
220 1
                false
221
            );
222
        }
223
224 5
        return new AnnotationReader();
225
    }
226
227
    /**
228
     * @param array          $methodAnnotations
229
     * @param RouteInterface $route
230
     *
231
     * @return void
232
     */
233 5
    protected function handleActionAnnotations(array $methodAnnotations, RouteInterface $route): void
0 ignored issues
show
Unused Code introduced by
The parameter $methodAnnotations 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...
Unused Code introduced by
The parameter $route 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...
234
    {
235 5
    }
236
}
237