From 35418919ed3c8abed81e091fbfabb1f4b4035ed4 Mon Sep 17 00:00:00 2001 From: Madeorsk Date: Fri, 8 Nov 2024 16:10:38 +0100 Subject: [PATCH] Initialize Nest HTTP library. --- .gitignore | 6 + composer.json | 37 ++ composer.lock | 520 ++++++++++++++++++++++ src/ContentType.php | 13 + src/ErrorsMiddleware.php | 63 +++ src/ExceptionRenderer.php | 39 ++ src/Exceptions/ClientException.php | 19 + src/Exceptions/HttpException.php | 17 + src/Exceptions/InternalErrorException.php | 16 + src/Exceptions/NotFoundException.php | 16 + src/Exceptions/ServerException.php | 19 + src/Header.php | 13 + src/Http.php | 132 ++++++ src/HttpConfiguration.php | 191 ++++++++ src/HttpService.php | 36 ++ src/Renderer/PhpRenderer.php | 31 ++ src/Renderer/Renderer.php | 16 + src/RequestAuthentication.php | 12 + src/Router/Controller.php | 105 +++++ src/Router/Route.php | 166 +++++++ src/Router/RouteMatch.php | 27 ++ src/Router/Router.php | 35 ++ src/Router/Routes.php | 79 ++++ src/Router/RoutesTree.php | 158 +++++++ src/StatusCode.php | 79 ++++ src/views/exception.php | 51 +++ 26 files changed, 1896 insertions(+) create mode 100644 .gitignore create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 src/ContentType.php create mode 100644 src/ErrorsMiddleware.php create mode 100644 src/ExceptionRenderer.php create mode 100644 src/Exceptions/ClientException.php create mode 100644 src/Exceptions/HttpException.php create mode 100644 src/Exceptions/InternalErrorException.php create mode 100644 src/Exceptions/NotFoundException.php create mode 100644 src/Exceptions/ServerException.php create mode 100644 src/Header.php create mode 100644 src/Http.php create mode 100644 src/HttpConfiguration.php create mode 100644 src/HttpService.php create mode 100644 src/Renderer/PhpRenderer.php create mode 100644 src/Renderer/Renderer.php create mode 100644 src/RequestAuthentication.php create mode 100644 src/Router/Controller.php create mode 100644 src/Router/Route.php create mode 100644 src/Router/RouteMatch.php create mode 100644 src/Router/Router.php create mode 100644 src/Router/Routes.php create mode 100644 src/Router/RoutesTree.php create mode 100644 src/StatusCode.php create mode 100644 src/views/exception.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2dfd9ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# IDEA +.idea/ +*.iml + +# Composer +vendor/ diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..21448d1 --- /dev/null +++ b/composer.json @@ -0,0 +1,37 @@ +{ + "version": "1.0", + "name": "nest/http", + "description": "Nest HTTP service and engine.", + "type": "library", + "authors": [ + { + "name": "Madeorsk", + "email": "madeorsk@protonmail.com" + } + ], + "autoload": { + "psr-4": { + "Nest\\Http\\": "src/" + } + }, + "repositories": [ + { + "type": "vcs", + "url": "https://code.zeptotech.net/Nest/Core" + } + ], + "require": { + "php": "^8.3", + "nest/core": "dev-main", + "nyholm/psr7": "^1.8", + "ralouphie/getallheaders": "^3.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0", + "psr/http-message": "^2.0", + "psr/http-factory": "^1.1", + "filp/whoops": "^2.16" + }, + "suggest": { + "ext-dom": "*" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..911cd5a --- /dev/null +++ b/composer.lock @@ -0,0 +1,520 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "2ddbf7190a614a3bab8ab92fd7179bbc", + "packages": [ + { + "name": "filp/whoops", + "version": "2.16.0", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2", + "reference": "befcdc0e5dce67252aa6322d82424be928214fa2", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.16.0" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2024-09-25T12:00:00+00:00" + }, + { + "name": "nest/core", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://code.zeptotech.net/Nest/Core", + "reference": "22296b8d7c8b528089189a8d9500395424cb6468" + }, + "require": { + "php": "^8.3" + }, + "default-branch": true, + "type": "library", + "autoload": { + "psr-4": { + "Nest\\": "src/" + }, + "files": [ + "src/Utils/Array.php", + "src/Utils/Paths.php", + "src/Utils/Reflection.php", + "src/Utils/String.php" + ] + }, + "authors": [ + { + "name": "Madeorsk", + "email": "madeorsk@protonmail.com" + } + ], + "description": "Nest framework core.", + "time": "2024-11-08T11:12:38+00:00" + }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "nest/core": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/src/ContentType.php b/src/ContentType.php new file mode 100644 index 0000000..a2f882f --- /dev/null +++ b/src/ContentType.php @@ -0,0 +1,13 @@ +handle($request); + } + catch (Throwable $exception) + { // Handle the exception. + if (!($exception instanceof HttpException)) + { + if ($this->configuration->getDebug()) + { // In debug mode, show details about the thrown exception. + $whoops = new Whoops(); + $whoops->writeToOutput(false); + $whoops->allowQuit(false); + $whoops->pushHandler(new PrettyPageHandler()); + $body = $whoops->handleException($exception); + + // Something bad happened, return an internal error response. + return new Response(500, body: $body); + } + + // Otherwise, show an internal error exception. + $exception = new InternalErrorException(previous: $exception); + } + + // Should always be an HTTP exception at this point. + $renderer = new ($this->configuration->getExceptionRenderer())($exception); + return new Response($renderer->getStatusCode(), body: $renderer->getBody()); + } + } +} diff --git a/src/ExceptionRenderer.php b/src/ExceptionRenderer.php new file mode 100644 index 0000000..285f968 --- /dev/null +++ b/src/ExceptionRenderer.php @@ -0,0 +1,39 @@ +exception->getStatusCode(); + } + + /** + * Return the body of the HTTP exception. + * @return string HTTP exception body. + */ + public function getBody(): string + { + // Render the default exception renderer. + return (new PhpRenderer(__DIR__."/views/exception.php"))->render((object) [ + "exception" => $this->exception, + ]); + } +} diff --git a/src/Exceptions/ClientException.php b/src/Exceptions/ClientException.php new file mode 100644 index 0000000..9f1fc9a --- /dev/null +++ b/src/Exceptions/ClientException.php @@ -0,0 +1,19 @@ +value; + } +} diff --git a/src/Exceptions/HttpException.php b/src/Exceptions/HttpException.php new file mode 100644 index 0000000..f3ff0d2 --- /dev/null +++ b/src/Exceptions/HttpException.php @@ -0,0 +1,17 @@ +value; + } +} diff --git a/src/Exceptions/NotFoundException.php b/src/Exceptions/NotFoundException.php new file mode 100644 index 0000000..5dd65f5 --- /dev/null +++ b/src/Exceptions/NotFoundException.php @@ -0,0 +1,16 @@ +value; + } +} diff --git a/src/Exceptions/ServerException.php b/src/Exceptions/ServerException.php new file mode 100644 index 0000000..930c385 --- /dev/null +++ b/src/Exceptions/ServerException.php @@ -0,0 +1,19 @@ +value; + } +} diff --git a/src/Header.php b/src/Header.php new file mode 100644 index 0000000..b76261b --- /dev/null +++ b/src/Header.php @@ -0,0 +1,13 @@ +configuration->getDebug(); + } + + /** + * Parse the current server request from global context. + * @return ServerRequestInterface Parsed server request. + */ + protected function parseRequest(): ServerRequestInterface + { + return $this->request = new ServerRequest( + $_SERVER["REQUEST_METHOD"], + $_SERVER["REQUEST_URI"], + getallheaders(), + fopen("php://input", "r"), + "1.1", + $_SERVER, + ); + } + + /** + * Build a chain of request handlers (with middlewares). + * @return RequestHandlerInterface + */ + protected function buildCallChain(): RequestHandlerInterface + { + // Set the final request handler as the current one. + $currentHandler = $this->configuration->getRequestHandler(); + + foreach ($this->configuration->getMiddlewares() as $middleware) + { // Add each middleware. + // The current handler is calling the middleware which will call the previously current one if needed. + $currentHandler = new class($middleware, $currentHandler) implements RequestHandlerInterface { + public function __construct(private MiddlewareInterface $middleware, private RequestHandlerInterface $requestHandler) + {} + + /** + * @inheritDoc + */ + #[Override] public function handle(ServerRequestInterface $request): ResponseInterface + { + return $this->middleware->process($request, $this->requestHandler); + } + }; + } + + // Return the built request handler. + return $currentHandler; + } + + /** + * Start HTTP engine. + * @return void + */ + public function __invoke(): void + { + // Create and initialize request object. + $this->parseRequest(); + + // Get response from handler and send it. + $response = $this->buildCallChain()->handle($this->request); + $this->output($response); + } + + /** + * Output a given response. + * @param ResponseInterface $response The response to output. + * @return void + */ + public function output(ResponseInterface $response): void + { + // Send status code. + http_response_code($response->getStatusCode()); + + // Send headers. + foreach ($response->getHeaders() as $name => $values) + { // Send each header. + foreach ($values as $value) + { // Send each header value. + header("$name: $value", false); + } + } + + // Send body. + echo $response->getBody()->getContents(); + } + + /** + * Get current request instance, if there is one. + * @return ServerRequestInterface|null + */ + public function getRequest(): ?ServerRequestInterface + { + return $this->request ?? null; + } +} diff --git a/src/HttpConfiguration.php b/src/HttpConfiguration.php new file mode 100644 index 0000000..a602a27 --- /dev/null +++ b/src/HttpConfiguration.php @@ -0,0 +1,191 @@ +addDefaultMiddlewares(); + } + + /** + * Set debug mode. + * @param bool $debug Debug mode status. + * @return $this + */ + public function debug(bool $debug = true): static + { + $this->debug = $debug; + return $this; + } + + /** + * Set exception renderer class. + * @param string $exceptionRenderer The exception renderer to use and an HTTP exception is thrown. + * @return $this + * @throws InvalidTypeException + */ + public function exceptionRenderer(string $exceptionRenderer): static + { + expect_type($exceptionRenderer, ExceptionRenderer::class); + $this->exceptionRenderer = $exceptionRenderer; + return $this; + } + + /** + * Set HTTP request handler. + * @param (callable(ServerRequestInterface): ResponseInterface)|RequestHandlerInterface $requestHandler + * @return $this + */ + public function requestHandler(callable|RequestHandlerInterface $requestHandler): static + { + if (!($requestHandler instanceof RequestHandlerInterface)) + // Convert a callable to a request handler interface. + $requestHandler = new class($requestHandler) implements RequestHandlerInterface { + private $requestHandler; + + public function __construct(callable $requestHandler) + { + $this->requestHandler = $requestHandler; + } + + /** + * @inheritDoc + */ + #[Override] public function handle(ServerRequestInterface $request): ResponseInterface + { + return ($this->requestHandler)($request); + } + }; + + $this->requestHandler = $requestHandler; + return $this; + } + + /** + * Reset middlewares. + * @return $this + */ + public function resetMiddlewares(): static + { + $this->middlewares = []; + return $this; + } + + /** + * Add default middlewares. + * They are added in the constructor, so if you didn't reset the middlewares, running this isn't required. + * @return $this + */ + public function addDefaultMiddlewares(): static + { + $this->middlewares[] = new ErrorsMiddleware($this); + return $this; + } + + /** + * Add a new middleware to the stack. + * What is added first is called first in request handling. + * @param (callable(ServerRequestInterface, RequestHandlerInterface): ResponseInterface)|MiddlewareInterface $middleware + * @return $this + */ + public function addMiddleware(callable|MiddlewareInterface $middleware): static + { + if (!($middleware instanceof MiddlewareInterface)) + // Convert a callable to a middleware handler interface. + $middleware = new class($middleware) implements MiddlewareInterface { + private $middleware; + + public function __construct(callable $middleware) + { + $this->middleware = $middleware; + } + + /** + * @inheritDoc + */ + #[Override] public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + return ($this->middleware)($request, $handler); + } + }; + + $this->middlewares[] = $middleware; + return $this; + } + + /** + * Get HTTP debug status. + * @return bool HTTP debug status. + */ + public function getDebug(): bool + { + return $this->debug; + } + + /** + * Get exception renderer instance. + * @return string + */ + public function getExceptionRenderer(): string + { + return $this->exceptionRenderer; + } + + /** + * Get configured request handler. + * @return RequestHandlerInterface + */ + public function getRequestHandler(): RequestHandlerInterface + { + return $this->requestHandler; + } + + /** + * Get configured middlewares stack. + * @return MiddlewareInterface[] + */ + public function getMiddlewares(): array + { + return $this->middlewares; + } +} diff --git a/src/HttpService.php b/src/HttpService.php new file mode 100644 index 0000000..c01146f --- /dev/null +++ b/src/HttpService.php @@ -0,0 +1,36 @@ +http = new Http($this, $this->getServiceConfiguration(HttpConfiguration::class)); + } + + /** + * Get the HTTP manager. + * @return Http HTTP manager instance. + */ + public function http(): Http + { + return $this->http; + } +} diff --git a/src/Renderer/PhpRenderer.php b/src/Renderer/PhpRenderer.php new file mode 100644 index 0000000..7713049 --- /dev/null +++ b/src/Renderer/PhpRenderer.php @@ -0,0 +1,31 @@ +viewPath; + + // Return buffered body. + return ob_get_clean(); + } +} diff --git a/src/Renderer/Renderer.php b/src/Renderer/Renderer.php new file mode 100644 index 0000000..5c38daf --- /dev/null +++ b/src/Renderer/Renderer.php @@ -0,0 +1,16 @@ +app->http()->getRequest(); + } + + /** + * Create a new response with the given status code and data. + * @param StatusCode $statusCode The status code to respond. + * @param string|null $contentType The content type of the response. + * @param string|resource|StreamInterface|null $body The body to respond. + * @return ResponseInterface The new response. + */ + public function newResponse(StatusCode $statusCode = StatusCode::OK, ?string $contentType = null, mixed $body = null): ResponseInterface + { + // Initialize headers. + $headers = []; + if (!empty($contentType)) $headers[Header::ContentType->value] = $contentType; + + return new Response( + // Pass the status code. + status: $statusCode->value, + // Pass headers. + headers: $headers, + // Pass the raw body. + body: $body, + ); + } + + /** + * Build a text response. + * @param string $value The text value to pass. + * @param StatusCode $statusCode The status code to send back. OK by default. + * @return ResponseInterface Built response. + */ + public function text(string $value, StatusCode $statusCode = StatusCode::OK): ResponseInterface + { + return $this->newResponse($statusCode, ContentType::Text->value, $value); + } + + /** + * Build a JSON response. + * @param mixed $value The value to serialize. + * @param StatusCode $statusCode The status code to send back. OK by default. + * @return ResponseInterface Built response. + */ + public function json(mixed $value, StatusCode $statusCode = StatusCode::OK): ResponseInterface + { + return $this->newResponse($statusCode, ContentType::Json->value, json_encode($value)); + } + + /** + * Build an XML response. + * @param DOMDocument $value The XML value to serialize. + * @param StatusCode $statusCode The status code to send back. OK by default. + * @return ResponseInterface Built response. + */ + public function xml(DOMDocument $value, StatusCode $statusCode = StatusCode::OK): ResponseInterface + { + return $this->newResponse($statusCode, ContentType::Json->value, $value->saveXML()); + } + + /** + * Build a rendered response. + * @param Renderer $renderer The renderer instance to use. + * @param object $arguments Arguments to use when rendering. + * @param StatusCode $statusCode The status code to send back. OK by default. + * @param string|null $contentType The content type of the response. + * @return ResponseInterface Built response. + */ + public function render(Renderer $renderer, object $arguments, StatusCode $statusCode = StatusCode::OK, ?string $contentType = null): ResponseInterface + { + return $this->newResponse($statusCode, $contentType ?? null, $renderer->render($arguments)); + } +} diff --git a/src/Router/Route.php b/src/Router/Route.php new file mode 100644 index 0000000..8c7bae2 --- /dev/null +++ b/src/Router/Route.php @@ -0,0 +1,166 @@ +methods, ...$method); + return $this; + } + /** + * Allow all request methods. + * @return $this + */ + public function allMethods(): static + { + $this->methods = []; + return $this; + } + /** + * Allow GET request method. + * @return $this + */ + public function get(): static + { + return $this->method("GET"); + } + /** + * Allow POST request method. + * @return $this + */ + public function post(): static + { + return $this->method("POST"); + } + /** + * Allow PATCH request method. + * @return $this + */ + public function patch(): static + { + return $this->method("PATCH"); + } + /** + * Allow DELETE request method. + * @return $this + */ + public function delete(): static + { + return $this->method("DELETE"); + } + + /** + * Reset allowed servers names to all by default. + * @return $this + */ + public function resetServersNames(): static + { + $this->serversNames = []; + return $this; + } + /** + * Add an allowed server name for the given route. + * @return $this + */ + public function serverName(string $serverName): static + { + $this->serversNames[] = $serverName; + return $this; + } + + /** + * Set request handler for given route. + * @param callable(mixed...): ResponseInterface $handler Request handler. Will be called with parameters in defined URI. + * @return $this + */ + public function handler(callable $handler): static + { + $this->handler = $handler; + return $this; + } + + /** + * Set a controller method as handler. + * @param string $controllerClass The controller class. + * @param string $controllerMethod The method to call in the controller. + * @return $this + */ + public function controller(string $controllerClass, string $controllerMethod): static + { + $this->handler = function (...$args) use ($controllerClass, $controllerMethod) { + return call_user_func_array([new $controllerClass(Application::get()), $controllerMethod], $args); + }; + return $this; + } + + /** + * Get route URI. + * @return string + */ + public function getUri(): string + { + return $this->uri; + } + + /** + * Get request handler for given route. + * @return callable(mixed...): ResponseInterface Request handler. Will be called with parameters in defined URI. + */ + public function getHandler(): callable + { + return $this->handler; + } + + /** + * Check if the route matches a request. + * @param ServerRequestInterface $request The request to match. + * @return bool True if the route matches the request. + */ + public function match(ServerRequestInterface $request): bool + { + return (empty($this->methods) || in_array($request->getMethod(), $this->methods)) && + (empty($this->serversNames) || in_array($request->getUri()->getHost(), $this->serversNames)) && + !empty($this->handler); + } +} diff --git a/src/Router/RouteMatch.php b/src/Router/RouteMatch.php new file mode 100644 index 0000000..cadead9 --- /dev/null +++ b/src/Router/RouteMatch.php @@ -0,0 +1,27 @@ + $params Matched URL parameters. + */ + public function __construct(public Route $route, public array $params) + {} + + /** + * Call the matched route handler with parameters. + * @return ResponseInterface The retrieved response from route. + */ + public function __invoke(): ResponseInterface + { + return call_user_func_array($this->route->getHandler(), $this->params); + } +} diff --git a/src/Router/Router.php b/src/Router/Router.php new file mode 100644 index 0000000..c92386a --- /dev/null +++ b/src/Router/Router.php @@ -0,0 +1,35 @@ +routes->define(); + } + + /** + * Handle the request with the right route, if there is one. + * @param ServerRequestInterface $request The request to handle. + * @return ResponseInterface The request response. + * @throws NotFoundException + */ + public function __invoke(ServerRequestInterface $request): ResponseInterface + { + $route = $this->routes->match($request); + return $route(); + } +} diff --git a/src/Router/Routes.php b/src/Router/Routes.php new file mode 100644 index 0000000..9dda8b9 --- /dev/null +++ b/src/Router/Routes.php @@ -0,0 +1,79 @@ +tree = new RoutesTree(""); + } + + /** + * Split the given URI it in route nodes names. + * @return string[] Routes nodes names. + */ + private function splitUri(string $uri): array + { + $uri = trim($uri, "/ \n\r\t\v\0"); + return array_map(fn ($node) => trim($node, "/ \n\r\t\v\0"), explode("/", $uri)); + } + + /** + * Add a new route for the given URI. + * @param Route $route The route to add. + * @return void + */ + protected function addRoute(Route $route): void + { + $nodes = $this->splitUri($route->getUri()); + // Register the new route with its split URI. + $this->tree->register($nodes, $route); + } + + /** + * Define a new route. + * @param string $uri The route URI. + * @return Route The defined route. + */ + public function route(string $uri): Route + { + // Create a route with the given URI and add it to the list. + $this->addRoute($route = new Route($uri)); + + // Return created route. + return $route; + } + + /** + * Try to match a defined route from the given request. + * @param ServerRequestInterface $request The request to match. + * @return RouteMatch The matched route. + * @throws NotFoundException + */ + public function match(ServerRequestInterface $request): RouteMatch + { + return $this->tree->match($this->splitUri($request->getUri()), $request); + } +} diff --git a/src/Router/RoutesTree.php b/src/Router/RoutesTree.php new file mode 100644 index 0000000..7449b26 --- /dev/null +++ b/src/Router/RoutesTree.php @@ -0,0 +1,158 @@ + + */ + protected array $staticChildren = []; + + /** + * Dynamic children of the current node. + * @var array + */ + protected array $dynamicChildren = []; + + /** + * Create a new route tree structure for the given node. + * @param string $node Tree node name. + */ + public function __construct(string $node) + { + $this->node = $node; + } + + /** + * Determine if the current node is dynamic or static. + * @return bool + */ + public function isDynamic(): bool + { + return $this->node[0] == ':'; + } + + /** + * Find the matching route for the given request. + * @param string[] $nodes The split URI of the route to match. + * @param ServerRequestInterface $request The request to match. + * @param array $params URL parameters values. + * @return RouteMatch Matching route. + * @throws NotFoundException + */ + public function match(array $nodes, ServerRequestInterface $request, array $params = []): RouteMatch + { + // Get the current subnode. + $subNode = array_shift($nodes); + if (!empty($subNode)) + // Trim the current URI part. + $subNode = rawurldecode(trim($subNode, "/ \n\r\t\v\0")); + + if (!empty($subNode)) + { // There is a subnode, try to match a child. + if (!empty($this->staticChildren[$subNode])) + { // Match a static child, trying to match on it. + try + { + return $this->staticChildren[$subNode]->match($nodes, $request, $params); + } + catch (NotFoundException $notFound) + { // No match, trying to find another one. + } + } + + foreach ($this->dynamicChildren as $varName => $child) + { // Try to match every dynamic child. + try + { + // Add subnode as route parameter. + $params[substr($varName, 1)] = $subNode; + return $child->match($nodes, $request, $params); + } + catch (NotFoundException $notFound) + { // No match, trying to find another one. + } + } + + // Still no match, throw a not found exception. + throw new NotFoundException(); + } + else + { // No subnode, the matched route can be in this node. + foreach ($this->routes as $route) + { // Trying each registered route for this node. + if ($route->match($request)) + // Found a matching route. + return new RouteMatch($route, $params); + } + + // No matching route found. + throw new NotFoundException(); + } + } + + /** + * Get any child with the given subnode name, if there is one. + * @param string $subnode Subnode name. + * @return RoutesTree|null The routes tree with the given subnode, or NULL if there's none. + */ + public function anyChild(string $subnode): ?RoutesTree + { + return $this->staticChildren[$subnode] ?? $this->dynamicChildren[$subnode] ?? null; + } + + /** + * Register a new route for the given URI. + * @param string[] $nodes The split URI of the route to register. + * @param Route $route The route to register. + * @return void + */ + public function register(array $nodes, Route $route): void + { + // Get the current subnode. + $subNode = array_shift($nodes); + if (!empty($subNode)) + // Trim the current URI part. + $subNode = trim($subNode, "/ \n\r\t\v\0"); + + if (!empty($subNode)) + { // There is a subnode to register, creating or getting its route tree and call register recursively. + $child = $this->anyChild($subNode); + if (empty($child)) + { // No child for the current subnode, creating a new one. + $child = new RoutesTree($subNode); + if ($child->isDynamic()) + $this->dynamicChildren[$subNode] = $child; + else + $this->staticChildren[$subNode] = $child; + } + + // Register the route in the child tree. + $child->register($nodes, $route); + } + else + { // We reached the end of the nodes list, registering the route in this tree. + $this->routes[] = $route; + } + } +} diff --git a/src/StatusCode.php b/src/StatusCode.php new file mode 100644 index 0000000..b699358 --- /dev/null +++ b/src/StatusCode.php @@ -0,0 +1,79 @@ + + + + + <?= $exception->getErrorId() ?> + + + + + + getStatusCode() ?> getErrorId() ?> + +
getMessage() ?>
+ + + +