Init
This commit is contained in:
commit
23d2d69e55
13
LICENSE
Normal file
13
LICENSE
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2004 Whomst Ever
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
copies of this license document, and changing it is allowed as long
|
||||||
|
as the name is changed.
|
||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
356
README.md
Normal file
356
README.md
Normal file
@ -0,0 +1,356 @@
|
|||||||
|
# Route2 🛣️
|
||||||
|
|
||||||
|
A simple routing library for PHP web applications.
|
||||||
|
|
||||||
|
### Features:
|
||||||
|
- 🌟 **Parameters**: Flexible routing with required, optional and wildcard parameters.
|
||||||
|
- 🔍 **Constraints**: Use regex or custom functions for parameter constraints.
|
||||||
|
- 🛡️ **Middleware**: Execute logic before and after route handling.
|
||||||
|
- 🗂️ **Grouping**: Organize routes with shared functionality for cleaner code.
|
||||||
|
- 🪶 **Lightweight**: A single-file, no-frills dependency-free routing solution.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Install with composer:
|
||||||
|
|
||||||
|
composer require wilaak/route2
|
||||||
|
|
||||||
|
Or simply include it in your project:
|
||||||
|
|
||||||
|
```php
|
||||||
|
require '/path/to/Route2.php'
|
||||||
|
```
|
||||||
|
|
||||||
|
Requires PHP 8.1 or newer
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
- [Usage](#usage)
|
||||||
|
- [FrankenPHP Worker Mode](#frankenphp-worker-mode)
|
||||||
|
- [Basic Routing](#basic-routing)
|
||||||
|
- [Available Methods](#available-methods)
|
||||||
|
- [Multiple HTTP-verbs](#multiple-http-verbs)
|
||||||
|
- [Route Parameters](#route-parameters)
|
||||||
|
- [Required Parameters](#required-parameters)
|
||||||
|
- [Optional Parameters](#optional-parameters)
|
||||||
|
- [Wildcard Parameters](#wildcard-parameters)
|
||||||
|
- [Parameter Constraints](#parameter-constraints)
|
||||||
|
- [Global Constraints](#global-constraints)
|
||||||
|
- [Middleware](#middleware)
|
||||||
|
- [Registering Middleware](#registering-middleware)
|
||||||
|
- [Route Groups](#route-groups)
|
||||||
|
- [Without prefix](#without-prefix)
|
||||||
|
- [Troubleshooting](#troubleshooting)
|
||||||
|
- [Dispatching](#dispatching)
|
||||||
|
- [Accessing your routes](#accessing-your-routes)
|
||||||
|
- [Hide scriptname from URL](#hide-scriptname-from-url)
|
||||||
|
- [FrankenPHP](#frankenphp)
|
||||||
|
- [NGINX](#nginx)
|
||||||
|
- [Apache](#apache)
|
||||||
|
- [Performance](#performance)
|
||||||
|
- [Benchmark](#benchmark)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Here's a basic getting started example:
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
use Wilaak\Http\Route2;
|
||||||
|
|
||||||
|
Route2::get('/{world?}', function($world = 'World') {
|
||||||
|
echo "Hello, $world!";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Route2::dispatch()) return;
|
||||||
|
http_response_code(404);
|
||||||
|
echo '404 Not Found';
|
||||||
|
```
|
||||||
|
|
||||||
|
### FrankenPHP Worker Mode
|
||||||
|
|
||||||
|
Boot your application once and keep it in memory by using [worker mode](https://frankenphp.dev/docs/worker/). This dramatically increases performance.
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
ignore_user_abort(true);
|
||||||
|
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
use Wilaak\Http\Route2;
|
||||||
|
|
||||||
|
Route2::get('/{world?}', function($world = 'World') {
|
||||||
|
echo "Hello, $world!";
|
||||||
|
});
|
||||||
|
|
||||||
|
$handler = static function() {
|
||||||
|
if (Route2::dispatch()) return;
|
||||||
|
http_response_code(404);
|
||||||
|
echo '404 Not Found';
|
||||||
|
};
|
||||||
|
|
||||||
|
while (frankenphp_handle_request($handler)) {
|
||||||
|
gc_collect_cycles();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Routing
|
||||||
|
|
||||||
|
The most basic routes accept a URI and a closure, providing a very simple and expressive method of defining routes and behavior:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route2::get('/greeting', function () {
|
||||||
|
echo 'Hello World';
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Methods
|
||||||
|
|
||||||
|
The router allows you to register routes that respond to any HTTP verb:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route2::get($uri, $callback);
|
||||||
|
Route2::post($uri, $callback);
|
||||||
|
Route2::put($uri, $callback);
|
||||||
|
Route2::patch($uri, $callback);
|
||||||
|
Route2::delete($uri, $callback);
|
||||||
|
Route2::options($uri, $callback);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multiple HTTP-verbs
|
||||||
|
|
||||||
|
Sometimes you may need to register a route that responds to multiple HTTP-verbs. You may do so using the match method. Or, you may even register a route that responds to all HTTP verbs using the any method:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route2::match('get|post', '/', function() {
|
||||||
|
// matches any method you like
|
||||||
|
});
|
||||||
|
|
||||||
|
Route2::forms('/', function() {
|
||||||
|
// matches GET and POST methods
|
||||||
|
});
|
||||||
|
|
||||||
|
Route2::any('/', function() {
|
||||||
|
// matches any HTTP method
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Route Parameters
|
||||||
|
|
||||||
|
Sometimes you will need to capture segments of the URI within your route. For example, you may need to capture a user's ID from the URL. You may do so by defining route parameters:
|
||||||
|
|
||||||
|
**Note**: The parameters injected into the controller will always be of type `string`.
|
||||||
|
|
||||||
|
You may define as many route parameters as required by your route:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route2::get('/posts/{post}/comments/{comment}', function ($post, $comment) {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Required Parameters
|
||||||
|
|
||||||
|
These parameters must be provided or the route is skipped.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route2::get('/user/{id}', function ($id) {
|
||||||
|
echo 'User '.$id;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Optional Parameters
|
||||||
|
|
||||||
|
Specify a route parameter that may not always be present in the URI. You may do so by placing a ? mark after the parameter name.
|
||||||
|
|
||||||
|
**Note**: Make sure to give the route's corresponding variable a default value:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route2::get('/user/{name?}', function (string $name = 'John') {
|
||||||
|
echo $name;
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Wildcard Parameters
|
||||||
|
|
||||||
|
Capture the whole segment including slashes by placing a * after the parameter name.
|
||||||
|
|
||||||
|
**Note**: Make sure to give the route's corresponding variable a default value:
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route2::get('/somewhere/{any*}', function ($any = 'Empty') {
|
||||||
|
// Matches everything after the parameter
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameter Constraints
|
||||||
|
|
||||||
|
You can constrain the format of your route parameters by using the named argument `expression`, which accepts an associative array where the key is the parameter name and the value is either a regex string or a function.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route2::get('/user/{id}', function ($id) {
|
||||||
|
echo "User ID: $id";
|
||||||
|
}, expression: ['id' => '[0-9]+']);
|
||||||
|
|
||||||
|
Route2::get('/user/{id}', function ($id) {
|
||||||
|
echo "User ID: $id";
|
||||||
|
}, expression: ['id' => is_numeric(...)]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### Global Constraints
|
||||||
|
|
||||||
|
If you would like a route parameter to always be constrained by a given expression, you may use the `expression` method. Routes added after this method will inherit the expression constraints.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route2::expression([
|
||||||
|
'id' => is_numeric(...)
|
||||||
|
]);
|
||||||
|
|
||||||
|
Route2::get('/user/{id}', function ($id) {
|
||||||
|
// Only executed if {id} is numeric...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Middleware
|
||||||
|
|
||||||
|
Middleware inspect and filter HTTP requests entering your application. For instance, authentication middleware can redirect unauthenticated users to a login screen, while allowing authenticated users to proceed. Middleware can also handle tasks like logging incoming requests.
|
||||||
|
|
||||||
|
**Note**: Middleware only run if a route is found.
|
||||||
|
|
||||||
|
### Registering Middleware
|
||||||
|
|
||||||
|
You may register middleware by using the `before` and `after` methods. Routes added after this method call will inherit the middleware. You may also assign a middleware to a specific route by using the named argument `middleware`:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Runs before the route callback
|
||||||
|
Route2::before(your_middleware(...));
|
||||||
|
|
||||||
|
// Runs after the route callback
|
||||||
|
Route2::after(function() {
|
||||||
|
echo 'Terminating!';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Runs before the route but after the inherited middleware
|
||||||
|
Route2::get('/', function() {
|
||||||
|
// ...
|
||||||
|
}, middleware: fn() => print('I am also a middleware'));
|
||||||
|
```
|
||||||
|
|
||||||
|
## Route Groups
|
||||||
|
|
||||||
|
Route groups let you share attributes like middleware and expressions across multiple routes, avoiding repetition. Nested groups inherit attributes from their parent, similar to variable scopes.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Group routes under a common prefix
|
||||||
|
Route2::group('/admin', function () {
|
||||||
|
// Middleware for all routes in this group and its nested groups
|
||||||
|
Route2::before(function () {
|
||||||
|
echo "Group middleware executed.<br>";
|
||||||
|
});
|
||||||
|
Route2::get('/dashboard', function () {
|
||||||
|
echo "Admin Dashboard";
|
||||||
|
});
|
||||||
|
Route2::post('/settings', function () {
|
||||||
|
echo "Admin Settings Updated";
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
In this example, all routes within the group will share the `/admin` prefix.
|
||||||
|
|
||||||
|
### Without prefix
|
||||||
|
|
||||||
|
You may also define a group without a prefix by omitting the first argument and using the named argument `callback`.
|
||||||
|
|
||||||
|
```php
|
||||||
|
Route2::group(callback: function () {
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Dispatching
|
||||||
|
|
||||||
|
When adding routes they are not going to be executed. To perform the routing call the `dispatch` method.
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Dispatch the request
|
||||||
|
if (!Route2::dispatch()) {
|
||||||
|
http_response_code(404);
|
||||||
|
echo "404 Not Found";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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`.
|
||||||
|
|
||||||
|
## Hide scriptname from URL
|
||||||
|
|
||||||
|
Want to hide that pesky script name (e.g., `index.php`) from the URL?
|
||||||
|
|
||||||
|
### FrankenPHP
|
||||||
|
|
||||||
|
In [FrankenPHP](https://frankenphp.dev); the modern PHP app server. This behavior is enabled by default for `index.php`
|
||||||
|
|
||||||
|
### NGINX
|
||||||
|
|
||||||
|
With PHP already installed and configured you may add this to the server block of your configuration to make requests that don't match a file on your server to be redirected to `index.php`
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Apache
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
```apache
|
||||||
|
RewriteEngine on
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-d
|
||||||
|
RewriteCond %{REQUEST_FILENAME} !-f
|
||||||
|
RewriteRule ^(.*)$ index.php?q=$1 [L,QSA]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
This router is not the fastest especially when using **Classic** PHP modes as it has to compile the routing tree for each request. If performance is crucial consider something else.
|
||||||
|
|
||||||
|
### Benchmark
|
||||||
|
|
||||||
|
Here is a test running against 178 routes. See `benchmark/route2/routes.php`. The baselines are doing no routing and responding immediately.
|
||||||
|
|
||||||
|
**Note**: This is running on year 2014 level desktop shared hardware. (`Xeon E3-1226 v3`)
|
||||||
|
|
||||||
|
```
|
||||||
|
+------------------------+-----------+------------+
|
||||||
|
| Benchmark | Latency | Per Second |
|
||||||
|
+------------------------+-----------+------------+
|
||||||
|
| Baseline\Worker | 19.94ms | 20765.94 |
|
||||||
|
| Route2\Worker | 22.65ms | 18092.66 |
|
||||||
|
| Baseline\Classic | 177.44ms | 7731.38 |
|
||||||
|
| Route2\Classic | 114.87ms | 3400.63 |
|
||||||
|
+------------------------+-----------+------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
Test was done using `wrk`on the same machine:
|
||||||
|
|
||||||
|
wrk -t8 -c400 -d5s http://127.0.0.1:8080
|
||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This library is licensed under the **WTFPL-1.0**. Do whatever you want with it.
|
181
benchmark/routes.php
Normal file
181
benchmark/routes.php
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Wilaak\Http\Route2;
|
||||||
|
|
||||||
|
Route2::get("/addon/linkers", function() {});
|
||||||
|
Route2::get("/addon/linkers/{linker_key}", function() {});
|
||||||
|
Route2::get("/addon/linkers/{linker_key}/values", function() {});
|
||||||
|
Route2::get("/addon/linkers/{linker_key}/values/{value_id}", function() {});
|
||||||
|
Route2::get("/hook_events", function() {});
|
||||||
|
Route2::get("/hook_events/{subject_type}", function() {});
|
||||||
|
Route2::get("/pullrequests/{selected_user}", function() {});
|
||||||
|
Route2::get("/repositories", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/branch-restrictions", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/branch-restrictions/{id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/branching-model", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/branching-model/settings", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/approve", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/comments", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/comments/{comment_id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/properties/{app_key}/{property_name}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/pullrequests", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/reports", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/reports/{reportId}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/reports/{reportId}/annotations", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/reports/{reportId}/annotations/{annotationId}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/statuses", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/statuses/build", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commit/{commit}/statuses/build/{key}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commits", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/commits/{revision}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/components", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/components/{component_id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/default-reviewers", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/default-reviewers/{target_username}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/deploy-keys", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/deploy-keys/{key_id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/deployments/", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/deployments/{deployment_uuid}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/deployments_config/environments/{environment_uuid}/variables", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/deployments_config/environments/{environment_uuid}/variables/{variable_uuid}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/diff/{spec}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/diffstat/{spec}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/downloads", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/downloads/{filename}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/environments/", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/environments/{environment_uuid}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/environments/{environment_uuid}/changes/", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/filehistory/{commit}/{path}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/forks", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/hooks", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/hooks/{uid}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/export", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/export/{repo_name}-issues-{task_id}.zip", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/import", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/{issue_id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/{issue_id}/attachments", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/{issue_id}/attachments/{path}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/{issue_id}/changes", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/{issue_id}/changes/{change_id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/{issue_id}/comments", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/{issue_id}/comments/{comment_id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/{issue_id}/vote", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/issues/{issue_id}/watch", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/merge-base/{revspec}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/milestones", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/milestones/{milestone_id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/patch/{spec}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines-config/caches/", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines-config/caches/{cache_uuid}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines-config/caches/{cache_uuid}/content-uri", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines/", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/steps/", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/steps/{step_uuid}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/steps/{step_uuid}/log", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/steps/{step_uuid}/logs/{log_uuid}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/steps/{step_uuid}/test_reports", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/steps/{step_uuid}/test_reports/test_cases", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/steps/{step_uuid}/test_reports/test_cases/{test_case_uuid}/test_case_reasons", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines/{pipeline_uuid}/stopPipeline", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines_config", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines_config/build_number", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines_config/schedules/", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines_config/schedules/{schedule_uuid}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines_config/schedules/{schedule_uuid}/executions/", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines_config/ssh/key_pair", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines_config/ssh/known_hosts/", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines_config/ssh/known_hosts/{known_host_uuid}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines_config/variables/", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pipelines_config/variables/{variable_uuid}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/properties/{app_key}/{property_name}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/activity", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/activity", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/approve", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/comments/{comment_id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/commits", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/decline", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diff", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/diffstat", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/merge", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/merge/task-status/{task_id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/patch", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/request-changes", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pull_request_id}/statuses", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/pullrequests/{pullrequest_id}/properties/{app_key}/{property_name}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/refs", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/refs/branches", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/refs/branches/{name}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/refs/tags", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/refs/tags/{name}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/src", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/src/{commit}/{path}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/versions", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/versions/{version_id}", function() {});
|
||||||
|
Route2::get("/repositories/{workspace}/{repo_slug}/watchers", function() {});
|
||||||
|
Route2::get("/snippets", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}/comments", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}/comments/{comment_id}", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}/commits", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}/commits/{revision}", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}/files/{path}", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}/watch", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}/watchers", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}/{node_id}", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}/{node_id}/files/{path}", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}/{revision}/diff", function() {});
|
||||||
|
Route2::get("/snippets/{workspace}/{encoded_id}/{revision}/patch", function() {});
|
||||||
|
Route2::get("/teams", function() {});
|
||||||
|
Route2::get("/teams/{username}", function() {});
|
||||||
|
Route2::get("/teams/{username}/followers", function() {});
|
||||||
|
Route2::get("/teams/{username}/following", function() {});
|
||||||
|
Route2::get("/teams/{username}/members", function() {});
|
||||||
|
Route2::get("/teams/{username}/permissions", function() {});
|
||||||
|
Route2::get("/teams/{username}/permissions/repositories", function() {});
|
||||||
|
Route2::get("/teams/{username}/permissions/repositories/{repo_slug}", function() {});
|
||||||
|
Route2::get("/teams/{username}/pipelines_config/variables/", function() {});
|
||||||
|
Route2::get("/teams/{username}/pipelines_config/variables/{variable_uuid}", function() {});
|
||||||
|
Route2::get("/teams/{username}/projects/", function() {});
|
||||||
|
Route2::get("/teams/{username}/projects/{project_key}", function() {});
|
||||||
|
Route2::get("/teams/{username}/search/code", function() {});
|
||||||
|
Route2::get("/teams/{workspace}/repositories", function() {});
|
||||||
|
Route2::get("/user", function() {});
|
||||||
|
Route2::get("/user/emails", function() {});
|
||||||
|
Route2::get("/user/emails/{email}", function() {});
|
||||||
|
Route2::get("/user/permissions/repositories", function() {});
|
||||||
|
Route2::get("/user/permissions/teams", function() {});
|
||||||
|
Route2::get("/user/permissions/workspaces", function() {});
|
||||||
|
Route2::get("/users/{selected_user}", function() {});
|
||||||
|
Route2::get("/users/{selected_user}/pipelines_config/variables/", function() {});
|
||||||
|
Route2::get("/users/{selected_user}/pipelines_config/variables/{variable_uuid}", function() {});
|
||||||
|
Route2::get("/users/{selected_user}/properties/{app_key}/{property_name}", function() {});
|
||||||
|
Route2::get("/users/{selected_user}/search/code", function() {});
|
||||||
|
Route2::get("/users/{selected_user}/ssh-keys", function() {});
|
||||||
|
Route2::get("/users/{selected_user}/ssh-keys/{key_id}", function() {});
|
||||||
|
Route2::get("/users/{username}/members", function() {});
|
||||||
|
Route2::get("/users/{workspace}/repositories", function() {});
|
||||||
|
Route2::get("/workspaces", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/hooks", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/hooks/{uid}", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/members", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/members/{member}", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/permissions", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/permissions/repositories", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/permissions/repositories/{repo_slug}", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/pipelines-config/identity/oidc/.well-known/openid-configuration", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/pipelines-config/identity/oidc/keys.json", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/pipelines-config/variables", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/pipelines-config/variables/{variable_uuid}", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/projects", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/projects/{project_key}", function() {});
|
||||||
|
Route2::get("/workspaces/{workspace}/search/code", function() {});
|
19
composer.json
Normal file
19
composer.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "wilaak/route2",
|
||||||
|
"description": "A simple routing library for PHP web applications.",
|
||||||
|
"type": "library",
|
||||||
|
"license": "WTFPL",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "William",
|
||||||
|
"email": "54738571+Wilaak@users.noreply.github.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Wilaak\\Http\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require": {}
|
||||||
|
}
|
373
src/Route2.php
Normal file
373
src/Route2.php
Normal file
@ -0,0 +1,373 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Wilaak\Http;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple routing library for PHP web applications.
|
||||||
|
*
|
||||||
|
* @author Wilaak
|
||||||
|
* @license WTFPL-1.0
|
||||||
|
*/
|
||||||
|
class Route2
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Route tree structure.
|
||||||
|
*
|
||||||
|
* The tree is structured as follows:
|
||||||
|
* - Each node represents a segment of the URI.
|
||||||
|
* - Each segment can have child segments.
|
||||||
|
* - The `(int) 1337` key contains an array of child nodes.
|
||||||
|
* - Each child node contains:
|
||||||
|
* - `methods`: An array of HTTP methods (e.g., GET, POST).
|
||||||
|
* - `uri`: The URI pattern for the route.
|
||||||
|
* - `controller`: The controller function to handle the route.
|
||||||
|
* - `middleware`: An array of middleware functions to execute
|
||||||
|
*/
|
||||||
|
private static array $routeTree = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Route addition context.
|
||||||
|
*/
|
||||||
|
private static string $routeGroupPrefix = '';
|
||||||
|
private static array $middlewareStack = [];
|
||||||
|
private static array $expressionStack = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a new route to the routing tree.
|
||||||
|
*
|
||||||
|
* @param string $methods A pipe-separated list of HTTP methods (e.g., "GET|POST").
|
||||||
|
* @param string $uri The URL pattern for the route.
|
||||||
|
* Supports:
|
||||||
|
* - Required parameters `{param}`.
|
||||||
|
* - Optional parameters `{param?}`.
|
||||||
|
* - Wildcard parameters `{param*}` (matches everything after the parameter).
|
||||||
|
* @param callable $controller The controller function to handle the route.
|
||||||
|
* @param callable|null $middleware Optional middleware to execute before the controller.
|
||||||
|
* @param array $expression Optional parameter expressions:
|
||||||
|
* - An associative array where:
|
||||||
|
* - The key is the parameter name.
|
||||||
|
* - The value is either:
|
||||||
|
* - A regex string (e.g., `['id' => '[0-9]+']` to match numbers).
|
||||||
|
* - A function that validates the parameter
|
||||||
|
* (e.g., `['id' => is_numeric(...)]`).
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function match(string $methods, string $uri, callable $controller, ?callable $middleware = null, array $expression = []): void
|
||||||
|
{
|
||||||
|
$methods = explode('|', strtoupper($methods));
|
||||||
|
|
||||||
|
$middlewareStack = self::$middlewareStack;
|
||||||
|
if ($middleware) {
|
||||||
|
$middlewareStack['before'][] = $middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
$segments = preg_replace('/\{(\w+)(\?|(\*))?\}/', '*', $uri);
|
||||||
|
$segments = preg_split('/(?=\/)/', self::$routeGroupPrefix . $segments, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
|
||||||
|
$currentNode = &self::$routeTree;
|
||||||
|
foreach ($segments as $segment) {
|
||||||
|
$currentNode[$segment] ??= [];
|
||||||
|
$currentNode = &$currentNode[$segment];
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentNode[1337][] = [
|
||||||
|
'uri' => self::$routeGroupPrefix . $uri,
|
||||||
|
'methods' => $methods,
|
||||||
|
'controller' => $controller,
|
||||||
|
'middleware' => $middlewareStack,
|
||||||
|
'expression' => self::$expressionStack + $expression,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for `Route2::match('GET', ...)`
|
||||||
|
*/
|
||||||
|
public static function get(string $uri, callable $controller, ?callable $middleware = null, array $expression = []): void
|
||||||
|
{
|
||||||
|
self::match('GET', $uri, $controller, $middleware, $expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for `Route2::match('POST', ...)`
|
||||||
|
*/
|
||||||
|
public static function post(string $uri, callable $controller, ?callable $middleware = null, array $expression = []): void
|
||||||
|
{
|
||||||
|
self::match('POST', $uri, $controller, $middleware, $expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for `Route2::match('PUT', ...)`
|
||||||
|
*/
|
||||||
|
public static function put(string $uri, callable $controller, ?callable $middleware = null, array $expression = []): void
|
||||||
|
{
|
||||||
|
self::match('PUT', $uri, $controller, $middleware, $expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for `Route2::match('DELETE', ...)`
|
||||||
|
*/
|
||||||
|
public static function delete(string $uri, callable $controller, ?callable $middleware = null, array $expression = []): void
|
||||||
|
{
|
||||||
|
self::match('DELETE', $uri, $controller, $middleware, $expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for `Route2::match('PATCH', ...)`
|
||||||
|
*/
|
||||||
|
public static function patch(string $uri, callable $controller, ?callable $middleware = null, array $expression = []): void
|
||||||
|
{
|
||||||
|
self::match('PATCH', $uri, $controller, $middleware, $expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for `Route2::match('OPTIONS', ...)`
|
||||||
|
*/
|
||||||
|
public static function options(string $uri, callable $controller, ?callable $middleware = null, array $expression = []): void
|
||||||
|
{
|
||||||
|
self::match('OPTIONS', $uri, $controller, $middleware, $expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for `Route2::match('GET|POST', ...)`
|
||||||
|
*/
|
||||||
|
public static function form(string $uri, callable $controller, ?callable $middleware = null, array $expression = []): void
|
||||||
|
{
|
||||||
|
self::match('GET|POST', $uri, $controller, $middleware, $expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shorthand for `Route2::match('GET|POST|PUT|DELETE|PATCH|OPTIONS', ...)`
|
||||||
|
*/
|
||||||
|
public static function any(string $uri, callable $controller, ?callable $middleware = null, array $expression = []): void
|
||||||
|
{
|
||||||
|
self::match('GET|POST|PUT|DELETE|PATCH|OPTIONS', $uri, $controller, $middleware, $expression);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a middleware to the "before" middleware stack.
|
||||||
|
*
|
||||||
|
* Routes added after this method call will inherit the middleware.
|
||||||
|
*
|
||||||
|
* @param callable $middleware The middleware to add to the "before" stack.
|
||||||
|
* It should be a callable function or method.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function before(callable $middleware): void
|
||||||
|
{
|
||||||
|
self::$middlewareStack['before'][] = $middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a middleware to the "after" middleware stack.
|
||||||
|
*
|
||||||
|
* Routes added after this method call will inherit the middleware.
|
||||||
|
*
|
||||||
|
* @param callable $middleware The middleware to add to the "after" stack.
|
||||||
|
* It should be a callable function or method.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function after(callable $middleware): void
|
||||||
|
{
|
||||||
|
self::$middlewareStack['after'][] = $middleware;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds expressions to the expression stack.
|
||||||
|
*
|
||||||
|
* Routes added after this method call will inherit the expression constraints.
|
||||||
|
*
|
||||||
|
* @param array $expressions An associative array where:
|
||||||
|
* - The key is the parameter name (e.g., 'id').
|
||||||
|
* - The value is either:
|
||||||
|
* - A regex string (e.g., `['id' => '[0-9]+']`) to match specific patterns.
|
||||||
|
* - A callable function that validates the parameter
|
||||||
|
* (e.g., `['id' => is_numeric(...)]`).
|
||||||
|
*
|
||||||
|
* @throws InvalidArgumentException If an expression is neither a string nor a callable.
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function expression(array $expression): void
|
||||||
|
{
|
||||||
|
foreach ($expression as $param => $regex) {
|
||||||
|
if (!is_string($regex) && !is_callable($regex)) {
|
||||||
|
throw new InvalidArgumentException("Expression for parameter '{$param}' must be a regex string or a callable function");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self::$expressionStack[] = $expression;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share attributes across multiple routes
|
||||||
|
*
|
||||||
|
* Preserves and restores the previous state after the callback.
|
||||||
|
*
|
||||||
|
* @param string|null $prefix Optional prefix to be applied to all routes in the group.
|
||||||
|
* @param callable|null $callback A callback function that defines the routes within the group. Required.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function group(?string $prefix = null, ?callable $callback = null): void
|
||||||
|
{
|
||||||
|
$previousPrefix = self::$routeGroupPrefix;
|
||||||
|
$previousMiddlewareStack = self::$middlewareStack;
|
||||||
|
$previousExpressionStack = self::$expressionStack;
|
||||||
|
|
||||||
|
self::$routeGroupPrefix .= $prefix ?? '';
|
||||||
|
$callback();
|
||||||
|
|
||||||
|
self::$routeGroupPrefix = $previousPrefix;
|
||||||
|
self::$middlewareStack = $previousMiddlewareStack;
|
||||||
|
self::$expressionStack = $previousExpressionStack;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the relative URI of the current HTTP request.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* - `/index.php/myroute` → `/myroute`
|
||||||
|
*
|
||||||
|
* @return string Relative request path.
|
||||||
|
*/
|
||||||
|
public static function getRelativeRequestUri(): string
|
||||||
|
{
|
||||||
|
$requestUri = $_SERVER['REQUEST_URI'];
|
||||||
|
|
||||||
|
$scriptName = $_SERVER['SCRIPT_NAME'];
|
||||||
|
$scriptDir = dirname($scriptName) . '/';
|
||||||
|
|
||||||
|
if (str_starts_with($requestUri, $scriptName)) {
|
||||||
|
$requestUri = substr($requestUri, strlen($scriptName));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($requestUri === $scriptDir) {
|
||||||
|
$requestUri = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($requestUri === '') {
|
||||||
|
$requestUri = '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $requestUri;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches a route in the tree structure based on the URI.
|
||||||
|
*
|
||||||
|
* @param string $requestUri The URI to match against the routes.
|
||||||
|
*
|
||||||
|
* @return array Returns the matched route details or an empty array if no match is found.
|
||||||
|
*/
|
||||||
|
private static function matchRoute(string $requestUri): array
|
||||||
|
{
|
||||||
|
$segments = preg_split('/(?=\/)/', $requestUri, -1, PREG_SPLIT_NO_EMPTY);
|
||||||
|
return self::matchRouteRecursive(self::$routeTree, $segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively matches a route in the tree structure.
|
||||||
|
*
|
||||||
|
* @param array $tree The current level of the route tree.
|
||||||
|
* @param array $segments The remaining URI segments to match.
|
||||||
|
* @param bool $wildcard Indicates if a wildcard match is active.
|
||||||
|
*
|
||||||
|
* @return array Returns the matched route details or an empty array if no match is found.
|
||||||
|
*/
|
||||||
|
private static function matchRouteRecursive(array $tree, array $segments, bool $wildcard = false): array
|
||||||
|
{
|
||||||
|
if (!$segments) {
|
||||||
|
return $tree[1337] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentSegment = array_shift($segments);
|
||||||
|
|
||||||
|
foreach ([$currentSegment, '/*'] as $key) {
|
||||||
|
if (isset($tree[$key])) {
|
||||||
|
$match = self::matchRouteRecursive($tree[$key], $segments, $key === '/*');
|
||||||
|
if ($match) {
|
||||||
|
return $match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $wildcard ? ($tree[1337] ?? []) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches the request to the appropriate route.
|
||||||
|
*
|
||||||
|
* @param string|null $method The HTTP method to match against the routes. If null, uses the current request method.
|
||||||
|
* @param string|null $uri The URI to match against the routes. If null, uses the current relative request URI.
|
||||||
|
*
|
||||||
|
* @return bool True if a route was matched and dispatched, false otherwise.
|
||||||
|
*/
|
||||||
|
public static function dispatch(?string $method = null, ?string $uri = null): bool
|
||||||
|
{
|
||||||
|
$method = strtoupper($method ?? $_SERVER['REQUEST_METHOD']);
|
||||||
|
$uri = rawurldecode($uri = strtok($uri ?? self::getRelativeRequestUri(), '?'));
|
||||||
|
|
||||||
|
$routes = self::matchRoute($uri);
|
||||||
|
|
||||||
|
foreach ($routes as $route) {
|
||||||
|
if (!in_array($method, $route['methods'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$pattern = preg_replace(
|
||||||
|
['/\{(\w+)\}/', '/\{(\w+)\?\}/', '/\{(\w+)\*\}/'],
|
||||||
|
['(?P<$1>[^/]+)', '(?P<$1>[^/]*)', '(?P<$1>.*)'],
|
||||||
|
$route['uri']
|
||||||
|
);
|
||||||
|
$pattern = '#^' . $pattern . '$#';
|
||||||
|
|
||||||
|
if (!preg_match($pattern, $uri, $matches)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($matches as $key => $value) {
|
||||||
|
if (is_string($key) && !empty($value)) {
|
||||||
|
$params[$key] = $value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($route['expression'] as $param => $expression) {
|
||||||
|
if (!isset($params[$param])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_callable($expression) && !$expression($params[$param])) {
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($expression) && !preg_match('#^' . $expression . '$#', $params[$param])) {
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_string($expression) && !is_callable($expression)) {
|
||||||
|
throw new InvalidArgumentException(
|
||||||
|
"Expression for parameter '{$param}' must be a regex string or callable function"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($route['middleware']['before'] ?? [] as $middleware) {
|
||||||
|
$middleware();
|
||||||
|
}
|
||||||
|
|
||||||
|
$route['controller'](...$params ?? []);
|
||||||
|
|
||||||
|
foreach ($route['middleware']['after'] ?? [] as $middleware) {
|
||||||
|
$middleware();
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user