Initialize Nest HTTP library.

This commit is contained in:
Madeorsk 2024-11-08 16:10:38 +01:00
commit 35418919ed
Signed by: Madeorsk
GPG key ID: 677E51CA765BB79F
26 changed files with 1896 additions and 0 deletions

6
.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
# IDEA
.idea/
*.iml
# Composer
vendor/

37
composer.json Normal file
View file

@ -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": "*"
}
}

520
composer.lock generated Normal file
View file

@ -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"
}

13
src/ContentType.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace Nest\Http;
/**
* Typical content types.
* A generic function shouldn't use it as an enforced parameters as content types can be fully customized.
*/
enum ContentType: string
{
case Json = "application/json";
case Text = "plain/text";
}

63
src/ErrorsMiddleware.php Normal file
View file

@ -0,0 +1,63 @@
<?php
namespace Nest\Http;
use Nest\Http\Exceptions\HttpException;
use Nest\Http\Exceptions\InternalErrorException;
use Nyholm\Psr7\Response;
use Override;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use Whoops\Handler\PrettyPageHandler;
use Whoops\Run as Whoops;
/**
* Default errors handler middleware.
*/
class ErrorsMiddleware implements MiddlewareInterface
{
/**
* @param HttpConfiguration $configuration HTTP service configuration instance.
*/
public function __construct(protected HttpConfiguration $configuration)
{
}
/**
* @inheritDoc
*/
#[Override] public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
try
{
return $handler->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());
}
}
}

39
src/ExceptionRenderer.php Normal file
View file

@ -0,0 +1,39 @@
<?php
namespace Nest\Http;
use Nest\Http\Exceptions\HttpException;
use Nest\Http\Renderer\PhpRenderer;
/**
* Render an HTTP exception.
*/
class ExceptionRenderer
{
/**
* @param HttpException $exception The exception to render.
*/
public function __construct(protected HttpException $exception)
{}
/**
* Return the status code for the exception.
* @return int Exception status code.
*/
public function getStatusCode(): int
{
return $this->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,
]);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Nest\Http\Exceptions;
use Nest\Http\StatusCode;
/**
* HTTP client error exception.
*/
class ClientException extends HttpException
{
/**
* @inheritDoc
*/
#[\Override] public function getStatusCode(): int
{
return StatusCode::BadRequest->value;
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Nest\Http\Exceptions;
use Nest\Exceptions\Exception;
/**
* An HTTP exception.
*/
abstract class HttpException extends Exception
{
/**
* Get the HTTP status code to use when this exception is thrown.
* @return int Status code to return.
*/
public abstract function getStatusCode(): int;
}

View file

@ -0,0 +1,16 @@
<?php
namespace Nest\Http\Exceptions;
use Nest\Http\StatusCode;
/**
* HTTP internal error exception.
*/
class InternalErrorException extends ClientException
{
public function getStatusCode(): int
{
return StatusCode::InternalServerError->value;
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace Nest\Http\Exceptions;
use Nest\Http\StatusCode;
/**
* HTTP not found exception.
*/
class NotFoundException extends ClientException
{
public function getStatusCode(): int
{
return StatusCode::NotFound->value;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Nest\Http\Exceptions;
use Nest\Http\StatusCode;
/**
* HTTP server error exception.
*/
class ServerException extends HttpException
{
/**
* @inheritDoc
*/
#[\Override] public function getStatusCode(): int
{
return StatusCode::InternalServerError->value;
}
}

13
src/Header.php Normal file
View file

@ -0,0 +1,13 @@
<?php
namespace Nest\Http;
/**
* Typical headers.
* A generic function shouldn't use it as an enforced parameters as headers can be fully customized.
*/
enum Header: string
{
case Authorization = "Authorization";
case ContentType = "ContentType";
}

132
src/Http.php Normal file
View file

@ -0,0 +1,132 @@
<?php
namespace Nest\Http;
use Nest\Application;
use Nyholm\Psr7\ServerRequest;
use Override;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* HTTP service manager.
*/
class Http
{
/**
* Current request instance.
* @var ServerRequestInterface
*/
private ServerRequestInterface $request;
/**
* @param Application $application The application.
* @param HttpConfiguration $configuration HTTP service configuration.
*/
public function __construct(protected Application $application, protected HttpConfiguration $configuration)
{
}
/**
* Get HTTP debug status.
* @return bool HTTP debug status.
*/
public function getDebug(): bool
{
return $this->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;
}
}

191
src/HttpConfiguration.php Normal file
View file

@ -0,0 +1,191 @@
<?php
namespace Nest\Http;
use Nest\Exceptions\InvalidTypeException;
use Nest\Services\ServiceConfiguration;
use Override;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function Nest\Utils\expect_type;
/**
* HTTP configuration class.
*/
class HttpConfiguration extends ServiceConfiguration
{
/**
* HTTP debug mode.
* This is NOT suitable for production as it outputs clearly some code and may share secrets.
* @var bool
*/
private bool $debug = false;
/**
* HTTP exception renderer class.
* @var string
*/
private string $exceptionRenderer = ExceptionRenderer::class;
/**
* The main request handler.
* @var RequestHandlerInterface
*/
private RequestHandlerInterface $requestHandler;
/**
* List of global middlewares.
* @var MiddlewareInterface[]
*/
private array $middlewares = [];
public function __construct()
{
// Add default middlewares.
$this->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;
}
}

36
src/HttpService.php Normal file
View file

@ -0,0 +1,36 @@
<?php
namespace Nest\Http;
use Nest\Exceptions\Services\Configuration\UndefinedServiceConfigurationException;
/**
* HTTP Nest service.
*/
trait HttpService
{
/**
* HTTP manager.
* @var Http
*/
private Http $http;
/**
* @return void
* @throws UndefinedServiceConfigurationException
*/
protected function __nest__HttpService(): void
{
// Initialize HTTP manager.
$this->http = new Http($this, $this->getServiceConfiguration(HttpConfiguration::class));
}
/**
* Get the HTTP manager.
* @return Http HTTP manager instance.
*/
public function http(): Http
{
return $this->http;
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Nest\Http\Renderer;
/**
* Plain PHP body renderer.
*/
class PhpRenderer implements Renderer
{
/**
* @param string $viewPath Path of the view to use when rendering.
*/
public function __construct(protected string $viewPath)
{}
/**
* @inheritDoc
*/
public function render(object $arguments): string
{
// Extract render arguments.
extract((array) $arguments);
// Start rendering while buffering the output.
ob_start();
include $this->viewPath;
// Return buffered body.
return ob_get_clean();
}
}

16
src/Renderer/Renderer.php Normal file
View file

@ -0,0 +1,16 @@
<?php
namespace Nest\Http\Renderer;
/**
* Renderer interface.
*/
interface Renderer
{
/**
* Render a view with the given arguments.
* @param object $arguments View arguments.
* @return string Produced view.
*/
function render(object $arguments): string;
}

View file

@ -0,0 +1,12 @@
<?php
namespace Nest\Http;
/**
* HTTP request authentication data.
*/
class RequestAuthentication
{
public function __construct(public ?string $type, public ?string $user, public ?string $password)
{}
}

105
src/Router/Controller.php Normal file
View file

@ -0,0 +1,105 @@
<?php
namespace Nest\Http\Router;
use DOMDocument;
use Nest\Application;
use Nest\Http\ContentType;
use Nest\Http\Header;
use Nest\Http\Renderer\Renderer;
use Nest\Http\StatusCode;
use Nyholm\Psr7\Response;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;
use Stringable;
/**
* An abstract HTTP route controller.
*/
abstract class Controller
{
/**
* @param Application $app The application.
*/
public function __construct(protected Application $app)
{}
/**
* Get current request.
* @return ServerRequestInterface The request instance.
*/
public function getRequest(): ServerRequestInterface
{
return $this->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));
}
}

166
src/Router/Route.php Normal file
View file

@ -0,0 +1,166 @@
<?php
namespace Nest\Http\Router;
use Nest\Application;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Route definition class.
*/
class Route
{
/**
* List of allowed request methods.
* All methods are allowed by default (empty array).
* @var string[]
*/
protected array $methods = [];
/**
* List of allowed servers names.
* All servers names are allowed by default (empty array).
* @var string[]
*/
protected array $serversNames = [];
/**
* Request handler for defined route (URI parameters as function parameters).
* @var callable(mixed...): ResponseInterface
*/
protected $handler;
/**
* Create a route from its URI.
* @param string $uri Route URI.
*/
public function __construct(protected string $uri)
{}
/**
* Add allowed request methods (GET, POST, etc.).
* @param string ...$method Allowed method.
* @return $this
*/
public function method(string ...$method): static
{
array_push($this->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);
}
}

27
src/Router/RouteMatch.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace Nest\Http\Router;
use Psr\Http\Message\ResponseInterface;
/**
* A matched route result.
*/
readonly class RouteMatch
{
/**
* @param Route $route Matched route.
* @param array<string, string> $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);
}
}

35
src/Router/Router.php Normal file
View file

@ -0,0 +1,35 @@
<?php
namespace Nest\Http\Router;
use Nest\Http\Exceptions\NotFoundException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
/**
* Router HTTP request handler.
*/
class Router
{
/**
* Define a new router request handler from the given routes.
* @param Routes $routes Routes instance.
*/
public function __construct(protected Routes $routes)
{
// Define routes.
$this->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();
}
}

79
src/Router/Routes.php Normal file
View file

@ -0,0 +1,79 @@
<?php
namespace Nest\Http\Router;
use Nest\Http\Exceptions\NotFoundException;
use Psr\Http\Message\ServerRequestInterface;
/**
* Routes definition class.
*/
abstract class Routes
{
/**
* Define routes.
* @return void
*/
public abstract function define(): void;
/**
* Main routes tree node.
* @var RoutesTree
*/
protected RoutesTree $tree;
/**
* Create a new routes definition object.
*/
public function __construct()
{
$this->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);
}
}

158
src/Router/RoutesTree.php Normal file
View file

@ -0,0 +1,158 @@
<?php
namespace Nest\Http\Router;
use Nest\Http\Exceptions\NotFoundException;
use Psr\Http\Message\ServerRequestInterface;
/**
* Routes tree structure class.
*/
class RoutesTree
{
/**
* Current node (static or dynamic name).
* @var string
*/
protected string $node;
/**
* The defined routes for the current node.
* @var Route[]
*/
protected array $routes = [];
/**
* Static children of the current node.
* @var array<string, RoutesTree>
*/
protected array $staticChildren = [];
/**
* Dynamic children of the current node.
* @var array<string, RoutesTree>
*/
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<string, string> $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;
}
}
}

79
src/StatusCode.php Normal file
View file

@ -0,0 +1,79 @@
<?php
namespace Nest\Http;
/**
* An HTTP status code.
*/
enum StatusCode: int
{
case Continue = 100;
case SwitchingProtocols = 101;
case Processing = 102;
case EarlyHints = 103;
case OK = 200;
case Created = 201;
case Accepted = 202;
case NonAuthoritativeInformation = 203;
case NoContent = 204;
case ResetContent = 205;
case PartialContent = 206;
case MultiStatus = 207;
case AlreadyReported = 208;
case IMUsed = 226;
case MultipleChoices = 300;
case MovedPermanently = 301;
case Found = 302;
case SeeOther = 303;
case NotModified = 304;
/**
* @deprecated
*/
case UseProxy = 305;
case TemporaryRedirect = 307;
case PermanentRedirect = 308;
case BadRequest = 400;
case Unauthorized = 401;
case PaymentRequired = 402;
case Forbidden = 403;
case NotFound = 404;
case MethodNotAllowed = 405;
case NotAcceptable = 406;
case ProxyAuthenticationRequired = 407;
case RequestTimeout = 408;
case Conflict = 409;
case Gone = 410;
case LengthRequired = 411;
case PreconditionFailed = 412;
case ContentTooLarge = 413;
case UriTooLong = 414;
case UnsupportedMediaType = 415;
case RangeNotSatisfiable = 416;
case ExpectationFailed = 417;
case Teapot = 418;
case MisdirectedRequest = 421;
case UnprocessableContent = 422;
case Locked = 423;
case FailedDependency = 424;
case TooEarly = 425;
case UpgradeRequired = 426;
case PreconditionRequired = 428;
case TooManyRequests = 429;
case HeaderFieldsTooLarge = 431;
case UnavailableForLegalReasons = 451;
case InternalServerError = 500;
case NotImplemented = 501;
case BadGateway = 502;
case ServiceUnavailable = 503;
case GatewayTimeout = 504;
case HttpVersionNotSupported = 505;
case VariantAlsoNegotiates = 506;
case InsufficientStorage = 507;
case LoopDetected = 508;
case NotExtended = 510;
case NetworkAuthenticationRequired = 511;
}

51
src/views/exception.php Normal file
View file

@ -0,0 +1,51 @@
<?php
/**
* @var \Nest\Http\Exceptions\HttpException $exception
*/
?>
<html lang="en">
<head>
<title><?= $exception->getErrorId() ?></title>
<style>
html, body
{
font-family: system-ui, sans-serif;
}
strong
{
display: block;
margin: 1em auto;
font-size: 1.5em;
text-align: center;
}
pre
{
font-size: 1rem;
font-family: monospace;
text-align: center;
}
footer
{
margin: 4em auto;
color: grey;
font-size: 0.8em;
text-align: center;
}
</style>
</head>
<body>
<strong>
<span class="code"><?= $exception->getStatusCode() ?></span> <span class="id"><?= $exception->getErrorId() ?></span>
</strong>
<pre class="message"><?= $exception->getMessage() ?></pre>
<footer>
Nest
</footer>
</body>
</html>