Issues (33)

src/Router.php (7 issues)

1
<?php
2
declare(strict_types=1);
3
4
namespace Utilities\Router;
5
6
use Utilities\Common\Time;
7
use Utilities\Router\Exceptions\ControllerException;
8
use Utilities\Router\Exceptions\SessionException;
9
use Utilities\Router\Traits\RouterTrait;
10
use Utilities\Router\Utils\Assistant;
11
12
/**
13
 * Router class
14
 *
15
 * @method static void any(string $uri, callable $callback) Create a route that matches any HTTP method
16
 * @method static void get(string $uri, callable $callback) Adds a GET route to the router.
17
 * @method static void post(string $uri, callable $callback) Adds a POST route to the router.
18
 * @method static void put(string $uri, callable $callback) Adds a PUT route to the router.
19
 * @method static void delete(string $uri, callable $callback) Adds a DELETE route to the router.
20
 * @method static void options(string $uri, callable $callback) Adds a OPTIONS route to the router.
21
 *
22
 * @link    https://github.com/utilities-php/router
23
 * @author  Shahrad Elahi (https://github.com/shahradelahi)
24
 * @license https://github.com/utilities-php/router/blob/master/LICENSE (MIT License)
25
 */
26
class Router
27
{
28
29
    use RouterTrait;
30
31
    /**
32
     * create a route that matches the given HTTP methods
33
     *
34
     * @param string $method
35
     * @param string $uri
36
     * @param callable $callback
37
     * @return void
38
     */
39
    public static function match(string $method, string $uri, callable $callback): void
40
    {
41
        if (!isset(static::$routes[$method])) {
0 ignored issues
show
Since $routes is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $routes to at least protected.
Loading history...
42
            static::$routes[$method] = [];
43
        }
44
45
        static::$routes[$method][$uri] = $callback;
46
        self::resolve();
47
    }
48
49
    /**
50
     * rate limit
51
     *
52
     * @param string $uri the uri to rate limit (e.g. /api/v1/users)
53
     * @param int $period the time period (e.g. 1 minute)
54
     * @param int $rate the limit of requests (e.g. 100 requests per minute)
55
     * @return void
56
     */
57
    public static function rateLimiter(string $uri, int $period, int $rate): void
58
    {
59
        if (Session::getStatus() !== 2) {
60
            throw new SessionException(
61
                'Session is not started yet. Please call Session::start() first.'
62
            );
63
        }
64
65
        static::$rateLimitedRoutes[$uri] = [
0 ignored issues
show
Since $rateLimitedRoutes is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $rateLimitedRoutes to at least protected.
Loading history...
66
            'rate' => $rate,
67
            'period' => $period,
68
            'created_at' => Time::getMillisecond(),
69
            'updated_at' => Time::getMillisecond(),
70
            'status' => true,
71
        ];
72
    }
73
74
    /**
75
     * add a controller to the router
76
     *
77
     * @param string $slug the slug of the controller (e.g. users)
78
     * @param string $uri the uri to rate limit (e.g. /api/v1/users)
79
     * @param string|AnonymousController $controller the controller class name (e.g. \App\Controllers\UsersController)
80
     * @return void
81
     */
82
    public static function controller(string $slug, string $uri, string|AnonymousController $controller): void
83
    {
84
        if (!is_subclass_of($controller, AnonymousController::class)) {
85
            throw new ControllerException(sprintf(
86
                'Class `%s` does not exist or not instance of `%s`.',
87
                $controller::class, Controller::class
88
            ));
89
        }
90
91
        if (is_string($controller)) {
92
            $controller = new $controller($slug);
93
        }
94
95
        try {
96
            foreach ((new \ReflectionClass($controller))->getMethods() as $refMethod) {
97
                if (str_starts_with($refMethod->getName(), '__')) {
98
                    continue;
99
                }
100
101
                if (Assistant::hasRouteAttribute($refMethod)) {
102
                    foreach (Assistant::extractRouteAttributes($refMethod) as $route) {
103
                        $methodName = $refMethod->getName();
104
                        $routeUri = $uri . $route->getUri();
105
                        Router::match($route->getMethod(), $routeUri, function (...$args) use ($controller, $methodName) {
106
                            Assistant::passDataToMethod($controller, $methodName);
107
                        });
108
                    }
109
110
                } else {
111
                    $methodName = $refMethod->getName();
112
                    $routeUri = $uri . '/' . $methodName;
113
114
                    if ($methodName === 'index' && !self::isRegistered($uri)) {
115
                        $routeUri = $uri;
116
                    }
117
118
                    Router::any($routeUri, function (...$args) use ($controller, $methodName) {
119
                        Assistant::passDataToMethod($controller, $methodName);
120
                    });
121
                }
122
123
            }
124
125
        } catch (\ReflectionException $e) {
126
            throw new ControllerException($e->getMessage(), $e->getCode(), $e);
127
        }
128
129
        self::$controllers[$uri] = $controller;
130
    }
131
132
    /**
133
     * Does uri registered in router?
134
     *
135
     * @param string $uri e.g. /api/v1/users or /api/v1/users/{id}
136
     * @return bool
137
     */
138
    public static function isRegistered(string $uri): bool
139
    {
140
        foreach (static::$routes as $method => $routes) {
0 ignored issues
show
Since $routes is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $routes to at least protected.
Loading history...
141
            foreach ($routes as $route => $callback) {
142
                if ($route === $uri) {
143
                    return true;
144
                }
145
            }
146
        }
147
148
        return false;
149
    }
150
151
    /**
152
     * create a resource route. it can be public documents, images, css, js, etc.
153
     *
154
     * Note: this will give access to whole directory. (e.g. /public/*)
155
     *
156
     * @todo: create test for this method
157
     *
158
     * @param string $uri the uri to rate limit (e.g. /docs)
159
     * @param string $localPath the absolute path to the directory (e.g. /var/www/html/docs)
160
     * @return void
161
     */
162
    public static function resource(string $uri, string $localPath): void
163
    {
164
        foreach (scandir($localPath) as $file) {
165
            if ($file !== '.' && $file !== '..') {
166
                $filePath = $localPath . '/' . $file;
167
                if (is_dir($filePath)) {
168
                    self::resource($uri . '/' . $file, $filePath);
169
                } else {
170
                    $extension = pathinfo($filePath, PATHINFO_EXTENSION);
171
                    header('Content-Type: ' . PathFinder::getMimeType($extension));
0 ignored issues
show
It seems like $extension can also be of type array; however, parameter $filePath of Utilities\Router\PathFinder::getMimeType() does only seem to accept string, 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

171
                    header('Content-Type: ' . PathFinder::getMimeType(/** @scrutinizer ignore-type */ $extension));
Loading history...
172
                    self::get($uri . '/' . $file, function () use ($filePath) {
173
                        return file_get_contents($filePath);
174
                    });
175
                }
176
            }
177
        }
178
    }
179
180
    /**
181
     * Resolve the router with the given Request
182
     *
183
     * @param Request|null $request
184
     * @return void
185
     */
186
    public static function resolve(Request|null $request = null): void
187
    {
188
        if ($request === null) {
189
            $request = self::createRequest();
190
        }
191
192
        $uri = $request === null ? Request::getUri() : $request::getUri();
193
        $method = $request === null ? Request::getMethod() : $request::getMethod();
194
        $uri = str_ends_with($uri, '/') ? substr($uri, 0, -1) : $uri;
195
196
        if (isset(static::$routes['ANY'])) {
0 ignored issues
show
Since $routes is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $routes to at least protected.
Loading history...
197
            static::$routes[$method] = array_merge(
198
                static::$routes[$method], static::$routes['ANY']
199
            );
200
201
            unset(static::$routes['ANY']);
202
        }
203
204
        self::findAndPassData($uri);
205
    }
206
207
    /**
208
     * Create request
209
     *
210
     * @return Request
211
     */
212
    public static function createRequest(): Request
213
    {
214
        $find = self::find($_SERVER['REQUEST_URI']);
215
        $params = $find !== false ? $find['params'] : [];
216
217
        $headers = (function () {
218
            $headers = [];
219
            foreach ($_SERVER as $key => $value) {
220
                if (str_starts_with($key, 'HTTP_')) {
221
                    $headers[str_replace('HTTP_', '', $key)] = $value;
222
                }
223
            }
224
            return $headers;
225
        })();
226
227
        return new Request([
228
            'uri' => $_SERVER['REQUEST_URI'],
229
            'method' => $_SERVER['REQUEST_METHOD'],
230
            'headers' => $headers,
231
            'body' => file_get_contents('php://input'),
232
            'query_string' => URLs::QueryString(),
233
            'params' => $params,
234
        ]);
235
    }
236
237
    /**
238
     * Clear every all defined routes and controllers
239
     *
240
     * @return void
241
     */
242
    public static function clear(): void
243
    {
244
        static::$routes = [];
0 ignored issues
show
Since $routes is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $routes to at least protected.
Loading history...
245
        static::$controllers = [];
0 ignored issues
show
Since $controllers is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $controllers to at least protected.
Loading history...
246
    }
247
248
    /**
249
     * Remove a route from the router
250
     *
251
     * @param string $uri
252
     * @return void
253
     */
254
    public static function removeRoute(string $uri): void
255
    {
256
        // TODO: Implement removeRoute() method.
257
    }
258
259
    /**
260
     * Remove a controller with its routes from the router
261
     *
262
     * @param string $slug
263
     * @return void
264
     */
265
    public static function removeController(string $slug): void
266
    {
267
        // TODO: Implement removeController() method.
268
    }
269
270
    /**
271
     * @param string $name
272
     * @param array $arguments
273
     * @return mixed
274
     */
275
    public static function __callStatic(string $name, array $arguments): mixed
276
    {
277
        if (in_array(strtoupper($name), self::$defaultMethods)) {
278
            self::match(strtoupper($name), $arguments[0], $arguments[1]);
279
            return true;
280
        }
281
282
        if (method_exists(self::class, $name)) {
283
            return call_user_func_array([self::class, $name], $arguments);
284
        }
285
286
        throw new \BadMethodCallException(sprintf(
287
            'Call to undefined method %s::%s()',
288
            self::class, $name
289
        ));
290
    }
291
292
}