diff --git a/.gitignore b/.gitignore
index e00353f..283692e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,7 @@
# VSCode
.vscode/*
-tools/HttpExceptionGeneration/HttpExceptions.md
\ No newline at end of file
+tools/HttpExceptionGeneration/HttpExceptions.md
+
+index.php
+tools/HttpExceptions/HttpExceptions.md
\ No newline at end of file
diff --git a/README.md b/README.md
index 5d59c12..d71558b 100644
--- a/README.md
+++ b/README.md
@@ -4,8 +4,9 @@ Totally Not Another PHP Framework's Route Component
# Table of Contents
+- [Tnapf/Router](#tnapfrouter)
+- [Table of Contents](#table-of-contents)
- [Installation](#installation)
-- [Basic Usage](#basic-usage)
- [Routing](#routing)
- [Routing Shorthands](#routing-shorthands)
- [Route Patterns](#route-patterns)
@@ -13,8 +14,6 @@ Totally Not Another PHP Framework's Route Component
- [Dynamic Placeholder-based Route Patterns](#dynamic-placeholder-based-route-patterns)
- [Dynamic PCRE-based Route Patterns](#dynamic-pcre-based-route-patterns)
- [Controllers](#controllers)
- - [Anonymous Function Controller](#anonymous-function-controller)
- - [Class Controller](#class-controller)
- [Template Engine Integration](#template-engine-integration)
- [Responding to requests](#responding-to-requests)
- [Catchable Routes](#catchable-routes)
@@ -23,9 +22,8 @@ Totally Not Another PHP Framework's Route Component
- [Custom Catchables](#custom-catchables)
- [Available HttpExceptions](#available-httpexceptions)
- [Middleware](#middleware)
- - [Before Middleware](#before-middleware)
- - [After Middleware](#after-middleware)
-- [Mounting routes (Group routes)](#mounting-routes-group-routes)
+- [Postware](#postware)
+- [Group routes](#group-routes)
# Installation
@@ -33,51 +31,12 @@ Totally Not Another PHP Framework's Route Component
composer require tnapf/router
```
-# Basic Usage
-
-```php
-username, $users)) {
- throw new HttpNotFound($req);
- }
-
- return new TextResponse("Viewing {$args->username}'s Profile");
-})->setParameter("username", "[a-zA-Z_]+");
-
-Router::catch(HttpNotFound::class, function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args): ResponseInterface {
- return new TextResponse("{$args->username} is not registered!");
-}, "/user/{username}")->setParameter("username", "[a-zA-Z_]+");
-
-Router::catch(HttpInternalServerError::class, function () {
- return new TextResponse("An internal server error has occurred");
-});
-
-Router::catch(HttpNotFound::class, function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args): ResponseInterface {
- return new TextResponse("{$req->getRequestTarget()} is not a valid URI");
-});
-
-Router::run();
-```
-
# Routing
You can manually create a route and then store it with the addRoute method
```php
-$route = new Route("pattern", function() { /* ... */ }, Methods::GET);
+$route = new Route("pattern", RequestHandlerInterfaceImplementation::class, Methods::GET);
Router::addRoute($route);
```
@@ -87,7 +46,7 @@ If you want the same controller to be used for multiple methods you can do the f
```php
use Tnapf\Router\Enums\Methods;
-$route = new Route("pattern", function() { /* ... */ }, Methods::GET, Methods::POST, Methods::HEAD, ...);
+$route = new Route("pattern", RequestHandlerInterfaceImplementation::class, Methods::GET, Methods::POST, Methods::HEAD, ...);
Router::addRoute($route);
```
@@ -97,19 +56,19 @@ Router::addRoute($route);
Shorthands for single request methods are provided
```php
-Router::get('pattern', function() { /* ... */ });
-Router::post('pattern', function() { /* ... */ });
-Router::put('pattern', function() { /* ... */ });
-Router::delete('pattern', function() { /* ... */ });
-Router::options('pattern', function() { /* ... */ });
-Router::patch('pattern', function() { /* ... */ });
-Router::head('pattern', function() { /* ... */ });
+Router::get('pattern', RequestHandlerInterfaceImplementation::class);
+Router::post('pattern', RequestHandlerInterfaceImplementation::class);
+Router::put('pattern', RequestHandlerInterfaceImplementation::class);
+Router::delete('pattern', RequestHandlerInterfaceImplementation::class);
+Router::options('pattern', RequestHandlerInterfaceImplementation::class);
+Router::patch('pattern', RequestHandlerInterfaceImplementation::class);
+Router::head('pattern', RequestHandlerInterfaceImplementation::class);
```
You can use this shorthand for a route that can be accessed using any method:
```php
-Router::all('pattern', function() { /* ... */ });
+Router::all('pattern', RequestHandlerInterfaceImplementation::class);
```
# Route Patterns
@@ -131,10 +90,15 @@ Examples:
Usage Examples:
```php
-Router::get('/about', function(ServerRequestInterface $req, ResponseInterface $res) {
- $res->getBody()->write("Hello World");
- return $res;
-});
+class AboutController implements RequestHandlerInterface {
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Next $next): ResponseInterface
+ {
+ $response->getBody()->write("Hello World");
+ return $response;
+ }
+}
+
+Router::get('/about', AboutController::class);
```
## Dynamic Placeholder-based Route Patterns
@@ -152,10 +116,15 @@ Examples:
Placeholders are easier to use than PRCEs, but offer you less control as they internally get translated to a PRCE that matches any character (`.*`).
```php
-Router::get('/movies/{movieId}/photos/{photoId}', function(ServerRequestInterface $req, ResponseInterface $res, stdClass $args) {
- $res->getBody()->write('Movie #'.$args->movieId.', photo #'.$args->photoId);
- return $res;
-});
+class MovieController implements \Tnapf\Router\Interfaces\RequestHandlerInterface {
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Next $next): ResponseInterface
+ {
+ $res->getBody()->write('Movie #'.$args->movieId.', photo #'.$args->photoId);
+ return $res;
+ }
+}
+
+Router::get('/movies/{movieId}/photos/{photoId}', MovieController::class);
```
## Dynamic PCRE-based Route Patterns
@@ -175,54 +144,33 @@ Note: The [PHP PCRE Cheat Sheet](https://courses.cs.washington.edu/courses/cse15
The __subpatterns__ defined in Dynamic PCRE-based Route Patterns are passed into the route's controller like __dynamic placeholders__.
```php
-Router::get('/hello/{name}', function(ServerRequestInterface $req, ResponseInterface $res, stdClass $args) {
- $res->getBody()->write('Hello '.htmlentities($args->name));
- return $res;
-})->setParameter("name", "\w+");
-```
-
-Note: The leading `/` at the very beginning of a route pattern is not mandatory, but is recommended.
-
-When multiple subpatterns are defined, the resulting __route handling parameters__ are passed into the route handling function in the order they are defined:
+class HelloController implements \Tnapf\Router\Interfaces\RequestHandlerInterface {
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Next $next): ResponseInterface
+ {
+ $response->getBody()->write('Hello '.htmlentities($args->name));
+ return $response;
+ }
+}
-```php
-Router::get('/movies/{movieId}/photos/{photoId}', function(ServerRequestInterface $req, ServerResponseInterface $res, stdClass $args) {
- $res->getBody()->write('Movie #'.$args->movieId.', photo #'.$args->photoId);
- return $res;
-})->setParameter("movieId", "\d+")->setParameter("photoId", "\d+");
+Router::get('/hello/{name}', HelloController::class)->setParameter("name", "\w+");
```
-
+]
# Controllers
-When defining a route you can either pass an anonymous function or an array that contains a class along with a static method to invoke. Additionally, your controller must return an implementation of the PSR7 Response Interface
-
-## Anonymous Function Controller
+When defining a route you pass a string of a class that implements `Tnapf\Router\Interfaces\RequestHandlerInterface`
```php
-Router::get("/home", function (ServerRequestInterface $req, ResponseInterface $res) {
- $res->getBody()->write("Welcome home!");
- return $res;
-});
-```
-
-## Class Controller
-
-Create a class with a static method to handle the route
-
-```php
-class HomeController extends Controller {
- public static function handle(ServerRequestInterface $req, ResponseInterface $res, stdClass $args): ResponseInterface
+class HomeController implements \Tnapf\Router\Interfaces\RequestHandlerInterface {
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Next $next): ResponseInterface
{
- $res->getBody()->write("Welcome home!");
- return $res;
+ $response->getBody()->write('Welcome Home!');
+ return $response;
}
}
```
-Then insert the class string into an array the first key is the class string and the second is the name of the method
-
```php
-Router::get("/home", [HomeController::class, "handle"]);
+Router::get("/home", HomeController::class);
```
# Template Engine Integration
@@ -237,13 +185,21 @@ $env->twig = new Environment(new \Twig\Loader\FilesystemLoader("/path/to/views")
// ...
-Router::get("/home", function (ServerRequestInterface $req, ResponseInterface $res): ResponseInterface {
- return new HtmlResponse($env->get("twig")->render("home.html"));
-});
+class HomeController implements \Tnapf\Router\Interfaces\RequestHandlerInterface {
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Next $next): ResponseInterface
+ {
+ return new HtmlResponse($env->get("twig")->render("home.html"));
+ return $response;
+ }
+}
+
+// ...
+
+Router::get("/home", HomeController::class);
```
# Responding to requests
-All controllers **MUST** return an implementation of `\Psr\Http\Message\ResponseInterface`. You can use the premade response object passed into the controller *or* instantiate your own. I recommend taking a look at [HttpSoft/Response](https://httpsoft.org/docs/response/v1/#usage) for prebuilt response types. This is also included with the route as it's used for the dev mode
+All controllers **MUST** return an implementation of `\Psr\Http\Message\ResponseInterface`. You can use the premade response object passed into the controller *or* instantiate your own. I recommend taking a look at [HttpSoft/Response](https://httpsoft.org/docs/response/v1/#usage) for prebuilt response types.
```php
$response = new HttpSoft\Response\HtmlResponse('
HTML
');
$response = new HttpSoft\Response\JsonResponse(['key' => 'value']);
@@ -260,30 +216,61 @@ Catchable routes are routes that are only invoked when exceptions are thrown whi
## Catching
```php
-Router::catch(HttpException::class, function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args) {
- return new TextResponse("An internal server error has occurred");
-});
+class CatchInternalServerError implements \Tnapf\Router\Interfaces\RequestHandlerInterface
+{
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, ?Next $next = null): ResponseInterface
+ {
+ $logs = fopen("./error.log", "w+");
+ fwrite($logs, $args->exception->__toString());
+ fclose($logs);
+
+ return HttpInternalServerError::buildHtmlResponse();
+ }
+}
+
+Router::catch(HttpInternalServerError::class, CatchInternalServerError::class);
```
-*Note that `$args->exception` will be the exception throw*
+*Note that `$args->exception` will be the exception thrown*
## Specific URI's
```php
-Router::catch(HttpNotFound::class, function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args): ResponseInterface {
- return new TextResponse("{$req->getRequestTarget()} is not a valid URI");
-}, "/users/{username}");
+class CatchInternalServerError implements \Tnapf\Router\Interfaces\RequestHandlerInterface {
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, ?Next $next = null): ResponseInterface
+ {
+ return new TextResponse("{$req->getRequestTarget()} is not a valid URI");
+ }
+}
+
+
+Router::catch(HttpNotFound::class, CatchInternalServerError::class, "/users/{username}");
```
-**Note: Catchers are treated just like routes meaning they can have custom parameters as shown in [Basic Usage](#basic-usage)**
+**Note: Catchers are treated just like routes meaning they can have custom parameters**
## Custom Catchables
-By default, you can only catch the exceptions shown below but let's say you make a custom exception named `UserNotFound` and want to have a custom response emitted when it's thrown...well you can...
+By default, you can only catch the exceptions shown below but you can create a custom exception by implementing `Tnapf\Router\Exceptions\HttpException`
```php
-Router::makeCatchable(UserNotFound::class)
-```
+class UserNotFound extends \Tnapf\Router\Exceptions\HttpException {
+ public const CODE = 403;
+ public const PHRASE = "User Not Found";
+ public const DESCRIPTION = "Indicates that the information provided does not link to an existing user.";
+ public const HREF = "https://docs.example.com/errors/UserNotFound";
+}
-and then catch it like a regular HttpException.
+class CatchUserNotFound extends \Tnapf\Router\Interfaces\RequestHandlerInterface {
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, ?Next $next = null): ResponseInterface
+ {
+ return new JsonResponse([
+ "errors" => ["UserNotFound"],
+ "data" => [/* a dump of the data received in the request */]
+ ]);
+ }
+}
+
+Router::catch(UserNotFound::class, CatchUserNotFound::class);
+```
## Available HttpExceptions
@@ -307,7 +294,7 @@ and then catch it like a regular HttpException.
|415|[Unsupported Media Type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415)|[HttpUnsupportedMediaType](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpUnsupportedMediaType.php)
|416|[Range Not Satisfiable](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416)|[HttpRangeNotSatisfiable](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpRangeNotSatisfiable.php)
|417|[Expectation Failed](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417)|[HttpExpectationFailed](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpExpectationFailed.php)
-|418|[I'm a teapot](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418)|[HttpImateapot](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpImateapot.php)
+|418|[I'm A Teapot](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418)|[HttpImATeapot](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpImATeapot.php)
|422|[Unprocessable Entity](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422)|[HttpUnprocessableEntity](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpUnprocessableEntity.php)
|423|[Locked](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/423)|[HttpLocked](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpLocked.php)
|424|[Failed Dependency](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/424)|[HttpFailedDependency](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpFailedDependency.php)
@@ -321,115 +308,112 @@ and then catch it like a regular HttpException.
|502|[Bad Gateway](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502)|[HttpBadGateway](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpBadGateway.php)
|503|[Service Unavailable](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503)|[HttpServiceUnavailable](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpServiceUnavailable.php)
|504|[Gateway Time-out](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504)|[HttpGatewayTimeout](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpGatewayTimeout.php)
-|505|[HTTP Version Not Supported](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/505)|[HttpHTTPVersionNotSupported](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpHTTPVersionNotSupported.php)
+|505|[Version Not Supported](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/505)|[HttpVersionNotSupported](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpVersionNotSupported.php)
|506|[Variant Also Negotiates](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/506)|[HttpVariantAlsoNegotiates](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpVariantAlsoNegotiates.php)
|507|[Insufficient Storage](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/507)|[HttpInsufficientStorage](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpInsufficientStorage.php)
|511|[Network Authentication Required](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/511)|[HttpNetworkAuthenticationRequired](https://github.com/tnapf/Router/blob/main/src/Exceptions/HttpNetworkAuthenticationRequired.php)
# Middleware
-Middleware is software that connects the model and view in an MVC application, facilitating the communication and data flow between these two components while also providing a layer of abstraction, decoupling the model and view and allowing them to interact without needing to know the details of how the other component operates.
-
-A good example is having before middleware that makes sure the user is an administrator before they go to a restricted page. You could do this in your routes controller for every admin page but that would be redundant. Or for after middleware, you may have a REST API that returns a JSON response. You can have after middleware to make sure the JSON response isn't malformed.
+Middleware is part of the request handling process that comes before the route controller is invoked.
-## Before Middleware
+A good example of middleware is making sure the user is an administrator before they go to a restricted page. You could do this in your routes controller for every admin page sure but, that would be redundant.
-You can add middleware to a route by invoking the before method and supply controller(s).
+You can add middleware to a route by invoking the addMiddleware method and supply controller(s).
**NOTE: The controllers will be invoked in the order they're appended!**
```php
-Router::get("/user/{username}", function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args): ResponseInterface {
- $users = ["cmdstr", "realdiegopoptart"];
+class UserExists implements RequestHandlerInterface
+{
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Next $next): ResponseInterface
+ {
+ $users = ["command_string", "realdiegopoptart"];
- if (!in_array($args->username, $users)) {
- throw new HttpNotFound($req);
- }
+ if (!in_array(strtolower($args->username), $users)) {
+ throw new HttpNotFound($request);
+ }
- $res->getBody()->write(" {$args->username}'s profile");
+ return $next->next($request, $response, $args);
+ }
+}
- return $res;
-})->setParameter("username", "[a-zA-Z_]+")->before(function (ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Closure $next): ResponseInterface
+class Handler implements RequestHandlerInterface
{
- $res = new TextResponse("Viewing");
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Next $next): ResponseInterface
+ {
+ $response = $response->withAddedHeader("content-type", "text/plain");
+ $response->getBody()->write("You're viewing {$args->username}'s profile!");
- $res->getBody()->seek(0, SEEK_END);
+ return $response;
+ }
+}
- return $next($res); // will go to the next part of middleware
-});
-```
+Router::get("/profile/{username}", Handler::class)->addMiddleware(UserExists::class)->setParameter("username", "[a-zA-Z_ 0-9]+");
-*Note: If you don't want to proceed to the next part of middleware just return a `ResponseInterface` instead of passing response to the `$next` closure*
+Router::emitHttpExceptions(Router::EMIT_HTML_RESPONSE);
+Router::run();
+```
-You can also include a class string just like the controller
+*Note: If you don't want to proceed to the next part of middleware just return a `ResponseInterface` instead of invoking `Next::Next`
-*Special note about middleware, you can pass variables from beforeMiddleware to the main route or from the main route to afterMiddleware by supplying it as the second argument in the next closure. These variables will be added as an additional argument in the next piece of middleware*
+# Postware
-## After Middleware
+Postware is a type of middleware that operates on the response generated by the Controller and can modify the response data before it is sent to the client. While it doesn't sit between the Controller and the View, it does operate on the response after the View has been generated.
-Adding after middleware is just like before middleware, just with a different method.
+Adding postware is just like middleware, just with a different method.
```php
-Router::get("/user/{username}", function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args, Closure $next): ResponseInterface {
- $users = ["cmdstr", "realdiegopoptart"];
+class UserExists implements RequestHandlerInterface
+{
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Next $next): ResponseInterface
+ {
+ $users = ["command_string", "realdiegopoptart"];
+
+ if (!in_array(strtolower($args->username), $users)) {
+ throw new HttpNotFound($request);
+ }
- if (!in_array($args->username, $users)) {
- throw new HttpNotFound($req);
+ return $next->next($request, $response, $args);
}
+}
- $res = new TextResponse("Viewing {$args->username}'s ");
+class Handler implements RequestHandlerInterface
+{
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Next $next): ResponseInterface
+ {
+ $response = $response->withAddedHeader("content-type", "text/plain");
+ $response->getBody()->write("You're viewing ");
- $res->getBody()->seek(0, SEEK_END);
+ return $next->next($request, $response, $args);
+ }
+}
- return $next($res);
-})->setParameter("username", "[a-zA-Z_]+")->after(function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args): ResponseInterface
-{
- $res->getBody()->write("profile");
+class AppendUsername implements RequestHandlerInterface {
+ public static function handle(ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Next $next): ResponseInterface
+ {
+ $response->getBody()->write("{$args->username}'s profile");
+
+ return $response;
+ }
+}
- return $res;
-});
+Router::get("/profile/{username}", Handler::class)->addMiddleware(UserExists::class)->addPostware(AppendUsername::class)->setParameter("username", "[a-zA-Z_ 0-9]+");
+
+Router::emitHttpExceptions(Router::EMIT_HTML_RESPONSE);
+Router::run();
```
-# Mounting routes (Group routes)
+# Group routes
-If you would multiple routes to inherit the same prefix URI and before/after middleware then you can define them inside the mount method...
+If you would multiple routes to inherit the same prefix URI and before/after middleware then you can define them inside the group method...
```php
-Router::mount("/app", function () {
- Router::get("/", function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args, int $userId) {
- return new TextResponse("Welcome Home!\nUserId: $userId");
- });
-
- Router::get("/channel/{channel}", function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args, int $userId) {
- return new TextResponse("Looking at channelId {$args->channel} as UserId $userId");
- })->setParameter("channel", "\d+");
-
- // You can now mount inside of a mount
- Router::mount("/api", function () {
- Router::get("/channel/{channel}", function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args, int $userId) {
- return new JsonResponse(["Looking at channelId {$args->channel} as UserId $userId"]);
- })->setParameter("channel", "\d+");
-
- Router::catch(HttpNotFound::class, function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args) {
- return new JsonResponse(["Channel id {$args->channel} is invalid"]);
- }, "/channel/{channel}")->setParameter("channel", "[a-zA-Z]+");
-
- Router::catch(HttpNotFound::class, function () {
- return new JsonResponse(["Api Endpoint Not Found"]);
- });
- }, [ /* added to the end of already established before/after middleware */ ]);
-
- Router::catch(HttpNotFound::class, function () {
- return new TextResponse("App Route Not Found");
- });
-
- Router::catch(HttpNotFound::class, function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args) {
- return new TextResponse("Channel id {$args->channel} is invalid");
- }, "/channel/{channel}")->setParameter("channel", "[a-zA-Z]+");
-}, [ // before middleware here
- function (ServerRequestInterface $request, ResponseInterface $response, stdClass $args, ?Closure $next = null): ResponseInterface
- {
- return $next($response, mt_rand(0, 100));
- }
-], [ /* after middleware here */ ]);
+Router::group("/app", function () {
+ // Routes that will after /app prepending their declared uri
+
+ Router::group("/api", function () {
+ // Routes will have /app/api prepending their declared uri
+ },);
+}, [ /* middleware here */ ], [ /* postware here */ ]);
```
diff --git a/composer.json b/composer.json
index 2295b92..f742426 100644
--- a/composer.json
+++ b/composer.json
@@ -15,10 +15,19 @@
}
],
"require": {
- "httpsoft/http-response": "^1.0",
"httpsoft/http-server-request": "^1.0",
"php": ">=8.1",
"commandstring/utils": "^1.4",
- "httpsoft/http-emitter": "^1.0"
+ "httpsoft/http-emitter": "^1.0",
+ "httpsoft/http-response": "^1.0"
+ },
+ "require-dev": {
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "scripts": {
+ "generate-exception-docs": "php tools/HttpExceptions/GenerateDocs.php",
+ "generate-exception-classes": "php tools/HttpExceptions/GenerateClasses.php",
+ "cs": "./vendor/bin/phpcs ./src ./examples --standard=psr12",
+ "csf": "./vendor/bin/phpcbf ./src ./examples --standard=psr12"
}
}
diff --git a/composer.lock b/composer.lock
index 5c8642b..e293015 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "a16db080b51304861df27441b837a4c5",
+ "content-hash": "8fc62dea589708ca03b8376e6fe27f06",
"packages": [
{
"name": "commandstring/utils",
@@ -391,7 +391,65 @@
"time": "2016-08-06T14:39:51+00:00"
}
],
- "packages-dev": [],
+ "packages-dev": [
+ {
+ "name": "squizlabs/php_codesniffer",
+ "version": "3.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/squizlabs/PHP_CodeSniffer.git",
+ "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/ed8e00df0a83aa96acf703f8c2979ff33341f879",
+ "reference": "ed8e00df0a83aa96acf703f8c2979ff33341f879",
+ "shasum": ""
+ },
+ "require": {
+ "ext-simplexml": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0"
+ },
+ "bin": [
+ "bin/phpcs",
+ "bin/phpcbf"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Greg Sherwood",
+ "role": "lead"
+ }
+ ],
+ "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.",
+ "homepage": "https://github.com/squizlabs/PHP_CodeSniffer",
+ "keywords": [
+ "phpcs",
+ "standards",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues",
+ "source": "https://github.com/squizlabs/PHP_CodeSniffer",
+ "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki"
+ },
+ "time": "2023-02-22T23:07:41+00:00"
+ }
+ ],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
diff --git a/examples/HttpCodesApi.php b/examples/HttpCodesApi.php
deleted file mode 100644
index 50bf522..0000000
--- a/examples/HttpCodesApi.php
+++ /dev/null
@@ -1,75 +0,0 @@
-code === $code->code) {
- if ($args->type === "json") {
- return new JsonResponse($code);
- } else {
- $html = <<
-
-
- {$code->code} - {$code->phrase}
-
-
-
-
-
-
-
{$code->description}
-
-
-
- TEMPLATE;
-
- return new HtmlResponse($html);
- }
- }
- }
-
- return throw new HttpNotFound($req);
-})->setParameter("code", "[0-9]{3}")->setParameter("type", "json|html");
-
-Router::get("/", function () {
- return new JsonResponse(getCodes());
-});
-
-Router::catch(HttpInternalServerError::class, function (ServerRequestInterface $req, ResponseInterface $res, stdClass $args) {
- throw $args->exception;
-});
-
-Router::emitHttpExceptions(Router::EMIT_JSON_RESPONSE);
-Router::run();
\ No newline at end of file
diff --git a/examples/HttpCodesApi/GetCode.php b/examples/HttpCodesApi/GetCode.php
new file mode 100644
index 0000000..0e371d7
--- /dev/null
+++ b/examples/HttpCodesApi/GetCode.php
@@ -0,0 +1,42 @@
+code === $code->code) {
+ if ($args->type === "json") {
+ return new JsonResponse($code);
+ }
+
+ ob_start();
+ require __DIR__ . "/HtmlResponse.php";
+ $html = ob_get_clean();
+
+ return new HtmlResponse($html);
+ }
+ }
+
+ return throw new HttpNotFound($request);
+ }
+
+ public static function getCodes()
+ {
+ return json_decode(file_get_contents(__DIR__ . "/../../tools/HttpExceptions/HttpCodes.json"));
+ }
+}
diff --git a/examples/HttpCodesApi/HtmlResponse.php b/examples/HttpCodesApi/HtmlResponse.php
new file mode 100644
index 0000000..9f46e0f
--- /dev/null
+++ b/examples/HttpCodesApi/HtmlResponse.php
@@ -0,0 +1,31 @@
+
+
+
+ = $code->code ?> - = $code->phrase ?>
+
+
+
+
+
+
+
= $code->description ?>
+
+
+
diff --git a/examples/HttpCodesApi/ListCodes.php b/examples/HttpCodesApi/ListCodes.php
new file mode 100644
index 0000000..5b26125
--- /dev/null
+++ b/examples/HttpCodesApi/ListCodes.php
@@ -0,0 +1,21 @@
+setParameter("code", "[0-9]{3}")
+ ->setParameter("type", "json|html")
+;
+
+Router::get("/", ListCodes::class);
+
+Router::emitHttpExceptions(Router::EMIT_HTML_RESPONSE);
+
+Router::run();
diff --git a/src/Enums/Methods.php b/src/Enums/Methods.php
index be3d39d..ca9f992 100644
--- a/src/Enums/Methods.php
+++ b/src/Enums/Methods.php
@@ -11,4 +11,4 @@ enum Methods: string
case OPTIONS = "OPTIONS";
case HEAD = "HEAD";
case PATCH = "PATCH";
-}
\ No newline at end of file
+}
diff --git a/src/Exceptions/HttpBadGateway.php b/src/Exceptions/HttpBadGateway.php
index 87055ed..13f3dfd 100644
--- a/src/Exceptions/HttpBadGateway.php
+++ b/src/Exceptions/HttpBadGateway.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpBadGateway extends HttpException {
+class HttpBadGateway extends HttpException
+{
public const CODE = 502;
public const PHRASE = "Bad Gateway";
public const DESCRIPTION = "Indicates that the server, while acting as a gateway or proxy, received an invalid response from an inbound server it accessed while attempting to fulfill the request.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502";
}
-
diff --git a/src/Exceptions/HttpBadRequest.php b/src/Exceptions/HttpBadRequest.php
index 6cc4b0f..ee3e31f 100644
--- a/src/Exceptions/HttpBadRequest.php
+++ b/src/Exceptions/HttpBadRequest.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpBadRequest extends HttpException {
+class HttpBadRequest extends HttpException
+{
public const CODE = 400;
public const PHRASE = "Bad Request";
public const DESCRIPTION = "Indicates that the server cannot or will not process the request because the received syntax is invalid, nonsensical, or exceeds some limitation on what the server is willing to process.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400";
}
-
diff --git a/src/Exceptions/HttpConflict.php b/src/Exceptions/HttpConflict.php
index 1c0e144..1117b1e 100644
--- a/src/Exceptions/HttpConflict.php
+++ b/src/Exceptions/HttpConflict.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpConflict extends HttpException {
+class HttpConflict extends HttpException
+{
public const CODE = 409;
public const PHRASE = "Conflict";
public const DESCRIPTION = "Indicates that the request could not be completed due to a conflict with the current state of the resource.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409";
}
-
diff --git a/src/Exceptions/HttpException.php b/src/Exceptions/HttpException.php
index 64e3ea6..8dc4a79 100644
--- a/src/Exceptions/HttpException.php
+++ b/src/Exceptions/HttpException.php
@@ -6,15 +6,18 @@
use HttpSoft\Response\HtmlResponse;
use HttpSoft\Response\JsonResponse;
use Psr\Http\Message\ServerRequestInterface;
+use RuntimeException;
-abstract class HttpException extends \Exception {
- public const CODE = 0;
+abstract class HttpException extends \Exception
+{
+ public const CODE = 500;
public const DESCRIPTION = "";
public const PHRASE = "";
public const HREF = "";
- public function __construct(public readonly ServerRequestInterface $request) {
- parent::__construct(static::DESCRIPTION." ".static::HREF, static::CODE);
+ public function __construct(public readonly ServerRequestInterface $request)
+ {
+ parent::__construct(static::DESCRIPTION . " " . static::HREF, static::CODE);
}
public static function buildEmptyResponse(): EmptyResponse
@@ -29,39 +32,19 @@ public static function buildHtmlResponse(): HtmlResponse
$phrase = static::PHRASE;
$href = static::HREF;
- $html = <<
-
-
- {$code} - {$phrase}
-
-
-
-
-
-
- TEMPLATE;
+ if (empty($phrase)) {
+ throw new RuntimeException("Phrase constant is not defined.");
+ }
+
+ if (!strlen($description)) {
+ throw new RuntimeException("Description constant defined.");
+ }
+
+ $title = "{$code} - {$phrase}";
+
+ ob_start();
+ include_once __DIR__ . "/HttpExceptionHtmlResponse.php";
+ $html = ob_get_clean();
return new HtmlResponse($html, static::CODE);
}
@@ -73,6 +56,24 @@ public static function buildJsonResponse(): JsonResponse
$phrase = static::PHRASE;
$href = static::HREF;
- return new JsonResponse(compact("code", "description", "phrase", "href"), static::CODE);
+ if (!strlen($phrase)) {
+ throw new RuntimeException("Phrase constant is not defined.");
+ }
+
+ if (!strlen($description)) {
+ throw new RuntimeException("Description constant defined.");
+ }
+
+ $json = compact("description", "phrase");
+
+ if ($code) {
+ $json["code"] = $code;
+ }
+
+ if (strlen($href)) {
+ $json["href"] = $href;
+ }
+
+ return new JsonResponse($json, static::CODE);
}
-}
\ No newline at end of file
+}
diff --git a/src/Exceptions/HttpExceptionHtmlResponse.php b/src/Exceptions/HttpExceptionHtmlResponse.php
new file mode 100644
index 0000000..964cc1d
--- /dev/null
+++ b/src/Exceptions/HttpExceptionHtmlResponse.php
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+ = $code ?> - = !empty($href) ? "{$phrase}" : $phrase; ?>
+
+
+
= $description ?>
+
+
+
+
diff --git a/src/Exceptions/HttpExpectationFailed.php b/src/Exceptions/HttpExpectationFailed.php
index 19e5c6f..3f33140 100644
--- a/src/Exceptions/HttpExpectationFailed.php
+++ b/src/Exceptions/HttpExpectationFailed.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpExpectationFailed extends HttpException {
+class HttpExpectationFailed extends HttpException
+{
public const CODE = 417;
public const PHRASE = "Expectation Failed";
public const DESCRIPTION = "Indicates that the expectation given in the request's Expect header field could not be met by at least one of the inbound servers.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417";
}
-
diff --git a/src/Exceptions/HttpFailedDependency.php b/src/Exceptions/HttpFailedDependency.php
index 5a83eb2..a99e43f 100644
--- a/src/Exceptions/HttpFailedDependency.php
+++ b/src/Exceptions/HttpFailedDependency.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpFailedDependency extends HttpException {
+class HttpFailedDependency extends HttpException
+{
public const CODE = 424;
public const PHRASE = "Failed Dependency";
public const DESCRIPTION = "Means that the method could not be performed on the resource because the requested action depended on another action and that action failed.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/424";
}
-
diff --git a/src/Exceptions/HttpForbidden.php b/src/Exceptions/HttpForbidden.php
index f313c57..0deafed 100644
--- a/src/Exceptions/HttpForbidden.php
+++ b/src/Exceptions/HttpForbidden.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpForbidden extends HttpException {
+class HttpForbidden extends HttpException
+{
public const CODE = 403;
public const PHRASE = "Forbidden";
public const DESCRIPTION = "Indicates that the server understood the request but refuses to authorize it.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403";
}
-
diff --git a/src/Exceptions/HttpGatewayTimeout.php b/src/Exceptions/HttpGatewayTimeout.php
index 6b0c404..4a75ff0 100644
--- a/src/Exceptions/HttpGatewayTimeout.php
+++ b/src/Exceptions/HttpGatewayTimeout.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpGatewayTimeout extends HttpException {
+class HttpGatewayTimeout extends HttpException
+{
public const CODE = 504;
public const PHRASE = "Gateway Time-out";
public const DESCRIPTION = "Indicates that the server, while acting as a gateway or proxy, did not receive a timely response from an upstream server it needed to access in order to complete the request.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/504";
}
-
diff --git a/src/Exceptions/HttpGone.php b/src/Exceptions/HttpGone.php
index ad0eeb9..8849dbc 100644
--- a/src/Exceptions/HttpGone.php
+++ b/src/Exceptions/HttpGone.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpGone extends HttpException {
+class HttpGone extends HttpException
+{
public const CODE = 410;
public const PHRASE = "Gone";
public const DESCRIPTION = "Indicates that access to the target resource is no longer available at the origin server and that this condition is likely to be permanent.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410";
}
-
diff --git a/src/Exceptions/HttpImateapot.php b/src/Exceptions/HttpImateapot.php
index 2774862..a37885c 100644
--- a/src/Exceptions/HttpImateapot.php
+++ b/src/Exceptions/HttpImateapot.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpImateapot extends HttpException {
+class HttpImATeapot extends HttpException
+{
public const CODE = 418;
- public const PHRASE = "I'm a teapot";
+ public const PHRASE = "I'm A Teapot";
public const DESCRIPTION = "Any attempt to brew coffee with a teapot should result in the error code 418 I'm a teapot.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/418";
}
-
diff --git a/src/Exceptions/HttpInsufficientStorage.php b/src/Exceptions/HttpInsufficientStorage.php
index 20e7e97..11b8386 100644
--- a/src/Exceptions/HttpInsufficientStorage.php
+++ b/src/Exceptions/HttpInsufficientStorage.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpInsufficientStorage extends HttpException {
+class HttpInsufficientStorage extends HttpException
+{
public const CODE = 507;
public const PHRASE = "Insufficient Storage";
public const DESCRIPTION = "Means the method could not be performed on the resource because the server is unable to store the representation needed to successfully complete the request.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/507";
}
-
diff --git a/src/Exceptions/HttpInternalServerError.php b/src/Exceptions/HttpInternalServerError.php
index d5b4fbc..f5ab4c6 100644
--- a/src/Exceptions/HttpInternalServerError.php
+++ b/src/Exceptions/HttpInternalServerError.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpInternalServerError extends HttpException {
+class HttpInternalServerError extends HttpException
+{
public const CODE = 500;
public const PHRASE = "Internal Server Error";
public const DESCRIPTION = "Indicates that the server encountered an unexpected condition that prevented it from fulfilling the request.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500";
}
-
diff --git a/src/Exceptions/HttpLengthRequired.php b/src/Exceptions/HttpLengthRequired.php
index 806efd3..047595f 100644
--- a/src/Exceptions/HttpLengthRequired.php
+++ b/src/Exceptions/HttpLengthRequired.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpLengthRequired extends HttpException {
+class HttpLengthRequired extends HttpException
+{
public const CODE = 411;
public const PHRASE = "Length Required";
public const DESCRIPTION = "Indicates that the server refuses to accept the request without a defined Content-Length.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411";
}
-
diff --git a/src/Exceptions/HttpLocked.php b/src/Exceptions/HttpLocked.php
index 3e7671a..e3cb486 100644
--- a/src/Exceptions/HttpLocked.php
+++ b/src/Exceptions/HttpLocked.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpLocked extends HttpException {
+class HttpLocked extends HttpException
+{
public const CODE = 423;
public const PHRASE = "Locked";
public const DESCRIPTION = "Means the source or destination resource of a method is locked.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/423";
}
-
diff --git a/src/Exceptions/HttpMethodNotAllowed.php b/src/Exceptions/HttpMethodNotAllowed.php
index 6ea98a2..762c5e5 100644
--- a/src/Exceptions/HttpMethodNotAllowed.php
+++ b/src/Exceptions/HttpMethodNotAllowed.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpMethodNotAllowed extends HttpException {
+class HttpMethodNotAllowed extends HttpException
+{
public const CODE = 405;
public const PHRASE = "Method Not Allowed";
public const DESCRIPTION = "Indicates that the method specified in the request-line is known by the origin server but not supported by the target resource.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405";
}
-
diff --git a/src/Exceptions/HttpNetworkAuthenticationRequired.php b/src/Exceptions/HttpNetworkAuthenticationRequired.php
index 024af77..559a9ea 100644
--- a/src/Exceptions/HttpNetworkAuthenticationRequired.php
+++ b/src/Exceptions/HttpNetworkAuthenticationRequired.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpNetworkAuthenticationRequired extends HttpException {
+class HttpNetworkAuthenticationRequired extends HttpException
+{
public const CODE = 511;
public const PHRASE = "Network Authentication Required";
public const DESCRIPTION = "Indicates that the client needs to authenticate to gain network access.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/511";
}
-
diff --git a/src/Exceptions/HttpNotAcceptable.php b/src/Exceptions/HttpNotAcceptable.php
index 629cf09..2313628 100644
--- a/src/Exceptions/HttpNotAcceptable.php
+++ b/src/Exceptions/HttpNotAcceptable.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpNotAcceptable extends HttpException {
+class HttpNotAcceptable extends HttpException
+{
public const CODE = 406;
public const PHRASE = "Not Acceptable";
public const DESCRIPTION = "Indicates that the target resource does not have a current representation that would be acceptable to the user agent, according to the proactive negotiation header fields received in the request, and the server is unwilling to supply a default representation.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406";
}
-
diff --git a/src/Exceptions/HttpNotFound.php b/src/Exceptions/HttpNotFound.php
index 2a1c319..a1aed24 100644
--- a/src/Exceptions/HttpNotFound.php
+++ b/src/Exceptions/HttpNotFound.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpNotFound extends HttpException {
+class HttpNotFound extends HttpException
+{
public const CODE = 404;
public const PHRASE = "Not Found";
public const DESCRIPTION = "Indicates that the origin server did not find a current representation for the target resource or is not willing to disclose that one exists.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404";
}
-
diff --git a/src/Exceptions/HttpNotImplemented.php b/src/Exceptions/HttpNotImplemented.php
index f7fd9cb..5d383fd 100644
--- a/src/Exceptions/HttpNotImplemented.php
+++ b/src/Exceptions/HttpNotImplemented.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpNotImplemented extends HttpException {
+class HttpNotImplemented extends HttpException
+{
public const CODE = 501;
public const PHRASE = "Not Implemented";
public const DESCRIPTION = "Indicates that the server does not support the functionality required to fulfill the request.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/501";
}
-
diff --git a/src/Exceptions/HttpPayloadTooLarge.php b/src/Exceptions/HttpPayloadTooLarge.php
index 4c156d5..d94e67f 100644
--- a/src/Exceptions/HttpPayloadTooLarge.php
+++ b/src/Exceptions/HttpPayloadTooLarge.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpPayloadTooLarge extends HttpException {
+class HttpPayloadTooLarge extends HttpException
+{
public const CODE = 413;
public const PHRASE = "Payload Too Large";
public const DESCRIPTION = "Indicates that the server is refusing to process a request because the request payload is larger than the server is willing or able to process.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413";
}
-
diff --git a/src/Exceptions/HttpPaymentRequired.php b/src/Exceptions/HttpPaymentRequired.php
index 9f09103..5a24d79 100644
--- a/src/Exceptions/HttpPaymentRequired.php
+++ b/src/Exceptions/HttpPaymentRequired.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpPaymentRequired extends HttpException {
+class HttpPaymentRequired extends HttpException
+{
public const CODE = 402;
public const PHRASE = "Payment Required";
public const DESCRIPTION = "*reserved*";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402";
}
-
diff --git a/src/Exceptions/HttpPreconditionFailed.php b/src/Exceptions/HttpPreconditionFailed.php
index 4e7ec01..25cce78 100644
--- a/src/Exceptions/HttpPreconditionFailed.php
+++ b/src/Exceptions/HttpPreconditionFailed.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpPreconditionFailed extends HttpException {
+class HttpPreconditionFailed extends HttpException
+{
public const CODE = 412;
public const PHRASE = "Precondition Failed";
public const DESCRIPTION = "Indicates that one or more preconditions given in the request header fields evaluated to false when tested on the server.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412";
}
-
diff --git a/src/Exceptions/HttpPreconditionRequired.php b/src/Exceptions/HttpPreconditionRequired.php
index ba6b314..5a261ed 100644
--- a/src/Exceptions/HttpPreconditionRequired.php
+++ b/src/Exceptions/HttpPreconditionRequired.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpPreconditionRequired extends HttpException {
+class HttpPreconditionRequired extends HttpException
+{
public const CODE = 428;
public const PHRASE = "Precondition Required";
public const DESCRIPTION = "Indicates that the origin server requires the request to be conditional.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428";
}
-
diff --git a/src/Exceptions/HttpProxyAuthenticationRequired.php b/src/Exceptions/HttpProxyAuthenticationRequired.php
index b6a7b44..a823280 100644
--- a/src/Exceptions/HttpProxyAuthenticationRequired.php
+++ b/src/Exceptions/HttpProxyAuthenticationRequired.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpProxyAuthenticationRequired extends HttpException {
+class HttpProxyAuthenticationRequired extends HttpException
+{
public const CODE = 407;
public const PHRASE = "Proxy Authentication Required";
public const DESCRIPTION = "Is similar to 401 (Unauthorized), but indicates that the client needs to authenticate itself in order to use a proxy.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407";
}
-
diff --git a/src/Exceptions/HttpRangeNotSatisfiable.php b/src/Exceptions/HttpRangeNotSatisfiable.php
index 87cbe72..170a8a1 100644
--- a/src/Exceptions/HttpRangeNotSatisfiable.php
+++ b/src/Exceptions/HttpRangeNotSatisfiable.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpRangeNotSatisfiable extends HttpException {
+class HttpRangeNotSatisfiable extends HttpException
+{
public const CODE = 416;
public const PHRASE = "Range Not Satisfiable";
public const DESCRIPTION = "Indicates that none of the ranges in the request's Range header field overlap the current extent of the selected resource or that the set of ranges requested has been rejected due to invalid ranges or an excessive request of small or overlapping ranges.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416";
}
-
diff --git a/src/Exceptions/HttpRequestHeaderFieldsTooLarge.php b/src/Exceptions/HttpRequestHeaderFieldsTooLarge.php
index d7a74b0..f835c7a 100644
--- a/src/Exceptions/HttpRequestHeaderFieldsTooLarge.php
+++ b/src/Exceptions/HttpRequestHeaderFieldsTooLarge.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpRequestHeaderFieldsTooLarge extends HttpException {
+class HttpRequestHeaderFieldsTooLarge extends HttpException
+{
public const CODE = 431;
public const PHRASE = "Request Header Fields Too Large";
public const DESCRIPTION = "Indicates that the server is unwilling to process the request because its header fields are too large.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431";
}
-
diff --git a/src/Exceptions/HttpRequestTimeout.php b/src/Exceptions/HttpRequestTimeout.php
index 5fdff64..8e0316b 100644
--- a/src/Exceptions/HttpRequestTimeout.php
+++ b/src/Exceptions/HttpRequestTimeout.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpRequestTimeout extends HttpException {
+class HttpRequestTimeout extends HttpException
+{
public const CODE = 408;
public const PHRASE = "Request Timeout";
public const DESCRIPTION = "Indicates that the server did not receive a complete request message within the time that it was prepared to wait.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408";
}
-
diff --git a/src/Exceptions/HttpServiceUnavailable.php b/src/Exceptions/HttpServiceUnavailable.php
index 935f9cb..63b41b6 100644
--- a/src/Exceptions/HttpServiceUnavailable.php
+++ b/src/Exceptions/HttpServiceUnavailable.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpServiceUnavailable extends HttpException {
+class HttpServiceUnavailable extends HttpException
+{
public const CODE = 503;
public const PHRASE = "Service Unavailable";
public const DESCRIPTION = "Indicates that the server is currently unable to handle the request due to a temporary overload or scheduled maintenance, which will likely be alleviated after some delay.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503";
}
-
diff --git a/src/Exceptions/HttpTooManyRequests.php b/src/Exceptions/HttpTooManyRequests.php
index 612e910..fe36d92 100644
--- a/src/Exceptions/HttpTooManyRequests.php
+++ b/src/Exceptions/HttpTooManyRequests.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpTooManyRequests extends HttpException {
+class HttpTooManyRequests extends HttpException
+{
public const CODE = 429;
public const PHRASE = "Too Many Requests";
public const DESCRIPTION = "Indicates that the user has sent too many requests in a given amount of time (rate limiting).";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429";
}
-
diff --git a/src/Exceptions/HttpURITooLong.php b/src/Exceptions/HttpURITooLong.php
index 70c12d3..1717214 100644
--- a/src/Exceptions/HttpURITooLong.php
+++ b/src/Exceptions/HttpURITooLong.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpURITooLong extends HttpException {
+class HttpURITooLong extends HttpException
+{
public const CODE = 414;
public const PHRASE = "URI Too Long";
public const DESCRIPTION = "Indicates that the server is refusing to service the request because the request-target is longer than the server is willing to interpret.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414";
}
-
diff --git a/src/Exceptions/HttpUnauthorized.php b/src/Exceptions/HttpUnauthorized.php
index 1cbffca..432d4ce 100644
--- a/src/Exceptions/HttpUnauthorized.php
+++ b/src/Exceptions/HttpUnauthorized.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpUnauthorized extends HttpException {
+class HttpUnauthorized extends HttpException
+{
public const CODE = 401;
public const PHRASE = "Unauthorized";
public const DESCRIPTION = "Indicates that the request has not been applied because it lacks valid authentication credentials for the target resource.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401";
}
-
diff --git a/src/Exceptions/HttpUnavailableForLegalReasons.php b/src/Exceptions/HttpUnavailableForLegalReasons.php
index be894f1..e846927 100644
--- a/src/Exceptions/HttpUnavailableForLegalReasons.php
+++ b/src/Exceptions/HttpUnavailableForLegalReasons.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpUnavailableForLegalReasons extends HttpException {
+class HttpUnavailableForLegalReasons extends HttpException
+{
public const CODE = 451;
public const PHRASE = "Unavailable For Legal Reasons";
public const DESCRIPTION = "This status code indicates that the server is denying access to the resource in response to a legal demand.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/451";
}
-
diff --git a/src/Exceptions/HttpUnprocessableEntity.php b/src/Exceptions/HttpUnprocessableEntity.php
index b907b20..2cf6429 100644
--- a/src/Exceptions/HttpUnprocessableEntity.php
+++ b/src/Exceptions/HttpUnprocessableEntity.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpUnprocessableEntity extends HttpException {
+class HttpUnprocessableEntity extends HttpException
+{
public const CODE = 422;
public const PHRASE = "Unprocessable Entity";
public const DESCRIPTION = "Means the server understands the content type of the request entity (hence a 415(Unsupported Media Type) status code is inappropriate), and the syntax of the request entity is correct (thus a 400 (Bad Request) status code is inappropriate) but was unable to process the contained instructions.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422";
}
-
diff --git a/src/Exceptions/HttpUnsupportedMediaType.php b/src/Exceptions/HttpUnsupportedMediaType.php
index e35917f..3e19508 100644
--- a/src/Exceptions/HttpUnsupportedMediaType.php
+++ b/src/Exceptions/HttpUnsupportedMediaType.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpUnsupportedMediaType extends HttpException {
+class HttpUnsupportedMediaType extends HttpException
+{
public const CODE = 415;
public const PHRASE = "Unsupported Media Type";
public const DESCRIPTION = "Indicates that the origin server is refusing to service the request because the payload is in a format not supported by the target resource for this method.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415";
}
-
diff --git a/src/Exceptions/HttpUpgradeRequired.php b/src/Exceptions/HttpUpgradeRequired.php
index 74957e2..bea65b3 100644
--- a/src/Exceptions/HttpUpgradeRequired.php
+++ b/src/Exceptions/HttpUpgradeRequired.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpUpgradeRequired extends HttpException {
+class HttpUpgradeRequired extends HttpException
+{
public const CODE = 426;
public const PHRASE = "Upgrade Required";
public const DESCRIPTION = "Indicates that the server refuses to perform the request using the current protocol but might be willing to do so after the client upgrades to a different protocol.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426";
}
-
diff --git a/src/Exceptions/HttpVariantAlsoNegotiates.php b/src/Exceptions/HttpVariantAlsoNegotiates.php
index 408a908..3cd2d75 100644
--- a/src/Exceptions/HttpVariantAlsoNegotiates.php
+++ b/src/Exceptions/HttpVariantAlsoNegotiates.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpVariantAlsoNegotiates extends HttpException {
+class HttpVariantAlsoNegotiates extends HttpException
+{
public const CODE = 506;
public const PHRASE = "Variant Also Negotiates";
public const DESCRIPTION = "Indicates that the server has an internal configuration error: the chosen variant resource is configured to engage in transparent content negotiation itself, and is therefore not a proper end point in the negotiation process.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/506";
}
-
diff --git a/src/Exceptions/HttpHTTPVersionNotSupported.php b/src/Exceptions/HttpVersionNotSupported.php
similarity index 73%
rename from src/Exceptions/HttpHTTPVersionNotSupported.php
rename to src/Exceptions/HttpVersionNotSupported.php
index f005914..5b3d0a2 100644
--- a/src/Exceptions/HttpHTTPVersionNotSupported.php
+++ b/src/Exceptions/HttpVersionNotSupported.php
@@ -2,10 +2,10 @@
namespace Tnapf\Router\Exceptions;
-class HttpHTTPVersionNotSupported extends HttpException {
+class HttpVersionNotSupported extends HttpException
+{
public const CODE = 505;
- public const PHRASE = "HTTP Version Not Supported";
+ public const PHRASE = "Version Not Supported";
public const DESCRIPTION = "Indicates that the server does not support, or refuses to support, the protocol version that was used in the request message.";
public const HREF = "https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/505";
}
-
diff --git a/src/Interfaces/RequestHandlerInterface.php b/src/Interfaces/RequestHandlerInterface.php
new file mode 100644
index 0000000..6a53131
--- /dev/null
+++ b/src/Interfaces/RequestHandlerInterface.php
@@ -0,0 +1,17 @@
+ [],
- Exceptions\HttpUnauthorized::class => [],
- Exceptions\HttpPaymentRequired::class => [],
- Exceptions\HttpForbidden::class => [],
- Exceptions\HttpNotFound::class => [],
- Exceptions\HttpMethodNotAllowed::class => [],
- Exceptions\HttpNotAcceptable::class => [],
- Exceptions\HttpProxyAuthenticationRequired::class => [],
- Exceptions\HttpRequestTimeout::class => [],
- Exceptions\HttpConflict::class => [],
- Exceptions\HttpGone::class => [],
- Exceptions\HttpLengthRequired::class => [],
- Exceptions\HttpPreconditionFailed::class => [],
- Exceptions\HttpPayloadTooLarge::class => [],
- Exceptions\HttpURITooLong::class => [],
- Exceptions\HttpUnsupportedMediaType::class => [],
- Exceptions\HttpRangeNotSatisfiable::class => [],
- Exceptions\HttpExpectationFailed::class => [],
- Exceptions\HttpImateapot::class => [],
- Exceptions\HttpUpgradeRequired::class => [],
- Exceptions\HttpUnprocessableEntity::class => [],
- Exceptions\HttpLocked::class => [],
- Exceptions\HttpFailedDependency::class => [],
- Exceptions\HttpPreconditionRequired::class => [],
- Exceptions\HttpTooManyRequests::class => [],
- Exceptions\HttpRequestHeaderFieldsTooLarge::class => [],
- Exceptions\HttpUnavailableForLegalReasons::class => [],
- Exceptions\HttpInternalServerError::class => [],
- Exceptions\HttpNotImplemented::class => [],
- Exceptions\HttpBadGateway::class => [],
- Exceptions\HttpServiceUnavailable::class => [],
- Exceptions\HttpGatewayTimeout::class => [],
- Exceptions\HttpHTTPVersionNotSupported::class => [],
- Exceptions\HttpVariantAlsoNegotiates::class => [],
- Exceptions\HttpInsufficientStorage::class => [],
- Exceptions\HttpNetworkAuthenticationRequired::class => []
- ];
+ protected static array $catchers = [];
/**
- * @param string $uri
- * @param array|Closure $controller
+ * @param string $uri
+ * @param class-string
* @return Route
*/
- public static function get(string $uri, array|Closure $controller): Route
+ public static function get(string $uri, string $controller): Route
{
$route = new Route($uri, $controller, Methods::GET);
-
+
self::addRoute($route);
-
+
return $route;
}
/**
- * @param string $uri
- * @param array|Closure $controller
+ * @param string $uri
+ * @param class-string
* @return Route
*/
- public static function post(string $uri, array|Closure $controller): Route
+ public static function post(string $uri, string $controller): Route
{
$route = new Route($uri, $controller, Methods::POST);
-
+
self::addRoute($route);
return $route;
}
/**
- * @param string $uri
- * @param array|Closure $controller
+ * @param string $uri
+ * @param class-string
* @return Route
*/
- public static function put(string $uri, array|Closure $controller): Route
+ public static function put(string $uri, string $controller): Route
{
$route = new Route($uri, $controller, Methods::PUT);
-
+
self::addRoute($route);
return $route;
}
/**
- * @param string $uri
- * @param array|Closure $controller
+ * @param string $uri
+ * @param class-string
* @return Route
*/
- public static function delete(string $uri, array|Closure $controller): Route
+ public static function delete(string $uri, string $controller): Route
{
$route = new Route($uri, $controller, Methods::DELETE);
-
+
self::addRoute($route);
return $route;
}
/**
- * @param string $uri
- * @param array|Closure $controller
+ * @param string $uri
+ * @param class-string
* @return Route
*/
- public static function options(string $uri, array|Closure $controller): Route
+ public static function options(string $uri, string $controller): Route
{
$route = new Route($uri, $controller, Methods::OPTIONS);
-
+
self::addRoute($route);
return $route;
}
/**
- * @param string $uri
- * @param array|Closure $controller
+ * @param string $uri
+ * @param class-string
* @return Route
*/
- public static function head(string $uri, array|Closure $controller): Route
+ public static function head(string $uri, string $controller): Route
{
$route = new Route($uri, $controller, Methods::HEAD);
-
+
self::addRoute($route);
return $route;
}
/**
- * @param string $uri
- * @param array|Closure $controller
+ * @param string $uri
+ * @param class-string
* @return Route
*/
- public static function all(string $uri, array|Closure $controller): Route
+ public static function all(string $uri, string $controller): Route
{
$route = new Route($uri, $controller, ...Methods::cases());
-
+
self::addRoute($route);
return $route;
}
/**
- * @param Route $route
+ * @param Route $route
* @return void
*/
public static function addRoute(Route &$route): void
{
- if (isset(self::$mount)) {
- foreach (self::$mount["beforeMiddleware"] ?? [] as $before) {
- $route->before($before);
+ if (isset(self::$group)) {
+ foreach (self::$group["middlewares"] ?? [] as $middlware) {
+ $route->addMiddleware($middlware);
}
-
- foreach (self::$mount["afterMiddleware"] ?? [] as $after) {
- $route->before($after);
+
+ foreach (self::$group["postwares"] ?? [] as $postware) {
+ $route->addPostware($postware);
}
}
self::$routes[$route->uri] = &$route;
}
- public static function mount(string $baseUri, Closure $mounting, array $beforeMiddleware = [], array $afterMiddleware = []): void
+ public static function group(string $baseUri, Closure $grouping, array $middleware = [], array $postware = []): void
{
- $oldMount = self::$mount;
+ $oldMount = self::$group;
- if (empty(self::$mount['baseUri'])) {
- self::$mount = compact("baseUri", "beforeMiddleware", "afterMiddleware");
+ if (empty(self::$group['baseUri'])) {
+ self::$group = compact("baseUri", "middleware", "postware");
} else {
- self::$mount['baseUri'] .= $baseUri;
- self::$mount['beforeMiddleware'] = array_merge(self::$mount['beforeMiddleware'], $beforeMiddleware);
- self::$mount['afterMiddleware'] = array_merge(self::$mount['afterMiddleware'], $afterMiddleware);
+ self::$group['baseUri'] .= $baseUri;
+ self::$group['middlewares'] = array_merge(self::$group['middlewares'], $middleware);
+ self::$group['postwares'] = array_merge(self::$group['postwares'], $postware);
}
- call_user_func($mounting);
+ $grouping();
- self::$mount = $oldMount;
+ self::$group = $oldMount;
}
public static function getBaseUri(): string
{
- return self::$mount["baseUri"] ?? "";
+ return self::$group["baseUri"] ?? "";
}
/**
- * @param array $routes
- * @return stdClass|null
+ * @param array $routes
+ * @return ResolvedRoute|null
*/
- private static function resolveRoute(array $routes): ?stdClass
+ private static function resolveRoute(array $routes): ?ResolvedRoute
{
- $routeMatches = static function (Route $route, string $requestUri, array|null &$matches) use (&$argNames): bool
- {
+ $routeMatches = static function (
+ Route $route,
+ string $requestUri,
+ array|null &$matches
+ ) use (&$argNames): bool {
$argNames = [];
- $routeParts = explode("/", $route->uri);
+ $routeParts = explode("/", $route->uri);
if (count(explode("/", $requestUri)) !== count($routeParts) && !str_ends_with($route->uri, "/(.*)")) {
return false;
@@ -240,7 +209,7 @@ private static function resolveRoute(array $routes): ?stdClass
$argNames[] = $name;
}
-
+
$routeParts[$key] = $part;
}
@@ -264,10 +233,7 @@ private static function resolveRoute(array $routes): ?stdClass
continue;
}
- $matchedRoute = new stdClass;
-
- $args = new stdClass;
-
+ $args = new stdClass();
$argsIterator = 0;
foreach ($matches as $index => $match) {
if (!$index) {
@@ -279,126 +245,92 @@ private static function resolveRoute(array $routes): ?stdClass
$args->$name = isset($match[0][0]) && $match[0][1] != -1 ? trim($match[0][0], '/') : null;
}
- $matchedRoute->route = $route;
- $matchedRoute->args = $args;
+ $resolvedRoute = new ResolvedRoute($route, $args);
}
- return $matchedRoute ?? null;
+ return $resolvedRoute ?? null;
}
/**
- * @param class-string $exceptionToCatch
- * @param array|Closure $controller
+ * @param class-string $exceptionToCatch
+ * @param class-string $controller
*/
- public static function catch(string $toCatch, array|Closure $controller, ?string $uri = "/(.*)"): Route
+ public static function catch(string $toCatch, string $controller, ?string $uri = "/(.*)"): Route
{
$catchable = array_keys(self::$catchers);
if (!in_array($toCatch, $catchable)) {
- throw new \InvalidArgumentException("You can only catch the following exceptions: ".implode(", ", $catchable));
+ self::makeCatchable($toCatch);
}
-
+
$route = new Route($uri, $controller, ...Methods::cases());
-
+
self::$catchers[$toCatch][] = &$route;
return $route;
}
-
+ /**
+ * @param class-string $toCatch
+ * @return void
+ */
public static function makeCatchable(string $toCatch): void
{
if (isset(self::$catchers[$toCatch])) {
return;
}
+ if (!is_subclass_of($toCatch, HttpException::class)) {
+ throw new \InvalidArgumentException("{$toCatch} must extend " . HttpException::class);
+ }
+
self::$catchers[$toCatch] = [];
}
/**
- * @param Route $route
- * @param stdClass|null $args
- * @param ServerRequestInterface $request
- * @return ResponseInterface
+ *
*/
- private static function invokeRoute(Route $route, ?stdClass $args = null, ServerRequestInterface $request): ResponseInterface
+ public static function emitHttpExceptions(int $type): void
{
- if ($args === null) {
- $args = new stdClass;
- }
-
- $params = [$request, null, $args];
-
- $befores = $route->getBefores();
-
- $basicMiddleware = static function (ServerRequestInterface $request, ResponseInterface $response, stdClass $args, Closure $next) {
- $next($response);
- };
-
- array_unshift($befores, $basicMiddleware);
-
- $nexts = [];
-
- foreach (array_keys($befores) as $key) {
- $nexts[] = static function (ResponseInterface $response, mixed ...$extra) use ($route, $basicMiddleware, $params, $befores, $key, &$nexts): ResponseInterface {
- if ($key === count($befores)-1) {
- $controller = $route->controller;
-
- if (!empty($route->getAfters()) && !isset($afters)) {
- $nexts = [];
- $afters = $route->getAfters();
- array_unshift($afters, $basicMiddleware);
-
- foreach (array_keys($afters) as $key) {
- $nexts[] = static function (ResponseInterface $response, mixed ...$extra) use ($params, $afters, $key, &$nexts): ResponseInterface {
- $controller = $afters[$key+1] ?? null;
- $params[] = $nexts[$key+1] ?? null;
-
- if ($controller === null) {
- return $response;
- }
-
- $params[1] = $response;
-
- $params = array_merge($params, $extra);
-
- return !is_callable($controller)
- ? call_user_func("{$controller[0]}::{$controller[1]}", ...$params)
- : $controller(...$params);
- };
- }
-
- $params[] = $nexts[0];
- }
- } else {
- $controller = $befores[$key+1];
- $params[] = $nexts[$key+1];
- }
-
- $params[1] = $response;
-
- $params = array_merge($params, $extra);
+ self::$emitHttpExceptions = $type;
+ }
+ protected static function invokeRoute(
+ ResolvedRoute $resolvedRoute,
+ ServerRequestInterface $request
+ ): ResponseInterface {
+ $response = new Response();
+
+ $controllers = [
+ ...$resolvedRoute->route->getMiddleware(),
+ $resolvedRoute->route->controller,
+ ...$resolvedRoute->route->getPostware()
+ ];
+
+ $next = function (
+ ServerRequestInterface $request,
+ ResponseInterface $response,
+ stdClass $args
+ ) use (
+ &$controllers,
+ &$next
+ ): ResponseInterface {
+ $controller = array_shift($controllers);
+
+ if ($controller === null) {
+ return $response;
+ }
- return !is_callable($controller)
- ? call_user_func("{$controller[0]}::{$controller[1]}", ...$params)
- : $controller(...$params);
- };
- }
+ return call_user_func("$controller::handle", $request, $response, $args, $next);
+ };
- return $nexts[0](new Response);
+ return $next($request, $response, $resolvedRoute->args);
}
- /**
- *
- */
- public static function emitHttpExceptions(int $type): void
+ public static function run(EmitterInterface $emitter = null): void
{
- self::$emitHttpExceptions = $type;
- }
+ self::$emitter = $emitter ?? new SapiEmitter();
- public static function run(): void
- {
$sortByLength = function (Route $a, Route $b) {
return (strlen($a->uri) > strlen($b->uri));
};
@@ -411,59 +343,44 @@ public static function run(): void
$resolved = self::resolveRoute(self::$routes);
- foreach ($_FILES as $key => $file) {
- $_FILES[$key] = new UploadedFile($file["tmp_name"], $file["size"], $file["error"], $file["type"]);
- }
-
- $request = new ServerRequest($_SERVER, $_FILES, $_COOKIE, $_GET, $_POST, $_SERVER["REQUEST_METHOD"], $_SERVER["REQUEST_URI"], getallheaders(), "php://input");
+ $request = ServerRequestCreator::createFromGlobals($_SERVER);
try {
if ($resolved === null) {
throw new Exceptions\HttpNotFound($request);
}
- $response = self::invokeRoute($resolved->route, $resolved->args, $request);
+ $response = self::invokeRoute($resolved, $request);
} catch (Throwable $e) {
if (in_array($e::class, array_keys(self::$catchers))) {
$resolved = self::resolveRoute(self::$catchers[$e::class]);
} else {
- $resolved = self::resolveRoute(self::$catchers[HttpInternalServerError::class]);
+ $resolved = self::resolveRoute(self::$catchers[HttpInternalServerError::class] ?? []);
}
if ($resolved === null) {
if (!self::$emitHttpExceptions) {
throw $e;
} else {
- if (!is_subclass_of($e, HttpException::class)) {
- $class = HttpInternalServerError::class;
- } else {
- $class = $e::class;
- }
-
- switch (self::$emitHttpExceptions) {
- case 1:
- $method = "buildEmptyResponse";
- break;
- case 2:
- $method = "buildHtmlResponse";
- break;
- case 3:
- $method = "buildJsonResponse";
- break;
- default:
- $method = "buildEmptyResponse";
- }
-
- $response = call_user_func("$class::$method");
+ $class = $e::class;
+
+ $method = match (self::$emitHttpExceptions) {
+ self::EMIT_EMPTY_RESPONSE => "buildEmptyResponse",
+ self::EMIT_HTML_RESPONSE => "buildHtmlResponse",
+ self::EMIT_JSON_RESPONSE => "buildJsonResponse",
+ default => "buildEmptyResponse"
+ };
+
+ $response = call_user_func("{$class}::{$method}");
}
}
if (!isset($response)) {
$resolved->args->exception = $e;
- $response = self::invokeRoute($resolved->route, $resolved->args, $request);
+ $response = self::invokeRoute($resolved, $request);
}
}
-
+
(new SapiEmitter())->emit($response);
}
-}
\ No newline at end of file
+}
diff --git a/src/Routing/ResolvedRoute.php b/src/Routing/ResolvedRoute.php
new file mode 100644
index 0000000..2044d85
--- /dev/null
+++ b/src/Routing/ResolvedRoute.php
@@ -0,0 +1,14 @@
+ $controller
+ * @param Methods ...$methods
+ */
+ public function __construct(string $uri, public readonly string $controller, Methods ...$methods)
+ {
if (!str_starts_with($uri, "/")) {
$uri = "/{$uri}";
}
- $this->uri = Router::getBaseUri()."$uri";
+ if (!is_subclass_of($controller, RequestHandlerInterface::class)) {
+ throw new InvalidArgumentException("{$controller} must implement " . RequestHandlerInterface::class);
+ }
- $this->parameters = new stdClass;
+ $this->uri = Router::getBaseUri() . "$uri";
- self::validateController($controller);
+ $this->parameters = new stdClass();
$this->methods = $methods;
}
-
- /**
- * @param array|Closure $controller
- * @return void
- *
- * @throws InvalidArgumentException
- */
- private static function validateController(array|Closure $controller): void
- {
- if (!is_array($controller)) {
- return;
- }
-
- $fullMethod = "{$controller[0]}::{$controller[1]}";
-
- if (!method_exists($controller[0], $controller[1])) {
- throw new InvalidArgumentException("{$fullMethod} doesn't exist");
- } else if (!(new ReflectionMethod($controller[0], $controller[1]))->isStatic()) {
- throw new InvalidArgumentException("{$fullMethod} is not a static method");
- }
- }
-
public function getParameter(string $name): ?string
{
return $this->parameters->$name ?? "{{$name}}";
@@ -69,40 +58,58 @@ public function setParameter(string $name, string $pattern): self
return $this;
}
- public function before(array|Closure ...$controllers): self
+ /**
+ * @param class-string ...$middlewares
+ * @return self
+ */
+ public function addMiddleware(string ...$middlewares): self
{
- foreach ($controllers as $controller) {
- self::validateController($controller);
+ foreach ($middlewares as $middleware) {
+ if (!is_subclass_of($middleware, RequestHandlerInterface::class)) {
+ throw new InvalidArgumentException("{$middleware} must implement " . RequestHandlerInterface::class);
+ }
- $this->before[] = $controller;
+ $this->middleware[] = $middleware;
}
return $this;
}
- public function after(array|Closure ...$controllers): self
+ /**
+ * @param class-string ...$postwares
+ * @return self
+ */
+ public function addPostware(string ...$postwares): self
{
- foreach ($controllers as $controller) {
- self::validateController($controller);
+ foreach ($postwares as $postware) {
+ if (!is_subclass_of($postware, RequestHandlerInterface::class)) {
+ throw new InvalidArgumentException("{$postware} must implement " . RequestHandlerInterface::class);
+ }
- $this->after[] = $controller;
+ $this->postware[] = $postware;
}
return $this;
}
- public function getBefores(): array
+ /**
+ * @return RequestHandlerInterface[]
+ */
+ public function getMiddleware(): array
{
- return $this->before;
+ return $this->middleware;
}
- public function getAfters(): array
+ /**
+ * @return RequestHandlerInterface[]
+ */
+ public function getPostware(): array
{
- return $this->after;
+ return $this->postware;
}
public function acceptsMethod(Methods $method): bool
{
return in_array($method, $this->methods);
}
-}
\ No newline at end of file
+}
diff --git a/tools/HttpExceptionGeneration/GenerateClasses.php b/tools/HttpExceptions/GenerateClasses.php
similarity index 65%
rename from tools/HttpExceptionGeneration/GenerateClasses.php
rename to tools/HttpExceptions/GenerateClasses.php
index 831c661..fd763a2 100644
--- a/tools/HttpExceptionGeneration/GenerateClasses.php
+++ b/tools/HttpExceptions/GenerateClasses.php
@@ -1,6 +1,6 @@
$code) {
/** @var Code $code */
@@ -30,24 +28,15 @@ abstract class Code {
namespace = $namespace ?>;
-class = $className ?> extends HttpException {
+class = $className ?> extends HttpException
+{
public const CODE = = $code->code ?>;
public const PHRASE = "= $code->phrase ?>";
public const DESCRIPTION = "= $code->description ?>";
public const HREF = "= $code->mdn ?>";
}
-
[]";
-echo ($key !== count($codes)-1) ? ",\n" : "\n";
+file_put_contents(__DIR__."/../../src/Exceptions/$className.php", ob_get_clean());
}
-
-echo TAB."];";
-
-echo "\nBE SURE TO UPDATE 'src/Router.php:32'";
\ No newline at end of file
diff --git a/tools/HttpExceptionGeneration/GenerateDocs.php b/tools/HttpExceptions/GenerateDocs.php
similarity index 79%
rename from tools/HttpExceptionGeneration/GenerateDocs.php
rename to tools/HttpExceptions/GenerateDocs.php
index 6fce4de..018a442 100644
--- a/tools/HttpExceptionGeneration/GenerateDocs.php
+++ b/tools/HttpExceptions/GenerateDocs.php
@@ -1,6 +1,6 @@
@@ -19,5 +19,4 @@
|= $code->code ?>|[= $code->phrase ?>](= $code->mdn ?>)|[= $className ?>](https://github.com/tnapf/Router/blob/main/src/Exceptions/= $className ?>.php)