mirror of
https://github.com/WilliamAAK/Route2.git
synced 2025-04-19 13:07:18 +00:00
404 lines
13 KiB
PHP
404 lines
13 KiB
PHP
<?php
|
|
|
|
namespace WilliamAAK\Http;
|
|
|
|
use InvalidArgumentException;
|
|
|
|
/**
|
|
* A simple routing system for PHP web applications.
|
|
*
|
|
* ### Features:
|
|
* - Define routes for specific HTTP methods (GET, POST, PUT, DELETE, etc.).
|
|
* - Apply middleware to routes or groups of routes.
|
|
* - Group routes under a common prefix for better organization.
|
|
* - Support for route parameters (e.g., `/user/$id`) and optional parameters (e.g., `/user/$id?`).
|
|
*
|
|
* ### Example:
|
|
* ```php
|
|
* // Setup the routing context. This method must be called before defining any routes
|
|
* Route2::setup();
|
|
*
|
|
* // Define a GET route
|
|
* Route2::get('/home', function () {
|
|
* echo "Welcome to the homepage!";
|
|
* });
|
|
*
|
|
* // Define a route with a parameter
|
|
* Route2::get('/user/$id', function ($id) {
|
|
* echo "User ID: $id";
|
|
* });
|
|
*
|
|
* // Define a route with an optional parameter
|
|
* Route2::get('/user/$id?', function ($id = null) {
|
|
* echo $id ? "User ID: $id" : "No User ID provided.";
|
|
* });
|
|
*
|
|
* // Defining middlewares
|
|
* Route2::before(function () {
|
|
* echo "Middleware executed before the route callback.";
|
|
* });
|
|
* Route2::after(function () {
|
|
* echo "Middleware executed after the route callback.";
|
|
* });
|
|
*
|
|
* // Define a route with a middleware attached to it
|
|
* Route2::post('/submit', function () {
|
|
* echo "Form submitted!";
|
|
* }, function () {
|
|
* echo "Middleware executed before the callback.";
|
|
* });
|
|
*
|
|
* // Group routes under a common prefix
|
|
* Route2::group('/admin', function () {
|
|
* // Middlewares defined here will not affect the routes outside this group
|
|
* Route2::before(function () {
|
|
* echo "Admin Middleware executed.";
|
|
* });
|
|
* Route2::get('/dashboard', function () {
|
|
* echo "Admin Dashboard";
|
|
* });
|
|
* Route2::post('/settings', function () {
|
|
* echo "Admin Settings";
|
|
* });
|
|
* });
|
|
* ```
|
|
*
|
|
* @package WilliamAAK\Http
|
|
* @version 1.0.0
|
|
* @author WilliamAAK
|
|
*/
|
|
class Route2
|
|
{
|
|
/**
|
|
* Router state variables.
|
|
*/
|
|
private static string $routePrefix;
|
|
private static array $beforeMiddleware;
|
|
private static array $afterMiddleware;
|
|
private static string $requestUri;
|
|
private static string $requestMethod;
|
|
|
|
/**
|
|
* Setup the routing context. This method must be called before defining any routes.
|
|
*
|
|
* If no request URI or method is provided, it will use the current request's URI and method.
|
|
*
|
|
* @param string|null $requestUri The request URI (optional).
|
|
* @param string|null $requestMethod The request method (optional).
|
|
*
|
|
* @return void
|
|
*/
|
|
static function setup(?string $requestUri = null, ?string $requestMethod = null)
|
|
{
|
|
self::$routePrefix = '';
|
|
self::$beforeMiddleware = [[]];
|
|
self::$afterMiddleware = [[]];
|
|
self::$requestUri = urldecode($requestUri ?? self::getRelativeRequestPath());
|
|
self::$requestMethod = strtoupper($requestMethod ?? $_SERVER['REQUEST_METHOD']);
|
|
}
|
|
|
|
/**
|
|
* Retrieves the relative path of the current HTTP request.
|
|
*
|
|
* ### Examples:
|
|
* 1. If the request URI is `/index.php/myprofile/sessions` and the script name is `/index.php`,
|
|
* this method will return:
|
|
* `/myprofile/sessions`
|
|
*
|
|
* 2. If the request URI is `/myprofile/sessions` and rewrite rules are used to hide `/index.php`,
|
|
* this method will return:
|
|
* `/myprofile/sessions`
|
|
*
|
|
* @return string The relative request path.
|
|
*/
|
|
static function getRelativeRequestPath(): string
|
|
{
|
|
$requestUrlPath = strtok($_SERVER['REQUEST_URI'], '?');
|
|
|
|
$scriptName = $_SERVER['SCRIPT_NAME'];
|
|
$scriptDir = dirname($scriptName) . '/';
|
|
|
|
if (str_starts_with($requestUrlPath, $scriptName)) {
|
|
$requestUrlPath = substr($requestUrlPath, strlen($scriptName));
|
|
}
|
|
|
|
if ($requestUrlPath === $scriptDir) {
|
|
$requestUrlPath = '/';
|
|
}
|
|
|
|
if ($requestUrlPath === '') {
|
|
$requestUrlPath = '/';
|
|
}
|
|
|
|
return $requestUrlPath;
|
|
}
|
|
|
|
/**
|
|
* Matches the current HTTP request to a defined route. If the route matches it runs the corresponding
|
|
* callback and middlewares before terminating the script.
|
|
*
|
|
* ### Examples:
|
|
* 1. Matching a route with parameters:
|
|
* ```php
|
|
* Route2::match('GET', '/user/$id', function ($id) {
|
|
* echo "User ID: $id";
|
|
* });
|
|
* ```
|
|
* - Request: GET /user/123
|
|
* - Output: "User ID: 123"
|
|
*
|
|
* 2. Matching a route with optional parameters:
|
|
* ```php
|
|
* Route2::match('GET', '/user/$id?', function ($id = null) {
|
|
* echo $id ? "User ID: $id" : "No User ID";
|
|
* });
|
|
* ```
|
|
* - Request: GET /user/
|
|
* - Output: "No User ID"
|
|
*
|
|
* @param string $methods Allowed HTTP methods (e.g., "GET|POST").
|
|
* @param string $route The route path to match (e.g., "/user/$id").
|
|
* @param callable $callback The callback to execute if the route matches.
|
|
* @param callable|null $middleware Optional middleware to execute before the callback.
|
|
*
|
|
* @return void
|
|
*/
|
|
static function match(string $methods, string $route, callable $callback, ?callable $middleware = null): void
|
|
{
|
|
$allowedMethods = array_map(strtoupper(...), explode('|', $methods));
|
|
if (!in_array(self::$requestMethod, $allowedMethods, true)) {
|
|
return;
|
|
}
|
|
|
|
$requestParts = explode('/', self::$requestUri);
|
|
$routeParts = explode('/', self::$routePrefix . $route);
|
|
$callbackArgs = [];
|
|
|
|
if (count($requestParts) !== count($routeParts)) {
|
|
return;
|
|
}
|
|
|
|
for ($i = 1; $i < count($routeParts); $i++) {
|
|
$routePart = $routeParts[$i] ?? '';
|
|
$requestPart = $requestParts[$i] ?? '';
|
|
|
|
if ($routePart === '') {
|
|
continue;
|
|
}
|
|
|
|
$isRouteParameter = $routePart[0] === '$';
|
|
if (!$isRouteParameter) {
|
|
if ($routePart !== $requestPart) {
|
|
return;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
$isOptional = $routePart[-1] === '?';
|
|
if (!$isOptional && $requestPart === '') {
|
|
return;
|
|
}
|
|
|
|
if ($isOptional && $i !== count($routeParts) - 1) {
|
|
throw new InvalidArgumentException('Only the last route parameter can be optional.');
|
|
}
|
|
|
|
if ($requestPart !== '') {
|
|
$callbackArgs[] = $requestPart;
|
|
}
|
|
|
|
$requestParts[$i] = $routePart;
|
|
}
|
|
|
|
if (implode('/', $routeParts) !== implode('/', $requestParts)) {
|
|
return;
|
|
}
|
|
|
|
if ($middleware !== null) {
|
|
self::before($middleware);
|
|
}
|
|
|
|
foreach (self::$beforeMiddleware as $middlewares) {
|
|
foreach ($middlewares as $middleware) {
|
|
$middleware();
|
|
}
|
|
}
|
|
|
|
$callback(...$callbackArgs);
|
|
|
|
foreach (self::$afterMiddleware as $middlewares) {
|
|
foreach ($middlewares as $middleware) {
|
|
$middleware();
|
|
}
|
|
}
|
|
|
|
die();
|
|
}
|
|
|
|
/**
|
|
* Shorthand for Route2::match('GET', ...).
|
|
*/
|
|
static function get(string $route, callable $callback, ?callable $middleware = null): void
|
|
{
|
|
self::match('get', $route, $callback, $middleware);
|
|
}
|
|
|
|
/**
|
|
* Shorthand for Route2::match('POST', ...).
|
|
*/
|
|
static function post(string $route, callable $callback, ?callable $middleware = null): void
|
|
{
|
|
self::match('post', $route, $callback, $middleware);
|
|
}
|
|
|
|
/**
|
|
* Shorthand for Route2::match('PUT', ...).
|
|
*/
|
|
static function put(string $route, callable $callback, ?callable $middleware = null): void
|
|
{
|
|
self::match('put', $route, $callback, $middleware);
|
|
}
|
|
|
|
/**
|
|
* Shorthand for Route2::match('PATCH', ...).
|
|
*/
|
|
static function patch(string $route, callable $callback, ?callable $middleware = null): void
|
|
{
|
|
self::match('patch', $route, $callback, $middleware);
|
|
}
|
|
|
|
/**
|
|
* Shorthand for Route2::match('DELETE', ...).
|
|
*/
|
|
static function delete(string $route, callable $callback, ?callable $middleware = null): void
|
|
{
|
|
self::match('delete', $route, $callback, $middleware);
|
|
}
|
|
|
|
/**
|
|
* Shorthand for Route2::match('OPTIONS', ...).
|
|
*/
|
|
static function options(string $route, callable $callback, ?callable $middleware = null): void
|
|
{
|
|
self::match('options', $route, $callback, $middleware);
|
|
}
|
|
|
|
/**
|
|
* Shorthand for Route2::match('GET|POST', ...).
|
|
*/
|
|
static function form(string $route, callable $callback, ?callable $middleware = null): void
|
|
{
|
|
self::match('get|post', $route, $callback, $middleware);
|
|
}
|
|
|
|
/**
|
|
* Shorthand for Route2::match('GET|POST|PUT|PATCH|DELETE|OPTIONS', ...).
|
|
*/
|
|
static function any(string $route, callable $callback, ?callable $middleware = null): void
|
|
{
|
|
self::match('get|post|put|patch|delete|options', $route, $callback, $middleware);
|
|
}
|
|
|
|
/**
|
|
* Registers middleware to be executed before the route callback.
|
|
*
|
|
* ### Example:
|
|
* ```php
|
|
* Route2::before(function () {
|
|
* echo "Middleware executed before the route callback.";
|
|
* });
|
|
* ```
|
|
*
|
|
* @param callable $middleware The middleware function to execute before the route callback.
|
|
*
|
|
* @return void
|
|
*/
|
|
static function before(callable $middleware): void
|
|
{
|
|
array_push(
|
|
self::$beforeMiddleware[array_key_last(self::$beforeMiddleware)],
|
|
$middleware
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Registers middleware to be executed after the route callback.
|
|
*
|
|
* ### Example:
|
|
* ```php
|
|
* Route2::after(function () {
|
|
* echo "Middleware executed after the route callback.";
|
|
* });
|
|
* ```
|
|
*
|
|
* @param callable $middleware The middleware function to execute after the route callback.
|
|
*
|
|
* @return void
|
|
*/
|
|
static function after(callable $middleware): void
|
|
{
|
|
array_push(
|
|
self::$afterMiddleware[array_key_last(self::$afterMiddleware)],
|
|
$middleware
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Group routes and middlewares with an optional common prefix.
|
|
* It is useful for organizing routes that share a common path or functionality.
|
|
*
|
|
* ### Examples:
|
|
* 1. Grouping routes under a prefix:
|
|
* ```php
|
|
* Route2::group('/admin', function () {
|
|
* Route2::get('/dashboard', function () {
|
|
* echo "Admin Dashboard";
|
|
* });
|
|
* });
|
|
* ```
|
|
* - Request: GET /admin/dashboard
|
|
* - Output: "Admin Dashboard"
|
|
*
|
|
* 2. Grouping middlewares and routes but this time without a prefix:
|
|
* ```php
|
|
* Route2::group(callback: function () {
|
|
* Route2::before(function () {
|
|
* echo "Hello routes from within or nested groups! ";
|
|
* });
|
|
* // Outputs: "Hello routes from within or nested groups! Hello from somewhere!"
|
|
* Route2::get('/somewhere', function () {
|
|
* echo "Hello from somewhere!";
|
|
* });
|
|
* });
|
|
*
|
|
* // Outputs: "I am unaffected by that group middleware."
|
|
* Route2::get('/', function () {
|
|
* echo "I am unaffected by that group middleware.";
|
|
* });
|
|
* ```
|
|
* @param string|null $routePrefix The optional route prefix for the group (e.g., "/admin").
|
|
* @param callable|null $callback The callback containing the route definitions for the group.
|
|
|
|
* @throws InvalidArgumentException If the callback is not provided.
|
|
*
|
|
* @return void
|
|
*/
|
|
static function group(?string $routePrefix = null, ?callable $callback = null): void
|
|
{
|
|
if ($callback === null) {
|
|
throw new InvalidArgumentException('You must provide a callback.');
|
|
}
|
|
if (!str_starts_with(
|
|
self::$requestUri,
|
|
self::$routePrefix . $routePrefix ?? ''
|
|
)) return;
|
|
self::$routePrefix = self::$routePrefix . $routePrefix ?? '';
|
|
array_push(self::$beforeMiddleware, []);
|
|
array_push(self::$afterMiddleware, []);
|
|
$callback();
|
|
array_pop(self::$beforeMiddleware);
|
|
array_pop(self::$afterMiddleware);
|
|
self::$routePrefix = rtrim(self::$routePrefix, $routePrefix ?? '');
|
|
}
|
|
}
|