Completed
Push — master ( 611d95...bacf6e )
by Alejandro
08:39
created

Router::dispatch()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
c 1
b 1
f 0
dl 0
loc 16
rs 8.2222
cc 7
eloc 12
nc 6
nop 2
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
 * Route class for creating route http 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
                echo "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() {
0 ignored issues
show
Coding Style introduced by
This method's name is shorter than the configured minimum length of 5 characters.

Even though PHP does not care about the name of your methods, it is generally a good practice to choose method names which can be easily understood by other human readers.

Loading history...
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() {
0 ignored issues
show
Coding Style introduced by
This method's name is shorter than the configured minimum length of 5 characters.

Even though PHP does not care about the name of your methods, it is generally a good practice to choose method names which can be easily understood by other human readers.

Loading history...
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() {
0 ignored issues
show
Coding Style introduced by
This method's name is shorter than the configured minimum length of 5 characters.

Even though PHP does not care about the name of your methods, it is generally a good practice to choose method names which can be easily understood by other human readers.

Loading history...
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) {
0 ignored issues
show
Coding Style introduced by
This method's name is shorter than the configured minimum length of 5 characters.

Even though PHP does not care about the name of your methods, it is generally a good practice to choose method names which can be easily understood by other human readers.

Loading history...
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...
Coding Style introduced by
This method's name is shorter than the configured minimum length of 5 characters.

Even though PHP does not care about the name of your methods, it is generally a good practice to choose method names which can be easily understood by other human readers.

Loading history...
194
        $pattern = $_SERVER['REQUEST_URI'];
195
        $method = $_SERVER['REQUEST_METHOD'];
196
        $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
    public function dispatch($pattern, $method) {
207
        $found = null;
208
        $index = 0;
209
        $size = count($this->routes);
210
        while ($index < $size && null === $found) {
211
            if ($this->routes[$index]->match($pattern) && in_array(strtolower($method), $this->routes[$index]->getHttpMethods())) {
212
                $found = $this->routes[$index];
213
            }
214
            $index++;
215
        }
216
        if (null !== $found && in_array(strtolower($method), $found->getHttpMethods())) {
217
            $found->dispatch();
218
        } else {
219
            call_user_func($this->errorHandler["notFound"]);
220
        }
221
    }
222
223
    public function getRoutes() {
224
        return $this->routes;
225
    }
226
227
}
228