Passed
Push — master ( 92e76a...1cfcdd )
by Robson
02:02
created

Dispatch::execute()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 28
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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