Router::__construct()   B
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 1
Metric Value
c 4
b 1
f 1
dl 0
loc 20
rs 8.8571
cc 5
eloc 12
nc 3
nop 1
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
 *      return "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) {
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::removeDouble("/".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 \Tight\Router Fluent method
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() {
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
     * @param \Closure $callback Callback function for not found handler
202
     */
203
    public function notFound($callback) {
204
        $this->errorHandler['notFound'] = $callback;
205
    }
206
207
    private function dispatchNotFound() {
208
        return call_user_func($this->errorHandler["notFound"]);
209
    }
210
211
    public function dispatch($pattern, $method) {
212
        $output = null;
213
        $found = null;
214
        $index = 0;
215
        $size = count($this->routes);
216
        while ($index < $size && null === $found) {
217
            if ($this->routes[$index]->match($pattern) && in_array(strtolower($method), $this->routes[$index]->getHttpMethods())) {
218
                $found = $this->routes[$index];
219
            }
220
            $index++;
221
        }
222
        if (null !== $found && in_array(strtolower($method), $found->getHttpMethods())) {
223
            $output = $found->dispatch();
224
        } else {
225
            $output = $this->dispatchNotFound();
226
        }
227
        return $output;
228
    }
229
230
    /**
231
     * Gets all routes
232
     * @return array Array of Tight\Route
233
     */
234
    public function getRoutes() {
235
        return $this->routes;
236
    }
237
238
    /**
239
     * Gets the request URN relative to Tight\Router::basePath
240
     * 
241
     * <b>NOTE</b>
242
     * <ul>
243
     * <li><b>URL: </b> http://example.com/</li>
244
     * <li><b>URN: </b> /path/to/file.html</li>
245
     * <li><b>URI: </b> URL + URN
246
     * </ul>
247
     * @return string Request URN
248
     */
249
    public function getRequestUrn() {
250
        $output = Utils::addTrailingSlash("/" . Utils::removeSubstring($_SERVER['REQUEST_URI'], $this->basePath));
251
        return $output;
252
    }
253
254
    /**
255
     * Method used for autoload classes
256
     * @param string $className Class name
257
     * @throws \Tight\Exception\FileNotFoundException If directories or files cant be found
258
     */
259
    private function registerClasses($className) {
260
        $config = \Tight\Tight::getInstance()->getConfig();
261
        $directoryNotFound = false;
262
        $fileNotFound = false;
263
        $controllerDir = $config->mvc["controller_dir"];
264
        $modelDir = $config->mvc["model_dir"];
265
        $viewDir = $config->mvc["view_dir"];
266
        if (is_dir($controllerDir) && is_dir($modelDir) && is_dir($viewDir)) {
267
            if (strpos(strtolower($className), "controller") !== FALSE) {
268
                if (is_file($controllerDir . $className . ".php")) {
269
                    require_once $controllerDir . $className . ".php";
270
                } else {
271
                    $fileNotFound = $controllerDir . $className . ".php";
272
                }
273
            } elseif (strpos(strtolower($className), "model") !== FALSE) {
274
                if (is_file($modelDir . $className . ".php")) {
275
                    require_once $modelDir . $className . ".php";
276
                } else {
277
                    $fileNotFound = $modelDir . $className . ".php";
278
                }
279
            } elseif (strpos(strtolower($className), "view") !== FALSE) {
280
                if (is_file($viewDir . $className . ".php")) {
281
                    require_once $viewDir . $className . ".php";
282
                } else {
283
                    $fileNotFound = $viewDir . $className . ".php";
284
                }
285
            }
286
        } else {
287
            $directoryNotFound = true;
288
        }
289
        if ($directoryNotFound) {
290
            $err = !is_dir($controllerDir) ? "Controller directory not found" : (!is_dir($modelDir) ? "Model directory not found" : "View directory not found");
291
            throw new \Tight\Exception\RouterException($err);
292
        } else if ($fileNotFound !== false) {
293
            $err = "File <strong>" . $fileNotFound . "</strong> not found";
294
            throw new \Tight\Exception\NotFoundException($err);
295
        }
296
    }
297
298
    public function runMvc() {
299
        try {
300
            $requestUrn = $this->getRequestUrn();
301
            spl_autoload_register(array($this, "registerClasses"), 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\RouterException("Method <strong>" . $method . "</strong> not defined for <strong>" . $contName . "</strong> class");
330
                }
331
            }
332
            $controller->render();
333
        } catch (\Tight\Exception\NotFoundException $ex) {
334
            echo $this->dispatchNotFound();
335
        } catch (\SmartyException $ex) {
336
            echo $this->dispatchNotFound();
337
        } catch (\Exception $ex) {
338
            \Tight\Tight::printException($ex);
339
        }
340
    }
341
342
}
343