Router   F
last analyzed

Complexity

Total Complexity 62

Size/Duplication

Total Lines 334
Duplicated Lines 0 %

Test Coverage

Coverage 76.43%

Importance

Changes 24
Bugs 0 Features 0
Metric Value
eloc 137
c 24
b 0
f 0
dl 0
loc 334
ccs 107
cts 140
cp 0.7643
rs 3.44
wmc 62

15 Methods

Rating   Name   Duplication   Size   Complexity  
A init() 0 9 3
A httpNotFound() 0 3 1
A __construct() 0 5 1
B executeCachedRoute() 0 26 7
A exists() 0 3 3
B searchAction() 0 30 9
A debugLoad() 0 9 2
A hydrateRouting() 0 15 6
A checkPreActions() 0 8 3
A generateRouting() 0 16 4
A run() 0 12 4
A hasToRunPreChecks() 0 3 1
B getRoute() 0 19 8
A execute() 0 18 4
A checkRequirements() 0 15 6

How to fix   Complexity   

Complex Class

Complex classes like Router often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Router, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PSFS\base;
4
5
use Exception;
6
use InvalidArgumentException;
7
use PSFS\base\config\Config;
8
use PSFS\base\exception\AccessDeniedException;
9
use PSFS\base\exception\AdminCredentialsException;
10
use PSFS\base\exception\ConfigException;
11
use PSFS\base\exception\GeneratorException;
12
use PSFS\base\exception\RouterException;
13
use PSFS\base\types\Controller;
14
use PSFS\base\types\helpers\Inspector;
15
use PSFS\base\types\helpers\ResponseHelper;
16
use PSFS\base\types\helpers\RouterHelper;
17
use PSFS\base\types\helpers\SecurityHelper;
18
use PSFS\base\types\interfaces\PreConditionedRunInterface;
19
use PSFS\base\types\traits\RouteCheckTrait;
20
use PSFS\base\types\traits\Router\ModulesTrait;
21
use PSFS\base\types\traits\SingletonTrait;
22
use PSFS\controller\base\Admin;
23
use ReflectionException;
24
25
/**
26
 * Class Router
27
 * @package PSFS
28
 */
29
class Router
30
{
31
    use SingletonTrait;
32
    use ModulesTrait;
33
    use RouteCheckTrait;
34
35
    const PSFS_BASE_NAMESPACE = 'PSFS';
36
    /**
37
     * @var Cache $cache
38
     */
39
    private $cache;
40
    /**
41
     * @var int
42
     */
43
    protected $cacheType = Cache::JSON;
44
45
    /**
46
     * Router constructor.
47
     * @throws GeneratorException
48
     * @throws ConfigException
49
     * @throws InvalidArgumentException
50
     * @throws ReflectionException
51
     */
52 4
    public function __construct()
53
    {
54 4
        $this->cache = Cache::getInstance();
55 4
        $this->initializeFinder();
56 4
        $this->init();
57
    }
58
59
    /**
60
     * @throws GeneratorException
61
     * @throws ConfigException
62
     * @throws InvalidArgumentException
63
     * @throws ReflectionException
64
     */
65 5
    public function init()
66
    {
67 5
        list($this->routing, $this->slugs) = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . 'urls.json', $this->cacheType, TRUE);
68 5
        $this->domains = $this->cache->getDataFromFile(CONFIG_DIR . DIRECTORY_SEPARATOR . 'domains.json', $this->cacheType, TRUE);
69 5
        if (empty($this->routing) || Config::getParam('debug', true)) {
70 5
            $this->debugLoad();
71
        }
72 5
        $this->checkExternalModules(false);
73 5
        $this->setLoaded();
74
    }
75
76
    /**
77
     * @throws GeneratorException
78
     * @throws ConfigException
79
     * @throws InvalidArgumentException
80
     * @throws ReflectionException
81
     */
82 5
    private function debugLoad()
83
    {
84 5
        if (!Config::getParam('skip.route_generation', false)) {
85 5
            Logger::log('Begin routes load');
86 5
            $this->hydrateRouting();
87 5
            $this->simpatize();
88 5
            Logger::log('End routes load');
89
        } else {
90
            Logger::log('Routes generation skipped');
91
        }
92
    }
93
94
    /**
95
     * @param string|null $route
96
     *
97
     * @return string HTML
98
     * @throws Exception
99
     */
100 5
    public function execute($route)
101
    {
102 5
        Inspector::stats('[Router] Executing the request', Inspector::SCOPE_DEBUG);
103
        try {
104
            //Search action and execute
105 5
            return $this->searchAction($route);
106 4
        } catch (AccessDeniedException $e) {
107 2
            Logger::log(t('Solicitamos credenciales de acceso a zona restringida'), LOG_WARNING, ['file' => $e->getFile() . '[' . $e->getLine() . ']']);
108 2
            return Admin::staticAdminLogon();
109 2
        } catch (RouterException $r) {
110 2
            Logger::log($r->getMessage(), LOG_WARNING);
111 2
            $code = $r->getCode();
112
        } catch (Exception $e) {
113
            Logger::log($e->getMessage(), LOG_ERR);
114
            throw $e;
115
        }
116
117 2
        throw new RouterException(t('Página no encontrada'), $code);
118
    }
119
120
    /**
121
     * @param string $route
122
     * @return mixed
123
     * @throws AccessDeniedException
124
     * @throws AdminCredentialsException
125
     * @throws RouterException
126
     * @throws Exception
127
     */
128 5
    protected function searchAction($route)
129
    {
130 5
        Inspector::stats('[Router] Searching action to execute: ' . $route, Inspector::SCOPE_DEBUG);
131
        //Revisamos si tenemos la ruta registrada
132 5
        $parts = parse_url($route);
133 5
        $path = array_key_exists('path', $parts) ? $parts['path'] : $route;
134 5
        $httpRequest = Request::getInstance()->getMethod();
135 5
        foreach ($this->routing as $pattern => $action) {
136 5
            list($httpMethod, $routePattern) = RouterHelper::extractHttpRoute($pattern);
137 5
            $matched = RouterHelper::matchRoutePattern($routePattern, $path);
138 5
            if ($matched && ($httpMethod === 'ALL' || $httpRequest === $httpMethod) && RouterHelper::compareSlashes($routePattern, $path)) {
139 3
                self::setCheckedRoute($action);
140
                // Checks restricted access
141 3
                SecurityHelper::checkRestrictedAccess($route);
142 1
                $get = RouterHelper::extractComponents($route, $routePattern);
143
                /** @var $class Controller */
144 1
                $class = RouterHelper::getClassToCall($action);
145
                try {
146 1
                    if ($this->checkRequirements($action, $get)) {
147 1
                        return $this->executeCachedRoute($route, $action, $class, $get);
0 ignored issues
show
Bug introduced by
$class of type PSFS\base\types\Controller is incompatible with the type string expected by parameter $class of PSFS\base\Router::executeCachedRoute(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

147
                        return $this->executeCachedRoute($route, $action, /** @scrutinizer ignore-type */ $class, $get);
Loading history...
148
                    }
149
150
                    throw new RouterException(t('Preconditions failed'), 412);
151
                } catch (Exception $e) {
152
                    Logger::log($e->getMessage(), LOG_ERR);
153
                    throw $e;
154
                }
155
            }
156
        }
157 2
        throw new RouterException(t('Ruta no encontrada'));
158
    }
159
160
    /**
161
     * @param array $action
162
     * @param array $params
163
     * @return bool
164
     */
165 1
    private function checkRequirements(array $action, $params = [])
166
    {
167 1
        Inspector::stats('[Router] Checking request requirements', Inspector::SCOPE_DEBUG);
168 1
        if (!empty($params) && !empty($action['requirements'])) {
169
            $checked = 0;
170
            foreach (array_keys($params) as $key) {
171
                if (in_array($key, $action['requirements'], true) && strlen($params[$key])) {
172
                    $checked++;
173
                }
174
            }
175
            $valid = count($action['requirements']) === $checked;
176
        } else {
177 1
            $valid = true;
178
        }
179 1
        return $valid;
180
    }
181
182
    /**
183
     * @throws ConfigException
184
     * @throws InvalidArgumentException
185
     * @throws ReflectionException
186
     * @throws GeneratorException
187
     */
188 5
    private function generateRouting()
189
    {
190 5
        $base = SOURCE_DIR;
191 5
        $modulesPath = realpath(CORE_DIR);
192 5
        $this->routing = $this->inspectDir($base, 'PSFS', array());
193 5
        $this->checkExternalModules();
194 5
        if (file_exists($modulesPath)) {
195 1
            $modules = $this->finder->directories()->in($modulesPath)->depth(0);
196 1
            if ($modules->hasResults()) {
197 1
                foreach ($modules->getIterator() as $modulePath) {
198 1
                    $module = $modulePath->getBasename();
199 1
                    $this->routing = $this->inspectDir($modulesPath . DIRECTORY_SEPARATOR . $module, $module, $this->routing);
200
                }
201
            }
202
        }
203 5
        $this->cache->storeData(CONFIG_DIR . DIRECTORY_SEPARATOR . 'domains.json', $this->domains, Cache::JSON, TRUE);
204
    }
205
206
    /**
207
     * @throws GeneratorException
208
     * @throws ConfigException
209
     * @throws InvalidArgumentException
210
     * @throws ReflectionException
211
     */
212 5
    public function hydrateRouting()
213
    {
214 5
        $this->generateRouting();
215 5
        $home = Config::getParam('home.action', 'admin');
216 5
        if (NULL !== $home || $home !== '') {
217 5
            $homeParams = NULL;
218 5
            foreach ($this->routing as $pattern => $params) {
219 5
                list($method, $route) = RouterHelper::extractHttpRoute($pattern);
220 5
                if (preg_match('/' . preg_quote($route, '/') . '$/i', '/' . $home)) {
221 5
                    $homeParams = $params;
222
                }
223 5
                unset($method);
224
            }
225 5
            if (NULL !== $homeParams) {
226 5
                $this->routing['/'] = $homeParams;
227
            }
228
        }
229
    }
230
231
    /**
232
     * @param string $namespace
233
     * @return bool
234
     */
235 7
    public static function exists($namespace)
236
    {
237 7
        return (class_exists($namespace) || interface_exists($namespace) || trait_exists($namespace));
238
    }
239
240
    /**
241
     * @param string $slug
242
     * @param boolean $absolute
243
     * @param array $params
244
     *
245
     * @return string|null
246
     * @throws RouterException
247
     */
248 3
    public function getRoute($slug = '', $absolute = false, array $params = [])
249
    {
250 3
        $baseUrl = $absolute ? Request::getInstance()->getRootUrl() : '';
251 3
        if ('' === $slug) {
252 1
            return $baseUrl . '/';
253
        }
254 3
        if (!is_array($this->slugs) || !array_key_exists($slug, $this->slugs)) {
0 ignored issues
show
introduced by
The condition is_array($this->slugs) is always true.
Loading history...
255 1
            throw new RouterException(t('No existe la ruta especificada'));
256
        }
257 3
        $url = $baseUrl . $this->slugs[$slug];
258 3
        if (!empty($params)) {
259 1
            foreach ($params as $key => $value) {
260 1
                $url = str_replace('{' . $key . '}', $value, $url);
261
            }
262 3
        } elseif (!empty($this->routing[$this->slugs[$slug]]['default'])) {
263 3
            $url = $baseUrl . $this->routing[$this->slugs[$slug]]['default'];
264
        }
265
266 3
        return preg_replace('/(GET|POST|PUT|DELETE|ALL|HEAD|PATCH)\#\|\#/', '', $url);
267
    }
268
269
    /**
270
     * @param string $class
271
     * @param string $method
272
     */
273 1
    private function checkPreActions($class, $method)
274
    {
275 1
        if ($this->hasToRunPreChecks($class)) {
276
            self::run($class, '__check', true);
277
        }
278 1
        $preAction = 'pre' . ucfirst($method);
279 1
        if (method_exists($class, $preAction)) {
280
            self::run($class, $preAction);
281
        }
282
    }
283
284
    /**
285
     * @param $class
286
     * @param string $method
287
     * @param boolean $throwExceptions
288
     * @return void
289
     * @throws Exception
290
     */
291
    public static function run($class, $method, $throwExceptions = false): void
292
    {
293
        Inspector::stats("[Router] Pre action invoked " . get_class($class) . "::{$method}", Inspector::SCOPE_DEBUG);
294
        try {
295
            if (false === call_user_func_array([$class, $method], [])) {
296
                Logger::log(t("[Router] action " . get_class($class) . "::{$method} failed"), LOG_ERR, [error_get_last()]);
297
                error_clear_last();
298
            }
299
        } catch (Exception $e) {
300
            Logger::log($e->getMessage(), LOG_ERR, [$class, $method]);
301
            if ($throwExceptions) {
302
                throw $e;
303
            }
304
        }
305
    }
306
307
    /**
308
     * Check if class to run route implements the PreConditionedRunInterface
309
     * @param string $class
310
     * @return bool
311
     */
312 1
    private function hasToRunPreChecks($class)
313
    {
314 1
        return in_array(PreConditionedRunInterface::class, class_implements($class));
315
    }
316
317
    /**
318
     * @param string $route
319
     * @param array $action
320
     * @param string $class
321
     * @param array $params
322
     * @return mixed
323
     * @throws GeneratorException
324
     * @throws ConfigException
325
     */
326 1
    protected function executeCachedRoute($route, $action, $class, $params = NULL)
327
    {
328 1
        Inspector::stats('[Router] Executing route ' . $route, Inspector::SCOPE_DEBUG);
329 1
        $action['params'] = array_merge($action['params'], $params, Request::getInstance()->getQueryParams());
0 ignored issues
show
Bug introduced by
It seems like $params can also be of type null; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

329
        $action['params'] = array_merge($action['params'], /** @scrutinizer ignore-type */ $params, Request::getInstance()->getQueryParams());
Loading history...
330 1
        Security::getInstance()->setSessionKey(Cache::CACHE_SESSION_VAR, $action);
331 1
        $cache = Cache::needCache();
332 1
        $execute = TRUE;
333 1
        $return = null;
334 1
        if (FALSE !== $cache && $action['http'] === 'GET' && Config::getParam('debug') === FALSE) {
335
            list($path, $cacheDataName) = $this->cache->getRequestCacheHash();
336
            $cachedData = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName, $cache);
0 ignored issues
show
Bug introduced by
It seems like $cache can also be of type true; however, parameter $expires of PSFS\base\Cache::readFromCache() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

336
            $cachedData = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName, /** @scrutinizer ignore-type */ $cache);
Loading history...
337
            if (NULL !== $cachedData) {
338
                $headers = $this->cache->readFromCache('json' . DIRECTORY_SEPARATOR . $path . $cacheDataName . '.headers', $cache, null, Cache::JSON);
339
                Template::getInstance()->renderCache($cachedData, $headers);
340
                $execute = FALSE;
341
            }
342
        }
343 1
        if ($execute) {
344 1
            Inspector::stats('[Router] Start executing action ' . $route, Inspector::SCOPE_DEBUG);
345 1
            $this->checkPreActions($class, $action['method']);
346 1
            $return = call_user_func_array([$class, $action['method']], $params);
0 ignored issues
show
Bug introduced by
It seems like $params can also be of type null; however, parameter $args of call_user_func_array() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

346
            $return = call_user_func_array([$class, $action['method']], /** @scrutinizer ignore-type */ $params);
Loading history...
347 1
            if (false === $return) {
348
                Logger::log(t('An error occurred trying to execute the action'), LOG_ERR, [error_get_last()]);
349
            }
350
        }
351 1
        return $return;
352
    }
353
354
    /**
355
     * @param Exception|null $exception
356
     * @param bool $isJson
357
     * @return string
358
     * @throws GeneratorException
359
     */
360
    public function httpNotFound(\Exception $exception = null, $isJson = false)
361
    {
362
        return ResponseHelper::httpNotFound($exception, $isJson);
363
    }
364
}
365