Passed
Push — master ( 7deaea...ceaa4a )
by Robson
01:50
created

Dispatch::route()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 3
b 0
f 0
nc 3
nop 2
dl 0
loc 8
rs 10
1
<?php
2
3
namespace CoffeeCode\Router;
4
5
/**
6
 * Class CoffeeCode Dispatch
7
 *
8
 * @author Robson V. Leite <https://github.com/robsonvleite>
9
 * @package CoffeeCode\Router
10
 */
11
abstract class Dispatch
12
{
13
    /** @var bool|string */
14
    protected $projectUrl;
15
16
    /** @var string */
17
    protected $patch;
18
19
    /** @var string */
20
    protected $separator;
21
22
    /** @var string */
23
    protected $httpMethod;
24
25
    /** @var array */
26
    protected $routes;
27
28
    /** @var null|string */
29
    protected $group;
30
31
    /** @var null|array */
32
    protected $route;
33
34
    /** @var null|string */
35
    protected $namespace;
36
37
    /** @var null|array */
38
    protected $data;
39
40
    /** @var int */
41
    protected $error;
42
43
    /** @const int Bad Request */
44
    public const BAD_REQUEST = 400;
45
46
    /** @const int Not Found */
47
    public const NOT_FOUND = 404;
48
49
    /** @const int Method Not Allowed */
50
    public const METHOD_NOT_ALLOWED = 405;
51
52
    /** @const int Not Implemented */
53
    public const NOT_IMPLEMENTED = 501;
54
55
    /**
56
     * Dispatch constructor.
57
     *
58
     * @param string $projectUrl
59
     * @param null|string $separator
60
     */
61
    public function __construct(string $projectUrl, ?string $separator = ":")
62
    {
63
        $this->projectUrl = (substr($projectUrl, "-1") == "/" ? substr($projectUrl, 0, -1) : $projectUrl);
64
        $this->patch = (filter_input(INPUT_GET, "route", FILTER_DEFAULT) ?? "/");
65
        $this->separator = ($separator ?? ":");
66
        $this->httpMethod = $_SERVER['REQUEST_METHOD'];
67
    }
68
69
    /**
70
     * @return array
71
     */
72
    public function __debugInfo()
73
    {
74
        return $this->routes;
75
    }
76
77
    /**
78
     * @param null|string $group
79
     * @return Dispatch
80
     */
81
    public function group(?string $group): Dispatch
82
    {
83
        $this->group = ($group ? str_replace("/", "", $group) : null);
84
        return $this;
85
    }
86
87
    /**
88
     * @param null|string $namespace
89
     * @return Dispatch
90
     */
91
    public function namespace(?string $namespace): Dispatch
92
    {
93
        $this->namespace = ($namespace ? ucwords($namespace) : null);
94
        return $this;
95
    }
96
97
    /**
98
     * @return null|array
99
     */
100
    public function data(): ?array
101
    {
102
        return $this->data;
103
    }
104
105
    /**
106
     * @return null|int
107
     */
108
    public function error(): ?int
109
    {
110
        return $this->error;
111
    }
112
113
    /**
114
     * @param string $name
115
     * @param $data
116
     * @return string|null
117
     */
118
    public function route(string $name, array $data = null): ?string
119
    {
120
        foreach ($this->routes as $http_verb) {
121
            foreach ($http_verb as $route_item) {
122
                $this->treat($name, $route_item, $data);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type null; however, parameter $data of CoffeeCode\Router\Dispatch::treat() 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

122
                $this->treat($name, $route_item, /** @scrutinizer ignore-type */ $data);
Loading history...
123
            }
124
        }
125
        return null;
126
    }
127
128
    /**
129
     * @param string $name
130
     * @param array $route_item
131
     * @param array $data
132
     * @return string|null
133
     */
134
    private function treat(string $name, array $route_item, array $data): ?string
135
    {
136
        if (!empty($route_item["name"]) && $route_item["name"] == $name) {
137
            $route = $route_item["route"];
138
            if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
139
                $arguments = [];
140
                foreach ($data as $key => $value) {
141
                    if (!strstr($route, "{{$key}}")) {
142
                        $params[$key] = $value;
143
                    }
144
                    $arguments["{{$key}}"] = $value;
145
                }
146
                $params = (!empty($params) ? "?" . http_build_query($params) : null);
147
                $route = str_replace(array_keys($arguments), array_values($arguments), $route) . "{$params}";
148
            }
149
            return "{$this->projectUrl}{$route}";
150
        }
151
152
        return null;
153
    }
154
155
    /**
156
     * @param string $route
157
     * @param array $data
158
     */
159
    public function redirect(string $route, array $data = null): void
160
    {
161
        if ($name = $this->route($route, $data)) {
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $name is correct as $this->route($route, $data) targeting CoffeeCode\Router\Dispatch::route() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
162
            header("Location: {$name}");
163
            exit;
164
        }
165
166
        if (filter_var($route, FILTER_VALIDATE_URL)) {
167
            header("Location: {$route}");
168
            exit;
169
        }
170
171
        $route = (substr($route, 0, 1) == "/" ? $route : "/{$route}");
172
        header("Location: {$this->projectUrl}{$route}");
173
        exit;
174
    }
175
176
    /**
177
     * @return bool
178
     */
179
    public function dispatch(): bool
180
    {
181
        if (empty($this->routes) || empty($this->routes[$this->httpMethod])) {
182
            $this->error = self::NOT_IMPLEMENTED;
183
            return false;
184
        }
185
186
        $this->route = null;
187
        foreach ($this->routes[$this->httpMethod] as $key => $route) {
188
            if (preg_match("~^" . $key . "$~", $this->patch, $found)) {
189
                $this->route = $route;
190
            }
191
        }
192
193
        return $this->execute();
194
    }
195
196
    /**
197
     * @return bool
198
     */
199
    private function execute()
200
    {
201
        if ($this->route) {
202
            if (is_callable($this->route['handler'])) {
203
                call_user_func($this->route['handler'], ($this->route['data'] ?? []));
204
                return true;
205
            }
206
207
            $controller = $this->route['handler'];
208
            $method = $this->route['action'];
209
210
            if (class_exists($controller)) {
211
                $newController = new $controller($this);
212
                if (method_exists($controller, $method)) {
213
                    $newController->$method(($this->route['data'] ?? []));
214
                    return true;
215
                }
216
217
                $this->error = self::METHOD_NOT_ALLOWED;
218
                return false;
219
            }
220
221
            $this->error = self::BAD_REQUEST;
222
            return false;
223
        }
224
225
        $this->error = self::NOT_FOUND;
226
        return false;
227
    }
228
229
    /**
230
     * @param string $method
231
     * @param string $route
232
     * @param string|callable $handler
233
     * @param null|string
234
     * @return Dispatch
235
     */
236
    protected function addRoute(string $method, string $route, $handler, string $name = null): Dispatch
237
    {
238
        if ($route == "/") {
239
            $this->addRoute($method, "", $handler, $name);
240
        }
241
242
        preg_match_all("~\{\s* ([a-zA-Z_][a-zA-Z0-9_-]*) \}~x", $route, $keys, PREG_SET_ORDER);
243
        $routeDiff = array_values(array_diff(explode("/", $this->patch), explode("/", $route)));
244
245
        $this->formSpoofing();
246
        $offset = ($this->group ? 1 : 0);
247
        foreach ($keys as $key) {
248
            $this->data[$key[1]] = ($routeDiff[$offset++] ?? null);
249
        }
250
251
        $route = (!$this->group ? $route : "/{$this->group}{$route}");
252
        $data = $this->data;
253
        $namespace = $this->namespace;
254
        $router = function () use ($method, $handler, $data, $route, $name, $namespace) {
255
            return [
256
                "route" => $route,
257
                "name" => $name,
258
                "method" => $method,
259
                "handler" => $this->handler($handler, $namespace),
260
                "action" => $this->action($handler),
261
                "data" => $data
262
            ];
263
        };
264
265
        $route = preg_replace('~{([^}]*)}~', "([^/]+)", $route);
266
        $this->routes[$method][$route] = $router();
267
        return $this;
268
    }
269
270
    /**
271
     * httpMethod form spoofing
272
     */
273
    protected function formSpoofing(): void
274
    {
275
        $post = filter_input_array(INPUT_POST, FILTER_DEFAULT);
276
277
        if (!empty($post['_method']) && in_array($post['_method'], ["PUT", "PATCH", "DELETE"])) {
278
            $this->httpMethod = $post['_method'];
279
            $this->data = $post;
280
281
            unset($this->data["_method"]);
282
            return;
283
        }
284
285
        if ($this->httpMethod == "POST") {
286
            $this->data = filter_input_array(INPUT_POST, FILTER_DEFAULT);
287
288
            unset($this->data["_method"]);
289
            return;
290
        }
291
292
        if (in_array($this->httpMethod, ["PUT", "PATCH", "DELETE"]) && !empty($_SERVER['CONTENT_LENGTH'])) {
293
            parse_str(file_get_contents('php://input', false, null, 0, $_SERVER['CONTENT_LENGTH']), $putPatch);
294
            $this->data = $putPatch;
295
296
            unset($this->data["_method"]);
297
            return;
298
        }
299
300
        $this->data = [];
301
        return;
302
    }
303
304
    /**
305
     * @param $handler
306
     * @param $namespace
307
     * @return string|callable
308
     */
309
    private function handler($handler, $namespace)
310
    {
311
        return (!is_string($handler) ? $handler : "{$namespace}\\" . explode($this->separator, $handler)[0]);
312
    }
313
314
    /**
315
     * @param $handler
316
     * @return null|string
317
     */
318
    private function action($handler): ?string
319
    {
320
        return (!is_string($handler) ?: (explode($this->separator, $handler)[1] ?? null));
321
    }
322
}