Completed
Push — master ( ee49f4...f8fced )
by Alejandro
02:23
created

Router::runMvc()   D

Complexity

Conditions 10
Paths 146

Size

Total Lines 44
Code Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 1
Metric Value
c 3
b 0
f 1
dl 0
loc 44
rs 4.606
cc 10
eloc 37
nc 146
nop 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Tight;
4
5
/*
6
 * The MIT License
7
 *
8
 * Copyright 2016 Alejandro Peña Florentín ([email protected])
9
 *
10
 * Permission is hereby granted, free of charge, to any person obtaining a copy
11
 * of this software and associated documentation files (the "Software"), to deal
12
 * in the Software without restriction, including without limitation the rights
13
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
 * copies of the Software, and to permit persons to whom the Software is
15
 * furnished to do so, subject to the following conditions:
16
 *
17
 * The above copyright notice and this permission notice shall be included in
18
 * all copies or substantial portions of the Software.
19
 *
20
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
 * THE SOFTWARE.
27
 */
28
29
/**
30
 * Router class for creating http route methods defined by patterns
31
 * 
32
 * MIDDLEWARE
33
 * 
34
 *  When creating routes you can set 1 or more middleware that will be executed
35
 * before the route callable.
36
 *  You can set route middleware by adding callables <b>before</b> the last 
37
 * parameter, which is the route callable, for the following class methods:
38
 * <ul>
39
 *  <li>Tight\Router::get,</li>
40
 *  <li>Tight\Router::post, </li>
41
 *  <li>Tight\Router::update,</li>
42
 *  <li>Tight\Router::delete and</li>
43
 *  <li>Tight\Router::map.</li>
44
 * </ul>
45
 * Eg.
46
 * <pre>
47
 * $router->get("/",'userDefinedFunction',function(){
48
 *      echo "Hello World!";
49
 * });
50
 * </pre>
51
 * @author Alejandro Peña Florentín ([email protected])
52
 */
53
class Router
54
{
55
56
    /**
57
     * @var array Routes
58
     */
59
    protected $routes;
60
61
    /**
62
     * @var string Base path for rutes. If Router is not instanciated in 
63
     * document root this value must be set
64
     */
65
    private $basePath;
66
67
    /**
68
     * @var array Array of callables for error handling
69
     */
70
    private $errorHandler = [
71
        "notFound" => null, // 404
72
    ];
73
74
    /**
75
     * Creates a new instance
76
     * @param string $basePath Base path used when creating the routes.If Router
77
     *  is not instanciated in server document root this value must be set
78
     */
79
    public function __construct($basePath) {
1 ignored issue
show
Coding Style introduced by
__construct uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
80
        if (null !== $basePath && is_string($basePath) && !empty($basePath)) {
81
            $basePath = Utils::filterPath($basePath);
82
            if (Utils::inString($basePath, $_SERVER['DOCUMENT_ROOT'])) {
83
                $this->basePath = Utils::removeSubstring($basePath, $_SERVER['DOCUMENT_ROOT']);
84
            } else {
85
                $this->basePath = $basePath;
86
            }
87
        } else {
88
            $this->basePath = "/";
89
        }
90
        $this->errorHandler = [
91
            /**
92
             * @codeCoverageIgnore
93
             */
94
            "notFound" => function() {
95
                return "Page not found";
96
            }
97
        ];
98
    }
99
100
    /**
101
     * Creates a new route using GET method
102
     * 
103
     * This method need at least 2 arguments: route pattern and callable.
104
     * You can create midleware callables which will be executed before the route
105
     * callable. 
106
     * @return \Tight\Router Fluent method
107
     */
108
    public function get() {
109
110
        return $this->url(Route::METHOD_GET, func_get_args());
111
    }
112
113
    /**
114
     * Creates a new route using POST method
115
     * 
116
     * This method need at least 2 arguments: route pattern and callable.
117
     * You can create midleware callables which will be executed before the route
118
     * @return type
0 ignored issues
show
Documentation introduced by
Should the return type not be Router?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
119
     */
120
    public function post() {
121
        return $this->url(Route::METHOD_POST, func_get_args());
122
    }
123
124
    /**
125
     * Creates a new route using UPDATE method
126
     * 
127
     * This method need at least 2 arguments: route pattern and callable.
128
     * You can create midleware callables which will be executed before the route
129
     * callable. 
130
     * @return \Tight\Router Fluent method
131
     */
132
    public function update() {
133
        return $this->url(Route::METHOD_UPDATE, func_get_args());
134
    }
135
136
    /**
137
     * Creates a new route using DELETE method
138
     * 
139
     * This method need at least 2 arguments: route pattern and callable.
140
     * You can create midleware callables which will be executed before the route
141
     * callable. 
142
     * @return \Tight\Router Fluent method
143
     */
144
    public function delete() {
145
        return $this->url(Route::METHOD_DELETE, func_get_args());
146
    }
147
148
    /**
149
     * Creates a new route for the given methods
150
     * 
151
     * This method need at least 3 arguments: method or array of methods, route pattern and callable.
152
     * You can create midleware callables which will be executed before the route
153
     * callable. 
154
     * @return \Tight\Router Fluent method
155
     */
156
    public function map() {
157
        $args = func_get_args();
158
        $methods = array_shift($args);
159
        return $this->url($methods, $args);
160
    }
161
162
    /**
163
     * Creates a new route
164
     * @param array|string $methods Allowed methods for the route
165
     * @param array $args Array of a minimum size of 2 indexes: pattern and callable
166
     * @return \Tight\Router Fluent method
167
     */
168
    private function url($methods, $args) {
169
        if (is_string($methods)) {
170
            $methods = [$methods];
171
        }
172
        $pattern = array_shift($args); // 1st index-> Pattern
173
        $callable = array_pop($args); // Last index-> callable
174
        $route = new Route(Utils::removeDouble($this->basePath . $pattern, "/"), $callable);
175
        $route->setHttpMethods($methods);
176
        if (count($args) > 0) {
177
            // Adds the middleware
178
            $route->setMiddleware($args);
179
        }
180
        $this->routes[] = $route;
181
        return $this;
182
    }
183
184
    /**
185
     * Run the router.
186
     * 
187
     * This method checks the current uri and searches any matched pattern created
188
     * as a route.
189
     * 
190
     * This method must be called the last
191
     * @codeCoverageIgnore
192
     */
193
    public function run() {
1 ignored issue
show
Coding Style introduced by
run uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
194
        $pattern = $_SERVER['REQUEST_URI'];
195
        $method = $_SERVER['REQUEST_METHOD'];
196
        echo $this->dispatch($pattern, $method);
197
    }
198
199
    /**
200
     * Method called when route cant be found
201
     */
202
    public function notFound($callback) {
203
        $this->errorHandler['notFound'] = $callback;
204
    }
205
206
    private function dispatchNotFound() {
207
        return call_user_func($this->errorHandler["notFound"]);
208
    }
209
210
    public function dispatch($pattern, $method) {
211
        $output = null;
1 ignored issue
show
Unused Code introduced by
$output is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
212
        $found = null;
213
        $index = 0;
214
        $size = count($this->routes);
215
        while ($index < $size && null === $found) {
216
            if ($this->routes[$index]->match($pattern) && in_array(strtolower($method), $this->routes[$index]->getHttpMethods())) {
217
                $found = $this->routes[$index];
218
            }
219
            $index++;
220
        }
221
        if (null !== $found && in_array(strtolower($method), $found->getHttpMethods())) {
222
            $output = $found->dispatch();
223
        } else {
224
            $output = $this->dispatchNotFound();
225
        }
226
        return $output;
227
    }
228
229
    /**
230
     * Gets all routes
231
     * @return array Array of Tight\Route
232
     */
233
    public function getRoutes() {
234
        return $this->routes;
235
    }
236
237
    /**
238
     * Gets the request URN relative to Tight\Router::basePath
239
     * 
240
     * <b>NOTE</b>
241
     * <ul>
242
     * <li><b>URL: </b> http://example.com/</li>
243
     * <li><b>URN: </b> /path/to/file.html</li>
244
     * <li><b>URI: </b> URL + URN
245
     * </ul>
246
     * @return string Request URN
247
     */
248
    public function getRequestUrn() {
1 ignored issue
show
Coding Style introduced by
getRequestUrn uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
249
        $output = Utils::addTrailingSlash("/" . Utils::removeSubstring($_SERVER['REQUEST_URI'], $this->basePath));
250
        return $output;
251
    }
252
253
    /**
254
     * Method used for autoload classes
255
     * @param string $className Class name
256
     * @throws \Tight\Exception\FileNotFoundException If directories or files cant be found
257
     */
258
    private function registerClasses($className) {
259
        $config = \Tight\Tight::getInstance()->getConfig();
260
        $directoryNotFound = false;
261
        $fileNotFound = false;
262
        $controllerDir = $config->mvc["controller_dir"];
263
        $modelDir = $config->mvc["model_dir"];
264
        $viewDir = $config->mvc["view_dir"];
265
        if (is_dir($controllerDir) && is_dir($modelDir) && is_dir($viewDir)) {
266
            if (strpos(strtolower($className), "controller") !== FALSE) {
267 View Code Duplication
                if (is_file($controllerDir . $className . ".php")) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
268
                    require_once $controllerDir . $className . ".php";
269
                } else {
270
                    $fileNotFound = $controllerDir . $className . ".php";
271
                }
272
            } elseif (strpos(strtolower($className), "model") !== FALSE) {
273 View Code Duplication
                if (is_file($modelDir . $className . ".php")) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
274
                    require_once $modelDir . $className . ".php";
275
                } else {
276
                    $fileNotFound = $modelDir . $className . ".php";
277
                }
278
            } elseif (strpos(strtolower($className), "view") !== FALSE) {
279 View Code Duplication
                if (is_file($viewDir . $className . ".php")) {
1 ignored issue
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
280
                    require_once $viewDir . $className . ".php";
281
                } else {
282
                    $fileNotFound = $viewDir . $className . ".php";
283
                }
284
            }
285
        } else {
286
            $directoryNotFound = true;
287
        }
288
        if ($directoryNotFound) {
289
            $err = !is_dir($controllerDir) ? "Controller directory cant be found" : !is_dir($modelDir) ? "Model directory cant be found" : "View directory cant be found";
290
            throw new \Tight\Exception\FileNotFoundException($err);
291
        } else if ($fileNotFound) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fileNotFound of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
292
            $err = "File <strong>" . $fileNotFound . "</strong> not found";
293
            throw new \Tight\Exception\FileNotFoundException($err);
294
        }
295
    }
296
297
    public function runMvc() {
298
        try {
299
            $config = \Tight\Tight::getInstance()->getConfig();
0 ignored issues
show
Unused Code introduced by
$config is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
300
            $requestUrn = $this->getRequestUrn();
301
            spl_autoload_register(array($this, "registerClasses"), TRUE, TRUE);
302
            $name = "";
303
            $method = null;
304
            $args = [];
305
            if ("/" == $requestUrn) {
306
                $name = \Tight\Tight::getInstance()->getConfig()->mvc["indexName"];
307
            } else {
308
                $requestUrn = trim($requestUrn, "/");
309
                $explode = explode("/", $requestUrn);
310
                $name = array_shift($explode);
311
                if (count($explode) > 0) {
312
                    $method = array_shift($explode);
313
                    if (count($explode) > 0) {
314
                        $args = $explode;
315
                    }
316
                }
317
            }
318
            $name = ucwords($name);
319
            $contName = $name . "Controller";
320
            $viewName = $name . "View";
321
            $modName = $name . "Model";
322
            $model = new $modName();
323
            $view = new $viewName();
324
            $controller = new $contName($model, $view);
325
            if ("" !== $method && !empty($method)) {
326
                if (method_exists($controller, $method)) {
327
                    call_user_method_array($method, $controller, $args);
328
                } else {
329
                    throw new \Tight\Exception\FileNotFoundException("Method <strong>" . $method . "</strong> not defined for <strong>" . $contName . "</strong> class");
330
                }
331
            }
332
            $controller->render();
333
        } catch (\Tight\Exception\FileNotFoundException $ex) {
334
            $this->dispatchNotFound();
335
        } catch (\SmartyException $ex) {
336
            $this->dispatchNotFound();
337
        } catch (\Exception $ex) {
338
            \Tight\Tight::printException($ex);
339
        }
340
    }
341
342
}
343