Completed
Push — master ( 8dd423...4c2657 )
by Alejandro
06:28
created

Router::map()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 5
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
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
            "notFound" => function() {
92
                echo "Page not found";
93
            }
94
        ];
95
    }
96
97
    /**
98
     * Creates a new route using GET method
99
     * 
100
     * This method need at least 2 arguments: route pattern and callable.
101
     * You can create midleware callables which will be executed before the route
102
     * callable. 
103
     * @return \Tight\Router Fluent method
104
     */
105
    public function get() {
106
107
        return $this->url(Route::METHOD_GET, func_get_args());
108
    }
109
110
    /**
111
     * Creates a new route using POST method
112
     * 
113
     * This method need at least 2 arguments: route pattern and callable.
114
     * You can create midleware callables which will be executed before the route
115
     * @return type
116
     */
117
    public function post() {
118
        return $this->url(Route::METHOD_POST, func_get_args());
119
    }
120
121
    /**
122
     * Creates a new route using UPDATE method
123
     * 
124
     * This method need at least 2 arguments: route pattern and callable.
125
     * You can create midleware callables which will be executed before the route
126
     * callable. 
127
     * @return \Tight\Router Fluent method
128
     */
129
    public function update() {
130
        return $this->url(Route::METHOD_DELETE, func_get_args());
131
    }
132
133
    /**
134
     * Creates a new route using DELETE method
135
     * 
136
     * This method need at least 2 arguments: route pattern and callable.
137
     * You can create midleware callables which will be executed before the route
138
     * callable. 
139
     * @return \Tight\Router Fluent method
140
     */
141
    public function delete() {
142
        return $this->url(Route::METHOD_UPDATE, func_get_args());
143
    }
144
145
    /**
146
     * Creates a new route for the given methods
147
     * 
148
     * This method need at least 3 arguments: method or array of methods, route pattern and callable.
149
     * You can create midleware callables which will be executed before the route
150
     * callable. 
151
     * @return \Tight\Router Fluent method
152
     */
153
    public function map() {
154
        $args = func_get_args();
155
        $methods = array_shift($args);
156
        return $this->url($methods, $args);
157
    }
158
159
    /**
160
     * Creates a new route
161
     * @param array|string $methods Allowed methods for the route
162
     * @param array $args Array of a minimum size of 2 indexes: pattern and callable
163
     * @return \Tight\Router Fluent method
164
     */
165
    private function url($methods, $args) {
166
        if (is_string($methods)) {
167
            $methods = [$methods];
168
        }
169
        $pattern = array_shift($args); // 1st index-> Pattern
170
        $callable = array_pop($args); // Last index-> callable
171
        $route = new Route(Utils::removeDouble($this->basePath . $pattern, "/"), $callable);
172
        $route->setHttpMethods($methods);
173
        if (count($args) > 0) {
174
            // Adds the middleware
175
            $route->setMiddleware($args);
176
        }
177
        $this->routes[] = $route;
178
        return $this;
179
    }
180
181
    /**
182
     * Run the router.
183
     * 
184
     * This method checks the current uri and searches any matched pattern created
185
     * as a route.
186
     * 
187
     * This method must be called the last
188
     */
189
    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...
190
        $uri = $_SERVER['REQUEST_URI'];
191
        $method = $_SERVER['REQUEST_METHOD'];
192
        $found = null;
193
        $index = 0;
194
        while ($index < count($this->routes) && null === $found) {
195
            if ($this->routes[$index]->match($uri) and in_array(strtolower($method), $this->routes[$index]->getHttpMethods())) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
Using logical operators such as and instead of && is generally not recommended.

PHP has two types of connecting operators (logical operators, and boolean operators):

  Logical Operators Boolean Operator
AND - meaning and &&
OR - meaning or ||

The difference between these is the order in which they are executed. In most cases, you would want to use a boolean operator like &&, or ||.

Let’s take a look at a few examples:

// Logical operators have lower precedence:
$f = false or true;

// is executed like this:
($f = false) or true;


// Boolean operators have higher precedence:
$f = false || true;

// is executed like this:
$f = (false || true);

Logical Operators are used for Control-Flow

One case where you explicitly want to use logical operators is for control-flow such as this:

$x === 5
    or die('$x must be 5.');

// Instead of
if ($x !== 5) {
    die('$x must be 5.');
}

Since die introduces problems of its own, f.e. it makes our code hardly testable, and prevents any kind of more sophisticated error handling; you probably do not want to use this in real-world code. Unfortunately, logical operators cannot be combined with throw at this point:

// The following is currently a parse error.
$x === 5
    or throw new RuntimeException('$x must be 5.');

These limitations lead to logical operators rarely being of use in current PHP code.

Loading history...
196
                $found = $this->routes[$index];
197
            }
198
            $index++;
199
        }
200
        if (null !== $found && in_array(strtolower($method), $found->getHttpMethods())) {
201
            $found->dispatch();
202
        } else {
203
            call_user_func($this->errorHandler["notFound"]);
204
        }
205
    }
206
207
    /**
208
     * Method called when route cant be found
209
     */
210
    public function notFound($callback) {
211
        $this->errorHandler['notFound'] = $callback;
212
    }
213
214
}
215