Compare commits

..

No commits in common. "32e6971bc6270e2b8db8e142d39a8a7e116dc4ca" and "c4134515001698ded42659c52a9813b387608aef" have entirely different histories.

3 changed files with 130 additions and 225 deletions

119
README.md
View File

@ -1,64 +1,61 @@
# Route2 🛣️
# Route
A simple routing system for PHP web applications.
A lightweight routing system for handling HTTP requests.
### Features:
- **Parameters**: Flexible URL handling with parameters (e.g, `/user/$id`)
- **Middleware**: Register callback functions to execute **before** and **after** route callback functions.
- **Groups**: Organize routes under an optional prefix (e.g, `/admin`) and apply group-specific middleware.
- **Lightweight**: A single-file, no-frills routing solution with zero dependencies.
- 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
// Initialize the router. This method must be called before defining any routes
Route2::setup();
// Define a GET route
Route2::get('/home', function () {
Route::get('/home', function () {
echo "Welcome to the homepage!";
});
// Define a route with a parameter
Route2::get('/user/$id', function ($id) {
Route::get('/user/$id', function ($id) {
echo "User ID: $id";
});
// Define a route with an optional parameter
Route2::get('/user/$id?', function ($id = null) {
Route::get('/user/$id?', function ($id = null) {
echo $id ? "User ID: $id" : "No User ID provided.";
});
// Defining middlewares
Route2::before(function () {
Route::before(function () {
echo "Middleware executed before the route callback.";
});
Route2::after(function () {
Route::after(function () {
echo "Middleware executed after the route callback.";
});
// Define a route with a middleware attached to it
Route2::post('/submit', function () {
Route::post('/submit', function () {
echo "Form submitted!";
}, function () {
echo "Middleware executed before the callback.";
});
// Group routes under a common prefix
Route2::group('/admin', function () {
Route::group('/admin', function () {
// Middlewares defined here will not affect the routes outside this group
Route2::before(function () {
Route::before(function () {
echo "Admin Middleware executed.";
});
Route2::get('/dashboard', function () {
Route::get('/dashboard', function () {
echo "Admin Dashboard";
});
Route2::post('/settings', function () {
Route::post('/settings', function () {
echo "Admin Settings";
});
});
```
## Installing
# Installation
### Composer:
@ -68,11 +65,11 @@ Composer needs to know where to locate the package. Add the repository to your p
"repositories": [
{
"type": "vcs",
"url": "https://github.com/WilliamAAK/Route2"
"url": "https://github.com/WilliamAAK/Route"
}
],
"require": {
"williamaak/route2": "dev-master"
"williamaak/route": "dev-master"
}
}
```
@ -85,93 +82,35 @@ Fetch the package by running
Its all in a single file; include it in your project like so.
```php
require '/YourPath/Route2.php'
require '/YourPath/Route.php'
```
## Using Route2
# Usage
To get started quickly you may copy this into your project.
### Classic mode
```php
<?php
use WilliamAAK\Http\Route;
require __DIR__.'/vendor/autoload.php';
use WilliamAAK\Http\Route2;
Route2::setup();
Route2::get('/$world?', function($world = 'World') {
Route::get('/$world?', function($world = 'World') {
echo "Hello, $world!";
});
// Since no route was found show a 404 page
// since no route was matched show a 404 page
http_response_code(404);
?>
<h1>Page not found!</h1>
```
### FrankenPHP worker mode
The simplest way to access your routes is to put the file in your favorite folder and run it! E.g if you request `http://your.site/yourscript.php/your/route` the route will be automatically converted to `/your/route`
Boot your application once and keep it in memory by using [worker mode](https://frankenphp.dev/docs/worker/). Simply do what you would normally do but inside the handler.
```php
<?php
// public/index.php
// Prevent worker script termination when a client connection is interrupted
ignore_user_abort(true);
// Boot your app
require __DIR__.'/vendor/autoload.php';
use WilliamAAK\Http\Route2;
// Handler outside the loop for better performance (doing less work)
$handler = static function () {
// Called when a request is received,
// superglobals, php://input and the like are reset
Route2::setup(
$_SERVER['REQUEST_URI']
);
Route2::get('/$world?', function($world = 'World') {
echo "Hello, $world!";
});
// Since no route was found show a 404 page
http_response_code(404);
?>
<h1>Page not found!</h1>
<?php
};
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
$keepRunning = \frankenphp_handle_request($handler);
// Call the garbage collector to reduce the chances of it being triggered in the middle of a page generation
gc_collect_cycles();
if (!$keepRunning) break;
}
```
## Accessing your routes
The simplest way to access your routes is to put the file in your folder and run it.
For example if you request ```http://your.site/yourscript.php/your/route``` it will automatically adjust to `/your/route`.
## Rewriting requests
# Rewrite requests
Want to hide that pesky script name (e.g `index.php`) from the URL?
### FrankenPHP:
It is recommended that you use [FrankenPHP](https://frankenphp.dev); modern PHP app server. This behavior is enabled by default.
It is recommended that you use FrankenPHP the modern PHP app server. This behavior is enabled by default.
### NGINX:
@ -185,7 +124,9 @@ location / {
### Apache:
Make sure that mod_rewrite is installed on Apache. On a unix system you can just do `a2enmod rewrite`
Make sure that mod_rewrite is installed on Apache. On a unix system you can just do
`a2enmod rewrite`
This snippet in your .htaccess will ensure that all requests for files and folders that does not exists will be redirected to `index.php`

View File

@ -5,59 +5,56 @@ namespace WilliamAAK\Http;
use InvalidArgumentException;
/**
* A simple routing system for PHP web applications.
* A lightweight routing system for handling HTTP requests.
*
* ### Features:
* - **Parameters**: Flexible URL handling with parameters (e.g, `/user/$id`)
* - **Middleware**: Register callback functions to execute **before** and **after** route callback functions.
* - **Groups**: Organize routes under an optional prefix (e.g, `/admin`) and apply group-specific middleware.
* - **Lightweight**: A single-file, no-frills routing solution with zero dependencies.
* - 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
* // Initialize the router. This method must be called before defining any routes
* Route2::setup();
*
* // Define a GET route
* Route2::get('/home', function () {
* Route::get('/home', function () {
* echo "Welcome to the homepage!";
* });
*
* // Define a route with a parameter
* Route2::get('/user/$id', function ($id) {
* Route::get('/user/$id', function ($id) {
* echo "User ID: $id";
* });
*
* // Define a route with an optional parameter
* Route2::get('/user/$id?', function ($id = null) {
* Route::get('/user/$id?', function ($id = null) {
* echo $id ? "User ID: $id" : "No User ID provided.";
* });
*
* // Defining middlewares
* Route2::before(function () {
* Route::before(function () {
* echo "Middleware executed before the route callback.";
* });
* Route2::after(function () {
* Route::after(function () {
* echo "Middleware executed after the route callback.";
* });
*
* // Define a route with a middleware attached to it
* Route2::post('/submit', function () {
* Route::post('/submit', function () {
* echo "Form submitted!";
* }, function () {
* echo "Middleware executed before the callback.";
* });
*
* // Group routes under a common prefix
* Route2::group('/admin', function () {
* Route::group('/admin', function () {
* // Middlewares defined here will not affect the routes outside this group
* Route2::before(function () {
* Route::before(function () {
* echo "Admin Middleware executed.";
* });
* Route2::get('/dashboard', function () {
* Route::get('/dashboard', function () {
* echo "Admin Dashboard";
* });
* Route2::post('/settings', function () {
* Route::post('/settings', function () {
* echo "Admin Settings";
* });
* });
@ -67,35 +64,11 @@ use InvalidArgumentException;
* @version 1.0.0
* @author WilliamAAK
*/
class Route2
class Route
{
/**
* 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']);
}
static string $pathPrefix = '';
static array $before = [ [ ] ];
static array $after = [ [ ] ];
/**
* Retrieves the relative path of the current HTTP request.
@ -113,7 +86,13 @@ class Route2
*/
static function getRelativeRequestPath(): string
{
$requestUrlPath = strtok($_SERVER['REQUEST_URI'], '?');
static $requestUrlPath;
if (isset($requestUrlPath)) {
return $requestUrlPath;
}
$requestUrlPath = urldecode(strtok($_SERVER['REQUEST_URI'], '?'));
$scriptName = $_SERVER['SCRIPT_NAME'];
$scriptDir = dirname($scriptName) . '/';
@ -134,13 +113,12 @@ class Route2
}
/**
* Matches the current HTTP request to a defined route. If the route matches it runs the corresponding
* callback and middlewares before terminating the script.
* Matches an incoming HTTP request to a defined route and executes the callback.
*
* ### Examples:
* 1. Matching a route with parameters:
* ```php
* Route2::match('GET', '/user/$id', function ($id) {
* Route::match('GET', '/user/$id', function ($id) {
* echo "User ID: $id";
* });
* ```
@ -149,7 +127,7 @@ class Route2
*
* 2. Matching a route with optional parameters:
* ```php
* Route2::match('GET', '/user/$id?', function ($id = null) {
* Route::match('GET', '/user/$id?', function ($id = null) {
* echo $id ? "User ID: $id" : "No User ID";
* });
* ```
@ -166,21 +144,21 @@ class Route2
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)) {
if (!in_array($_SERVER['REQUEST_METHOD'], $allowedMethods)) {
return;
}
$requestParts = explode('/', self::$requestUri);
$routeParts = explode('/', self::$routePrefix . $route);
$callbackArgs = [];
$requestPathParts = explode('/', self::getRelativeRequestPath());
$routeParts = explode('/', self::$pathPrefix . $route);
$callbackArgs = [];
if (count($requestParts) !== count($routeParts)) {
if (count($requestPathParts) !== count($routeParts)) {
return;
}
for ($i = 1; $i < count($routeParts); $i++) {
$routePart = $routeParts[$i] ?? '';
$requestPart = $requestParts[$i] ?? '';
$requestPart = $requestPathParts[$i] ?? '';
if ($routePart === '') {
continue;
@ -204,13 +182,13 @@ class Route2
}
if ($requestPart !== '') {
$callbackArgs[] = $requestPart;
$callbackArgs[] = htmlspecialchars($requestPart);
}
$requestParts[$i] = $routePart;
$requestPathParts[$i] = $routePart;
}
if (implode('/', $routeParts) !== implode('/', $requestParts)) {
if (implode('/', $routeParts) !== implode('/', $requestPathParts)) {
return;
}
@ -218,7 +196,7 @@ class Route2
self::before($middleware);
}
foreach (self::$beforeMiddleware as $middlewares) {
foreach (self::$before as $middlewares) {
foreach ($middlewares as $middleware) {
$middleware();
}
@ -226,7 +204,7 @@ class Route2
$callback(...$callbackArgs);
foreach (self::$afterMiddleware as $middlewares) {
foreach (self::$after as $middlewares) {
foreach ($middlewares as $middleware) {
$middleware();
}
@ -236,75 +214,59 @@ class Route2
}
/**
* Shorthand for Route2::match('GET', ...).
* Shorthand for Route::match('GET', ...).
*/
static function get(string $route, callable $callback, ?callable $middleware = null): void
{
self::match('get', $route, $callback, $middleware);
}
{self::match('get', $route, $callback, $middleware);}
/**
* Shorthand for Route2::match('POST', ...).
* Shorthand for Route::match('POST', ...).
*/
static function post(string $route, callable $callback, ?callable $middleware = null): void
{
self::match('post', $route, $callback, $middleware);
}
{self::match('post', $route, $callback, $middleware);}
/**
* Shorthand for Route2::match('PUT', ...).
* Shorthand for Route::match('PUT', ...).
*/
static function put(string $route, callable $callback, ?callable $middleware = null): void
{
self::match('put', $route, $callback, $middleware);
}
{self::match('put', $route, $callback, $middleware);}
/**
* Shorthand for Route2::match('PATCH', ...).
* Shorthand for Route::match('PATCH', ...).
*/
static function patch(string $route, callable $callback, ?callable $middleware = null): void
{
self::match('patch', $route, $callback, $middleware);
}
{self::match('patch', $route, $callback, $middleware);}
/**
* Shorthand for Route2::match('DELETE', ...).
* Shorthand for Route::match('DELETE', ...).
*/
static function delete(string $route, callable $callback, ?callable $middleware = null): void
{
self::match('delete', $route, $callback, $middleware);
}
{self::match('delete', $route, $callback, $middleware);}
/**
* Shorthand for Route2::match('OPTIONS', ...).
* Shorthand for Route::match('OPTIONS', ...).
*/
static function options(string $route, callable $callback, ?callable $middleware = null): void
{
self::match('options', $route, $callback, $middleware);
}
{self::match('options', $route, $callback, $middleware);}
/**
* Shorthand for Route2::match('GET|POST', ...).
* Shorthand for Route::match('GET|POST', ...).
*/
static function form(string $route, callable $callback, ?callable $middleware = null): void
{
self::match('get|post', $route, $callback, $middleware);
}
{self::match('get|post', $route, $callback, $middleware);}
/**
* Shorthand for Route2::match('GET|POST|PUT|PATCH|DELETE|OPTIONS', ...).
* Shorthand for Route::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);
}
{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 () {
* Route::before(function () {
* echo "Middleware executed before the route callback.";
* });
* ```
@ -316,7 +278,7 @@ class Route2
static function before(callable $middleware): void
{
array_push(
self::$beforeMiddleware[array_key_last(self::$beforeMiddleware)],
self::$before[array_key_last(self::$before)],
$middleware
);
}
@ -326,7 +288,7 @@ class Route2
*
* ### Example:
* ```php
* Route2::after(function () {
* Route::after(function () {
* echo "Middleware executed after the route callback.";
* });
* ```
@ -338,7 +300,7 @@ class Route2
static function after(callable $middleware): void
{
array_push(
self::$afterMiddleware[array_key_last(self::$afterMiddleware)],
self::$after[array_key_last(self::$after)],
$middleware
);
}
@ -350,8 +312,8 @@ class Route2
* ### Examples:
* 1. Grouping routes under a prefix:
* ```php
* Route2::group('/admin', function () {
* Route2::get('/dashboard', function () {
* Route::group('/admin', function () {
* Route::get('/dashboard', function () {
* echo "Admin Dashboard";
* });
* });
@ -361,43 +323,43 @@ class Route2
*
* 2. Grouping middlewares and routes but this time without a prefix:
* ```php
* Route2::group(callback: function () {
* Route2::before(function () {
* Route::group(callback: function () {
* Route::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 () {
* Route::get('/somewhere', function () {
* echo "Hello from somewhere!";
* });
* });
*
* // Outputs: "I am unaffected by that group middleware."
* Route2::get('/', function () {
* Route::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.
* @param string|null $prefix The optional path 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
static function group(?string $prefix = 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 ?? ''
self::getRelativeRequestPath(),
self::$pathPrefix . $prefix ?? ''
)) return;
self::$routePrefix = self::$routePrefix . $routePrefix ?? '';
array_push(self::$beforeMiddleware, []);
array_push(self::$afterMiddleware, []);
self::$pathPrefix = self::$pathPrefix . $prefix ?? '';
array_push(self::$before, []);
array_push(self::$after, []);
$callback();
array_pop(self::$beforeMiddleware);
array_pop(self::$afterMiddleware);
self::$routePrefix = rtrim(self::$routePrefix, $routePrefix ?? '');
array_pop(self::$before);
array_pop(self::$after);
self::$pathPrefix = rtrim(self::$pathPrefix, $prefix ?? '');
}
}
}

View File

@ -1,19 +1,21 @@
{
"name": "williamaak/route2",
"description": "A simple routing system for PHP web applications.",
"type": "library",
"license": "WTFPL",
"authors": [
{
"name": "William",
"email": "54738571+WilliamAAK@users.noreply.github.com"
}
],
"minimum-stability": "stable",
"autoload": {
"psr-4": {
"WilliamAAK\\Http\\": "src/"
}
},
"require": {}
}
{
"name": "williamaak/route",
"description": " A lightweight routing system for handling HTTP requests.",
"type": "library",
"require": {
"php": ">=8.1.0"
},
"license": "WTFPL",
"authors": [
{
"name": "WilliamAAK",
"email": "54738571+WilliamAAK@users.noreply.github.com"
}
],
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"WilliamAAK\\": "./"
}
}
}