Router::executeControllerMethod()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 3
nop 2
dl 0
loc 15
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
// src/Router.php
4
5
namespace PedroQuezado\Code\Router;
6
7
class Router {
8
    private $routes = []; // Armazena as rotas definidas
9
    private $domain; // Domínio do projeto
10
    private $separator; // Separador de callback (padrão: "::")
11
    private $namespace = ''; // Namespace para as chamadas de métodos de classe
12
    private $routeGroups = []; // Grupos de rota
13
    private $middlewares = []; // Middlewares das rotas
14
    private $namedRoutes = []; // Nomes de rota
15
    private $cachedRoutes; // Rotas em cache
16
17
    /**
18
     * Construtor da classe Router.
19
     *
20
     * @param string $domain Domínio do projeto
21
     * @param string $separator Separador de callback (padrão: "::")
22
     */
23
    public function __construct(string $domain, string $separator = "::")
24
    {
25
        $this->domain = rtrim($domain, "/"); // Remove a barra final do domínio, se houver
26
        $this->separator = $separator;
27
    }
28
29
    /**
30
     * Define o namespace para as chamadas de métodos de classe.
31
     *
32
     * @param string $namespace O namespace a ser definido
33
     * @return void
34
     */
35
    public function namespace(string $namespace): void
36
    {
37
        $this->namespace = $namespace;
38
    }
39
40
    /**
41
     * Define um grupo de rota.
42
     *
43
     * @param array $attributes Atributos do grupo de rota
44
     * @param \Closure $callback Função de callback para definir as rotas do grupo
45
     * @return void
46
     */
47
    public function group(array $attributes, \Closure $callback): void
48
    {
49
        $this->routeGroups[] = $attributes;
50
        $callback();
51
        array_pop($this->routeGroups);
52
    }
53
54
    /**
55
     * Define um middleware para as rotas.
56
     *
57
     * @param mixed $middleware Middleware a ser aplicado (função ou classe de middleware)
58
     * @return void
59
     */
60
    public function middleware($middleware): void
61
    {
62
        $this->middlewares[] = $middleware;
63
    }
64
65
    /**
66
     * Define uma rota HTTP GET.
67
     *
68
     * @param string $path Caminho da rota
69
     * @param mixed $callback Callback da rota (função ou método de classe)
70
     * @return void
71
     */
72
    public function get(string $path, $callback): void
73
    {
74
        $this->addRoute('GET', $path, $callback);
75
    }
76
77
    /**
78
     * Define uma rota HTTP POST.
79
     *
80
     * @param string $path Caminho da rota
81
     * @param mixed $callback Callback da rota (função ou método de classe)
82
     * @return void
83
     */
84
    public function post(string $path, $callback): void
85
    {
86
        $this->addRoute('POST', $path, $callback);
87
    }
88
89
    /**
90
     * Define uma rota HTTP PUT.
91
     *
92
     * @param string $path Caminho da rota
93
     * @param mixed $callback Callback da rota (função ou método de classe)
94
     * @return void
95
     */
96
    public function put(string $path, $callback): void
97
    {
98
        $this->addRoute('PUT', $path, $callback);
99
    }
100
101
    /**
102
     * Define uma rota HTTP PATCH.
103
     *
104
     * @param string $path Caminho da rota
105
     * @param mixed $callback Callback da rota (função ou método de classe)
106
     * @return void
107
     */
108
    public function patch(string $path, $callback): void
109
    {
110
        $this->addRoute('PATCH', $path, $callback);
111
    }
112
113
    /**
114
     * Define uma rota HTTP DELETE.
115
     *
116
     * @param string $path Caminho da rota
117
     * @param mixed $callback Callback da rota (função ou método de classe)
118
     * @return void
119
     */
120
    public function delete(string $path, $callback): void
121
    {
122
        $this->addRoute('DELETE', $path, $callback);
123
    }
124
125
    /**
126
     * Adiciona uma nova rota.
127
     *
128
     * @param string $method Método HTTP da rota (GET, POST, etc.)
129
     * @param string $path Caminho da rota
130
     * @param mixed $callback Callback da rota (função ou método de classe)
131
     * @return void
132
     */
133
    private function addRoute(string $method, string $path, $callback): void
134
    {
135
        $route = [
136
            'method' => $method,
137
            'path' => $path,
138
            'callback' => $callback,
139
            'middlewares' => $this->middlewares,
140
            'groupName' => end($this->routeGroups)['name'] ?? null
141
        ];
142
143
        $this->routes[] = $route;
144
145
        if (isset($route['name'])) {
146
            $this->namedRoutes[$route['name']] = $route;
147
        }
148
149
        $this->middlewares = [];
150
    }
151
152
    /**
153
     * Executa o roteamento das requisições.
154
     *
155
     * @return void
156
     */
157
    public function run(): void
158
    {
159
        $requestMethod = $_SERVER['REQUEST_METHOD'];
160
        $requestPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
161
162
        $matchedRoute = $this->findMatchingRoute($requestMethod, $requestPath);
163
164
        if ($matchedRoute) {
165
            $this->executeRoute($matchedRoute);
166
        } else {
167
            $this->renderErrorPage(404, 'Page not found');
168
        }
169
    }
170
171
    /**
172
     * Encontra a rota correspondente à requisição atual.
173
     *
174
     * @param string $requestMethod Método da requisição atual
175
     * @param string $requestPath Caminho da requisição atual
176
     * @return array|null Rota correspondente ou null se nenhuma rota corresponder
177
     */
178
    private function findMatchingRoute(string $requestMethod, string $requestPath): ?array
179
    {
180
        if ($this->cachedRoutes === null) {
181
            $this->cachedRoutes = $this->cacheRoutes();
182
        }
183
184
        foreach ($this->cachedRoutes as $route) {
185
            if ($this->matchRoute($route, $requestMethod, $requestPath)) {
186
                return $route;
187
            }
188
        }
189
190
        return null;
191
    }
192
193
    /**
194
     * Armazena em cache as rotas definidas.
195
     *
196
     * @return array Rotas em cache
197
     */
198
    private function cacheRoutes(): array
199
    {
200
        $cachedRoutes = [];
201
202
        foreach ($this->routes as $route) {
203
            $pattern = $this->convertPathToRegex($route['path']);
204
            $route['pattern'] = $pattern;
205
            $cachedRoutes[] = $route;
206
        }
207
208
        return $cachedRoutes;
209
    }
210
211
    /**
212
     * Verifica se a rota atual corresponde à rota definida.
213
     *
214
     * @param array $route Rota definida
215
     * @param string $requestMethod Método da requisição atual
216
     * @param string $requestPath Caminho da requisição atual
217
     * @return bool true se a rota corresponder, false caso contrário
218
     */
219
    private function matchRoute(array $route, string $requestMethod, string $requestPath): bool
220
    {
221
        if ($route['method'] !== $requestMethod) {
222
            return false;
223
        }
224
225
        $pattern = $route['pattern'];
226
227
        // Substitui os grupos de rota na expressão regular
228
        $pattern = $this->replaceRouteGroups($pattern);
229
230
        // Verifica se a rota corresponde ao padrão
231
        if (!preg_match($pattern, $requestPath, $matches)) {
232
            return false;
233
        }
234
235
        $route['matches'] = $matches;
236
        return true;
237
    }
238
239
    /**
240
     * Executa a rota correspondente à requisição atual.
241
     *
242
     * @param array $route Rota correspondente
243
     * @return void
244
     */
245
    private function executeRoute(array $route): void
246
    {
247
        $callback = $route['callback'];
248
        $matches = $route['matches'];
249
250
        // Verifica se a rota pertence a um grupo
251
        if (!empty($route['groupName'])) {
252
            $group = $this->getRouteGroupByName($route['groupName']);
253
            $callback = $this->wrapMiddleware($callback, $group['middlewares']);
0 ignored issues
show
Bug introduced by
The method wrapMiddleware() does not exist on PedroQuezado\Code\Router\Router. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

253
            /** @scrutinizer ignore-call */ $callback = $this->wrapMiddleware($callback, $group['middlewares']);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
254
        }
255
256
        // Executa o callback da rota
257
        if (is_callable($callback)) {
258
            call_user_func_array($callback, array_slice($matches, 1));
259
        } else {
260
            $this->executeControllerMethod($callback, $matches);
261
        }
262
    }
263
264
    /**
265
     * Executa o método de um controlador.
266
     *
267
     * @param string $callback Callback do método do controlador (ex: "Controller@method")
268
     * @param array $matches Parâmetros capturados na rota
269
     * @return void
270
     */
271
    private function executeControllerMethod(string $callback, array $matches): void
272
    {
273
        list($controller, $method) = explode($this->separator, $callback);
274
        $controller = $this->namespace . '\\' . $controller;
275
276
        if (class_exists($controller)) {
277
            $controllerInstance = new $controller();
278
279
            if (method_exists($controllerInstance, $method)) {
280
                call_user_func_array([$controllerInstance, $method], array_slice($matches, 1));
281
            } else {
282
                $this->renderErrorPage(500, 'Internal Server Error');
283
            }
284
        } else {
285
            $this->renderErrorPage(500, 'Internal Server Error');
286
        }
287
    }
288
289
    /**
290
     * Substitui os grupos de rota na expressão regular.
291
     *
292
     * @param string $pattern Expressão regular da rota
293
     * @return string Expressão regular modificada
294
     */
295
    private function replaceRouteGroups(string $pattern): string
296
    {
297
        foreach ($this->routeGroups as $group) {
298
            $prefix = isset($group['prefix']) ? $group['prefix'] : '';
299
            $pattern = str_replace('{' . $group['name'] . '}', $prefix, $pattern);
300
        }
301
302
        return $pattern;
303
    }
304
305
    /**
306
     * Retorna um grupo de rota pelo nome.
307
     *
308
     * @param string $name Nome do grupo de rota
309
     * @return array|null Grupo de rota correspondente ou null se não encontrado
310
     */
311
    private function getRouteGroupByName(string $name): ?array
312
    {
313
        foreach ($this->routeGroups as $group) {
314
            if ($group['name'] === $name) {
315
                return $group;
316
            }
317
        }
318
319
        return null;
320
    }
321
322
    /**
323
     * Retorna o caminho de uma rota pelo nome.
324
     *
325
     * @param string $name Nome da rota
326
     * @param array $parameters Parâmetros da rota
327
     * @return string|null Caminho da rota correspondente ou null se não encontrado
328
     */
329
    public function route(string $name, array $parameters = []): ?string
330
    {
331
        if (isset($this->namedRoutes[$name])) {
332
            $route = $this->namedRoutes[$name];
333
            $path = $route['path'];
334
335
            // Substitui os parâmetros na rota
336
            foreach ($parameters as $key => $value) {
337
                $path = str_replace('{' . $key . '}', $value, $path);
338
            }
339
340
            return $this->domain . $path;
341
        }
342
343
        return null;
344
    }
345
346
    /**
347
     * Converte um caminho de rota em uma expressão regular.
348
     *
349
     * @param string $path Caminho da rota
350
     * @return string Expressão regular correspondente
351
     */
352
    private function convertPathToRegex(string $path): string
353
    {
354
        $pattern = preg_quote($path, '/');
355
356
        // Substitui os parâmetros da rota
357
        $pattern = preg_replace('/\\\{([A-Za-z0-9_]+)\\\}/', '([^\/]+)', $pattern);
358
359
        return '/^' . $pattern . '$/';
360
    }
361
362
    /**
363
     * Renderiza uma página de erro.
364
     *
365
     * @param int $statusCode Código de status HTTP
366
     * @param string $message Mensagem de erro
367
     * @return void
368
     */
369
    private function renderErrorPage(int $statusCode, string $message): void
370
    {
371
        http_response_code($statusCode);
372
        echo "<h1>Error $statusCode</h1>";
373
        echo "<p>$message</p>";
374
        exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
375
    }
376
    
377
    /**
378
     * Retorna as informações de depuração para var_dump.
379
     *
380
     * @return array Informações de depuração
381
     */
382
    public function __debugInfo(): array
383
    {
384
        return [
385
            'routes' => $this->routes,
386
            'domain' => $this->domain,
387
            'separator' => $this->separator,
388
            'namespace' => $this->namespace,
389
        ];
390
    }
391
}
392