Passed
Push — master ( 6d8dc7...78f9e1 )
by Robson
01:57
created

Dispatch::process()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 2
c 1
b 0
f 0
nc 2
nop 3
dl 0
loc 4
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 array|null $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);
123
            }
124
        }
125
        return null;
126
    }
127
128
    /**
129
     * @param string $name
130
     * @param array $route_item
131
     * @param array|null $data
132
     * @return string|null
133
     */
134
    private function treat(string $name, array $route_item, array $data = null): ?string
135
    {
136
        if (!empty($route_item["name"]) && $route_item["name"] == $name) {
137
            $route = $route_item["route"];
138
            if (!empty($data)) {
139
                $arguments = [];
140
                $params = [];
141
                foreach ($data as $key => $value) {
142
                    if (!strstr($route, "{{$key}}")) {
143
                        $params[$key] = $value;
144
                    }
145
                    $arguments["{{$key}}"] = $value;
146
                }
147
                $route = $this->process($route, $arguments, $params);
148
            }
149
            return "{$this->projectUrl}{$route}";
150
        }
151
        return null;
152
    }
153
154
    /**
155
     * @param string $route
156
     * @param array $arguments
157
     * @param array|null $params
158
     * @return string
159
     */
160
    private function process(string $route, array $arguments, array $params = null): string
161
    {
162
        $params = (!empty($params) ? "?" . http_build_query($params) : null);
163
        return str_replace(array_keys($arguments), array_values($arguments), $route) . "{$params}";
164
    }
165
166
    /**
167
     * @param string $route
168
     * @param array|null $data
169
     */
170
    public function redirect(string $route, array $data = null): void
171
    {
172
        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...
173
            header("Location: {$name}");
174
            exit;
175
        }
176
177
        if (filter_var($route, FILTER_VALIDATE_URL)) {
178
            header("Location: {$route}");
179
            exit;
180
        }
181
182
        $route = (substr($route, 0, 1) == "/" ? $route : "/{$route}");
183
        header("Location: {$this->projectUrl}{$route}");
184
        exit;
185
    }
186
187
    /**
188
     * @return bool
189
     */
190
    public function dispatch(): bool
191
    {
192
        if (empty($this->routes) || empty($this->routes[$this->httpMethod])) {
193
            $this->error = self::NOT_IMPLEMENTED;
194
            return false;
195
        }
196
197
        $this->route = null;
198
        foreach ($this->routes[$this->httpMethod] as $key => $route) {
199
            if (preg_match("~^" . $key . "$~", $this->patch, $found)) {
200
                $this->route = $route;
201
            }
202
        }
203
204
        return $this->execute();
205
    }
206
207
    /**
208
     * @return bool
209
     */
210
    private function execute()
211
    {
212
        if ($this->route) {
213
            if (is_callable($this->route['handler'])) {
214
                call_user_func($this->route['handler'], ($this->route['data'] ?? []));
215
                return true;
216
            }
217
218
            $controller = $this->route['handler'];
219
            $method = $this->route['action'];
220
221
            if (class_exists($controller)) {
222
                $newController = new $controller($this);
223
                if (method_exists($controller, $method)) {
224
                    $newController->$method(($this->route['data'] ?? []));
225
                    return true;
226
                }
227
228
                $this->error = self::METHOD_NOT_ALLOWED;
229
                return false;
230
            }
231
232
            $this->error = self::BAD_REQUEST;
233
            return false;
234
        }
235
236
        $this->error = self::NOT_FOUND;
237
        return false;
238
    }
239
240
    /**
241
     * @param string $method
242
     * @param string $route
243
     * @param string|callable $handler
244
     * @param null|string
245
     * @return Dispatch
246
     */
247
    protected function addRoute(string $method, string $route, $handler, string $name = null): Dispatch
248
    {
249
        if ($route == "/") {
250
            $this->addRoute($method, "", $handler, $name);
251
        }
252
253
        preg_match_all("~\{\s* ([a-zA-Z_][a-zA-Z0-9_-]*) \}~x", $route, $keys, PREG_SET_ORDER);
254
        $routeDiff = array_values(array_diff(explode("/", $this->patch), explode("/", $route)));
255
256
        $this->formSpoofing();
257
        $offset = ($this->group ? 1 : 0);
258
        foreach ($keys as $key) {
259
            $this->data[$key[1]] = ($routeDiff[$offset++] ?? null);
260
        }
261
262
        $route = (!$this->group ? $route : "/{$this->group}{$route}");
263
        $data = $this->data;
264
        $namespace = $this->namespace;
265
        $router = function () use ($method, $handler, $data, $route, $name, $namespace) {
266
            return [
267
                "route" => $route,
268
                "name" => $name,
269
                "method" => $method,
270
                "handler" => $this->handler($handler, $namespace),
271
                "action" => $this->action($handler),
272
                "data" => $data
273
            ];
274
        };
275
276
        $route = preg_replace('~{([^}]*)}~', "([^/]+)", $route);
277
        $this->routes[$method][$route] = $router();
278
        return $this;
279
    }
280
281
    /**
282
     * httpMethod form spoofing
283
     */
284
    protected function formSpoofing(): void
285
    {
286
        $post = filter_input_array(INPUT_POST, FILTER_DEFAULT);
287
288
        if (!empty($post['_method']) && in_array($post['_method'], ["PUT", "PATCH", "DELETE"])) {
289
            $this->httpMethod = $post['_method'];
290
            $this->data = $post;
291
292
            unset($this->data["_method"]);
293
            return;
294
        }
295
296
        if ($this->httpMethod == "POST") {
297
            $this->data = filter_input_array(INPUT_POST, FILTER_DEFAULT);
298
299
            unset($this->data["_method"]);
300
            return;
301
        }
302
303
        if (in_array($this->httpMethod, ["PUT", "PATCH", "DELETE"]) && !empty($_SERVER['CONTENT_LENGTH'])) {
304
            parse_str(file_get_contents('php://input', false, null, 0, $_SERVER['CONTENT_LENGTH']), $putPatch);
305
            $this->data = $putPatch;
306
307
            unset($this->data["_method"]);
308
            return;
309
        }
310
311
        $this->data = [];
312
        return;
313
    }
314
315
    /**
316
     * @param $handler
317
     * @param $namespace
318
     * @return string|callable
319
     */
320
    private function handler($handler, $namespace)
321
    {
322
        return (!is_string($handler) ? $handler : "{$namespace}\\" . explode($this->separator, $handler)[0]);
323
    }
324
325
    /**
326
     * @param $handler
327
     * @return null|string
328
     */
329
    private function action($handler): ?string
330
    {
331
        return (!is_string($handler) ?: (explode($this->separator, $handler)[1] ?? null));
332
    }
333
}