From 569d828e80e5d77a7192d2861187ec8a4da5e027 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 6 Jan 2023 11:18:42 +0100 Subject: [PATCH 001/180] new http exceptions --- .../Http/HttpBadRequestException.php | 45 +++++++ src/Exception/Http/HttpConflictException.php | 44 ++++++ src/Exception/Http/HttpException.php | 57 ++++++++ src/Exception/Http/HttpExceptionInterface.php | 34 +++++ .../Http/HttpExpectationFailedException.php | 44 ++++++ .../Http/HttpFailedDependencyException.php | 42 ++++++ src/Exception/Http/HttpForbiddenException.php | 45 +++++++ src/Exception/Http/HttpGoneException.php | 47 +++++++ .../Http/HttpInternalServerErrorException.php | 44 ++++++ .../Http/HttpLengthRequiredException.php | 44 ++++++ src/Exception/Http/HttpLockedException.php | 42 ++++++ .../Http/HttpMethodNotAllowedException.php | 126 ++++++++++++++++++ .../Http/HttpMisdirectedRequestException.php | 43 ++++++ .../Http/HttpNotAcceptableException.php | 45 +++++++ src/Exception/Http/HttpNotFoundException.php | 47 +++++++ .../Http/HttpPayloadTooLargeException.php | 45 +++++++ .../Http/HttpPaymentRequiredException.php | 45 +++++++ .../Http/HttpPreconditionFailedException.php | 44 ++++++ .../HttpPreconditionRequiredException.php | 46 +++++++ ...tpProxyAuthenticationRequiredException.php | 44 ++++++ .../Http/HttpRangeNotSatisfiableException.php | 45 +++++++ ...tpRequestHeaderFieldsTooLargeException.php | 45 +++++++ .../Http/HttpRequestTimeoutException.php | 47 +++++++ .../Http/HttpServiceUnavailableException.php | 48 +++++++ src/Exception/Http/HttpTooEarlyException.php | 44 ++++++ .../Http/HttpTooManyRequestsException.php | 44 ++++++ .../Http/HttpUnauthorizedException.php | 45 +++++++ ...ttpUnavailableForLegalReasonsException.php | 45 +++++++ .../Http/HttpUnprocessableEntityException.php | 44 ++++++ .../HttpUnsupportedMediaTypeException.php | 121 +++++++++++++++++ .../Http/HttpUpgradeRequiredException.php | 46 +++++++ .../Http/HttpUriTooLongException.php | 44 ++++++ 32 files changed, 1591 insertions(+) create mode 100644 src/Exception/Http/HttpBadRequestException.php create mode 100644 src/Exception/Http/HttpConflictException.php create mode 100644 src/Exception/Http/HttpException.php create mode 100644 src/Exception/Http/HttpExceptionInterface.php create mode 100644 src/Exception/Http/HttpExpectationFailedException.php create mode 100644 src/Exception/Http/HttpFailedDependencyException.php create mode 100644 src/Exception/Http/HttpForbiddenException.php create mode 100644 src/Exception/Http/HttpGoneException.php create mode 100644 src/Exception/Http/HttpInternalServerErrorException.php create mode 100644 src/Exception/Http/HttpLengthRequiredException.php create mode 100644 src/Exception/Http/HttpLockedException.php create mode 100644 src/Exception/Http/HttpMethodNotAllowedException.php create mode 100644 src/Exception/Http/HttpMisdirectedRequestException.php create mode 100644 src/Exception/Http/HttpNotAcceptableException.php create mode 100644 src/Exception/Http/HttpNotFoundException.php create mode 100644 src/Exception/Http/HttpPayloadTooLargeException.php create mode 100644 src/Exception/Http/HttpPaymentRequiredException.php create mode 100644 src/Exception/Http/HttpPreconditionFailedException.php create mode 100644 src/Exception/Http/HttpPreconditionRequiredException.php create mode 100644 src/Exception/Http/HttpProxyAuthenticationRequiredException.php create mode 100644 src/Exception/Http/HttpRangeNotSatisfiableException.php create mode 100644 src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php create mode 100644 src/Exception/Http/HttpRequestTimeoutException.php create mode 100644 src/Exception/Http/HttpServiceUnavailableException.php create mode 100644 src/Exception/Http/HttpTooEarlyException.php create mode 100644 src/Exception/Http/HttpTooManyRequestsException.php create mode 100644 src/Exception/Http/HttpUnauthorizedException.php create mode 100644 src/Exception/Http/HttpUnavailableForLegalReasonsException.php create mode 100644 src/Exception/Http/HttpUnprocessableEntityException.php create mode 100644 src/Exception/Http/HttpUnsupportedMediaTypeException.php create mode 100644 src/Exception/Http/HttpUpgradeRequiredException.php create mode 100644 src/Exception/Http/HttpUriTooLongException.php diff --git a/src/Exception/Http/HttpBadRequestException.php b/src/Exception/Http/HttpBadRequestException.php new file mode 100644 index 00000000..efdd06bd --- /dev/null +++ b/src/Exception/Http/HttpBadRequestException.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Bad Request Exception + * + * The server cannot or will not process the request due to something that is perceived to be a client error (e.g., + * malformed request syntax, invalid request message framing, or deceptive request routing). + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400 + * + * @since 3.0.0 + */ +class HttpBadRequestException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Bad Request'; + + parent::__construct(self::STATUS_BAD_REQUEST, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpConflictException.php b/src/Exception/Http/HttpConflictException.php new file mode 100644 index 00000000..6e5115fa --- /dev/null +++ b/src/Exception/Http/HttpConflictException.php @@ -0,0 +1,44 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Conflict Exception + * + * This response is sent when a request conflicts with the current state of the server. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409 + * + * @since 3.0.0 + */ +class HttpConflictException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Conflict'; + + parent::__construct(self::STATUS_CONFLICT, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpException.php b/src/Exception/Http/HttpException.php new file mode 100644 index 00000000..4987e8dd --- /dev/null +++ b/src/Exception/Http/HttpException.php @@ -0,0 +1,57 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Exception; +use Throwable; + +/** + * Base HTTP exception + * + * @since 3.0.0 + */ +class HttpException extends Exception implements HttpExceptionInterface +{ + + /** + * HTTP status code + * + * @var int + */ + private int $statusCode; + + /** + * Constructor of the class + * + * @param int $statusCode + * @param string $message + * @param int $errorCode + * @param ?Throwable $previous + */ + public function __construct(int $statusCode, string $message, int $errorCode = 0, ?Throwable $previous = null) + { + parent::__construct($message, $errorCode, $previous); + + $this->statusCode = $statusCode; + } + + /** + * {@inheritdoc} + */ + final public function getStatusCode(): int + { + return $this->statusCode; + } +} diff --git a/src/Exception/Http/HttpExceptionInterface.php b/src/Exception/Http/HttpExceptionInterface.php new file mode 100644 index 00000000..f56a38a7 --- /dev/null +++ b/src/Exception/Http/HttpExceptionInterface.php @@ -0,0 +1,34 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Fig\Http\Message\StatusCodeInterface; +use Sunrise\Http\Router\Exception\ExceptionInterface; + +/** + * Base HTTP exception interface + * + * @since 3.0.0 + */ +interface HttpExceptionInterface extends ExceptionInterface, StatusCodeInterface +{ + + /** + * Gets HTTP status code + * + * @return int + */ + public function getStatusCode(): int; +} diff --git a/src/Exception/Http/HttpExpectationFailedException.php b/src/Exception/Http/HttpExpectationFailedException.php new file mode 100644 index 00000000..4d976867 --- /dev/null +++ b/src/Exception/Http/HttpExpectationFailedException.php @@ -0,0 +1,44 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Expectation Failed Exception + * + * This response code means the expectation indicated by the Expect request header field cannot be met by the server. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417 + * + * @since 3.0.0 + */ +class HttpExpectationFailedException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Expectation Failed'; + + parent::__construct(self::STATUS_EXPECTATION_FAILED, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpFailedDependencyException.php b/src/Exception/Http/HttpFailedDependencyException.php new file mode 100644 index 00000000..c895fd6a --- /dev/null +++ b/src/Exception/Http/HttpFailedDependencyException.php @@ -0,0 +1,42 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Failed Dependency Exception + * + * The request failed due to failure of a previous request. + * + * @since 3.0.0 + */ +class HttpFailedDependencyException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Failed Dependency'; + + parent::__construct(self::STATUS_FAILED_DEPENDENCY, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpForbiddenException.php b/src/Exception/Http/HttpForbiddenException.php new file mode 100644 index 00000000..a1b03951 --- /dev/null +++ b/src/Exception/Http/HttpForbiddenException.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Forbidden Exception + * + * The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give + * the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403 + * + * @since 3.0.0 + */ +class HttpForbiddenException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Forbidden'; + + parent::__construct(self::STATUS_FORBIDDEN, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpGoneException.php b/src/Exception/Http/HttpGoneException.php new file mode 100644 index 00000000..0eae223b --- /dev/null +++ b/src/Exception/Http/HttpGoneException.php @@ -0,0 +1,47 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Gone Exception + * + * This response is sent when the requested content has been permanently deleted from server, with no forwarding + * address. Clients are expected to remove their caches and links to the resource. The HTTP specification intends this + * status code to be used for "limited-time, promotional services". APIs should not feel compelled to indicate resources + * that have been deleted with this status code. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410 + * + * @since 3.0.0 + */ +class HttpGoneException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Gone'; + + parent::__construct(self::STATUS_GONE, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpInternalServerErrorException.php b/src/Exception/Http/HttpInternalServerErrorException.php new file mode 100644 index 00000000..9f43458e --- /dev/null +++ b/src/Exception/Http/HttpInternalServerErrorException.php @@ -0,0 +1,44 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Internal Server Error Exception + * + * The server has encountered a situation it does not know how to handle. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 + * + * @since 3.0.0 + */ +class HttpInternalServerErrorException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Internal Server Error'; + + parent::__construct(self::STATUS_INTERNAL_SERVER_ERROR, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpLengthRequiredException.php b/src/Exception/Http/HttpLengthRequiredException.php new file mode 100644 index 00000000..bf1e58cf --- /dev/null +++ b/src/Exception/Http/HttpLengthRequiredException.php @@ -0,0 +1,44 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Length Required Exception + * + * Server rejected the request because the Content-Length header field is not defined and the server requires it. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411 + * + * @since 3.0.0 + */ +class HttpLengthRequiredException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Length Required'; + + parent::__construct(self::STATUS_LENGTH_REQUIRED, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpLockedException.php b/src/Exception/Http/HttpLockedException.php new file mode 100644 index 00000000..6b280521 --- /dev/null +++ b/src/Exception/Http/HttpLockedException.php @@ -0,0 +1,42 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Locked Exception + * + * The resource that is being accessed is locked. + * + * @since 3.0.0 + */ +class HttpLockedException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Locked'; + + parent::__construct(self::STATUS_LOCKED, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpMethodNotAllowedException.php b/src/Exception/Http/HttpMethodNotAllowedException.php new file mode 100644 index 00000000..b70663cc --- /dev/null +++ b/src/Exception/Http/HttpMethodNotAllowedException.php @@ -0,0 +1,126 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * Import functions + */ +use function join; + +/** + * HTTP Method Not Allowed Exception + * + * The request method is known by the server but is not supported by the target resource. For example, an API may not + * allow calling DELETE to remove a resource. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 + * + * @since 3.0.0 + */ +class HttpMethodNotAllowedException extends HttpException +{ + + /** + * Unallowed HTTP method + * + * @var string + */ + private string $unallowedMethod; + + /** + * Allowed HTTP methods + * + * @var list + */ + private array $allowedMethods; + + /** + * Constructor of the class + * + * @param string $unallowedMethod + * @param list $allowedMethods + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct( + string $unallowedMethod, + array $allowedMethods, + ?string $message = null, + int $code = 0, + ?Throwable $previous = null + ) { + $message ??= 'Method Not Allowed'; + + parent::__construct(self::STATUS_METHOD_NOT_ALLOWED, $message, $code, $previous); + + $this->unallowedMethod = $unallowedMethod; + $this->allowedMethods = $allowedMethods; + } + + /** + * Gets unallowed HTTP method + * + * @return string + */ + final public function getMethod(): string + { + return $this->unallowedMethod; + } + + /** + * Gets allowed HTTP methods + * + * @return list + */ + final public function getAllowedMethods(): array + { + return $this->allowedMethods; + } + + /** + * Gets joined allowed HTTP methods + * + * @return string + */ + final public function getJoinedAllowedMethods(): string + { + return join(',', $this->getAllowedMethods()); + } + + /** + * Gets arguments for an Allow header field + * + * The server must generate an Allow header field in a 405 status code response. + * + * The field must contain a list of methods that the target resource currently supports. + * + * Returns an array where key 0 contains the header name and key 1 contains its value. + * + * + * $response = $response + * ->withStatus($e->getStatusCode()) + * ->withHeader(...$e->getAllowHeaderArguments()); + * + * + * @return array{0: string, 1: string} + */ + final public function getAllowHeaderArguments(): array + { + return ['Allow', $this->getJoinedAllowedMethods()]; + } +} diff --git a/src/Exception/Http/HttpMisdirectedRequestException.php b/src/Exception/Http/HttpMisdirectedRequestException.php new file mode 100644 index 00000000..14228d90 --- /dev/null +++ b/src/Exception/Http/HttpMisdirectedRequestException.php @@ -0,0 +1,43 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Misdirected Request Exception + * + * The request was directed at a server that is not able to produce a response. This can be sent by a server that is not + * configured to produce responses for the combination of scheme and authority that are included in the request URI. + * + * @since 3.0.0 + */ +class HttpMisdirectedRequestException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Misdirected Request'; + + parent::__construct(self::STATUS_MISDIRECTED_REQUEST, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpNotAcceptableException.php b/src/Exception/Http/HttpNotAcceptableException.php new file mode 100644 index 00000000..b0eb0f19 --- /dev/null +++ b/src/Exception/Http/HttpNotAcceptableException.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Not Acceptable Exception + * + * This response is sent when the web server, after performing server-driven content negotiation, doesn't find any + * content that conforms to the criteria given by the user agent. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406 + * + * @since 3.0.0 + */ +class HttpNotAcceptableException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Not Acceptable'; + + parent::__construct(self::STATUS_NOT_ACCEPTABLE, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpNotFoundException.php b/src/Exception/Http/HttpNotFoundException.php new file mode 100644 index 00000000..8ee9fada --- /dev/null +++ b/src/Exception/Http/HttpNotFoundException.php @@ -0,0 +1,47 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Not Found Exception + * + * The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this + * can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response + * instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is + * probably the most well known due to its frequent occurrence on the web. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404 + * + * @since 3.0.0 + */ +class HttpNotFoundException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Not Found'; + + parent::__construct(self::STATUS_NOT_FOUND, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpPayloadTooLargeException.php b/src/Exception/Http/HttpPayloadTooLargeException.php new file mode 100644 index 00000000..6a411a43 --- /dev/null +++ b/src/Exception/Http/HttpPayloadTooLargeException.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Payload Too Large Exception + * + * Request entity is larger than limits defined by server. The server might close the connection or return an + * Retry-After header field. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413 + * + * @since 3.0.0 + */ +class HttpPayloadTooLargeException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Payload Too Large'; + + parent::__construct(self::STATUS_PAYLOAD_TOO_LARGE, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpPaymentRequiredException.php b/src/Exception/Http/HttpPaymentRequiredException.php new file mode 100644 index 00000000..d5684ef7 --- /dev/null +++ b/src/Exception/Http/HttpPaymentRequiredException.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Payment Required Exception + * + * This response code is reserved for future use. The initial aim for creating this code was using it for digital + * payment systems, however this status code is used very rarely and no standard convention exists. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402 + * + * @since 3.0.0 + */ +class HttpPaymentRequiredException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Payment Required'; + + parent::__construct(self::STATUS_PAYMENT_REQUIRED, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpPreconditionFailedException.php b/src/Exception/Http/HttpPreconditionFailedException.php new file mode 100644 index 00000000..18ab7ba3 --- /dev/null +++ b/src/Exception/Http/HttpPreconditionFailedException.php @@ -0,0 +1,44 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Precondition Failed Exception + * + * The server does not meet one of the preconditions that the requester put on the request header fields. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412 + * + * @since 3.0.0 + */ +class HttpPreconditionFailedException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Precondition Failed'; + + parent::__construct(self::STATUS_PRECONDITION_FAILED, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpPreconditionRequiredException.php b/src/Exception/Http/HttpPreconditionRequiredException.php new file mode 100644 index 00000000..44eb6c51 --- /dev/null +++ b/src/Exception/Http/HttpPreconditionRequiredException.php @@ -0,0 +1,46 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Precondition Required Exception + * + * The origin server requires the request to be conditional. This response is intended to prevent the 'lost update' + * problem, where a client GETs a resource's state, modifies it and PUTs it back to the server, when meanwhile a third + * party has modified the state on the server, leading to a conflict. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428 + * + * @since 3.0.0 + */ +class HttpPreconditionRequiredException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Precondition Required'; + + parent::__construct(self::STATUS_PRECONDITION_REQUIRED, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpProxyAuthenticationRequiredException.php b/src/Exception/Http/HttpProxyAuthenticationRequiredException.php new file mode 100644 index 00000000..0fed38a5 --- /dev/null +++ b/src/Exception/Http/HttpProxyAuthenticationRequiredException.php @@ -0,0 +1,44 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Proxy Authentication Required Exception + * + * This is similar to 401 Unauthorized but authentication is needed to be done by a proxy. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407 + * + * @since 3.0.0 + */ +class HttpProxyAuthenticationRequiredException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Proxy Authentication Required'; + + parent::__construct(self::STATUS_PROXY_AUTHENTICATION_REQUIRED, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpRangeNotSatisfiableException.php b/src/Exception/Http/HttpRangeNotSatisfiableException.php new file mode 100644 index 00000000..66bb81ad --- /dev/null +++ b/src/Exception/Http/HttpRangeNotSatisfiableException.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Range Not Satisfiable Exception + * + * The range specified by the Range header field in the request cannot be fulfilled. It's possible that the range is + * outside the size of the target URI's data. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416 + * + * @since 3.0.0 + */ +class HttpRangeNotSatisfiableException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Range Not Satisfiable'; + + parent::__construct(self::STATUS_RANGE_NOT_SATISFIABLE, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php b/src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php new file mode 100644 index 00000000..c0c31e8b --- /dev/null +++ b/src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Request Header Fields Too Large Exception + * + * The server is unwilling to process the request because its header fields are too large. The request may be + * resubmitted after reducing the size of the request header fields. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431 + * + * @since 3.0.0 + */ +class HttpRequestHeaderFieldsTooLargeException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Request Header Fields Too Large'; + + parent::__construct(self::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpRequestTimeoutException.php b/src/Exception/Http/HttpRequestTimeoutException.php new file mode 100644 index 00000000..f667cb26 --- /dev/null +++ b/src/Exception/Http/HttpRequestTimeoutException.php @@ -0,0 +1,47 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Request Timeout Exception + * + * This response is sent on an idle connection by some servers, even without any previous request by the client. It + * means that the server would like to shut down this unused connection. This response is used much more since some + * browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection mechanisms to speed up surfing. Also note that + * some servers merely shut down the connection without sending this message. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408 + * + * @since 3.0.0 + */ +class HttpRequestTimeoutException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Request Timeout'; + + parent::__construct(self::STATUS_REQUEST_TIMEOUT, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpServiceUnavailableException.php b/src/Exception/Http/HttpServiceUnavailableException.php new file mode 100644 index 00000000..fc974870 --- /dev/null +++ b/src/Exception/Http/HttpServiceUnavailableException.php @@ -0,0 +1,48 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Service Unavailable Exception + * + * The server is not ready to handle the request. Common causes are a server that is down for maintenance or that is + * overloaded. Note that together with this response, a user-friendly page explaining the problem should be sent. This + * response should be used for temporary conditions and the Retry-After HTTP header should, if possible, contain the + * estimated time before the recovery of the service. The webmaster must also take care about the caching-related + * headers that are sent along with this response, as these temporary condition responses should usually not be cached. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503 + * + * @since 3.0.0 + */ +class HttpServiceUnavailableException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Service Unavailable'; + + parent::__construct(self::STATUS_SERVICE_UNAVAILABLE, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpTooEarlyException.php b/src/Exception/Http/HttpTooEarlyException.php new file mode 100644 index 00000000..e2837cc2 --- /dev/null +++ b/src/Exception/Http/HttpTooEarlyException.php @@ -0,0 +1,44 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Too Early Exception + * + * Indicates that the server is unwilling to risk processing a request that might be replayed. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/425 + * + * @since 3.0.0 + */ +class HttpTooEarlyException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Too Early'; + + parent::__construct(self::STATUS_TOO_EARLY, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpTooManyRequestsException.php b/src/Exception/Http/HttpTooManyRequestsException.php new file mode 100644 index 00000000..812cb313 --- /dev/null +++ b/src/Exception/Http/HttpTooManyRequestsException.php @@ -0,0 +1,44 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Too Many Requests Exception + * + * The user has sent too many requests in a given amount of time ("rate limiting"). + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429 + * + * @since 3.0.0 + */ +class HttpTooManyRequestsException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Too Many Requests'; + + parent::__construct(self::STATUS_TOO_MANY_REQUESTS, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpUnauthorizedException.php b/src/Exception/Http/HttpUnauthorizedException.php new file mode 100644 index 00000000..c6e2a006 --- /dev/null +++ b/src/Exception/Http/HttpUnauthorizedException.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Unauthorized Exception + * + * Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the + * client must authenticate itself to get the requested response. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401 + * + * @since 3.0.0 + */ +class HttpUnauthorizedException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Unauthorized'; + + parent::__construct(self::STATUS_UNAUTHORIZED, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpUnavailableForLegalReasonsException.php b/src/Exception/Http/HttpUnavailableForLegalReasonsException.php new file mode 100644 index 00000000..728e0bff --- /dev/null +++ b/src/Exception/Http/HttpUnavailableForLegalReasonsException.php @@ -0,0 +1,45 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Unavailable For Legal Reasons Exception + * + * The server is unwilling to process the request because its header fields are too large. The request may be + * resubmitted after reducing the size of the request header fields. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/451 + * + * @since 3.0.0 + */ +class HttpUnavailableForLegalReasonsException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Unavailable For Legal Reasons'; + + parent::__construct(self::STATUS_UNAVAILABLE_FOR_LEGAL_REASONS, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpUnprocessableEntityException.php b/src/Exception/Http/HttpUnprocessableEntityException.php new file mode 100644 index 00000000..34f8f121 --- /dev/null +++ b/src/Exception/Http/HttpUnprocessableEntityException.php @@ -0,0 +1,44 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Unprocessable Entity Exception + * + * The request was well-formed but was unable to be followed due to semantic errors. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 + * + * @since 3.0.0 + */ +class HttpUnprocessableEntityException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Unprocessable Entity'; + + parent::__construct(self::STATUS_UNPROCESSABLE_ENTITY, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpUnsupportedMediaTypeException.php b/src/Exception/Http/HttpUnsupportedMediaTypeException.php new file mode 100644 index 00000000..6c31d2ff --- /dev/null +++ b/src/Exception/Http/HttpUnsupportedMediaTypeException.php @@ -0,0 +1,121 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * Import functions + */ +use function join; + +/** + * HTTP Unsupported Media Type Exception + * + * The media format of the requested data is not supported by the server, so the server is rejecting the request. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415 + * + * @since 3.0.0 + */ +class HttpUnsupportedMediaTypeException extends HttpException +{ + + /** + * Unsupported media type + * + * @var string + */ + private string $unsupportedMediaType; + + /** + * Supported media types + * + * @var list + */ + private array $supportedMediaTypes; + + /** + * Constructor of the class + * + * @param string $unsupportedMediaType + * @param list $supportedMediaTypes + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct( + string $unsupportedMediaType, + array $supportedMediaTypes, + ?string $message = null, + int $code = 0, + ?Throwable $previous = null + ) { + $message ??= 'Unsupported Media Type'; + + parent::__construct(self::STATUS_UNSUPPORTED_MEDIA_TYPE, $message, $code, $previous); + + $this->unsupportedMediaType = $unsupportedMediaType; + $this->supportedMediaTypes = $supportedMediaTypes; + } + + /** + * Gets unsupported media type + * + * @return string + */ + final public function getType(): string + { + return $this->unsupportedMediaType; + } + + /** + * Gets supported media types + * + * @return list + */ + final public function getSupportedTypes(): array + { + return $this->supportedMediaTypes; + } + + /** + * Gets joined supported media types + * + * @return string + */ + final public function getJoinedSupportedTypes(): string + { + return join(',', $this->getSupportedTypes()); + } + + /** + * Gets arguments for an Accept header field + * + * Returns an array where key 0 contains the header name and key 1 contains its value. + * + * + * $response = $response + * ->withStatus($e->getStatusCode()) + * ->withHeader(...$e->getAcceptHeaderArguments()); + * + * + * @return array{0: string, 1: string} + */ + final public function getAcceptHeaderArguments(): array + { + return ['Accept', $this->getJoinedSupportedTypes()]; + } +} diff --git a/src/Exception/Http/HttpUpgradeRequiredException.php b/src/Exception/Http/HttpUpgradeRequiredException.php new file mode 100644 index 00000000..4d0d4d88 --- /dev/null +++ b/src/Exception/Http/HttpUpgradeRequiredException.php @@ -0,0 +1,46 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP Upgrade Required Exception + * + * 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. The server sends an Upgrade header in a 426 response to indicate the required + * protocol(s). + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426 + * + * @since 3.0.0 + */ +class HttpUpgradeRequiredException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'Upgrade Required'; + + parent::__construct(self::STATUS_UPGRADE_REQUIRED, $message, $code, $previous); + } +} diff --git a/src/Exception/Http/HttpUriTooLongException.php b/src/Exception/Http/HttpUriTooLongException.php new file mode 100644 index 00000000..5b92cce5 --- /dev/null +++ b/src/Exception/Http/HttpUriTooLongException.php @@ -0,0 +1,44 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception\Http; + +/** + * Import classes + */ +use Throwable; + +/** + * HTTP URI Too Long Exception + * + * The URI requested by the client is longer than the server is willing to interpret. + * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414 + * + * @since 3.0.0 + */ +class HttpUriTooLongException extends HttpException +{ + + /** + * Constructor of the class + * + * @param ?string $message + * @param int $code + * @param ?Throwable $previous + */ + public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'URI Too Long'; + + parent::__construct(self::STATUS_URI_TOO_LONG, $message, $code, $previous); + } +} From 5301636f43ed27f2399a3d72f287222522380dd3 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 14:27:23 +0100 Subject: [PATCH 002/180] Delete config.yml --- .circleci/config.yml | 64 -------------------------------------------- 1 file changed, 64 deletions(-) delete mode 100644 .circleci/config.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 706d6406..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,64 +0,0 @@ -# PHP CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-php/ for more details -# -version: 2 -jobs: - php71: - docker: - - image: cimg/php:7.1 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text - php72: - docker: - - image: cimg/php:7.2 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text - php73: - docker: - - image: cimg/php:7.3 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text - php74: - docker: - - image: cimg/php:7.4 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text - php80: - docker: - - image: cimg/php:8.0 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text - php81: - docker: - - image: cimg/php:8.1 - steps: - - checkout - - run: php -v - - run: composer install --no-interaction - - run: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-text -workflows: - version: 2 - build: - jobs: - - php71 - - php72 - - php73 - - php74 - - php80 - - php81 From 8a93367d2ff1b93138bef0ca8d678966611821d8 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 14:27:30 +0100 Subject: [PATCH 003/180] Update FUNDING.yml --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 1e976c3a..52a1ded1 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,7 +1,7 @@ # These are supported funding model platforms github: fenric # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] -patreon: # Replace with a single Patreon username +patreon: afenric open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: "packagist/sunrise/http-router" # Replace with a single Tidelift platform-name/package-name e.g., npm/babel From 4ad388adcd98ea15ce19ffc4e9fa15901f55cb64 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 14:27:33 +0100 Subject: [PATCH 004/180] Update .scrutinizer.yml --- .scrutinizer.yml | 44 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 1e6a96fc..44be2791 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,16 +1,54 @@ build: - environment: - php: - version: '8.0' + image: default-bionic nodes: analysis: + environment: + php: 8.2 tests: override: - php-scrutinizer-run coverage: + environment: + php: 8.2 tests: override: - command: XDEBUG_MODE=coverage php vendor/bin/phpunit --coverage-clover coverage.xml coverage: file: coverage.xml format: clover + php81: + environment: + php: 8.1 + tests: + override: + - command: php vendor/bin/phpunit + php80: + environment: + php: 8.0 + tests: + override: + - command: php vendor/bin/phpunit + php74: + environment: + php: 7.4 + tests: + override: + - command: php vendor/bin/phpunit + php73: + environment: + php: 7.3 + tests: + override: + - command: php vendor/bin/phpunit + php72: + environment: + php: 7.2 + tests: + override: + - command: php vendor/bin/phpunit + php71: + environment: + php: 7.1 + tests: + override: + - command: php vendor/bin/phpunit From 381e657fadd6dcabde36e7d9caa0f63c87289d2e Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 19:40:41 +0100 Subject: [PATCH 005/180] Type declarations for properties --- src/Annotation/Host.php | 6 +-- src/Annotation/Middleware.php | 11 ++--- src/Annotation/Postfix.php | 6 +-- src/Annotation/Prefix.php | 6 +-- src/Annotation/Route.php | 84 ++++++++++++++++++----------------- 5 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/Annotation/Host.php b/src/Annotation/Host.php index 209c4f5c..4b4f9c8d 100644 --- a/src/Annotation/Host.php +++ b/src/Annotation/Host.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -38,7 +38,7 @@ final class Host * * @var string */ - public $value; + public string $value; /** * Constructor of the class diff --git a/src/Annotation/Middleware.php b/src/Annotation/Middleware.php index eba445c1..1d339476 100644 --- a/src/Annotation/Middleware.php +++ b/src/Annotation/Middleware.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -15,6 +15,7 @@ * Import classes */ use Attribute; +use Psr\Http\Server\MiddlewareInterface; /** * @Annotation @@ -36,14 +37,14 @@ final class Middleware /** * The attribute value * - * @var string + * @var class-string */ - public $value; + public string $value; /** * Constructor of the class * - * @param string $value + * @param class-string $value */ public function __construct(string $value) { diff --git a/src/Annotation/Postfix.php b/src/Annotation/Postfix.php index 53f7ab82..c2bb2a7d 100644 --- a/src/Annotation/Postfix.php +++ b/src/Annotation/Postfix.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -38,7 +38,7 @@ final class Postfix * * @var string */ - public $value; + public string $value; /** * Constructor of the class diff --git a/src/Annotation/Prefix.php b/src/Annotation/Prefix.php index 84fdfc5c..71f3baa8 100644 --- a/src/Annotation/Prefix.php +++ b/src/Annotation/Prefix.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -38,7 +38,7 @@ final class Prefix * * @var string */ - public $value; + public string $value; /** * Constructor of the class diff --git a/src/Annotation/Route.php b/src/Annotation/Route.php index 1b7d5c7b..f2fce7b2 100644 --- a/src/Annotation/Route.php +++ b/src/Annotation/Route.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -15,6 +15,8 @@ * Import classes */ use Attribute; +use Fig\Http\Message\RequestMethodInterface; +use Psr\Http\Server\MiddlewareInterface; /** * @Annotation @@ -38,102 +40,102 @@ * }) */ #[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD)] -final class Route +final class Route implements RequestMethodInterface { /** * The descriptor holder * - * @var mixed + * @var class-string|array{0: class-string, 1: string}|null * * @internal */ - public $holder; + public $holder = null; /** - * A route name + * The route name * * @var string */ - public $name; + public string $name; /** - * A route host + * The route host * * @var string|null */ - public $host; + public ?string $host; /** - * A route path + * The route path * * @var string */ - public $path; + public string $path; /** - * A route methods + * The route methods * - * @var string[] + * @var list */ - public $methods; + public array $methods; /** - * A route middlewares + * The route middlewares * - * @var string[] + * @var list> */ - public $middlewares; + public array $middlewares; /** - * A route attributes + * The route attributes * - * @var array + * @var array */ - public $attributes; + public array $attributes; /** - * A route summary + * The route summary * * @var string */ - public $summary; + public string $summary; /** - * A route description + * The route description * * @var string */ - public $description; + public string $description; /** - * A route tags + * The route tags * - * @var string[] + * @var list */ - public $tags; + public array $tags; /** - * A route priority + * The route priority * * @var int */ - public $priority; + public int $priority; /** * Constructor of the class * - * @param string $name The route name - * @param string|null $host The route host - * @param string $path The route path - * @param string|null $method The route method - * @param string[] $methods The route methods - * @param string[] $middlewares The route middlewares - * @param array $attributes The route attributes - * @param string $summary The route summary - * @param string $description The route description - * @param string[] $tags The route tags - * @param int $priority The route priority (default 0) + * @param string $name The route name + * @param string|null $host The route host + * @param string $path The route path + * @param string|null $method The route method + * @param list $methods The route methods + * @param list> $middlewares The route middlewares + * @param array $attributes The route attributes + * @param string $summary The route summary + * @param string $description The route description + * @param list $tags The route tags + * @param int $priority The route priority (default 0) */ public function __construct( string $name, @@ -155,7 +157,7 @@ public function __construct( // if no methods are specified, // such a route is a GET route. if (empty($methods)) { - $methods[] = 'GET'; + $methods[] = self::METHOD_GET; } $this->name = $name; From 41884261532f4d312643d93fe527b58cd9f2d89e Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 19:43:28 +0100 Subject: [PATCH 006/180] New attribute to mark DTO --- src/Annotation/Body.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/Annotation/Body.php diff --git a/src/Annotation/Body.php b/src/Annotation/Body.php new file mode 100644 index 00000000..ad90075c --- /dev/null +++ b/src/Annotation/Body.php @@ -0,0 +1,25 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Annotation; + +/** + * Import classes + */ +use Attribute; + +/** + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_PARAMETER)] +final class Body +{ +} From 74aeb7e465464cc2d09c78f7a7efc993a4b69121 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 19:47:38 +0100 Subject: [PATCH 007/180] Improved code 1. Removed static properties; 2. Involved the configure method; 3. RuntimeException was replaced with LogicException. --- src/Command/RouteListCommand.php | 49 ++++++++++++++------------------ 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/Command/RouteListCommand.php b/src/Command/RouteListCommand.php index 15c7aff3..b6f6a678 100644 --- a/src/Command/RouteListCommand.php +++ b/src/Command/RouteListCommand.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -14,7 +14,7 @@ /** * Import classes */ -use RuntimeException; +use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\Router; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; @@ -31,33 +31,21 @@ /** * This command will list all routes in your application * - * If you cannot pass the router to the constructor + * If you can't pass the router to the constructor, * or your architecture has problems with autowiring, - * then just inherit this class and override the getRouter method. + * just inherit this class and override the getRouter method. * * @since 2.9.0 */ class RouteListCommand extends Command { - /** - * {@inheritdoc} - */ - protected static $defaultName = 'router:route-list'; - - /** - * {@inheritdoc} - * - * @var string - */ - protected static $defaultDescription = 'Lists all routes in your application'; - /** * The router instance populated with routes * * @var Router|null */ - private $router; + private ?Router $router; /** * Constructor of the class @@ -68,28 +56,35 @@ public function __construct(?Router $router = null) { parent::__construct(); - $this->setName(static::$defaultName); - $this->setDescription(static::$defaultDescription); - $this->router = $router; } + /** + * {@inheritdoc} + */ + protected function configure(): void + { + $this->setName('router:route-list'); + $this->setDescription('Lists all routes in your application'); + } + /** * Gets the router instance populated with routes * * @return Router * - * @throws RuntimeException + * @throws LogicException * If the command doesn't contain the router instance. * * @since 2.11.0 */ - protected function getRouter() : Router + protected function getRouter(): Router { - if (null === $this->router) { - throw new RuntimeException(sprintf( + if (!isset($this->router)) { + throw new LogicException(sprintf( 'The %2$s() method MUST return the %1$s class instance. ' . - 'Pass the %1$s class instance to the constructor, or override the %2$s() method.', + 'Pass the %1$s class instance to the constructor, ' . + 'or override the %2$s() method.', Router::class, __METHOD__ )); @@ -101,7 +96,7 @@ protected function getRouter() : Router /** * {@inheritdoc} */ - final protected function execute(InputInterface $input, OutputInterface $output) : int + final protected function execute(InputInterface $input, OutputInterface $output): int { $table = new Table($output); From 0ed647e9d1eb3ece4baae2521ca9853718654dd3 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 19:48:06 +0100 Subject: [PATCH 008/180] Type declarations for properties --- src/Event/RouteEvent.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Event/RouteEvent.php b/src/Event/RouteEvent.php index 08bda16c..cd5f6f36 100644 --- a/src/Event/RouteEvent.php +++ b/src/Event/RouteEvent.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -34,12 +34,12 @@ final class RouteEvent extends Event /** * @var RouteInterface */ - private $route; + private RouteInterface $route; /** * @var ServerRequestInterface */ - private $request; + private ServerRequestInterface $request; /** * Constructor of the class @@ -56,7 +56,7 @@ public function __construct(RouteInterface $route, ServerRequestInterface $reque /** * @return RouteInterface */ - public function getRoute() : RouteInterface + public function getRoute(): RouteInterface { return $this->route; } @@ -64,7 +64,7 @@ public function getRoute() : RouteInterface /** * @return ServerRequestInterface */ - public function getRequest() : ServerRequestInterface + public function getRequest(): ServerRequestInterface { return $this->request; } @@ -74,7 +74,7 @@ public function getRequest() : ServerRequestInterface * * @return void */ - public function setRequest(ServerRequestInterface $request) : void + public function setRequest(ServerRequestInterface $request): void { $this->request = $request; } From 4f883b9f92788d40633886e69868ae3996970ece Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 19:49:49 +0100 Subject: [PATCH 009/180] Update psalm.xml.dist --- psalm.xml.dist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/psalm.xml.dist b/psalm.xml.dist index 3becd9ba..38cce4cf 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -1,6 +1,6 @@ Date: Sun, 8 Jan 2023 19:49:55 +0100 Subject: [PATCH 010/180] Update phpunit.xml.dist --- phpunit.xml.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 66c8dba8..0ac15159 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -5,6 +5,7 @@ > + ./functions ./src From 72946071ca37da90c03f555835000c1c7044509c Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 19:50:34 +0100 Subject: [PATCH 011/180] removed deprecated loaders --- src/Loader/AnnotationDirectoryLoader.php | 21 --------------------- src/Loader/CollectableFileLoader.php | 21 --------------------- src/Loader/DescriptorDirectoryLoader.php | 21 --------------------- 3 files changed, 63 deletions(-) delete mode 100644 src/Loader/AnnotationDirectoryLoader.php delete mode 100644 src/Loader/CollectableFileLoader.php delete mode 100644 src/Loader/DescriptorDirectoryLoader.php diff --git a/src/Loader/AnnotationDirectoryLoader.php b/src/Loader/AnnotationDirectoryLoader.php deleted file mode 100644 index 06985a40..00000000 --- a/src/Loader/AnnotationDirectoryLoader.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Fenric - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Loader; - -/** - * AnnotationDirectoryLoader - * - * @deprecated 2.6.0 Use the DescriptorLoader class. - */ -class AnnotationDirectoryLoader extends DescriptorLoader -{ -} diff --git a/src/Loader/CollectableFileLoader.php b/src/Loader/CollectableFileLoader.php deleted file mode 100644 index e1b22879..00000000 --- a/src/Loader/CollectableFileLoader.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Fenric - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Loader; - -/** - * CollectableFileLoader - * - * @deprecated 2.10.0 Use the ConfigLoader class. - */ -class CollectableFileLoader extends ConfigLoader -{ -} diff --git a/src/Loader/DescriptorDirectoryLoader.php b/src/Loader/DescriptorDirectoryLoader.php deleted file mode 100644 index 93614634..00000000 --- a/src/Loader/DescriptorDirectoryLoader.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Fenric - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Loader; - -/** - * DescriptorDirectoryLoader - * - * @deprecated 2.10.0 Use the DescriptorLoader class. - */ -class DescriptorDirectoryLoader extends DescriptorLoader -{ -} From c2140c39164d20a051ede046fb353576d61a9161 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 19:53:18 +0100 Subject: [PATCH 012/180] Update ConfigLoader.php 1. Type declarations for properties; 2. New methods getResponseResolver and setResponseResolver; 3. Improved a resource validation; 4. Code style. --- src/Loader/ConfigLoader.php | 96 +++++++++++++++++++++++++------------ 1 file changed, 65 insertions(+), 31 deletions(-) diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index f57dfb1a..96cdc357 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -18,6 +18,7 @@ use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; +use Sunrise\Http\Router\ResponseResolverInterface; use Sunrise\Http\Router\RouteCollectionFactory; use Sunrise\Http\Router\RouteCollectionFactoryInterface; use Sunrise\Http\Router\RouteCollectionInterface; @@ -31,7 +32,7 @@ use function glob; use function is_dir; use function is_file; -use function sprintf; +use function is_string; /** * ConfigLoader @@ -40,24 +41,24 @@ class ConfigLoader implements LoaderInterface { /** - * @var string[] + * @var list */ - private $resources = []; + private array $resources = []; /** * @var RouteCollectionFactoryInterface */ - private $collectionFactory; + private RouteCollectionFactoryInterface $collectionFactory; /** * @var RouteFactoryInterface */ - private $routeFactory; + private RouteFactoryInterface $routeFactory; /** * @var ReferenceResolverInterface */ - private $referenceResolver; + private ReferenceResolverInterface $referenceResolver; /** * Constructor of the class @@ -83,7 +84,7 @@ public function __construct( * * @since 2.9.0 */ - public function getContainer() : ?ContainerInterface + public function getContainer(): ?ContainerInterface { return $this->referenceResolver->getContainer(); } @@ -97,40 +98,75 @@ public function getContainer() : ?ContainerInterface * * @since 2.9.0 */ - public function setContainer(?ContainerInterface $container) : void + public function setContainer(?ContainerInterface $container): void { $this->referenceResolver->setContainer($container); } + /** + * Gets the loader response resolver + * + * @return ResponseResolverInterface|null + * + * @since 3.0.0 + */ + public function getResponseResolver(): ?ResponseResolverInterface + { + return $this->referenceResolver->getResponseResolver(); + } + + /** + * Sets the given response resolver to the loader + * + * @param ResponseResolverInterface|null $responseResolver + * + * @return void + * + * @since 3.0.0 + */ + public function setResponseResolver(?ResponseResolverInterface $responseResolver): void + { + $this->referenceResolver->setResponseResolver($responseResolver); + } + /** * {@inheritdoc} */ - public function attach($resource) : void + public function attach($resource): void { - if (is_dir($resource)) { - $fileNames = glob($resource . '/*.php'); - foreach ($fileNames as $fileName) { - $this->resources[] = $fileName; - } + if (!is_string($resource)) { + throw new InvalidLoaderResourceException( + 'Config route loader only expects string resources' + ); + } + if (is_file($resource)) { + $this->resources[] = $resource; return; } - if (!is_file($resource)) { - throw new InvalidLoaderResourceException(sprintf( - 'The resource "%s" is not found.', - $resource - )); + if (is_dir($resource)) { + $filenames = glob($resource . '/*.php'); + foreach ($filenames as $filename) { + $this->resources[] = $filename; + } + + return; } - $this->resources[] = $resource; + throw new InvalidLoaderResourceException(sprintf( + 'Config route loader only handles file or directory paths, ' . + 'however the given resource "%s" is not as expected', + $resource + )); } /** * {@inheritdoc} */ - public function attachArray(array $resources) : void + public function attachArray(array $resources): void { + /** @psalm-suppress MixedAssignment */ foreach ($resources as $resource) { $this->attach($resource); } @@ -139,7 +175,7 @@ public function attachArray(array $resources) : void /** * {@inheritdoc} */ - public function load() : RouteCollectionInterface + public function load(): RouteCollectionInterface { $collector = new RouteCollector( $this->collectionFactory, @@ -147,13 +183,11 @@ public function load() : RouteCollectionInterface $this->referenceResolver ); - foreach ($this->resources as $resource) { - (function () use ($resource) { - /** - * @psalm-suppress UnresolvableInclude - */ - require $resource; - })->call($collector); + foreach ($this->resources as $filename) { + (function (string $filename) { + /** @psalm-suppress UnresolvableInclude */ + require $filename; + })->call($collector, $filename); } return $collector->getCollection(); From 128c4384c8482d9322c163437f934970dd96c283 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 19:57:02 +0100 Subject: [PATCH 013/180] Update DescriptorLoader.php 1. Type declarations for properties; 2. Annotation support for PHP 8 is no longer automatically enabled; 3. New methods getResponseResolver, setResponseResolver, getAnnotationReader, setAnnotationReader, useDefaultAnnotationReader; 4. Improved resources validation; 5. Code style. --- src/Loader/DescriptorLoader.php | 260 +++++++++++++++++++++----------- 1 file changed, 172 insertions(+), 88 deletions(-) diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 12ef0caa..a8237386 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -14,7 +14,8 @@ /** * Import classes */ -use Doctrine\Common\Annotations\SimpleAnnotationReader; +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\Reader as AnnotationReaderInterface; use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -25,19 +26,23 @@ use Sunrise\Http\Router\Annotation\Prefix; use Sunrise\Http\Router\Annotation\Route; use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; -use Sunrise\Http\Router\Exception\UnresolvableReferenceException; +use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; +use Sunrise\Http\Router\ResponseResolverInterface; use Sunrise\Http\Router\RouteCollectionFactory; use Sunrise\Http\Router\RouteCollectionFactoryInterface; use Sunrise\Http\Router\RouteCollectionInterface; use Sunrise\Http\Router\RouteFactory; use Sunrise\Http\Router\RouteFactoryInterface; +use Iterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; +use ReflectionAttribute; use ReflectionClass; use ReflectionMethod; use Reflector; +use SplFileInfo; /** * Import functions @@ -47,7 +52,7 @@ use function get_declared_classes; use function hash; use function is_dir; -use function sprintf; +use function is_string; use function usort; /** @@ -62,39 +67,39 @@ class DescriptorLoader implements LoaderInterface { /** - * @var class-string[] + * @var list */ - private $resources = []; + private array $resources = []; /** * @var RouteCollectionFactoryInterface */ - private $collectionFactory; + private RouteCollectionFactoryInterface $collectionFactory; /** * @var RouteFactoryInterface */ - private $routeFactory; + private RouteFactoryInterface $routeFactory; /** * @var ReferenceResolverInterface */ - private $referenceResolver; + private ReferenceResolverInterface $referenceResolver; /** - * @var SimpleAnnotationReader|null + * @var AnnotationReaderInterface|null */ - private $annotationReader = null; + private ?AnnotationReaderInterface $annotationReader = null; /** * @var CacheInterface|null */ - private $cache = null; + private ?CacheInterface $cache = null; /** * @var string|null */ - private $cacheKey = null; + private ?string $cacheKey = null; /** * Constructor of the class @@ -112,10 +117,8 @@ public function __construct( $this->routeFactory = $routeFactory ?? new RouteFactory(); $this->referenceResolver = $referenceResolver ?? new ReferenceResolver(); - // the "doctrine/annotations" package must be installed manually - if (class_exists(SimpleAnnotationReader::class)) { - $this->annotationReader = /** @scrutinizer ignore-deprecated */ new SimpleAnnotationReader(); - $this->annotationReader->addNamespace('Sunrise\Http\Router\Annotation'); + if (8 > PHP_MAJOR_VERSION) { + $this->useDefaultAnnotationReader(); } } @@ -124,43 +127,83 @@ public function __construct( * * @return ContainerInterface|null */ - public function getContainer() : ?ContainerInterface + public function getContainer(): ?ContainerInterface { return $this->referenceResolver->getContainer(); } /** - * Gets the loader cache + * Sets the given container to the loader * - * @return CacheInterface|null + * @param ContainerInterface|null $container + * + * @return void */ - public function getCache() : ?CacheInterface + public function setContainer(?ContainerInterface $container): void { - return $this->cache; + $this->referenceResolver->setContainer($container); } /** - * Gets the loader cache key + * Gets the loader response resolver * - * @return string|null + * @return ResponseResolverInterface|null * - * @since 2.10.0 + * @since 3.0.0 */ - public function getCacheKey() : ?string + public function getResponseResolver(): ?ResponseResolverInterface { - return $this->cacheKey; + return $this->referenceResolver->getResponseResolver(); } /** - * Sets the given container to the loader + * Sets the given response resolver to the loader * - * @param ContainerInterface|null $container + * @param ResponseResolverInterface|null $responseResolver * * @return void + * + * @since 3.0.0 */ - public function setContainer(?ContainerInterface $container) : void + public function setResponseResolver(?ResponseResolverInterface $responseResolver): void { - $this->referenceResolver->setContainer($container); + $this->referenceResolver->setResponseResolver($responseResolver); + } + + /** + * Gets the loader annotation reader + * + * @return AnnotationReaderInterface|null + * + * @since 3.0.0 + */ + public function getAnnotationReader(): ?AnnotationReaderInterface + { + return $this->annotationReader; + } + + /** + * Sets the given annotation reader to the loader + * + * @param AnnotationReaderInterface|null $annotationReader + * + * @return void + * + * @since 3.0.0 + */ + public function setAnnotationReader(?AnnotationReaderInterface $annotationReader): void + { + $this->annotationReader = $annotationReader; + } + + /** + * Gets the loader cache + * + * @return CacheInterface|null + */ + public function getCache(): ?CacheInterface + { + return $this->cache; } /** @@ -170,11 +213,23 @@ public function setContainer(?ContainerInterface $container) : void * * @return void */ - public function setCache(?CacheInterface $cache) : void + public function setCache(?CacheInterface $cache): void { $this->cache = $cache; } + /** + * Gets the loader cache key + * + * @return string + * + * @since 2.10.0 + */ + public function getCacheKey(): string + { + return $this->cacheKey ??= hash('md5', 'router:descriptors'); + } + /** * Sets the given cache key to the loader * @@ -184,16 +239,49 @@ public function setCache(?CacheInterface $cache) : void * * @since 2.10.0 */ - public function setCacheKey(?string $cacheKey) : void + public function setCacheKey(?string $cacheKey): void { $this->cacheKey = $cacheKey; } + /** + * Uses the default annotation reader + * + * @return void + * + * @throws LogicException + * If the doctrine/annotations package isn't installed. + * + * @since 3.0.0 + */ + public function useDefaultAnnotationReader(): void + { + if (!class_exists(AnnotationReader::class)) { + throw new LogicException( + 'Loading routes from descriptors requires the "doctrine/annotations" package that is not installed, ' . + 'run the command "composer install doctrine/annotations" and try again' + ); + } + + $this->setAnnotationReader(new AnnotationReader()); + } + /** * {@inheritdoc} */ - public function attach($resource) : void + public function attach($resource): void { + if (!is_string($resource)) { + throw new InvalidLoaderResourceException( + 'Descriptor route loader only expects string resources' + ); + } + + if (class_exists($resource)) { + $this->resources[] = $resource; + return; + } + if (is_dir($resource)) { $classNames = $this->scandir($resource); foreach ($classNames as $className) { @@ -203,21 +291,19 @@ public function attach($resource) : void return; } - if (!class_exists($resource)) { - throw new InvalidLoaderResourceException(sprintf( - 'The resource "%s" is not found.', - $resource - )); - } - - $this->resources[] = $resource; + throw new InvalidLoaderResourceException(sprintf( + 'Descriptor route loader only handles class names or directory paths, ' . + 'however the given resource "%s" is not as expected', + $resource + )); } /** * {@inheritdoc} */ - public function attachArray(array $resources) : void + public function attachArray(array $resources): void { + /** @psalm-suppress MixedAssignment */ foreach ($resources as $resource) { $this->attach($resource); } @@ -225,27 +311,19 @@ public function attachArray(array $resources) : void /** * {@inheritdoc} - * - * @throws UnresolvableReferenceException - * If one of the found middlewares cannot be resolved. */ - public function load() : RouteCollectionInterface + public function load(): RouteCollectionInterface { - $descriptors = $this->getCachedDescriptors(); + $descriptors = $this->getDescriptors(); $routes = []; foreach ($descriptors as $descriptor) { - $middlewares = []; - foreach ($descriptor->middlewares as $className) { - $middlewares[] = $this->referenceResolver->toMiddleware($className); - } - $routes[] = $this->routeFactory->createRoute( $descriptor->name, $descriptor->path, $descriptor->methods, $this->referenceResolver->toRequestHandler($descriptor->holder), - $middlewares, + $this->referenceResolver->toMiddlewares($descriptor->middlewares), $descriptor->attributes ) ->setHost($descriptor->host) @@ -260,33 +338,34 @@ public function load() : RouteCollectionInterface /** * Gets descriptors from the cache if they are stored in it, * otherwise collects them from the loader resources, - * and then tries to cache them + * and then tries to cache and return them * - * @return Route[] + * @return list */ - private function getCachedDescriptors() : array + private function getDescriptors(): array { - $key = $this->cacheKey ?? hash('md5', 'router:descriptors'); + $cacheKey = $this->getCacheKey(); - if ($this->cache && $this->cache->has($key)) { - return $this->cache->get($key); + if (isset($this->cache) && $this->cache->has($cacheKey)) { + /** @var list */ + return $this->cache->get($cacheKey); } - $result = $this->collectDescriptors(); + $descriptors = $this->collectDescriptors(); - if ($this->cache) { - $this->cache->set($key, $result); + if (isset($this->cache)) { + $this->cache->set($cacheKey, $descriptors); } - return $result; + return $descriptors; } /** - * Collects descriptors from the loader resources + * Collects and returns descriptors from the loader resources * - * @return Route[] + * @return list */ - private function collectDescriptors() : array + private function collectDescriptors(): array { $result = []; foreach ($this->resources as $resource) { @@ -297,7 +376,7 @@ private function collectDescriptors() : array } } - usort($result, function (Route $a, Route $b) : int { + usort($result, function (Route $a, Route $b): int { return $b->priority <=> $a->priority; }); @@ -309,15 +388,17 @@ private function collectDescriptors() : array * * @param ReflectionClass $class * - * @return Route[] + * @return list */ - private function getClassDescriptors(ReflectionClass $class) : array + private function getClassDescriptors(ReflectionClass $class): array { - if ($class->isAbstract()) { + // e.g., interfaces, traits, enums, abstract classes, + // classes with private constructor... + if (!$class->isInstantiable()) { return []; } - $result = []; + $descriptors = []; if ($class->isSubclassOf(RequestHandlerInterface::class)) { $annotations = $this->getAnnotations($class, Route::class); @@ -325,15 +406,13 @@ private function getClassDescriptors(ReflectionClass $class) : array $descriptor = $annotations[0]; $this->supplementDescriptor($descriptor, $class); $descriptor->holder = $class->getName(); - $result[] = $descriptor; + $descriptors[] = $descriptor; } } foreach ($class->getMethods() as $method) { // ignore non-available methods... - if ($method->isStatic() || - $method->isPrivate() || - $method->isProtected()) { + if (!$method->isPublic() || $method->isStatic()) { continue; } @@ -343,16 +422,20 @@ private function getClassDescriptors(ReflectionClass $class) : array $this->supplementDescriptor($descriptor, $class); $this->supplementDescriptor($descriptor, $method); $descriptor->holder = [$class->getName(), $method->getName()]; - $result[] = $descriptor; + $descriptors[] = $descriptor; } } - return $result; + return $descriptors; } /** * Supplements the given descriptor from the given class or method with data such as: - * host, path prefix, path postfix and middlewares + * + * - host + * - path prefix + * - path postfix + * - middlewares * * ```php * #[Prefix('/api/v1')] @@ -375,7 +458,7 @@ private function getClassDescriptors(ReflectionClass $class) : array * * @return void */ - private function supplementDescriptor(Route $descriptor, Reflector $reflector) : void + private function supplementDescriptor(Route $descriptor, Reflector $reflector): void { $annotations = $this->getAnnotations($reflector, Host::class); if (isset($annotations[0])) { @@ -404,22 +487,24 @@ private function supplementDescriptor(Route $descriptor, Reflector $reflector) : * @param ReflectionClass|ReflectionMethod $reflector * @param class-string $annotationName * - * @return T[] + * @return list * * @template T */ - private function getAnnotations(Reflector $reflector, string $annotationName) : array + private function getAnnotations(Reflector $reflector, string $annotationName): array { $result = []; if (8 === PHP_MAJOR_VERSION) { + /** @var ReflectionAttribute[] */ $attributes = $reflector->getAttributes($annotationName); foreach ($attributes as $attribute) { + /** @var T */ $result[] = $attribute->newInstance(); } } - if (empty($result) and isset($this->annotationReader)) { + if (isset($this->annotationReader) && empty($result)) { $annotations = ($reflector instanceof ReflectionClass) ? $this->annotationReader->getClassAnnotations($reflector) : $this->annotationReader->getMethodAnnotations($reflector); @@ -441,8 +526,9 @@ private function getAnnotations(Reflector $reflector, string $annotationName) : * * @return class-string[] */ - private function scandir(string $directory) : array + private function scandir(string $directory): array { + /** @var Iterator */ $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($directory) ); @@ -451,9 +537,7 @@ private function scandir(string $directory) : array foreach ($files as $file) { if ('php' === $file->getExtension()) { - /** - * @psalm-suppress UnresolvableInclude - */ + /** @psalm-suppress UnresolvableInclude */ require_once $file->getPathname(); } } From 52a9ce8913206ae0acd2d0841852d8bc20ddcc3f Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 19:57:18 +0100 Subject: [PATCH 014/180] cs --- src/Loader/LoaderInterface.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Loader/LoaderInterface.php b/src/Loader/LoaderInterface.php index 750a4aa4..b268edc4 100644 --- a/src/Loader/LoaderInterface.php +++ b/src/Loader/LoaderInterface.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -33,24 +33,24 @@ interface LoaderInterface * @throws InvalidLoaderResourceException * If the given resource isn't valid. */ - public function attach($resource) : void; + public function attach($resource): void; /** * Attaches the given resources to the loader * - * @param array $resources + * @param array $resources * * @return void * * @throws InvalidLoaderResourceException * If one of the given resources isn't valid. */ - public function attachArray(array $resources) : void; + public function attachArray(array $resources): void; /** * Loads routes from previously attached resources * * @return RouteCollectionInterface */ - public function load() : RouteCollectionInterface; + public function load(): RouteCollectionInterface; } From 4f639db9b269411eb5112953f85a98b48d319ef9 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 19:57:30 +0100 Subject: [PATCH 015/180] cs --- src/Middleware/CallableMiddleware.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index ab2bef18..a7eeccbb 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -51,7 +51,7 @@ public function __construct(callable $callback) * * @since 2.10.0 */ - public function getCallback() : callable + public function getCallback(): callable { return $this->callback; } @@ -59,8 +59,11 @@ public function getCallback() : callable /** * {@inheritdoc} */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - return ($this->callback)($request, $handler); + /** @var ResponseInterface */ + $response = ($this->callback)($request, $handler); + + return $response; } } From 750f5bcd30768fb50895efb0b015554e300a55fb Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 19:58:19 +0100 Subject: [PATCH 016/180] Update JsonPayloadDecodingMiddleware.php 1. New constant JSON_MAXIMAL_DEPTH; 2. Code style. --- .../JsonPayloadDecodingMiddleware.php | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index b758dce4..ddf5d6ce 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -14,18 +14,19 @@ /** * Import classes */ +use JsonException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\UndecodablePayloadException; +use Sunrise\Http\Router\Exception\InvalidPayloadException; /** * Import functions */ +use function is_array; +use function is_object; use function json_decode; -use function json_last_error; -use function json_last_error_msg; use function rtrim; use function strpos; use function substr; @@ -34,7 +35,8 @@ * Import constants */ use const JSON_BIGINT_AS_STRING; -use const JSON_ERROR_NONE; +use const JSON_OBJECT_AS_ARRAY; +use const JSON_THROW_ON_ERROR; /** * JsonPayloadDecodingMiddleware @@ -45,7 +47,7 @@ class JsonPayloadDecodingMiddleware implements MiddlewareInterface { /** - * JSON Media Type + * JSON media type * * @var string * @@ -53,6 +55,13 @@ class JsonPayloadDecodingMiddleware implements MiddlewareInterface */ private const JSON_MEDIA_TYPE = 'application/json'; + /** + * JSON maximal depth + * + * @var int + */ + protected const JSON_MAXIMAL_DEPTH = 512; + /** * JSON decoding options * @@ -60,20 +69,19 @@ class JsonPayloadDecodingMiddleware implements MiddlewareInterface * * @link https://www.php.net/manual/ru/json.constants.php */ - protected const JSON_DECODING_OPTIONS = JSON_BIGINT_AS_STRING; + protected const JSON_DECODING_OPTIONS = JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY; /** * {@inheritdoc} */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (!$this->isSupportedRequest($request)) { - return $handler->handle($request); + if ($this->isSupportedRequest($request)) { + $data = $this->decodeRequestJsonPayload($request); + $request = $request->withParsedBody($data); } - $parsedBody = $this->decodeRequestJsonPayload($request); - - return $handler->handle($request->withParsedBody($parsedBody)); + return $handler->handle($request); } /** @@ -83,13 +91,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface * * @return bool */ - private function isSupportedRequest(ServerRequestInterface $request) : bool + private function isSupportedRequest(ServerRequestInterface $request): bool { return self::JSON_MEDIA_TYPE === $this->getRequestMediaType($request); } /** - * Gets Media Type from the given request + * Gets media type from the given request * * @param ServerRequestInterface $request * @@ -97,7 +105,7 @@ private function isSupportedRequest(ServerRequestInterface $request) : bool * * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1 */ - private function getRequestMediaType(ServerRequestInterface $request) : ?string + private function getRequestMediaType(ServerRequestInterface $request): ?string { if (!$request->hasHeader('Content-Type')) { return null; @@ -119,19 +127,30 @@ private function getRequestMediaType(ServerRequestInterface $request) : ?string * * @param ServerRequestInterface $request * - * @return mixed + * @return array|object|null * - * @throws UndecodablePayloadException + * @throws InvalidPayloadException * If the request's payload cannot be decoded. */ private function decodeRequestJsonPayload(ServerRequestInterface $request) { - json_decode(''); - $result = json_decode($request->getBody()->__toString(), true, 512, static::JSON_DECODING_OPTIONS); - if (JSON_ERROR_NONE === json_last_error()) { + /** @var int */ + $depth = static::JSON_MAXIMAL_DEPTH; + + /** @var int */ + $flags = static::JSON_DECODING_OPTIONS; + + try { + /** @var mixed */ + $result = json_decode($request->getBody()->__toString(), null, $depth, $flags | JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new InvalidPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); + } + + if (is_array($result) || is_object($result)) { return $result; } - throw new UndecodablePayloadException(sprintf('Invalid Payload: %s', json_last_error_msg())); + return null; } } From 94df5e06a4a9fa81b1fd045e06f346463906fb65 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 20:00:12 +0100 Subject: [PATCH 017/180] New middleware for mapping request body --- .../ParsedBodyMappingMiddleware.php | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 src/Middleware/ParsedBodyMappingMiddleware.php diff --git a/src/Middleware/ParsedBodyMappingMiddleware.php b/src/Middleware/ParsedBodyMappingMiddleware.php new file mode 100644 index 00000000..f7f25804 --- /dev/null +++ b/src/Middleware/ParsedBodyMappingMiddleware.php @@ -0,0 +1,154 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Middleware; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\Annotation\Body; +use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; +use Sunrise\Http\Router\RouteInterface; +use Sunrise\Hydrator\Exception\InvalidValueException; +use Sunrise\Hydrator\Exception\InvalidObjectException; +use Sunrise\Hydrator\HydratorInterface; +use Sunrise\Hydrator\Hydrator; +use ReflectionNamedType; + +/** + * Import functions + */ +use function class_exists; +use function sprintf; + +/** + * Import constants + */ +use const PHP_MAJOR_VERSION; + +/** + * ParsedBodyMappingMiddleware + * + * @link https://github.com/sunrise-php/hydrator + * + * @since 3.0.0 + */ +class ParsedBodyMappingMiddleware implements MiddlewareInterface +{ + + /** + * Objects hydrator + * + * @var HydratorInterface + */ + private HydratorInterface $objectHydrator; + + /** + * Constructor of the class + * + * @param HydratorInterface|null $objectHydrator + * + * @throws LogicException + * If the PHP version less than 8. + */ + public function __construct(?HydratorInterface $objectHydrator = null) + { + if (PHP_MAJOR_VERSION < 8) { + throw new LogicException('Parsed body mapping requires PHP version 8'); + } + + $this->objectHydrator = $objectHydrator ?? new Hydrator(); + } + + /** + * {@inheritdoc} + * + * @throws LogicException + * If something went wrong... + * + * @throws HttpUnprocessableEntityException + * If the request body isn't valid. + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $route = $request->getAttribute(RouteInterface::ATTR_ROUTE); + if (!($route instanceof RouteInterface)) { + throw new LogicException(sprintf( + 'The "%s" middleware must be related with a route', + __CLASS__ + )); + } + + $routeRequestHandler = $route->getRequestHandler(); + if (!($routeRequestHandler instanceof CallableRequestHandler)) { + throw new LogicException(sprintf( + 'The "%s" route cannot be related with the "%s" middleware', + $route->getName(), + __CLASS__ + )); + } + + $routeRequestHandlerProcessableParameter = $routeRequestHandler->getAttributedParameter(Body::class); + if (!isset($routeRequestHandlerProcessableParameter)) { + return $handler->handle($request); + } + + $routeRequestHandlerProcessableParameterType = $routeRequestHandlerProcessableParameter->getType(); + if (!($routeRequestHandlerProcessableParameterType instanceof ReflectionNamedType)) { + throw new LogicException(sprintf( + 'The "%s" parameter of the "%s" route request handler cannot be hydrated ' . + 'because its type is not supported, do not use union or intersection types', + $routeRequestHandlerProcessableParameter->getName(), + $route->getName() + )); + } + + $routeRequestHandlerProcessableParameterClassName = $routeRequestHandlerProcessableParameterType->getName(); + if (!class_exists($routeRequestHandlerProcessableParameterClassName)) { + throw new LogicException(sprintf( + 'The "%s" parameter of the "%s" route request handler cannot be hydrated ' . + 'because its refers to the "%s" class that cannot be found', + $routeRequestHandlerProcessableParameter->getName(), + $route->getName(), + $routeRequestHandlerProcessableParameterType->getName() + )); + } + + try { + $hydratedObject = $this->objectHydrator->hydrate( + $routeRequestHandlerProcessableParameterClassName, + (array) $request->getParsedBody() + ); + } catch (InvalidObjectException $e) { + throw new LogicException(sprintf( + 'The "%s" parameter of the "%s" route request handler cannot be hydrated ' . + 'because its refers to an invalid DTO: %s', + $routeRequestHandlerProcessableParameter->getName(), + $route->getName(), + $e->getMessage() + ), 0, $e); + } catch (InvalidValueException $e) { + throw new HttpUnprocessableEntityException($e->getMessage(), 0, $e); + } + + $routeRequestHandlerArguments = $routeRequestHandler->getArguments(); + $routeRequestHandlerArguments[$routeRequestHandlerProcessableParameter->getPosition()] = $hydratedObject; + $route->setRequestHandler($routeRequestHandler->withArguments($routeRequestHandlerArguments)); + + return $handler->handle($request); + } +} From 8bf270830d8237ca5ce47c34263f0f5a068b5a9c Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 20:01:45 +0100 Subject: [PATCH 018/180] Update ReferenceResolver.php 1. Type declarations for properties; 2. New methods: getResponseResolver, setResponseResolver and toMiddlewares; 3. Code style. --- src/ReferenceResolver.php | 94 ++++++++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index b0461b78..30a3c658 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -17,7 +17,7 @@ use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\UnresolvableReferenceException; +use Sunrise\Http\Router\Exception\InvalidReferenceException; use Sunrise\Http\Router\Middleware\CallableMiddleware; use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use Closure; @@ -45,12 +45,19 @@ class ReferenceResolver implements ReferenceResolverInterface * * @var ContainerInterface|null */ - private $container = null; + private ?ContainerInterface $container = null; + + /** + * The reference resolver's response resolver + * + * @var ResponseResolverInterface|null + */ + private ?ResponseResolverInterface $responseResolver = null; /** * {@inheritdoc} */ - public function getContainer() : ?ContainerInterface + public function getContainer(): ?ContainerInterface { return $this->container; } @@ -58,7 +65,7 @@ public function getContainer() : ?ContainerInterface /** * {@inheritdoc} */ - public function setContainer(?ContainerInterface $container) : void + public function setContainer(?ContainerInterface $container): void { $this->container = $container; } @@ -66,27 +73,46 @@ public function setContainer(?ContainerInterface $container) : void /** * {@inheritdoc} */ - public function toRequestHandler($reference) : RequestHandlerInterface + public function getResponseResolver(): ?ResponseResolverInterface + { + return $this->responseResolver; + } + + /** + * {@inheritdoc} + */ + public function setResponseResolver(?ResponseResolverInterface $responseResolver): void + { + $this->responseResolver = $responseResolver; + } + + /** + * {@inheritdoc} + */ + public function toRequestHandler($reference): RequestHandlerInterface { if ($reference instanceof RequestHandlerInterface) { return $reference; } if ($reference instanceof Closure) { - return new CallableRequestHandler($reference); + return new CallableRequestHandler($reference, $this->responseResolver); } list($class, $method) = $this->normalizeReference($reference); if (isset($class) && isset($method) && method_exists($class, $method)) { - return new CallableRequestHandler([$this->resolveClass($class), $method]); + /** @var callable */ + $callback = [$this->resolveClass($class), $method]; + + return new CallableRequestHandler($callback, $this->responseResolver); } if (!isset($method) && isset($class) && is_subclass_of($class, RequestHandlerInterface::class)) { return $this->resolveClass($class); } - throw new UnresolvableReferenceException(sprintf( + throw new InvalidReferenceException(sprintf( 'Unable to resolve the "%s" reference to a request handler.', $this->stringifyReference($reference) )); @@ -95,7 +121,7 @@ public function toRequestHandler($reference) : RequestHandlerInterface /** * {@inheritdoc} */ - public function toMiddleware($reference) : MiddlewareInterface + public function toMiddleware($reference): MiddlewareInterface { if ($reference instanceof MiddlewareInterface) { return $reference; @@ -115,30 +141,42 @@ public function toMiddleware($reference) : MiddlewareInterface return $this->resolveClass($class); } - throw new UnresolvableReferenceException(sprintf( + throw new InvalidReferenceException(sprintf( 'Unable to resolve the "%s" reference to a middleware.', $this->stringifyReference($reference) )); } + /** + * {@inheritdoc} + */ + public function toMiddlewares(array $references): array + { + $middlewares = []; + /** @psalm-suppress MixedAssignment */ + foreach ($references as $reference) { + $middlewares[] = $this->toMiddleware($reference); + } + + return $middlewares; + } + /** * Normalizes the given reference * * @param mixed $reference * - * @return array{0: ?class-string, 1: ?string} + * @return array{0: ?class-string, 1: ?non-empty-string} */ - private function normalizeReference($reference) : array + private function normalizeReference($reference): array { if (is_array($reference) && is_callable($reference, true)) { - /** @var array{0: class-string, 1: string} $reference */ - + /** @var array{0: class-string, 1: non-empty-string} $reference */ return $reference; } if (is_string($reference)) { /** @var class-string $reference */ - return [$reference, null]; } @@ -152,14 +190,16 @@ private function normalizeReference($reference) : array * * @return string */ - private function stringifyReference($reference) : string + private function stringifyReference($reference): string { - if (is_array($reference) && is_callable($reference, true)) { + $reference = $this->normalizeReference($reference); + + if (isset($reference[0], $reference[1])) { return $reference[0] . '@' . $reference[1]; } - if (is_string($reference)) { - return $reference; + if (isset($reference[0])) { + return $reference[0]; } return ''; @@ -168,18 +208,20 @@ private function stringifyReference($reference) : string /** * Resolves the given class * - * @param class-string $class + * @param class-string $className * * @return T * * @template T */ - private function resolveClass(string $class) + private function resolveClass(string $className) { - if ($this->container && $this->container->has($class)) { - return $this->container->get($class); + if (isset($this->container) && $this->container->has($className)) { + /** @var T */ + return $this->container->get($className); } - return new $class; + /** @psalm-suppress MixedMethodCall */ + return new $className; } } From 6c50e91ce5b40eaa6f4450fb9a9c396863b3b942 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 20:02:13 +0100 Subject: [PATCH 019/180] New methods was added --- src/ReferenceResolverInterface.php | 52 ++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/ReferenceResolverInterface.php b/src/ReferenceResolverInterface.php index 219b9360..a939fa1c 100644 --- a/src/ReferenceResolverInterface.php +++ b/src/ReferenceResolverInterface.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -17,7 +17,7 @@ use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\UnresolvableReferenceException; +use Sunrise\Http\Router\Exception\InvalidReferenceException; /** * ReferenceResolverInterface @@ -32,7 +32,7 @@ interface ReferenceResolverInterface * * @return ContainerInterface|null */ - public function getContainer() : ?ContainerInterface; + public function getContainer(): ?ContainerInterface; /** * Sets the given container to the reference resolver @@ -41,7 +41,27 @@ public function getContainer() : ?ContainerInterface; * * @return void */ - public function setContainer(?ContainerInterface $container) : void; + public function setContainer(?ContainerInterface $container): void; + + /** + * Gets the reference resolver's response resolver + * + * @return ResponseResolverInterface|null + * + * @since 3.0.0 + */ + public function getResponseResolver(): ?ResponseResolverInterface; + + /** + * Sets the given response resolver to the reference resolver + * + * @param ResponseResolverInterface|null $responseResolver + * + * @return void + * + * @since 3.0.0 + */ + public function setResponseResolver(?ResponseResolverInterface $responseResolver): void; /** * Resolves the given reference to a request handler @@ -50,10 +70,10 @@ public function setContainer(?ContainerInterface $container) : void; * * @return RequestHandlerInterface * - * @throws UnresolvableReferenceException + * @throws InvalidReferenceException * If the given reference cannot be resolved to a request handler. */ - public function toRequestHandler($reference) : RequestHandlerInterface; + public function toRequestHandler($reference): RequestHandlerInterface; /** * Resolves the given reference to a middleware @@ -62,8 +82,22 @@ public function toRequestHandler($reference) : RequestHandlerInterface; * * @return MiddlewareInterface * - * @throws UnresolvableReferenceException + * @throws InvalidReferenceException * If the given reference cannot be resolved to a middleware. */ - public function toMiddleware($reference) : MiddlewareInterface; + public function toMiddleware($reference): MiddlewareInterface; + + /** + * Resolves the given references to middlewares + * + * @param array $references + * + * @return list + * + * @throws InvalidReferenceException + * If one of the given references cannot be resolved to a middleware. + * + * @since 3.0.0 + */ + public function toMiddlewares(array $references): array; } From 64486b90f8bd3c30e820ee324f0452ff49086cf3 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 20:04:06 +0100 Subject: [PATCH 020/180] Update CallableRequestHandler.php 1. Ability to pass the response resolver to the constructor; 2. New methods getReflection, getParameters, getAttributedParameter, getReturnType, getArguments, withArguments; 3. Ability to pass arguments to the callback; 4. Code style. --- src/RequestHandler/CallableRequestHandler.php | 151 +++++++++++++++++- 1 file changed, 143 insertions(+), 8 deletions(-) diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index d84e313b..ee03fa83 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -17,6 +17,18 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\ResponseResolverInterface; +use ReflectionFunctionAbstract; +use ReflectionFunction; +use ReflectionMethod; +use ReflectionParameter; +use ReflectionType; + +/** + * Import functions + */ +use function ksort; +use function Sunrise\Http\Router\reflect_callable; /** * CallableRequestHandler @@ -25,39 +37,162 @@ class CallableRequestHandler implements RequestHandlerInterface { /** - * The request handler callback + * The handler callback * * @var callable */ private $callback; + /** + * The callback arguments + * + * @var list + * + * @since 3.0.0 + * + * @psalm-immutable + */ + private array $arguments = []; + + /** + * The response resolver + * + * @var ResponseResolverInterface|null + * + * @since 3.0.0 + */ + private ?ResponseResolverInterface $responseResolver; + /** * Constructor of the class * * @param callable $callback + * @param ResponseResolverInterface|null $responseResolver */ - public function __construct(callable $callback) + public function __construct(callable $callback, ?ResponseResolverInterface $responseResolver = null) { $this->callback = $callback; + $this->responseResolver = $responseResolver; } /** - * Gets the request handler callback + * Gets the handler callback * * @return callable * * @since 2.10.0 */ - public function getCallback() : callable + public function getCallback(): callable { return $this->callback; } + /** + * Gets the callback reflection + * + * @return ReflectionFunction|ReflectionMethod + * + * @since 3.0.0 + */ + public function getReflection(): ReflectionFunctionAbstract + { + return reflect_callable($this->callback); + } + + /** + * Gets the callback parameters + * + * @return list + * + * @since 3.0.0 + */ + public function getParameters(): array + { + return $this->getReflection()->getParameters(); + } + + /** + * Gets the callback's attributed parameter + * + * @param class-string $attribute + * + * @return ReflectionParameter|null + * + * @since 3.0.0 + */ + public function getAttributedParameter(string $attribute): ?ReflectionParameter + { + foreach ($this->getParameters() as $parameter) { + if ($parameter->getAttributes($attribute)) { + return $parameter; + } + } + + return null; + } + + /** + * Gets the callback's return type + * + * @return ReflectionType|null + * + * @since 3.0.0 + */ + public function getReturnType(): ?ReflectionType + { + return $this->getReflection()->getReturnType(); + } + + /** + * Gets the callback arguments + * + * @return list + * + * @since 3.0.0 + */ + public function getArguments(): array + { + return $this->arguments; + } + + /** + * Creates a new instance of the request handler with the given arguments and returns it + * + * Please note that the first argument will be the server request instance. + * + * @param array $arguments + * + * @return static + * + * @since 3.0.0 + */ + public function withArguments(array $arguments): self + { + ksort($arguments, SORT_NUMERIC); + + $clone = clone $this; + $clone->arguments = []; + + /** @psalm-suppress MixedAssignment */ + foreach ($arguments as $argument) { + $clone->arguments[] = $argument; + } + + return $clone; + } + /** * {@inheritdoc} */ - public function handle(ServerRequestInterface $request) : ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { - return ($this->callback)($request); + /** @var ResponseInterface $response */ + $response = ($this->callback)($request, ...$this->arguments); + + if (isset($this->responseResolver)) { + return $this->responseResolver->resolveResponse($response); + } + + return $response; } } From 84a4ea476747e608cef84a405b48e43c39133bee Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 20:05:38 +0100 Subject: [PATCH 021/180] Update QueueableRequestHandler.php 1. Type declarations for properties; 2. Code style. --- .../QueueableRequestHandler.php | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/RequestHandler/QueueableRequestHandler.php b/src/RequestHandler/QueueableRequestHandler.php index ce9c45f0..ce55f941 100644 --- a/src/RequestHandler/QueueableRequestHandler.php +++ b/src/RequestHandler/QueueableRequestHandler.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -29,16 +29,16 @@ class QueueableRequestHandler implements RequestHandlerInterface /** * The request handler queue * - * @var SplQueue + * @var SplQueue */ - private $queue; + private SplQueue $queue; /** * The request handler endpoint * * @var RequestHandlerInterface */ - private $endpoint; + private RequestHandlerInterface $endpoint; /** * Constructor of the class @@ -47,7 +47,10 @@ class QueueableRequestHandler implements RequestHandlerInterface */ public function __construct(RequestHandlerInterface $endpoint) { - $this->queue = new SplQueue(); + /** @var SplQueue */ + $queue = new SplQueue(); + + $this->queue = $queue; $this->endpoint = $endpoint; } @@ -58,7 +61,7 @@ public function __construct(RequestHandlerInterface $endpoint) * * @return void */ - public function add(MiddlewareInterface ...$middlewares) : void + public function add(MiddlewareInterface ...$middlewares): void { foreach ($middlewares as $middleware) { $this->queue->enqueue($middleware); @@ -68,7 +71,7 @@ public function add(MiddlewareInterface ...$middlewares) : void /** * {@inheritdoc} */ - public function handle(ServerRequestInterface $request) : ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { if (!$this->queue->isEmpty()) { return $this->queue->dequeue()->process($request, $this); From b243cc7d967613262ea39462884a0fdccef460a2 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 20:05:49 +0100 Subject: [PATCH 022/180] Create ResponseResolverInterface.php --- src/ResponseResolverInterface.php | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/ResponseResolverInterface.php diff --git a/src/ResponseResolverInterface.php b/src/ResponseResolverInterface.php new file mode 100644 index 00000000..56ae81f6 --- /dev/null +++ b/src/ResponseResolverInterface.php @@ -0,0 +1,39 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseInterface; +use Sunrise\Http\Router\Exception\LogicException; + +/** + * ResponseResolverInterface + * + * @since 3.0.0 + */ +interface ResponseResolverInterface +{ + + /** + * Resolves the given data to the response instance + * + * @param mixed $data + * + * @return ResponseInterface + * + * @throws LogicException + * If the data cannot be resolved to the response. + */ + public function resolveResponse($data): ResponseInterface; +} From e5513e4fad1cc78d83a7114700cd661962f53bbc Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 20:07:06 +0100 Subject: [PATCH 023/180] Update Route.php 1. Type declarations for properties; 2. Deleted deprecated constants and sections of code; 3. Code style. --- src/Route.php | 151 +++++++++++++++++++++----------------------------- 1 file changed, 62 insertions(+), 89 deletions(-) diff --git a/src/Route.php b/src/Route.php index e442795c..03049070 100644 --- a/src/Route.php +++ b/src/Route.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -20,10 +20,7 @@ use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; -use Closure; use ReflectionClass; -use ReflectionMethod; -use ReflectionFunction; use Reflector; /** @@ -40,103 +37,85 @@ class Route implements RouteInterface { - /** - * Server Request attribute name for the route - * - * @var string - * - * @deprecated 2.11.0 Use the RouteInterface::ATTR_ROUTE constant. - */ - public const ATTR_NAME_FOR_ROUTE = self::ATTR_ROUTE; - - /** - * Server Request attribute name for the route name - * - * @var string - * - * @deprecated 2.9.0 - */ - public const ATTR_NAME_FOR_ROUTE_NAME = '@route-name'; - /** * The route name * * @var string */ - private $name; + private string $name; /** * The route host * * @var string|null */ - private $host = null; + private ?string $host = null; /** * The route path * * @var string */ - private $path; + private string $path; /** * The route methods * - * @var string[] + * @var list */ - private $methods; + private array $methods = []; /** * The route request handler * * @var RequestHandlerInterface */ - private $requestHandler; + private RequestHandlerInterface $requestHandler; /** * The route middlewares * - * @var MiddlewareInterface[] + * @var list */ - private $middlewares = []; + private array $middlewares = []; /** * The route attributes * - * @var array + * @var array */ - private $attributes = []; + private array $attributes = []; /** * The route summary * * @var string */ - private $summary = ''; + private string $summary = ''; /** * The route description * * @var string */ - private $description = ''; + private string $description = ''; /** * The route tags * - * @var string[] + * @var list */ - private $tags = []; + private array $tags = []; /** * Constructor of the class * * @param string $name * @param string $path - * @param string[] $methods + * @param list $methods * @param RequestHandlerInterface $requestHandler - * @param MiddlewareInterface[] $middlewares - * @param array $attributes + * @param list $middlewares + * @param array $attributes */ public function __construct( string $name, @@ -146,18 +125,18 @@ public function __construct( array $middlewares = [], array $attributes = [] ) { - $this->setName($name); - $this->setPath($path); + $this->name = $name; + $this->path = $path; $this->setMethods(...$methods); - $this->setRequestHandler($requestHandler); + $this->requestHandler = $requestHandler; $this->setMiddlewares(...$middlewares); - $this->setAttributes($attributes); + $this->attributes = $attributes; } /** * {@inheritdoc} */ - public function getName() : string + public function getName(): string { return $this->name; } @@ -165,7 +144,7 @@ public function getName() : string /** * {@inheritdoc} */ - public function getHost() : ?string + public function getHost(): ?string { return $this->host; } @@ -173,7 +152,7 @@ public function getHost() : ?string /** * {@inheritdoc} */ - public function getPath() : string + public function getPath(): string { return $this->path; } @@ -181,7 +160,7 @@ public function getPath() : string /** * {@inheritdoc} */ - public function getMethods() : array + public function getMethods(): array { return $this->methods; } @@ -189,7 +168,7 @@ public function getMethods() : array /** * {@inheritdoc} */ - public function getRequestHandler() : RequestHandlerInterface + public function getRequestHandler(): RequestHandlerInterface { return $this->requestHandler; } @@ -197,7 +176,7 @@ public function getRequestHandler() : RequestHandlerInterface /** * {@inheritdoc} */ - public function getMiddlewares() : array + public function getMiddlewares(): array { return $this->middlewares; } @@ -205,7 +184,7 @@ public function getMiddlewares() : array /** * {@inheritdoc} */ - public function getAttributes() : array + public function getAttributes(): array { return $this->attributes; } @@ -213,7 +192,7 @@ public function getAttributes() : array /** * {@inheritdoc} */ - public function getSummary() : string + public function getSummary(): string { return $this->summary; } @@ -221,7 +200,7 @@ public function getSummary() : string /** * {@inheritdoc} */ - public function getDescription() : string + public function getDescription(): string { return $this->description; } @@ -229,7 +208,7 @@ public function getDescription() : string /** * {@inheritdoc} */ - public function getTags() : array + public function getTags(): array { return $this->tags; } @@ -237,27 +216,19 @@ public function getTags() : array /** * {@inheritdoc} */ - public function getHolder() : Reflector + public function getHolder(): Reflector { - $handler = $this->requestHandler; - if ($handler instanceof CallableRequestHandler) { - $callback = $handler->getCallback(); - if ($callback instanceof Closure) { - return new ReflectionFunction($callback); - } - - /** @var array{0: class-string|object, 1: string} $callback */ - - return new ReflectionMethod(...$callback); + if ($this->requestHandler instanceof CallableRequestHandler) { + return $this->requestHandler->getReflection(); } - return new ReflectionClass($handler); + return new ReflectionClass($this->requestHandler); } /** * {@inheritdoc} */ - public function setName(string $name) : RouteInterface + public function setName(string $name): RouteInterface { $this->name = $name; @@ -267,7 +238,7 @@ public function setName(string $name) : RouteInterface /** * {@inheritdoc} */ - public function setHost(?string $host) : RouteInterface + public function setHost(?string $host): RouteInterface { $this->host = $host; @@ -277,7 +248,7 @@ public function setHost(?string $host) : RouteInterface /** * {@inheritdoc} */ - public function setPath(string $path) : RouteInterface + public function setPath(string $path): RouteInterface { $this->path = $path; @@ -287,21 +258,20 @@ public function setPath(string $path) : RouteInterface /** * {@inheritdoc} */ - public function setMethods(string ...$methods) : RouteInterface + public function setMethods(string ...$methods): RouteInterface { - foreach ($methods as &$method) { - $method = strtoupper($method); + $this->methods = []; + foreach ($methods as $method) { + $this->methods[] = strtoupper($method); } - $this->methods = $methods; - return $this; } /** * {@inheritdoc} */ - public function setRequestHandler(RequestHandlerInterface $requestHandler) : RouteInterface + public function setRequestHandler(RequestHandlerInterface $requestHandler): RouteInterface { $this->requestHandler = $requestHandler; @@ -311,8 +281,10 @@ public function setRequestHandler(RequestHandlerInterface $requestHandler) : Rou /** * {@inheritdoc} */ - public function setMiddlewares(MiddlewareInterface ...$middlewares) : RouteInterface + public function setMiddlewares(MiddlewareInterface ...$middlewares): RouteInterface { + /** @var list $middlewares */ + $this->middlewares = $middlewares; return $this; @@ -321,7 +293,7 @@ public function setMiddlewares(MiddlewareInterface ...$middlewares) : RouteInter /** * {@inheritdoc} */ - public function setAttributes(array $attributes) : RouteInterface + public function setAttributes(array $attributes): RouteInterface { $this->attributes = $attributes; @@ -331,7 +303,7 @@ public function setAttributes(array $attributes) : RouteInterface /** * {@inheritdoc} */ - public function setSummary(string $summary) : RouteInterface + public function setSummary(string $summary): RouteInterface { $this->summary = $summary; @@ -341,7 +313,7 @@ public function setSummary(string $summary) : RouteInterface /** * {@inheritdoc} */ - public function setDescription(string $description) : RouteInterface + public function setDescription(string $description): RouteInterface { $this->description = $description; @@ -351,8 +323,10 @@ public function setDescription(string $description) : RouteInterface /** * {@inheritdoc} */ - public function setTags(string ...$tags) : RouteInterface + public function setTags(string ...$tags): RouteInterface { + /** @var list $tags */ + $this->tags = $tags; return $this; @@ -361,7 +335,7 @@ public function setTags(string ...$tags) : RouteInterface /** * {@inheritdoc} */ - public function addPrefix(string $prefix) : RouteInterface + public function addPrefix(string $prefix): RouteInterface { // https://github.com/sunrise-php/http-router/issues/26 $prefix = rtrim($prefix, '/'); @@ -374,7 +348,7 @@ public function addPrefix(string $prefix) : RouteInterface /** * {@inheritdoc} */ - public function addSuffix(string $suffix) : RouteInterface + public function addSuffix(string $suffix): RouteInterface { $this->path .= $suffix; @@ -384,7 +358,7 @@ public function addSuffix(string $suffix) : RouteInterface /** * {@inheritdoc} */ - public function addMethod(string ...$methods) : RouteInterface + public function addMethod(string ...$methods): RouteInterface { foreach ($methods as $method) { $this->methods[] = strtoupper($method); @@ -396,7 +370,7 @@ public function addMethod(string ...$methods) : RouteInterface /** * {@inheritdoc} */ - public function addMiddleware(MiddlewareInterface ...$middlewares) : RouteInterface + public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterface { foreach ($middlewares as $middleware) { $this->middlewares[] = $middleware; @@ -408,10 +382,11 @@ public function addMiddleware(MiddlewareInterface ...$middlewares) : RouteInterf /** * {@inheritdoc} */ - public function withAddedAttributes(array $attributes) : RouteInterface + public function withAddedAttributes(array $attributes): RouteInterface { $clone = clone $this; + /** @psalm-suppress MixedAssignment */ foreach ($attributes as $key => $value) { $clone->attributes[$key] = $value; } @@ -422,13 +397,11 @@ public function withAddedAttributes(array $attributes) : RouteInterface /** * {@inheritdoc} */ - public function handle(ServerRequestInterface $request) : ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $request = $request->withAttribute(self::ATTR_ROUTE, $this); - /** @todo Must be removed from the 3.0.0 version */ - $request = $request->withAttribute(self::ATTR_NAME_FOR_ROUTE_NAME, $this->name); - + /** @psalm-suppress MixedAssignment */ foreach ($this->attributes as $key => $value) { $request = $request->withAttribute($key, $value); } From dc8d3aab01d4ed988109e574c49d782a8fb8fba9 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 21:08:54 +0100 Subject: [PATCH 024/180] Update RouteListCommand.php --- src/Command/RouteListCommand.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Command/RouteListCommand.php b/src/Command/RouteListCommand.php index b6f6a678..259aa205 100644 --- a/src/Command/RouteListCommand.php +++ b/src/Command/RouteListCommand.php @@ -59,15 +59,6 @@ public function __construct(?Router $router = null) $this->router = $router; } - /** - * {@inheritdoc} - */ - protected function configure(): void - { - $this->setName('router:route-list'); - $this->setDescription('Lists all routes in your application'); - } - /** * Gets the router instance populated with routes * @@ -93,6 +84,15 @@ protected function getRouter(): Router return $this->router; } + /** + * {@inheritdoc} + */ + protected function configure(): void + { + $this->setName('router:route-list'); + $this->setDescription('Lists all routes in your application'); + } + /** * {@inheritdoc} */ From 30d3ad69aad63a7e3387f9a3f7eaa1b5086cb98d Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 21:09:29 +0100 Subject: [PATCH 025/180] Removed the getCallback method --- src/Middleware/CallableMiddleware.php | 12 ------------ src/RequestHandler/CallableRequestHandler.php | 12 ------------ 2 files changed, 24 deletions(-) diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index a7eeccbb..e565ad4d 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -44,18 +44,6 @@ public function __construct(callable $callback) $this->callback = $callback; } - /** - * Gets the middleware callback - * - * @return callable - * - * @since 2.10.0 - */ - public function getCallback(): callable - { - return $this->callback; - } - /** * {@inheritdoc} */ diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index ee03fa83..5ea01241 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -75,18 +75,6 @@ public function __construct(callable $callback, ?ResponseResolverInterface $resp $this->responseResolver = $responseResolver; } - /** - * Gets the handler callback - * - * @return callable - * - * @since 2.10.0 - */ - public function getCallback(): callable - { - return $this->callback; - } - /** * Gets the callback reflection * From 9657812e74daa45f5e2283ac87da1bf46d2f3fc3 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 8 Jan 2023 21:11:35 +0100 Subject: [PATCH 026/180] Update CallableRequestHandler.php --- src/RequestHandler/CallableRequestHandler.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index 5ea01241..dadbd247 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -48,8 +48,6 @@ class CallableRequestHandler implements RequestHandlerInterface * * @var list * - * @since 3.0.0 - * * @psalm-immutable */ private array $arguments = []; @@ -58,8 +56,6 @@ class CallableRequestHandler implements RequestHandlerInterface * The response resolver * * @var ResponseResolverInterface|null - * - * @since 3.0.0 */ private ?ResponseResolverInterface $responseResolver; From a8e67b311f945ef7bd0709273d74af4f8dd251ab Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sat, 14 Jan 2023 04:47:08 +0100 Subject: [PATCH 027/180] v3 --- CHANGELOG.md | 7 +- composer.json | 22 +- functions/emit.php | 22 +- functions/path_build.php | 16 +- functions/path_match.php | 4 +- functions/path_parse.php | 37 +-- functions/path_plain.php | 4 +- functions/path_regex.php | 4 +- functions/reflect_callable.php | 68 +++++ src/Exception/BadRequestException.php | 43 --- src/Exception/Exception.php | 51 +--- src/Exception/ExceptionInterface.php | 23 +- src/Exception/Http/HttpException.php | 2 +- .../Http/HttpMethodNotAllowedException.php | 8 +- .../HttpUnsupportedMediaTypeException.php | 8 +- src/Exception/InvalidArgumentException.php | 11 +- .../InvalidAttributeValueException.php | 19 -- .../InvalidLoaderResourceException.php | 6 +- src/Exception/InvalidPayloadException.php | 26 ++ .../InvalidReferenceException.php} | 12 +- src/Exception/LogicException.php | 26 ++ src/Exception/MethodNotAllowedException.php | 42 +-- src/Exception/PageNotFoundException.php | 6 +- src/Exception/RouteNotFoundException.php | 11 +- ...eption.php => RoutePathBuildException.php} | 10 +- ...eption.php => RoutePathParseException.php} | 10 +- ...n.php => RoutePathProcessingException.php} | 10 +- .../UnsupportedMediaTypeException.php | 54 ---- src/Loader/ConfigLoader.php | 27 ++ src/Loader/DescriptorLoader.php | 31 +- src/Middleware/CallableMiddleware.php | 65 +++- .../JsonPayloadDecodingMiddleware.php | 36 +-- .../ParsedBodyMappingMiddleware.php | 154 ---------- src/ParameterResolver.php | 287 ++++++++++++++++++ ...ion.php => ParameterResolverInterface.php} | 12 +- src/ReferenceResolver.php | 7 + src/RequestHandler/CallableRequestHandler.php | 134 ++------ .../QueueableRequestHandler.php | 2 +- src/ResponseResolver.php | 39 +++ src/ResponseResolverInterface.php | 8 +- src/RouteCollection.php | 46 +-- src/RouteCollectionFactory.php | 6 +- src/RouteCollectionFactoryInterface.php | 6 +- src/RouteCollectionInterface.php | 26 +- src/RouteCollector.php | 84 +++-- src/RouteFactory.php | 4 +- src/RouteFactoryInterface.php | 6 +- src/RouteInterface.php | 10 +- src/Router.php | 58 ++-- src/RouterBuilder.php | 25 +- 50 files changed, 893 insertions(+), 742 deletions(-) create mode 100644 functions/reflect_callable.php delete mode 100644 src/Exception/BadRequestException.php delete mode 100644 src/Exception/InvalidAttributeValueException.php create mode 100644 src/Exception/InvalidPayloadException.php rename src/{Annotation/Body.php => Exception/InvalidReferenceException.php} (71%) create mode 100644 src/Exception/LogicException.php rename src/Exception/{UndecodablePayloadException.php => RoutePathBuildException.php} (58%) rename src/Exception/{InvalidPathException.php => RoutePathParseException.php} (58%) rename src/Exception/{UnresolvableReferenceException.php => RoutePathProcessingException.php} (59%) delete mode 100644 src/Exception/UnsupportedMediaTypeException.php delete mode 100644 src/Middleware/ParsedBodyMappingMiddleware.php create mode 100644 src/ParameterResolver.php rename src/{Exception/MissingAttributeValueException.php => ParameterResolverInterface.php} (52%) create mode 100644 src/ResponseResolver.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a4cc563b..c35e1984 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# v2.16.0 +# v3.0.0 + +* Minimal PHP version **7.4**; +* ... + +## v2.16.0 * New method: `Router::hasRoute(string):bool`. diff --git a/composer.json b/composer.json index 304c6856..36e331d7 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "sunrise/http-router", "homepage": "https://github.com/sunrise-php/http-router", - "description": "HTTP router for PHP 7.1+ based on PSR-7 and PSR-15 with support for annotations/attributes and OpenAPI (Swagger) Specification", + "description": "HTTP router for PHP 7.4+ based on PSR-7 and PSR-15 with support for annotations/attributes and OpenAPI (Swagger) Specification", "license": "MIT", "keywords": [ "fenric", @@ -21,26 +21,27 @@ ], "authors": [ { - "name": "Anatoly Fenric", + "name": "Anatoly Nekhay", "email": "afenric@gmail.com", "homepage": "https://github.com/fenric" } ], "require": { - "php": "^7.1|^8.0", + "php": ">=7.4", "fig/http-message-util": "^1.1", - "psr/container": "^1.0", + "psr/container": "^1.0 || ^2.0", "psr/http-message": "^1.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", - "psr/simple-cache": "^1.0" + "psr/simple-cache": "^1.0", + "sunrise/hydrator": "^2.7" }, "require-dev": { - "phpunit/phpunit": "7.5.20|9.5.0", - "sunrise/coding-standard": "1.0.0", - "sunrise/http-factory": "2.0.0", + "phpunit/phpunit": "~9.5.0", + "sunrise/coding-standard": "~1.0.0", + "sunrise/http-message": "^3.0", "doctrine/annotations": "^1.6", - "symfony/console": "^4.4", + "symfony/console": "^5.4", "symfony/event-dispatcher": "^4.4" }, "autoload": { @@ -50,7 +51,8 @@ "functions/path_match.php", "functions/path_parse.php", "functions/path_plain.php", - "functions/path_regex.php" + "functions/path_regex.php", + "functions/reflect_callable.php" ], "psr-4": { "Sunrise\\Http\\Router\\": "src/" diff --git a/functions/emit.php b/functions/emit.php index 39466230..38251649 100644 --- a/functions/emit.php +++ b/functions/emit.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -25,14 +25,19 @@ /** * Sends the given response * - * Don't use the function in your production environment, it's only for tests! - * * @param ResponseInterface $response * * @return void */ -function emit(ResponseInterface $response) : void +function emit(ResponseInterface $response): void { + header(sprintf( + 'HTTP/%s %d %s', + $response->getProtocolVersion(), + $response->getStatusCode(), + $response->getReasonPhrase() + ), true); + foreach ($response->getHeaders() as $name => $values) { foreach ($values as $value) { header(sprintf( @@ -43,12 +48,5 @@ function emit(ResponseInterface $response) : void } } - header(sprintf( - 'HTTP/%s %d %s', - $response->getProtocolVersion(), - $response->getStatusCode(), - $response->getReasonPhrase() - ), true); - echo $response->getBody(); } diff --git a/functions/path_build.php b/functions/path_build.php index d7557b9a..4ebff507 100644 --- a/functions/path_build.php +++ b/functions/path_build.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -14,8 +14,7 @@ /** * Import classes */ -use Sunrise\Http\Router\Exception\InvalidAttributeValueException; -use Sunrise\Http\Router\Exception\MissingAttributeValueException; +use Sunrise\Http\Router\Exception\RoutePathBuildException; /** * Import functions @@ -27,7 +26,7 @@ /** * Builds the given path using the given attributes * - * If strict mode is enabled, each attribute value will be validated. + * If strict mode is enabled then each attribute value will be validated. * * @param string $path * @param array $attributes @@ -35,8 +34,7 @@ * * @return string * - * @throws InvalidAttributeValueException - * @throws MissingAttributeValueException + * @throws RoutePathBuildException */ function path_build(string $path, array $attributes = [], bool $strict = false) : string { @@ -49,7 +47,7 @@ function path_build(string $path, array $attributes = [], bool $strict = false) if (!$match['isOptional']) { $errmsg = '[%s] build error: no value given for the attribute "%s".'; - throw new MissingAttributeValueException(sprintf($errmsg, $path, $match['name']), [ + throw new RoutePathBuildException(sprintf($errmsg, $path, $match['name']), [ 'path' => $path, 'match' => $match, ]); @@ -67,7 +65,7 @@ function path_build(string $path, array $attributes = [], bool $strict = false) if (!preg_match('#^' . $match['pattern'] . '$#u', $replacement)) { $errmsg = '[%s] build error: the given value for the attribute "%s" does not match its pattern.'; - throw new InvalidAttributeValueException(sprintf($errmsg, $path, $match['name']), [ + throw new RoutePathBuildException(sprintf($errmsg, $path, $match['name']), [ 'path' => $path, 'value' => $replacement, 'match' => $match, diff --git a/functions/path_match.php b/functions/path_match.php index 2142c96c..715413c0 100644 --- a/functions/path_match.php +++ b/functions/path_match.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ diff --git a/functions/path_parse.php b/functions/path_parse.php index b95be62b..4e00b732 100644 --- a/functions/path_parse.php +++ b/functions/path_parse.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -14,7 +14,7 @@ /** * Import classes */ -use Sunrise\Http\Router\Exception\InvalidPathException; +use Sunrise\Http\Router\Exception\RoutePathParseException; /** * Import functions @@ -59,7 +59,8 @@ * * @return array * - * @throws InvalidPathException If the given path syntax isn't valid. + * @throws RoutePathParseException + * If the given path syntax isn't valid. */ function path_parse(string $path) : array { @@ -104,7 +105,7 @@ function path_parse(string $path) : array if ('(' === $char && !$cursorInAttribute) { if ($cursorInParentheses) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] parentheses inside parentheses are not allowed.', $path, $cursorPosition) ); } @@ -116,13 +117,13 @@ function path_parse(string $path) : array if ('{' === $char && !$cursorInPattern) { if ($cursorInAttribute) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] braces inside attributes are not allowed.', $path, $cursorPosition) ); } if ($parenthesesBusy) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] multiple attributes inside parentheses are not allowed.', $path, $cursorPosition) ); } @@ -145,7 +146,7 @@ function path_parse(string $path) : array if ('<' === $char && $cursorInAttribute) { if ($cursorInPattern) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] the char "<" inside patterns is not allowed.', $path, $cursorPosition) ); } @@ -160,13 +161,13 @@ function path_parse(string $path) : array if ('>' === $char && $cursorInAttribute) { if (!$cursorInPattern) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] at position %2$d an extra char ">" was found.', $path, $cursorPosition) ); } if (null === $attributes[$attributeIndex]['pattern']) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] an attribute pattern is empty.', $path, $cursorPosition) ); } @@ -184,13 +185,13 @@ function path_parse(string $path) : array if ('}' === $char && !$cursorInPattern) { if (!$cursorInAttribute) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] at position %2$d an extra closing brace was found.', $path, $cursorPosition) ); } if (null === $attributes[$attributeIndex]['name']) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] an attribute name is empty.', $path, $cursorPosition) ); } @@ -206,7 +207,7 @@ function path_parse(string $path) : array if (')' === $char && !$cursorInAttribute) { if (!$cursorInParentheses) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] at position %2$d an extra closing parenthesis was found.', $path, $cursorPosition) ); } @@ -240,7 +241,7 @@ function path_parse(string $path) : array if ($cursorInAttributeName) { if (null === $attributes[$attributeIndex]['name']) { if (!isset(CHARACTER_TABLE_FOR_FIRST_CHARACTER_OF_SUBPATTERN_NAME[$char])) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] an attribute name must begin with "A-Za-z_".', $path, $cursorPosition) ); } @@ -248,7 +249,7 @@ function path_parse(string $path) : array if (null !== $attributes[$attributeIndex]['name']) { if (!isset(CHARACTER_TABLE_FOR_SUBPATTERN_NAME[$char])) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] an attribute name must contain only "0-9A-Za-z_".', $path, $cursorPosition) ); } @@ -259,7 +260,7 @@ function path_parse(string $path) : array if ($cursorInPattern) { if ('#' === $char) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s:%d] unallowed character "#" in an attribute pattern.', $path, $cursorPosition) ); } @@ -269,13 +270,13 @@ function path_parse(string $path) : array } if ($cursorInParentheses) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s] the route path contains non-closed parentheses.', $path) ); } if ($cursorInAttribute) { - throw new InvalidPathException( + throw new RoutePathParseException( sprintf('[%s] the route path contains non-closed attribute.', $path) ); } diff --git a/functions/path_plain.php b/functions/path_plain.php index d8638eec..a07e856b 100644 --- a/functions/path_plain.php +++ b/functions/path_plain.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ diff --git a/functions/path_regex.php b/functions/path_regex.php index 40162180..5f28274a 100644 --- a/functions/path_regex.php +++ b/functions/path_regex.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ diff --git a/functions/reflect_callable.php b/functions/reflect_callable.php new file mode 100644 index 00000000..d756bdd9 --- /dev/null +++ b/functions/reflect_callable.php @@ -0,0 +1,68 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use Closure; +use InvalidArgumentException; +use ReflectionFunctionAbstract; +use ReflectionFunction; +use ReflectionMethod; + +/** + * Import functions + */ +use function function_exists; +use function is_array; +use function is_object; +use function is_string; +use function method_exists; +use function strpos; + +/** + * Tries to reflect the given callback + * + * @param callable $callback + * + * @return ReflectionFunction|ReflectionMethod + * + * @throws InvalidArgumentException + * If the given callback cannot be reflected. + * + * @since 3.0.0 + */ +function reflect_callable(callable $callback): ReflectionFunctionAbstract +{ + if ($callback instanceof Closure) { + return new ReflectionFunction($callback); + } + + if (is_array($callback)) { + return new ReflectionMethod(...$callback); + } + + if (is_object($callback) && method_exists($callback, '__invoke')) { + return new ReflectionMethod($callback, '__invoke'); + } + + if (is_string($callback) && strpos($callback, '::')) { + return new ReflectionMethod($callback); + } + + if (is_string($callback) && function_exists($callback)) { + return new ReflectionFunction($callback); + } + + throw new InvalidArgumentException('Unsupported callback notation'); +} diff --git a/src/Exception/BadRequestException.php b/src/Exception/BadRequestException.php deleted file mode 100644 index 3cad99ee..00000000 --- a/src/Exception/BadRequestException.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Fenric - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception; - -/** - * BadRequestException - */ -class BadRequestException extends Exception -{ - - /** - * Gets errors - * - * @return mixed - * - * @since 2.9.0 - */ - public function getErrors() - { - return $this->fromContext('errors', []); - } - - /** - * Gets violations - * - * @return mixed - * - * @deprecated 2.9.0 Use the getErrors method. - */ - public function getViolations() - { - return $this->fromContext('violations', []); - } -} diff --git a/src/Exception/Exception.php b/src/Exception/Exception.php index d773c958..d414fe46 100644 --- a/src/Exception/Exception.php +++ b/src/Exception/Exception.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -14,52 +14,11 @@ /** * Import classes */ -use RuntimeException; -use Throwable; +use Exception as BaseException; /** - * Exception + * The package base exception */ -class Exception extends RuntimeException implements ExceptionInterface +class Exception extends BaseException implements ExceptionInterface { - - /** - * Context of the exception - * - * @var array - */ - private $context = []; - - /** - * Constructor of the exception - * - * @param string $message - * @param array $context - * @param int $code - * @param Throwable|null $previous - * - * @link https://www.php.net/manual/en/exception.construct.php - */ - public function __construct(string $message = '', array $context = [], int $code = 0, ?Throwable $previous = null) - { - $this->context = $context; - - parent::__construct($message, $code, $previous); - } - - /** - * {@inheritdoc} - */ - final public function getContext() : array - { - return $this->context; - } - - /** - * {@inheritdoc} - */ - final public function fromContext($key, $default = null) - { - return $this->context[$key] ?? $default; - } } diff --git a/src/Exception/ExceptionInterface.php b/src/Exception/ExceptionInterface.php index b0f0940c..ec36257a 100644 --- a/src/Exception/ExceptionInterface.php +++ b/src/Exception/ExceptionInterface.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -17,25 +17,8 @@ use Throwable; /** - * ExceptionInterface + * The package base exception interface */ interface ExceptionInterface extends Throwable { - - /** - * Gets the exception context - * - * @return array - */ - public function getContext() : array; - - /** - * Gets a value from the exception context by the given key - * - * @param mixed $key - * @param mixed $default - * - * @return mixed - */ - public function fromContext($key, $default); } diff --git a/src/Exception/Http/HttpException.php b/src/Exception/Http/HttpException.php index 4987e8dd..401fea9b 100644 --- a/src/Exception/Http/HttpException.php +++ b/src/Exception/Http/HttpException.php @@ -30,7 +30,7 @@ class HttpException extends Exception implements HttpExceptionInterface * * @var int */ - private int $statusCode; + private $statusCode; /** * Constructor of the class diff --git a/src/Exception/Http/HttpMethodNotAllowedException.php b/src/Exception/Http/HttpMethodNotAllowedException.php index b70663cc..5c61c488 100644 --- a/src/Exception/Http/HttpMethodNotAllowedException.php +++ b/src/Exception/Http/HttpMethodNotAllowedException.php @@ -46,13 +46,13 @@ class HttpMethodNotAllowedException extends HttpException * * @var list */ - private array $allowedMethods; + private array $allowedMethods = []; /** * Constructor of the class * * @param string $unallowedMethod - * @param list $allowedMethods + * @param string[] $allowedMethods * @param ?string $message * @param int $code * @param ?Throwable $previous @@ -69,7 +69,9 @@ public function __construct( parent::__construct(self::STATUS_METHOD_NOT_ALLOWED, $message, $code, $previous); $this->unallowedMethod = $unallowedMethod; - $this->allowedMethods = $allowedMethods; + foreach ($allowedMethods as $allowedMethod) { + $this->allowedMethods[] = $allowedMethod; + } } /** diff --git a/src/Exception/Http/HttpUnsupportedMediaTypeException.php b/src/Exception/Http/HttpUnsupportedMediaTypeException.php index 6c31d2ff..090887b7 100644 --- a/src/Exception/Http/HttpUnsupportedMediaTypeException.php +++ b/src/Exception/Http/HttpUnsupportedMediaTypeException.php @@ -45,13 +45,13 @@ class HttpUnsupportedMediaTypeException extends HttpException * * @var list */ - private array $supportedMediaTypes; + private array $supportedMediaTypes = []; /** * Constructor of the class * * @param string $unsupportedMediaType - * @param list $supportedMediaTypes + * @param string[] $supportedMediaTypes * @param ?string $message * @param int $code * @param ?Throwable $previous @@ -68,7 +68,9 @@ public function __construct( parent::__construct(self::STATUS_UNSUPPORTED_MEDIA_TYPE, $message, $code, $previous); $this->unsupportedMediaType = $unsupportedMediaType; - $this->supportedMediaTypes = $supportedMediaTypes; + foreach ($supportedMediaTypes as $supportedMediaType) { + $this->supportedMediaTypes[] = $supportedMediaType; + } } /** diff --git a/src/Exception/InvalidArgumentException.php b/src/Exception/InvalidArgumentException.php index 7319ec42..1ef49b94 100644 --- a/src/Exception/InvalidArgumentException.php +++ b/src/Exception/InvalidArgumentException.php @@ -3,19 +3,24 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ namespace Sunrise\Http\Router\Exception; +/** + * Import classes + */ +use InvalidArgumentException as BaseInvalidArgumentException; + /** * InvalidArgumentException * * @since 2.9.0 */ -class InvalidArgumentException extends Exception +class InvalidArgumentException extends BaseInvalidArgumentException implements ExceptionInterface { } diff --git a/src/Exception/InvalidAttributeValueException.php b/src/Exception/InvalidAttributeValueException.php deleted file mode 100644 index aa16e284..00000000 --- a/src/Exception/InvalidAttributeValueException.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Fenric - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception; - -/** - * InvalidAttributeValueException - */ -class InvalidAttributeValueException extends Exception -{ -} diff --git a/src/Exception/InvalidLoaderResourceException.php b/src/Exception/InvalidLoaderResourceException.php index 883a8112..42301f93 100644 --- a/src/Exception/InvalidLoaderResourceException.php +++ b/src/Exception/InvalidLoaderResourceException.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -14,6 +14,6 @@ /** * InvalidLoaderResourceException */ -class InvalidLoaderResourceException extends Exception +class InvalidLoaderResourceException extends InvalidArgumentException { } diff --git a/src/Exception/InvalidPayloadException.php b/src/Exception/InvalidPayloadException.php new file mode 100644 index 00000000..b10d32de --- /dev/null +++ b/src/Exception/InvalidPayloadException.php @@ -0,0 +1,26 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; + +/** + * InvalidPayloadException + * + * @since 3.0.0 + */ +class InvalidPayloadException extends HttpBadRequestException +{ +} diff --git a/src/Annotation/Body.php b/src/Exception/InvalidReferenceException.php similarity index 71% rename from src/Annotation/Body.php rename to src/Exception/InvalidReferenceException.php index ad90075c..a099da3d 100644 --- a/src/Annotation/Body.php +++ b/src/Exception/InvalidReferenceException.php @@ -9,17 +9,13 @@ * @link https://github.com/sunrise-php/http-router */ -namespace Sunrise\Http\Router\Annotation; - -/** - * Import classes - */ -use Attribute; +namespace Sunrise\Http\Router\Exception; /** + * InvalidReferenceException + * * @since 3.0.0 */ -#[Attribute(Attribute::TARGET_PARAMETER)] -final class Body +class InvalidReferenceException extends LogicException { } diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php new file mode 100644 index 00000000..17c8238b --- /dev/null +++ b/src/Exception/LogicException.php @@ -0,0 +1,26 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use LogicException as BaseLogicException; + +/** + * LogicException + * + * @since 3.0.0 + */ +class LogicException extends BaseLogicException implements ExceptionInterface +{ +} diff --git a/src/Exception/MethodNotAllowedException.php b/src/Exception/MethodNotAllowedException.php index 60101ad8..b2edb2f4 100644 --- a/src/Exception/MethodNotAllowedException.php +++ b/src/Exception/MethodNotAllowedException.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -12,45 +12,13 @@ namespace Sunrise\Http\Router\Exception; /** - * Import functions + * Import classes */ -use function implode; +use Sunrise\Http\Router\Exception\Http\HttpMethodNotAllowedException; /** * MethodNotAllowedException */ -class MethodNotAllowedException extends Exception +class MethodNotAllowedException extends HttpMethodNotAllowedException { - - /** - * Gets a method - * - * @return string - * - * @since 2.9.0 - */ - public function getMethod() : string - { - return $this->fromContext('method', ''); - } - - /** - * Gets allowed methods - * - * @return string[] - */ - public function getAllowedMethods() : array - { - return $this->fromContext('allowed', []); - } - - /** - * Gets joined allowed methods - * - * @return string - */ - public function getJoinedAllowedMethods() : string - { - return implode(',', $this->getAllowedMethods()); - } } diff --git a/src/Exception/PageNotFoundException.php b/src/Exception/PageNotFoundException.php index 1a26d248..461cc136 100644 --- a/src/Exception/PageNotFoundException.php +++ b/src/Exception/PageNotFoundException.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -13,8 +13,6 @@ /** * PageNotFoundException - * - * @since 2.4.2 */ class PageNotFoundException extends RouteNotFoundException { diff --git a/src/Exception/RouteNotFoundException.php b/src/Exception/RouteNotFoundException.php index 8877a240..4e78d5d8 100644 --- a/src/Exception/RouteNotFoundException.php +++ b/src/Exception/RouteNotFoundException.php @@ -3,17 +3,22 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ namespace Sunrise\Http\Router\Exception; +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; + /** * RouteNotFoundException */ -class RouteNotFoundException extends Exception +class RouteNotFoundException extends HttpNotFoundException { } diff --git a/src/Exception/UndecodablePayloadException.php b/src/Exception/RoutePathBuildException.php similarity index 58% rename from src/Exception/UndecodablePayloadException.php rename to src/Exception/RoutePathBuildException.php index 2a440230..45e4da74 100644 --- a/src/Exception/UndecodablePayloadException.php +++ b/src/Exception/RoutePathBuildException.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -12,10 +12,10 @@ namespace Sunrise\Http\Router\Exception; /** - * UndecodablePayloadException + * RoutePathBuildException * - * @since 2.15.0 + * @since 3.0.0 */ -class UndecodablePayloadException extends BadRequestException +class RoutePathBuildException extends RoutePathProcessingException { } diff --git a/src/Exception/InvalidPathException.php b/src/Exception/RoutePathParseException.php similarity index 58% rename from src/Exception/InvalidPathException.php rename to src/Exception/RoutePathParseException.php index ad788e8f..158f92f9 100644 --- a/src/Exception/InvalidPathException.php +++ b/src/Exception/RoutePathParseException.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -12,8 +12,10 @@ namespace Sunrise\Http\Router\Exception; /** - * InvalidPathException + * RoutePathParseException + * + * @since 3.0.0 */ -class InvalidPathException extends Exception +class RoutePathParseException extends RoutePathProcessingException { } diff --git a/src/Exception/UnresolvableReferenceException.php b/src/Exception/RoutePathProcessingException.php similarity index 59% rename from src/Exception/UnresolvableReferenceException.php rename to src/Exception/RoutePathProcessingException.php index a3b78143..8e4022e0 100644 --- a/src/Exception/UnresolvableReferenceException.php +++ b/src/Exception/RoutePathProcessingException.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -12,10 +12,10 @@ namespace Sunrise\Http\Router\Exception; /** - * UnresolvableReferenceException + * RoutePathProcessingException * - * @since 2.10.0 + * @since 3.0.0 */ -class UnresolvableReferenceException extends Exception +class RoutePathProcessingException extends Exception { } diff --git a/src/Exception/UnsupportedMediaTypeException.php b/src/Exception/UnsupportedMediaTypeException.php deleted file mode 100644 index 2f4c795b..00000000 --- a/src/Exception/UnsupportedMediaTypeException.php +++ /dev/null @@ -1,54 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Fenric - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception; - -/** - * Import functions - */ -use function implode; - -/** - * UnsupportedMediaTypeException - */ -class UnsupportedMediaTypeException extends Exception -{ - - /** - * Gets a type - * - * @return string - */ - public function getType() : string - { - return $this->fromContext('type', ''); - } - - /** - * Gets supported types - * - * @return string[] - */ - public function getSupportedTypes() : array - { - return $this->fromContext('supported', []); - } - - /** - * Gets joined supported types - * - * @return string - */ - public function getJoinedSupportedTypes() : string - { - return implode(',', $this->getSupportedTypes()); - } -} diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 96cdc357..9d0bca76 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -16,6 +16,7 @@ */ use Psr\Container\ContainerInterface; use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; +use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; use Sunrise\Http\Router\ResponseResolverInterface; @@ -103,6 +104,32 @@ public function setContainer(?ContainerInterface $container): void $this->referenceResolver->setContainer($container); } + /** + * Gets the loader parameter resolver + * + * @return ParameterResolverInterface|null + * + * @since 3.0.0 + */ + public function getParameterResolver(): ?ParameterResolverInterface + { + return $this->referenceResolver->getParameterResolver(); + } + + /** + * Sets the given parameter resolver to the loader + * + * @param ParameterResolverInterface|null $parameterResolver + * + * @return void + * + * @since 3.0.0 + */ + public function setParameterResolver(?ParameterResolverInterface $parameterResolver): void + { + $this->referenceResolver->setParameterResolver($parameterResolver); + } + /** * Gets the loader response resolver * diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index a8237386..f8517755 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -27,6 +27,7 @@ use Sunrise\Http\Router\Annotation\Route; use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; use Sunrise\Http\Router\ResponseResolverInterface; @@ -117,7 +118,7 @@ public function __construct( $this->routeFactory = $routeFactory ?? new RouteFactory(); $this->referenceResolver = $referenceResolver ?? new ReferenceResolver(); - if (8 > PHP_MAJOR_VERSION) { + if (PHP_MAJOR_VERSION < 8) { $this->useDefaultAnnotationReader(); } } @@ -144,6 +145,32 @@ public function setContainer(?ContainerInterface $container): void $this->referenceResolver->setContainer($container); } + /** + * Gets the loader parameter resolver + * + * @return ParameterResolverInterface|null + * + * @since 3.0.0 + */ + public function getParameterResolver(): ?ParameterResolverInterface + { + return $this->referenceResolver->getParameterResolver(); + } + + /** + * Sets the given parameter resolver to the loader + * + * @param ParameterResolverInterface|null $parameterResolver + * + * @return void + * + * @since 3.0.0 + */ + public function setParameterResolver(?ParameterResolverInterface $parameterResolver): void + { + $this->referenceResolver->setParameterResolver($parameterResolver); + } + /** * Gets the loader response resolver * @@ -495,7 +522,7 @@ private function getAnnotations(Reflector $reflector, string $annotationName): a { $result = []; - if (8 === PHP_MAJOR_VERSION) { + if (PHP_MAJOR_VERSION === 8) { /** @var ReflectionAttribute[] */ $attributes = $reflector->getAttributes($annotationName); foreach ($attributes as $attribute) { diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index e565ad4d..49cd99ff 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -18,13 +18,23 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\ParameterResolverInterface; +use Sunrise\Http\Router\ResponseResolverInterface; +use ReflectionFunctionAbstract; +use ReflectionFunction; +use ReflectionMethod; + +/** + * Import functions + */ +use function Sunrise\Http\Router\reflect_callable; /** * CallableMiddleware * * @since 2.8.0 */ -class CallableMiddleware implements MiddlewareInterface +final class CallableMiddleware implements MiddlewareInterface { /** @@ -34,14 +44,54 @@ class CallableMiddleware implements MiddlewareInterface */ private $callback; + /** + * The callback's parameter resolver + * + * @var ParameterResolverInterface + */ + private ParameterResolverInterface $parameterResolver; + + /** + * The callback's response resolver + * + * @var ResponseResolverInterface + */ + private ResponseResolverInterface $responseResolver; + + /** + * The callback's reflection + * + * @var ReflectionFunction|ReflectionMethod + */ + private ?ReflectionFunctionAbstract $reflection = null; + /** * Constructor of the class * * @param callable $callback + * @param ParameterResolverInterface $parameterResolver + * @param ResponseResolverInterface $responseResolver */ - public function __construct(callable $callback) - { + public function __construct( + callable $callback, + ParameterResolverInterface $parameterResolver, + ResponseResolverInterface $responseResolver + ) { $this->callback = $callback; + $this->parameterResolver = $parameterResolver; + $this->responseResolver = $responseResolver; + } + + /** + * Gets the callback's reflection + * + * @return ReflectionFunction|ReflectionMethod + * + * @since 3.0.0 + */ + public function getReflection(): ReflectionFunctionAbstract + { + return $this->reflection ??= reflect_callable($this->callback); } /** @@ -49,9 +99,12 @@ public function __construct(callable $callback) */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - /** @var ResponseInterface */ - $response = ($this->callback)($request, $handler); + $arguments = $this->parameterResolver + ->withNames($request->getAttributes()) + ->withType(ServerRequestInterface::class, $request) + ->withType(RequestHandlerInterface::class, $handler) + ->resolveParameters(...$this->getReflection()->getParameters()); - return $response; + return $this->responseResolver->resolveResponse(($this->callback)(...$arguments)); } } diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index ddf5d6ce..2c1431f2 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -55,26 +55,19 @@ class JsonPayloadDecodingMiddleware implements MiddlewareInterface */ private const JSON_MEDIA_TYPE = 'application/json'; - /** - * JSON maximal depth - * - * @var int - */ - protected const JSON_MAXIMAL_DEPTH = 512; - /** * JSON decoding options * * @var int * - * @link https://www.php.net/manual/ru/json.constants.php + * @link https://www.php.net/json.constants */ protected const JSON_DECODING_OPTIONS = JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY; /** * {@inheritdoc} */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + final public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if ($this->isSupportedRequest($request)) { $data = $this->decodeRequestJsonPayload($request); @@ -93,11 +86,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface */ private function isSupportedRequest(ServerRequestInterface $request): bool { - return self::JSON_MEDIA_TYPE === $this->getRequestMediaType($request); + return $this->getRequestMediaType($request) === self::JSON_MEDIA_TYPE; } /** - * Gets media type from the given request + * Gets a media type from the given request + * + * Returns null if a media type cannot be retrieved. * * @param ServerRequestInterface $request * @@ -114,12 +109,12 @@ private function getRequestMediaType(ServerRequestInterface $request): ?string // type "/" subtype *( OWS ";" OWS parameter ) $mediaType = $request->getHeaderLine('Content-Type'); - $semicolonPosition = strpos($mediaType, ';'); - if (false === $semicolonPosition) { + $semicolon = strpos($mediaType, ';'); + if (false === $semicolon) { return $mediaType; } - return rtrim(substr($mediaType, 0, $semicolonPosition)); + return rtrim(substr($mediaType, 0, $semicolon)); } /** @@ -130,27 +125,20 @@ private function getRequestMediaType(ServerRequestInterface $request): ?string * @return array|object|null * * @throws InvalidPayloadException - * If the request's payload cannot be decoded. + * If the request's JSON payload cannot be decoded. */ private function decodeRequestJsonPayload(ServerRequestInterface $request) { - /** @var int */ - $depth = static::JSON_MAXIMAL_DEPTH; - /** @var int */ $flags = static::JSON_DECODING_OPTIONS; try { /** @var mixed */ - $result = json_decode($request->getBody()->__toString(), null, $depth, $flags | JSON_THROW_ON_ERROR); + $result = json_decode($request->getBody()->__toString(), null, 512, $flags | JSON_THROW_ON_ERROR); } catch (JsonException $e) { throw new InvalidPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); } - if (is_array($result) || is_object($result)) { - return $result; - } - - return null; + return (is_array($result) || is_object($result)) ? $result : null; } } diff --git a/src/Middleware/ParsedBodyMappingMiddleware.php b/src/Middleware/ParsedBodyMappingMiddleware.php deleted file mode 100644 index f7f25804..00000000 --- a/src/Middleware/ParsedBodyMappingMiddleware.php +++ /dev/null @@ -1,154 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Middleware; - -/** - * Import classes - */ -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Annotation\Body; -use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; -use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; -use Sunrise\Http\Router\RouteInterface; -use Sunrise\Hydrator\Exception\InvalidValueException; -use Sunrise\Hydrator\Exception\InvalidObjectException; -use Sunrise\Hydrator\HydratorInterface; -use Sunrise\Hydrator\Hydrator; -use ReflectionNamedType; - -/** - * Import functions - */ -use function class_exists; -use function sprintf; - -/** - * Import constants - */ -use const PHP_MAJOR_VERSION; - -/** - * ParsedBodyMappingMiddleware - * - * @link https://github.com/sunrise-php/hydrator - * - * @since 3.0.0 - */ -class ParsedBodyMappingMiddleware implements MiddlewareInterface -{ - - /** - * Objects hydrator - * - * @var HydratorInterface - */ - private HydratorInterface $objectHydrator; - - /** - * Constructor of the class - * - * @param HydratorInterface|null $objectHydrator - * - * @throws LogicException - * If the PHP version less than 8. - */ - public function __construct(?HydratorInterface $objectHydrator = null) - { - if (PHP_MAJOR_VERSION < 8) { - throw new LogicException('Parsed body mapping requires PHP version 8'); - } - - $this->objectHydrator = $objectHydrator ?? new Hydrator(); - } - - /** - * {@inheritdoc} - * - * @throws LogicException - * If something went wrong... - * - * @throws HttpUnprocessableEntityException - * If the request body isn't valid. - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - $route = $request->getAttribute(RouteInterface::ATTR_ROUTE); - if (!($route instanceof RouteInterface)) { - throw new LogicException(sprintf( - 'The "%s" middleware must be related with a route', - __CLASS__ - )); - } - - $routeRequestHandler = $route->getRequestHandler(); - if (!($routeRequestHandler instanceof CallableRequestHandler)) { - throw new LogicException(sprintf( - 'The "%s" route cannot be related with the "%s" middleware', - $route->getName(), - __CLASS__ - )); - } - - $routeRequestHandlerProcessableParameter = $routeRequestHandler->getAttributedParameter(Body::class); - if (!isset($routeRequestHandlerProcessableParameter)) { - return $handler->handle($request); - } - - $routeRequestHandlerProcessableParameterType = $routeRequestHandlerProcessableParameter->getType(); - if (!($routeRequestHandlerProcessableParameterType instanceof ReflectionNamedType)) { - throw new LogicException(sprintf( - 'The "%s" parameter of the "%s" route request handler cannot be hydrated ' . - 'because its type is not supported, do not use union or intersection types', - $routeRequestHandlerProcessableParameter->getName(), - $route->getName() - )); - } - - $routeRequestHandlerProcessableParameterClassName = $routeRequestHandlerProcessableParameterType->getName(); - if (!class_exists($routeRequestHandlerProcessableParameterClassName)) { - throw new LogicException(sprintf( - 'The "%s" parameter of the "%s" route request handler cannot be hydrated ' . - 'because its refers to the "%s" class that cannot be found', - $routeRequestHandlerProcessableParameter->getName(), - $route->getName(), - $routeRequestHandlerProcessableParameterType->getName() - )); - } - - try { - $hydratedObject = $this->objectHydrator->hydrate( - $routeRequestHandlerProcessableParameterClassName, - (array) $request->getParsedBody() - ); - } catch (InvalidObjectException $e) { - throw new LogicException(sprintf( - 'The "%s" parameter of the "%s" route request handler cannot be hydrated ' . - 'because its refers to an invalid DTO: %s', - $routeRequestHandlerProcessableParameter->getName(), - $route->getName(), - $e->getMessage() - ), 0, $e); - } catch (InvalidValueException $e) { - throw new HttpUnprocessableEntityException($e->getMessage(), 0, $e); - } - - $routeRequestHandlerArguments = $routeRequestHandler->getArguments(); - $routeRequestHandlerArguments[$routeRequestHandlerProcessableParameter->getPosition()] = $hydratedObject; - $route->setRequestHandler($routeRequestHandler->withArguments($routeRequestHandlerArguments)); - - return $handler->handle($request); - } -} diff --git a/src/ParameterResolver.php b/src/ParameterResolver.php new file mode 100644 index 00000000..6f5c1771 --- /dev/null +++ b/src/ParameterResolver.php @@ -0,0 +1,287 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use Psr\Container\ContainerInterface; +use ReflectionNamedType; +use ReflectionParameter; + +/** + * ParameterResolver + * + * @since 3.0.0 + */ +final class ParameterResolver implements ParameterResolverInterface +{ + + /** + * Known parameter names + * + * @var array + */ + private array $names = []; + + /** + * Known types + * + * @var array + */ + private array $types = []; + + /** + * The resolver's container + * + * @var ContainerInterface|null + */ + private ?ContainerInterface $container = null; + + /** + * {@inheritdoc} + */ + public function getNames(): array + { + return $this->names; + } + + /** + * {@inheritdoc} + */ + public function getTypes(): array + { + return $this->types; + } + + /** + * {@inheritdoc} + */ + public function getContainer(): ?ContainerInterface + { + return $this->container; + } + + /** + * {@inheritdoc} + */ + public function setContainer(?ContainerInterface $container): void + { + $this->container = $container; + } + + /** + * {@inheritdoc} + */ + public function withName(string $name, $value): ParameterResolverInterface + { + $clone = clone $this; + $clone->setName($name, $value); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function withNames(array $names): ParameterResolverInterface + { + $clone = clone $this; + foreach ($names as $name => $value) { + $clone->setName($name, $value); + } + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function withType(string $type, $value): ParameterResolverInterface + { + $clone = clone $this; + $clone->setType($type, $value); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function withTypes(array $types): ParameterResolverInterface + { + $clone = clone $this; + foreach ($types as $type => $value) { + $clone->setType($type, $value); + } + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function resolveParameters(ReflectionParameter ...$parameters): array + { + $arguments = []; + foreach ($parameters as $parameter) { + $arguments[] = $this->resolveParameter($parameter); + } + + return $arguments; + } + + /** + * Sets a new known name + * + * @param string $name + * @param mixed $value + * + * @return void + */ + private function setName(string $name, $value): void + { + $this->names[$name] = $value; + } + + /** + * Sets a new known type + * + * @param string $type + * @param mixed $value + * + * @return void + * + * @throws LogicException + * If the value isn't an instance of the type. + */ + private function setType(string $type, $value): void + { + if (!($value instanceof $type)) { + throw new LogicException(); + } + + $this->types[$type] = $value; + } + + /** + * Resolves the given parameter + * + * @param ReflectionParameter $parameter + * + * @return mixed + * + * @throws LogicException + * If the parameter cannot be resolved. + */ + private function resolveParameter(ReflectionParameter $parameter) + { + if (!$parameter->hasType()) { + return $this->resolveUntypedParameter($parameter); + } + + $type = $parameter->getType(); + if (!($type instanceof ReflectionNamedType)) { + throw new LogicException(); + } + + return !$type->isBuiltin() ? + $this->resolveTypedParameterWithNonBuiltinType($type, $parameter) : + $this->resolveTypedParameterWithBuiltinType($type, $parameter); + } + + /** + * Resolves the given untyped parameter + * + * @param ReflectionParameter $parameter + * + * @return mixed + * + * @throws LogicException + * If the parameter cannot be resolved. + */ + private function resolveUntypedParameter(ReflectionParameter $parameter) + { + if (array_key_exists($parameter->getName(), $this->names)) { + return $this->names[$parameter->getName()]; + } + + if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + + throw new LogicException(); + } + + /** + * Resolves the given typed parameter with non-built-in type + * + * @param ReflectionNamedType $type + * @param ReflectionParameter $parameter + * + * @return mixed + * + * @throws LogicException + * If the parameter cannot be resolved. + */ + private function resolveTypedParameterWithNonBuiltinType(ReflectionNamedType $type, ReflectionParameter $parameter) + { + if (isset($this->types[$parameter->getName()])) { + return $this->types[$parameter->getName()]; + } + + if (isset($this->container) && $this->container->has($type->getName())) { + return $this->container->get($type->getName()); + } + + if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + + throw new LogicException(); + } + + /** + * Resolves the given typed parameter with built-in type + * + * @param ReflectionNamedType $type + * @param ReflectionParameter $parameter + * + * @return mixed + * + * @throws LogicException + * If the parameter cannot be resolved. + */ + private function resolveTypedParameterWithBuiltinType(ReflectionNamedType $type, ReflectionParameter $parameter) + { + if (!array_key_exists($parameter->getName(), $this->names)) { + if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + + throw new LogicException(); + } + + switch ($type->getName()) { + case 'bool': + break; + case 'int': + break; + case 'float': + break; + case 'string': + break; + } + + throw new LogicException(); + } +} diff --git a/src/Exception/MissingAttributeValueException.php b/src/ParameterResolverInterface.php similarity index 52% rename from src/Exception/MissingAttributeValueException.php rename to src/ParameterResolverInterface.php index 142d2d07..281faed7 100644 --- a/src/Exception/MissingAttributeValueException.php +++ b/src/ParameterResolverInterface.php @@ -3,17 +3,19 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ -namespace Sunrise\Http\Router\Exception; +namespace Sunrise\Http\Router; /** - * MissingAttributeValueException + * ParameterResolverInterface + * + * @since 3.0.0 */ -class MissingAttributeValueException extends Exception +interface ParameterResolverInterface { } diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index 30a3c658..9e25e165 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -47,6 +47,13 @@ class ReferenceResolver implements ReferenceResolverInterface */ private ?ContainerInterface $container = null; + /** + * The reference resolver's parameter resolver + * + * @var ParameterResolverInterface|null + */ + private ?ParameterResolverInterface $parameterResolver = null; + /** * The reference resolver's response resolver * diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index dadbd247..c2bd086a 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -17,23 +17,21 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ResponseResolverInterface; use ReflectionFunctionAbstract; use ReflectionFunction; use ReflectionMethod; -use ReflectionParameter; -use ReflectionType; /** * Import functions */ -use function ksort; use function Sunrise\Http\Router\reflect_callable; /** * CallableRequestHandler */ -class CallableRequestHandler implements RequestHandlerInterface +final class CallableRequestHandler implements RequestHandlerInterface { /** @@ -44,35 +42,45 @@ class CallableRequestHandler implements RequestHandlerInterface private $callback; /** - * The callback arguments + * The callback's parameter resolver * - * @var list + * @var ParameterResolverInterface + */ + private ParameterResolverInterface $parameterResolver; + + /** + * The callback's response resolver * - * @psalm-immutable + * @var ResponseResolverInterface */ - private array $arguments = []; + private ResponseResolverInterface $responseResolver; /** - * The response resolver + * The callback's reflection * - * @var ResponseResolverInterface|null + * @var ReflectionFunction|ReflectionMethod */ - private ?ResponseResolverInterface $responseResolver; + private ?ReflectionFunctionAbstract $reflection = null; /** * Constructor of the class * * @param callable $callback - * @param ResponseResolverInterface|null $responseResolver + * @param ParameterResolverInterface $parameterResolver + * @param ResponseResolverInterface $responseResolver */ - public function __construct(callable $callback, ?ResponseResolverInterface $responseResolver = null) - { + public function __construct( + callable $callback, + ParameterResolverInterface $parameterResolver, + ResponseResolverInterface $responseResolver + ) { $this->callback = $callback; + $this->parameterResolver = $parameterResolver; $this->responseResolver = $responseResolver; } /** - * Gets the callback reflection + * Gets the callback's reflection * * @return ReflectionFunction|ReflectionMethod * @@ -80,89 +88,7 @@ public function __construct(callable $callback, ?ResponseResolverInterface $resp */ public function getReflection(): ReflectionFunctionAbstract { - return reflect_callable($this->callback); - } - - /** - * Gets the callback parameters - * - * @return list - * - * @since 3.0.0 - */ - public function getParameters(): array - { - return $this->getReflection()->getParameters(); - } - - /** - * Gets the callback's attributed parameter - * - * @param class-string $attribute - * - * @return ReflectionParameter|null - * - * @since 3.0.0 - */ - public function getAttributedParameter(string $attribute): ?ReflectionParameter - { - foreach ($this->getParameters() as $parameter) { - if ($parameter->getAttributes($attribute)) { - return $parameter; - } - } - - return null; - } - - /** - * Gets the callback's return type - * - * @return ReflectionType|null - * - * @since 3.0.0 - */ - public function getReturnType(): ?ReflectionType - { - return $this->getReflection()->getReturnType(); - } - - /** - * Gets the callback arguments - * - * @return list - * - * @since 3.0.0 - */ - public function getArguments(): array - { - return $this->arguments; - } - - /** - * Creates a new instance of the request handler with the given arguments and returns it - * - * Please note that the first argument will be the server request instance. - * - * @param array $arguments - * - * @return static - * - * @since 3.0.0 - */ - public function withArguments(array $arguments): self - { - ksort($arguments, SORT_NUMERIC); - - $clone = clone $this; - $clone->arguments = []; - - /** @psalm-suppress MixedAssignment */ - foreach ($arguments as $argument) { - $clone->arguments[] = $argument; - } - - return $clone; + return $this->reflection ??= reflect_callable($this->callback); } /** @@ -170,13 +96,11 @@ public function withArguments(array $arguments): self */ public function handle(ServerRequestInterface $request): ResponseInterface { - /** @var ResponseInterface $response */ - $response = ($this->callback)($request, ...$this->arguments); - - if (isset($this->responseResolver)) { - return $this->responseResolver->resolveResponse($response); - } + $arguments = $this->parameterResolver + ->withNames($request->getAttributes()) + ->withType(ServerRequestInterface::class, $request) + ->resolveParameters(...$this->getReflection()->getParameters()); - return $response; + return $this->responseResolver->resolveResponse(($this->callback)(...$arguments)); } } diff --git a/src/RequestHandler/QueueableRequestHandler.php b/src/RequestHandler/QueueableRequestHandler.php index ce55f941..00282bff 100644 --- a/src/RequestHandler/QueueableRequestHandler.php +++ b/src/RequestHandler/QueueableRequestHandler.php @@ -23,7 +23,7 @@ /** * QueueableRequestHandler */ -class QueueableRequestHandler implements RequestHandlerInterface +final class QueueableRequestHandler implements RequestHandlerInterface { /** diff --git a/src/ResponseResolver.php b/src/ResponseResolver.php new file mode 100644 index 00000000..dcbc6703 --- /dev/null +++ b/src/ResponseResolver.php @@ -0,0 +1,39 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseInterface; +use Sunrise\Http\Router\Exception\LogicException; + +/** + * ResponseResolver + * + * @since 3.0.0 + */ +final class ResponseResolver implements ResponseResolverInterface +{ + + /** + * {@inheritdoc} + */ + public function resolveResponse($response): ResponseInterface + { + if ($response instanceof ResponseInterface) { + return $response; + } + + throw new LogicException(); + } +} diff --git a/src/ResponseResolverInterface.php b/src/ResponseResolverInterface.php index 56ae81f6..af0c7f04 100644 --- a/src/ResponseResolverInterface.php +++ b/src/ResponseResolverInterface.php @@ -26,14 +26,14 @@ interface ResponseResolverInterface { /** - * Resolves the given data to the response instance + * Resolves the given raw response to the response object * - * @param mixed $data + * @param mixed $response * * @return ResponseInterface * * @throws LogicException - * If the data cannot be resolved to the response. + * If the raw response cannot be resolved to the response object. */ - public function resolveResponse($data): ResponseInterface; + public function resolveResponse($response): ResponseInterface; } diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 8dcbf0ce..d6da65c3 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -32,9 +32,9 @@ class RouteCollection implements RouteCollectionInterface /** * The collection routes * - * @var RouteInterface[] + * @var list */ - private $routes; + private $routes = []; /** * Constructor of the class @@ -43,13 +43,15 @@ class RouteCollection implements RouteCollectionInterface */ public function __construct(RouteInterface ...$routes) { + /** @var list $routes */ + $this->routes = $routes; } /** * {@inheritdoc} */ - public function all() : array + public function all(): array { return $this->routes; } @@ -57,7 +59,7 @@ public function all() : array /** * {@inheritdoc} */ - public function get(string $name) : ?RouteInterface + public function get(string $name): ?RouteInterface { foreach ($this->routes as $route) { if ($name === $route->getName()) { @@ -71,7 +73,7 @@ public function get(string $name) : ?RouteInterface /** * {@inheritdoc} */ - public function has(string $name) : bool + public function has(string $name): bool { return $this->get($name) instanceof RouteInterface; } @@ -79,7 +81,7 @@ public function has(string $name) : bool /** * {@inheritdoc} */ - public function add(RouteInterface ...$routes) : RouteCollectionInterface + public function add(RouteInterface ...$routes): RouteCollectionInterface { foreach ($routes as $route) { $this->routes[] = $route; @@ -91,7 +93,7 @@ public function add(RouteInterface ...$routes) : RouteCollectionInterface /** * {@inheritdoc} */ - public function setHost(string $host) : RouteCollectionInterface + public function setHost(string $host): RouteCollectionInterface { foreach ($this->routes as $route) { $route->setHost($host); @@ -103,7 +105,7 @@ public function setHost(string $host) : RouteCollectionInterface /** * {@inheritdoc} */ - public function addPrefix(string $prefix) : RouteCollectionInterface + public function addPrefix(string $prefix): RouteCollectionInterface { foreach ($this->routes as $route) { $route->addPrefix($prefix); @@ -115,7 +117,7 @@ public function addPrefix(string $prefix) : RouteCollectionInterface /** * {@inheritdoc} */ - public function addSuffix(string $suffix) : RouteCollectionInterface + public function addSuffix(string $suffix): RouteCollectionInterface { foreach ($this->routes as $route) { $route->addSuffix($suffix); @@ -127,7 +129,7 @@ public function addSuffix(string $suffix) : RouteCollectionInterface /** * {@inheritdoc} */ - public function addMethod(string ...$methods) : RouteCollectionInterface + public function addMethod(string ...$methods): RouteCollectionInterface { foreach ($this->routes as $route) { $route->addMethod(...$methods); @@ -139,7 +141,7 @@ public function addMethod(string ...$methods) : RouteCollectionInterface /** * {@inheritdoc} */ - public function addMiddleware(MiddlewareInterface ...$middlewares) : RouteCollectionInterface + public function addMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface { foreach ($this->routes as $route) { $route->addMiddleware(...$middlewares); @@ -151,7 +153,7 @@ public function addMiddleware(MiddlewareInterface ...$middlewares) : RouteCollec /** * {@inheritdoc} */ - public function prependMiddleware(MiddlewareInterface ...$middlewares) : RouteCollectionInterface + public function prependMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface { foreach ($this->routes as $route) { $route->setMiddlewares(...array_merge($middlewares, $route->getMiddlewares())); @@ -159,20 +161,4 @@ public function prependMiddleware(MiddlewareInterface ...$middlewares) : RouteCo return $this; } - - /** - * @deprecated 2.12.0 Use the addMiddleware method. - */ - public function appendMiddleware(MiddlewareInterface ...$middlewares) : RouteCollectionInterface - { - return $this->addMiddleware(...$middlewares); - } - - /** - * @deprecated 2.10.0 Use the prependMiddleware method. - */ - public function unshiftMiddleware(MiddlewareInterface ...$middlewares) : RouteCollectionInterface - { - return $this->prependMiddleware(...$middlewares); - } } diff --git a/src/RouteCollectionFactory.php b/src/RouteCollectionFactory.php index eec33899..40274f2f 100644 --- a/src/RouteCollectionFactory.php +++ b/src/RouteCollectionFactory.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -20,7 +20,7 @@ class RouteCollectionFactory implements RouteCollectionFactoryInterface /** * {@inheritdoc} */ - public function createCollection(RouteInterface ...$routes) : RouteCollectionInterface + public function createCollection(RouteInterface ...$routes): RouteCollectionInterface { return new RouteCollection(...$routes); } diff --git a/src/RouteCollectionFactoryInterface.php b/src/RouteCollectionFactoryInterface.php index e69350c2..768a0dc1 100644 --- a/src/RouteCollectionFactoryInterface.php +++ b/src/RouteCollectionFactoryInterface.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -24,5 +24,5 @@ interface RouteCollectionFactoryInterface * * @return RouteCollectionInterface */ - public function createCollection(RouteInterface ...$routes) : RouteCollectionInterface; + public function createCollection(RouteInterface ...$routes): RouteCollectionInterface; } diff --git a/src/RouteCollectionInterface.php b/src/RouteCollectionInterface.php index 6adc3211..ff392e70 100644 --- a/src/RouteCollectionInterface.php +++ b/src/RouteCollectionInterface.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -25,9 +25,9 @@ interface RouteCollectionInterface /** * Gets all routes from the collection * - * @return RouteInterface[] + * @return list */ - public function all() : array; + public function all(): array; /** * Gets a route by the given name @@ -38,7 +38,7 @@ public function all() : array; * * @since 2.10.0 */ - public function get(string $name) : ?RouteInterface; + public function get(string $name): ?RouteInterface; /** * Checks by the given name if a route exists in the collection @@ -49,7 +49,7 @@ public function get(string $name) : ?RouteInterface; * * @since 2.10.0 */ - public function has(string $name) : bool; + public function has(string $name): bool; /** * Adds the given route(s) to the collection @@ -58,7 +58,7 @@ public function has(string $name) : bool; * * @return RouteCollectionInterface */ - public function add(RouteInterface ...$routes) : RouteCollectionInterface; + public function add(RouteInterface ...$routes): RouteCollectionInterface; /** * Sets the given host to all routes in the collection @@ -69,7 +69,7 @@ public function add(RouteInterface ...$routes) : RouteCollectionInterface; * * @since 2.9.0 */ - public function setHost(string $host) : RouteCollectionInterface; + public function setHost(string $host): RouteCollectionInterface; /** * Adds the given path prefix to all routes in the collection @@ -80,7 +80,7 @@ public function setHost(string $host) : RouteCollectionInterface; * * @since 2.9.0 */ - public function addPrefix(string $prefix) : RouteCollectionInterface; + public function addPrefix(string $prefix): RouteCollectionInterface; /** * Adds the given path suffix to all routes in the collection @@ -91,7 +91,7 @@ public function addPrefix(string $prefix) : RouteCollectionInterface; * * @since 2.9.0 */ - public function addSuffix(string $suffix) : RouteCollectionInterface; + public function addSuffix(string $suffix): RouteCollectionInterface; /** * Adds the given method(s) to all routes in the collection @@ -102,7 +102,7 @@ public function addSuffix(string $suffix) : RouteCollectionInterface; * * @since 2.9.0 */ - public function addMethod(string ...$methods) : RouteCollectionInterface; + public function addMethod(string ...$methods): RouteCollectionInterface; /** * Adds the given middleware(s) to all routes in the collection @@ -113,7 +113,7 @@ public function addMethod(string ...$methods) : RouteCollectionInterface; * * @since 2.9.0 */ - public function addMiddleware(MiddlewareInterface ...$middlewares) : RouteCollectionInterface; + public function addMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface; /** * Adds the given middleware(s) to the beginning of all routes in the collection @@ -124,5 +124,5 @@ public function addMiddleware(MiddlewareInterface ...$middlewares) : RouteCollec * * @since 2.9.0 */ - public function prependMiddleware(MiddlewareInterface ...$middlewares) : RouteCollectionInterface; + public function prependMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface; } diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 7e8653fc..3e76ad6a 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -15,7 +15,7 @@ * Import classes */ use Psr\Container\ContainerInterface; -use Sunrise\Http\Router\Exception\UnresolvableReferenceException; +use Sunrise\Http\Router\Exception\InvalidReferenceException; /** * RouteCollector @@ -106,6 +106,32 @@ public function setContainer(?ContainerInterface $container) : void $this->referenceResolver->setContainer($container); } + /** + * Gets the collector response resolver + * + * @return ResponseResolverInterface|null + * + * @since 3.0.0 + */ + public function getResponseResolver(): ?ResponseResolverInterface + { + return $this->referenceResolver->getResponseResolver(); + } + + /** + * Sets the given response resolver to the collector + * + * @param ResponseResolverInterface|null $responseResolver + * + * @return void + * + * @since 3.0.0 + */ + public function setResponseResolver(?ResponseResolverInterface $responseResolver): void + { + $this->referenceResolver->setResponseResolver($responseResolver); + } + /** * Makes a new route from the given parameters * @@ -114,11 +140,11 @@ public function setContainer(?ContainerInterface $container) : void * @param string[] $methods * @param mixed $requestHandler * @param array $middlewares - * @param array $attributes + * @param array $attributes * * @return RouteInterface * - * @throws UnresolvableReferenceException + * @throws InvalidReferenceException * If the given request handler or one of the given middlewares cannot be resolved. */ public function route( @@ -129,16 +155,12 @@ public function route( array $middlewares = [], array $attributes = [] ) : RouteInterface { - foreach ($middlewares as &$middleware) { - $middleware = $this->referenceResolver->toMiddleware($middleware); - } - $route = $this->routeFactory->createRoute( $name, $path, $methods, $this->referenceResolver->toRequestHandler($requestHandler), - $middlewares, + $this->referenceResolver->toMiddlewares($middlewares), $attributes ); @@ -154,11 +176,11 @@ public function route( * @param string $path * @param mixed $requestHandler * @param array $middlewares - * @param array $attributes + * @param array $attributes * * @return RouteInterface * - * @throws UnresolvableReferenceException + * @throws InvalidReferenceException * If the given request handler or one of the given middlewares cannot be resolved. */ public function head( @@ -185,11 +207,11 @@ public function head( * @param string $path * @param mixed $requestHandler * @param array $middlewares - * @param array $attributes + * @param array $attributes * * @return RouteInterface * - * @throws UnresolvableReferenceException + * @throws InvalidReferenceException * If the given request handler or one of the given middlewares cannot be resolved. */ public function get( @@ -216,11 +238,11 @@ public function get( * @param string $path * @param mixed $requestHandler * @param array $middlewares - * @param array $attributes + * @param array $attributes * * @return RouteInterface * - * @throws UnresolvableReferenceException + * @throws InvalidReferenceException * If the given request handler or one of the given middlewares cannot be resolved. */ public function post( @@ -247,11 +269,11 @@ public function post( * @param string $path * @param mixed $requestHandler * @param array $middlewares - * @param array $attributes + * @param array $attributes * * @return RouteInterface * - * @throws UnresolvableReferenceException + * @throws InvalidReferenceException * If the given request handler or one of the given middlewares cannot be resolved. */ public function put( @@ -278,11 +300,11 @@ public function put( * @param string $path * @param mixed $requestHandler * @param array $middlewares - * @param array $attributes + * @param array $attributes * * @return RouteInterface * - * @throws UnresolvableReferenceException + * @throws InvalidReferenceException * If the given request handler or one of the given middlewares cannot be resolved. */ public function patch( @@ -309,11 +331,11 @@ public function patch( * @param string $path * @param mixed $requestHandler * @param array $middlewares - * @param array $attributes + * @param array $attributes * * @return RouteInterface * - * @throws UnresolvableReferenceException + * @throws InvalidReferenceException * If the given request handler or one of the given middlewares cannot be resolved. */ public function delete( @@ -340,11 +362,11 @@ public function delete( * @param string $path * @param mixed $requestHandler * @param array $middlewares - * @param array $attributes + * @param array $attributes * * @return RouteInterface * - * @throws UnresolvableReferenceException + * @throws InvalidReferenceException * If the given request handler or one of the given middlewares cannot be resolved. */ public function purge( @@ -372,15 +394,11 @@ public function purge( * * @return RouteCollectionInterface * - * @throws UnresolvableReferenceException + * @throws InvalidReferenceException * If one of the given middlewares cannot be resolved. */ public function group(callable $callback, array $middlewares = []) : RouteCollectionInterface { - foreach ($middlewares as &$middleware) { - $middleware = $this->referenceResolver->toMiddleware($middleware); - } - $collector = new self( $this->collectionFactory, $this->routeFactory, @@ -389,9 +407,13 @@ public function group(callable $callback, array $middlewares = []) : RouteCollec $callback($collector); - $collector->collection->prependMiddleware(...$middlewares); + $collector->collection->prependMiddleware( + ...$this->referenceResolver->toMiddlewares($middlewares) + ); - $this->collection->add(...$collector->collection->all()); + $this->collection->add( + ...$collector->collection->all() + ); return $collector->collection; } diff --git a/src/RouteFactory.php b/src/RouteFactory.php index 343548a4..5c5602c8 100644 --- a/src/RouteFactory.php +++ b/src/RouteFactory.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ diff --git a/src/RouteFactoryInterface.php b/src/RouteFactoryInterface.php index 2fe69bf7..86541d9b 100644 --- a/src/RouteFactoryInterface.php +++ b/src/RouteFactoryInterface.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -31,7 +31,7 @@ interface RouteFactoryInterface * @param string[] $methods * @param RequestHandlerInterface $requestHandler * @param MiddlewareInterface[] $middlewares - * @param array $attributes + * @param array $attributes * * @return RouteInterface */ diff --git a/src/RouteInterface.php b/src/RouteInterface.php index 31d739a3..935bc6f0 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -83,7 +83,7 @@ public function getMiddlewares() : array; /** * Gets the route attributes * - * @return array + * @return array */ public function getAttributes() : array; @@ -182,7 +182,7 @@ public function setMiddlewares(MiddlewareInterface ...$middlewares) : RouteInter /** * Sets the given attributes to the route * - * @param array $attributes + * @param array $attributes * * @return RouteInterface */ @@ -262,7 +262,7 @@ public function addMiddleware(MiddlewareInterface ...$middlewares) : RouteInterf * * This method MUST NOT change the state of the object. * - * @param array $attributes + * @param array $attributes * * @return RouteInterface */ diff --git a/src/Router.php b/src/Router.php index 6b973897..1b21d50a 100644 --- a/src/Router.php +++ b/src/Router.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -21,9 +21,9 @@ use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\Event\RouteEvent; use Sunrise\Http\Router\Exception\InvalidArgumentException; -use Sunrise\Http\Router\Exception\MethodNotAllowedException; use Sunrise\Http\Router\Exception\PageNotFoundException; use Sunrise\Http\Router\Exception\RouteNotFoundException; +use Sunrise\Http\Router\Exception\MethodNotAllowedException; use Sunrise\Http\Router\Loader\LoaderInterface; use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; @@ -400,7 +400,7 @@ public function getRoute(string $name) : RouteInterface * Generates a URI for the given named route * * @param string $name - * @param array $attributes + * @param array $attributes * @param bool $strict * * @return string @@ -408,11 +408,9 @@ public function getRoute(string $name) : RouteInterface * @throws RouteNotFoundException * If the given named route wasn't found. * - * @throws Exception\InvalidAttributeValueException - * It can be thrown in strict mode, if an attribute value is not valid. - * - * @throws Exception\MissingAttributeValueException - * If a required attribute value is not given. + * @throws Exception\RoutePathBuildException + * If a required attribute value is not given, + * or if an attribute value is not valid in strict mode. */ public function generateUri(string $name, array $attributes = [], bool $strict = false) : string { @@ -430,19 +428,19 @@ public function generateUri(string $name, array $attributes = [], bool $strict = * * @return RouteInterface * - * @throws MethodNotAllowedException * @throws PageNotFoundException + * @throws MethodNotAllowedException */ public function match(ServerRequestInterface $request) : RouteInterface { - $currentHost = $request->getUri()->getHost(); - $currentPath = $request->getUri()->getPath(); + $currentUri = $request->getUri(); + $currentHost = $currentUri->getHost(); + $currentPath = $currentUri->getPath(); $currentMethod = $request->getMethod(); $allowedMethods = []; + $currentHostRoutes = $this->getRoutesByHostname($currentHost); - $routes = $this->getRoutesByHostname($currentHost); - - foreach ($routes as $route) { + foreach ($currentHostRoutes as $route) { // https://github.com/sunrise-php/http-router/issues/50 // https://tools.ietf.org/html/rfc7231#section-6.5.5 if (!path_match($route->getPath(), $currentPath, $attributes)) { @@ -452,24 +450,26 @@ public function match(ServerRequestInterface $request) : RouteInterface $routeMethods = []; foreach ($route->getMethods() as $routeMethod) { $routeMethods[$routeMethod] = true; - $allowedMethods[$routeMethod] = true; + $allowedMethods[$routeMethod] = $routeMethod; } if (!isset($routeMethods[$currentMethod])) { continue; } + /** @var array $attributes */ + return $route->withAddedAttributes($attributes); } - if (!empty($allowedMethods)) { - throw new MethodNotAllowedException('Method Not Allowed', [ - 'method' => $currentMethod, - 'allowed' => array_keys($allowedMethods), - ]); + if (empty($allowedMethods)) { + throw new PageNotFoundException(); } - throw new PageNotFoundException('Page Not Found'); + throw new MethodNotAllowedException( + $currentMethod, + $allowedMethods + ); } /** @@ -491,9 +491,7 @@ public function run(ServerRequestInterface $request) : ResponseInterface if (isset($this->eventDispatcher)) { $event = new RouteEvent($route, $request); - /** - * @psalm-suppress TooManyArguments - */ + /** @psalm-suppress TooManyArguments */ $this->eventDispatcher->dispatch($event, RouteEvent::NAME); $request = $event->getRequest(); @@ -524,9 +522,7 @@ public function handle(ServerRequestInterface $request) : ResponseInterface if (isset($this->eventDispatcher)) { $event = new RouteEvent($route, $request); - /** - * @psalm-suppress TooManyArguments - */ + /** @psalm-suppress TooManyArguments */ $this->eventDispatcher->dispatch($event, RouteEvent::NAME); $request = $event->getRequest(); @@ -550,10 +546,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { try { return $this->handle($request); - } catch (MethodNotAllowedException|PageNotFoundException $e) { - $request = $request->withAttribute(self::ATTR_NAME_FOR_ROUTING_ERROR, $e); - - return $handler->handle($request); + } catch (PageNotFoundException|MethodNotAllowedException $e) { + return $handler->handle($request->withAttribute(self::ATTR_NAME_FOR_ROUTING_ERROR, $e)); } } diff --git a/src/RouterBuilder.php b/src/RouterBuilder.php index d8f017b0..fcc38a62 100644 --- a/src/RouterBuilder.php +++ b/src/RouterBuilder.php @@ -3,8 +3,8 @@ /** * It's free open-source software released under the MIT License. * - * @author Anatoly Fenric - * @copyright Copyright (c) 2018, Anatoly Fenric + * @author Anatoly Nekhay + * @copyright Copyright (c) 2018, Anatoly Nekhay * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE * @link https://github.com/sunrise-php/http-router */ @@ -39,6 +39,11 @@ final class RouterBuilder */ private $container = null; + /** + * @var ResponseResolverInterface|null + */ + private $responseResolver = null; + /** * @var CacheInterface|null */ @@ -104,6 +109,20 @@ public function setContainer(?ContainerInterface $container) : self return $this; } + /** + * Sets the given response resolver to the builder + * + * @param ResponseResolverInterface|null $responseResolver + * + * @return self + */ + public function setResponseResolver(?ResponseResolverInterface $responseResolver) : self + { + $this->responseResolver = $responseResolver; + + return $this; + } + /** * Sets the given cache to the builder * @@ -239,11 +258,13 @@ public function build() : Router if (isset($this->configLoader)) { $this->configLoader->setContainer($this->container); + $this->configLoader->setResponseResolver($this->responseResolver); $router->load($this->configLoader); } if (isset($this->descriptorLoader)) { $this->descriptorLoader->setContainer($this->container); + $this->descriptorLoader->setResponseResolver($this->responseResolver); $this->descriptorLoader->setCache($this->cache); $this->descriptorLoader->setCacheKey($this->cacheKey); $router->load($this->descriptorLoader); From 2d426e7d5ea40e376422e9fdb71e471f2c259ad4 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sat, 14 Jan 2023 05:01:00 +0100 Subject: [PATCH 028/180] v3 --- src/Event/RouteEvent.php | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/Event/RouteEvent.php b/src/Event/RouteEvent.php index cd5f6f36..9e94bdbc 100644 --- a/src/Event/RouteEvent.php +++ b/src/Event/RouteEvent.php @@ -68,14 +68,4 @@ public function getRequest(): ServerRequestInterface { return $this->request; } - - /** - * @param ServerRequestInterface $request - * - * @return void - */ - public function setRequest(ServerRequestInterface $request): void - { - $this->request = $request; - } } From 98337d5fcadad37ffa99f350f200cd2b52db210b Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 15 Jan 2023 09:43:36 +0100 Subject: [PATCH 029/180] v3 --- src/Annotation/RequestBody.php | 25 ++ src/Event/RouteEvent.php | 5 - src/Exception/ParameterResolvingException.php | 57 ++++ src/Loader/ConfigLoader.php | 54 ---- src/Loader/DescriptorLoader.php | 54 ---- src/Middleware/CallableMiddleware.php | 54 ++-- .../JsonPayloadDecodingMiddleware.php | 35 +-- src/ParameterResolutioner.php | 150 +++++++++ src/ParameterResolutionerInterface.php | 84 +++++ src/ParameterResolver.php | 287 ------------------ .../RequestBodyParameterResolver.php | 120 ++++++++ src/ParameterResolverInterface.php | 30 ++ src/ReferenceResolver.php | 41 +-- src/ReferenceResolverInterface.php | 20 -- src/RequestBodyInterface.php | 21 ++ src/RequestHandler/CallableRequestHandler.php | 54 ++-- .../QueueableRequestHandler.php | 2 +- ...eResolver.php => ResponseResolutioner.php} | 22 +- ....php => ResponseResolutionerInterface.php} | 15 +- src/RouteCollector.php | 26 -- src/Router.php | 14 +- src/RouterBuilder.php | 21 -- 22 files changed, 598 insertions(+), 593 deletions(-) create mode 100644 src/Annotation/RequestBody.php create mode 100644 src/Exception/ParameterResolvingException.php create mode 100644 src/ParameterResolutioner.php create mode 100644 src/ParameterResolutionerInterface.php delete mode 100644 src/ParameterResolver.php create mode 100644 src/ParameterResolver/RequestBodyParameterResolver.php create mode 100644 src/RequestBodyInterface.php rename src/{ResponseResolver.php => ResponseResolutioner.php} (64%) rename src/{ResponseResolverInterface.php => ResponseResolutionerInterface.php} (68%) diff --git a/src/Annotation/RequestBody.php b/src/Annotation/RequestBody.php new file mode 100644 index 00000000..56ae949a --- /dev/null +++ b/src/Annotation/RequestBody.php @@ -0,0 +1,25 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Annotation; + +/** + * Import classes + */ +use Attribute; + +/** + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_PARAMETER)] +final class RequestBody +{ +} diff --git a/src/Event/RouteEvent.php b/src/Event/RouteEvent.php index 9e94bdbc..af880bfb 100644 --- a/src/Event/RouteEvent.php +++ b/src/Event/RouteEvent.php @@ -26,11 +26,6 @@ final class RouteEvent extends Event { - /** - * @var string - */ - public const NAME = 'router.route'; - /** * @var RouteInterface */ diff --git a/src/Exception/ParameterResolvingException.php b/src/Exception/ParameterResolvingException.php new file mode 100644 index 00000000..81773b51 --- /dev/null +++ b/src/Exception/ParameterResolvingException.php @@ -0,0 +1,57 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use ReflectionParameter; + +/** + * Import functions + */ +use function sprintf; + +/** + * ParameterResolvingException + * + * @since 3.0.0 + */ +class ParameterResolvingException extends Exception +{ + + /** + * @return self + */ + final public static function unsupportedParameterTypeDeclaration(ReflectionParameter $parameter): self + { + return new self(sprintf( + '%s($%s[%d]): Unsupported parameter type declaration', + $parameter->getDeclaringFunction()->getName(), + $parameter->getName(), + $parameter->getPosition() + )); + } + + /** + * @return self + */ + final public static function unknownParameter(ReflectionParameter $parameter): self + { + return new self(sprintf( + '%s($%s[%d]): Unknown parameter', + $parameter->getDeclaringFunction()->getName(), + $parameter->getName(), + $parameter->getPosition() + )); + } +} diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 9d0bca76..0a70af38 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -16,10 +16,8 @@ */ use Psr\Container\ContainerInterface; use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; -use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; -use Sunrise\Http\Router\ResponseResolverInterface; use Sunrise\Http\Router\RouteCollectionFactory; use Sunrise\Http\Router\RouteCollectionFactoryInterface; use Sunrise\Http\Router\RouteCollectionInterface; @@ -104,58 +102,6 @@ public function setContainer(?ContainerInterface $container): void $this->referenceResolver->setContainer($container); } - /** - * Gets the loader parameter resolver - * - * @return ParameterResolverInterface|null - * - * @since 3.0.0 - */ - public function getParameterResolver(): ?ParameterResolverInterface - { - return $this->referenceResolver->getParameterResolver(); - } - - /** - * Sets the given parameter resolver to the loader - * - * @param ParameterResolverInterface|null $parameterResolver - * - * @return void - * - * @since 3.0.0 - */ - public function setParameterResolver(?ParameterResolverInterface $parameterResolver): void - { - $this->referenceResolver->setParameterResolver($parameterResolver); - } - - /** - * Gets the loader response resolver - * - * @return ResponseResolverInterface|null - * - * @since 3.0.0 - */ - public function getResponseResolver(): ?ResponseResolverInterface - { - return $this->referenceResolver->getResponseResolver(); - } - - /** - * Sets the given response resolver to the loader - * - * @param ResponseResolverInterface|null $responseResolver - * - * @return void - * - * @since 3.0.0 - */ - public function setResponseResolver(?ResponseResolverInterface $responseResolver): void - { - $this->referenceResolver->setResponseResolver($responseResolver); - } - /** * {@inheritdoc} */ diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index f8517755..557079d2 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -27,10 +27,8 @@ use Sunrise\Http\Router\Annotation\Route; use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; -use Sunrise\Http\Router\ResponseResolverInterface; use Sunrise\Http\Router\RouteCollectionFactory; use Sunrise\Http\Router\RouteCollectionFactoryInterface; use Sunrise\Http\Router\RouteCollectionInterface; @@ -145,58 +143,6 @@ public function setContainer(?ContainerInterface $container): void $this->referenceResolver->setContainer($container); } - /** - * Gets the loader parameter resolver - * - * @return ParameterResolverInterface|null - * - * @since 3.0.0 - */ - public function getParameterResolver(): ?ParameterResolverInterface - { - return $this->referenceResolver->getParameterResolver(); - } - - /** - * Sets the given parameter resolver to the loader - * - * @param ParameterResolverInterface|null $parameterResolver - * - * @return void - * - * @since 3.0.0 - */ - public function setParameterResolver(?ParameterResolverInterface $parameterResolver): void - { - $this->referenceResolver->setParameterResolver($parameterResolver); - } - - /** - * Gets the loader response resolver - * - * @return ResponseResolverInterface|null - * - * @since 3.0.0 - */ - public function getResponseResolver(): ?ResponseResolverInterface - { - return $this->referenceResolver->getResponseResolver(); - } - - /** - * Sets the given response resolver to the loader - * - * @param ResponseResolverInterface|null $responseResolver - * - * @return void - * - * @since 3.0.0 - */ - public function setResponseResolver(?ResponseResolverInterface $responseResolver): void - { - $this->referenceResolver->setResponseResolver($responseResolver); - } - /** * Gets the loader annotation reader * diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index 49cd99ff..e100ffa9 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -14,15 +14,15 @@ /** * Import classes */ +use ReflectionFunctionAbstract; +use ReflectionFunction; +use ReflectionMethod; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\ParameterResolverInterface; -use Sunrise\Http\Router\ResponseResolverInterface; -use ReflectionFunctionAbstract; -use ReflectionFunction; -use ReflectionMethod; +use Sunrise\Http\Router\ParameterResolutionerInterface; +use Sunrise\Http\Router\ResponseResolutionerInterface; /** * Import functions @@ -45,41 +45,34 @@ final class CallableMiddleware implements MiddlewareInterface private $callback; /** - * The callback's parameter resolver + * The callback's parameter resolutioner * - * @var ParameterResolverInterface + * @var ParameterResolutionerInterface */ - private ParameterResolverInterface $parameterResolver; + private ParameterResolutionerInterface $parameterResolutioner; /** - * The callback's response resolver + * The callback's response resolutioner * - * @var ResponseResolverInterface + * @var ResponseResolutionerInterface */ - private ResponseResolverInterface $responseResolver; - - /** - * The callback's reflection - * - * @var ReflectionFunction|ReflectionMethod - */ - private ?ReflectionFunctionAbstract $reflection = null; + private ResponseResolutionerInterface $responseResolutioner; /** * Constructor of the class * * @param callable $callback - * @param ParameterResolverInterface $parameterResolver - * @param ResponseResolverInterface $responseResolver + * @param ParameterResolutionerInterface $parameterResolutioner + * @param ResponseResolutionerInterface $responseResolutioner */ public function __construct( callable $callback, - ParameterResolverInterface $parameterResolver, - ResponseResolverInterface $responseResolver + ParameterResolutionerInterface $parameterResolutioner, + ResponseResolutionerInterface $responseResolutioner ) { $this->callback = $callback; - $this->parameterResolver = $parameterResolver; - $this->responseResolver = $responseResolver; + $this->parameterResolutioner = $parameterResolutioner; + $this->responseResolutioner = $responseResolutioner; } /** @@ -91,7 +84,7 @@ public function __construct( */ public function getReflection(): ReflectionFunctionAbstract { - return $this->reflection ??= reflect_callable($this->callback); + return reflect_callable($this->callback); } /** @@ -99,12 +92,17 @@ public function getReflection(): ReflectionFunctionAbstract */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $arguments = $this->parameterResolver - ->withNames($request->getAttributes()) + $arguments = $this->parameterResolutioner + ->withContext($request) ->withType(ServerRequestInterface::class, $request) ->withType(RequestHandlerInterface::class, $handler) ->resolveParameters(...$this->getReflection()->getParameters()); - return $this->responseResolver->resolveResponse(($this->callback)(...$arguments)); + /** @var mixed */ + $response = ($this->callback)(...$arguments); + + return $this->responseResolutioner + ->withContext($request) + ->resolveResponse($response); } } diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 2c1431f2..46ea269a 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -28,6 +28,7 @@ use function is_object; use function json_decode; use function rtrim; +use function sprintf; use function strpos; use function substr; @@ -43,31 +44,22 @@ * * @since 2.15.0 */ -class JsonPayloadDecodingMiddleware implements MiddlewareInterface +final class JsonPayloadDecodingMiddleware implements MiddlewareInterface { /** * JSON media type * - * @var string - * * @link https://datatracker.ietf.org/doc/html/rfc4627 - */ - private const JSON_MEDIA_TYPE = 'application/json'; - - /** - * JSON decoding options * - * @var int - * - * @link https://www.php.net/json.constants + * @var string */ - protected const JSON_DECODING_OPTIONS = JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY; + private const JSON_MEDIA_TYPE = 'application/json'; /** * {@inheritdoc} */ - final public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if ($this->isSupportedRequest($request)) { $data = $this->decodeRequestJsonPayload($request); @@ -86,7 +78,7 @@ final public function process(ServerRequestInterface $request, RequestHandlerInt */ private function isSupportedRequest(ServerRequestInterface $request): bool { - return $this->getRequestMediaType($request) === self::JSON_MEDIA_TYPE; + return self::JSON_MEDIA_TYPE === $this->getRequestMediaType($request); } /** @@ -94,11 +86,11 @@ private function isSupportedRequest(ServerRequestInterface $request): bool * * Returns null if a media type cannot be retrieved. * + * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1 + * * @param ServerRequestInterface $request * * @return string|null - * - * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1 */ private function getRequestMediaType(ServerRequestInterface $request): ?string { @@ -129,8 +121,8 @@ private function getRequestMediaType(ServerRequestInterface $request): ?string */ private function decodeRequestJsonPayload(ServerRequestInterface $request) { - /** @var int */ - $flags = static::JSON_DECODING_OPTIONS; + // https://www.php.net/json.constants + $flags = JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY; try { /** @var mixed */ @@ -139,6 +131,11 @@ private function decodeRequestJsonPayload(ServerRequestInterface $request) throw new InvalidPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); } - return (is_array($result) || is_object($result)) ? $result : null; + if (is_array($result) || + is_object($result)) { + return $result; + } + + return null; } } diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php new file mode 100644 index 00000000..2cee788d --- /dev/null +++ b/src/ParameterResolutioner.php @@ -0,0 +1,150 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use ReflectionNamedType; +use ReflectionParameter; +use Psr\Container\ContainerInterface; +use Sunrise\Http\Router\Exception\ParameterResolvingException; + +/** + * ParameterResolutioner + * + * @since 3.0.0 + */ +final class ParameterResolutioner implements ParameterResolutionerInterface +{ + + /** + * The current context + * + * @var mixed + */ + private $context = null; + + /** + * Known types + * + * @var array + */ + private array $types = []; + + /** + * The resolutioner's resolvers + * + * @var list + */ + private array $resolvers = []; + + /** + * The resolutioner's container + * + * @var ContainerInterface|null + */ + private ?ContainerInterface $container = null; + + /** + * {@inheritdoc} + */ + public function withContext($context): ParameterResolutionerInterface + { + $clone = clone $this; + $clone->context = $context; + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function withType(string $type, object $value): ParameterResolutionerInterface + { + $clone = clone $this; + $clone->types[$type] = $value; + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function addResolver(ParameterResolverInterface ...$resolvers): void + { + foreach ($resolvers as $resolver) { + $this->resolvers[] = $resolver; + } + } + + /** + * {@inheritdoc} + */ + public function setContainer(?ContainerInterface $container): void + { + $this->container = $container; + } + + /** + * {@inheritdoc} + */ + public function resolveParameters(ReflectionParameter ...$parameters): array + { + $arguments = []; + foreach ($parameters as $parameter) { + /** @var mixed */ + $arguments[] = $this->resolveParameter($parameter); + } + + return $arguments; + } + + /** + * Tries to resolve the given parameter to an argument + * + * @param ReflectionParameter $parameter + * + * @return mixed + * The ready-to-pass argument. + * + * @throws ParameterResolvingException + * If the parameter cannot be resolved to an argument. + */ + private function resolveParameter(ReflectionParameter $parameter) + { + $type = $parameter->getType(); + if (!($type instanceof ReflectionNamedType)) { + throw ParameterResolvingException::unsupportedParameterTypeDeclaration($parameter); + } + + if (isset($this->types[$type->getName()])) { + return $this->types[$type->getName()]; + } + + foreach ($this->resolvers as $resolver) { + if ($resolver->supportsParameter($parameter, $this->context)) { + return $resolver->resolveParameter($parameter, $this->context); + } + } + + if (isset($this->container) && $this->container->has($type->getName())) { + return $this->container->get($type->getName()); + } + + if ($parameter->isDefaultValueAvailable()) { + return $parameter->getDefaultValue(); + } + + throw ParameterResolvingException::unknownParameter($parameter); + } +} diff --git a/src/ParameterResolutionerInterface.php b/src/ParameterResolutionerInterface.php new file mode 100644 index 00000000..c4f1d3e2 --- /dev/null +++ b/src/ParameterResolutionerInterface.php @@ -0,0 +1,84 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use ReflectionParameter; +use Psr\Container\ContainerInterface; +use Sunrise\Http\Router\Exception\ParameterResolvingException; + +/** + * ParameterResolutionerInterface + * + * @since 3.0.0 + */ +interface ParameterResolutionerInterface +{ + + /** + * Creates a new instance of the resolutioner with the given current context + * + * Please note that this method MUST NOT change the object state. + * + * @param mixed $context + * + * @return static + */ + public function withContext($context): ParameterResolutionerInterface; + + /** + * Creates a new instance of the resolutioner with the given data for resolve a non-built-in typed parameter + * + * Please note that this method MUST NOT change the object state. + * + * @param class-string $type + * @param T $value + * + * @return static + * + * @template T + */ + public function withType(string $type, object $value): ParameterResolutionerInterface; + + /** + * Adds the given parameter resolver(s) to the resolutioner + * + * @param ParameterResolverInterface ...$resolvers + * + * @return void + */ + public function addResolver(ParameterResolverInterface ...$resolvers): void; + + /** + * Sets the given container to the resolutioner for resolve non-built-in typed parameters + * + * @param ContainerInterface|null $container + * + * @return void + */ + public function setContainer(?ContainerInterface $container): void; + + /** + * Resolves the given parameter(s) to arguments + * + * @param ReflectionParameter ...$parameters + * + * @return list + * List of ready-to-pass arguments. + * + * @throws ParameterResolvingException + * If one of the parameters cannot be resolved to an argument. + */ + public function resolveParameters(ReflectionParameter ...$parameters): array; +} diff --git a/src/ParameterResolver.php b/src/ParameterResolver.php deleted file mode 100644 index 6f5c1771..00000000 --- a/src/ParameterResolver.php +++ /dev/null @@ -1,287 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router; - -/** - * Import classes - */ -use Psr\Container\ContainerInterface; -use ReflectionNamedType; -use ReflectionParameter; - -/** - * ParameterResolver - * - * @since 3.0.0 - */ -final class ParameterResolver implements ParameterResolverInterface -{ - - /** - * Known parameter names - * - * @var array - */ - private array $names = []; - - /** - * Known types - * - * @var array - */ - private array $types = []; - - /** - * The resolver's container - * - * @var ContainerInterface|null - */ - private ?ContainerInterface $container = null; - - /** - * {@inheritdoc} - */ - public function getNames(): array - { - return $this->names; - } - - /** - * {@inheritdoc} - */ - public function getTypes(): array - { - return $this->types; - } - - /** - * {@inheritdoc} - */ - public function getContainer(): ?ContainerInterface - { - return $this->container; - } - - /** - * {@inheritdoc} - */ - public function setContainer(?ContainerInterface $container): void - { - $this->container = $container; - } - - /** - * {@inheritdoc} - */ - public function withName(string $name, $value): ParameterResolverInterface - { - $clone = clone $this; - $clone->setName($name, $value); - - return $clone; - } - - /** - * {@inheritdoc} - */ - public function withNames(array $names): ParameterResolverInterface - { - $clone = clone $this; - foreach ($names as $name => $value) { - $clone->setName($name, $value); - } - - return $clone; - } - - /** - * {@inheritdoc} - */ - public function withType(string $type, $value): ParameterResolverInterface - { - $clone = clone $this; - $clone->setType($type, $value); - - return $clone; - } - - /** - * {@inheritdoc} - */ - public function withTypes(array $types): ParameterResolverInterface - { - $clone = clone $this; - foreach ($types as $type => $value) { - $clone->setType($type, $value); - } - - return $clone; - } - - /** - * {@inheritdoc} - */ - public function resolveParameters(ReflectionParameter ...$parameters): array - { - $arguments = []; - foreach ($parameters as $parameter) { - $arguments[] = $this->resolveParameter($parameter); - } - - return $arguments; - } - - /** - * Sets a new known name - * - * @param string $name - * @param mixed $value - * - * @return void - */ - private function setName(string $name, $value): void - { - $this->names[$name] = $value; - } - - /** - * Sets a new known type - * - * @param string $type - * @param mixed $value - * - * @return void - * - * @throws LogicException - * If the value isn't an instance of the type. - */ - private function setType(string $type, $value): void - { - if (!($value instanceof $type)) { - throw new LogicException(); - } - - $this->types[$type] = $value; - } - - /** - * Resolves the given parameter - * - * @param ReflectionParameter $parameter - * - * @return mixed - * - * @throws LogicException - * If the parameter cannot be resolved. - */ - private function resolveParameter(ReflectionParameter $parameter) - { - if (!$parameter->hasType()) { - return $this->resolveUntypedParameter($parameter); - } - - $type = $parameter->getType(); - if (!($type instanceof ReflectionNamedType)) { - throw new LogicException(); - } - - return !$type->isBuiltin() ? - $this->resolveTypedParameterWithNonBuiltinType($type, $parameter) : - $this->resolveTypedParameterWithBuiltinType($type, $parameter); - } - - /** - * Resolves the given untyped parameter - * - * @param ReflectionParameter $parameter - * - * @return mixed - * - * @throws LogicException - * If the parameter cannot be resolved. - */ - private function resolveUntypedParameter(ReflectionParameter $parameter) - { - if (array_key_exists($parameter->getName(), $this->names)) { - return $this->names[$parameter->getName()]; - } - - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); - } - - throw new LogicException(); - } - - /** - * Resolves the given typed parameter with non-built-in type - * - * @param ReflectionNamedType $type - * @param ReflectionParameter $parameter - * - * @return mixed - * - * @throws LogicException - * If the parameter cannot be resolved. - */ - private function resolveTypedParameterWithNonBuiltinType(ReflectionNamedType $type, ReflectionParameter $parameter) - { - if (isset($this->types[$parameter->getName()])) { - return $this->types[$parameter->getName()]; - } - - if (isset($this->container) && $this->container->has($type->getName())) { - return $this->container->get($type->getName()); - } - - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); - } - - throw new LogicException(); - } - - /** - * Resolves the given typed parameter with built-in type - * - * @param ReflectionNamedType $type - * @param ReflectionParameter $parameter - * - * @return mixed - * - * @throws LogicException - * If the parameter cannot be resolved. - */ - private function resolveTypedParameterWithBuiltinType(ReflectionNamedType $type, ReflectionParameter $parameter) - { - if (!array_key_exists($parameter->getName(), $this->names)) { - if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); - } - - throw new LogicException(); - } - - switch ($type->getName()) { - case 'bool': - break; - case 'int': - break; - case 'float': - break; - case 'string': - break; - } - - throw new LogicException(); - } -} diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php new file mode 100644 index 00000000..14745c93 --- /dev/null +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -0,0 +1,120 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ParameterResolver; + +/** + * Import classes + */ +use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\Annotation\RequestBody; +use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; +use Sunrise\Http\Router\Exception\ParameterResolvingException; +use Sunrise\Http\Router\ParameterResolverInterface; +use Sunrise\Http\Router\RequestBodyInterface; +use Sunrise\Hydrator\Exception\InvalidObjectException; +use Sunrise\Hydrator\Exception\InvalidValueException; +use Sunrise\Hydrator\HydratorInterface; +use Sunrise\Hydrator\Hydrator; +use ReflectionNamedType; +use ReflectionParameter; + +/** + * Import constants + */ +use const PHP_MAJOR_VERSION; + +/** + * RequestBodyParameterResolver + * + * @link https://github.com/sunrise-php/hydrator + * + * @since 3.0.0 + */ +final class RequestBodyParameterResolver implements ParameterResolverInterface +{ + + /** + * @var HydratorInterface|null + */ + private ?HydratorInterface $hydrator = null; + + /** + * @param HydratorInterface|null $hydrator + */ + public function __construct(?HydratorInterface $hydrator = null) + { + $this->hydrator = $hydrator; + } + + /** + * @return HydratorInterface + */ + private function getHydrator(): HydratorInterface + { + if (isset($this->hydrator)) { + return $this->hydrator; + } + + $this->hydrator = new Hydrator(); + + // auto-enable annotation support for php 7 + if (7 === PHP_MAJOR_VERSION) { + $this->hydrator->useAnnotations(); + } + + return $this->hydrator; + } + + /** + * {@inheritdoc} + */ + public function supportsParameter(ReflectionParameter $parameter, $context): bool + { + if (!($context instanceof ServerRequestInterface)) { + return false; + } + + if (!($parameter->getType() instanceof ReflectionNamedType)) { + return false; + } + + if ($parameter->getType()->getName() instanceof RequestBodyInterface) { + return true; + } + + if (8 === PHP_MAJOR_VERSION && $parameter->getAttributes(RequestBody::class)) { + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function resolveParameter(ReflectionParameter $parameter, $context) + { + /** @var ServerRequestInterface */ + $context = $context; + + /** @var ReflectionNamedType */ + $parameterType = $parameter->getType(); + + try { + return $this->getHydrator()->hydrate($parameterType->getName(), (array) $context->getParsedBody()); + } catch (InvalidObjectException $e) { + throw new ParameterResolvingException($e->getMessage(), 0, $e); + } catch (InvalidValueException $e) { + throw new HttpUnprocessableEntityException($e->getMessage(), 0, $e); + } + } +} diff --git a/src/ParameterResolverInterface.php b/src/ParameterResolverInterface.php index 281faed7..0a6de0e8 100644 --- a/src/ParameterResolverInterface.php +++ b/src/ParameterResolverInterface.php @@ -11,6 +11,12 @@ namespace Sunrise\Http\Router; +/** + * Import classes + */ +use ReflectionParameter; +use Sunrise\Http\Router\Exception\ParameterResolvingException; + /** * ParameterResolverInterface * @@ -18,4 +24,28 @@ */ interface ParameterResolverInterface { + + /** + * Checks if the given parameter is supported + * + * @param ReflectionParameter $parameter + * @param mixed $context + * + * @return bool + */ + public function supportsParameter(ReflectionParameter $parameter, $context): bool; + + /** + * Resolves the given parameter to an argument + * + * @param ReflectionParameter $parameter + * @param mixed $context + * + * @return mixed + * The ready-to-pass argument. + * + * @throws ParameterResolvingException + * If the parameter cannot be resolved to an argument. + */ + public function resolveParameter(ReflectionParameter $parameter, $context); } diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index 9e25e165..4a06deb8 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -47,20 +47,6 @@ class ReferenceResolver implements ReferenceResolverInterface */ private ?ContainerInterface $container = null; - /** - * The reference resolver's parameter resolver - * - * @var ParameterResolverInterface|null - */ - private ?ParameterResolverInterface $parameterResolver = null; - - /** - * The reference resolver's response resolver - * - * @var ResponseResolverInterface|null - */ - private ?ResponseResolverInterface $responseResolver = null; - /** * {@inheritdoc} */ @@ -77,22 +63,6 @@ public function setContainer(?ContainerInterface $container): void $this->container = $container; } - /** - * {@inheritdoc} - */ - public function getResponseResolver(): ?ResponseResolverInterface - { - return $this->responseResolver; - } - - /** - * {@inheritdoc} - */ - public function setResponseResolver(?ResponseResolverInterface $responseResolver): void - { - $this->responseResolver = $responseResolver; - } - /** * {@inheritdoc} */ @@ -103,7 +73,7 @@ public function toRequestHandler($reference): RequestHandlerInterface } if ($reference instanceof Closure) { - return new CallableRequestHandler($reference, $this->responseResolver); + // return new CallableRequestHandler($reference, $this->parameterResolver, $this->responseResolver); } list($class, $method) = $this->normalizeReference($reference); @@ -112,7 +82,7 @@ public function toRequestHandler($reference): RequestHandlerInterface /** @var callable */ $callback = [$this->resolveClass($class), $method]; - return new CallableRequestHandler($callback, $this->responseResolver); + // return new CallableRequestHandler($callback, $this->parameterResolver, $this->responseResolver); } if (!isset($method) && isset($class) && is_subclass_of($class, RequestHandlerInterface::class)) { @@ -135,13 +105,16 @@ public function toMiddleware($reference): MiddlewareInterface } if ($reference instanceof Closure) { - return new CallableMiddleware($reference); + // return new CallableMiddleware($reference, $this->parameterResolver, $this->responseResolver); } list($class, $method) = $this->normalizeReference($reference); if (isset($class) && isset($method) && method_exists($class, $method)) { - return new CallableMiddleware([$this->resolveClass($class), $method]); + /** @var callable */ + $callback = [$this->resolveClass($class), $method]; + + // return new CallableMiddleware($callback, $this->parameterResolver, $this->responseResolver); } if (!isset($method) && isset($class) && is_subclass_of($class, MiddlewareInterface::class)) { diff --git a/src/ReferenceResolverInterface.php b/src/ReferenceResolverInterface.php index a939fa1c..67b6208d 100644 --- a/src/ReferenceResolverInterface.php +++ b/src/ReferenceResolverInterface.php @@ -43,26 +43,6 @@ public function getContainer(): ?ContainerInterface; */ public function setContainer(?ContainerInterface $container): void; - /** - * Gets the reference resolver's response resolver - * - * @return ResponseResolverInterface|null - * - * @since 3.0.0 - */ - public function getResponseResolver(): ?ResponseResolverInterface; - - /** - * Sets the given response resolver to the reference resolver - * - * @param ResponseResolverInterface|null $responseResolver - * - * @return void - * - * @since 3.0.0 - */ - public function setResponseResolver(?ResponseResolverInterface $responseResolver): void; - /** * Resolves the given reference to a request handler * diff --git a/src/RequestBodyInterface.php b/src/RequestBodyInterface.php new file mode 100644 index 00000000..b6610e37 --- /dev/null +++ b/src/RequestBodyInterface.php @@ -0,0 +1,21 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * RequestBodyInterface + * + * @since 3.0.0 + */ +interface RequestBodyInterface +{ +} diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index c2bd086a..aaf00728 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -14,14 +14,14 @@ /** * Import classes */ -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\ParameterResolverInterface; -use Sunrise\Http\Router\ResponseResolverInterface; use ReflectionFunctionAbstract; use ReflectionFunction; use ReflectionMethod; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\ParameterResolutionerInterface; +use Sunrise\Http\Router\ResponseResolutionerInterface; /** * Import functions @@ -42,41 +42,34 @@ final class CallableRequestHandler implements RequestHandlerInterface private $callback; /** - * The callback's parameter resolver + * The callback's parameter resolutioner * - * @var ParameterResolverInterface + * @var ParameterResolutionerInterface */ - private ParameterResolverInterface $parameterResolver; + private ParameterResolutionerInterface $parameterResolutioner; /** - * The callback's response resolver + * The callback's response resolutioner * - * @var ResponseResolverInterface + * @var ResponseResolutionerInterface */ - private ResponseResolverInterface $responseResolver; - - /** - * The callback's reflection - * - * @var ReflectionFunction|ReflectionMethod - */ - private ?ReflectionFunctionAbstract $reflection = null; + private ResponseResolutionerInterface $responseResolutioner; /** * Constructor of the class * * @param callable $callback - * @param ParameterResolverInterface $parameterResolver - * @param ResponseResolverInterface $responseResolver + * @param ParameterResolutionerInterface $parameterResolutioner + * @param ResponseResolutionerInterface $responseResolutioner */ public function __construct( callable $callback, - ParameterResolverInterface $parameterResolver, - ResponseResolverInterface $responseResolver + ParameterResolutionerInterface $parameterResolutioner, + ResponseResolutionerInterface $responseResolutioner ) { $this->callback = $callback; - $this->parameterResolver = $parameterResolver; - $this->responseResolver = $responseResolver; + $this->parameterResolutioner = $parameterResolutioner; + $this->responseResolutioner = $responseResolutioner; } /** @@ -88,7 +81,7 @@ public function __construct( */ public function getReflection(): ReflectionFunctionAbstract { - return $this->reflection ??= reflect_callable($this->callback); + return reflect_callable($this->callback); } /** @@ -96,11 +89,16 @@ public function getReflection(): ReflectionFunctionAbstract */ public function handle(ServerRequestInterface $request): ResponseInterface { - $arguments = $this->parameterResolver - ->withNames($request->getAttributes()) + $arguments = $this->parameterResolutioner + ->withContext($request) ->withType(ServerRequestInterface::class, $request) ->resolveParameters(...$this->getReflection()->getParameters()); - return $this->responseResolver->resolveResponse(($this->callback)(...$arguments)); + /** @var mixed */ + $response = ($this->callback)(...$arguments); + + return $this->responseResolutioner + ->withContext($request) + ->resolveResponse($response); } } diff --git a/src/RequestHandler/QueueableRequestHandler.php b/src/RequestHandler/QueueableRequestHandler.php index 00282bff..62f62a4b 100644 --- a/src/RequestHandler/QueueableRequestHandler.php +++ b/src/RequestHandler/QueueableRequestHandler.php @@ -14,11 +14,11 @@ /** * Import classes */ +use SplQueue; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use SplQueue; /** * QueueableRequestHandler diff --git a/src/ResponseResolver.php b/src/ResponseResolutioner.php similarity index 64% rename from src/ResponseResolver.php rename to src/ResponseResolutioner.php index dcbc6703..0b16fe5d 100644 --- a/src/ResponseResolver.php +++ b/src/ResponseResolutioner.php @@ -18,13 +18,31 @@ use Sunrise\Http\Router\Exception\LogicException; /** - * ResponseResolver + * ResponseResolutioner * * @since 3.0.0 */ -final class ResponseResolver implements ResponseResolverInterface +final class ResponseResolutioner implements ResponseResolutionerInterface { + /** + * The current context + * + * @var mixed + */ + private $context = null; + + /** + * {@inheritdoc} + */ + public function withContext($context): ResponseResolutionerInterface + { + $clone = clone $this; + $clone->context = $context; + + return $clone; + } + /** * {@inheritdoc} */ diff --git a/src/ResponseResolverInterface.php b/src/ResponseResolutionerInterface.php similarity index 68% rename from src/ResponseResolverInterface.php rename to src/ResponseResolutionerInterface.php index af0c7f04..cbbb3bb5 100644 --- a/src/ResponseResolverInterface.php +++ b/src/ResponseResolutionerInterface.php @@ -18,13 +18,24 @@ use Sunrise\Http\Router\Exception\LogicException; /** - * ResponseResolverInterface + * ResponseResolutionerInterface * * @since 3.0.0 */ -interface ResponseResolverInterface +interface ResponseResolutionerInterface { + /** + * Creates a new instance of the resolutioner with the given current context + * + * Please note that this method MUST NOT change the object state. + * + * @param mixed $context + * + * @return static + */ + public function withContext($context): ResponseResolutionerInterface; + /** * Resolves the given raw response to the response object * diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 3e76ad6a..b0f92900 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -106,32 +106,6 @@ public function setContainer(?ContainerInterface $container) : void $this->referenceResolver->setContainer($container); } - /** - * Gets the collector response resolver - * - * @return ResponseResolverInterface|null - * - * @since 3.0.0 - */ - public function getResponseResolver(): ?ResponseResolverInterface - { - return $this->referenceResolver->getResponseResolver(); - } - - /** - * Sets the given response resolver to the collector - * - * @param ResponseResolverInterface|null $responseResolver - * - * @return void - * - * @since 3.0.0 - */ - public function setResponseResolver(?ResponseResolverInterface $responseResolver): void - { - $this->referenceResolver->setResponseResolver($responseResolver); - } - /** * Makes a new route from the given parameters * diff --git a/src/Router.php b/src/Router.php index 1b21d50a..d3365bcc 100644 --- a/src/Router.php +++ b/src/Router.php @@ -489,12 +489,7 @@ public function run(ServerRequestInterface $request) : ResponseInterface $this->matchedRoute = $route; if (isset($this->eventDispatcher)) { - $event = new RouteEvent($route, $request); - - /** @psalm-suppress TooManyArguments */ - $this->eventDispatcher->dispatch($event, RouteEvent::NAME); - - $request = $event->getRequest(); + $this->eventDispatcher->dispatch(new RouteEvent($route, $request)); } return $route->handle($request); @@ -520,12 +515,7 @@ public function handle(ServerRequestInterface $request) : ResponseInterface $this->matchedRoute = $route; if (isset($this->eventDispatcher)) { - $event = new RouteEvent($route, $request); - - /** @psalm-suppress TooManyArguments */ - $this->eventDispatcher->dispatch($event, RouteEvent::NAME); - - $request = $event->getRequest(); + $this->eventDispatcher->dispatch(new RouteEvent($route, $request)); } $middlewares = $this->getMiddlewares(); diff --git a/src/RouterBuilder.php b/src/RouterBuilder.php index fcc38a62..a0dbbeb9 100644 --- a/src/RouterBuilder.php +++ b/src/RouterBuilder.php @@ -39,11 +39,6 @@ final class RouterBuilder */ private $container = null; - /** - * @var ResponseResolverInterface|null - */ - private $responseResolver = null; - /** * @var CacheInterface|null */ @@ -109,20 +104,6 @@ public function setContainer(?ContainerInterface $container) : self return $this; } - /** - * Sets the given response resolver to the builder - * - * @param ResponseResolverInterface|null $responseResolver - * - * @return self - */ - public function setResponseResolver(?ResponseResolverInterface $responseResolver) : self - { - $this->responseResolver = $responseResolver; - - return $this; - } - /** * Sets the given cache to the builder * @@ -258,13 +239,11 @@ public function build() : Router if (isset($this->configLoader)) { $this->configLoader->setContainer($this->container); - $this->configLoader->setResponseResolver($this->responseResolver); $router->load($this->configLoader); } if (isset($this->descriptorLoader)) { $this->descriptorLoader->setContainer($this->container); - $this->descriptorLoader->setResponseResolver($this->responseResolver); $this->descriptorLoader->setCache($this->cache); $this->descriptorLoader->setCacheKey($this->cacheKey); $router->load($this->descriptorLoader); From 81a7a683b2f0c6f1ecdf90d13fc2f13a1bc57865 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 15 Jan 2023 21:14:26 +0100 Subject: [PATCH 030/180] v3 --- composer.json | 7 +- functions/get_debug_type.php | 47 ++++++++++ src/Annotation/Middleware.php | 5 +- src/Annotation/Route.php | 25 +++--- src/Exception/ParameterResolvingException.php | 38 +-------- src/Exception/ReferenceResolvingException.php | 21 +++++ src/Exception/ResolvingException.php | 21 +++++ src/Exception/ResponseResolvingException.php | 21 +++++ src/Loader/ConfigLoader.php | 12 --- src/Loader/DescriptorLoader.php | 14 +-- src/ParameterResolutioner.php | 27 ++++-- .../RequestBodyParameterResolver.php | 34 ++------ src/ReferenceResolver.php | 85 ++++++++++++++----- src/ReferenceResolverInterface.php | 29 +++---- src/ResponseResolutioner.php | 36 +++++++- src/ResponseResolutionerInterface.php | 17 +++- .../StatusCodeResponseResolver.php | 64 ++++++++++++++ src/ResponseResolverInterface.php | 50 +++++++++++ src/RouteCollector.php | 36 +++----- src/Router.php | 20 +++-- 20 files changed, 419 insertions(+), 190 deletions(-) create mode 100644 functions/get_debug_type.php create mode 100644 src/Exception/ReferenceResolvingException.php create mode 100644 src/Exception/ResolvingException.php create mode 100644 src/Exception/ResponseResolvingException.php create mode 100644 src/ResponseResolver/StatusCodeResponseResolver.php create mode 100644 src/ResponseResolverInterface.php diff --git a/composer.json b/composer.json index 36e331d7..9c9cb8b8 100644 --- a/composer.json +++ b/composer.json @@ -33,20 +33,21 @@ "psr/http-message": "^1.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", - "psr/simple-cache": "^1.0", - "sunrise/hydrator": "^2.7" + "psr/simple-cache": "^1.0" }, "require-dev": { + "doctrine/annotations": "^2.0", "phpunit/phpunit": "~9.5.0", "sunrise/coding-standard": "~1.0.0", "sunrise/http-message": "^3.0", - "doctrine/annotations": "^1.6", + "sunrise/hydrator": "^2.7", "symfony/console": "^5.4", "symfony/event-dispatcher": "^4.4" }, "autoload": { "files": [ "functions/emit.php", + "functions/get_debug_type.php", "functions/path_build.php", "functions/path_match.php", "functions/path_parse.php", diff --git a/functions/get_debug_type.php b/functions/get_debug_type.php new file mode 100644 index 00000000..282b5875 --- /dev/null +++ b/functions/get_debug_type.php @@ -0,0 +1,47 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +if (PHP_MAJOR_VERSION < 8) { + + /** + * Polyfill for the get_debug_type function + * + * @param mixed $value + * + * @return string + * + * @since 3.0.0 + */ + function get_debug_type($value): string + { + if (null === $value) { + return 'null'; + } + + if (is_bool($value)) { + return 'bool'; + } + + if (is_int($value)) { + return 'int'; + } + + if (is_float($value)) { + return 'float'; + } + + if (is_object($value)) { + return get_class($value); + } + + return gettype($value); + } +} diff --git a/src/Annotation/Middleware.php b/src/Annotation/Middleware.php index 1d339476..275b0e17 100644 --- a/src/Annotation/Middleware.php +++ b/src/Annotation/Middleware.php @@ -15,7 +15,6 @@ * Import classes */ use Attribute; -use Psr\Http\Server\MiddlewareInterface; /** * @Annotation @@ -37,14 +36,14 @@ final class Middleware /** * The attribute value * - * @var class-string + * @var class-string */ public string $value; /** * Constructor of the class * - * @param class-string $value + * @param class-string $value */ public function __construct(string $value) { diff --git a/src/Annotation/Route.php b/src/Annotation/Route.php index f2fce7b2..a25293bf 100644 --- a/src/Annotation/Route.php +++ b/src/Annotation/Route.php @@ -16,7 +16,6 @@ */ use Attribute; use Fig\Http\Message\RequestMethodInterface; -use Psr\Http\Server\MiddlewareInterface; /** * @Annotation @@ -83,7 +82,7 @@ final class Route implements RequestMethodInterface /** * The route middlewares * - * @var list> + * @var list */ public array $middlewares; @@ -125,17 +124,17 @@ final class Route implements RequestMethodInterface /** * Constructor of the class * - * @param string $name The route name - * @param string|null $host The route host - * @param string $path The route path - * @param string|null $method The route method - * @param list $methods The route methods - * @param list> $middlewares The route middlewares - * @param array $attributes The route attributes - * @param string $summary The route summary - * @param string $description The route description - * @param list $tags The route tags - * @param int $priority The route priority (default 0) + * @param string $name The route name + * @param string|null $host The route host + * @param string $path The route path + * @param string|null $method The route method + * @param list $methods The route methods + * @param list $middlewares The route middlewares + * @param array $attributes The route attributes + * @param string $summary The route summary + * @param string $description The route description + * @param list $tags The route tags + * @param int $priority The route priority (default 0) */ public function __construct( string $name, diff --git a/src/Exception/ParameterResolvingException.php b/src/Exception/ParameterResolvingException.php index 81773b51..ad85b9cf 100644 --- a/src/Exception/ParameterResolvingException.php +++ b/src/Exception/ParameterResolvingException.php @@ -11,47 +11,11 @@ namespace Sunrise\Http\Router\Exception; -/** - * Import classes - */ -use ReflectionParameter; - -/** - * Import functions - */ -use function sprintf; - /** * ParameterResolvingException * * @since 3.0.0 */ -class ParameterResolvingException extends Exception +class ParameterResolvingException extends ResolvingException { - - /** - * @return self - */ - final public static function unsupportedParameterTypeDeclaration(ReflectionParameter $parameter): self - { - return new self(sprintf( - '%s($%s[%d]): Unsupported parameter type declaration', - $parameter->getDeclaringFunction()->getName(), - $parameter->getName(), - $parameter->getPosition() - )); - } - - /** - * @return self - */ - final public static function unknownParameter(ReflectionParameter $parameter): self - { - return new self(sprintf( - '%s($%s[%d]): Unknown parameter', - $parameter->getDeclaringFunction()->getName(), - $parameter->getName(), - $parameter->getPosition() - )); - } } diff --git a/src/Exception/ReferenceResolvingException.php b/src/Exception/ReferenceResolvingException.php new file mode 100644 index 00000000..9a8387b7 --- /dev/null +++ b/src/Exception/ReferenceResolvingException.php @@ -0,0 +1,21 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * ReferenceResolvingException + * + * @since 3.0.0 + */ +class ReferenceResolvingException extends ResolvingException +{ +} diff --git a/src/Exception/ResolvingException.php b/src/Exception/ResolvingException.php new file mode 100644 index 00000000..4fafb2a5 --- /dev/null +++ b/src/Exception/ResolvingException.php @@ -0,0 +1,21 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * ResolvingException + * + * @since 3.0.0 + */ +class ResolvingException extends Exception +{ +} diff --git a/src/Exception/ResponseResolvingException.php b/src/Exception/ResponseResolvingException.php new file mode 100644 index 00000000..6b4db132 --- /dev/null +++ b/src/Exception/ResponseResolvingException.php @@ -0,0 +1,21 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * ResponseResolvingException + * + * @since 3.0.0 + */ +class ResponseResolvingException extends ResolvingException +{ +} diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 0a70af38..4b64bab4 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -76,18 +76,6 @@ public function __construct( $this->referenceResolver = $referenceResolver ?? new ReferenceResolver(); } - /** - * Gets the loader container - * - * @return ContainerInterface|null - * - * @since 2.9.0 - */ - public function getContainer(): ?ContainerInterface - { - return $this->referenceResolver->getContainer(); - } - /** * Sets the given container to the loader * diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 557079d2..c5b0bde2 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -121,16 +121,6 @@ public function __construct( } } - /** - * Gets the loader container - * - * @return ContainerInterface|null - */ - public function getContainer(): ?ContainerInterface - { - return $this->referenceResolver->getContainer(); - } - /** * Sets the given container to the loader * @@ -295,8 +285,8 @@ public function load(): RouteCollectionInterface $descriptor->name, $descriptor->path, $descriptor->methods, - $this->referenceResolver->toRequestHandler($descriptor->holder), - $this->referenceResolver->toMiddlewares($descriptor->middlewares), + $this->referenceResolver->resolveRequestHandler($descriptor->holder), + $this->referenceResolver->resolveMiddlewares($descriptor->middlewares), $descriptor->attributes ) ->setHost($descriptor->host) diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index 2cee788d..780bd6db 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -19,6 +19,11 @@ use Psr\Container\ContainerInterface; use Sunrise\Http\Router\Exception\ParameterResolvingException; +/** + * Import functions + */ +use function sprintf; + /** * ParameterResolutioner * @@ -123,12 +128,11 @@ public function resolveParameters(ReflectionParameter ...$parameters): array private function resolveParameter(ReflectionParameter $parameter) { $type = $parameter->getType(); - if (!($type instanceof ReflectionNamedType)) { - throw ParameterResolvingException::unsupportedParameterTypeDeclaration($parameter); - } - if (isset($this->types[$type->getName()])) { - return $this->types[$type->getName()]; + if (($type instanceof ReflectionNamedType) && !$type->isBuiltin()) { + if (isset($this->types[$type->getName()])) { + return $this->types[$type->getName()]; + } } foreach ($this->resolvers as $resolver) { @@ -137,14 +141,21 @@ private function resolveParameter(ReflectionParameter $parameter) } } - if (isset($this->container) && $this->container->has($type->getName())) { - return $this->container->get($type->getName()); + if (($type instanceof ReflectionNamedType) && !$type->isBuiltin()) { + if (isset($this->container) && $this->container->has($type->getName())) { + return $this->container->get($type->getName()); + } } if ($parameter->isDefaultValueAvailable()) { return $parameter->getDefaultValue(); } - throw ParameterResolvingException::unknownParameter($parameter); + throw new ParameterResolvingException(sprintf( + 'Unexpected parameter {%s($%s[%d])}', + $parameter->getDeclaringFunction()->getName(), + $parameter->getName(), + $parameter->getPosition() + )); } } diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index 14745c93..001d9c32 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -23,7 +23,6 @@ use Sunrise\Hydrator\Exception\InvalidObjectException; use Sunrise\Hydrator\Exception\InvalidValueException; use Sunrise\Hydrator\HydratorInterface; -use Sunrise\Hydrator\Hydrator; use ReflectionNamedType; use ReflectionParameter; @@ -43,37 +42,18 @@ final class RequestBodyParameterResolver implements ParameterResolverInterface { /** - * @var HydratorInterface|null + * @var HydratorInterface */ - private ?HydratorInterface $hydrator = null; + private HydratorInterface $hydrator; /** - * @param HydratorInterface|null $hydrator + * @param HydratorInterface $hydrator */ - public function __construct(?HydratorInterface $hydrator = null) + public function __construct(HydratorInterface $hydrator) { $this->hydrator = $hydrator; } - /** - * @return HydratorInterface - */ - private function getHydrator(): HydratorInterface - { - if (isset($this->hydrator)) { - return $this->hydrator; - } - - $this->hydrator = new Hydrator(); - - // auto-enable annotation support for php 7 - if (7 === PHP_MAJOR_VERSION) { - $this->hydrator->useAnnotations(); - } - - return $this->hydrator; - } - /** * {@inheritdoc} */ @@ -87,6 +67,10 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo return false; } + if ($parameter->getType()->isBuiltin()) { + return false; + } + if ($parameter->getType()->getName() instanceof RequestBodyInterface) { return true; } @@ -110,7 +94,7 @@ public function resolveParameter(ReflectionParameter $parameter, $context) $parameterType = $parameter->getType(); try { - return $this->getHydrator()->hydrate($parameterType->getName(), (array) $context->getParsedBody()); + return $this->hydrator->hydrate($parameterType->getName(), (array) $context->getParsedBody()); } catch (InvalidObjectException $e) { throw new ParameterResolvingException($e->getMessage(), 0, $e); } catch (InvalidValueException $e) { diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index 4a06deb8..ce0025e3 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -21,6 +21,7 @@ use Sunrise\Http\Router\Middleware\CallableMiddleware; use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use Closure; +use ReflectionClass; /** * Import functions @@ -41,18 +42,37 @@ class ReferenceResolver implements ReferenceResolverInterface { /** - * The reference resolver container + * The resolver's parameter resolutioner + * + * @var ParameterResolutionerInterface + */ + private ParameterResolutionerInterface $parameterResolutioner; + /** + * The resolver's response resolutioner + * + * @var ResponseResolutionerInterface + */ + private ResponseResolutionerInterface $responseResolutioner; + + /** + * The resolver's container * * @var ContainerInterface|null */ private ?ContainerInterface $container = null; /** - * {@inheritdoc} + * Constructor of the class + * + * @param ParameterResolutionerInterface|null $parameterResolutioner + * @param ResponseResolutionerInterface|null $responseResolutioner */ - public function getContainer(): ?ContainerInterface - { - return $this->container; + public function __construct( + ?ParameterResolutionerInterface $parameterResolutioner = null, + ?ResponseResolutionerInterface $responseResolutioner = null + ) { + $this->parameterResolutioner = $parameterResolutioner ?? new ParameterResolutioner(); + $this->responseResolutioner = $responseResolutioner ?? new ResponseResolutioner(); } /** @@ -61,28 +81,34 @@ public function getContainer(): ?ContainerInterface public function setContainer(?ContainerInterface $container): void { $this->container = $container; + $this->parameterResolutioner->setContainer($container); } /** * {@inheritdoc} */ - public function toRequestHandler($reference): RequestHandlerInterface + public function resolveRequestHandler($reference): RequestHandlerInterface { if ($reference instanceof RequestHandlerInterface) { return $reference; } if ($reference instanceof Closure) { - // return new CallableRequestHandler($reference, $this->parameterResolver, $this->responseResolver); + return new CallableRequestHandler( + $reference, + $this->parameterResolutioner, + $this->responseResolutioner + ); } list($class, $method) = $this->normalizeReference($reference); if (isset($class) && isset($method) && method_exists($class, $method)) { - /** @var callable */ - $callback = [$this->resolveClass($class), $method]; - - // return new CallableRequestHandler($callback, $this->parameterResolver, $this->responseResolver); + return new CallableRequestHandler( + [$this->resolveClass($class), $method], + $this->parameterResolutioner, + $this->responseResolutioner + ); } if (!isset($method) && isset($class) && is_subclass_of($class, RequestHandlerInterface::class)) { @@ -98,23 +124,28 @@ public function toRequestHandler($reference): RequestHandlerInterface /** * {@inheritdoc} */ - public function toMiddleware($reference): MiddlewareInterface + public function resolveMiddleware($reference): MiddlewareInterface { if ($reference instanceof MiddlewareInterface) { return $reference; } if ($reference instanceof Closure) { - // return new CallableMiddleware($reference, $this->parameterResolver, $this->responseResolver); + return new CallableMiddleware( + $reference, + $this->parameterResolutioner, + $this->responseResolutioner + ); } list($class, $method) = $this->normalizeReference($reference); if (isset($class) && isset($method) && method_exists($class, $method)) { - /** @var callable */ - $callback = [$this->resolveClass($class), $method]; - - // return new CallableMiddleware($callback, $this->parameterResolver, $this->responseResolver); + return new CallableMiddleware( + [$this->resolveClass($class), $method], + $this->parameterResolutioner, + $this->responseResolutioner + ); } if (!isset($method) && isset($class) && is_subclass_of($class, MiddlewareInterface::class)) { @@ -130,12 +161,12 @@ public function toMiddleware($reference): MiddlewareInterface /** * {@inheritdoc} */ - public function toMiddlewares(array $references): array + public function resolveMiddlewares(array $references): array { $middlewares = []; /** @psalm-suppress MixedAssignment */ foreach ($references as $reference) { - $middlewares[] = $this->toMiddleware($reference); + $middlewares[] = $this->resolveMiddleware($reference); } return $middlewares; @@ -201,7 +232,19 @@ private function resolveClass(string $className) return $this->container->get($className); } - /** @psalm-suppress MixedMethodCall */ - return new $className; + $reflection = new ReflectionClass($className); + if (!$reflection->isInstantiable()) { + throw new ReferenceResolvingException(); + } + + $arguments = []; + $constructor = $reflection->getConstructor(); + if (isset($constructor)) { + $arguments = $this->parameterResolutioner->resolveParameters( + ...$constructor->getParameters() + ); + } + + return new $className(...$arguments); } } diff --git a/src/ReferenceResolverInterface.php b/src/ReferenceResolverInterface.php index 67b6208d..25916e85 100644 --- a/src/ReferenceResolverInterface.php +++ b/src/ReferenceResolverInterface.php @@ -17,7 +17,7 @@ use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\InvalidReferenceException; +use Sunrise\Http\Router\Exception\ReferenceResolvingException; /** * ReferenceResolverInterface @@ -28,14 +28,7 @@ interface ReferenceResolverInterface { /** - * Gets the reference resolver container - * - * @return ContainerInterface|null - */ - public function getContainer(): ?ContainerInterface; - - /** - * Sets the given container to the reference resolver + * Sets the given container to the resolver * * @param ContainerInterface|null $container * @@ -50,10 +43,10 @@ public function setContainer(?ContainerInterface $container): void; * * @return RequestHandlerInterface * - * @throws InvalidReferenceException - * If the given reference cannot be resolved to a request handler. + * @throws ReferenceResolvingException + * If the reference cannot be resolved to a request handler. */ - public function toRequestHandler($reference): RequestHandlerInterface; + public function resolveRequestHandler($reference): RequestHandlerInterface; /** * Resolves the given reference to a middleware @@ -62,10 +55,10 @@ public function toRequestHandler($reference): RequestHandlerInterface; * * @return MiddlewareInterface * - * @throws InvalidReferenceException - * If the given reference cannot be resolved to a middleware. + * @throws ReferenceResolvingException + * If the reference cannot be resolved to a middleware. */ - public function toMiddleware($reference): MiddlewareInterface; + public function resolveMiddleware($reference): MiddlewareInterface; /** * Resolves the given references to middlewares @@ -74,10 +67,10 @@ public function toMiddleware($reference): MiddlewareInterface; * * @return list * - * @throws InvalidReferenceException - * If one of the given references cannot be resolved to a middleware. + * @throws ReferenceResolvingException + * If one of the references cannot be resolved to a middleware. * * @since 3.0.0 */ - public function toMiddlewares(array $references): array; + public function resolveMiddlewares(array $references): array; } diff --git a/src/ResponseResolutioner.php b/src/ResponseResolutioner.php index 0b16fe5d..f5147350 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolutioner.php @@ -15,7 +15,13 @@ * Import classes */ use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\ResponseResolvingException; + +/** + * Import functions + */ +use function get_debug_type; +use function sprintf; /** * ResponseResolutioner @@ -32,6 +38,13 @@ final class ResponseResolutioner implements ResponseResolutionerInterface */ private $context = null; + /** + * The resolutioner's resolvers + * + * @var list + */ + private array $resolvers = []; + /** * {@inheritdoc} */ @@ -43,6 +56,16 @@ public function withContext($context): ResponseResolutionerInterface return $clone; } + /** + * {@inheritdoc} + */ + public function addResolver(ResponseResolverInterface ...$resolvers): void + { + foreach ($resolvers as $resolver) { + $this->resolvers[] = $resolver; + } + } + /** * {@inheritdoc} */ @@ -52,6 +75,15 @@ public function resolveResponse($response): ResponseInterface return $response; } - throw new LogicException(); + foreach ($this->resolvers as $resolver) { + if ($resolver->supportsResponse($response, $this->context)) { + return $resolver->resolveResponse($response, $this->context); + } + } + + throw new ResponseResolvingException(sprintf( + 'Unexpected response {%s}', + get_debug_type($response) + )); } } diff --git a/src/ResponseResolutionerInterface.php b/src/ResponseResolutionerInterface.php index cbbb3bb5..f737802c 100644 --- a/src/ResponseResolutionerInterface.php +++ b/src/ResponseResolutionerInterface.php @@ -15,7 +15,7 @@ * Import classes */ use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\ResponseResolvingException; /** * ResponseResolutionerInterface @@ -37,14 +37,23 @@ interface ResponseResolutionerInterface public function withContext($context): ResponseResolutionerInterface; /** - * Resolves the given raw response to the response object + * Adds the given response resolver(s) to the resolutioner + * + * @param ResponseResolverInterface ...$resolvers + * + * @return void + */ + public function addResolver(ResponseResolverInterface ...$resolvers): void; + + /** + * Resolves the given raw response to the object * * @param mixed $response * * @return ResponseInterface * - * @throws LogicException - * If the raw response cannot be resolved to the response object. + * @throws ResponseResolvingException + * If the raw response cannot be resolved to the object. */ public function resolveResponse($response): ResponseInterface; } diff --git a/src/ResponseResolver/StatusCodeResponseResolver.php b/src/ResponseResolver/StatusCodeResponseResolver.php new file mode 100644 index 00000000..c7b97262 --- /dev/null +++ b/src/ResponseResolver/StatusCodeResponseResolver.php @@ -0,0 +1,64 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ResponseResolver; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Sunrise\Http\Router\ResponseResolverInterface; + +/** + * Import functions + */ +use function is_int; + +/** + * StatusCodeResponseResolver + * + * @since 3.0.0 + */ +final class StatusCodeResponseResolver implements ResponseResolverInterface +{ + + /** + * @var ResponseFactoryInterface + */ + private ResponseFactoryInterface $responseFactory; + + /** + * @param ResponseFactoryInterface $responseFactory + */ + public function __construct(ResponseFactoryInterface $responseFactory) + { + $this->responseFactory = $responseFactory; + } + + /** + * {@inheritdoc} + */ + public function supportsResponse($response, $context): bool + { + return is_int($response) && $response >= 100 && $response <= 599; + } + + /** + * {@inheritdoc} + */ + public function resolveResponse($response, $context): ResponseInterface + { + /** @var int $response */ + + return $this->responseFactory->createResponse($response); + } +} diff --git a/src/ResponseResolverInterface.php b/src/ResponseResolverInterface.php new file mode 100644 index 00000000..fa6fd515 --- /dev/null +++ b/src/ResponseResolverInterface.php @@ -0,0 +1,50 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseInterface; +use Sunrise\Http\Router\Exception\ResponseResolvingException; + +/** + * ResponseResolverInterface + * + * @since 3.0.0 + */ +interface ResponseResolverInterface +{ + + /** + * Checks if the given raw response is supported + * + * @param mixed $response + * @param mixed $context + * + * @return bool + */ + public function supportsResponse($response, $context): bool; + + /** + * Resolves the given raw response to the object + * + * @param mixed $response + * @param mixed $context + * + * @return ResponseInterface + * + * @throws ResponseResolvingException + * If the raw response cannot be resolved to the object. + */ + public function resolveResponse($response, $context): ResponseInterface; +} diff --git a/src/RouteCollector.php b/src/RouteCollector.php index b0f92900..512e23e2 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -71,39 +71,27 @@ public function __construct( } /** - * Gets the collector collection + * Sets the given container to the collector * - * @return RouteCollectionInterface - */ - public function getCollection() : RouteCollectionInterface - { - return $this->collection; - } - - /** - * Gets the collector container + * @param ContainerInterface|null $container * - * @return ContainerInterface|null + * @return void * * @since 2.9.0 */ - public function getContainer() : ?ContainerInterface + public function setContainer(?ContainerInterface $container) : void { - return $this->referenceResolver->getContainer(); + $this->referenceResolver->setContainer($container); } /** - * Sets the given container to the collector - * - * @param ContainerInterface|null $container - * - * @return void + * Gets the collector collection * - * @since 2.9.0 + * @return RouteCollectionInterface */ - public function setContainer(?ContainerInterface $container) : void + public function getCollection() : RouteCollectionInterface { - $this->referenceResolver->setContainer($container); + return $this->collection; } /** @@ -133,8 +121,8 @@ public function route( $name, $path, $methods, - $this->referenceResolver->toRequestHandler($requestHandler), - $this->referenceResolver->toMiddlewares($middlewares), + $this->referenceResolver->resolveRequestHandler($requestHandler), + $this->referenceResolver->resolveMiddlewares($middlewares), $attributes ); @@ -382,7 +370,7 @@ public function group(callable $callback, array $middlewares = []) : RouteCollec $callback($collector); $collector->collection->prependMiddleware( - ...$this->referenceResolver->toMiddlewares($middlewares) + ...$this->referenceResolver->resolveMiddlewares($middlewares) ); $this->collection->add( diff --git a/src/Router.php b/src/Router.php index d3365bcc..b59a6764 100644 --- a/src/Router.php +++ b/src/Router.php @@ -484,16 +484,20 @@ public function match(ServerRequestInterface $request) : RouteInterface public function run(ServerRequestInterface $request) : ResponseInterface { // lazy resolving of the given request... - $routing = new CallableRequestHandler(function (ServerRequestInterface $request) : ResponseInterface { - $route = $this->match($request); - $this->matchedRoute = $route; + $routing = new class implements RequestHandlerInterface + { + public function handle(ServerRequestInterface $request): ResponseInterface + { + $route = $this->match($request); + $this->matchedRoute = $route; + + if (isset($this->eventDispatcher)) { + $this->eventDispatcher->dispatch(new RouteEvent($route, $request)); + } - if (isset($this->eventDispatcher)) { - $this->eventDispatcher->dispatch(new RouteEvent($route, $request)); + return $route->handle($request); } - - return $route->handle($request); - }); + }; $middlewares = $this->getMiddlewares(); if (empty($middlewares)) { From ed185552636af92e5ee7802d54287ea9c92f05b4 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 22 Jan 2023 01:16:55 +0100 Subject: [PATCH 031/180] v3 --- composer.json | 1 + functions/find_classes.php | 62 +++++++ src/Annotation/Middleware.php | 5 +- src/Annotation/Route.php | 25 +-- src/Command/RouteListCommand.php | 2 +- src/Loader/ConfigLoader.php | 47 ++++-- src/Loader/DescriptorLoader.php | 112 ++++++------- .../JsonPayloadDecodingMiddleware.php | 30 ++-- src/ParameterResolutioner.php | 2 +- .../RequestBodyParameterResolver.php | 15 +- src/ReferenceResolver.php | 154 ++++++++---------- src/ReferenceResolverInterface.php | 22 +++ src/ResponseResolutioner.php | 2 +- src/RouteCollector.php | 6 +- src/RouteFactoryInterface.php | 4 +- src/RouteInterface.php | 52 +++--- src/Router.php | 46 +++--- src/RouterBuilder.php | 22 +-- 18 files changed, 340 insertions(+), 269 deletions(-) create mode 100644 functions/find_classes.php diff --git a/composer.json b/composer.json index 9c9cb8b8..b19d2476 100644 --- a/composer.json +++ b/composer.json @@ -47,6 +47,7 @@ "autoload": { "files": [ "functions/emit.php", + "functions/find_classes.php", "functions/get_debug_type.php", "functions/path_build.php", "functions/path_match.php", diff --git a/functions/find_classes.php b/functions/find_classes.php new file mode 100644 index 00000000..755aa46b --- /dev/null +++ b/functions/find_classes.php @@ -0,0 +1,62 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use Iterator; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use SplFileInfo; + +/** + * Import functions + */ +use function array_diff; +use function get_declared_classes; + +/** + * Scans the given directory and returns the found classes + * + * @param string $directory + * + * @return class-string[] + * + * @since 3.0.0 + */ +function find_classes(string $directory): array +{ + static $cache = []; + + if (isset($cache[$directory])) { + return $cache[$directory]; + } + + /** @var Iterator */ + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory) + ); + + $knownClasses = get_declared_classes(); + + foreach ($files as $file) { + if ('php' === $file->getExtension()) { + /** @psalm-suppress UnresolvableInclude */ + require_once $file->getPathname(); + } + } + + $cache[$directory] = array_diff(get_declared_classes(), $knownClasses); + + return $cache[$directory]; +} diff --git a/src/Annotation/Middleware.php b/src/Annotation/Middleware.php index 275b0e17..1d339476 100644 --- a/src/Annotation/Middleware.php +++ b/src/Annotation/Middleware.php @@ -15,6 +15,7 @@ * Import classes */ use Attribute; +use Psr\Http\Server\MiddlewareInterface; /** * @Annotation @@ -36,14 +37,14 @@ final class Middleware /** * The attribute value * - * @var class-string + * @var class-string */ public string $value; /** * Constructor of the class * - * @param class-string $value + * @param class-string $value */ public function __construct(string $value) { diff --git a/src/Annotation/Route.php b/src/Annotation/Route.php index a25293bf..f2fce7b2 100644 --- a/src/Annotation/Route.php +++ b/src/Annotation/Route.php @@ -16,6 +16,7 @@ */ use Attribute; use Fig\Http\Message\RequestMethodInterface; +use Psr\Http\Server\MiddlewareInterface; /** * @Annotation @@ -82,7 +83,7 @@ final class Route implements RequestMethodInterface /** * The route middlewares * - * @var list + * @var list> */ public array $middlewares; @@ -124,17 +125,17 @@ final class Route implements RequestMethodInterface /** * Constructor of the class * - * @param string $name The route name - * @param string|null $host The route host - * @param string $path The route path - * @param string|null $method The route method - * @param list $methods The route methods - * @param list $middlewares The route middlewares - * @param array $attributes The route attributes - * @param string $summary The route summary - * @param string $description The route description - * @param list $tags The route tags - * @param int $priority The route priority (default 0) + * @param string $name The route name + * @param string|null $host The route host + * @param string $path The route path + * @param string|null $method The route method + * @param list $methods The route methods + * @param list> $middlewares The route middlewares + * @param array $attributes The route attributes + * @param string $summary The route summary + * @param string $description The route description + * @param list $tags The route tags + * @param int $priority The route priority (default 0) */ public function __construct( string $name, diff --git a/src/Command/RouteListCommand.php b/src/Command/RouteListCommand.php index 259aa205..65052713 100644 --- a/src/Command/RouteListCommand.php +++ b/src/Command/RouteListCommand.php @@ -32,7 +32,7 @@ * This command will list all routes in your application * * If you can't pass the router to the constructor, - * or your architecture has problems with autowiring, + * or your architecture has problems with the autowiring, * just inherit this class and override the getRouter method. * * @since 2.9.0 diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 4b64bab4..1694048a 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -16,8 +16,10 @@ */ use Psr\Container\ContainerInterface; use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; +use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; +use Sunrise\Http\Router\ResponseResolverInterface; use Sunrise\Http\Router\RouteCollectionFactory; use Sunrise\Http\Router\RouteCollectionFactoryInterface; use Sunrise\Http\Router\RouteCollectionInterface; @@ -28,6 +30,7 @@ /** * Import functions */ +use function get_debug_type; use function glob; use function is_dir; use function is_file; @@ -36,7 +39,7 @@ /** * ConfigLoader */ -class ConfigLoader implements LoaderInterface +final class ConfigLoader implements LoaderInterface { /** @@ -77,7 +80,7 @@ public function __construct( } /** - * Sets the given container to the loader + * Sets the given container to the reference resolver * * @param ContainerInterface|null $container * @@ -90,23 +93,45 @@ public function setContainer(?ContainerInterface $container): void $this->referenceResolver->setContainer($container); } + /** + * Adds the given parameter resolver(s) to the reference resolver + * + * @param ParameterResolverInterface ...$resolvers + * + * @return void + * + * @since 3.0.0 + */ + public function addParameterResolver(ParameterResolverInterface ...$resolvers): void + { + $this->referenceResolver->addParameterResolver(...$resolvers); + } + + /** + * Adds the given response resolver(s) to the reference resolver + * + * @param ResponseResolverInterface ...$resolvers + * + * @return void + * + * @since 3.0.0 + */ + public function addResponseResolver(ResponseResolverInterface ...$resolvers): void + { + $this->referenceResolver->addResponseResolver(...$resolvers); + } + /** * {@inheritdoc} */ public function attach($resource): void { - if (!is_string($resource)) { - throw new InvalidLoaderResourceException( - 'Config route loader only expects string resources' - ); - } - - if (is_file($resource)) { + if (is_string($resource) && is_file($resource)) { $this->resources[] = $resource; return; } - if (is_dir($resource)) { + if (is_string($resource) && is_dir($resource)) { $filenames = glob($resource . '/*.php'); foreach ($filenames as $filename) { $this->resources[] = $filename; @@ -118,7 +143,7 @@ public function attach($resource): void throw new InvalidLoaderResourceException(sprintf( 'Config route loader only handles file or directory paths, ' . 'however the given resource "%s" is not as expected', - $resource + is_string($resource) ? $resource : get_debug_type($resource) )); } diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index c5b0bde2..d48120ef 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -27,32 +27,30 @@ use Sunrise\Http\Router\Annotation\Route; use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; +use Sunrise\Http\Router\ResponseResolverInterface; use Sunrise\Http\Router\RouteCollectionFactory; use Sunrise\Http\Router\RouteCollectionFactoryInterface; use Sunrise\Http\Router\RouteCollectionInterface; use Sunrise\Http\Router\RouteFactory; use Sunrise\Http\Router\RouteFactoryInterface; -use Iterator; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; use ReflectionAttribute; use ReflectionClass; use ReflectionMethod; use Reflector; -use SplFileInfo; /** * Import functions */ -use function array_diff; use function class_exists; -use function get_declared_classes; +use function get_debug_type; use function hash; use function is_dir; use function is_string; use function usort; +use Sunrise\Http\Router\find_classes; /** * Import constants @@ -122,7 +120,7 @@ public function __construct( } /** - * Sets the given container to the loader + * Sets the given container to the reference resolver * * @param ContainerInterface|null $container * @@ -134,15 +132,31 @@ public function setContainer(?ContainerInterface $container): void } /** - * Gets the loader annotation reader + * Adds the given parameter resolver(s) to the reference resolver * - * @return AnnotationReaderInterface|null + * @param ParameterResolverInterface ...$resolvers + * + * @return void + * + * @since 3.0.0 + */ + public function addParameterResolver(ParameterResolverInterface ...$resolvers): void + { + $this->referenceResolver->addParameterResolver(...$resolvers); + } + + /** + * Adds the given response resolver(s) to the reference resolver + * + * @param ResponseResolverInterface ...$resolvers + * + * @return void * * @since 3.0.0 */ - public function getAnnotationReader(): ?AnnotationReaderInterface + public function addResponseResolver(ResponseResolverInterface ...$resolvers): void { - return $this->annotationReader; + $this->referenceResolver->addResponseResolver(...$resolvers); } /** @@ -234,21 +248,15 @@ public function useDefaultAnnotationReader(): void */ public function attach($resource): void { - if (!is_string($resource)) { - throw new InvalidLoaderResourceException( - 'Descriptor route loader only expects string resources' - ); - } - - if (class_exists($resource)) { + if (is_string($resource) && class_exists($resource)) { $this->resources[] = $resource; return; } - if (is_dir($resource)) { - $classNames = $this->scandir($resource); - foreach ($classNames as $className) { - $this->resources[] = $className; + if (is_string($resource) && is_dir($resource)) { + $classnames = find_classes($resource); + foreach ($classnames as $classname) { + $this->resources[] = $classname; } return; @@ -257,7 +265,7 @@ public function attach($resource): void throw new InvalidLoaderResourceException(sprintf( 'Descriptor route loader only handles class names or directory paths, ' . 'however the given resource "%s" is not as expected', - $resource + is_string($resource) ? $resource : get_debug_type($resource) )); } @@ -333,7 +341,7 @@ private function collectDescriptors(): array $result = []; foreach ($this->resources as $resource) { $class = new ReflectionClass($resource); - $descriptors = $this->getClassDescriptors($class); + $descriptors = $this->getDescriptorsFromClass($class); foreach ($descriptors as $descriptor) { $result[] = $descriptor; } @@ -353,7 +361,7 @@ private function collectDescriptors(): array * * @return list */ - private function getClassDescriptors(ReflectionClass $class): array + private function getDescriptorsFromClass(ReflectionClass $class): array { // e.g., interfaces, traits, enums, abstract classes, // classes with private constructor... @@ -364,26 +372,26 @@ private function getClassDescriptors(ReflectionClass $class): array $descriptors = []; if ($class->isSubclassOf(RequestHandlerInterface::class)) { - $annotations = $this->getAnnotations($class, Route::class); + $annotations = $this->getAnnotationsFromClassOrMethod($class, Route::class); if (isset($annotations[0])) { $descriptor = $annotations[0]; - $this->supplementDescriptor($descriptor, $class); + $this->supplementDescriptorFromClassOrMethod($descriptor, $class); $descriptor->holder = $class->getName(); $descriptors[] = $descriptor; } } foreach ($class->getMethods() as $method) { - // ignore non-available methods... - if (!$method->isPublic() || $method->isStatic()) { + // ignore non-public methods... + if (!$method->isPublic()) { continue; } - $annotations = $this->getAnnotations($method, Route::class); + $annotations = $this->getAnnotationsFromClassOrMethod($method, Route::class); if (isset($annotations[0])) { $descriptor = $annotations[0]; - $this->supplementDescriptor($descriptor, $class); - $this->supplementDescriptor($descriptor, $method); + $this->supplementDescriptorFromClassOrMethod($descriptor, $class); + $this->supplementDescriptorFromClassOrMethod($descriptor, $method); $descriptor->holder = [$class->getName(), $method->getName()]; $descriptors[] = $descriptor; } @@ -421,24 +429,24 @@ private function getClassDescriptors(ReflectionClass $class): array * * @return void */ - private function supplementDescriptor(Route $descriptor, Reflector $reflector): void + private function supplementDescriptorFromClassOrMethod(Route $descriptor, Reflector $reflector): void { - $annotations = $this->getAnnotations($reflector, Host::class); + $annotations = $this->getAnnotationsFromClassOrMethod($reflector, Host::class); if (isset($annotations[0])) { $descriptor->host = $annotations[0]->value; } - $annotations = $this->getAnnotations($reflector, Prefix::class); + $annotations = $this->getAnnotationsFromClassOrMethod($reflector, Prefix::class); if (isset($annotations[0])) { $descriptor->path = $annotations[0]->value . $descriptor->path; } - $annotations = $this->getAnnotations($reflector, Postfix::class); + $annotations = $this->getAnnotationsFromClassOrMethod($reflector, Postfix::class); if (isset($annotations[0])) { $descriptor->path = $descriptor->path . $annotations[0]->value; } - $annotations = $this->getAnnotations($reflector, Middleware::class); + $annotations = $this->getAnnotationsFromClassOrMethod($reflector, Middleware::class); foreach ($annotations as $annotation) { $descriptor->middlewares[] = $annotation->value; } @@ -454,7 +462,7 @@ private function supplementDescriptor(Route $descriptor, Reflector $reflector): * * @template T */ - private function getAnnotations(Reflector $reflector, string $annotationName): array + private function getAnnotationsFromClassOrMethod(Reflector $reflector, string $annotationName): array { $result = []; @@ -467,7 +475,7 @@ private function getAnnotations(Reflector $reflector, string $annotationName): a } } - if (isset($this->annotationReader) && empty($result)) { + if (isset($this->annotationReader)) { $annotations = ($reflector instanceof ReflectionClass) ? $this->annotationReader->getClassAnnotations($reflector) : $this->annotationReader->getMethodAnnotations($reflector); @@ -481,30 +489,4 @@ private function getAnnotations(Reflector $reflector, string $annotationName): a return $result; } - - /** - * Scans the given directory and returns the found classes - * - * @param string $directory - * - * @return class-string[] - */ - private function scandir(string $directory): array - { - /** @var Iterator */ - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($directory) - ); - - $declared = get_declared_classes(); - - foreach ($files as $file) { - if ('php' === $file->getExtension()) { - /** @psalm-suppress UnresolvableInclude */ - require_once $file->getPathname(); - } - } - - return array_diff(get_declared_classes(), $declared); - } } diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 46ea269a..70740f04 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -47,21 +47,12 @@ final class JsonPayloadDecodingMiddleware implements MiddlewareInterface { - /** - * JSON media type - * - * @link https://datatracker.ietf.org/doc/html/rfc4627 - * - * @var string - */ - private const JSON_MEDIA_TYPE = 'application/json'; - /** * {@inheritdoc} */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if ($this->isSupportedRequest($request)) { + if ($this->supportsRequest($request)) { $data = $this->decodeRequestJsonPayload($request); $request = $request->withParsedBody($data); } @@ -72,13 +63,15 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface /** * Checks if the given request is supported * + * @link https://datatracker.ietf.org/doc/html/rfc4627 + * * @param ServerRequestInterface $request * * @return bool */ - private function isSupportedRequest(ServerRequestInterface $request): bool + private function supportsRequest(ServerRequestInterface $request): bool { - return self::JSON_MEDIA_TYPE === $this->getRequestMediaType($request); + return 'application/json' === $this->getRequestMediaType($request); } /** @@ -99,14 +92,13 @@ private function getRequestMediaType(ServerRequestInterface $request): ?string } // type "/" subtype *( OWS ";" OWS parameter ) - $mediaType = $request->getHeaderLine('Content-Type'); + $mediatype = $request->getHeaderLine('Content-Type'); - $semicolon = strpos($mediaType, ';'); - if (false === $semicolon) { - return $mediaType; + if ($semicolon = strpos($mediatype, ';')) { + $mediatype = substr($mediatype, 0, $semicolon); } - return rtrim(substr($mediaType, 0, $semicolon)); + return rtrim($mediatype); } /** @@ -117,12 +109,12 @@ private function getRequestMediaType(ServerRequestInterface $request): ?string * @return array|object|null * * @throws InvalidPayloadException - * If the request's JSON payload cannot be decoded. + * If the request's "JSON" payload cannot be decoded. */ private function decodeRequestJsonPayload(ServerRequestInterface $request) { // https://www.php.net/json.constants - $flags = JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY; + $flags = JSON_OBJECT_AS_ARRAY | JSON_BIGINT_AS_STRING; try { /** @var mixed */ diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index 780bd6db..d85fcd14 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -152,7 +152,7 @@ private function resolveParameter(ReflectionParameter $parameter) } throw new ParameterResolvingException(sprintf( - 'Unexpected parameter {%s($%s[%d])}', + 'Unable to resolve the parameter {%s($%s[%d])}', $parameter->getDeclaringFunction()->getName(), $parameter->getName(), $parameter->getPosition() diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index 001d9c32..f70a4593 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -26,6 +26,11 @@ use ReflectionNamedType; use ReflectionParameter; +/** + * Import functions + */ +use function is_subclass_of; + /** * Import constants */ @@ -63,19 +68,15 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo return false; } - if (!($parameter->getType() instanceof ReflectionNamedType)) { - return false; - } - - if ($parameter->getType()->isBuiltin()) { + if (!($parameter->getType() instanceof ReflectionNamedType) || $parameter->getType()->isBuiltin()) { return false; } - if ($parameter->getType()->getName() instanceof RequestBodyInterface) { + if (8 === PHP_MAJOR_VERSION && $parameter->getAttributes(RequestBody::class)) { return true; } - if (8 === PHP_MAJOR_VERSION && $parameter->getAttributes(RequestBody::class)) { + if (is_subclass_of($parameter->getType()->getName(), RequestBodyInterface::class)) { return true; } diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index ce0025e3..80361180 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -17,7 +17,7 @@ use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\InvalidReferenceException; +use Sunrise\Http\Router\Exception\ReferenceResolvingException; use Sunrise\Http\Router\Middleware\CallableMiddleware; use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use Closure; @@ -26,6 +26,7 @@ /** * Import functions */ +use function get_debug_type; use function is_array; use function is_callable; use function is_string; @@ -38,7 +39,7 @@ * * @since 2.10.0 */ -class ReferenceResolver implements ReferenceResolverInterface +final class ReferenceResolver implements ReferenceResolverInterface { /** @@ -81,9 +82,26 @@ public function __construct( public function setContainer(?ContainerInterface $container): void { $this->container = $container; + $this->parameterResolutioner->setContainer($container); } + /** + * {@inheritdoc} + */ + public function addParameterResolver(ParameterResolverInterface ...$resolvers): void + { + $this->parameterResolutioner->addResolver(...$resolvers); + } + + /** + * {@inheritdoc} + */ + public function addResponseResolver(ResponseResolverInterface ...$resolvers): void + { + $this->responseResolutioner->addResolver(...$resolvers); + } + /** * {@inheritdoc} */ @@ -94,29 +112,26 @@ public function resolveRequestHandler($reference): RequestHandlerInterface } if ($reference instanceof Closure) { - return new CallableRequestHandler( - $reference, - $this->parameterResolutioner, - $this->responseResolutioner - ); + return new CallableRequestHandler($reference, $this->parameterResolutioner, $this->responseResolutioner); } - list($class, $method) = $this->normalizeReference($reference); + // https://github.com/php/php-src/blob/3ed526441400060aa4e618b91b3352371fcd02a8/Zend/zend_API.c#L3884-L3932 + /** @psalm-suppress MixedArgument */ + if (is_array($reference) && is_callable($reference, true) && method_exists($reference[0], $reference[1])) { + /** @var array{0: object|class-string, 1: non-empty-string} $reference */ - if (isset($class) && isset($method) && method_exists($class, $method)) { - return new CallableRequestHandler( - [$this->resolveClass($class), $method], - $this->parameterResolutioner, - $this->responseResolutioner - ); + $callback = [is_string($reference[0]) ? $this->resolveClass($reference[0]) : $reference[0], $reference[1]]; + + return new CallableRequestHandler($callback, $this->parameterResolutioner, $this->responseResolutioner); } - if (!isset($method) && isset($class) && is_subclass_of($class, RequestHandlerInterface::class)) { - return $this->resolveClass($class); + if (is_string($reference) && is_subclass_of($reference, RequestHandlerInterface::class)) { + /** @var RequestHandlerInterface */ + return $this->resolveClass($reference); } - throw new InvalidReferenceException(sprintf( - 'Unable to resolve the "%s" reference to a request handler.', + throw new ReferenceResolvingException(sprintf( + 'Unable to resolve the reference {%s} to a request handler.', $this->stringifyReference($reference) )); } @@ -131,29 +146,26 @@ public function resolveMiddleware($reference): MiddlewareInterface } if ($reference instanceof Closure) { - return new CallableMiddleware( - $reference, - $this->parameterResolutioner, - $this->responseResolutioner - ); + return new CallableMiddleware($reference, $this->parameterResolutioner, $this->responseResolutioner); } - list($class, $method) = $this->normalizeReference($reference); - - if (isset($class) && isset($method) && method_exists($class, $method)) { - return new CallableMiddleware( - [$this->resolveClass($class), $method], - $this->parameterResolutioner, - $this->responseResolutioner - ); + if (is_string($reference) && is_subclass_of($reference, MiddlewareInterface::class)) { + /** @var MiddlewareInterface */ + return $this->resolveClass($reference); } - if (!isset($method) && isset($class) && is_subclass_of($class, MiddlewareInterface::class)) { - return $this->resolveClass($class); + // https://github.com/php/php-src/blob/3ed526441400060aa4e618b91b3352371fcd02a8/Zend/zend_API.c#L3884-L3932 + /** @psalm-suppress MixedArgument */ + if (is_array($reference) && is_callable($reference, true) && method_exists($reference[0], $reference[1])) { + /** @var array{0: object|class-string, 1: non-empty-string} $reference */ + + $callback = [is_string($reference[0]) ? $this->resolveClass($reference[0]) : $reference[0], $reference[1]]; + + return new CallableMiddleware($callback, $this->parameterResolutioner, $this->responseResolutioner); } - throw new InvalidReferenceException(sprintf( - 'Unable to resolve the "%s" reference to a middleware.', + throw new ReferenceResolvingException(sprintf( + 'Unable to resolve the reference {%s} to a middleware.', $this->stringifyReference($reference) )); } @@ -173,25 +185,31 @@ public function resolveMiddlewares(array $references): array } /** - * Normalizes the given reference + * Resolves the given class * - * @param mixed $reference + * @param class-string $class * - * @return array{0: ?class-string, 1: ?non-empty-string} + * @return T + * + * @template T */ - private function normalizeReference($reference): array + private function resolveClass(string $class): object { - if (is_array($reference) && is_callable($reference, true)) { - /** @var array{0: class-string, 1: non-empty-string} $reference */ - return $reference; + if (isset($this->container) && $this->container->has($class)) { + /** @var T */ + return $this->container->get($class); } - if (is_string($reference)) { - /** @var class-string $reference */ - return [$reference, null]; + $arguments = []; + $reflection = new ReflectionClass($class); + $constructor = $reflection->getConstructor(); + if (isset($constructor)) { + $arguments = $this->parameterResolutioner->resolveParameters( + ...$constructor->getParameters() + ); } - return [null, null]; + return $reflection->newInstance(...$arguments); } /** @@ -203,48 +221,14 @@ private function normalizeReference($reference): array */ private function stringifyReference($reference): string { - $reference = $this->normalizeReference($reference); - - if (isset($reference[0], $reference[1])) { - return $reference[0] . '@' . $reference[1]; - } - - if (isset($reference[0])) { - return $reference[0]; + if (is_array($reference) && is_callable($reference, true, $refString)) { + return $refString; } - return ''; - } - - /** - * Resolves the given class - * - * @param class-string $className - * - * @return T - * - * @template T - */ - private function resolveClass(string $className) - { - if (isset($this->container) && $this->container->has($className)) { - /** @var T */ - return $this->container->get($className); - } - - $reflection = new ReflectionClass($className); - if (!$reflection->isInstantiable()) { - throw new ReferenceResolvingException(); - } - - $arguments = []; - $constructor = $reflection->getConstructor(); - if (isset($constructor)) { - $arguments = $this->parameterResolutioner->resolveParameters( - ...$constructor->getParameters() - ); + if (is_string($reference)) { + return $reference; } - return new $className(...$arguments); + return get_debug_type($reference); } } diff --git a/src/ReferenceResolverInterface.php b/src/ReferenceResolverInterface.php index 25916e85..c818b4e2 100644 --- a/src/ReferenceResolverInterface.php +++ b/src/ReferenceResolverInterface.php @@ -36,6 +36,28 @@ interface ReferenceResolverInterface */ public function setContainer(?ContainerInterface $container): void; + /** + * Adds the given parameter resolver(s) to the parameter resolutioner + * + * @param ParameterResolverInterface ...$resolvers + * + * @return void + * + * @since 3.0.0 + */ + public function addParameterResolver(ParameterResolverInterface ...$resolvers): void; + + /** + * Adds the given response resolver(s) to the response resolutioner + * + * @param ResponseResolverInterface ...$resolvers + * + * @return void + * + * @since 3.0.0 + */ + public function addResponseResolver(ResponseResolverInterface ...$resolvers): void; + /** * Resolves the given reference to a request handler * diff --git a/src/ResponseResolutioner.php b/src/ResponseResolutioner.php index f5147350..b85ce085 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolutioner.php @@ -82,7 +82,7 @@ public function resolveResponse($response): ResponseInterface } throw new ResponseResolvingException(sprintf( - 'Unexpected response {%s}', + 'Unable to resolve the response {%s}', get_debug_type($response) )); } diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 512e23e2..05dd4214 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -79,7 +79,7 @@ public function __construct( * * @since 2.9.0 */ - public function setContainer(?ContainerInterface $container) : void + public function setContainer(?ContainerInterface $container): void { $this->referenceResolver->setContainer($container); } @@ -89,7 +89,7 @@ public function setContainer(?ContainerInterface $container) : void * * @return RouteCollectionInterface */ - public function getCollection() : RouteCollectionInterface + public function getCollection(): RouteCollectionInterface { return $this->collection; } @@ -359,7 +359,7 @@ public function purge( * @throws InvalidReferenceException * If one of the given middlewares cannot be resolved. */ - public function group(callable $callback, array $middlewares = []) : RouteCollectionInterface + public function group(callable $callback, array $middlewares = []): RouteCollectionInterface { $collector = new self( $this->collectionFactory, diff --git a/src/RouteFactoryInterface.php b/src/RouteFactoryInterface.php index 86541d9b..8bd56fdf 100644 --- a/src/RouteFactoryInterface.php +++ b/src/RouteFactoryInterface.php @@ -28,9 +28,9 @@ interface RouteFactoryInterface * * @param string $name * @param string $path - * @param string[] $methods + * @param list $methods * @param RequestHandlerInterface $requestHandler - * @param MiddlewareInterface[] $middlewares + * @param list $middlewares * @param array $attributes * * @return RouteInterface diff --git a/src/RouteInterface.php b/src/RouteInterface.php index 935bc6f0..dbc66928 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -41,7 +41,7 @@ interface RouteInterface extends RequestHandlerInterface * * @return string */ - public function getName() : string; + public function getName(): string; /** * Gets the route host @@ -50,42 +50,42 @@ public function getName() : string; * * @since 2.6.0 */ - public function getHost() : ?string; + public function getHost(): ?string; /** * Gets the route path * * @return string */ - public function getPath() : string; + public function getPath(): string; /** * Gets the route methods * * @return string[] */ - public function getMethods() : array; + public function getMethods(): array; /** * Gets the route request handler * * @return RequestHandlerInterface */ - public function getRequestHandler() : RequestHandlerInterface; + public function getRequestHandler(): RequestHandlerInterface; /** * Gets the route middlewares * * @return MiddlewareInterface[] */ - public function getMiddlewares() : array; + public function getMiddlewares(): array; /** * Gets the route attributes * * @return array */ - public function getAttributes() : array; + public function getAttributes(): array; /** * Gets the route summary @@ -94,7 +94,7 @@ public function getAttributes() : array; * * @since 2.4.0 */ - public function getSummary() : string; + public function getSummary(): string; /** * Gets the route description @@ -103,7 +103,7 @@ public function getSummary() : string; * * @since 2.4.0 */ - public function getDescription() : string; + public function getDescription(): string; /** * Gets the route tags @@ -112,7 +112,7 @@ public function getDescription() : string; * * @since 2.4.0 */ - public function getTags() : array; + public function getTags(): array; /** * Gets the route holder @@ -121,7 +121,7 @@ public function getTags() : array; * * @since 2.14.0 */ - public function getHolder() : Reflector; + public function getHolder(): Reflector; /** * Sets the given name to the route @@ -130,7 +130,7 @@ public function getHolder() : Reflector; * * @return RouteInterface */ - public function setName(string $name) : RouteInterface; + public function setName(string $name): RouteInterface; /** * Sets the given host to the route @@ -141,7 +141,7 @@ public function setName(string $name) : RouteInterface; * * @since 2.6.0 */ - public function setHost(?string $host) : RouteInterface; + public function setHost(?string $host): RouteInterface; /** * Sets the given path to the route @@ -150,7 +150,7 @@ public function setHost(?string $host) : RouteInterface; * * @return RouteInterface */ - public function setPath(string $path) : RouteInterface; + public function setPath(string $path): RouteInterface; /** * Sets the given method(s) to the route @@ -159,7 +159,7 @@ public function setPath(string $path) : RouteInterface; * * @return RouteInterface */ - public function setMethods(string ...$methods) : RouteInterface; + public function setMethods(string ...$methods): RouteInterface; /** * Sets the given request handler to the route @@ -168,7 +168,7 @@ public function setMethods(string ...$methods) : RouteInterface; * * @return RouteInterface */ - public function setRequestHandler(RequestHandlerInterface $requestHandler) : RouteInterface; + public function setRequestHandler(RequestHandlerInterface $requestHandler): RouteInterface; /** * Sets the given middleware(s) to the route @@ -177,7 +177,7 @@ public function setRequestHandler(RequestHandlerInterface $requestHandler) : Rou * * @return RouteInterface */ - public function setMiddlewares(MiddlewareInterface ...$middlewares) : RouteInterface; + public function setMiddlewares(MiddlewareInterface ...$middlewares): RouteInterface; /** * Sets the given attributes to the route @@ -186,7 +186,7 @@ public function setMiddlewares(MiddlewareInterface ...$middlewares) : RouteInter * * @return RouteInterface */ - public function setAttributes(array $attributes) : RouteInterface; + public function setAttributes(array $attributes): RouteInterface; /** * Sets the given summary to the route @@ -197,7 +197,7 @@ public function setAttributes(array $attributes) : RouteInterface; * * @since 2.4.0 */ - public function setSummary(string $summary) : RouteInterface; + public function setSummary(string $summary): RouteInterface; /** * Sets the given description to the route @@ -208,7 +208,7 @@ public function setSummary(string $summary) : RouteInterface; * * @since 2.4.0 */ - public function setDescription(string $description) : RouteInterface; + public function setDescription(string $description): RouteInterface; /** * Sets the given tag(s) to the route @@ -219,7 +219,7 @@ public function setDescription(string $description) : RouteInterface; * * @since 2.4.0 */ - public function setTags(string ...$tags) : RouteInterface; + public function setTags(string ...$tags): RouteInterface; /** * Adds the given prefix to the route path @@ -228,7 +228,7 @@ public function setTags(string ...$tags) : RouteInterface; * * @return RouteInterface */ - public function addPrefix(string $prefix) : RouteInterface; + public function addPrefix(string $prefix): RouteInterface; /** * Adds the given suffix to the route path @@ -237,7 +237,7 @@ public function addPrefix(string $prefix) : RouteInterface; * * @return RouteInterface */ - public function addSuffix(string $suffix) : RouteInterface; + public function addSuffix(string $suffix): RouteInterface; /** * Adds the given method(s) to the route @@ -246,7 +246,7 @@ public function addSuffix(string $suffix) : RouteInterface; * * @return RouteInterface */ - public function addMethod(string ...$methods) : RouteInterface; + public function addMethod(string ...$methods): RouteInterface; /** * Adds the given middleware(s) to the route @@ -255,7 +255,7 @@ public function addMethod(string ...$methods) : RouteInterface; * * @return RouteInterface */ - public function addMiddleware(MiddlewareInterface ...$middlewares) : RouteInterface; + public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterface; /** * Returns the route clone with the given attributes @@ -266,5 +266,5 @@ public function addMiddleware(MiddlewareInterface ...$middlewares) : RouteInterf * * @return RouteInterface */ - public function withAddedAttributes(array $attributes) : RouteInterface; + public function withAddedAttributes(array $attributes): RouteInterface; } diff --git a/src/Router.php b/src/Router.php index b59a6764..2dda44c8 100644 --- a/src/Router.php +++ b/src/Router.php @@ -102,7 +102,7 @@ class Router implements MiddlewareInterface, RequestHandlerInterface, RequestMet * * @since 2.13.0 */ - private $eventDispatcher = null; + private ?EventDispatcherInterface $eventDispatcher = null; /** * Gets the router's host table @@ -111,7 +111,7 @@ class Router implements MiddlewareInterface, RequestHandlerInterface, RequestMet * * @since 2.6.0 */ - public function getHosts() : array + public function getHosts(): array { return $this->hosts; } @@ -125,7 +125,7 @@ public function getHosts() : array * * @since 2.14.0 */ - public function resolveHostname(string $hostname) : ?string + public function resolveHostname(string $hostname): ?string { foreach ($this->hosts as $alias => $hostnames) { foreach ($hostnames as $value) { @@ -143,7 +143,7 @@ public function resolveHostname(string $hostname) : ?string * * @return RouteInterface[] */ - public function getRoutes() : array + public function getRoutes(): array { $routes = []; foreach ($this->routes as $route) { @@ -162,7 +162,7 @@ public function getRoutes() : array * * @since 2.14.0 */ - public function getRoutesByHostname(string $hostname) : array + public function getRoutesByHostname(string $hostname): array { // the hostname's alias. $alias = $this->resolveHostname($hostname); @@ -183,7 +183,7 @@ public function getRoutesByHostname(string $hostname) : array * * @return MiddlewareInterface[] */ - public function getMiddlewares() : array + public function getMiddlewares(): array { $middlewares = []; foreach ($this->middlewares as $middleware) { @@ -198,7 +198,7 @@ public function getMiddlewares() : array * * @return RouteInterface|null */ - public function getMatchedRoute() : ?RouteInterface + public function getMatchedRoute(): ?RouteInterface { return $this->matchedRoute; } @@ -210,7 +210,7 @@ public function getMatchedRoute() : ?RouteInterface * * @since 2.13.0 */ - public function getEventDispatcher() : ?EventDispatcherInterface + public function getEventDispatcher(): ?EventDispatcherInterface { return $this->eventDispatcher; } @@ -235,7 +235,7 @@ public function getEventDispatcher() : ?EventDispatcherInterface * * @since 2.11.0 */ - public function addPatterns(array $patterns) : void + public function addPatterns(array $patterns): void { foreach ($patterns as $alias => $pattern) { self::$patterns[$alias] = $pattern; @@ -262,7 +262,7 @@ public function addPatterns(array $patterns) : void * * @since 2.11.0 */ - public function addHosts(array $hosts) : void + public function addHosts(array $hosts): void { foreach ($hosts as $alias => $hostnames) { $this->addHost($alias, ...$hostnames); @@ -279,7 +279,7 @@ public function addHosts(array $hosts) : void * * @since 2.6.0 */ - public function addHost(string $alias, string ...$hostnames) : void + public function addHost(string $alias, string ...$hostnames): void { $this->hosts[$alias] = $hostnames; } @@ -294,7 +294,7 @@ public function addHost(string $alias, string ...$hostnames) : void * @throws InvalidArgumentException * if one of the given routes already exists. */ - public function addRoute(RouteInterface ...$routes) : void + public function addRoute(RouteInterface ...$routes): void { foreach ($routes as $route) { $name = $route->getName(); @@ -319,7 +319,7 @@ public function addRoute(RouteInterface ...$routes) : void * @throws InvalidArgumentException * if one of the given middlewares already exists. */ - public function addMiddleware(MiddlewareInterface ...$middlewares) : void + public function addMiddleware(MiddlewareInterface ...$middlewares): void { foreach ($middlewares as $middleware) { $hash = spl_object_hash($middleware); @@ -343,7 +343,7 @@ public function addMiddleware(MiddlewareInterface ...$middlewares) : void * * @since 2.13.0 */ - public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher) : void + public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher): void { $this->eventDispatcher = $eventDispatcher; } @@ -353,7 +353,7 @@ public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher) : * * @return string[] */ - public function getAllowedMethods() : array + public function getAllowedMethods(): array { $methods = []; foreach ($this->routes as $route) { @@ -370,7 +370,7 @@ public function getAllowedMethods() : array * * @return bool */ - public function hasRoute(string $name) : bool + public function hasRoute(string $name): bool { return isset($this->routes[$name]); } @@ -384,7 +384,7 @@ public function hasRoute(string $name) : bool * * @throws RouteNotFoundException */ - public function getRoute(string $name) : RouteInterface + public function getRoute(string $name): RouteInterface { if (!isset($this->routes[$name])) { throw new RouteNotFoundException(sprintf( @@ -412,7 +412,7 @@ public function getRoute(string $name) : RouteInterface * If a required attribute value is not given, * or if an attribute value is not valid in strict mode. */ - public function generateUri(string $name, array $attributes = [], bool $strict = false) : string + public function generateUri(string $name, array $attributes = [], bool $strict = false): string { $route = $this->getRoute($name); @@ -431,7 +431,7 @@ public function generateUri(string $name, array $attributes = [], bool $strict = * @throws PageNotFoundException * @throws MethodNotAllowedException */ - public function match(ServerRequestInterface $request) : RouteInterface + public function match(ServerRequestInterface $request): RouteInterface { $currentUri = $request->getUri(); $currentHost = $currentUri->getHost(); @@ -481,7 +481,7 @@ public function match(ServerRequestInterface $request) : RouteInterface * * @since 2.8.0 */ - public function run(ServerRequestInterface $request) : ResponseInterface + public function run(ServerRequestInterface $request): ResponseInterface { // lazy resolving of the given request... $routing = new class implements RequestHandlerInterface @@ -513,7 +513,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface /** * {@inheritdoc} */ - public function handle(ServerRequestInterface $request) : ResponseInterface + public function handle(ServerRequestInterface $request): ResponseInterface { $route = $this->match($request); $this->matchedRoute = $route; @@ -536,7 +536,7 @@ public function handle(ServerRequestInterface $request) : ResponseInterface /** * {@inheritdoc} */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { try { return $this->handle($request); @@ -552,7 +552,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface * * @return void */ - public function load(LoaderInterface ...$loaders) : void + public function load(LoaderInterface ...$loaders): void { foreach ($loaders as $loader) { $this->addRoute(...$loader->load()->all()); diff --git a/src/RouterBuilder.php b/src/RouterBuilder.php index a0dbbeb9..598662f8 100644 --- a/src/RouterBuilder.php +++ b/src/RouterBuilder.php @@ -83,7 +83,7 @@ final class RouterBuilder * * @since 2.14.0 */ - public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher) : self + public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher): self { $this->eventDispatcher = $eventDispatcher; @@ -97,7 +97,7 @@ public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher) : * * @return self */ - public function setContainer(?ContainerInterface $container) : self + public function setContainer(?ContainerInterface $container): self { $this->container = $container; @@ -111,7 +111,7 @@ public function setContainer(?ContainerInterface $container) : self * * @return self */ - public function setCache(?CacheInterface $cache) : self + public function setCache(?CacheInterface $cache): self { $this->cache = $cache; @@ -127,7 +127,7 @@ public function setCache(?CacheInterface $cache) : self * * @since 2.10.0 */ - public function setCacheKey(?string $cacheKey) : self + public function setCacheKey(?string $cacheKey): self { $this->cacheKey = $cacheKey; @@ -141,7 +141,7 @@ public function setCacheKey(?string $cacheKey) : self * * @return self */ - public function useConfigLoader(array $resources) : self + public function useConfigLoader(array $resources): self { $this->configLoader = new Loader\ConfigLoader(); $this->configLoader->attachArray($resources); @@ -156,7 +156,7 @@ public function useConfigLoader(array $resources) : self * * @return self */ - public function useDescriptorLoader(array $resources) : self + public function useDescriptorLoader(array $resources): self { $this->descriptorLoader = new Loader\DescriptorLoader(); $this->descriptorLoader->attachArray($resources); @@ -173,7 +173,7 @@ public function useDescriptorLoader(array $resources) : self * * @return self */ - public function useMetadataLoader(array $resources) : self + public function useMetadataLoader(array $resources): self { $this->useDescriptorLoader($resources); @@ -189,7 +189,7 @@ public function useMetadataLoader(array $resources) : self * * @since 2.11.0 */ - public function setPatterns(?array $patterns) : self + public function setPatterns(?array $patterns): self { $this->patterns = $patterns; @@ -203,7 +203,7 @@ public function setPatterns(?array $patterns) : self * * @return self */ - public function setHosts(?array $hosts) : self + public function setHosts(?array $hosts): self { $this->hosts = $hosts; @@ -217,7 +217,7 @@ public function setHosts(?array $hosts) : self * * @return self */ - public function setMiddlewares(?array $middlewares) : self + public function setMiddlewares(?array $middlewares): self { $this->middlewares = $middlewares; @@ -229,7 +229,7 @@ public function setMiddlewares(?array $middlewares) : self * * @return Router */ - public function build() : Router + public function build(): Router { $router = new Router(); From a5b25bb4c623e25f9e96e8aef6b5414964b95a08 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 22 Jan 2023 05:55:44 +0100 Subject: [PATCH 032/180] v3 --- src/Annotation/RequestQuery.php | 25 +++++ src/Loader/DescriptorLoader.php | 4 +- .../JsonPayloadDecodingMiddleware.php | 16 +-- .../WhitespaceStrippingMiddleware.php | 56 ++++++++++ .../RequestQueryParameterResolver.php | 105 ++++++++++++++++++ .../UnsafeCallableRequestHandler.php | 54 +++++++++ src/RequestQueryInterface.php | 21 ++++ src/RouteCollector.php | 2 +- src/Router.php | 34 +++--- 9 files changed, 285 insertions(+), 32 deletions(-) create mode 100644 src/Annotation/RequestQuery.php create mode 100644 src/Middleware/WhitespaceStrippingMiddleware.php create mode 100644 src/ParameterResolver/RequestQueryParameterResolver.php create mode 100644 src/RequestHandler/UnsafeCallableRequestHandler.php create mode 100644 src/RequestQueryInterface.php diff --git a/src/Annotation/RequestQuery.php b/src/Annotation/RequestQuery.php new file mode 100644 index 00000000..e2aa1e52 --- /dev/null +++ b/src/Annotation/RequestQuery.php @@ -0,0 +1,25 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Annotation; + +/** + * Import classes + */ +use Attribute; + +/** + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_PARAMETER)] +final class RequestQuery +{ +} diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index d48120ef..eaa7fb92 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -50,7 +50,7 @@ use function is_dir; use function is_string; use function usort; -use Sunrise\Http\Router\find_classes; +use function Sunrise\Http\Router\find_classes; /** * Import constants @@ -347,7 +347,7 @@ private function collectDescriptors(): array } } - usort($result, function (Route $a, Route $b): int { + usort($result, static function (Route $a, Route $b): int { return $b->priority <=> $a->priority; }); diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 70740f04..3cee875d 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -25,7 +25,6 @@ * Import functions */ use function is_array; -use function is_object; use function json_decode; use function rtrim; use function sprintf; @@ -106,28 +105,23 @@ private function getRequestMediaType(ServerRequestInterface $request): ?string * * @param ServerRequestInterface $request * - * @return array|object|null + * @return array|null * * @throws InvalidPayloadException * If the request's "JSON" payload cannot be decoded. */ - private function decodeRequestJsonPayload(ServerRequestInterface $request) + private function decodeRequestJsonPayload(ServerRequestInterface $request): ?array { // https://www.php.net/json.constants - $flags = JSON_OBJECT_AS_ARRAY | JSON_BIGINT_AS_STRING; + $flags = JSON_OBJECT_AS_ARRAY | JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR; try { /** @var mixed */ - $result = json_decode($request->getBody()->__toString(), null, 512, $flags | JSON_THROW_ON_ERROR); + $result = json_decode($request->getBody()->__toString(), null, 512, $flags); } catch (JsonException $e) { throw new InvalidPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); } - if (is_array($result) || - is_object($result)) { - return $result; - } - - return null; + return is_array($result) ? $result : null; } } diff --git a/src/Middleware/WhitespaceStrippingMiddleware.php b/src/Middleware/WhitespaceStrippingMiddleware.php new file mode 100644 index 00000000..83b6af11 --- /dev/null +++ b/src/Middleware/WhitespaceStrippingMiddleware.php @@ -0,0 +1,56 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Middleware; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +/** + * Import functions + */ +use function array_walk_recursive; +use function is_array; +use function is_string; +use function trim; + +/** + * WhitespaceStrippingMiddleware + * + * @since 3.0.0 + */ +final class WhitespaceStrippingMiddleware implements MiddlewareInterface +{ + + /** + * {@inheritdoc} + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $parsedBody = $request->getParsedBody(); + + if (!empty($parsedBody) && is_array($parsedBody)) { + /** @psalm-suppress MissingClosureParamType, MixedAssignment */ + array_walk_recursive($parsedBody, static function (&$value): void { + $value = is_string($value) ? trim($value) : $value; + }); + + $request = $request->withParsedBody($parsedBody); + } + + return $handler->handle($request); + } +} diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php new file mode 100644 index 00000000..017bb5a7 --- /dev/null +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -0,0 +1,105 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ParameterResolver; + +/** + * Import classes + */ +use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\Annotation\RequestQuery; +use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; +use Sunrise\Http\Router\Exception\ParameterResolvingException; +use Sunrise\Http\Router\ParameterResolverInterface; +use Sunrise\Http\Router\RequestQueryInterface; +use Sunrise\Hydrator\Exception\InvalidObjectException; +use Sunrise\Hydrator\Exception\InvalidValueException; +use Sunrise\Hydrator\HydratorInterface; +use ReflectionNamedType; +use ReflectionParameter; + +/** + * Import functions + */ +use function is_subclass_of; + +/** + * Import constants + */ +use const PHP_MAJOR_VERSION; + +/** + * RequestQueryParameterResolver + * + * @link https://github.com/sunrise-php/hydrator + * + * @since 3.0.0 + */ +final class RequestQueryParameterResolver implements ParameterResolverInterface +{ + + /** + * @var HydratorInterface + */ + private HydratorInterface $hydrator; + + /** + * @param HydratorInterface $hydrator + */ + public function __construct(HydratorInterface $hydrator) + { + $this->hydrator = $hydrator; + } + + /** + * {@inheritdoc} + */ + public function supportsParameter(ReflectionParameter $parameter, $context): bool + { + if (!($context instanceof ServerRequestInterface)) { + return false; + } + + if (!($parameter->getType() instanceof ReflectionNamedType) || $parameter->getType()->isBuiltin()) { + return false; + } + + if (8 === PHP_MAJOR_VERSION && $parameter->getAttributes(RequestQuery::class)) { + return true; + } + + if (is_subclass_of($parameter->getType()->getName(), RequestQueryInterface::class)) { + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function resolveParameter(ReflectionParameter $parameter, $context) + { + /** @var ServerRequestInterface */ + $context = $context; + + /** @var ReflectionNamedType */ + $parameterType = $parameter->getType(); + + try { + return $this->hydrator->hydrate($parameterType->getName(), $context->getQueryParams()); + } catch (InvalidObjectException $e) { + throw new ParameterResolvingException($e->getMessage(), 0, $e); + } catch (InvalidValueException $e) { + throw new HttpUnprocessableEntityException($e->getMessage(), 0, $e); + } + } +} diff --git a/src/RequestHandler/UnsafeCallableRequestHandler.php b/src/RequestHandler/UnsafeCallableRequestHandler.php new file mode 100644 index 00000000..36c46b15 --- /dev/null +++ b/src/RequestHandler/UnsafeCallableRequestHandler.php @@ -0,0 +1,54 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\RequestHandler; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; + +/** + * UnsafeCallableRequestHandler + * + * @since 3.0.0 + */ +final class UnsafeCallableRequestHandler implements RequestHandlerInterface +{ + + /** + * The handler callback + * + * @var callable + */ + private $callback; + + /** + * Constructor of the class + * + * @param callable $callback + */ + public function __construct(callable $callback) + { + $this->callback = $callback; + } + + /** + * {@inheritdoc} + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + /** @var ResponseInterface */ + return ($this->callback)($request); + } +} diff --git a/src/RequestQueryInterface.php b/src/RequestQueryInterface.php new file mode 100644 index 00000000..8b59053c --- /dev/null +++ b/src/RequestQueryInterface.php @@ -0,0 +1,21 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * RequestQueryInterface + * + * @since 3.0.0 + */ +interface RequestQueryInterface +{ +} diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 05dd4214..6fd0c634 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -99,7 +99,7 @@ public function getCollection(): RouteCollectionInterface * * @param string $name * @param string $path - * @param string[] $methods + * @param list $methods * @param mixed $requestHandler * @param array $middlewares * @param array $attributes diff --git a/src/Router.php b/src/Router.php index 2dda44c8..267d8703 100644 --- a/src/Router.php +++ b/src/Router.php @@ -25,8 +25,8 @@ use Sunrise\Http\Router\Exception\RouteNotFoundException; use Sunrise\Http\Router\Exception\MethodNotAllowedException; use Sunrise\Http\Router\Loader\LoaderInterface; -use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; +use Sunrise\Http\Router\RequestHandler\UnsafeCallableRequestHandler; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** @@ -484,20 +484,17 @@ public function match(ServerRequestInterface $request): RouteInterface public function run(ServerRequestInterface $request): ResponseInterface { // lazy resolving of the given request... - $routing = new class implements RequestHandlerInterface - { - public function handle(ServerRequestInterface $request): ResponseInterface - { - $route = $this->match($request); - $this->matchedRoute = $route; - - if (isset($this->eventDispatcher)) { - $this->eventDispatcher->dispatch(new RouteEvent($route, $request)); - } + $routing = new UnsafeCallableRequestHandler(function (ServerRequestInterface $request): ResponseInterface { + $this->matchedRoute = $this->match($request); - return $route->handle($request); + if (isset($this->eventDispatcher)) { + $this->eventDispatcher->dispatch( + new RouteEvent($this->matchedRoute, $request) + ); } - }; + + return $this->matchedRoute->handle($request); + }); $middlewares = $this->getMiddlewares(); if (empty($middlewares)) { @@ -515,19 +512,20 @@ public function handle(ServerRequestInterface $request): ResponseInterface */ public function handle(ServerRequestInterface $request): ResponseInterface { - $route = $this->match($request); - $this->matchedRoute = $route; + $this->matchedRoute = $this->match($request); if (isset($this->eventDispatcher)) { - $this->eventDispatcher->dispatch(new RouteEvent($route, $request)); + $this->eventDispatcher->dispatch( + new RouteEvent($this->matchedRoute, $request) + ); } $middlewares = $this->getMiddlewares(); if (empty($middlewares)) { - return $route->handle($request); + return $this->matchedRoute->handle($request); } - $handler = new QueueableRequestHandler($route); + $handler = new QueueableRequestHandler($this->matchedRoute); $handler->add(...$middlewares); return $handler->handle($request); From 8e6f50dc9327368cbc1d7287482f9039c16d42e3 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 22 Jan 2023 07:19:00 +0100 Subject: [PATCH 033/180] v3 --- composer.json | 1 - functions/find_classes.php | 62 ------------- src/Loader/ConfigLoader.php | 4 +- src/Loader/DescriptorLoader.php | 87 +++++++++++-------- .../JsonPayloadDecodingMiddleware.php | 2 +- 5 files changed, 56 insertions(+), 100 deletions(-) delete mode 100644 functions/find_classes.php diff --git a/composer.json b/composer.json index b19d2476..9c9cb8b8 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,6 @@ "autoload": { "files": [ "functions/emit.php", - "functions/find_classes.php", "functions/get_debug_type.php", "functions/path_build.php", "functions/path_match.php", diff --git a/functions/find_classes.php b/functions/find_classes.php deleted file mode 100644 index 755aa46b..00000000 --- a/functions/find_classes.php +++ /dev/null @@ -1,62 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router; - -/** - * Import classes - */ -use Iterator; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; -use SplFileInfo; - -/** - * Import functions - */ -use function array_diff; -use function get_declared_classes; - -/** - * Scans the given directory and returns the found classes - * - * @param string $directory - * - * @return class-string[] - * - * @since 3.0.0 - */ -function find_classes(string $directory): array -{ - static $cache = []; - - if (isset($cache[$directory])) { - return $cache[$directory]; - } - - /** @var Iterator */ - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($directory) - ); - - $knownClasses = get_declared_classes(); - - foreach ($files as $file) { - if ('php' === $file->getExtension()) { - /** @psalm-suppress UnresolvableInclude */ - require_once $file->getPathname(); - } - } - - $cache[$directory] = array_diff(get_declared_classes(), $knownClasses); - - return $cache[$directory]; -} diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 1694048a..fb6df141 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -142,7 +142,7 @@ public function attach($resource): void throw new InvalidLoaderResourceException(sprintf( 'Config route loader only handles file or directory paths, ' . - 'however the given resource "%s" is not as expected', + 'however the given resource "%s" is not one of them', is_string($resource) ? $resource : get_debug_type($resource) )); } @@ -170,7 +170,7 @@ public function load(): RouteCollectionInterface ); foreach ($this->resources as $filename) { - (function (string $filename) { + (function (string $filename): void { /** @psalm-suppress UnresolvableInclude */ require $filename; })->call($collector, $filename); diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index eaa7fb92..b3bc5c7c 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -36,21 +36,26 @@ use Sunrise\Http\Router\RouteCollectionInterface; use Sunrise\Http\Router\RouteFactory; use Sunrise\Http\Router\RouteFactoryInterface; +use Iterator; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; use ReflectionAttribute; use ReflectionClass; use ReflectionMethod; use Reflector; +use SplFileInfo; /** * Import functions */ +use function array_diff; use function class_exists; use function get_debug_type; +use function get_declared_classes; use function hash; use function is_dir; use function is_string; use function usort; -use function Sunrise\Http\Router\find_classes; /** * Import constants @@ -114,7 +119,7 @@ public function __construct( $this->routeFactory = $routeFactory ?? new RouteFactory(); $this->referenceResolver = $referenceResolver ?? new ReferenceResolver(); - if (PHP_MAJOR_VERSION < 8) { + if (PHP_MAJOR_VERSION === 7) { $this->useDefaultAnnotationReader(); } } @@ -227,7 +232,7 @@ public function setCacheKey(?string $cacheKey): void * @return void * * @throws LogicException - * If the doctrine/annotations package isn't installed. + * If the "doctrine/annotations" package isn't installed. * * @since 3.0.0 */ @@ -235,7 +240,7 @@ public function useDefaultAnnotationReader(): void { if (!class_exists(AnnotationReader::class)) { throw new LogicException( - 'Loading routes from descriptors requires the "doctrine/annotations" package that is not installed, ' . + 'Loading routes from descriptors requires an uninstalled "doctrine/annotations" package, ' . 'run the command "composer install doctrine/annotations" and try again' ); } @@ -254,9 +259,9 @@ public function attach($resource): void } if (is_string($resource) && is_dir($resource)) { - $classnames = find_classes($resource); - foreach ($classnames as $classname) { - $this->resources[] = $classname; + $classNames = $this->scandir($resource); + foreach ($classNames as $className) { + $this->resources[] = $className; } return; @@ -264,7 +269,7 @@ public function attach($resource): void throw new InvalidLoaderResourceException(sprintf( 'Descriptor route loader only handles class names or directory paths, ' . - 'however the given resource "%s" is not as expected', + 'however the given resource "%s" is not one of them', is_string($resource) ? $resource : get_debug_type($resource) )); } @@ -369,7 +374,7 @@ private function getDescriptorsFromClass(ReflectionClass $class): array return []; } - $descriptors = []; + $result = []; if ($class->isSubclassOf(RequestHandlerInterface::class)) { $annotations = $this->getAnnotationsFromClassOrMethod($class, Route::class); @@ -377,7 +382,7 @@ private function getDescriptorsFromClass(ReflectionClass $class): array $descriptor = $annotations[0]; $this->supplementDescriptorFromClassOrMethod($descriptor, $class); $descriptor->holder = $class->getName(); - $descriptors[] = $descriptor; + $result[] = $descriptor; } } @@ -393,36 +398,15 @@ private function getDescriptorsFromClass(ReflectionClass $class): array $this->supplementDescriptorFromClassOrMethod($descriptor, $class); $this->supplementDescriptorFromClassOrMethod($descriptor, $method); $descriptor->holder = [$class->getName(), $method->getName()]; - $descriptors[] = $descriptor; + $result[] = $descriptor; } } - return $descriptors; + return $result; } /** - * Supplements the given descriptor from the given class or method with data such as: - * - * - host - * - path prefix - * - path postfix - * - middlewares - * - * ```php - * #[Prefix('/api/v1')] - * class SomeController { - * - * #[Route('foo', path: '/foo')] - * public function foo() { - * // will be available at: /api/v1/foo - * } - * - * #[Route('bar', path: '/bar')] - * public function bar() { - * // will be available at: /api/v1/bar - * } - * } - * ``` + * Supplements the given descriptor from the given class or method * * @param Route $descriptor * @param ReflectionClass|ReflectionMethod $reflector @@ -489,4 +473,39 @@ private function getAnnotationsFromClassOrMethod(Reflector $reflector, string $a return $result; } + + /** + * Scans the given directory and returns the found classes + * + * @param string $directory + * + * @return class-string[] + */ + private function scandir(string $directory): array + { + /** @var array */ + static $cache = []; + + if (isset($cache[$directory])) { + return $cache[$directory]; + } + + $known = get_declared_classes(); + + /** @var Iterator */ + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory) + ); + + foreach ($files as $file) { + if ('php' === $file->getExtension()) { + /** @psalm-suppress UnresolvableInclude */ + require_once $file->getPathname(); + } + } + + $cache[$directory] = array_diff(get_declared_classes(), $known); + + return $cache[$directory]; + } } diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 3cee875d..5371d7da 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -113,7 +113,7 @@ private function getRequestMediaType(ServerRequestInterface $request): ?string private function decodeRequestJsonPayload(ServerRequestInterface $request): ?array { // https://www.php.net/json.constants - $flags = JSON_OBJECT_AS_ARRAY | JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR; + $flags = JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY | JSON_THROW_ON_ERROR; try { /** @var mixed */ From 554dcaf24360d6417c63880c9ed2bc7bb29960bd Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 22 Jan 2023 18:32:51 +0100 Subject: [PATCH 034/180] v3 --- src/Loader/ConfigLoader.php | 10 +- src/Loader/DescriptorLoader.php | 145 ++++++++---------- .../WhitespaceStrippingMiddleware.php | 1 + 3 files changed, 69 insertions(+), 87 deletions(-) diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index fb6df141..926f6309 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -126,11 +126,6 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo */ public function attach($resource): void { - if (is_string($resource) && is_file($resource)) { - $this->resources[] = $resource; - return; - } - if (is_string($resource) && is_dir($resource)) { $filenames = glob($resource . '/*.php'); foreach ($filenames as $filename) { @@ -140,6 +135,11 @@ public function attach($resource): void return; } + if (is_string($resource) && is_file($resource)) { + $this->resources[] = $resource; + return; + } + throw new InvalidLoaderResourceException(sprintf( 'Config route loader only handles file or directory paths, ' . 'however the given resource "%s" is not one of them', diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index b3bc5c7c..226c9efb 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -253,20 +253,20 @@ public function useDefaultAnnotationReader(): void */ public function attach($resource): void { - if (is_string($resource) && class_exists($resource)) { - $this->resources[] = $resource; - return; - } - if (is_string($resource) && is_dir($resource)) { - $classNames = $this->scandir($resource); - foreach ($classNames as $className) { - $this->resources[] = $className; + $classnames = $this->scandir($resource); + foreach ($classnames as $classname) { + $this->resources[] = $classname; } return; } + if (is_string($resource) && class_exists($resource)) { + $this->resources[] = $resource; + return; + } + throw new InvalidLoaderResourceException(sprintf( 'Descriptor route loader only handles class names or directory paths, ' . 'however the given resource "%s" is not one of them', @@ -290,9 +290,8 @@ public function attachArray(array $resources): void */ public function load(): RouteCollectionInterface { - $descriptors = $this->getDescriptors(); - $routes = []; + $descriptors = $this->getDescriptors(); foreach ($descriptors as $descriptor) { $routes[] = $this->routeFactory->createRoute( $descriptor->name, @@ -314,7 +313,7 @@ public function load(): RouteCollectionInterface /** * Gets descriptors from the cache if they are stored in it, * otherwise collects them from the loader resources, - * and then tries to cache and return them + * then tries to cache and return them * * @return list */ @@ -327,26 +326,13 @@ private function getDescriptors(): array return $this->cache->get($cacheKey); } - $descriptors = $this->collectDescriptors(); - - if (isset($this->cache)) { - $this->cache->set($cacheKey, $descriptors); - } - - return $descriptors; - } - - /** - * Collects and returns descriptors from the loader resources - * - * @return list - */ - private function collectDescriptors(): array - { $result = []; + foreach ($this->resources as $resource) { - $class = new ReflectionClass($resource); - $descriptors = $this->getDescriptorsFromClass($class); + $descriptors = $this->getClassDescriptors( + new ReflectionClass($resource) + ); + foreach ($descriptors as $descriptor) { $result[] = $descriptor; } @@ -356,6 +342,10 @@ private function collectDescriptors(): array return $b->priority <=> $a->priority; }); + if (isset($this->cache)) { + $this->cache->set($cacheKey, $result); + } + return $result; } @@ -366,7 +356,7 @@ private function collectDescriptors(): array * * @return list */ - private function getDescriptorsFromClass(ReflectionClass $class): array + private function getClassDescriptors(ReflectionClass $class): array { // e.g., interfaces, traits, enums, abstract classes, // classes with private constructor... @@ -377,11 +367,11 @@ private function getDescriptorsFromClass(ReflectionClass $class): array $result = []; if ($class->isSubclassOf(RequestHandlerInterface::class)) { - $annotations = $this->getAnnotationsFromClassOrMethod($class, Route::class); + $annotations = $this->getClassOrMethodAnnotations($class, Route::class); if (isset($annotations[0])) { $descriptor = $annotations[0]; - $this->supplementDescriptorFromClassOrMethod($descriptor, $class); $descriptor->holder = $class->getName(); + $this->supplementDescriptorFromClassOrMethod($descriptor, $class); $result[] = $descriptor; } } @@ -392,12 +382,12 @@ private function getDescriptorsFromClass(ReflectionClass $class): array continue; } - $annotations = $this->getAnnotationsFromClassOrMethod($method, Route::class); + $annotations = $this->getClassOrMethodAnnotations($method, Route::class); if (isset($annotations[0])) { $descriptor = $annotations[0]; + $descriptor->holder = [$class->getName(), $method->getName()]; $this->supplementDescriptorFromClassOrMethod($descriptor, $class); $this->supplementDescriptorFromClassOrMethod($descriptor, $method); - $descriptor->holder = [$class->getName(), $method->getName()]; $result[] = $descriptor; } } @@ -406,53 +396,22 @@ private function getDescriptorsFromClass(ReflectionClass $class): array } /** - * Supplements the given descriptor from the given class or method - * - * @param Route $descriptor - * @param ReflectionClass|ReflectionMethod $reflector - * - * @return void - */ - private function supplementDescriptorFromClassOrMethod(Route $descriptor, Reflector $reflector): void - { - $annotations = $this->getAnnotationsFromClassOrMethod($reflector, Host::class); - if (isset($annotations[0])) { - $descriptor->host = $annotations[0]->value; - } - - $annotations = $this->getAnnotationsFromClassOrMethod($reflector, Prefix::class); - if (isset($annotations[0])) { - $descriptor->path = $annotations[0]->value . $descriptor->path; - } - - $annotations = $this->getAnnotationsFromClassOrMethod($reflector, Postfix::class); - if (isset($annotations[0])) { - $descriptor->path = $descriptor->path . $annotations[0]->value; - } - - $annotations = $this->getAnnotationsFromClassOrMethod($reflector, Middleware::class); - foreach ($annotations as $annotation) { - $descriptor->middlewares[] = $annotation->value; - } - } - - /** - * Gets annotations from the given class or method + * Gets annotations from the given class or method by the given name * - * @param ReflectionClass|ReflectionMethod $reflector + * @param ReflectionClass|ReflectionMethod $reflection * @param class-string $annotationName * * @return list * * @template T */ - private function getAnnotationsFromClassOrMethod(Reflector $reflector, string $annotationName): array + private function getClassOrMethodAnnotations(Reflector $reflection, string $annotationName): array { $result = []; if (PHP_MAJOR_VERSION === 8) { /** @var ReflectionAttribute[] */ - $attributes = $reflector->getAttributes($annotationName); + $attributes = $reflection->getAttributes($annotationName); foreach ($attributes as $attribute) { /** @var T */ $result[] = $attribute->newInstance(); @@ -460,9 +419,9 @@ private function getAnnotationsFromClassOrMethod(Reflector $reflector, string $a } if (isset($this->annotationReader)) { - $annotations = ($reflector instanceof ReflectionClass) ? - $this->annotationReader->getClassAnnotations($reflector) : - $this->annotationReader->getMethodAnnotations($reflector); + $annotations = ($reflection instanceof ReflectionClass) ? + $this->annotationReader->getClassAnnotations($reflection) : + $this->annotationReader->getMethodAnnotations($reflection); foreach ($annotations as $annotation) { if ($annotation instanceof $annotationName) { @@ -474,6 +433,37 @@ private function getAnnotationsFromClassOrMethod(Reflector $reflector, string $a return $result; } + /** + * Supplements the given descriptor from the given class or method + * + * @param Route $descriptor + * @param ReflectionClass|ReflectionMethod $reflection + * + * @return void + */ + private function supplementDescriptorFromClassOrMethod(Route $descriptor, Reflector $reflection): void + { + $annotations = $this->getClassOrMethodAnnotations($reflection, Host::class); + if (isset($annotations[0])) { + $descriptor->host = $annotations[0]->value; + } + + $annotations = $this->getClassOrMethodAnnotations($reflection, Prefix::class); + if (isset($annotations[0])) { + $descriptor->path = $annotations[0]->value . $descriptor->path; + } + + $annotations = $this->getClassOrMethodAnnotations($reflection, Postfix::class); + if (isset($annotations[0])) { + $descriptor->path = $descriptor->path . $annotations[0]->value; + } + + $annotations = $this->getClassOrMethodAnnotations($reflection, Middleware::class); + foreach ($annotations as $annotation) { + $descriptor->middlewares[] = $annotation->value; + } + } + /** * Scans the given directory and returns the found classes * @@ -483,13 +473,6 @@ private function getAnnotationsFromClassOrMethod(Reflector $reflector, string $a */ private function scandir(string $directory): array { - /** @var array */ - static $cache = []; - - if (isset($cache[$directory])) { - return $cache[$directory]; - } - $known = get_declared_classes(); /** @var Iterator */ @@ -504,8 +487,6 @@ private function scandir(string $directory): array } } - $cache[$directory] = array_diff(get_declared_classes(), $known); - - return $cache[$directory]; + return array_diff(get_declared_classes(), $known); } } diff --git a/src/Middleware/WhitespaceStrippingMiddleware.php b/src/Middleware/WhitespaceStrippingMiddleware.php index 83b6af11..766e6de7 100644 --- a/src/Middleware/WhitespaceStrippingMiddleware.php +++ b/src/Middleware/WhitespaceStrippingMiddleware.php @@ -43,6 +43,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $parsedBody = $request->getParsedBody(); if (!empty($parsedBody) && is_array($parsedBody)) { + /** @psalm-suppress MissingClosureParamType, MixedAssignment */ array_walk_recursive($parsedBody, static function (&$value): void { $value = is_string($value) ? trim($value) : $value; From 037aa7bb66e91862578636e7041b8aacca075d50 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 22 Jan 2023 19:39:06 +0100 Subject: [PATCH 035/180] v3 --- .../InvalidLoaderResourceException.php | 19 -------------- src/Exception/InvalidPayloadException.php | 26 ------------------- src/Loader/ConfigLoader.php | 17 +++++++----- src/Loader/DescriptorLoader.php | 25 +++++++++++------- src/Loader/LoaderInterface.php | 6 ++--- .../JsonPayloadDecodingMiddleware.php | 6 ++--- 6 files changed, 32 insertions(+), 67 deletions(-) delete mode 100644 src/Exception/InvalidLoaderResourceException.php delete mode 100644 src/Exception/InvalidPayloadException.php diff --git a/src/Exception/InvalidLoaderResourceException.php b/src/Exception/InvalidLoaderResourceException.php deleted file mode 100644 index 42301f93..00000000 --- a/src/Exception/InvalidLoaderResourceException.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception; - -/** - * InvalidLoaderResourceException - */ -class InvalidLoaderResourceException extends InvalidArgumentException -{ -} diff --git a/src/Exception/InvalidPayloadException.php b/src/Exception/InvalidPayloadException.php deleted file mode 100644 index b10d32de..00000000 --- a/src/Exception/InvalidPayloadException.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception; - -/** - * Import classes - */ -use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; - -/** - * InvalidPayloadException - * - * @since 3.0.0 - */ -class InvalidPayloadException extends HttpBadRequestException -{ -} diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 926f6309..bb9c030a 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -15,7 +15,7 @@ * Import classes */ use Psr\Container\ContainerInterface; -use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; +use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; @@ -30,7 +30,6 @@ /** * Import functions */ -use function get_debug_type; use function glob; use function is_dir; use function is_file; @@ -126,7 +125,13 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo */ public function attach($resource): void { - if (is_string($resource) && is_dir($resource)) { + if (!is_string($resource)) { + throw new InvalidArgumentException( + 'Config route loader only handles string resources' + ); + } + + if (is_dir($resource)) { $filenames = glob($resource . '/*.php'); foreach ($filenames as $filename) { $this->resources[] = $filename; @@ -135,15 +140,15 @@ public function attach($resource): void return; } - if (is_string($resource) && is_file($resource)) { + if (is_file($resource)) { $this->resources[] = $resource; return; } - throw new InvalidLoaderResourceException(sprintf( + throw new InvalidArgumentException(sprintf( 'Config route loader only handles file or directory paths, ' . 'however the given resource "%s" is not one of them', - is_string($resource) ? $resource : get_debug_type($resource) + $resource )); } diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 226c9efb..e2e7fe59 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -25,7 +25,7 @@ use Sunrise\Http\Router\Annotation\Postfix; use Sunrise\Http\Router\Annotation\Prefix; use Sunrise\Http\Router\Annotation\Route; -use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; +use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; @@ -50,7 +50,6 @@ */ use function array_diff; use function class_exists; -use function get_debug_type; use function get_declared_classes; use function hash; use function is_dir; @@ -253,7 +252,13 @@ public function useDefaultAnnotationReader(): void */ public function attach($resource): void { - if (is_string($resource) && is_dir($resource)) { + if (!is_string($resource)) { + throw new InvalidArgumentException( + 'Descriptor route loader only handles string resources' + ); + } + + if (is_dir($resource)) { $classnames = $this->scandir($resource); foreach ($classnames as $classname) { $this->resources[] = $classname; @@ -262,15 +267,15 @@ public function attach($resource): void return; } - if (is_string($resource) && class_exists($resource)) { + if (class_exists($resource)) { $this->resources[] = $resource; return; } - throw new InvalidLoaderResourceException(sprintf( + throw new InvalidArgumentException(sprintf( 'Descriptor route loader only handles class names or directory paths, ' . 'however the given resource "%s" is not one of them', - is_string($resource) ? $resource : get_debug_type($resource) + $resource )); } @@ -319,11 +324,11 @@ public function load(): RouteCollectionInterface */ private function getDescriptors(): array { - $cacheKey = $this->getCacheKey(); + $key = $this->getCacheKey(); - if (isset($this->cache) && $this->cache->has($cacheKey)) { + if (isset($this->cache) && $this->cache->has($key)) { /** @var list */ - return $this->cache->get($cacheKey); + return $this->cache->get($key); } $result = []; @@ -343,7 +348,7 @@ private function getDescriptors(): array }); if (isset($this->cache)) { - $this->cache->set($cacheKey, $result); + $this->cache->set($key, $result); } return $result; diff --git a/src/Loader/LoaderInterface.php b/src/Loader/LoaderInterface.php index b268edc4..be6408b4 100644 --- a/src/Loader/LoaderInterface.php +++ b/src/Loader/LoaderInterface.php @@ -14,7 +14,7 @@ /** * Import classes */ -use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; +use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\RouteCollectionInterface; /** @@ -30,7 +30,7 @@ interface LoaderInterface * * @return void * - * @throws InvalidLoaderResourceException + * @throws InvalidArgumentException * If the given resource isn't valid. */ public function attach($resource): void; @@ -42,7 +42,7 @@ public function attach($resource): void; * * @return void * - * @throws InvalidLoaderResourceException + * @throws InvalidArgumentException * If one of the given resources isn't valid. */ public function attachArray(array $resources): void; diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 5371d7da..71e7516d 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -19,7 +19,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\InvalidPayloadException; +use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; /** * Import functions @@ -107,7 +107,7 @@ private function getRequestMediaType(ServerRequestInterface $request): ?string * * @return array|null * - * @throws InvalidPayloadException + * @throws HttpBadRequestException * If the request's "JSON" payload cannot be decoded. */ private function decodeRequestJsonPayload(ServerRequestInterface $request): ?array @@ -119,7 +119,7 @@ private function decodeRequestJsonPayload(ServerRequestInterface $request): ?arr /** @var mixed */ $result = json_decode($request->getBody()->__toString(), null, 512, $flags); } catch (JsonException $e) { - throw new InvalidPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); + throw new HttpBadRequestException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); } return is_array($result) ? $result : null; From 5024f93511df8793c388f37dda8764688dcaf1b6 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 22 Jan 2023 22:05:56 +0100 Subject: [PATCH 036/180] v3 --- src/Exception/InvalidRequestBodyException.php | 26 +++++++++++++++++++ .../InvalidRequestPayloadException.php | 26 +++++++++++++++++++ .../InvalidRequestQueryException.php | 26 +++++++++++++++++++ .../JsonPayloadDecodingMiddleware.php | 6 ++--- .../RequestBodyParameterResolver.php | 13 ++++++---- .../RequestQueryParameterResolver.php | 13 ++++++---- 6 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 src/Exception/InvalidRequestBodyException.php create mode 100644 src/Exception/InvalidRequestPayloadException.php create mode 100644 src/Exception/InvalidRequestQueryException.php diff --git a/src/Exception/InvalidRequestBodyException.php b/src/Exception/InvalidRequestBodyException.php new file mode 100644 index 00000000..3e1135a4 --- /dev/null +++ b/src/Exception/InvalidRequestBodyException.php @@ -0,0 +1,26 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; + +/** + * InvalidRequestBodyException + * + * @since 3.0.0 + */ +class InvalidRequestBodyException extends HttpBadRequestException +{ +} diff --git a/src/Exception/InvalidRequestPayloadException.php b/src/Exception/InvalidRequestPayloadException.php new file mode 100644 index 00000000..24c8c478 --- /dev/null +++ b/src/Exception/InvalidRequestPayloadException.php @@ -0,0 +1,26 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; + +/** + * InvalidRequestPayloadException + * + * @since 3.0.0 + */ +class InvalidRequestPayloadException extends HttpBadRequestException +{ +} diff --git a/src/Exception/InvalidRequestQueryException.php b/src/Exception/InvalidRequestQueryException.php new file mode 100644 index 00000000..6f7a6499 --- /dev/null +++ b/src/Exception/InvalidRequestQueryException.php @@ -0,0 +1,26 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; + +/** + * InvalidRequestQueryException + * + * @since 3.0.0 + */ +class InvalidRequestQueryException extends HttpBadRequestException +{ +} diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 71e7516d..e566b60c 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -19,7 +19,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; +use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; /** * Import functions @@ -107,7 +107,7 @@ private function getRequestMediaType(ServerRequestInterface $request): ?string * * @return array|null * - * @throws HttpBadRequestException + * @throws InvalidRequestPayloadException * If the request's "JSON" payload cannot be decoded. */ private function decodeRequestJsonPayload(ServerRequestInterface $request): ?array @@ -119,7 +119,7 @@ private function decodeRequestJsonPayload(ServerRequestInterface $request): ?arr /** @var mixed */ $result = json_decode($request->getBody()->__toString(), null, 512, $flags); } catch (JsonException $e) { - throw new HttpBadRequestException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); + throw new InvalidRequestPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); } return is_array($result) ? $result : null; diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index f70a4593..9b699d4f 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -16,8 +16,7 @@ */ use Psr\Http\Message\ServerRequestInterface; use Sunrise\Http\Router\Annotation\RequestBody; -use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; -use Sunrise\Http\Router\Exception\ParameterResolvingException; +use Sunrise\Http\Router\Exception\InvalidRequestBodyException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\RequestBodyInterface; use Sunrise\Hydrator\Exception\InvalidObjectException; @@ -85,6 +84,12 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} + * + * @throws InvalidObjectException + * If the DTO isn't valid. + * + * @throws InvalidRequestBodyException + * If the DTO cannot be hydrated with the request body. */ public function resolveParameter(ReflectionParameter $parameter, $context) { @@ -96,10 +101,8 @@ public function resolveParameter(ReflectionParameter $parameter, $context) try { return $this->hydrator->hydrate($parameterType->getName(), (array) $context->getParsedBody()); - } catch (InvalidObjectException $e) { - throw new ParameterResolvingException($e->getMessage(), 0, $e); } catch (InvalidValueException $e) { - throw new HttpUnprocessableEntityException($e->getMessage(), 0, $e); + throw new InvalidRequestBodyException($e->getMessage(), 0, $e); } } } diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php index 017bb5a7..93980eb8 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -16,8 +16,7 @@ */ use Psr\Http\Message\ServerRequestInterface; use Sunrise\Http\Router\Annotation\RequestQuery; -use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; -use Sunrise\Http\Router\Exception\ParameterResolvingException; +use Sunrise\Http\Router\Exception\InvalidRequestQueryException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\RequestQueryInterface; use Sunrise\Hydrator\Exception\InvalidObjectException; @@ -85,6 +84,12 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} + * + * @throws InvalidObjectException + * If the DTO isn't valid. + * + * @throws InvalidRequestQueryException + * If the DTO cannot be hydrated with the request query. */ public function resolveParameter(ReflectionParameter $parameter, $context) { @@ -96,10 +101,8 @@ public function resolveParameter(ReflectionParameter $parameter, $context) try { return $this->hydrator->hydrate($parameterType->getName(), $context->getQueryParams()); - } catch (InvalidObjectException $e) { - throw new ParameterResolvingException($e->getMessage(), 0, $e); } catch (InvalidValueException $e) { - throw new HttpUnprocessableEntityException($e->getMessage(), 0, $e); + throw new InvalidRequestQueryException($e->getMessage(), 0, $e); } } } From 8ee8bb3de1ace473a3e686670ab6f94688bb63f2 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Mon, 23 Jan 2023 20:06:16 +0100 Subject: [PATCH 037/180] v3 --- src/AnnotationReader.php | 117 +++++++++++++++++ src/Exception/ParameterResolvingException.php | 21 ---- src/Exception/ResolvingException.php | 2 +- src/Exception/ResponseResolvingException.php | 21 ---- src/Loader/ConfigLoader.php | 4 +- src/Loader/DescriptorLoader.php | 119 ++++++------------ src/Middleware/CallableMiddleware.php | 15 ++- .../JsonPayloadDecodingMiddleware.php | 2 +- src/ParameterResolutioner.php | 111 +++++++++------- src/ParameterResolutionerInterface.php | 23 +--- .../DependencyInjectionParameterResolver.php | 73 +++++++++++ .../KnownTypeParameterResolver.php | 93 ++++++++++++++ .../RequestBodyParameterResolver.php | 17 ++- .../RequestQueryParameterResolver.php | 17 ++- src/ParameterResolverInterface.php | 4 +- src/ReferenceResolver.php | 2 - src/RequestHandler/CallableRequestHandler.php | 13 +- .../QueueableRequestHandler.php | 2 +- src/ResponseResolutioner.php | 4 +- src/ResponseResolutionerInterface.php | 4 +- src/ResponseResolverInterface.php | 4 +- 21 files changed, 444 insertions(+), 224 deletions(-) create mode 100644 src/AnnotationReader.php delete mode 100644 src/Exception/ParameterResolvingException.php delete mode 100644 src/Exception/ResponseResolvingException.php create mode 100644 src/ParameterResolver/DependencyInjectionParameterResolver.php create mode 100644 src/ParameterResolver/KnownTypeParameterResolver.php diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php new file mode 100644 index 00000000..6ecd595d --- /dev/null +++ b/src/AnnotationReader.php @@ -0,0 +1,117 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use Doctrine\Common\Annotations\AnnotationReader as DoctrineAnnotationReader; +use Doctrine\Common\Annotations\Reader as DoctrineAnnotationReaderInterface; +use Sunrise\Http\Router\Exception\LogicException; +use ReflectionAttribute; +use ReflectionClass; +use ReflectionMethod; +use Reflector; + +/** + * Import functions + */ +use function class_exists; + +/** + * Import constants + */ +use const PHP_MAJOR_VERSION; + +/** + * AnnotationReader + * + * @since 3.0.0 + */ +final class AnnotationReader +{ + + /** + * @var DoctrineAnnotationReaderInterface|null + */ + private ?DoctrineAnnotationReaderInterface $annotationReader = null; + + /** + * Sets the given annotation reader to the reader + * + * @param DoctrineAnnotationReaderInterface|null $annotationReader + * + * @return void + */ + public function setAnnotationReader(?DoctrineAnnotationReaderInterface $annotationReader): void + { + $this->annotationReader = $annotationReader; + } + + /** + * Sets the default annotation reader to the reader + * + * @return void + * + * @throws LogicException + * If the "doctrine/annotations" package isn't installed. + */ + public function useDefaultAnnotationReader(): void + { + if (!class_exists(DoctrineAnnotationReader::class)) { + throw new LogicException( + 'The annotations reading logic requires an uninstalled "doctrine/annotations" package, ' . + 'run the command "composer install doctrine/annotations" and try again' + ); + } + + $this->setAnnotationReader(new DoctrineAnnotationReader()); + } + + /** + * Gets annotations by the given name from the given class or method + * + * @param ReflectionClass|ReflectionMethod $classOrMethod + * @param class-string $annotationName + * + * @return list + * + * @template T + */ + public function getAnnotations(Reflector $classOrMethod, string $annotationName): array + { + $result = []; + + if (PHP_MAJOR_VERSION === 8) { + /** @var ReflectionAttribute[] */ + $attributes = $classOrMethod->getAttributes($annotationName); + foreach ($attributes as $attribute) { + /** @var T */ + $result[] = $attribute->newInstance(); + } + } + + if (isset($this->annotationReader)) { + $annotations = ($classOrMethod instanceof ReflectionClass) ? + $this->annotationReader->getClassAnnotations($classOrMethod) : + $this->annotationReader->getMethodAnnotations($classOrMethod); + + foreach ($annotations as $annotation) { + if ($annotation instanceof $annotationName) { + $result[] = $annotation; + } + } + } + + return $result; + } +} diff --git a/src/Exception/ParameterResolvingException.php b/src/Exception/ParameterResolvingException.php deleted file mode 100644 index ad85b9cf..00000000 --- a/src/Exception/ParameterResolvingException.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception; - -/** - * ParameterResolvingException - * - * @since 3.0.0 - */ -class ParameterResolvingException extends ResolvingException -{ -} diff --git a/src/Exception/ResolvingException.php b/src/Exception/ResolvingException.php index 4fafb2a5..85953a58 100644 --- a/src/Exception/ResolvingException.php +++ b/src/Exception/ResolvingException.php @@ -16,6 +16,6 @@ * * @since 3.0.0 */ -class ResolvingException extends Exception +class ResolvingException extends LogicException { } diff --git a/src/Exception/ResponseResolvingException.php b/src/Exception/ResponseResolvingException.php deleted file mode 100644 index 6b4db132..00000000 --- a/src/Exception/ResponseResolvingException.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception; - -/** - * ResponseResolvingException - * - * @since 3.0.0 - */ -class ResponseResolvingException extends ResolvingException -{ -} diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index bb9c030a..3a44852e 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -127,7 +127,7 @@ public function attach($resource): void { if (!is_string($resource)) { throw new InvalidArgumentException( - 'Config route loader only handles string resources' + 'The config route loader only handles string resources' ); } @@ -146,7 +146,7 @@ public function attach($resource): void } throw new InvalidArgumentException(sprintf( - 'Config route loader only handles file or directory paths, ' . + 'The config route loader only handles file or directory paths, ' . 'however the given resource "%s" is not one of them', $resource )); diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index e2e7fe59..e9002683 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -14,10 +14,8 @@ /** * Import classes */ -use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\Reader as AnnotationReaderInterface; use Psr\Container\ContainerInterface; -use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\SimpleCache\CacheInterface; use Sunrise\Http\Router\Annotation\Host; @@ -26,7 +24,7 @@ use Sunrise\Http\Router\Annotation\Prefix; use Sunrise\Http\Router\Annotation\Route; use Sunrise\Http\Router\Exception\InvalidArgumentException; -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\AnnotationReader; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; @@ -39,7 +37,6 @@ use Iterator; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; -use ReflectionAttribute; use ReflectionClass; use ReflectionMethod; use Reflector; @@ -64,7 +61,7 @@ /** * DescriptorLoader */ -class DescriptorLoader implements LoaderInterface +final class DescriptorLoader implements LoaderInterface { /** @@ -88,9 +85,9 @@ class DescriptorLoader implements LoaderInterface private ReferenceResolverInterface $referenceResolver; /** - * @var AnnotationReaderInterface|null + * @var AnnotationReader */ - private ?AnnotationReaderInterface $annotationReader = null; + private AnnotationReader $annotationReader; /** * @var CacheInterface|null @@ -118,8 +115,10 @@ public function __construct( $this->routeFactory = $routeFactory ?? new RouteFactory(); $this->referenceResolver = $referenceResolver ?? new ReferenceResolver(); - if (PHP_MAJOR_VERSION === 7) { - $this->useDefaultAnnotationReader(); + $this->annotationReader = new AnnotationReader(); + + if (8 > PHP_MAJOR_VERSION) { + $this->annotationReader->useDefaultAnnotationReader(); } } @@ -164,7 +163,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo } /** - * Sets the given annotation reader to the loader + * Sets the given annotation reader to the descriptor loader * * @param AnnotationReaderInterface|null $annotationReader * @@ -174,7 +173,19 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo */ public function setAnnotationReader(?AnnotationReaderInterface $annotationReader): void { - $this->annotationReader = $annotationReader; + $this->annotationReader->setAnnotationReader($annotationReader); + } + + /** + * Uses the default annotation reader + * + * @return void + * + * @since 3.0.0 + */ + public function useDefaultAnnotationReader(): void + { + $this->annotationReader->useDefaultAnnotationReader(); } /** @@ -225,28 +236,6 @@ public function setCacheKey(?string $cacheKey): void $this->cacheKey = $cacheKey; } - /** - * Uses the default annotation reader - * - * @return void - * - * @throws LogicException - * If the "doctrine/annotations" package isn't installed. - * - * @since 3.0.0 - */ - public function useDefaultAnnotationReader(): void - { - if (!class_exists(AnnotationReader::class)) { - throw new LogicException( - 'Loading routes from descriptors requires an uninstalled "doctrine/annotations" package, ' . - 'run the command "composer install doctrine/annotations" and try again' - ); - } - - $this->setAnnotationReader(new AnnotationReader()); - } - /** * {@inheritdoc} */ @@ -254,7 +243,7 @@ public function attach($resource): void { if (!is_string($resource)) { throw new InvalidArgumentException( - 'Descriptor route loader only handles string resources' + 'The descriptor route loader only handles string resources' ); } @@ -273,7 +262,7 @@ public function attach($resource): void } throw new InvalidArgumentException(sprintf( - 'Descriptor route loader only handles class names or directory paths, ' . + 'The descriptor route loader only handles class names or directory paths, ' . 'however the given resource "%s" is not one of them', $resource )); @@ -372,11 +361,11 @@ private function getClassDescriptors(ReflectionClass $class): array $result = []; if ($class->isSubclassOf(RequestHandlerInterface::class)) { - $annotations = $this->getClassOrMethodAnnotations($class, Route::class); + $annotations = $this->annotationReader->getAnnotations($class, Route::class); if (isset($annotations[0])) { $descriptor = $annotations[0]; $descriptor->holder = $class->getName(); - $this->supplementDescriptorFromClassOrMethod($descriptor, $class); + $this->supplementDescriptor($descriptor, $class); $result[] = $descriptor; } } @@ -387,12 +376,12 @@ private function getClassDescriptors(ReflectionClass $class): array continue; } - $annotations = $this->getClassOrMethodAnnotations($method, Route::class); + $annotations = $this->annotationReader->getAnnotations($method, Route::class); if (isset($annotations[0])) { $descriptor = $annotations[0]; $descriptor->holder = [$class->getName(), $method->getName()]; - $this->supplementDescriptorFromClassOrMethod($descriptor, $class); - $this->supplementDescriptorFromClassOrMethod($descriptor, $method); + $this->supplementDescriptor($descriptor, $class); + $this->supplementDescriptor($descriptor, $method); $result[] = $descriptor; } } @@ -400,70 +389,32 @@ private function getClassDescriptors(ReflectionClass $class): array return $result; } - /** - * Gets annotations from the given class or method by the given name - * - * @param ReflectionClass|ReflectionMethod $reflection - * @param class-string $annotationName - * - * @return list - * - * @template T - */ - private function getClassOrMethodAnnotations(Reflector $reflection, string $annotationName): array - { - $result = []; - - if (PHP_MAJOR_VERSION === 8) { - /** @var ReflectionAttribute[] */ - $attributes = $reflection->getAttributes($annotationName); - foreach ($attributes as $attribute) { - /** @var T */ - $result[] = $attribute->newInstance(); - } - } - - if (isset($this->annotationReader)) { - $annotations = ($reflection instanceof ReflectionClass) ? - $this->annotationReader->getClassAnnotations($reflection) : - $this->annotationReader->getMethodAnnotations($reflection); - - foreach ($annotations as $annotation) { - if ($annotation instanceof $annotationName) { - $result[] = $annotation; - } - } - } - - return $result; - } - /** * Supplements the given descriptor from the given class or method * * @param Route $descriptor - * @param ReflectionClass|ReflectionMethod $reflection + * @param ReflectionClass|ReflectionMethod $source * * @return void */ - private function supplementDescriptorFromClassOrMethod(Route $descriptor, Reflector $reflection): void + private function supplementDescriptor(Route $descriptor, Reflector $source): void { - $annotations = $this->getClassOrMethodAnnotations($reflection, Host::class); + $annotations = $this->annotationReader->getAnnotations($source, Host::class); if (isset($annotations[0])) { $descriptor->host = $annotations[0]->value; } - $annotations = $this->getClassOrMethodAnnotations($reflection, Prefix::class); + $annotations = $this->annotationReader->getAnnotations($source, Prefix::class); if (isset($annotations[0])) { $descriptor->path = $annotations[0]->value . $descriptor->path; } - $annotations = $this->getClassOrMethodAnnotations($reflection, Postfix::class); + $annotations = $this->annotationReader->getAnnotations($source, Postfix::class); if (isset($annotations[0])) { $descriptor->path = $descriptor->path . $annotations[0]->value; } - $annotations = $this->getClassOrMethodAnnotations($reflection, Middleware::class); + $annotations = $this->annotationReader->getAnnotations($source, Middleware::class); foreach ($annotations as $annotation) { $descriptor->middlewares[] = $annotation->value; } diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index e100ffa9..5d8d3a57 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -14,15 +14,16 @@ /** * Import classes */ -use ReflectionFunctionAbstract; -use ReflectionFunction; -use ReflectionMethod; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\ParameterResolver\KnownTypeParameterResolver; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ResponseResolutionerInterface; +use ReflectionFunctionAbstract; +use ReflectionFunction; +use ReflectionMethod; /** * Import functions @@ -92,10 +93,14 @@ public function getReflection(): ReflectionFunctionAbstract */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + $resolvers = [ + new KnownTypeParameterResolver(ServerRequestInterface::class, $request), + new KnownTypeParameterResolver(RequestHandlerInterface::class, $handler), + ]; + $arguments = $this->parameterResolutioner ->withContext($request) - ->withType(ServerRequestInterface::class, $request) - ->withType(RequestHandlerInterface::class, $handler) + ->withPriorityResolver(...$resolvers) ->resolveParameters(...$this->getReflection()->getParameters()); /** @var mixed */ diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index e566b60c..b853d822 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -14,12 +14,12 @@ /** * Import classes */ -use JsonException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; +use JsonException; /** * Import functions diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index d85fcd14..090cf505 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -14,10 +14,10 @@ /** * Import classes */ -use ReflectionNamedType; +use Sunrise\Http\Router\Exception\LogicException; +use ReflectionFunctionAbstract; +use ReflectionMethod; use ReflectionParameter; -use Psr\Container\ContainerInterface; -use Sunrise\Http\Router\Exception\ParameterResolvingException; /** * Import functions @@ -39,13 +39,6 @@ final class ParameterResolutioner implements ParameterResolutionerInterface */ private $context = null; - /** - * Known types - * - * @var array - */ - private array $types = []; - /** * The resolutioner's resolvers * @@ -53,13 +46,6 @@ final class ParameterResolutioner implements ParameterResolutionerInterface */ private array $resolvers = []; - /** - * The resolutioner's container - * - * @var ContainerInterface|null - */ - private ?ContainerInterface $container = null; - /** * {@inheritdoc} */ @@ -74,10 +60,16 @@ public function withContext($context): ParameterResolutionerInterface /** * {@inheritdoc} */ - public function withType(string $type, object $value): ParameterResolutionerInterface + public function withPriorityResolver(ParameterResolverInterface ...$resolvers): ParameterResolutionerInterface { + /** @var list $resolvers */ + + foreach ($this->resolvers as $resolver) { + $resolvers[] = $resolver; + } + $clone = clone $this; - $clone->types[$type] = $value; + $clone->resolvers = $resolvers; return $clone; } @@ -92,14 +84,6 @@ public function addResolver(ParameterResolverInterface ...$resolvers): void } } - /** - * {@inheritdoc} - */ - public function setContainer(?ContainerInterface $container): void - { - $this->container = $container; - } - /** * {@inheritdoc} */ @@ -122,40 +106,75 @@ public function resolveParameters(ReflectionParameter ...$parameters): array * @return mixed * The ready-to-pass argument. * - * @throws ParameterResolvingException + * @throws LogicException * If the parameter cannot be resolved to an argument. */ private function resolveParameter(ReflectionParameter $parameter) { - $type = $parameter->getType(); - - if (($type instanceof ReflectionNamedType) && !$type->isBuiltin()) { - if (isset($this->types[$type->getName()])) { - return $this->types[$type->getName()]; - } - } - foreach ($this->resolvers as $resolver) { if ($resolver->supportsParameter($parameter, $this->context)) { return $resolver->resolveParameter($parameter, $this->context); } } - if (($type instanceof ReflectionNamedType) && !$type->isBuiltin()) { - if (isset($this->container) && $this->container->has($type->getName())) { - return $this->container->get($type->getName()); - } - } - if ($parameter->isDefaultValueAvailable()) { return $parameter->getDefaultValue(); } - throw new ParameterResolvingException(sprintf( - 'Unable to resolve the parameter {%s($%s[%d])}', - $parameter->getDeclaringFunction()->getName(), + throw new LogicException(sprintf( + 'Unable to resolve the parameter {%s}', + $this->stringifyParameter($parameter) + )); + } + + /** + * Stringifies the given parameter + * + * @param ReflectionParameter $parameter + * + * @return string + */ + private function stringifyParameter(ReflectionParameter $parameter): string + { + return ($parameter->getDeclaringFunction() instanceof ReflectionMethod) ? + $this->stringifyMethodParameter($parameter->getDeclaringFunction(), $parameter) : + $this->stringifyFunctionParameter($parameter->getDeclaringFunction(), $parameter); + } + + /** + * Stringifies the given method parameter + * + * @param ReflectionMethod $method + * @param ReflectionParameter $parameter + * + * @return string + */ + private function stringifyMethodParameter(ReflectionMethod $method, ReflectionParameter $parameter): string + { + return sprintf( + '%s::%s($%s[%d])', + $method->getDeclaringClass()->getName(), + $method->getName(), $parameter->getName(), $parameter->getPosition() - )); + ); + } + + /** + * Stringifies the given function parameter + * + * @param ReflectionFunctionAbstract $function + * @param ReflectionParameter $parameter + * + * @return string + */ + private function stringifyFunctionParameter(ReflectionFunctionAbstract $function, ReflectionParameter $parameter): string + { + return sprintf( + '%s($%s[%d])', + $function->getName(), + $parameter->getName(), + $parameter->getPosition() + ); } } diff --git a/src/ParameterResolutionerInterface.php b/src/ParameterResolutionerInterface.php index c4f1d3e2..d81c9313 100644 --- a/src/ParameterResolutionerInterface.php +++ b/src/ParameterResolutionerInterface.php @@ -14,9 +14,8 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\LogicException; use ReflectionParameter; -use Psr\Container\ContainerInterface; -use Sunrise\Http\Router\Exception\ParameterResolvingException; /** * ParameterResolutionerInterface @@ -38,18 +37,15 @@ interface ParameterResolutionerInterface public function withContext($context): ParameterResolutionerInterface; /** - * Creates a new instance of the resolutioner with the given data for resolve a non-built-in typed parameter + * Creates a new instance of the resolutioner with the given priority parameter resolver(s) * * Please note that this method MUST NOT change the object state. * - * @param class-string $type - * @param T $value + * @param ParameterResolverInterface ...$resolvers * * @return static - * - * @template T */ - public function withType(string $type, object $value): ParameterResolutionerInterface; + public function withPriorityResolver(ParameterResolverInterface ...$resolvers): ParameterResolutionerInterface; /** * Adds the given parameter resolver(s) to the resolutioner @@ -60,15 +56,6 @@ public function withType(string $type, object $value): ParameterResolutionerInte */ public function addResolver(ParameterResolverInterface ...$resolvers): void; - /** - * Sets the given container to the resolutioner for resolve non-built-in typed parameters - * - * @param ContainerInterface|null $container - * - * @return void - */ - public function setContainer(?ContainerInterface $container): void; - /** * Resolves the given parameter(s) to arguments * @@ -77,7 +64,7 @@ public function setContainer(?ContainerInterface $container): void; * @return list * List of ready-to-pass arguments. * - * @throws ParameterResolvingException + * @throws LogicException * If one of the parameters cannot be resolved to an argument. */ public function resolveParameters(ReflectionParameter ...$parameters): array; diff --git a/src/ParameterResolver/DependencyInjectionParameterResolver.php b/src/ParameterResolver/DependencyInjectionParameterResolver.php new file mode 100644 index 00000000..38eb7df1 --- /dev/null +++ b/src/ParameterResolver/DependencyInjectionParameterResolver.php @@ -0,0 +1,73 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ParameterResolver; + +/** + * Import classes + */ +use Psr\Container\ContainerInterface; +use Sunrise\Http\Router\ParameterResolverInterface; +use ReflectionNamedType; +use ReflectionParameter; + +/** + * DependencyInjectionParameterResolver + * + * @since 3.0.0 + */ +final class DependencyInjectionParameterResolver implements ParameterResolverInterface +{ + + /** + * @var ContainerInterface + */ + private ContainerInterface $container; + + /** + * @param ContainerInterface $container + */ + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + /** + * {@inheritdoc} + */ + public function supportsParameter(ReflectionParameter $parameter, $context): bool + { + if (!($parameter->getType() instanceof ReflectionNamedType)) { + return false; + } + + if ($parameter->getType()->isBuiltin()) { + return false; + } + + if (!$this->container->has($parameter->getType()->getName())) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function resolveParameter(ReflectionParameter $parameter, $context) + { + /** @var ReflectionNamedType */ + $parameterType = $parameter->getType(); + + return $this->container->get($parameterType->getName()); + } +} diff --git a/src/ParameterResolver/KnownTypeParameterResolver.php b/src/ParameterResolver/KnownTypeParameterResolver.php new file mode 100644 index 00000000..f531c328 --- /dev/null +++ b/src/ParameterResolver/KnownTypeParameterResolver.php @@ -0,0 +1,93 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ParameterResolver; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\InvalidArgumentException; +use Sunrise\Http\Router\ParameterResolverInterface; +use ReflectionNamedType; +use ReflectionParameter; + +/** + * Import classes + */ +use function get_class; +use function sprintf; + +/** + * KnownTypeParameterResolver + * + * @template T as object + * + * @since 3.0.0 + */ +final class KnownTypeParameterResolver implements ParameterResolverInterface +{ + + /** + * @var class-string + */ + private string $type; + + /** + * @var T + */ + private object $value; + + /** + * @param class-string $type + * @param T $value + * + * @throws InvalidArgumentException + * If the given value is not an instance of the given type. + */ + public function __construct(string $type, object $value) + { + if (!($value instanceof $type)) { + throw new InvalidArgumentException(sprintf( + 'The known type parameter resolver cannot accept the value "%s" ' . + 'because it is not an instance of the "%s"', + get_class($value), + $type + )); + } + + $this->type = $type; + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function supportsParameter(ReflectionParameter $parameter, $context): bool + { + if (!($parameter->getType() instanceof ReflectionNamedType)) { + return false; + } + + if ($parameter->getType()->isBuiltin()) { + return false; + } + + return $this->type === $parameter->getType()->getName(); + } + + /** + * {@inheritdoc} + */ + public function resolveParameter(ReflectionParameter $parameter, $context) + { + return $this->value; + } +} diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index 9b699d4f..6abe86ab 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -17,6 +17,7 @@ use Psr\Http\Message\ServerRequestInterface; use Sunrise\Http\Router\Annotation\RequestBody; use Sunrise\Http\Router\Exception\InvalidRequestBodyException; +use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\RequestBodyInterface; use Sunrise\Hydrator\Exception\InvalidObjectException; @@ -67,7 +68,11 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo return false; } - if (!($parameter->getType() instanceof ReflectionNamedType) || $parameter->getType()->isBuiltin()) { + if (!($parameter->getType() instanceof ReflectionNamedType)) { + return false; + } + + if ($parameter->getType()->isBuiltin()) { return false; } @@ -85,11 +90,11 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} * - * @throws InvalidObjectException - * If the DTO isn't valid. - * * @throws InvalidRequestBodyException - * If the DTO cannot be hydrated with the request body. + * If the request body isn't valid. + * + * @throws LogicException + * If the DTO isn't valid. */ public function resolveParameter(ReflectionParameter $parameter, $context) { @@ -103,6 +108,8 @@ public function resolveParameter(ReflectionParameter $parameter, $context) return $this->hydrator->hydrate($parameterType->getName(), (array) $context->getParsedBody()); } catch (InvalidValueException $e) { throw new InvalidRequestBodyException($e->getMessage(), 0, $e); + } catch (InvalidObjectException $e) { + throw new LogicException($e->getMessage(), 0, $e); } } } diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php index 93980eb8..91f5039a 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -17,6 +17,7 @@ use Psr\Http\Message\ServerRequestInterface; use Sunrise\Http\Router\Annotation\RequestQuery; use Sunrise\Http\Router\Exception\InvalidRequestQueryException; +use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\RequestQueryInterface; use Sunrise\Hydrator\Exception\InvalidObjectException; @@ -67,7 +68,11 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo return false; } - if (!($parameter->getType() instanceof ReflectionNamedType) || $parameter->getType()->isBuiltin()) { + if (!($parameter->getType() instanceof ReflectionNamedType)) { + return false; + } + + if ($parameter->getType()->isBuiltin()) { return false; } @@ -85,11 +90,11 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} * - * @throws InvalidObjectException - * If the DTO isn't valid. - * * @throws InvalidRequestQueryException - * If the DTO cannot be hydrated with the request query. + * If the request query isn't valid. + * + * @throws LogicException + * If the DTO isn't valid. */ public function resolveParameter(ReflectionParameter $parameter, $context) { @@ -103,6 +108,8 @@ public function resolveParameter(ReflectionParameter $parameter, $context) return $this->hydrator->hydrate($parameterType->getName(), $context->getQueryParams()); } catch (InvalidValueException $e) { throw new InvalidRequestQueryException($e->getMessage(), 0, $e); + } catch (InvalidObjectException $e) { + throw new LogicException($e->getMessage(), 0, $e); } } } diff --git a/src/ParameterResolverInterface.php b/src/ParameterResolverInterface.php index 0a6de0e8..41cf3ef5 100644 --- a/src/ParameterResolverInterface.php +++ b/src/ParameterResolverInterface.php @@ -14,8 +14,8 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\LogicException; use ReflectionParameter; -use Sunrise\Http\Router\Exception\ParameterResolvingException; /** * ParameterResolverInterface @@ -44,7 +44,7 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo * @return mixed * The ready-to-pass argument. * - * @throws ParameterResolvingException + * @throws LogicException * If the parameter cannot be resolved to an argument. */ public function resolveParameter(ReflectionParameter $parameter, $context); diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index 80361180..6761a53e 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -82,8 +82,6 @@ public function __construct( public function setContainer(?ContainerInterface $container): void { $this->container = $container; - - $this->parameterResolutioner->setContainer($container); } /** diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index aaf00728..4c17c11c 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -14,14 +14,15 @@ /** * Import classes */ -use ReflectionFunctionAbstract; -use ReflectionFunction; -use ReflectionMethod; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\ParameterResolver\KnownTypeParameterResolver; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ResponseResolutionerInterface; +use ReflectionFunctionAbstract; +use ReflectionFunction; +use ReflectionMethod; /** * Import functions @@ -89,9 +90,13 @@ public function getReflection(): ReflectionFunctionAbstract */ public function handle(ServerRequestInterface $request): ResponseInterface { + $resolvers = [ + new KnownTypeParameterResolver(ServerRequestInterface::class, $request), + ]; + $arguments = $this->parameterResolutioner ->withContext($request) - ->withType(ServerRequestInterface::class, $request) + ->withPriorityResolver(...$resolvers) ->resolveParameters(...$this->getReflection()->getParameters()); /** @var mixed */ diff --git a/src/RequestHandler/QueueableRequestHandler.php b/src/RequestHandler/QueueableRequestHandler.php index 62f62a4b..00282bff 100644 --- a/src/RequestHandler/QueueableRequestHandler.php +++ b/src/RequestHandler/QueueableRequestHandler.php @@ -14,11 +14,11 @@ /** * Import classes */ -use SplQueue; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use SplQueue; /** * QueueableRequestHandler diff --git a/src/ResponseResolutioner.php b/src/ResponseResolutioner.php index b85ce085..f845f7d8 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolutioner.php @@ -15,7 +15,7 @@ * Import classes */ use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Exception\ResponseResolvingException; +use Sunrise\Http\Router\Exception\LogicException; /** * Import functions @@ -81,7 +81,7 @@ public function resolveResponse($response): ResponseInterface } } - throw new ResponseResolvingException(sprintf( + throw new LogicException(sprintf( 'Unable to resolve the response {%s}', get_debug_type($response) )); diff --git a/src/ResponseResolutionerInterface.php b/src/ResponseResolutionerInterface.php index f737802c..6c36b0e9 100644 --- a/src/ResponseResolutionerInterface.php +++ b/src/ResponseResolutionerInterface.php @@ -15,7 +15,7 @@ * Import classes */ use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Exception\ResponseResolvingException; +use Sunrise\Http\Router\Exception\LogicException; /** * ResponseResolutionerInterface @@ -52,7 +52,7 @@ public function addResolver(ResponseResolverInterface ...$resolvers): void; * * @return ResponseInterface * - * @throws ResponseResolvingException + * @throws LogicException * If the raw response cannot be resolved to the object. */ public function resolveResponse($response): ResponseInterface; diff --git a/src/ResponseResolverInterface.php b/src/ResponseResolverInterface.php index fa6fd515..77d2a6fa 100644 --- a/src/ResponseResolverInterface.php +++ b/src/ResponseResolverInterface.php @@ -15,7 +15,7 @@ * Import classes */ use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Exception\ResponseResolvingException; +use Sunrise\Http\Router\Exception\LogicException; /** * ResponseResolverInterface @@ -43,7 +43,7 @@ public function supportsResponse($response, $context): bool; * * @return ResponseInterface * - * @throws ResponseResolvingException + * @throws LogicException * If the raw response cannot be resolved to the object. */ public function resolveResponse($response, $context): ResponseInterface; From ecc0719488be21d5913ac05049f7ae1ae0aafc87 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Tue, 24 Jan 2023 03:13:06 +0100 Subject: [PATCH 038/180] v3 --- ...on.php => ResolvingParameterException.php} | 4 +- src/Exception/ResolvingReferenceException.php | 21 ++++++++++ ...ion.php => ResolvingResponseException.php} | 4 +- src/Loader/DescriptorLoader.php | 12 +++--- .../JsonPayloadDecodingMiddleware.php | 12 +++--- src/ParameterResolutioner.php | 6 +-- src/ParameterResolutionerInterface.php | 4 +- .../RequestBodyParameterResolver.php | 12 +++--- .../RequestQueryParameterResolver.php | 12 +++--- src/ParameterResolverInterface.php | 4 +- src/ReferenceResolver.php | 42 ++++++++++++------- src/ReferenceResolverInterface.php | 12 +++--- src/ResponseResolutioner.php | 4 +- src/ResponseResolutionerInterface.php | 4 +- src/ResponseResolverInterface.php | 4 +- src/RouteCollector.php | 28 ------------- 16 files changed, 94 insertions(+), 91 deletions(-) rename src/Exception/{ReferenceResolvingException.php => ResolvingParameterException.php} (81%) create mode 100644 src/Exception/ResolvingReferenceException.php rename src/Exception/{InvalidReferenceException.php => ResolvingResponseException.php} (81%) diff --git a/src/Exception/ReferenceResolvingException.php b/src/Exception/ResolvingParameterException.php similarity index 81% rename from src/Exception/ReferenceResolvingException.php rename to src/Exception/ResolvingParameterException.php index 9a8387b7..3e46f9e4 100644 --- a/src/Exception/ReferenceResolvingException.php +++ b/src/Exception/ResolvingParameterException.php @@ -12,10 +12,10 @@ namespace Sunrise\Http\Router\Exception; /** - * ReferenceResolvingException + * ResolvingParameterException * * @since 3.0.0 */ -class ReferenceResolvingException extends ResolvingException +class ResolvingParameterException extends ResolvingException { } diff --git a/src/Exception/ResolvingReferenceException.php b/src/Exception/ResolvingReferenceException.php new file mode 100644 index 00000000..63ddb6f9 --- /dev/null +++ b/src/Exception/ResolvingReferenceException.php @@ -0,0 +1,21 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * ResolvingReferenceException + * + * @since 3.0.0 + */ +class ResolvingReferenceException extends ResolvingException +{ +} diff --git a/src/Exception/InvalidReferenceException.php b/src/Exception/ResolvingResponseException.php similarity index 81% rename from src/Exception/InvalidReferenceException.php rename to src/Exception/ResolvingResponseException.php index a099da3d..266aaca9 100644 --- a/src/Exception/InvalidReferenceException.php +++ b/src/Exception/ResolvingResponseException.php @@ -12,10 +12,10 @@ namespace Sunrise\Http\Router\Exception; /** - * InvalidReferenceException + * ResolvingResponseException * * @since 3.0.0 */ -class InvalidReferenceException extends LogicException +class ResolvingResponseException extends ResolvingException { } diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index e9002683..1197d6a7 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -393,28 +393,28 @@ private function getClassDescriptors(ReflectionClass $class): array * Supplements the given descriptor from the given class or method * * @param Route $descriptor - * @param ReflectionClass|ReflectionMethod $source + * @param ReflectionClass|ReflectionMethod $classOrMethod * * @return void */ - private function supplementDescriptor(Route $descriptor, Reflector $source): void + private function supplementDescriptor(Route $descriptor, Reflector $classOrMethod): void { - $annotations = $this->annotationReader->getAnnotations($source, Host::class); + $annotations = $this->annotationReader->getAnnotations($classOrMethod, Host::class); if (isset($annotations[0])) { $descriptor->host = $annotations[0]->value; } - $annotations = $this->annotationReader->getAnnotations($source, Prefix::class); + $annotations = $this->annotationReader->getAnnotations($classOrMethod, Prefix::class); if (isset($annotations[0])) { $descriptor->path = $annotations[0]->value . $descriptor->path; } - $annotations = $this->annotationReader->getAnnotations($source, Postfix::class); + $annotations = $this->annotationReader->getAnnotations($classOrMethod, Postfix::class); if (isset($annotations[0])) { $descriptor->path = $descriptor->path . $annotations[0]->value; } - $annotations = $this->annotationReader->getAnnotations($source, Middleware::class); + $annotations = $this->annotationReader->getAnnotations($classOrMethod, Middleware::class); foreach ($annotations as $annotation) { $descriptor->middlewares[] = $annotation->value; } diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index b853d822..e77ded54 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -26,10 +26,10 @@ */ use function is_array; use function json_decode; -use function rtrim; use function sprintf; use function strpos; -use function substr; +use function strstr; +use function trim; /** * Import constants @@ -91,13 +91,13 @@ private function getRequestMediaType(ServerRequestInterface $request): ?string } // type "/" subtype *( OWS ";" OWS parameter ) - $mediatype = $request->getHeaderLine('Content-Type'); + $mediaType = $request->getHeaderLine('Content-Type'); - if ($semicolon = strpos($mediatype, ';')) { - $mediatype = substr($mediatype, 0, $semicolon); + if (false !== strpos($mediaType, ';')) { + $mediaType = strstr($mediaType, ';', true); } - return rtrim($mediatype); + return trim($mediaType); } /** diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index 090cf505..15fad058 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -14,7 +14,7 @@ /** * Import classes */ -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\ResolvingParameterException; use ReflectionFunctionAbstract; use ReflectionMethod; use ReflectionParameter; @@ -106,7 +106,7 @@ public function resolveParameters(ReflectionParameter ...$parameters): array * @return mixed * The ready-to-pass argument. * - * @throws LogicException + * @throws ResolvingParameterException * If the parameter cannot be resolved to an argument. */ private function resolveParameter(ReflectionParameter $parameter) @@ -121,7 +121,7 @@ private function resolveParameter(ReflectionParameter $parameter) return $parameter->getDefaultValue(); } - throw new LogicException(sprintf( + throw new ResolvingParameterException(sprintf( 'Unable to resolve the parameter {%s}', $this->stringifyParameter($parameter) )); diff --git a/src/ParameterResolutionerInterface.php b/src/ParameterResolutionerInterface.php index d81c9313..1d04ea3e 100644 --- a/src/ParameterResolutionerInterface.php +++ b/src/ParameterResolutionerInterface.php @@ -14,7 +14,7 @@ /** * Import classes */ -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\ResolvingParameterException; use ReflectionParameter; /** @@ -64,7 +64,7 @@ public function addResolver(ParameterResolverInterface ...$resolvers): void; * @return list * List of ready-to-pass arguments. * - * @throws LogicException + * @throws ResolvingParameterException * If one of the parameters cannot be resolved to an argument. */ public function resolveParameters(ReflectionParameter ...$parameters): array; diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index 6abe86ab..bf5936f9 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -17,7 +17,7 @@ use Psr\Http\Message\ServerRequestInterface; use Sunrise\Http\Router\Annotation\RequestBody; use Sunrise\Http\Router\Exception\InvalidRequestBodyException; -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\ResolvingParameterException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\RequestBodyInterface; use Sunrise\Hydrator\Exception\InvalidObjectException; @@ -90,11 +90,11 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} * + * @throws ResolvingParameterException + * If the object cannot be hydrated. + * * @throws InvalidRequestBodyException * If the request body isn't valid. - * - * @throws LogicException - * If the DTO isn't valid. */ public function resolveParameter(ReflectionParameter $parameter, $context) { @@ -106,10 +106,10 @@ public function resolveParameter(ReflectionParameter $parameter, $context) try { return $this->hydrator->hydrate($parameterType->getName(), (array) $context->getParsedBody()); + } catch (InvalidObjectException $e) { + throw new ResolvingParameterException($e->getMessage(), 0, $e); } catch (InvalidValueException $e) { throw new InvalidRequestBodyException($e->getMessage(), 0, $e); - } catch (InvalidObjectException $e) { - throw new LogicException($e->getMessage(), 0, $e); } } } diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php index 91f5039a..43ec85f7 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -17,7 +17,7 @@ use Psr\Http\Message\ServerRequestInterface; use Sunrise\Http\Router\Annotation\RequestQuery; use Sunrise\Http\Router\Exception\InvalidRequestQueryException; -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\ResolvingParameterException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\RequestQueryInterface; use Sunrise\Hydrator\Exception\InvalidObjectException; @@ -90,11 +90,11 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} * + * @throws ResolvingParameterException + * If the object cannot be hydrated. + * * @throws InvalidRequestQueryException * If the request query isn't valid. - * - * @throws LogicException - * If the DTO isn't valid. */ public function resolveParameter(ReflectionParameter $parameter, $context) { @@ -106,10 +106,10 @@ public function resolveParameter(ReflectionParameter $parameter, $context) try { return $this->hydrator->hydrate($parameterType->getName(), $context->getQueryParams()); + } catch (InvalidObjectException $e) { + throw new ResolvingParameterException($e->getMessage(), 0, $e); } catch (InvalidValueException $e) { throw new InvalidRequestQueryException($e->getMessage(), 0, $e); - } catch (InvalidObjectException $e) { - throw new LogicException($e->getMessage(), 0, $e); } } } diff --git a/src/ParameterResolverInterface.php b/src/ParameterResolverInterface.php index 41cf3ef5..e3f6b46b 100644 --- a/src/ParameterResolverInterface.php +++ b/src/ParameterResolverInterface.php @@ -14,7 +14,7 @@ /** * Import classes */ -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\ResolvingParameterException; use ReflectionParameter; /** @@ -44,7 +44,7 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo * @return mixed * The ready-to-pass argument. * - * @throws LogicException + * @throws ResolvingParameterException * If the parameter cannot be resolved to an argument. */ public function resolveParameter(ReflectionParameter $parameter, $context); diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index 6761a53e..d340c491 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -17,7 +17,8 @@ use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\ReferenceResolvingException; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\ResolvingReferenceException; use Sunrise\Http\Router\Middleware\CallableMiddleware; use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use Closure; @@ -116,7 +117,7 @@ public function resolveRequestHandler($reference): RequestHandlerInterface // https://github.com/php/php-src/blob/3ed526441400060aa4e618b91b3352371fcd02a8/Zend/zend_API.c#L3884-L3932 /** @psalm-suppress MixedArgument */ if (is_array($reference) && is_callable($reference, true) && method_exists($reference[0], $reference[1])) { - /** @var array{0: object|class-string, 1: non-empty-string} $reference */ + /** @var array{0: class-string|object, 1: non-empty-string} $reference */ $callback = [is_string($reference[0]) ? $this->resolveClass($reference[0]) : $reference[0], $reference[1]]; @@ -128,8 +129,8 @@ public function resolveRequestHandler($reference): RequestHandlerInterface return $this->resolveClass($reference); } - throw new ReferenceResolvingException(sprintf( - 'Unable to resolve the reference {%s} to a request handler.', + throw new ResolvingReferenceException(sprintf( + 'Unable to resolve the reference {%s}', $this->stringifyReference($reference) )); } @@ -147,23 +148,23 @@ public function resolveMiddleware($reference): MiddlewareInterface return new CallableMiddleware($reference, $this->parameterResolutioner, $this->responseResolutioner); } - if (is_string($reference) && is_subclass_of($reference, MiddlewareInterface::class)) { - /** @var MiddlewareInterface */ - return $this->resolveClass($reference); - } - // https://github.com/php/php-src/blob/3ed526441400060aa4e618b91b3352371fcd02a8/Zend/zend_API.c#L3884-L3932 /** @psalm-suppress MixedArgument */ if (is_array($reference) && is_callable($reference, true) && method_exists($reference[0], $reference[1])) { - /** @var array{0: object|class-string, 1: non-empty-string} $reference */ + /** @var array{0: class-string|object, 1: non-empty-string} $reference */ $callback = [is_string($reference[0]) ? $this->resolveClass($reference[0]) : $reference[0], $reference[1]]; return new CallableMiddleware($callback, $this->parameterResolutioner, $this->responseResolutioner); } - throw new ReferenceResolvingException(sprintf( - 'Unable to resolve the reference {%s} to a middleware.', + if (is_string($reference) && is_subclass_of($reference, MiddlewareInterface::class)) { + /** @var MiddlewareInterface */ + return $this->resolveClass($reference); + } + + throw new ResolvingReferenceException(sprintf( + 'Unable to resolve the reference {%s}', $this->stringifyReference($reference) )); } @@ -189,6 +190,9 @@ public function resolveMiddlewares(array $references): array * * @return T * + * @throws LogicException + * If the class cannot be directly initialized. + * * @template T */ private function resolveClass(string $class): object @@ -198,13 +202,19 @@ private function resolveClass(string $class): object return $this->container->get($class); } - $arguments = []; $reflection = new ReflectionClass($class); + if (!$reflection->isInstantiable()) { + throw new LogicException(sprintf( + 'The class %s cannot be initialized', + $class + )); + } + + $arguments = []; $constructor = $reflection->getConstructor(); if (isset($constructor)) { - $arguments = $this->parameterResolutioner->resolveParameters( - ...$constructor->getParameters() - ); + $arguments = $this->parameterResolutioner + ->resolveParameters(...$constructor->getParameters()); } return $reflection->newInstance(...$arguments); diff --git a/src/ReferenceResolverInterface.php b/src/ReferenceResolverInterface.php index c818b4e2..f7862239 100644 --- a/src/ReferenceResolverInterface.php +++ b/src/ReferenceResolverInterface.php @@ -17,7 +17,7 @@ use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\ReferenceResolvingException; +use Sunrise\Http\Router\Exception\ResolvingReferenceException; /** * ReferenceResolverInterface @@ -37,7 +37,7 @@ interface ReferenceResolverInterface public function setContainer(?ContainerInterface $container): void; /** - * Adds the given parameter resolver(s) to the parameter resolutioner + * Adds the given parameter resolver(s) to the resolver * * @param ParameterResolverInterface ...$resolvers * @@ -48,7 +48,7 @@ public function setContainer(?ContainerInterface $container): void; public function addParameterResolver(ParameterResolverInterface ...$resolvers): void; /** - * Adds the given response resolver(s) to the response resolutioner + * Adds the given response resolver(s) to the resolver * * @param ResponseResolverInterface ...$resolvers * @@ -65,7 +65,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo * * @return RequestHandlerInterface * - * @throws ReferenceResolvingException + * @throws ResolvingReferenceException * If the reference cannot be resolved to a request handler. */ public function resolveRequestHandler($reference): RequestHandlerInterface; @@ -77,7 +77,7 @@ public function resolveRequestHandler($reference): RequestHandlerInterface; * * @return MiddlewareInterface * - * @throws ReferenceResolvingException + * @throws ResolvingReferenceException * If the reference cannot be resolved to a middleware. */ public function resolveMiddleware($reference): MiddlewareInterface; @@ -89,7 +89,7 @@ public function resolveMiddleware($reference): MiddlewareInterface; * * @return list * - * @throws ReferenceResolvingException + * @throws ResolvingReferenceException * If one of the references cannot be resolved to a middleware. * * @since 3.0.0 diff --git a/src/ResponseResolutioner.php b/src/ResponseResolutioner.php index f845f7d8..1bd59e29 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolutioner.php @@ -15,7 +15,7 @@ * Import classes */ use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\ResolvingResponseException; /** * Import functions @@ -81,7 +81,7 @@ public function resolveResponse($response): ResponseInterface } } - throw new LogicException(sprintf( + throw new ResolvingResponseException(sprintf( 'Unable to resolve the response {%s}', get_debug_type($response) )); diff --git a/src/ResponseResolutionerInterface.php b/src/ResponseResolutionerInterface.php index 6c36b0e9..e56ced98 100644 --- a/src/ResponseResolutionerInterface.php +++ b/src/ResponseResolutionerInterface.php @@ -15,7 +15,7 @@ * Import classes */ use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\ResolvingResponseException; /** * ResponseResolutionerInterface @@ -52,7 +52,7 @@ public function addResolver(ResponseResolverInterface ...$resolvers): void; * * @return ResponseInterface * - * @throws LogicException + * @throws ResolvingResponseException * If the raw response cannot be resolved to the object. */ public function resolveResponse($response): ResponseInterface; diff --git a/src/ResponseResolverInterface.php b/src/ResponseResolverInterface.php index 77d2a6fa..fa18d042 100644 --- a/src/ResponseResolverInterface.php +++ b/src/ResponseResolverInterface.php @@ -15,7 +15,7 @@ * Import classes */ use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\ResolvingResponseException; /** * ResponseResolverInterface @@ -43,7 +43,7 @@ public function supportsResponse($response, $context): bool; * * @return ResponseInterface * - * @throws LogicException + * @throws ResolvingResponseException * If the raw response cannot be resolved to the object. */ public function resolveResponse($response, $context): ResponseInterface; diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 6fd0c634..f5903794 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -15,7 +15,6 @@ * Import classes */ use Psr\Container\ContainerInterface; -use Sunrise\Http\Router\Exception\InvalidReferenceException; /** * RouteCollector @@ -105,9 +104,6 @@ public function getCollection(): RouteCollectionInterface * @param array $attributes * * @return RouteInterface - * - * @throws InvalidReferenceException - * If the given request handler or one of the given middlewares cannot be resolved. */ public function route( string $name, @@ -141,9 +137,6 @@ public function route( * @param array $attributes * * @return RouteInterface - * - * @throws InvalidReferenceException - * If the given request handler or one of the given middlewares cannot be resolved. */ public function head( string $name, @@ -172,9 +165,6 @@ public function head( * @param array $attributes * * @return RouteInterface - * - * @throws InvalidReferenceException - * If the given request handler or one of the given middlewares cannot be resolved. */ public function get( string $name, @@ -203,9 +193,6 @@ public function get( * @param array $attributes * * @return RouteInterface - * - * @throws InvalidReferenceException - * If the given request handler or one of the given middlewares cannot be resolved. */ public function post( string $name, @@ -234,9 +221,6 @@ public function post( * @param array $attributes * * @return RouteInterface - * - * @throws InvalidReferenceException - * If the given request handler or one of the given middlewares cannot be resolved. */ public function put( string $name, @@ -265,9 +249,6 @@ public function put( * @param array $attributes * * @return RouteInterface - * - * @throws InvalidReferenceException - * If the given request handler or one of the given middlewares cannot be resolved. */ public function patch( string $name, @@ -296,9 +277,6 @@ public function patch( * @param array $attributes * * @return RouteInterface - * - * @throws InvalidReferenceException - * If the given request handler or one of the given middlewares cannot be resolved. */ public function delete( string $name, @@ -327,9 +305,6 @@ public function delete( * @param array $attributes * * @return RouteInterface - * - * @throws InvalidReferenceException - * If the given request handler or one of the given middlewares cannot be resolved. */ public function purge( string $name, @@ -355,9 +330,6 @@ public function purge( * @param array $middlewares * * @return RouteCollectionInterface - * - * @throws InvalidReferenceException - * If one of the given middlewares cannot be resolved. */ public function group(callable $callback, array $middlewares = []): RouteCollectionInterface { From 62113e5a6b81510f4d2bc2327cefa8338c41f85c Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Tue, 24 Jan 2023 04:10:27 +0100 Subject: [PATCH 039/180] v3 --- composer.json | 1 + functions/get_directory_classes.php | 54 +++++++++++++++++ src/AnnotationReader.php | 92 ++++++++++++++++++++++++----- src/Loader/DescriptorLoader.php | 52 ++++------------ src/ParameterResolutioner.php | 12 ++-- 5 files changed, 150 insertions(+), 61 deletions(-) create mode 100644 functions/get_directory_classes.php diff --git a/composer.json b/composer.json index 9c9cb8b8..81f47bcd 100644 --- a/composer.json +++ b/composer.json @@ -48,6 +48,7 @@ "files": [ "functions/emit.php", "functions/get_debug_type.php", + "functions/get_directory_classes.php", "functions/path_build.php", "functions/path_match.php", "functions/path_parse.php", diff --git a/functions/get_directory_classes.php b/functions/get_directory_classes.php new file mode 100644 index 00000000..f083c5cb --- /dev/null +++ b/functions/get_directory_classes.php @@ -0,0 +1,54 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use Iterator; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use SplFileInfo; + +/** + * Import functions + */ +use function array_diff; +use function get_declared_classes; + +/** + * Scans the given directory and returns the found classes + * + * @param string $directory + * + * @return class-string[] + * + * @since 3.0.0 + */ +function get_directory_classes(string $directory): array +{ + $known = get_declared_classes(); + + /** @var Iterator */ + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($directory) + ); + + foreach ($files as $file) { + if ('php' === $file->getExtension()) { + /** @psalm-suppress UnresolvableInclude */ + require_once $file->getPathname(); + } + } + + return array_diff(get_declared_classes(), $known); +} diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index 6ecd595d..037863a9 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -14,8 +14,6 @@ /** * Import classes */ -use Doctrine\Common\Annotations\AnnotationReader as DoctrineAnnotationReader; -use Doctrine\Common\Annotations\Reader as DoctrineAnnotationReaderInterface; use Sunrise\Http\Router\Exception\LogicException; use ReflectionAttribute; use ReflectionClass; @@ -26,6 +24,7 @@ * Import functions */ use function class_exists; +use function sprintf; /** * Import constants @@ -41,24 +40,24 @@ final class AnnotationReader { /** - * @var DoctrineAnnotationReaderInterface|null + * @var \Doctrine\Common\Annotations\Reader|null */ - private ?DoctrineAnnotationReaderInterface $annotationReader = null; + private ?\Doctrine\Common\Annotations\Reader $annotationReader = null; /** * Sets the given annotation reader to the reader * - * @param DoctrineAnnotationReaderInterface|null $annotationReader + * @param \Doctrine\Common\Annotations\Reader|null $annotationReader * * @return void */ - public function setAnnotationReader(?DoctrineAnnotationReaderInterface $annotationReader): void + public function setAnnotationReader(?\Doctrine\Common\Annotations\Reader $annotationReader): void { $this->annotationReader = $annotationReader; } /** - * Sets the default annotation reader to the reader + * Uses the default annotation reader * * @return void * @@ -67,33 +66,64 @@ public function setAnnotationReader(?DoctrineAnnotationReaderInterface $annotati */ public function useDefaultAnnotationReader(): void { - if (!class_exists(DoctrineAnnotationReader::class)) { + if (!class_exists(\Doctrine\Common\Annotations\AnnotationReader::class)) { throw new LogicException( 'The annotations reading logic requires an uninstalled "doctrine/annotations" package, ' . 'run the command "composer install doctrine/annotations" and try again' ); } - $this->setAnnotationReader(new DoctrineAnnotationReader()); + $this->setAnnotationReader(new \Doctrine\Common\Annotations\AnnotationReader()); } /** - * Gets annotations by the given name from the given class or method + * Gets annotations from the given class or method by the given annotation name * * @param ReflectionClass|ReflectionMethod $classOrMethod * @param class-string $annotationName * * @return list * + * @throws LogicException + * If the given reflection isn't supported. + * + * @psalm-suppress RedundantConditionGivenDocblockType + * + * @template T + */ + public function getClassOrMethodAnnotations(Reflector $classOrMethod, string $annotationName): array + { + if ($classOrMethod instanceof ReflectionClass) { + return $this->getClassAnnotations($classOrMethod, $annotationName); + } + + if ($classOrMethod instanceof ReflectionMethod) { + return $this->getMethodAnnotations($classOrMethod, $annotationName); + } + + throw new LogicException(sprintf( + 'The %s method only handles class or method reflection', + __METHOD__ + )); + } + + /** + * Gets annotations from the given class by the given annotation name + * + * @param ReflectionClass $class + * @param class-string $annotationName + * + * @return list + * * @template T */ - public function getAnnotations(Reflector $classOrMethod, string $annotationName): array + public function getClassAnnotations(ReflectionClass $class, string $annotationName): array { $result = []; if (PHP_MAJOR_VERSION === 8) { /** @var ReflectionAttribute[] */ - $attributes = $classOrMethod->getAttributes($annotationName); + $attributes = $class->getAttributes($annotationName); foreach ($attributes as $attribute) { /** @var T */ $result[] = $attribute->newInstance(); @@ -101,10 +131,42 @@ public function getAnnotations(Reflector $classOrMethod, string $annotationName) } if (isset($this->annotationReader)) { - $annotations = ($classOrMethod instanceof ReflectionClass) ? - $this->annotationReader->getClassAnnotations($classOrMethod) : - $this->annotationReader->getMethodAnnotations($classOrMethod); + $annotations = $this->annotationReader->getClassAnnotations($class); + foreach ($annotations as $annotation) { + if ($annotation instanceof $annotationName) { + $result[] = $annotation; + } + } + } + return $result; + } + + /** + * Gets annotations from the given method by the given annotation name + * + * @param ReflectionMethod $method + * @param class-string $annotationName + * + * @return list + * + * @template T + */ + public function getMethodAnnotations(ReflectionMethod $method, string $annotationName): array + { + $result = []; + + if (PHP_MAJOR_VERSION === 8) { + /** @var ReflectionAttribute[] */ + $attributes = $method->getAttributes($annotationName); + foreach ($attributes as $attribute) { + /** @var T */ + $result[] = $attribute->newInstance(); + } + } + + if (isset($this->annotationReader)) { + $annotations = $this->annotationReader->getMethodAnnotations($method); foreach ($annotations as $annotation) { if ($annotation instanceof $annotationName) { $result[] = $annotation; diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 1197d6a7..d1df4c87 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -14,7 +14,6 @@ /** * Import classes */ -use Doctrine\Common\Annotations\Reader as AnnotationReaderInterface; use Psr\Container\ContainerInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\SimpleCache\CacheInterface; @@ -34,24 +33,19 @@ use Sunrise\Http\Router\RouteCollectionInterface; use Sunrise\Http\Router\RouteFactory; use Sunrise\Http\Router\RouteFactoryInterface; -use Iterator; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; use ReflectionClass; use ReflectionMethod; use Reflector; -use SplFileInfo; /** * Import functions */ -use function array_diff; use function class_exists; -use function get_declared_classes; use function hash; use function is_dir; use function is_string; use function usort; +use function Sunrise\Http\Router\get_directory_classes; /** * Import constants @@ -165,13 +159,13 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo /** * Sets the given annotation reader to the descriptor loader * - * @param AnnotationReaderInterface|null $annotationReader + * @param \Doctrine\Common\Annotations\Reader|null $annotationReader * * @return void * * @since 3.0.0 */ - public function setAnnotationReader(?AnnotationReaderInterface $annotationReader): void + public function setAnnotationReader(?\Doctrine\Common\Annotations\Reader $annotationReader): void { $this->annotationReader->setAnnotationReader($annotationReader); } @@ -248,7 +242,7 @@ public function attach($resource): void } if (is_dir($resource)) { - $classnames = $this->scandir($resource); + $classnames = get_directory_classes($resource); foreach ($classnames as $classname) { $this->resources[] = $classname; } @@ -361,7 +355,7 @@ private function getClassDescriptors(ReflectionClass $class): array $result = []; if ($class->isSubclassOf(RequestHandlerInterface::class)) { - $annotations = $this->annotationReader->getAnnotations($class, Route::class); + $annotations = $this->annotationReader->getClassAnnotations($class, Route::class); if (isset($annotations[0])) { $descriptor = $annotations[0]; $descriptor->holder = $class->getName(); @@ -376,7 +370,7 @@ private function getClassDescriptors(ReflectionClass $class): array continue; } - $annotations = $this->annotationReader->getAnnotations($method, Route::class); + $annotations = $this->annotationReader->getMethodAnnotations($method, Route::class); if (isset($annotations[0])) { $descriptor = $annotations[0]; $descriptor->holder = [$class->getName(), $method->getName()]; @@ -399,50 +393,24 @@ private function getClassDescriptors(ReflectionClass $class): array */ private function supplementDescriptor(Route $descriptor, Reflector $classOrMethod): void { - $annotations = $this->annotationReader->getAnnotations($classOrMethod, Host::class); + $annotations = $this->annotationReader->getClassOrMethodAnnotations($classOrMethod, Host::class); if (isset($annotations[0])) { $descriptor->host = $annotations[0]->value; } - $annotations = $this->annotationReader->getAnnotations($classOrMethod, Prefix::class); + $annotations = $this->annotationReader->getClassOrMethodAnnotations($classOrMethod, Prefix::class); if (isset($annotations[0])) { $descriptor->path = $annotations[0]->value . $descriptor->path; } - $annotations = $this->annotationReader->getAnnotations($classOrMethod, Postfix::class); + $annotations = $this->annotationReader->getClassOrMethodAnnotations($classOrMethod, Postfix::class); if (isset($annotations[0])) { $descriptor->path = $descriptor->path . $annotations[0]->value; } - $annotations = $this->annotationReader->getAnnotations($classOrMethod, Middleware::class); + $annotations = $this->annotationReader->getClassOrMethodAnnotations($classOrMethod, Middleware::class); foreach ($annotations as $annotation) { $descriptor->middlewares[] = $annotation->value; } } - - /** - * Scans the given directory and returns the found classes - * - * @param string $directory - * - * @return class-string[] - */ - private function scandir(string $directory): array - { - $known = get_declared_classes(); - - /** @var Iterator */ - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($directory) - ); - - foreach ($files as $file) { - if ('php' === $file->getExtension()) { - /** @psalm-suppress UnresolvableInclude */ - require_once $file->getPathname(); - } - } - - return array_diff(get_declared_classes(), $known); - } } diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index 15fad058..ed23459a 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -149,8 +149,10 @@ private function stringifyParameter(ReflectionParameter $parameter): string * * @return string */ - private function stringifyMethodParameter(ReflectionMethod $method, ReflectionParameter $parameter): string - { + private function stringifyMethodParameter( + ReflectionMethod $method, + ReflectionParameter $parameter + ) : string { return sprintf( '%s::%s($%s[%d])', $method->getDeclaringClass()->getName(), @@ -168,8 +170,10 @@ private function stringifyMethodParameter(ReflectionMethod $method, ReflectionPa * * @return string */ - private function stringifyFunctionParameter(ReflectionFunctionAbstract $function, ReflectionParameter $parameter): string - { + private function stringifyFunctionParameter( + ReflectionFunctionAbstract $function, + ReflectionParameter $parameter + ) : string { return sprintf( '%s($%s[%d])', $function->getName(), From 848c1db7373c2b66c652eb51d67843457a6d396c Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Tue, 24 Jan 2023 04:53:52 +0100 Subject: [PATCH 040/180] v3 --- .../WhitespaceStrippingMiddleware.php | 10 +++- src/ResponseResolutioner.php | 16 ++++- .../RouteResponseResolver.php | 59 +++++++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 src/ResponseResolver/RouteResponseResolver.php diff --git a/src/Middleware/WhitespaceStrippingMiddleware.php b/src/Middleware/WhitespaceStrippingMiddleware.php index 766e6de7..020eac59 100644 --- a/src/Middleware/WhitespaceStrippingMiddleware.php +++ b/src/Middleware/WhitespaceStrippingMiddleware.php @@ -45,9 +45,13 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface if (!empty($parsedBody) && is_array($parsedBody)) { /** @psalm-suppress MissingClosureParamType, MixedAssignment */ - array_walk_recursive($parsedBody, static function (&$value): void { - $value = is_string($value) ? trim($value) : $value; - }); + $walker = static function (&$value): void { + if (is_string($value)) { + $value = trim($value); + } + }; + + array_walk_recursive($parsedBody, $walker); $request = $request->withParsedBody($parsedBody); } diff --git a/src/ResponseResolutioner.php b/src/ResponseResolutioner.php index 1bd59e29..6422c26f 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolutioner.php @@ -83,7 +83,21 @@ public function resolveResponse($response): ResponseInterface throw new ResolvingResponseException(sprintf( 'Unable to resolve the response {%s}', - get_debug_type($response) + $this->stringifyResponse($response) )); } + + /** + * Stringifies the given raw response + * + * @param mixed $response + * + * @return string + * + * @todo Think about how to display the responder... + */ + private function stringifyResponse($response): string + { + return get_debug_type($response); + } } diff --git a/src/ResponseResolver/RouteResponseResolver.php b/src/ResponseResolver/RouteResponseResolver.php new file mode 100644 index 00000000..d338cdce --- /dev/null +++ b/src/ResponseResolver/RouteResponseResolver.php @@ -0,0 +1,59 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ResponseResolver; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\ResponseResolverInterface; +use Sunrise\Http\Router\RouteInterface; + +/** + * RouteResponseResolver + * + * @since 3.0.0 + */ +final class RouteResponseResolver implements ResponseResolverInterface +{ + + /** + * {@inheritdoc} + */ + public function supportsResponse($response, $context): bool + { + if (!($response instanceof RouteInterface)) { + return false; + } + + if (!($context instanceof ServerRequestInterface)) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function resolveResponse($response, $context): ResponseInterface + { + /** @var RouteInterface */ + $response = $response; + + /** @var ServerRequestInterface */ + $context = $context; + + return $response->handle($context); + } +} From 32a93fcca07fb36132b30463b02180a773895a39 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Tue, 24 Jan 2023 05:28:36 +0100 Subject: [PATCH 041/180] v3 --- composer.json | 3 +- ...ectory_classes.php => get_dir_classes.php} | 29 +++++++------- functions/get_file_classes.php | 38 +++++++++++++++++++ src/Loader/DescriptorLoader.php | 4 +- 4 files changed, 56 insertions(+), 18 deletions(-) rename functions/{get_directory_classes.php => get_dir_classes.php} (61%) create mode 100644 functions/get_file_classes.php diff --git a/composer.json b/composer.json index 81f47bcd..8dc8187d 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,8 @@ "files": [ "functions/emit.php", "functions/get_debug_type.php", - "functions/get_directory_classes.php", + "functions/get_dir_classes.php", + "functions/get_file_classes.php", "functions/path_build.php", "functions/path_match.php", "functions/path_parse.php", diff --git a/functions/get_directory_classes.php b/functions/get_dir_classes.php similarity index 61% rename from functions/get_directory_classes.php rename to functions/get_dir_classes.php index f083c5cb..c32ff6e6 100644 --- a/functions/get_directory_classes.php +++ b/functions/get_dir_classes.php @@ -19,36 +19,35 @@ use RecursiveIteratorIterator; use SplFileInfo; -/** - * Import functions - */ -use function array_diff; -use function get_declared_classes; - /** * Scans the given directory and returns the found classes * - * @param string $directory + * @param string $dirname * * @return class-string[] * * @since 3.0.0 */ -function get_directory_classes(string $directory): array +function get_dir_classes(string $dirname): array { - $known = get_declared_classes(); - /** @var Iterator */ $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($directory) + new RecursiveDirectoryIterator($dirname) ); + $result = []; + foreach ($files as $file) { - if ('php' === $file->getExtension()) { - /** @psalm-suppress UnresolvableInclude */ - require_once $file->getPathname(); + // only php files... + if ($file->getExtension() !== 'php') { + continue; + } + + $classnames = get_file_classes($file->getPathname()); + foreach ($classnames as $classname) { + $result[] = $classname; } } - return array_diff(get_declared_classes(), $known); + return $result; } diff --git a/functions/get_file_classes.php b/functions/get_file_classes.php new file mode 100644 index 00000000..58b17e38 --- /dev/null +++ b/functions/get_file_classes.php @@ -0,0 +1,38 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import functions + */ +use function array_diff; +use function get_declared_classes; + +/** + * Scans the given file and returns the found classes + * + * @param string $filename + * + * @return class-string[] + * + * @since 3.0.0 + * + * @todo https://www.php.net/manual/en/book.tokenizer.php + */ +function get_file_classes(string $filename): array +{ + $snapshot = get_declared_classes(); + + require_once $filename; + + return array_diff(get_declared_classes(), $snapshot); +} diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index d1df4c87..fb7af185 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -45,7 +45,7 @@ use function is_dir; use function is_string; use function usort; -use function Sunrise\Http\Router\get_directory_classes; +use function Sunrise\Http\Router\get_dir_classes; /** * Import constants @@ -242,7 +242,7 @@ public function attach($resource): void } if (is_dir($resource)) { - $classnames = get_directory_classes($resource); + $classnames = get_dir_classes($resource); foreach ($classnames as $classname) { $this->resources[] = $classname; } From a4e9470a80e6888876ea7b92be7e48b5ae42bca6 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Wed, 25 Jan 2023 01:22:36 +0100 Subject: [PATCH 042/180] v3 --- .../KnownTypeParameterResolver.php | 4 +- .../RequestRouteParameterResolver.php | 65 +++++++++++++++++++ .../RouteResponseResolver.php | 4 +- src/RouteCollector.php | 56 ++++++++++++---- 4 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 src/ParameterResolver/RequestRouteParameterResolver.php diff --git a/src/ParameterResolver/KnownTypeParameterResolver.php b/src/ParameterResolver/KnownTypeParameterResolver.php index f531c328..ad4373b9 100644 --- a/src/ParameterResolver/KnownTypeParameterResolver.php +++ b/src/ParameterResolver/KnownTypeParameterResolver.php @@ -76,11 +76,11 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo return false; } - if ($parameter->getType()->isBuiltin()) { + if (!($parameter->getType()->getName() === $this->type)) { return false; } - return $this->type === $parameter->getType()->getName(); + return true; } /** diff --git a/src/ParameterResolver/RequestRouteParameterResolver.php b/src/ParameterResolver/RequestRouteParameterResolver.php new file mode 100644 index 00000000..3db1a9f9 --- /dev/null +++ b/src/ParameterResolver/RequestRouteParameterResolver.php @@ -0,0 +1,65 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ParameterResolver; + +/** + * Import classes + */ +use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\ParameterResolverInterface; +use Sunrise\Http\Router\RouteInterface; +use ReflectionNamedType; +use ReflectionParameter; + +/** + * RequestRouteParameterResolver + * + * @since 3.0.0 + */ +final class RequestRouteParameterResolver implements ParameterResolverInterface +{ + + /** + * {@inheritdoc} + */ + public function supportsParameter(ReflectionParameter $parameter, $context): bool + { + if (!($context instanceof ServerRequestInterface)) { + return false; + } + + if (!($parameter->getType() instanceof ReflectionNamedType)) { + return false; + } + + if (!($parameter->getType()->getName() === RouteInterface::class)) { + return false; + } + + if (!($context->getAttribute(RouteInterface::ATTR_ROUTE) instanceof RouteInterface)) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function resolveParameter(ReflectionParameter $parameter, $context) + { + /** @var ServerRequestInterface */ + $context = $context; + + return $context->getAttribute(RouteInterface::ATTR_ROUTE); + } +} diff --git a/src/ResponseResolver/RouteResponseResolver.php b/src/ResponseResolver/RouteResponseResolver.php index d338cdce..8e913ce6 100644 --- a/src/ResponseResolver/RouteResponseResolver.php +++ b/src/ResponseResolver/RouteResponseResolver.php @@ -32,11 +32,11 @@ final class RouteResponseResolver implements ResponseResolverInterface */ public function supportsResponse($response, $context): bool { - if (!($response instanceof RouteInterface)) { + if (!($context instanceof ServerRequestInterface)) { return false; } - if (!($context instanceof ServerRequestInterface)) { + if (!($response instanceof RouteInterface)) { return false; } diff --git a/src/RouteCollector.php b/src/RouteCollector.php index f5903794..5d18c147 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -27,28 +27,28 @@ class RouteCollector * * @var RouteCollectionFactoryInterface */ - private $collectionFactory; + private RouteCollectionFactoryInterface $collectionFactory; /** * Route factory of the collector * * @var RouteFactoryInterface */ - private $routeFactory; + private RouteFactoryInterface $routeFactory; /** * Reference resolver of the collector * * @var ReferenceResolverInterface */ - private $referenceResolver; + private ReferenceResolverInterface $referenceResolver; /** * Route collection of the collector * * @var RouteCollectionInterface */ - private $collection; + private RouteCollectionInterface $collection; /** * Constructor of the class @@ -84,7 +84,35 @@ public function setContainer(?ContainerInterface $container): void } /** - * Gets the collector collection + * Adds the given parameter resolver(s) to the collector + * + * @param ParameterResolverInterface ...$resolvers + * + * @return void + * + * @since 3.0.0 + */ + public function addParameterResolver(ParameterResolverInterface ...$resolvers): void + { + $this->referenceResolver->addParameterResolver(...$resolvers); + } + + /** + * Adds the given response resolver(s) to the collector + * + * @param ResponseResolverInterface ...$resolvers + * + * @return void + * + * @since 3.0.0 + */ + public function addResponseResolver(ResponseResolverInterface ...$resolvers): void + { + $this->referenceResolver->addResponseResolver(...$resolvers); + } + + /** + * Gets the collector's route collection * * @return RouteCollectionInterface */ @@ -100,7 +128,7 @@ public function getCollection(): RouteCollectionInterface * @param string $path * @param list $methods * @param mixed $requestHandler - * @param array $middlewares + * @param array $middlewares * @param array $attributes * * @return RouteInterface @@ -133,7 +161,7 @@ public function route( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param array $middlewares * @param array $attributes * * @return RouteInterface @@ -161,7 +189,7 @@ public function head( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param array $middlewares * @param array $attributes * * @return RouteInterface @@ -189,7 +217,7 @@ public function get( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param array $middlewares * @param array $attributes * * @return RouteInterface @@ -217,7 +245,7 @@ public function post( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param array $middlewares * @param array $attributes * * @return RouteInterface @@ -245,7 +273,7 @@ public function put( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param array $middlewares * @param array $attributes * * @return RouteInterface @@ -273,7 +301,7 @@ public function patch( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param array $middlewares * @param array $attributes * * @return RouteInterface @@ -301,7 +329,7 @@ public function delete( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param array $middlewares * @param array $attributes * * @return RouteInterface @@ -327,7 +355,7 @@ public function purge( * Route grouping logic * * @param callable $callback - * @param array $middlewares + * @param array $middlewares * * @return RouteCollectionInterface */ From 97b432cb8a441d7bfd3dfe8b5100afa43519d574 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Thu, 26 Jan 2023 04:18:07 +0100 Subject: [PATCH 043/180] v3 --- src/AnnotationReader.php | 5 +- src/ClassResolver.php | 93 +++++++++++++ src/ClassResolverInterface.php | 42 ++++++ src/Exception/Http/HttpException.php | 4 +- src/Loader/ConfigLoader.php | 79 +++++++++-- src/Loader/DescriptorLoader.php | 79 +++++++++-- .../JsonPayloadDecodingMiddleware.php | 15 ++- ...rsedBodyWhitespaceStrippingMiddleware.php} | 29 ++-- src/Middleware/UnsafeCallableMiddleware.php | 55 ++++++++ src/ReferenceResolver.php | 105 ++++----------- src/ReferenceResolverInterface.php | 32 ----- src/Route.php | 8 +- src/RouteCollector.php | 127 +++++++++++++----- src/RouteInterface.php | 3 +- src/Router.php | 21 +-- src/RouterBuilder.php | 22 --- 16 files changed, 480 insertions(+), 239 deletions(-) create mode 100644 src/ClassResolver.php create mode 100644 src/ClassResolverInterface.php rename src/Middleware/{WhitespaceStrippingMiddleware.php => ParsedBodyWhitespaceStrippingMiddleware.php} (61%) create mode 100644 src/Middleware/UnsafeCallableMiddleware.php diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index 037863a9..67f6321d 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -14,6 +14,7 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; use ReflectionAttribute; use ReflectionClass; @@ -84,7 +85,7 @@ public function useDefaultAnnotationReader(): void * * @return list * - * @throws LogicException + * @throws InvalidArgumentException * If the given reflection isn't supported. * * @psalm-suppress RedundantConditionGivenDocblockType @@ -101,7 +102,7 @@ public function getClassOrMethodAnnotations(Reflector $classOrMethod, string $an return $this->getMethodAnnotations($classOrMethod, $annotationName); } - throw new LogicException(sprintf( + throw new InvalidArgumentException(sprintf( 'The %s method only handles class or method reflection', __METHOD__ )); diff --git a/src/ClassResolver.php b/src/ClassResolver.php new file mode 100644 index 00000000..403e9ff5 --- /dev/null +++ b/src/ClassResolver.php @@ -0,0 +1,93 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\InvalidArgumentException; +use Sunrise\Http\Router\Exception\LogicException; +use ReflectionClass; + +/** + * Import functions + */ +use function class_exists; +use function sprintf; + +/** + * ClassResolver + * + * @since 3.0.0 + */ +final class ClassResolver implements ClassResolverInterface +{ + + /** + * Map of classes that are already resolved + * + * @var array + */ + private array $resolvedClasses = []; + + /** + * The resolver's parameter resolutioner + * + * @var ParameterResolutionerInterface + */ + private ParameterResolutionerInterface $parameterResolutioner; + + /** + * Constructor of the class + * + * @param ParameterResolutionerInterface $parameterResolutioner + */ + public function __construct(ParameterResolutionerInterface $parameterResolutioner) + { + $this->parameterResolutioner = $parameterResolutioner; + } + + /** + * {@inheritdoc} + */ + public function resolveClass(string $classname): object + { + if (isset($this->resolvedClasses[$classname])) { + return $this->resolvedClasses[$classname]; + } + + if (!class_exists($classname)) { + throw new InvalidArgumentException(sprintf( + 'The class %s was not found', + $classname + )); + } + + $reflection = new ReflectionClass($classname); + if (!$reflection->isInstantiable()) { + throw new LogicException(sprintf( + 'The class %s cannot be initialized directly', + $classname + )); + } + + $arguments = []; + $constructor = $reflection->getConstructor(); + if (isset($constructor) && $constructor->getNumberOfParameters() > 0) { + $arguments = $this->parameterResolutioner->resolveParameters( + ...$constructor->getParameters() + ); + } + + return $this->resolvedClasses[$classname] = $reflection->newInstance(...$arguments); + } +} diff --git a/src/ClassResolverInterface.php b/src/ClassResolverInterface.php new file mode 100644 index 00000000..2757e97d --- /dev/null +++ b/src/ClassResolverInterface.php @@ -0,0 +1,42 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\InvalidArgumentException; +use Sunrise\Http\Router\Exception\LogicException; + +/** + * ClassResolverInterface + * + * @since 3.0.0 + */ +interface ClassResolverInterface +{ + + /** + * Resolves the given class by its name + * + * @param class-string $classname + * + * @return object + * + * @throws InvalidArgumentException + * If the class doesn't exist. + * + * @throws LogicException + * If the class cannot be initialized directly. + */ + public function resolveClass(string $classname): object; +} diff --git a/src/Exception/Http/HttpException.php b/src/Exception/Http/HttpException.php index 401fea9b..b29cc1e5 100644 --- a/src/Exception/Http/HttpException.php +++ b/src/Exception/Http/HttpException.php @@ -14,7 +14,7 @@ /** * Import classes */ -use Exception; +use RuntimeException; use Throwable; /** @@ -22,7 +22,7 @@ * * @since 3.0.0 */ -class HttpException extends Exception implements HttpExceptionInterface +class HttpException extends RuntimeException implements HttpExceptionInterface { /** diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 3a44852e..d3f36285 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -16,9 +16,15 @@ */ use Psr\Container\ContainerInterface; use Sunrise\Http\Router\Exception\InvalidArgumentException; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ParameterResolver\DependencyInjectionParameterResolver; +use Sunrise\Http\Router\ParameterResolutioner; +use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; +use Sunrise\Http\Router\ResponseResolutioner; +use Sunrise\Http\Router\ResponseResolutionerInterface; use Sunrise\Http\Router\ResponseResolverInterface; use Sunrise\Http\Router\RouteCollectionFactory; use Sunrise\Http\Router\RouteCollectionFactoryInterface; @@ -61,63 +67,114 @@ final class ConfigLoader implements LoaderInterface */ private ReferenceResolverInterface $referenceResolver; + /** + * @var ParameterResolutionerInterface|null + */ + private ?ParameterResolutionerInterface $parameterResolutioner = null; + + /** + * @var ResponseResolutionerInterface|null + */ + private ?ResponseResolutionerInterface $responseResolutioner = null; + /** * Constructor of the class * * @param RouteCollectionFactoryInterface|null $collectionFactory * @param RouteFactoryInterface|null $routeFactory * @param ReferenceResolverInterface|null $referenceResolver + * @param ParameterResolutionerInterface|null $parameterResolutioner + * @param ResponseResolutionerInterface|null $responseResolutioner */ public function __construct( ?RouteCollectionFactoryInterface $collectionFactory = null, ?RouteFactoryInterface $routeFactory = null, - ?ReferenceResolverInterface $referenceResolver = null + ?ReferenceResolverInterface $referenceResolver = null, + ?ParameterResolutionerInterface $parameterResolutioner = null, + ?ResponseResolutionerInterface $responseResolutioner = null ) { $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory(); $this->routeFactory = $routeFactory ?? new RouteFactory(); - $this->referenceResolver = $referenceResolver ?? new ReferenceResolver(); + + $this->parameterResolutioner = $parameterResolutioner; + $this->responseResolutioner = $responseResolutioner; + + $this->referenceResolver = $referenceResolver ?? new ReferenceResolver( + $this->parameterResolutioner ??= new ParameterResolutioner(), + $this->responseResolutioner ??= new ResponseResolutioner() + ); } /** - * Sets the given container to the reference resolver + * Sets the given container to the parameter resolutioner * - * @param ContainerInterface|null $container + * @param ContainerInterface $container * * @return void * - * @since 2.9.0 + * @throws LogicException + * If a custom reference resolver was setted. */ - public function setContainer(?ContainerInterface $container): void + public function setContainer(ContainerInterface $container): void { - $this->referenceResolver->setContainer($container); + if (!isset($this->parameterResolutioner)) { + throw new LogicException( + 'The config route loader cannot accept the container ' . + 'because a custom reference resolver was setted' + ); + } + + $this->parameterResolutioner->addResolver( + new DependencyInjectionParameterResolver($container) + ); } /** - * Adds the given parameter resolver(s) to the reference resolver + * Adds the given parameter resolver(s) to the parameter resolutioner * * @param ParameterResolverInterface ...$resolvers * * @return void * + * @throws LogicException + * If a custom reference resolver was setted. + * * @since 3.0.0 */ public function addParameterResolver(ParameterResolverInterface ...$resolvers): void { - $this->referenceResolver->addParameterResolver(...$resolvers); + if (!isset($this->parameterResolutioner)) { + throw new LogicException( + 'The config route loader cannot accept the parameter resolver ' . + 'because a custom reference resolver was setted' + ); + } + + $this->parameterResolutioner->addResolver(...$resolvers); } /** - * Adds the given response resolver(s) to the reference resolver + * Adds the given response resolver(s) to the response resolutioner * * @param ResponseResolverInterface ...$resolvers * * @return void * + * @throws LogicException + * If a custom reference resolver was setted. + * * @since 3.0.0 */ public function addResponseResolver(ResponseResolverInterface ...$resolvers): void { - $this->referenceResolver->addResponseResolver(...$resolvers); + if (!isset($this->responseResolutioner)) { + throw new LogicException( + 'The config route loader cannot accept the response resolver ' . + 'because a custom reference resolver was setted' + ); + } + + $this->responseResolutioner->addResolver(...$resolvers); } /** diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index fb7af185..bd1e6abe 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -23,10 +23,16 @@ use Sunrise\Http\Router\Annotation\Prefix; use Sunrise\Http\Router\Annotation\Route; use Sunrise\Http\Router\Exception\InvalidArgumentException; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ParameterResolver\DependencyInjectionParameterResolver; use Sunrise\Http\Router\AnnotationReader; +use Sunrise\Http\Router\ParameterResolutioner; +use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; +use Sunrise\Http\Router\ResponseResolutioner; +use Sunrise\Http\Router\ResponseResolutionerInterface; use Sunrise\Http\Router\ResponseResolverInterface; use Sunrise\Http\Router\RouteCollectionFactory; use Sunrise\Http\Router\RouteCollectionFactoryInterface; @@ -78,6 +84,16 @@ final class DescriptorLoader implements LoaderInterface */ private ReferenceResolverInterface $referenceResolver; + /** + * @var ParameterResolutionerInterface|null + */ + private ?ParameterResolutionerInterface $parameterResolutioner = null; + + /** + * @var ResponseResolutionerInterface|null + */ + private ?ResponseResolutionerInterface $responseResolutioner = null; + /** * @var AnnotationReader */ @@ -99,15 +115,26 @@ final class DescriptorLoader implements LoaderInterface * @param RouteCollectionFactoryInterface|null $collectionFactory * @param RouteFactoryInterface|null $routeFactory * @param ReferenceResolverInterface|null $referenceResolver + * @param ParameterResolutionerInterface|null $parameterResolutioner + * @param ResponseResolutionerInterface|null $responseResolutioner */ public function __construct( ?RouteCollectionFactoryInterface $collectionFactory = null, ?RouteFactoryInterface $routeFactory = null, - ?ReferenceResolverInterface $referenceResolver = null + ?ReferenceResolverInterface $referenceResolver = null, + ?ParameterResolutionerInterface $parameterResolutioner = null, + ?ResponseResolutionerInterface $responseResolutioner = null ) { $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory(); $this->routeFactory = $routeFactory ?? new RouteFactory(); - $this->referenceResolver = $referenceResolver ?? new ReferenceResolver(); + + $this->parameterResolutioner = $parameterResolutioner; + $this->responseResolutioner = $responseResolutioner; + + $this->referenceResolver = $referenceResolver ?? new ReferenceResolver( + $this->parameterResolutioner ??= new ParameterResolutioner(), + $this->responseResolutioner ??= new ResponseResolutioner() + ); $this->annotationReader = new AnnotationReader(); @@ -117,43 +144,75 @@ public function __construct( } /** - * Sets the given container to the reference resolver + * Sets the given container to the parameter resolutioner * - * @param ContainerInterface|null $container + * @param ContainerInterface $container * * @return void + * + * @throws LogicException + * If a custom reference resolver was setted. */ - public function setContainer(?ContainerInterface $container): void + public function setContainer(ContainerInterface $container): void { - $this->referenceResolver->setContainer($container); + if (!isset($this->parameterResolutioner)) { + throw new LogicException( + 'The descriptor route loader cannot accept the container ' . + 'because a custom reference resolver was setted' + ); + } + + $this->parameterResolutioner->addResolver( + new DependencyInjectionParameterResolver($container) + ); } /** - * Adds the given parameter resolver(s) to the reference resolver + * Adds the given parameter resolver(s) to the parameter resolutioner * * @param ParameterResolverInterface ...$resolvers * * @return void * + * @throws LogicException + * If a custom reference resolver was setted. + * * @since 3.0.0 */ public function addParameterResolver(ParameterResolverInterface ...$resolvers): void { - $this->referenceResolver->addParameterResolver(...$resolvers); + if (!isset($this->parameterResolutioner)) { + throw new LogicException( + 'The descriptor route loader cannot accept the parameter resolver ' . + 'because a custom reference resolver was setted' + ); + } + + $this->parameterResolutioner->addResolver(...$resolvers); } /** - * Adds the given response resolver(s) to the reference resolver + * Adds the given response resolver(s) to the response resolutioner * * @param ResponseResolverInterface ...$resolvers * * @return void * + * @throws LogicException + * If a custom reference resolver was setted. + * * @since 3.0.0 */ public function addResponseResolver(ResponseResolverInterface ...$resolvers): void { - $this->referenceResolver->addResponseResolver(...$resolvers); + if (!isset($this->responseResolutioner)) { + throw new LogicException( + 'The descriptor route loader cannot accept the response resolver ' . + 'because a custom reference resolver was setted' + ); + } + + $this->responseResolutioner->addResolver(...$resolvers); } /** diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index e77ded54..2bf508bb 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -52,8 +52,11 @@ final class JsonPayloadDecodingMiddleware implements MiddlewareInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if ($this->supportsRequest($request)) { - $data = $this->decodeRequestJsonPayload($request); - $request = $request->withParsedBody($data); + return $handler->handle( + $request->withParsedBody( + $this->decodeRequestPayload($request) + ) + ); } return $handler->handle($request); @@ -70,7 +73,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface */ private function supportsRequest(ServerRequestInterface $request): bool { - return 'application/json' === $this->getRequestMediaType($request); + return $this->getRequestMediaType($request) === 'application/json'; } /** @@ -101,16 +104,16 @@ private function getRequestMediaType(ServerRequestInterface $request): ?string } /** - * Tries to decode the given request's JSON payload + * Tries to decode the given request's payload * * @param ServerRequestInterface $request * * @return array|null * * @throws InvalidRequestPayloadException - * If the request's "JSON" payload cannot be decoded. + * If the request's payload cannot be decoded. */ - private function decodeRequestJsonPayload(ServerRequestInterface $request): ?array + private function decodeRequestPayload(ServerRequestInterface $request): ?array { // https://www.php.net/json.constants $flags = JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY | JSON_THROW_ON_ERROR; diff --git a/src/Middleware/WhitespaceStrippingMiddleware.php b/src/Middleware/ParsedBodyWhitespaceStrippingMiddleware.php similarity index 61% rename from src/Middleware/WhitespaceStrippingMiddleware.php rename to src/Middleware/ParsedBodyWhitespaceStrippingMiddleware.php index 020eac59..656cec85 100644 --- a/src/Middleware/WhitespaceStrippingMiddleware.php +++ b/src/Middleware/ParsedBodyWhitespaceStrippingMiddleware.php @@ -28,11 +28,11 @@ use function trim; /** - * WhitespaceStrippingMiddleware + * ParsedBodyWhitespaceStrippingMiddleware * * @since 3.0.0 */ -final class WhitespaceStrippingMiddleware implements MiddlewareInterface +final class ParsedBodyWhitespaceStrippingMiddleware implements MiddlewareInterface { /** @@ -42,20 +42,21 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { $parsedBody = $request->getParsedBody(); - if (!empty($parsedBody) && is_array($parsedBody)) { - - /** @psalm-suppress MissingClosureParamType, MixedAssignment */ - $walker = static function (&$value): void { - if (is_string($value)) { - $value = trim($value); - } - }; + if (empty($parsedBody) || !is_array($parsedBody)) { + return $handler->handle($request); + } - array_walk_recursive($parsedBody, $walker); + /** @psalm-suppress MissingClosureParamType, MixedAssignment */ + $walker = static function (&$value): void { + if (is_string($value)) { + $value = trim($value); + } + }; - $request = $request->withParsedBody($parsedBody); - } + array_walk_recursive($parsedBody, $walker); - return $handler->handle($request); + return $handler->handle( + $request->withParsedBody($parsedBody) + ); } } diff --git a/src/Middleware/UnsafeCallableMiddleware.php b/src/Middleware/UnsafeCallableMiddleware.php new file mode 100644 index 00000000..b8b570a0 --- /dev/null +++ b/src/Middleware/UnsafeCallableMiddleware.php @@ -0,0 +1,55 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Middleware; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +/** + * UnsafeCallableMiddleware + * + * @since 3.0.0 + */ +final class UnsafeCallableMiddleware implements MiddlewareInterface +{ + + /** + * The middleware callback + * + * @var callable + */ + private $callback; + + /** + * Constructor of the class + * + * @param callable $callback + */ + public function __construct(callable $callback) + { + $this->callback = $callback; + } + + /** + * {@inheritdoc} + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + /** @var ResponseInterface */ + return ($this->callback)($request, $handler); + } +} diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index d340c491..77230a21 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -14,15 +14,12 @@ /** * Import classes */ -use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\Exception\ResolvingReferenceException; use Sunrise\Http\Router\Middleware\CallableMiddleware; use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use Closure; -use ReflectionClass; /** * Import functions @@ -57,48 +54,29 @@ final class ReferenceResolver implements ReferenceResolverInterface private ResponseResolutionerInterface $responseResolutioner; /** - * The resolver's container + * The resolver's class resolver * - * @var ContainerInterface|null + * @var ClassResolverInterface */ - private ?ContainerInterface $container = null; + private ClassResolverInterface $classResolver; /** * Constructor of the class * - * @param ParameterResolutionerInterface|null $parameterResolutioner - * @param ResponseResolutionerInterface|null $responseResolutioner + * @param ParameterResolutionerInterface $parameterResolutioner + * @param ResponseResolutionerInterface $responseResolutioner + * @param ClassResolverInterface|null $classResolver */ public function __construct( - ?ParameterResolutionerInterface $parameterResolutioner = null, - ?ResponseResolutionerInterface $responseResolutioner = null + ParameterResolutionerInterface $parameterResolutioner, + ResponseResolutionerInterface $responseResolutioner, + ?ClassResolverInterface $classResolver = null ) { - $this->parameterResolutioner = $parameterResolutioner ?? new ParameterResolutioner(); - $this->responseResolutioner = $responseResolutioner ?? new ResponseResolutioner(); - } - - /** - * {@inheritdoc} - */ - public function setContainer(?ContainerInterface $container): void - { - $this->container = $container; - } - - /** - * {@inheritdoc} - */ - public function addParameterResolver(ParameterResolverInterface ...$resolvers): void - { - $this->parameterResolutioner->addResolver(...$resolvers); - } + $classResolver ??= new ClassResolver($parameterResolutioner); - /** - * {@inheritdoc} - */ - public function addResponseResolver(ResponseResolverInterface ...$resolvers): void - { - $this->responseResolutioner->addResolver(...$resolvers); + $this->parameterResolutioner = $parameterResolutioner; + $this->responseResolutioner = $responseResolutioner; + $this->classResolver = $classResolver; } /** @@ -119,14 +97,18 @@ public function resolveRequestHandler($reference): RequestHandlerInterface if (is_array($reference) && is_callable($reference, true) && method_exists($reference[0], $reference[1])) { /** @var array{0: class-string|object, 1: non-empty-string} $reference */ - $callback = [is_string($reference[0]) ? $this->resolveClass($reference[0]) : $reference[0], $reference[1]]; + $object = is_string($reference[0]) ? $this->classResolver->resolveClass($reference[0]) : $reference[0]; - return new CallableRequestHandler($callback, $this->parameterResolutioner, $this->responseResolutioner); + return new CallableRequestHandler( + [$object, $reference[1]], + $this->parameterResolutioner, + $this->responseResolutioner + ); } if (is_string($reference) && is_subclass_of($reference, RequestHandlerInterface::class)) { /** @var RequestHandlerInterface */ - return $this->resolveClass($reference); + return $this->classResolver->resolveClass($reference); } throw new ResolvingReferenceException(sprintf( @@ -153,14 +135,18 @@ public function resolveMiddleware($reference): MiddlewareInterface if (is_array($reference) && is_callable($reference, true) && method_exists($reference[0], $reference[1])) { /** @var array{0: class-string|object, 1: non-empty-string} $reference */ - $callback = [is_string($reference[0]) ? $this->resolveClass($reference[0]) : $reference[0], $reference[1]]; + $object = is_string($reference[0]) ? $this->classResolver->resolveClass($reference[0]) : $reference[0]; - return new CallableMiddleware($callback, $this->parameterResolutioner, $this->responseResolutioner); + return new CallableMiddleware( + [$object, $reference[1]], + $this->parameterResolutioner, + $this->responseResolutioner + ); } if (is_string($reference) && is_subclass_of($reference, MiddlewareInterface::class)) { /** @var MiddlewareInterface */ - return $this->resolveClass($reference); + return $this->classResolver->resolveClass($reference); } throw new ResolvingReferenceException(sprintf( @@ -183,43 +169,6 @@ public function resolveMiddlewares(array $references): array return $middlewares; } - /** - * Resolves the given class - * - * @param class-string $class - * - * @return T - * - * @throws LogicException - * If the class cannot be directly initialized. - * - * @template T - */ - private function resolveClass(string $class): object - { - if (isset($this->container) && $this->container->has($class)) { - /** @var T */ - return $this->container->get($class); - } - - $reflection = new ReflectionClass($class); - if (!$reflection->isInstantiable()) { - throw new LogicException(sprintf( - 'The class %s cannot be initialized', - $class - )); - } - - $arguments = []; - $constructor = $reflection->getConstructor(); - if (isset($constructor)) { - $arguments = $this->parameterResolutioner - ->resolveParameters(...$constructor->getParameters()); - } - - return $reflection->newInstance(...$arguments); - } - /** * Stringifies the given reference * diff --git a/src/ReferenceResolverInterface.php b/src/ReferenceResolverInterface.php index f7862239..9df2cd07 100644 --- a/src/ReferenceResolverInterface.php +++ b/src/ReferenceResolverInterface.php @@ -14,7 +14,6 @@ /** * Import classes */ -use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\Exception\ResolvingReferenceException; @@ -27,37 +26,6 @@ interface ReferenceResolverInterface { - /** - * Sets the given container to the resolver - * - * @param ContainerInterface|null $container - * - * @return void - */ - public function setContainer(?ContainerInterface $container): void; - - /** - * Adds the given parameter resolver(s) to the resolver - * - * @param ParameterResolverInterface ...$resolvers - * - * @return void - * - * @since 3.0.0 - */ - public function addParameterResolver(ParameterResolverInterface ...$resolvers): void; - - /** - * Adds the given response resolver(s) to the resolver - * - * @param ResponseResolverInterface ...$resolvers - * - * @return void - * - * @since 3.0.0 - */ - public function addResponseResolver(ResponseResolverInterface ...$resolvers): void; - /** * Resolves the given reference to a request handler * diff --git a/src/Route.php b/src/Route.php index 03049070..e70a0c07 100644 --- a/src/Route.php +++ b/src/Route.php @@ -218,11 +218,9 @@ public function getTags(): array */ public function getHolder(): Reflector { - if ($this->requestHandler instanceof CallableRequestHandler) { - return $this->requestHandler->getReflection(); - } - - return new ReflectionClass($this->requestHandler); + return ($this->requestHandler instanceof CallableRequestHandler) ? + $this->requestHandler->getReflection() : + new ReflectionClass($this->requestHandler); } /** diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 5d18c147..72208412 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -15,6 +15,8 @@ * Import classes */ use Psr\Container\ContainerInterface; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ParameterResolver\DependencyInjectionParameterResolver; /** * RouteCollector @@ -23,32 +25,42 @@ class RouteCollector { /** - * Route collection factory of the collector + * The collector's route collection + * + * @var RouteCollectionInterface + */ + private RouteCollectionInterface $collection; + + /** + * The collector's route collection factory * * @var RouteCollectionFactoryInterface */ private RouteCollectionFactoryInterface $collectionFactory; /** - * Route factory of the collector + * The collector's route factory * * @var RouteFactoryInterface */ private RouteFactoryInterface $routeFactory; /** - * Reference resolver of the collector + * The collector's reference resolver * * @var ReferenceResolverInterface */ private ReferenceResolverInterface $referenceResolver; /** - * Route collection of the collector - * - * @var RouteCollectionInterface + * @var ParameterResolutionerInterface|null */ - private RouteCollectionInterface $collection; + private ?ParameterResolutionerInterface $parameterResolutioner = null; + + /** + * @var ResponseResolutionerInterface|null + */ + private ?ResponseResolutionerInterface $responseResolutioner = null; /** * Constructor of the class @@ -56,69 +68,110 @@ class RouteCollector * @param RouteCollectionFactoryInterface|null $collectionFactory * @param RouteFactoryInterface|null $routeFactory * @param ReferenceResolverInterface|null $referenceResolver + * @param ParameterResolutionerInterface|null $parameterResolutioner + * @param ResponseResolutionerInterface|null $responseResolutioner */ public function __construct( ?RouteCollectionFactoryInterface $collectionFactory = null, ?RouteFactoryInterface $routeFactory = null, - ?ReferenceResolverInterface $referenceResolver = null + ?ReferenceResolverInterface $referenceResolver = null, + ?ParameterResolutionerInterface $parameterResolutioner = null, + ?ResponseResolutionerInterface $responseResolutioner = null ) { $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory(); $this->routeFactory = $routeFactory ?? new RouteFactory(); - $this->referenceResolver = $referenceResolver ?? new ReferenceResolver(); + + $this->parameterResolutioner = $parameterResolutioner; + $this->responseResolutioner = $responseResolutioner; + + $this->referenceResolver = $referenceResolver ?? new ReferenceResolver( + $this->parameterResolutioner ??= new ParameterResolutioner(), + $this->responseResolutioner ??= new ResponseResolutioner() + ); $this->collection = $this->collectionFactory->createCollection(); } /** - * Sets the given container to the collector + * Gets the collector's route collection * - * @param ContainerInterface|null $container + * @return RouteCollectionInterface + */ + public function getCollection(): RouteCollectionInterface + { + return $this->collection; + } + + /** + * Sets the given container to the parameter resolutioner + * + * @param ContainerInterface $container * * @return void * - * @since 2.9.0 + * @throws LogicException + * If a custom reference resolver was setted. */ - public function setContainer(?ContainerInterface $container): void + public function setContainer(ContainerInterface $container): void { - $this->referenceResolver->setContainer($container); + if (!isset($this->parameterResolutioner)) { + throw new LogicException( + 'The route collector cannot accept the container ' . + 'because a custom reference resolver was setted' + ); + } + + $this->parameterResolutioner->addResolver( + new DependencyInjectionParameterResolver($container) + ); } /** - * Adds the given parameter resolver(s) to the collector + * Adds the given parameter resolver(s) to the parameter resolutioner * * @param ParameterResolverInterface ...$resolvers * * @return void * + * @throws LogicException + * If a custom reference resolver was setted. + * * @since 3.0.0 */ public function addParameterResolver(ParameterResolverInterface ...$resolvers): void { - $this->referenceResolver->addParameterResolver(...$resolvers); + if (!isset($this->parameterResolutioner)) { + throw new LogicException( + 'The route collector cannot accept the parameter resolver ' . + 'because a custom reference resolver was setted' + ); + } + + $this->parameterResolutioner->addResolver(...$resolvers); } /** - * Adds the given response resolver(s) to the collector + * Adds the given response resolver(s) to the response resolutioner * * @param ResponseResolverInterface ...$resolvers * * @return void * + * @throws LogicException + * If a custom reference resolver was setted. + * * @since 3.0.0 */ public function addResponseResolver(ResponseResolverInterface ...$resolvers): void { - $this->referenceResolver->addResponseResolver(...$resolvers); - } + if (!isset($this->responseResolutioner)) { + throw new LogicException( + 'The route collector cannot accept the response resolver ' . + 'because a custom reference resolver was setted' + ); + } - /** - * Gets the collector's route collection - * - * @return RouteCollectionInterface - */ - public function getCollection(): RouteCollectionInterface - { - return $this->collection; + $this->responseResolutioner->addResolver(...$resolvers); } /** @@ -126,7 +179,7 @@ public function getCollection(): RouteCollectionInterface * * @param string $name * @param string $path - * @param list $methods + * @param array $methods * @param mixed $requestHandler * @param array $middlewares * @param array $attributes @@ -176,7 +229,7 @@ public function head( return $this->route( $name, $path, - [Router::METHOD_HEAD], + [RouteInterface::METHOD_HEAD], $requestHandler, $middlewares, $attributes @@ -204,7 +257,7 @@ public function get( return $this->route( $name, $path, - [Router::METHOD_GET], + [RouteInterface::METHOD_GET], $requestHandler, $middlewares, $attributes @@ -232,7 +285,7 @@ public function post( return $this->route( $name, $path, - [Router::METHOD_POST], + [RouteInterface::METHOD_POST], $requestHandler, $middlewares, $attributes @@ -260,7 +313,7 @@ public function put( return $this->route( $name, $path, - [Router::METHOD_PUT], + [RouteInterface::METHOD_PUT], $requestHandler, $middlewares, $attributes @@ -288,7 +341,7 @@ public function patch( return $this->route( $name, $path, - [Router::METHOD_PATCH], + [RouteInterface::METHOD_PATCH], $requestHandler, $middlewares, $attributes @@ -316,7 +369,7 @@ public function delete( return $this->route( $name, $path, - [Router::METHOD_DELETE], + [RouteInterface::METHOD_DELETE], $requestHandler, $middlewares, $attributes @@ -344,7 +397,7 @@ public function purge( return $this->route( $name, $path, - [Router::METHOD_PURGE], + [RouteInterface::METHOD_PURGE], $requestHandler, $middlewares, $attributes @@ -364,7 +417,9 @@ public function group(callable $callback, array $middlewares = []): RouteCollect $collector = new self( $this->collectionFactory, $this->routeFactory, - $this->referenceResolver + $this->referenceResolver, + $this->parameterResolutioner, + $this->responseResolutioner ); $callback($collector); diff --git a/src/RouteInterface.php b/src/RouteInterface.php index dbc66928..af817570 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -14,6 +14,7 @@ /** * Import classes */ +use Fig\Http\Message\RequestMethodInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use ReflectionClass; @@ -24,7 +25,7 @@ /** * RouteInterface */ -interface RouteInterface extends RequestHandlerInterface +interface RouteInterface extends RequestHandlerInterface, RequestMethodInterface { /** diff --git a/src/Router.php b/src/Router.php index 267d8703..17eae62f 100644 --- a/src/Router.php +++ b/src/Router.php @@ -42,16 +42,9 @@ /** * Router */ -class Router implements MiddlewareInterface, RequestHandlerInterface, RequestMethodInterface +class Router implements RequestHandlerInterface, RequestMethodInterface { - /** - * Server Request attribute name for routing error instance - * - * @var string - */ - public const ATTR_NAME_FOR_ROUTING_ERROR = '@routing-error'; - /** * Global patterns * @@ -531,18 +524,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface return $handler->handle($request); } - /** - * {@inheritdoc} - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - try { - return $this->handle($request); - } catch (PageNotFoundException|MethodNotAllowedException $e) { - return $handler->handle($request->withAttribute(self::ATTR_NAME_FOR_ROUTING_ERROR, $e)); - } - } - /** * Loads routes through the given loaders * diff --git a/src/RouterBuilder.php b/src/RouterBuilder.php index 598662f8..fe1f96d7 100644 --- a/src/RouterBuilder.php +++ b/src/RouterBuilder.php @@ -14,7 +14,6 @@ /** * Import classes */ -use Psr\Container\ContainerInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\SimpleCache\CacheInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; @@ -34,11 +33,6 @@ final class RouterBuilder */ private $eventDispatcher = null; - /** - * @var ContainerInterface|null - */ - private $container = null; - /** * @var CacheInterface|null */ @@ -90,20 +84,6 @@ public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher): return $this; } - /** - * Sets the given container to the builder - * - * @param ContainerInterface|null $container - * - * @return self - */ - public function setContainer(?ContainerInterface $container): self - { - $this->container = $container; - - return $this; - } - /** * Sets the given cache to the builder * @@ -238,12 +218,10 @@ public function build(): Router } if (isset($this->configLoader)) { - $this->configLoader->setContainer($this->container); $router->load($this->configLoader); } if (isset($this->descriptorLoader)) { - $this->descriptorLoader->setContainer($this->container); $this->descriptorLoader->setCache($this->cache); $this->descriptorLoader->setCacheKey($this->cacheKey); $router->load($this->descriptorLoader); From 088c024a942439dd8c458a90b9611bbb2f6aacf7 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Thu, 26 Jan 2023 04:21:58 +0100 Subject: [PATCH 044/180] v3 --- src/ReferenceResolver.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index 77230a21..c48a3670 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -46,6 +46,7 @@ final class ReferenceResolver implements ReferenceResolverInterface * @var ParameterResolutionerInterface */ private ParameterResolutionerInterface $parameterResolutioner; + /** * The resolver's response resolutioner * From e77ac9185ef9163c8f309dfcd477885a2554d09b Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 27 Jan 2023 20:46:48 +0100 Subject: [PATCH 045/180] v3 --- functions/get_debug_type.php | 2 +- src/Exception/Http/HttpException.php | 2 +- src/Exception/ResolvingException.php | 21 -------- src/Exception/ResolvingParameterException.php | 2 +- src/Exception/ResolvingReferenceException.php | 2 +- src/Exception/ResolvingResponseException.php | 2 +- src/Loader/ConfigLoader.php | 18 ++++--- src/Loader/DescriptorLoader.php | 26 +++++++--- src/Middleware/CallableMiddleware.php | 10 ++-- src/RequestHandler/CallableRequestHandler.php | 8 ++- src/RouteCollection.php | 50 +++++++++++++------ src/RouteCollectionInterface.php | 20 ++++++-- src/RouteCollector.php | 34 +++++++++---- src/RouteInterface.php | 4 +- src/Router.php | 20 ++++---- 15 files changed, 129 insertions(+), 92 deletions(-) delete mode 100644 src/Exception/ResolvingException.php diff --git a/functions/get_debug_type.php b/functions/get_debug_type.php index 282b5875..ed2302a8 100644 --- a/functions/get_debug_type.php +++ b/functions/get_debug_type.php @@ -9,7 +9,7 @@ * @link https://github.com/sunrise-php/http-router */ -if (PHP_MAJOR_VERSION < 8) { +if (!function_exists('get_debug_type')) { /** * Polyfill for the get_debug_type function diff --git a/src/Exception/Http/HttpException.php b/src/Exception/Http/HttpException.php index b29cc1e5..9c01bcf1 100644 --- a/src/Exception/Http/HttpException.php +++ b/src/Exception/Http/HttpException.php @@ -30,7 +30,7 @@ class HttpException extends RuntimeException implements HttpExceptionInterface * * @var int */ - private $statusCode; + private int $statusCode; /** * Constructor of the class diff --git a/src/Exception/ResolvingException.php b/src/Exception/ResolvingException.php deleted file mode 100644 index 85953a58..00000000 --- a/src/Exception/ResolvingException.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception; - -/** - * ResolvingException - * - * @since 3.0.0 - */ -class ResolvingException extends LogicException -{ -} diff --git a/src/Exception/ResolvingParameterException.php b/src/Exception/ResolvingParameterException.php index 3e46f9e4..a84f69f8 100644 --- a/src/Exception/ResolvingParameterException.php +++ b/src/Exception/ResolvingParameterException.php @@ -16,6 +16,6 @@ * * @since 3.0.0 */ -class ResolvingParameterException extends ResolvingException +class ResolvingParameterException extends LogicException { } diff --git a/src/Exception/ResolvingReferenceException.php b/src/Exception/ResolvingReferenceException.php index 63ddb6f9..ce74aa7f 100644 --- a/src/Exception/ResolvingReferenceException.php +++ b/src/Exception/ResolvingReferenceException.php @@ -16,6 +16,6 @@ * * @since 3.0.0 */ -class ResolvingReferenceException extends ResolvingException +class ResolvingReferenceException extends LogicException { } diff --git a/src/Exception/ResolvingResponseException.php b/src/Exception/ResolvingResponseException.php index 266aaca9..c2160da7 100644 --- a/src/Exception/ResolvingResponseException.php +++ b/src/Exception/ResolvingResponseException.php @@ -16,6 +16,6 @@ * * @since 3.0.0 */ -class ResolvingResponseException extends ResolvingException +class ResolvingResponseException extends LogicException { } diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index d3f36285..3beff857 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -113,14 +113,16 @@ public function __construct( * @return void * * @throws LogicException - * If a custom reference resolver was setted. + * If a custom reference resolver was setted + * and a parameter resolutioner was not passed. */ public function setContainer(ContainerInterface $container): void { if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The config route loader cannot accept the container ' . - 'because a custom reference resolver was setted' + 'because a custom reference resolver was setted ' . + 'and a parameter resolutioner was not passed' ); } @@ -137,7 +139,8 @@ public function setContainer(ContainerInterface $container): void * @return void * * @throws LogicException - * If a custom reference resolver was setted. + * If a custom reference resolver was setted + * and a parameter resolutioner was not passed. * * @since 3.0.0 */ @@ -146,7 +149,8 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The config route loader cannot accept the parameter resolver ' . - 'because a custom reference resolver was setted' + 'because a custom reference resolver was setted' . + 'and a parameter resolutioner was not passed' ); } @@ -161,7 +165,8 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): * @return void * * @throws LogicException - * If a custom reference resolver was setted. + * If a custom reference resolver was setted + * and a response resolutioner was not passed. * * @since 3.0.0 */ @@ -170,7 +175,8 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo if (!isset($this->responseResolutioner)) { throw new LogicException( 'The config route loader cannot accept the response resolver ' . - 'because a custom reference resolver was setted' + 'because a custom reference resolver was setted' . + 'and a response resolutioner was not passed' ); } diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index bd1e6abe..a566d934 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -117,13 +117,15 @@ final class DescriptorLoader implements LoaderInterface * @param ReferenceResolverInterface|null $referenceResolver * @param ParameterResolutionerInterface|null $parameterResolutioner * @param ResponseResolutionerInterface|null $responseResolutioner + * @param \Doctrine\Common\Annotations\Reader|null $annotationReader */ public function __construct( ?RouteCollectionFactoryInterface $collectionFactory = null, ?RouteFactoryInterface $routeFactory = null, ?ReferenceResolverInterface $referenceResolver = null, ?ParameterResolutionerInterface $parameterResolutioner = null, - ?ResponseResolutionerInterface $responseResolutioner = null + ?ResponseResolutionerInterface $responseResolutioner = null, + ?\Doctrine\Common\Annotations\Reader $annotationReader = null ) { $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory(); $this->routeFactory = $routeFactory ?? new RouteFactory(); @@ -138,7 +140,9 @@ public function __construct( $this->annotationReader = new AnnotationReader(); - if (8 > PHP_MAJOR_VERSION) { + if (isset($annotationReader)) { + $this->annotationReader->setAnnotationReader($annotationReader); + } elseif (PHP_MAJOR_VERSION < 8) { $this->annotationReader->useDefaultAnnotationReader(); } } @@ -151,14 +155,16 @@ public function __construct( * @return void * * @throws LogicException - * If a custom reference resolver was setted. + * If a custom reference resolver was setted + * and a parameter resolutioner was not passed. */ public function setContainer(ContainerInterface $container): void { if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The descriptor route loader cannot accept the container ' . - 'because a custom reference resolver was setted' + 'because a custom reference resolver was setted ' . + 'and a parameter resolutioner was not passed' ); } @@ -175,7 +181,8 @@ public function setContainer(ContainerInterface $container): void * @return void * * @throws LogicException - * If a custom reference resolver was setted. + * If a custom reference resolver was setted + * and a parameter resolutioner was not passed. * * @since 3.0.0 */ @@ -184,7 +191,8 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The descriptor route loader cannot accept the parameter resolver ' . - 'because a custom reference resolver was setted' + 'because a custom reference resolver was setted ' . + 'and a parameter resolutioner was not passed' ); } @@ -199,7 +207,8 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): * @return void * * @throws LogicException - * If a custom reference resolver was setted. + * If a custom reference resolver was setted + * and a response resolutioner was not passed. * * @since 3.0.0 */ @@ -208,7 +217,8 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo if (!isset($this->responseResolutioner)) { throw new LogicException( 'The descriptor route loader cannot accept the response resolver ' . - 'because a custom reference resolver was setted' + 'because a custom reference resolver was setted ' . + 'and a response resolutioner was not passed' ); } diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index 5d8d3a57..67751e47 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -93,14 +93,12 @@ public function getReflection(): ReflectionFunctionAbstract */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $resolvers = [ - new KnownTypeParameterResolver(ServerRequestInterface::class, $request), - new KnownTypeParameterResolver(RequestHandlerInterface::class, $handler), - ]; - $arguments = $this->parameterResolutioner ->withContext($request) - ->withPriorityResolver(...$resolvers) + ->withPriorityResolver( + new KnownTypeParameterResolver(ServerRequestInterface::class, $request), + new KnownTypeParameterResolver(RequestHandlerInterface::class, $handler) + ) ->resolveParameters(...$this->getReflection()->getParameters()); /** @var mixed */ diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index 4c17c11c..2b4552d5 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -90,13 +90,11 @@ public function getReflection(): ReflectionFunctionAbstract */ public function handle(ServerRequestInterface $request): ResponseInterface { - $resolvers = [ - new KnownTypeParameterResolver(ServerRequestInterface::class, $request), - ]; - $arguments = $this->parameterResolutioner ->withContext($request) - ->withPriorityResolver(...$resolvers) + ->withPriorityResolver( + new KnownTypeParameterResolver(ServerRequestInterface::class, $request) + ) ->resolveParameters(...$this->getReflection()->getParameters()); /** @var mixed */ diff --git a/src/RouteCollection.php b/src/RouteCollection.php index d6da65c3..136131f9 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -20,6 +20,7 @@ * Import functions */ use function array_merge; +use function count; /** * RouteCollection @@ -32,9 +33,9 @@ class RouteCollection implements RouteCollectionInterface /** * The collection routes * - * @var list + * @var array */ - private $routes = []; + private array $routes = []; /** * Constructor of the class @@ -43,9 +44,7 @@ class RouteCollection implements RouteCollectionInterface */ public function __construct(RouteInterface ...$routes) { - /** @var list $routes */ - - $this->routes = $routes; + $this->add(...$routes); } /** @@ -53,7 +52,12 @@ public function __construct(RouteInterface ...$routes) */ public function all(): array { - return $this->routes; + $routes = []; + foreach ($this->routes as $route) { + $routes[] = $route; + } + + return $routes; } /** @@ -61,13 +65,7 @@ public function all(): array */ public function get(string $name): ?RouteInterface { - foreach ($this->routes as $route) { - if ($name === $route->getName()) { - return $route; - } - } - - return null; + return $this->routes[$name] ?? null; } /** @@ -75,7 +73,7 @@ public function get(string $name): ?RouteInterface */ public function has(string $name): bool { - return $this->get($name) instanceof RouteInterface; + return isset($this->routes[$name]); } /** @@ -84,7 +82,7 @@ public function has(string $name): bool public function add(RouteInterface ...$routes): RouteCollectionInterface { foreach ($routes as $route) { - $this->routes[] = $route; + $this->routes[$route->getName()] = $route; } return $this; @@ -153,7 +151,7 @@ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteCollect /** * {@inheritdoc} */ - public function prependMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface + public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface { foreach ($this->routes as $route) { $route->setMiddlewares(...array_merge($middlewares, $route->getMiddlewares())); @@ -161,4 +159,24 @@ public function prependMiddleware(MiddlewareInterface ...$middlewares): RouteCol return $this; } + + /** + * {@inheritdoc} + */ + public function addTag(string ...$tags): RouteCollectionInterface + { + foreach ($this->routes as $route) { + $route->addTag(...$tags); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function count(): int + { + return count($this->routes); + } } diff --git a/src/RouteCollectionInterface.php b/src/RouteCollectionInterface.php index ff392e70..6d5f1b17 100644 --- a/src/RouteCollectionInterface.php +++ b/src/RouteCollectionInterface.php @@ -15,11 +15,12 @@ * Import classes */ use Psr\Http\Server\MiddlewareInterface; +use Countable; /** * RouteCollectionInterface */ -interface RouteCollectionInterface +interface RouteCollectionInterface extends Countable { /** @@ -116,13 +117,24 @@ public function addMethod(string ...$methods): RouteCollectionInterface; public function addMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface; /** - * Adds the given middleware(s) to the beginning of all routes in the collection + * Adds the given priority middleware(s) to all routes in the collection * * @param MiddlewareInterface ...$middlewares * * @return RouteCollectionInterface * - * @since 2.9.0 + * @since 3.0.0 + */ + public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface; + + /** + * Adds the given tag(s) to all routes in the collection + * + * @param string ...$tags + * + * @return RouteCollectionInterface + * + * @since 3.0.0 */ - public function prependMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface; + public function addTag(string ...$tags): RouteCollectionInterface; } diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 72208412..bdc9442d 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -102,6 +102,16 @@ public function getCollection(): RouteCollectionInterface return $this->collection; } + /** + * Gets the collector's routes + * + * @return list + */ + public function getRoutes(): array + { + return $this->collection->all(); + } + /** * Sets the given container to the parameter resolutioner * @@ -110,14 +120,16 @@ public function getCollection(): RouteCollectionInterface * @return void * * @throws LogicException - * If a custom reference resolver was setted. + * If a custom reference resolver was setted + * and a parameter resolutioner was not passed. */ public function setContainer(ContainerInterface $container): void { if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The route collector cannot accept the container ' . - 'because a custom reference resolver was setted' + 'because a custom reference resolver was setted ' . + 'and a parameter resolutioner was not passed' ); } @@ -134,7 +146,8 @@ public function setContainer(ContainerInterface $container): void * @return void * * @throws LogicException - * If a custom reference resolver was setted. + * If a custom reference resolver was setted + * and a parameter resolutioner was not passed. * * @since 3.0.0 */ @@ -143,7 +156,8 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The route collector cannot accept the parameter resolver ' . - 'because a custom reference resolver was setted' + 'because a custom reference resolver was setted ' . + 'and a parameter resolutioner was not passed' ); } @@ -158,7 +172,8 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): * @return void * * @throws LogicException - * If a custom reference resolver was setted. + * If a custom reference resolver was setted + * and a response resolutioner was not passed. * * @since 3.0.0 */ @@ -167,7 +182,8 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo if (!isset($this->responseResolutioner)) { throw new LogicException( 'The route collector cannot accept the response resolver ' . - 'because a custom reference resolver was setted' + 'because a custom reference resolver was setted ' . + 'and a response resolutioner was not passed' ); } @@ -424,13 +440,11 @@ public function group(callable $callback, array $middlewares = []): RouteCollect $callback($collector); - $collector->collection->prependMiddleware( + $collector->collection->addPriorityMiddleware( ...$this->referenceResolver->resolveMiddlewares($middlewares) ); - $this->collection->add( - ...$collector->collection->all() - ); + $this->collection->add(...$collector->collection->all()); return $collector->collection; } diff --git a/src/RouteInterface.php b/src/RouteInterface.php index af817570..534b9928 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -29,7 +29,7 @@ interface RouteInterface extends RequestHandlerInterface, RequestMethodInterface { /** - * Request attribute name for the route instance + * Request attribute name for a route instance * * @var string * @@ -63,7 +63,7 @@ public function getPath(): string; /** * Gets the route methods * - * @return string[] + * @return list */ public function getMethods(): array; diff --git a/src/Router.php b/src/Router.php index 17eae62f..229cac16 100644 --- a/src/Router.php +++ b/src/Router.php @@ -477,17 +477,19 @@ public function match(ServerRequestInterface $request): RouteInterface public function run(ServerRequestInterface $request): ResponseInterface { // lazy resolving of the given request... - $routing = new UnsafeCallableRequestHandler(function (ServerRequestInterface $request): ResponseInterface { - $this->matchedRoute = $this->match($request); + $routing = new UnsafeCallableRequestHandler( + function (ServerRequestInterface $request): ResponseInterface { + $this->matchedRoute = $this->match($request); + + if (isset($this->eventDispatcher)) { + $this->eventDispatcher->dispatch( + new RouteEvent($this->matchedRoute, $request) + ); + } - if (isset($this->eventDispatcher)) { - $this->eventDispatcher->dispatch( - new RouteEvent($this->matchedRoute, $request) - ); + return $this->matchedRoute->handle($request); } - - return $this->matchedRoute->handle($request); - }); + ); $middlewares = $this->getMiddlewares(); if (empty($middlewares)) { From 486054215d4a5429a0bfabf700972e9b48ac9e89 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 27 Jan 2023 20:50:44 +0100 Subject: [PATCH 046/180] v3 --- src/Loader/ConfigLoader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 3beff857..809323b3 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -149,7 +149,7 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The config route loader cannot accept the parameter resolver ' . - 'because a custom reference resolver was setted' . + 'because a custom reference resolver was setted ' . 'and a parameter resolutioner was not passed' ); } @@ -175,7 +175,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo if (!isset($this->responseResolutioner)) { throw new LogicException( 'The config route loader cannot accept the response resolver ' . - 'because a custom reference resolver was setted' . + 'because a custom reference resolver was setted ' . 'and a response resolutioner was not passed' ); } From 0d27d08d8744ac042444dc0dd47e097fab9d4b82 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Wed, 1 Feb 2023 19:46:29 +0100 Subject: [PATCH 047/180] v3 --- composer.json | 4 +- src/Annotation/RequestEntity.php | 41 +++++ src/Annotation/Route.php | 2 +- src/AnnotationReader.php | 4 +- src/ClassResolver.php | 29 ++-- src/ClassResolverInterface.php | 12 +- src/Exception/EntityNotFoundException.php | 26 +++ .../UnprocessableRequestBodyException.php | 52 ++++++ .../UnprocessableRequestQueryException.php | 52 ++++++ src/Middleware/UnsafeCallableMiddleware.php | 7 +- src/ParameterResolutioner.php | 50 ++---- .../AbstractParameterResolver.php | 60 +++++++ .../KnownTypeParameterResolver.php | 10 +- .../RequestBodyParameterResolver.php | 27 ++- .../RequestEntityParameterResolver.php | 158 ++++++++++++++++++ .../RequestQueryParameterResolver.php | 27 ++- src/ParameterResolverInterface.php | 1 - src/ReferenceResolver.php | 52 +++--- .../UnsafeCallableRequestHandler.php | 7 +- src/Route.php | 30 +++- src/RouteCollection.php | 12 ++ src/RouteCollectionInterface.php | 12 ++ src/RouteCollector.php | 20 +-- src/RouteInterface.php | 31 +++- src/Router.php | 44 ++--- 25 files changed, 616 insertions(+), 154 deletions(-) create mode 100644 src/Annotation/RequestEntity.php create mode 100644 src/Exception/EntityNotFoundException.php create mode 100644 src/Exception/UnprocessableRequestBodyException.php create mode 100644 src/Exception/UnprocessableRequestQueryException.php create mode 100644 src/ParameterResolver/AbstractParameterResolver.php create mode 100644 src/ParameterResolver/RequestEntityParameterResolver.php diff --git a/composer.json b/composer.json index 8dc8187d..81fa0839 100644 --- a/composer.json +++ b/composer.json @@ -37,12 +37,14 @@ }, "require-dev": { "doctrine/annotations": "^2.0", + "doctrine/persistence": "^3.1", "phpunit/phpunit": "~9.5.0", "sunrise/coding-standard": "~1.0.0", "sunrise/http-message": "^3.0", "sunrise/hydrator": "^2.7", "symfony/console": "^5.4", - "symfony/event-dispatcher": "^4.4" + "symfony/event-dispatcher": "^4.4", + "symfony/validator": "^5.4" }, "autoload": { "files": [ diff --git a/src/Annotation/RequestEntity.php b/src/Annotation/RequestEntity.php new file mode 100644 index 00000000..56fac3de --- /dev/null +++ b/src/Annotation/RequestEntity.php @@ -0,0 +1,41 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Annotation; + +/** + * Import classes + */ +use Attribute; + +/** + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_PARAMETER)] +final class RequestEntity +{ + + /** + * Constructor of the class + * + * @param string|null $em + * @param string $findBy + * @param string $paramKey + * @param array $criteria + */ + public function __construct( + public ?string $em = null, + public string $findBy = 'id', + public string $paramKey = 'id', + public array $criteria = [] + ) { + } +} diff --git a/src/Annotation/Route.php b/src/Annotation/Route.php index f2fce7b2..9f0d25f4 100644 --- a/src/Annotation/Route.php +++ b/src/Annotation/Route.php @@ -46,7 +46,7 @@ final class Route implements RequestMethodInterface /** * The descriptor holder * - * @var class-string|array{0: class-string, 1: string}|null + * @var class-string|array{0: class-string, 1: non-empty-string}|null * * @internal */ diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php index 67f6321d..f20c850d 100644 --- a/src/AnnotationReader.php +++ b/src/AnnotationReader.php @@ -80,7 +80,7 @@ public function useDefaultAnnotationReader(): void /** * Gets annotations from the given class or method by the given annotation name * - * @param ReflectionClass|ReflectionMethod $classOrMethod + * @param Reflector $classOrMethod * @param class-string $annotationName * * @return list @@ -88,8 +88,6 @@ public function useDefaultAnnotationReader(): void * @throws InvalidArgumentException * If the given reflection isn't supported. * - * @psalm-suppress RedundantConditionGivenDocblockType - * * @template T */ public function getClassOrMethodAnnotations(Reflector $classOrMethod, string $annotationName): array diff --git a/src/ClassResolver.php b/src/ClassResolver.php index 403e9ff5..e3d14e26 100644 --- a/src/ClassResolver.php +++ b/src/ClassResolver.php @@ -28,6 +28,10 @@ * ClassResolver * * @since 3.0.0 + * + * @template T of object + * + * @implements ClassResolverInterface */ final class ClassResolver implements ClassResolverInterface { @@ -35,7 +39,7 @@ final class ClassResolver implements ClassResolverInterface /** * Map of classes that are already resolved * - * @var array + * @var array, T> */ private array $resolvedClasses = []; @@ -59,24 +63,24 @@ public function __construct(ParameterResolutionerInterface $parameterResolutione /** * {@inheritdoc} */ - public function resolveClass(string $classname): object + public function resolveClass(string $className): object { - if (isset($this->resolvedClasses[$classname])) { - return $this->resolvedClasses[$classname]; + if (isset($this->resolvedClasses[$className])) { + return $this->resolvedClasses[$className]; } - if (!class_exists($classname)) { + if (!class_exists($className)) { throw new InvalidArgumentException(sprintf( - 'The class %s was not found', - $classname + 'Class %s does not exist', + $className )); } - $reflection = new ReflectionClass($classname); + $reflection = new ReflectionClass($className); if (!$reflection->isInstantiable()) { throw new LogicException(sprintf( - 'The class %s cannot be initialized directly', - $classname + 'Class %s cannot be initialized', + $className )); } @@ -88,6 +92,9 @@ public function resolveClass(string $classname): object ); } - return $this->resolvedClasses[$classname] = $reflection->newInstance(...$arguments); + /** @var T */ + $this->resolvedClasses[$className] = $reflection->newInstance(...$arguments); + + return $this->resolvedClasses[$className]; } } diff --git a/src/ClassResolverInterface.php b/src/ClassResolverInterface.php index 2757e97d..c76bab7c 100644 --- a/src/ClassResolverInterface.php +++ b/src/ClassResolverInterface.php @@ -21,22 +21,24 @@ * ClassResolverInterface * * @since 3.0.0 + * + * @template T of object */ interface ClassResolverInterface { /** - * Resolves the given class by its name + * Resolves the given named class * - * @param class-string $classname + * @param class-string $className * - * @return object + * @return T * * @throws InvalidArgumentException * If the class doesn't exist. * * @throws LogicException - * If the class cannot be initialized directly. + * If the class cannot be resolved. */ - public function resolveClass(string $classname): object; + public function resolveClass(string $className): object; } diff --git a/src/Exception/EntityNotFoundException.php b/src/Exception/EntityNotFoundException.php new file mode 100644 index 00000000..460ead31 --- /dev/null +++ b/src/Exception/EntityNotFoundException.php @@ -0,0 +1,26 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; + +/** + * EntityNotFoundException + * + * @since 3.0.0 + */ +class EntityNotFoundException extends HttpNotFoundException +{ +} diff --git a/src/Exception/UnprocessableRequestBodyException.php b/src/Exception/UnprocessableRequestBodyException.php new file mode 100644 index 00000000..4cb2de1c --- /dev/null +++ b/src/Exception/UnprocessableRequestBodyException.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; +use Symfony\Component\Validator\ConstraintViolationListInterface; + +/** + * UnprocessableRequestBodyException + * + * @since 3.0.0 + */ +class UnprocessableRequestBodyException extends HttpUnprocessableEntityException +{ + + /** + * @var ConstraintViolationListInterface + */ + private ConstraintViolationListInterface $violations; + + /** + * Constructor of the class + * + * @param ConstraintViolationListInterface $violations + */ + public function __construct(ConstraintViolationListInterface $violations) + { + $this->violations = $violations; + } + + /** + * Gets the violations list + * + * @return ConstraintViolationListInterface + */ + final public function getViolations(): ConstraintViolationListInterface + { + return $this->violations; + } +} diff --git a/src/Exception/UnprocessableRequestQueryException.php b/src/Exception/UnprocessableRequestQueryException.php new file mode 100644 index 00000000..10193101 --- /dev/null +++ b/src/Exception/UnprocessableRequestQueryException.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; +use Symfony\Component\Validator\ConstraintViolationListInterface; + +/** + * UnprocessableRequestQueryException + * + * @since 3.0.0 + */ +class UnprocessableRequestQueryException extends HttpUnprocessableEntityException +{ + + /** + * @var ConstraintViolationListInterface + */ + private ConstraintViolationListInterface $violations; + + /** + * Constructor of the class + * + * @param ConstraintViolationListInterface $violations + */ + public function __construct(ConstraintViolationListInterface $violations) + { + $this->violations = $violations; + } + + /** + * Gets the violations list + * + * @return ConstraintViolationListInterface + */ + final public function getViolations(): ConstraintViolationListInterface + { + return $this->violations; + } +} diff --git a/src/Middleware/UnsafeCallableMiddleware.php b/src/Middleware/UnsafeCallableMiddleware.php index b8b570a0..430f0149 100644 --- a/src/Middleware/UnsafeCallableMiddleware.php +++ b/src/Middleware/UnsafeCallableMiddleware.php @@ -23,6 +23,8 @@ * UnsafeCallableMiddleware * * @since 3.0.0 + * + * @template T as callable(ServerRequestInterface=, RequestHandlerInterface=): ResponseInterface */ final class UnsafeCallableMiddleware implements MiddlewareInterface { @@ -30,14 +32,14 @@ final class UnsafeCallableMiddleware implements MiddlewareInterface /** * The middleware callback * - * @var callable + * @var T */ private $callback; /** * Constructor of the class * - * @param callable $callback + * @param T $callback */ public function __construct(callable $callback) { @@ -49,7 +51,6 @@ public function __construct(callable $callback) */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - /** @var ResponseInterface */ return ($this->callback)($request, $handler); } } diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index ed23459a..7004f4a0 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -15,7 +15,6 @@ * Import classes */ use Sunrise\Http\Router\Exception\ResolvingParameterException; -use ReflectionFunctionAbstract; use ReflectionMethod; use ReflectionParameter; @@ -104,7 +103,6 @@ public function resolveParameters(ReflectionParameter ...$parameters): array * @param ReflectionParameter $parameter * * @return mixed - * The ready-to-pass argument. * * @throws ResolvingParameterException * If the parameter cannot be resolved to an argument. @@ -136,47 +134,19 @@ private function resolveParameter(ReflectionParameter $parameter) */ private function stringifyParameter(ReflectionParameter $parameter): string { - return ($parameter->getDeclaringFunction() instanceof ReflectionMethod) ? - $this->stringifyMethodParameter($parameter->getDeclaringFunction(), $parameter) : - $this->stringifyFunctionParameter($parameter->getDeclaringFunction(), $parameter); - } - - /** - * Stringifies the given method parameter - * - * @param ReflectionMethod $method - * @param ReflectionParameter $parameter - * - * @return string - */ - private function stringifyMethodParameter( - ReflectionMethod $method, - ReflectionParameter $parameter - ) : string { - return sprintf( - '%s::%s($%s[%d])', - $method->getDeclaringClass()->getName(), - $method->getName(), - $parameter->getName(), - $parameter->getPosition() - ); - } + if ($parameter->getDeclaringFunction() instanceof ReflectionMethod) { + return sprintf( + '%s::%s($%s[%d])', + $parameter->getDeclaringFunction()->getDeclaringClass()->getName(), + $parameter->getDeclaringFunction()->getName(), + $parameter->getName(), + $parameter->getPosition() + ); + } - /** - * Stringifies the given function parameter - * - * @param ReflectionFunctionAbstract $function - * @param ReflectionParameter $parameter - * - * @return string - */ - private function stringifyFunctionParameter( - ReflectionFunctionAbstract $function, - ReflectionParameter $parameter - ) : string { return sprintf( '%s($%s[%d])', - $function->getName(), + $parameter->getDeclaringFunction()->getName(), $parameter->getName(), $parameter->getPosition() ); diff --git a/src/ParameterResolver/AbstractParameterResolver.php b/src/ParameterResolver/AbstractParameterResolver.php new file mode 100644 index 00000000..1d4c0898 --- /dev/null +++ b/src/ParameterResolver/AbstractParameterResolver.php @@ -0,0 +1,60 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ParameterResolver; + +/** + * Import classes + */ +use Sunrise\Http\Router\ParameterResolverInterface; +use ReflectionMethod; +use ReflectionParameter; + +/** + * Import functions + */ +use function sprintf; + +/** + * AbstractParameterResolver + * + * @since 3.0.0 + */ +abstract class AbstractParameterResolver implements ParameterResolverInterface +{ + + /** + * Stringifies the given parameter + * + * @param ReflectionParameter $parameter + * + * @return string + */ + final protected function stringifyParameter(ReflectionParameter $parameter): string + { + if ($parameter->getDeclaringFunction() instanceof ReflectionMethod) { + return sprintf( + '%s::%s($%s[%d])', + $parameter->getDeclaringFunction()->getDeclaringClass()->getName(), + $parameter->getDeclaringFunction()->getName(), + $parameter->getName(), + $parameter->getPosition() + ); + } + + return sprintf( + '%s($%s[%d])', + $parameter->getDeclaringFunction()->getName(), + $parameter->getName(), + $parameter->getPosition() + ); + } +} diff --git a/src/ParameterResolver/KnownTypeParameterResolver.php b/src/ParameterResolver/KnownTypeParameterResolver.php index ad4373b9..bc5b1808 100644 --- a/src/ParameterResolver/KnownTypeParameterResolver.php +++ b/src/ParameterResolver/KnownTypeParameterResolver.php @@ -28,26 +28,24 @@ /** * KnownTypeParameterResolver * - * @template T as object - * * @since 3.0.0 */ final class KnownTypeParameterResolver implements ParameterResolverInterface { /** - * @var class-string + * @var class-string */ private string $type; /** - * @var T + * @var object */ private object $value; /** - * @param class-string $type - * @param T $value + * @param class-string $type + * @param object $value * * @throws InvalidArgumentException * If the given value is not an instance of the given type. diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index bf5936f9..674b0fba 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -18,11 +18,13 @@ use Sunrise\Http\Router\Annotation\RequestBody; use Sunrise\Http\Router\Exception\InvalidRequestBodyException; use Sunrise\Http\Router\Exception\ResolvingParameterException; +use Sunrise\Http\Router\Exception\UnprocessableRequestBodyException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\RequestBodyInterface; use Sunrise\Hydrator\Exception\InvalidObjectException; use Sunrise\Hydrator\Exception\InvalidValueException; use Sunrise\Hydrator\HydratorInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; use ReflectionNamedType; use ReflectionParameter; @@ -51,12 +53,19 @@ final class RequestBodyParameterResolver implements ParameterResolverInterface */ private HydratorInterface $hydrator; + /** + * @var ValidatorInterface|null + */ + private ?ValidatorInterface $validator; + /** * @param HydratorInterface $hydrator + * @param ValidatorInterface|null $validator */ - public function __construct(HydratorInterface $hydrator) + public function __construct(HydratorInterface $hydrator, ?ValidatorInterface $validator = null) { $this->hydrator = $hydrator; + $this->validator = $validator; } /** @@ -94,7 +103,10 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo * If the object cannot be hydrated. * * @throws InvalidRequestBodyException - * If the request body isn't valid. + * If the request body structure isn't valid. + * + * @throws UnprocessableRequestBodyException + * If the request body data isn't valid. */ public function resolveParameter(ReflectionParameter $parameter, $context) { @@ -105,11 +117,20 @@ public function resolveParameter(ReflectionParameter $parameter, $context) $parameterType = $parameter->getType(); try { - return $this->hydrator->hydrate($parameterType->getName(), (array) $context->getParsedBody()); + $object = $this->hydrator->hydrate($parameterType->getName(), (array) $context->getParsedBody()); } catch (InvalidObjectException $e) { throw new ResolvingParameterException($e->getMessage(), 0, $e); } catch (InvalidValueException $e) { throw new InvalidRequestBodyException($e->getMessage(), 0, $e); } + + if (isset($this->validator)) { + $violations = $this->validator->validate($object); + if ($violations->count() > 0) { + throw new UnprocessableRequestBodyException($violations); + } + } + + return $object; } } diff --git a/src/ParameterResolver/RequestEntityParameterResolver.php b/src/ParameterResolver/RequestEntityParameterResolver.php new file mode 100644 index 00000000..8b91886f --- /dev/null +++ b/src/ParameterResolver/RequestEntityParameterResolver.php @@ -0,0 +1,158 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ParameterResolver; + +/** + * Import classes + */ +use Doctrine\Persistence\ManagerRegistry as EntityManagerRegistryInterface; +use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\Annotation\RequestEntity; +use Sunrise\Http\Router\Exception\EntityNotFoundException; +use Sunrise\Http\Router\Exception\ResolvingParameterException; +use ReflectionAttribute; +use ReflectionNamedType; +use ReflectionParameter; + +/** + * Import functions + */ +use function class_exists; +use function sprintf; + +/** + * RequestEntityParameterResolver + * + * @since 3.0.0 + */ +final class RequestEntityParameterResolver extends AbstractParameterResolver +{ + + /** + * @var EntityManagerRegistryInterface + */ + private EntityManagerRegistryInterface $entityManagerRegistry; + + /** + * @param EntityManagerRegistryInterface $entityManagerRegistry + */ + public function __construct(EntityManagerRegistryInterface $entityManagerRegistry) + { + $this->entityManagerRegistry = $entityManagerRegistry; + } + + /** + * {@inheritdoc} + */ + public function supportsParameter(ReflectionParameter $parameter, $context): bool + { + if (!($context instanceof ServerRequestInterface)) { + return false; + } + + if (!($parameter->getType() instanceof ReflectionNamedType)) { + return false; + } + + if ($parameter->getType()->isBuiltin()) { + return false; + } + + if (8 === PHP_MAJOR_VERSION && $parameter->getAttributes(RequestEntity::class)) { + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function resolveParameter(ReflectionParameter $parameter, $context) + { + /** @var ServerRequestInterface */ + $context = $context; + + /** @var ReflectionNamedType */ + $type = $parameter->getType(); + + /** @var array{0: ReflectionAttribute} */ + $attributes = $parameter->getAttributes(RequestEntity::class); + + /** @var RequestEntity */ + $requestEntity = $attributes[0]->newInstance(); + + /** @var mixed */ + $entityId = $context->getAttribute($requestEntity->paramKey); + if (!isset($entityId)) { + throw new ResolvingParameterException(sprintf( + '{%s} Unable to get Entity ID (%s) by key %s', + $this->stringifyParameter($parameter), + $requestEntity->findBy, + $requestEntity->paramKey + )); + } + + $entityName = $type->getName(); + if (!class_exists($entityName)) { + throw new ResolvingParameterException(sprintf( + '{%s} Entity %s does not exist', + $this->stringifyParameter($parameter), + $entityName + )); + } + + $entityManager = isset($requestEntity->em) ? + $this->entityManagerRegistry->getManager($requestEntity->em) : + $this->entityManagerRegistry->getManagerForClass($entityName); + + if (!isset($entityManager)) { + throw new ResolvingParameterException(sprintf( + '{%s} Unable to get Entity Manager for %s', + $this->stringifyParameter($parameter), + $entityName + )); + } + + $entityMetadata = $entityManager->getClassMetadata($entityName); + if (!$entityMetadata->hasField($requestEntity->findBy)) { + throw new ResolvingParameterException(sprintf( + '{%s} Entity %s does not contain field %s', + $this->stringifyParameter($parameter), + $entityName, + $requestEntity->findBy + )); + } + + $criteria = [ + $requestEntity->findBy => $entityId, + ]; + + $criteria += $requestEntity->criteria; + + $entity = $entityManager->getRepository($entityName) + ->findOneBy($criteria); + + if (isset($entity)) { + return $entity; + } + + if ($parameter->allowsNull()) { + return null; + } + + throw new EntityNotFoundException(sprintf( + '%s Not Found', + $entityMetadata->getReflectionClass()->getShortName() + )); + } +} diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php index 43ec85f7..cc529589 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -18,11 +18,13 @@ use Sunrise\Http\Router\Annotation\RequestQuery; use Sunrise\Http\Router\Exception\InvalidRequestQueryException; use Sunrise\Http\Router\Exception\ResolvingParameterException; +use Sunrise\Http\Router\Exception\UnprocessableRequestQueryException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\RequestQueryInterface; use Sunrise\Hydrator\Exception\InvalidObjectException; use Sunrise\Hydrator\Exception\InvalidValueException; use Sunrise\Hydrator\HydratorInterface; +use Symfony\Component\Validator\Validator\ValidatorInterface; use ReflectionNamedType; use ReflectionParameter; @@ -51,12 +53,19 @@ final class RequestQueryParameterResolver implements ParameterResolverInterface */ private HydratorInterface $hydrator; + /** + * @var ValidatorInterface|null + */ + private ?ValidatorInterface $validator; + /** * @param HydratorInterface $hydrator + * @param ValidatorInterface|null $validator */ - public function __construct(HydratorInterface $hydrator) + public function __construct(HydratorInterface $hydrator, ?ValidatorInterface $validator = null) { $this->hydrator = $hydrator; + $this->validator = $validator; } /** @@ -94,7 +103,10 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo * If the object cannot be hydrated. * * @throws InvalidRequestQueryException - * If the request query isn't valid. + * If the request query structure isn't valid. + * + * @throws UnprocessableRequestQueryException + * If the request query data isn't valid. */ public function resolveParameter(ReflectionParameter $parameter, $context) { @@ -105,11 +117,20 @@ public function resolveParameter(ReflectionParameter $parameter, $context) $parameterType = $parameter->getType(); try { - return $this->hydrator->hydrate($parameterType->getName(), $context->getQueryParams()); + $object = $this->hydrator->hydrate($parameterType->getName(), $context->getQueryParams()); } catch (InvalidObjectException $e) { throw new ResolvingParameterException($e->getMessage(), 0, $e); } catch (InvalidValueException $e) { throw new InvalidRequestQueryException($e->getMessage(), 0, $e); } + + if (isset($this->validator)) { + $violations = $this->validator->validate($object); + if ($violations->count() > 0) { + throw new UnprocessableRequestQueryException($violations); + } + } + + return $object; } } diff --git a/src/ParameterResolverInterface.php b/src/ParameterResolverInterface.php index e3f6b46b..510d63f7 100644 --- a/src/ParameterResolverInterface.php +++ b/src/ParameterResolverInterface.php @@ -42,7 +42,6 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo * @param mixed $context * * @return mixed - * The ready-to-pass argument. * * @throws ResolvingParameterException * If the parameter cannot be resolved to an argument. diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index c48a3670..f76d2c0e 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -40,6 +40,13 @@ final class ReferenceResolver implements ReferenceResolverInterface { + /** + * The resolver's class resolver + * + * @var ClassResolverInterface + */ + private ClassResolverInterface $classResolver; + /** * The resolver's parameter resolutioner * @@ -54,13 +61,6 @@ final class ReferenceResolver implements ReferenceResolverInterface */ private ResponseResolutionerInterface $responseResolutioner; - /** - * The resolver's class resolver - * - * @var ClassResolverInterface - */ - private ClassResolverInterface $classResolver; - /** * Constructor of the class * @@ -75,9 +75,9 @@ public function __construct( ) { $classResolver ??= new ClassResolver($parameterResolutioner); + $this->classResolver = $classResolver; $this->parameterResolutioner = $parameterResolutioner; $this->responseResolutioner = $responseResolutioner; - $this->classResolver = $classResolver; } /** @@ -93,25 +93,27 @@ public function resolveRequestHandler($reference): RequestHandlerInterface return new CallableRequestHandler($reference, $this->parameterResolutioner, $this->responseResolutioner); } + if (is_string($reference) && is_subclass_of($reference, RequestHandlerInterface::class)) { + /** @var RequestHandlerInterface */ + return $this->classResolver->resolveClass($reference); + } + // https://github.com/php/php-src/blob/3ed526441400060aa4e618b91b3352371fcd02a8/Zend/zend_API.c#L3884-L3932 /** @psalm-suppress MixedArgument */ if (is_array($reference) && is_callable($reference, true) && method_exists($reference[0], $reference[1])) { /** @var array{0: class-string|object, 1: non-empty-string} $reference */ - $object = is_string($reference[0]) ? $this->classResolver->resolveClass($reference[0]) : $reference[0]; + if (is_string($reference[0])) { + $reference[0] = $this->classResolver->resolveClass($reference[0]); + } return new CallableRequestHandler( - [$object, $reference[1]], + [$reference[0], $reference[1]], $this->parameterResolutioner, $this->responseResolutioner ); } - if (is_string($reference) && is_subclass_of($reference, RequestHandlerInterface::class)) { - /** @var RequestHandlerInterface */ - return $this->classResolver->resolveClass($reference); - } - throw new ResolvingReferenceException(sprintf( 'Unable to resolve the reference {%s}', $this->stringifyReference($reference) @@ -131,25 +133,27 @@ public function resolveMiddleware($reference): MiddlewareInterface return new CallableMiddleware($reference, $this->parameterResolutioner, $this->responseResolutioner); } + if (is_string($reference) && is_subclass_of($reference, MiddlewareInterface::class)) { + /** @var MiddlewareInterface */ + return $this->classResolver->resolveClass($reference); + } + // https://github.com/php/php-src/blob/3ed526441400060aa4e618b91b3352371fcd02a8/Zend/zend_API.c#L3884-L3932 /** @psalm-suppress MixedArgument */ if (is_array($reference) && is_callable($reference, true) && method_exists($reference[0], $reference[1])) { /** @var array{0: class-string|object, 1: non-empty-string} $reference */ - $object = is_string($reference[0]) ? $this->classResolver->resolveClass($reference[0]) : $reference[0]; + if (is_string($reference[0])) { + $reference[0] = $this->classResolver->resolveClass($reference[0]); + } return new CallableMiddleware( - [$object, $reference[1]], + [$reference[0], $reference[1]], $this->parameterResolutioner, $this->responseResolutioner ); } - if (is_string($reference) && is_subclass_of($reference, MiddlewareInterface::class)) { - /** @var MiddlewareInterface */ - return $this->classResolver->resolveClass($reference); - } - throw new ResolvingReferenceException(sprintf( 'Unable to resolve the reference {%s}', $this->stringifyReference($reference) @@ -179,8 +183,8 @@ public function resolveMiddlewares(array $references): array */ private function stringifyReference($reference): string { - if (is_array($reference) && is_callable($reference, true, $refString)) { - return $refString; + if (is_array($reference) && is_callable($reference, true, $stringReference)) { + return $stringReference; } if (is_string($reference)) { diff --git a/src/RequestHandler/UnsafeCallableRequestHandler.php b/src/RequestHandler/UnsafeCallableRequestHandler.php index 36c46b15..2c08f3be 100644 --- a/src/RequestHandler/UnsafeCallableRequestHandler.php +++ b/src/RequestHandler/UnsafeCallableRequestHandler.php @@ -22,6 +22,8 @@ * UnsafeCallableRequestHandler * * @since 3.0.0 + * + * @template T as callable(ServerRequestInterface=): ResponseInterface */ final class UnsafeCallableRequestHandler implements RequestHandlerInterface { @@ -29,14 +31,14 @@ final class UnsafeCallableRequestHandler implements RequestHandlerInterface /** * The handler callback * - * @var callable + * @var T */ private $callback; /** * Constructor of the class * - * @param callable $callback + * @param T $callback */ public function __construct(callable $callback) { @@ -48,7 +50,6 @@ public function __construct(callable $callback) */ public function handle(ServerRequestInterface $request): ResponseInterface { - /** @var ResponseInterface */ return ($this->callback)($request); } } diff --git a/src/Route.php b/src/Route.php index e70a0c07..7df2afd4 100644 --- a/src/Route.php +++ b/src/Route.php @@ -218,9 +218,11 @@ public function getTags(): array */ public function getHolder(): Reflector { - return ($this->requestHandler instanceof CallableRequestHandler) ? - $this->requestHandler->getReflection() : - new ReflectionClass($this->requestHandler); + if ($this->requestHandler instanceof CallableRequestHandler) { + return $this->requestHandler->getReflection(); + } + + return new ReflectionClass($this->requestHandler); } /** @@ -298,6 +300,16 @@ public function setAttributes(array $attributes): RouteInterface return $this; } + /** + * {@inheritdoc} + */ + public function setAttribute(string $name, $value): RouteInterface + { + $this->attributes[$name] = $value; + + return $this; + } + /** * {@inheritdoc} */ @@ -377,6 +389,18 @@ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterfa return $this; } + /** + * {@inheritdoc} + */ + public function addTag(string ...$tags): RouteInterface + { + foreach ($tags as $tag) { + $this->tags[] = $tag; + } + + return $this; + } + /** * {@inheritdoc} */ diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 136131f9..4d40a35f 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -100,6 +100,18 @@ public function setHost(string $host): RouteCollectionInterface return $this; } + /** + * {@inheritdoc} + */ + public function setAttribute(string $name, $value): RouteCollectionInterface + { + foreach ($this->routes as $route) { + $route->setAttribute($name, $value); + } + + return $this; + } + /** * {@inheritdoc} */ diff --git a/src/RouteCollectionInterface.php b/src/RouteCollectionInterface.php index 6d5f1b17..5b00c533 100644 --- a/src/RouteCollectionInterface.php +++ b/src/RouteCollectionInterface.php @@ -72,6 +72,18 @@ public function add(RouteInterface ...$routes): RouteCollectionInterface; */ public function setHost(string $host): RouteCollectionInterface; + /** + * Sets the given attribute to all routes in the collection + * + * @param string $name + * @param mixed $value + * + * @return RouteCollectionInterface + * + * @since 3.0.0 + */ + public function setAttribute(string $name, $value): RouteCollectionInterface; + /** * Adds the given path prefix to all routes in the collection * diff --git a/src/RouteCollector.php b/src/RouteCollector.php index bdc9442d..043fbbda 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -195,9 +195,9 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo * * @param string $name * @param string $path - * @param array $methods + * @param list $methods * @param mixed $requestHandler - * @param array $middlewares + * @param list $middlewares * @param array $attributes * * @return RouteInterface @@ -230,7 +230,7 @@ public function route( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param list $middlewares * @param array $attributes * * @return RouteInterface @@ -258,7 +258,7 @@ public function head( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param list $middlewares * @param array $attributes * * @return RouteInterface @@ -286,7 +286,7 @@ public function get( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param list $middlewares * @param array $attributes * * @return RouteInterface @@ -314,7 +314,7 @@ public function post( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param list $middlewares * @param array $attributes * * @return RouteInterface @@ -342,7 +342,7 @@ public function put( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param list $middlewares * @param array $attributes * * @return RouteInterface @@ -370,7 +370,7 @@ public function patch( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param list $middlewares * @param array $attributes * * @return RouteInterface @@ -398,7 +398,7 @@ public function delete( * @param string $name * @param string $path * @param mixed $requestHandler - * @param array $middlewares + * @param list $middlewares * @param array $attributes * * @return RouteInterface @@ -424,7 +424,7 @@ public function purge( * Route grouping logic * * @param callable $callback - * @param array $middlewares + * @param list $middlewares * * @return RouteCollectionInterface */ diff --git a/src/RouteInterface.php b/src/RouteInterface.php index 534b9928..05fe3d96 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -77,7 +77,7 @@ public function getRequestHandler(): RequestHandlerInterface; /** * Gets the route middlewares * - * @return MiddlewareInterface[] + * @return list */ public function getMiddlewares(): array; @@ -109,7 +109,7 @@ public function getDescription(): string; /** * Gets the route tags * - * @return string[] + * @return list * * @since 2.4.0 */ @@ -189,6 +189,18 @@ public function setMiddlewares(MiddlewareInterface ...$middlewares): RouteInterf */ public function setAttributes(array $attributes): RouteInterface; + /** + * Sets the given attribute to the route + * + * @param string $name + * @param mixed $value + * + * @return RouteInterface + * + * @since 3.0.0 + */ + public function setAttribute(string $name, $value): RouteInterface; + /** * Sets the given summary to the route * @@ -258,14 +270,25 @@ public function addMethod(string ...$methods): RouteInterface; */ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterface; + /** + * Adds the given tag(s) to the route + * + * @param string ...$tags + * + * @return RouteInterface + * + * @since 3.0.0 + */ + public function addTag(string ...$tags): RouteInterface; + /** * Returns the route clone with the given attributes * - * This method MUST NOT change the state of the object. + * This method MUST NOT change the object state. * * @param array $attributes * - * @return RouteInterface + * @return static */ public function withAddedAttributes(array $attributes): RouteInterface; } diff --git a/src/Router.php b/src/Router.php index 229cac16..ea3fe2b0 100644 --- a/src/Router.php +++ b/src/Router.php @@ -15,6 +15,7 @@ * Import classes */ use Fig\Http\Message\RequestMethodInterface; +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; @@ -27,7 +28,6 @@ use Sunrise\Http\Router\Loader\LoaderInterface; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; use Sunrise\Http\Router\RequestHandler\UnsafeCallableRequestHandler; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * Import functions @@ -36,7 +36,6 @@ use function Sunrise\Http\Router\path_match; use function array_keys; use function get_class; -use function spl_object_hash; use function sprintf; /** @@ -178,12 +177,7 @@ public function getRoutesByHostname(string $hostname): array */ public function getMiddlewares(): array { - $middlewares = []; - foreach ($this->middlewares as $middleware) { - $middlewares[] = $middleware; - } - - return $middlewares; + return $this->middlewares; } /** @@ -308,22 +302,11 @@ public function addRoute(RouteInterface ...$routes): void * @param MiddlewareInterface ...$middlewares * * @return void - * - * @throws InvalidArgumentException - * if one of the given middlewares already exists. */ public function addMiddleware(MiddlewareInterface ...$middlewares): void { foreach ($middlewares as $middleware) { - $hash = spl_object_hash($middleware); - if (isset($this->middlewares[$hash])) { - throw new InvalidArgumentException(sprintf( - 'The middleware "%s" already exists.', - get_class($middleware) - )); - } - - $this->middlewares[$hash] = $middleware; + $this->middlewares[] = $middleware; } } @@ -421,8 +404,8 @@ public function generateUri(string $name, array $attributes = [], bool $strict = * * @return RouteInterface * - * @throws PageNotFoundException * @throws MethodNotAllowedException + * @throws PageNotFoundException */ public function match(ServerRequestInterface $request): RouteInterface { @@ -455,14 +438,11 @@ public function match(ServerRequestInterface $request): RouteInterface return $route->withAddedAttributes($attributes); } - if (empty($allowedMethods)) { - throw new PageNotFoundException(); + if (!empty($allowedMethods)) { + throw new MethodNotAllowedException($currentMethod, $allowedMethods); } - throw new MethodNotAllowedException( - $currentMethod, - $allowedMethods - ); + throw new PageNotFoundException(); } /** @@ -491,13 +471,12 @@ function (ServerRequestInterface $request): ResponseInterface { } ); - $middlewares = $this->getMiddlewares(); - if (empty($middlewares)) { + if (empty($this->middlewares)) { return $routing->handle($request); } $handler = new QueueableRequestHandler($routing); - $handler->add(...$middlewares); + $handler->add(...$this->middlewares); return $handler->handle($request); } @@ -515,13 +494,12 @@ public function handle(ServerRequestInterface $request): ResponseInterface ); } - $middlewares = $this->getMiddlewares(); - if (empty($middlewares)) { + if (empty($this->middlewares)) { return $this->matchedRoute->handle($request); } $handler = new QueueableRequestHandler($this->matchedRoute); - $handler->add(...$middlewares); + $handler->add(...$this->middlewares); return $handler->handle($request); } From c9dd0dc316b98fd64bd6d9372c3854317b33d2d9 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Wed, 1 Feb 2023 20:04:52 +0100 Subject: [PATCH 048/180] v3 --- composer.json | 2 +- src/Event/RouteEvent.php | 3 +- .../UnprocessableRequestBodyException.php | 33 +----------- .../UnprocessableRequestEntityException.php | 52 +++++++++++++++++++ .../UnprocessableRequestQueryException.php | 33 +----------- src/Router.php | 3 +- src/RouterBuilder.php | 2 +- 7 files changed, 58 insertions(+), 70 deletions(-) create mode 100644 src/Exception/UnprocessableRequestEntityException.php diff --git a/composer.json b/composer.json index 81fa0839..0682353f 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "php": ">=7.4", "fig/http-message-util": "^1.1", "psr/container": "^1.0 || ^2.0", + "psr/event-dispatcher": "^1.0", "psr/http-message": "^1.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", @@ -43,7 +44,6 @@ "sunrise/http-message": "^3.0", "sunrise/hydrator": "^2.7", "symfony/console": "^5.4", - "symfony/event-dispatcher": "^4.4", "symfony/validator": "^5.4" }, "autoload": { diff --git a/src/Event/RouteEvent.php b/src/Event/RouteEvent.php index af880bfb..54942692 100644 --- a/src/Event/RouteEvent.php +++ b/src/Event/RouteEvent.php @@ -16,14 +16,13 @@ */ use Psr\Http\Message\ServerRequestInterface; use Sunrise\Http\Router\RouteInterface; -use Symfony\Contracts\EventDispatcher\Event; /** * RouteEvent * * @since 2.13.0 */ -final class RouteEvent extends Event +final class RouteEvent { /** diff --git a/src/Exception/UnprocessableRequestBodyException.php b/src/Exception/UnprocessableRequestBodyException.php index 4cb2de1c..023741bf 100644 --- a/src/Exception/UnprocessableRequestBodyException.php +++ b/src/Exception/UnprocessableRequestBodyException.php @@ -11,42 +11,11 @@ namespace Sunrise\Http\Router\Exception; -/** - * Import classes - */ -use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; -use Symfony\Component\Validator\ConstraintViolationListInterface; - /** * UnprocessableRequestBodyException * * @since 3.0.0 */ -class UnprocessableRequestBodyException extends HttpUnprocessableEntityException +class UnprocessableRequestBodyException extends UnprocessableRequestEntityException { - - /** - * @var ConstraintViolationListInterface - */ - private ConstraintViolationListInterface $violations; - - /** - * Constructor of the class - * - * @param ConstraintViolationListInterface $violations - */ - public function __construct(ConstraintViolationListInterface $violations) - { - $this->violations = $violations; - } - - /** - * Gets the violations list - * - * @return ConstraintViolationListInterface - */ - final public function getViolations(): ConstraintViolationListInterface - { - return $this->violations; - } } diff --git a/src/Exception/UnprocessableRequestEntityException.php b/src/Exception/UnprocessableRequestEntityException.php new file mode 100644 index 00000000..d7d12cce --- /dev/null +++ b/src/Exception/UnprocessableRequestEntityException.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; +use Symfony\Component\Validator\ConstraintViolationListInterface; + +/** + * UnprocessableRequestEntityException + * + * @since 3.0.0 + */ +class UnprocessableRequestEntityException extends HttpUnprocessableEntityException +{ + + /** + * @var ConstraintViolationListInterface + */ + private ConstraintViolationListInterface $violations; + + /** + * Constructor of the class + * + * @param ConstraintViolationListInterface $violations + */ + public function __construct(ConstraintViolationListInterface $violations) + { + $this->violations = $violations; + } + + /** + * Gets the violations list + * + * @return ConstraintViolationListInterface + */ + final public function getViolations(): ConstraintViolationListInterface + { + return $this->violations; + } +} diff --git a/src/Exception/UnprocessableRequestQueryException.php b/src/Exception/UnprocessableRequestQueryException.php index 10193101..bcd1b8c8 100644 --- a/src/Exception/UnprocessableRequestQueryException.php +++ b/src/Exception/UnprocessableRequestQueryException.php @@ -11,42 +11,11 @@ namespace Sunrise\Http\Router\Exception; -/** - * Import classes - */ -use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; -use Symfony\Component\Validator\ConstraintViolationListInterface; - /** * UnprocessableRequestQueryException * * @since 3.0.0 */ -class UnprocessableRequestQueryException extends HttpUnprocessableEntityException +class UnprocessableRequestQueryException extends UnprocessableRequestEntityException { - - /** - * @var ConstraintViolationListInterface - */ - private ConstraintViolationListInterface $violations; - - /** - * Constructor of the class - * - * @param ConstraintViolationListInterface $violations - */ - public function __construct(ConstraintViolationListInterface $violations) - { - $this->violations = $violations; - } - - /** - * Gets the violations list - * - * @return ConstraintViolationListInterface - */ - final public function getViolations(): ConstraintViolationListInterface - { - return $this->violations; - } } diff --git a/src/Router.php b/src/Router.php index ea3fe2b0..bf5991e9 100644 --- a/src/Router.php +++ b/src/Router.php @@ -75,8 +75,7 @@ class Router implements RequestHandlerInterface, RequestMethodInterface /** * The router's middlewares * - * @var array - * The keys is an object hash. + * @var list */ private $middlewares = []; diff --git a/src/RouterBuilder.php b/src/RouterBuilder.php index fe1f96d7..94edd9bb 100644 --- a/src/RouterBuilder.php +++ b/src/RouterBuilder.php @@ -14,9 +14,9 @@ /** * Import classes */ +use Psr\EventDispatcher\EventDispatcherInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\SimpleCache\CacheInterface; -use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * RouterBuilder From a5a606c21c4b82abe6681786950b9ade02490589 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Wed, 1 Feb 2023 20:20:19 +0100 Subject: [PATCH 049/180] v3 --- src/Exception/PageNotFoundException.php | 7 +++- src/Exception/RouteNotFoundException.php | 7 +--- ...n.php => UnprocessableEntityException.php} | 4 +- .../UnprocessableRequestBodyException.php | 2 +- .../UnprocessableRequestQueryException.php | 2 +- .../Middleware/ErrorHandlingMiddleware.php | 42 +++++++++++++++++++ 6 files changed, 53 insertions(+), 11 deletions(-) rename src/Exception/{UnprocessableRequestEntityException.php => UnprocessableEntityException.php} (90%) create mode 100644 src/Rest/Middleware/ErrorHandlingMiddleware.php diff --git a/src/Exception/PageNotFoundException.php b/src/Exception/PageNotFoundException.php index 461cc136..4a47e46a 100644 --- a/src/Exception/PageNotFoundException.php +++ b/src/Exception/PageNotFoundException.php @@ -11,9 +11,14 @@ namespace Sunrise\Http\Router\Exception; +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; + /** * PageNotFoundException */ -class PageNotFoundException extends RouteNotFoundException +class PageNotFoundException extends HttpNotFoundException { } diff --git a/src/Exception/RouteNotFoundException.php b/src/Exception/RouteNotFoundException.php index 4e78d5d8..8b1f8bf8 100644 --- a/src/Exception/RouteNotFoundException.php +++ b/src/Exception/RouteNotFoundException.php @@ -11,14 +11,9 @@ namespace Sunrise\Http\Router\Exception; -/** - * Import classes - */ -use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; - /** * RouteNotFoundException */ -class RouteNotFoundException extends HttpNotFoundException +class RouteNotFoundException extends LogicException { } diff --git a/src/Exception/UnprocessableRequestEntityException.php b/src/Exception/UnprocessableEntityException.php similarity index 90% rename from src/Exception/UnprocessableRequestEntityException.php rename to src/Exception/UnprocessableEntityException.php index d7d12cce..c7a88561 100644 --- a/src/Exception/UnprocessableRequestEntityException.php +++ b/src/Exception/UnprocessableEntityException.php @@ -18,11 +18,11 @@ use Symfony\Component\Validator\ConstraintViolationListInterface; /** - * UnprocessableRequestEntityException + * UnprocessableEntityException * * @since 3.0.0 */ -class UnprocessableRequestEntityException extends HttpUnprocessableEntityException +class UnprocessableEntityException extends HttpUnprocessableEntityException { /** diff --git a/src/Exception/UnprocessableRequestBodyException.php b/src/Exception/UnprocessableRequestBodyException.php index 023741bf..5e06f180 100644 --- a/src/Exception/UnprocessableRequestBodyException.php +++ b/src/Exception/UnprocessableRequestBodyException.php @@ -16,6 +16,6 @@ * * @since 3.0.0 */ -class UnprocessableRequestBodyException extends UnprocessableRequestEntityException +class UnprocessableRequestBodyException extends UnprocessableEntityException { } diff --git a/src/Exception/UnprocessableRequestQueryException.php b/src/Exception/UnprocessableRequestQueryException.php index bcd1b8c8..c1bae14a 100644 --- a/src/Exception/UnprocessableRequestQueryException.php +++ b/src/Exception/UnprocessableRequestQueryException.php @@ -16,6 +16,6 @@ * * @since 3.0.0 */ -class UnprocessableRequestQueryException extends UnprocessableRequestEntityException +class UnprocessableRequestQueryException extends UnprocessableEntityException { } diff --git a/src/Rest/Middleware/ErrorHandlingMiddleware.php b/src/Rest/Middleware/ErrorHandlingMiddleware.php new file mode 100644 index 00000000..506d6e78 --- /dev/null +++ b/src/Rest/Middleware/ErrorHandlingMiddleware.php @@ -0,0 +1,42 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Rest\Middleware; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Throwable; + +/** + * ErrorHandlingMiddleware + * + * @since 3.0.0 + */ +final class ErrorHandlingMiddleware implements MiddlewareInterface +{ + + /** + * {@inheritdoc} + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + try { + return $handler->handle($request); + } catch (Throwable $e) { + throw $e; + } + } +} From 325a3ef8af3e2e3ba2a5e38220f229413240c1a8 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Wed, 1 Feb 2023 20:30:08 +0100 Subject: [PATCH 050/180] v3 --- src/Rest/Middleware/ErrorHandlingMiddleware.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Rest/Middleware/ErrorHandlingMiddleware.php b/src/Rest/Middleware/ErrorHandlingMiddleware.php index 506d6e78..c43dba62 100644 --- a/src/Rest/Middleware/ErrorHandlingMiddleware.php +++ b/src/Rest/Middleware/ErrorHandlingMiddleware.php @@ -14,6 +14,7 @@ /** * Import classes */ +use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; @@ -28,6 +29,19 @@ final class ErrorHandlingMiddleware implements MiddlewareInterface { + /** + * @var ResponseFactoryInterface + */ + private ResponseFactoryInterface $responseFactory; + + /** + * @param ResponseFactoryInterface $responseFactory + */ + public function __construct(ResponseFactoryInterface $responseFactory) + { + $this->responseFactory = $responseFactory; + } + /** * {@inheritdoc} */ From 8fed719077385306ac4d021c1530baf16a3dcc8e Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Thu, 2 Feb 2023 19:20:51 +0100 Subject: [PATCH 051/180] v3 --- src/Annotation/Tag.php | 52 ++++++++++++ src/Component/Rest/Dto/ErrorDto.php | 53 ++++++++++++ src/Component/Rest/Dto/ErrorsDto.php | 85 +++++++++++++++++++ .../Middleware/ErrorHandlingMiddleware.php | 21 ++++- src/Loader/DescriptorLoader.php | 6 ++ src/ParameterResolutioner.php | 12 +-- src/Route.php | 27 ++++-- src/RouteCollection.php | 3 +- src/RouteInterface.php | 11 +++ 9 files changed, 253 insertions(+), 17 deletions(-) create mode 100644 src/Annotation/Tag.php create mode 100644 src/Component/Rest/Dto/ErrorDto.php create mode 100644 src/Component/Rest/Dto/ErrorsDto.php rename src/{ => Component}/Rest/Middleware/ErrorHandlingMiddleware.php (53%) diff --git a/src/Annotation/Tag.php b/src/Annotation/Tag.php new file mode 100644 index 00000000..8af2e525 --- /dev/null +++ b/src/Annotation/Tag.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Annotation; + +/** + * Import classes + */ +use Attribute; + +/** + * @Annotation + * + * @Target({"CLASS", "METHOD"}) + * + * @NamedArgumentConstructor + * + * @Attributes({ + * @Attribute("value", type="string", required=true), + * }) + * + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)] +final class Tag +{ + + /** + * The attribute value + * + * @var string + */ + public string $value; + + /** + * Constructor of the class + * + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } +} diff --git a/src/Component/Rest/Dto/ErrorDto.php b/src/Component/Rest/Dto/ErrorDto.php new file mode 100644 index 00000000..49f528a1 --- /dev/null +++ b/src/Component/Rest/Dto/ErrorDto.php @@ -0,0 +1,53 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Component\Rest\Dto; + +/** + * Import classes + */ +use Sunrise\Http\Router\OpenApi\Annotation\OpenApi as OA; +use Symfony\Component\Validator\ConstraintViolationInterface; +use JsonSerializable; + +/** + * Error DTO + * + * @OA\SchemaObject({ + * }) + * + * @link https://jsonapi.org/format/#error-objects + * + * @since 3.0.0 + */ +final class ErrorDto implements JsonSerializable +{ + + /** + * @param ConstraintViolationInterface $violation + * + * @return self + */ + public static function fromViolation(ConstraintViolationInterface $violation): self + { + return new ErrorDto(); + } + + /** + * @return array{ + * } + */ + public function jsonSerialize(): array + { + return [ + ]; + } +} diff --git a/src/Component/Rest/Dto/ErrorsDto.php b/src/Component/Rest/Dto/ErrorsDto.php new file mode 100644 index 00000000..b731c291 --- /dev/null +++ b/src/Component/Rest/Dto/ErrorsDto.php @@ -0,0 +1,85 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Component\Rest\Dto; + +/** + * Import classes + */ +use Sunrise\Http\Router\OpenApi\Annotation\OpenApi as OA; +use Symfony\Component\Validator\ConstraintViolationListInterface; +use JsonSerializable; + +/** + * Errors DTO + * + * @OA\SchemaObject({ + * "errors": @OA\SchemaReference(".errors"), + * }) + * + * @OA\Response( + * description="Something went wrong", + * content={ + * "application/json": @OA\MediaType( + * schema=@OA\SchemaReference("ErrorsDto"), + * ), + * }, + * ) + * + * @since 3.0.0 + */ +final class ErrorsDto implements JsonSerializable +{ + + /** + * @OA\SchemaArray( + * @OA\SchemaReference("ErrorDto") + * ) + * + * @var list + */ + private array $errors = []; + + /** + * @param ErrorDto ...$errors + */ + public function __construct(ErrorDto ...$errors) + { + foreach ($errors as $error) { + $this->errors[] = $error; + } + } + + /** + * @param ConstraintViolationListInterface $violations + * + * @return self + */ + public static function fromViolations(ConstraintViolationListInterface $violations): self + { + $errors = []; + foreach ($violations as $violation) { + $errors[] = ErrorDto::fromViolation($violation); + } + + return new self(...$errors); + } + + /** + * @return array{errors: list} + */ + public function jsonSerialize(): array + { + return [ + 'errors' => $this->errors, + ]; + } +} diff --git a/src/Rest/Middleware/ErrorHandlingMiddleware.php b/src/Component/Rest/Middleware/ErrorHandlingMiddleware.php similarity index 53% rename from src/Rest/Middleware/ErrorHandlingMiddleware.php rename to src/Component/Rest/Middleware/ErrorHandlingMiddleware.php index c43dba62..6f178da1 100644 --- a/src/Rest/Middleware/ErrorHandlingMiddleware.php +++ b/src/Component/Rest/Middleware/ErrorHandlingMiddleware.php @@ -9,7 +9,7 @@ * @link https://github.com/sunrise-php/http-router */ -namespace Sunrise\Http\Router\Rest\Middleware; +namespace Sunrise\Http\Router\Component\Rest\Middleware; /** * Import classes @@ -19,7 +19,10 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Throwable; +use Sunrise\Http\Router\Exception\Http\HttpExceptionInterface; +use Sunrise\Http\Router\Exception\Http\HttpMethodNotAllowedException; +use Sunrise\Http\Router\Exception\Http\HttpUnsupportedMediaTypeException; +use Sunrise\Http\Router\Exception\UnprocessableEntityException; /** * ErrorHandlingMiddleware @@ -49,8 +52,18 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface { try { return $handler->handle($request); - } catch (Throwable $e) { - throw $e; + } catch (HttpMethodNotAllowedException $e) { + /** @psalm-suppress TooFewArguments, MixedArgument */ + return $this->responseFactory->createResponse($e->getStatusCode()) + ->withHeader(...$e->getAllowHeaderArguments()); + } catch (HttpUnsupportedMediaTypeException $e) { + /** @psalm-suppress TooFewArguments, MixedArgument */ + return $this->responseFactory->createResponse($e->getStatusCode()) + ->withHeader(...$e->getAcceptHeaderArguments()); + } catch (UnprocessableEntityException $e) { + return $this->responseFactory->createResponse($e->getStatusCode()); + } catch (HttpExceptionInterface $e) { + return $this->responseFactory->createResponse($e->getStatusCode()); } } } diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index a566d934..554a52c4 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -22,6 +22,7 @@ use Sunrise\Http\Router\Annotation\Postfix; use Sunrise\Http\Router\Annotation\Prefix; use Sunrise\Http\Router\Annotation\Route; +use Sunrise\Http\Router\Annotation\Tag; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ParameterResolver\DependencyInjectionParameterResolver; @@ -481,5 +482,10 @@ private function supplementDescriptor(Route $descriptor, Reflector $classOrMetho foreach ($annotations as $annotation) { $descriptor->middlewares[] = $annotation->value; } + + $annotations = $this->annotationReader->getClassOrMethodAnnotations($classOrMethod, Tag::class); + foreach ($annotations as $annotation) { + $descriptor->tags[] = $annotation->value; + } } } diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index 7004f4a0..45682616 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -61,14 +61,16 @@ public function withContext($context): ParameterResolutionerInterface */ public function withPriorityResolver(ParameterResolverInterface ...$resolvers): ParameterResolutionerInterface { - /** @var list $resolvers */ + $clone = clone $this; + $clone->resolvers = []; - foreach ($this->resolvers as $resolver) { - $resolvers[] = $resolver; + foreach ($resolvers as $resolver) { + $clone->resolvers[] = $resolver; } - $clone = clone $this; - $clone->resolvers = $resolvers; + foreach ($this->resolvers as $resolver) { + $clone->resolvers[] = $resolver; + } return $clone; } diff --git a/src/Route.php b/src/Route.php index 7df2afd4..6358a15b 100644 --- a/src/Route.php +++ b/src/Route.php @@ -26,6 +26,7 @@ /** * Import functions */ +use function array_values; use function rtrim; use function strtoupper; @@ -283,9 +284,10 @@ public function setRequestHandler(RequestHandlerInterface $requestHandler): Rout */ public function setMiddlewares(MiddlewareInterface ...$middlewares): RouteInterface { - /** @var list $middlewares */ - - $this->middlewares = $middlewares; + $this->middlewares = []; + foreach ($middlewares as $middleware) { + $this->middlewares[] = $middleware; + } return $this; } @@ -335,9 +337,10 @@ public function setDescription(string $description): RouteInterface */ public function setTags(string ...$tags): RouteInterface { - /** @var list $tags */ - - $this->tags = $tags; + $this->tags = []; + foreach ($tags as $tag) { + $this->tags[] = $tag; + } return $this; } @@ -389,6 +392,18 @@ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterfa return $this; } + /** + * {@inheritdoc} + */ + public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): RouteInterface + { + $middlewares = array_values($middlewares); + + $this->middlewares = [...$middlewares, ...$this->middlewares]; + + return $this; + } + /** * {@inheritdoc} */ diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 4d40a35f..c84cb11e 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -19,7 +19,6 @@ /** * Import functions */ -use function array_merge; use function count; /** @@ -166,7 +165,7 @@ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteCollect public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface { foreach ($this->routes as $route) { - $route->setMiddlewares(...array_merge($middlewares, $route->getMiddlewares())); + $route->addPriorityMiddleware(...$middlewares); } return $this; diff --git a/src/RouteInterface.php b/src/RouteInterface.php index 05fe3d96..42a7620f 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -270,6 +270,17 @@ public function addMethod(string ...$methods): RouteInterface; */ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterface; + /** + * Adds the given priority middleware(s) to the route + * + * @param MiddlewareInterface ...$middlewares + * + * @return RouteInterface + * + * @since 3.0.0 + */ + public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): RouteInterface; + /** * Adds the given tag(s) to the route * From ec1faa2866d640da914069f5af8d0d96f6b1e99d Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Thu, 2 Feb 2023 19:34:14 +0100 Subject: [PATCH 052/180] v3 --- src/Middleware/CallableMiddleware.php | 25 +++++++++++++++---- src/RequestHandler/CallableRequestHandler.php | 23 ++++++++++++++--- 2 files changed, 39 insertions(+), 9 deletions(-) diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index 67751e47..fb2874e7 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -24,6 +24,7 @@ use ReflectionFunctionAbstract; use ReflectionFunction; use ReflectionMethod; +use ReflectionParameter; /** * Import functions @@ -88,18 +89,32 @@ public function getReflection(): ReflectionFunctionAbstract return reflect_callable($this->callback); } + /** + * Gets the callback's parameters + * + * @return list + * + * @since 3.0.0 + */ + public function getParameters(): array + { + return $this->getReflection()->getParameters(); + } + /** * {@inheritdoc} */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + $parameterResolvers = [ + new KnownTypeParameterResolver(ServerRequestInterface::class, $request), + new KnownTypeParameterResolver(RequestHandlerInterface::class, $handler), + ]; + $arguments = $this->parameterResolutioner ->withContext($request) - ->withPriorityResolver( - new KnownTypeParameterResolver(ServerRequestInterface::class, $request), - new KnownTypeParameterResolver(RequestHandlerInterface::class, $handler) - ) - ->resolveParameters(...$this->getReflection()->getParameters()); + ->withPriorityResolver(...$parameterResolvers) + ->resolveParameters(...$this->getParameters()); /** @var mixed */ $response = ($this->callback)(...$arguments); diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index 2b4552d5..8ea31019 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -23,6 +23,7 @@ use ReflectionFunctionAbstract; use ReflectionFunction; use ReflectionMethod; +use ReflectionParameter; /** * Import functions @@ -85,17 +86,31 @@ public function getReflection(): ReflectionFunctionAbstract return reflect_callable($this->callback); } + /** + * Gets the callback's parameters + * + * @return list + * + * @since 3.0.0 + */ + public function getParameters(): array + { + return $this->getReflection()->getParameters(); + } + /** * {@inheritdoc} */ public function handle(ServerRequestInterface $request): ResponseInterface { + $parameterResolvers = [ + new KnownTypeParameterResolver(ServerRequestInterface::class, $request), + ]; + $arguments = $this->parameterResolutioner ->withContext($request) - ->withPriorityResolver( - new KnownTypeParameterResolver(ServerRequestInterface::class, $request) - ) - ->resolveParameters(...$this->getReflection()->getParameters()); + ->withPriorityResolver(...$parameterResolvers) + ->resolveParameters(...$this->getParameters()); /** @var mixed */ $response = ($this->callback)(...$arguments); From 87e8987065bac6d5445c98a65c80b77399932d21 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Tue, 7 Feb 2023 21:55:06 +0100 Subject: [PATCH 053/180] v3 --- src/Component/Rest/Dto/ErrorDto.php | 53 ------------ src/Component/Rest/Dto/ErrorsDto.php | 85 ------------------- .../Middleware/ErrorHandlingMiddleware.php | 69 --------------- .../AbstractParameterResolver.php | 60 ------------- .../RequestBodyParameterResolver.php | 7 +- .../RequestEntityParameterResolver.php | 30 ++++++- .../RequestQueryParameterResolver.php | 7 +- 7 files changed, 39 insertions(+), 272 deletions(-) delete mode 100644 src/Component/Rest/Dto/ErrorDto.php delete mode 100644 src/Component/Rest/Dto/ErrorsDto.php delete mode 100644 src/Component/Rest/Middleware/ErrorHandlingMiddleware.php delete mode 100644 src/ParameterResolver/AbstractParameterResolver.php diff --git a/src/Component/Rest/Dto/ErrorDto.php b/src/Component/Rest/Dto/ErrorDto.php deleted file mode 100644 index 49f528a1..00000000 --- a/src/Component/Rest/Dto/ErrorDto.php +++ /dev/null @@ -1,53 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Component\Rest\Dto; - -/** - * Import classes - */ -use Sunrise\Http\Router\OpenApi\Annotation\OpenApi as OA; -use Symfony\Component\Validator\ConstraintViolationInterface; -use JsonSerializable; - -/** - * Error DTO - * - * @OA\SchemaObject({ - * }) - * - * @link https://jsonapi.org/format/#error-objects - * - * @since 3.0.0 - */ -final class ErrorDto implements JsonSerializable -{ - - /** - * @param ConstraintViolationInterface $violation - * - * @return self - */ - public static function fromViolation(ConstraintViolationInterface $violation): self - { - return new ErrorDto(); - } - - /** - * @return array{ - * } - */ - public function jsonSerialize(): array - { - return [ - ]; - } -} diff --git a/src/Component/Rest/Dto/ErrorsDto.php b/src/Component/Rest/Dto/ErrorsDto.php deleted file mode 100644 index b731c291..00000000 --- a/src/Component/Rest/Dto/ErrorsDto.php +++ /dev/null @@ -1,85 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Component\Rest\Dto; - -/** - * Import classes - */ -use Sunrise\Http\Router\OpenApi\Annotation\OpenApi as OA; -use Symfony\Component\Validator\ConstraintViolationListInterface; -use JsonSerializable; - -/** - * Errors DTO - * - * @OA\SchemaObject({ - * "errors": @OA\SchemaReference(".errors"), - * }) - * - * @OA\Response( - * description="Something went wrong", - * content={ - * "application/json": @OA\MediaType( - * schema=@OA\SchemaReference("ErrorsDto"), - * ), - * }, - * ) - * - * @since 3.0.0 - */ -final class ErrorsDto implements JsonSerializable -{ - - /** - * @OA\SchemaArray( - * @OA\SchemaReference("ErrorDto") - * ) - * - * @var list - */ - private array $errors = []; - - /** - * @param ErrorDto ...$errors - */ - public function __construct(ErrorDto ...$errors) - { - foreach ($errors as $error) { - $this->errors[] = $error; - } - } - - /** - * @param ConstraintViolationListInterface $violations - * - * @return self - */ - public static function fromViolations(ConstraintViolationListInterface $violations): self - { - $errors = []; - foreach ($violations as $violation) { - $errors[] = ErrorDto::fromViolation($violation); - } - - return new self(...$errors); - } - - /** - * @return array{errors: list} - */ - public function jsonSerialize(): array - { - return [ - 'errors' => $this->errors, - ]; - } -} diff --git a/src/Component/Rest/Middleware/ErrorHandlingMiddleware.php b/src/Component/Rest/Middleware/ErrorHandlingMiddleware.php deleted file mode 100644 index 6f178da1..00000000 --- a/src/Component/Rest/Middleware/ErrorHandlingMiddleware.php +++ /dev/null @@ -1,69 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Component\Rest\Middleware; - -/** - * Import classes - */ -use Psr\Http\Message\ResponseFactoryInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\Http\HttpExceptionInterface; -use Sunrise\Http\Router\Exception\Http\HttpMethodNotAllowedException; -use Sunrise\Http\Router\Exception\Http\HttpUnsupportedMediaTypeException; -use Sunrise\Http\Router\Exception\UnprocessableEntityException; - -/** - * ErrorHandlingMiddleware - * - * @since 3.0.0 - */ -final class ErrorHandlingMiddleware implements MiddlewareInterface -{ - - /** - * @var ResponseFactoryInterface - */ - private ResponseFactoryInterface $responseFactory; - - /** - * @param ResponseFactoryInterface $responseFactory - */ - public function __construct(ResponseFactoryInterface $responseFactory) - { - $this->responseFactory = $responseFactory; - } - - /** - * {@inheritdoc} - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - try { - return $handler->handle($request); - } catch (HttpMethodNotAllowedException $e) { - /** @psalm-suppress TooFewArguments, MixedArgument */ - return $this->responseFactory->createResponse($e->getStatusCode()) - ->withHeader(...$e->getAllowHeaderArguments()); - } catch (HttpUnsupportedMediaTypeException $e) { - /** @psalm-suppress TooFewArguments, MixedArgument */ - return $this->responseFactory->createResponse($e->getStatusCode()) - ->withHeader(...$e->getAcceptHeaderArguments()); - } catch (UnprocessableEntityException $e) { - return $this->responseFactory->createResponse($e->getStatusCode()); - } catch (HttpExceptionInterface $e) { - return $this->responseFactory->createResponse($e->getStatusCode()); - } - } -} diff --git a/src/ParameterResolver/AbstractParameterResolver.php b/src/ParameterResolver/AbstractParameterResolver.php deleted file mode 100644 index 1d4c0898..00000000 --- a/src/ParameterResolver/AbstractParameterResolver.php +++ /dev/null @@ -1,60 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\ParameterResolver; - -/** - * Import classes - */ -use Sunrise\Http\Router\ParameterResolverInterface; -use ReflectionMethod; -use ReflectionParameter; - -/** - * Import functions - */ -use function sprintf; - -/** - * AbstractParameterResolver - * - * @since 3.0.0 - */ -abstract class AbstractParameterResolver implements ParameterResolverInterface -{ - - /** - * Stringifies the given parameter - * - * @param ReflectionParameter $parameter - * - * @return string - */ - final protected function stringifyParameter(ReflectionParameter $parameter): string - { - if ($parameter->getDeclaringFunction() instanceof ReflectionMethod) { - return sprintf( - '%s::%s($%s[%d])', - $parameter->getDeclaringFunction()->getDeclaringClass()->getName(), - $parameter->getDeclaringFunction()->getName(), - $parameter->getName(), - $parameter->getPosition() - ); - } - - return sprintf( - '%s($%s[%d])', - $parameter->getDeclaringFunction()->getName(), - $parameter->getName(), - $parameter->getPosition() - ); - } -} diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index 674b0fba..24ca56ee 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -42,6 +42,7 @@ * RequestBodyParameterResolver * * @link https://github.com/sunrise-php/hydrator + * @link https://github.com/symfony/validator * * @since 3.0.0 */ @@ -62,8 +63,10 @@ final class RequestBodyParameterResolver implements ParameterResolverInterface * @param HydratorInterface $hydrator * @param ValidatorInterface|null $validator */ - public function __construct(HydratorInterface $hydrator, ?ValidatorInterface $validator = null) - { + public function __construct( + HydratorInterface $hydrator, + ?ValidatorInterface $validator = null + ) { $this->hydrator = $hydrator; $this->validator = $validator; } diff --git a/src/ParameterResolver/RequestEntityParameterResolver.php b/src/ParameterResolver/RequestEntityParameterResolver.php index 8b91886f..ac819d91 100644 --- a/src/ParameterResolver/RequestEntityParameterResolver.php +++ b/src/ParameterResolver/RequestEntityParameterResolver.php @@ -20,6 +20,7 @@ use Sunrise\Http\Router\Exception\EntityNotFoundException; use Sunrise\Http\Router\Exception\ResolvingParameterException; use ReflectionAttribute; +use ReflectionMethod; use ReflectionNamedType; use ReflectionParameter; @@ -34,7 +35,7 @@ * * @since 3.0.0 */ -final class RequestEntityParameterResolver extends AbstractParameterResolver +final class RequestEntityParameterResolver implements ParameterResolverInterface { /** @@ -155,4 +156,31 @@ public function resolveParameter(ReflectionParameter $parameter, $context) $entityMetadata->getReflectionClass()->getShortName() )); } + + /** + * Stringifies the given parameter + * + * @param ReflectionParameter $parameter + * + * @return string + */ + private function stringifyParameter(ReflectionParameter $parameter): string + { + if ($parameter->getDeclaringFunction() instanceof ReflectionMethod) { + return sprintf( + '%s::%s($%s[%d])', + $parameter->getDeclaringFunction()->getDeclaringClass()->getName(), + $parameter->getDeclaringFunction()->getName(), + $parameter->getName(), + $parameter->getPosition() + ); + } + + return sprintf( + '%s($%s[%d])', + $parameter->getDeclaringFunction()->getName(), + $parameter->getName(), + $parameter->getPosition() + ); + } } diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php index cc529589..da5997a8 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -42,6 +42,7 @@ * RequestQueryParameterResolver * * @link https://github.com/sunrise-php/hydrator + * @link https://github.com/symfony/validator * * @since 3.0.0 */ @@ -62,8 +63,10 @@ final class RequestQueryParameterResolver implements ParameterResolverInterface * @param HydratorInterface $hydrator * @param ValidatorInterface|null $validator */ - public function __construct(HydratorInterface $hydrator, ?ValidatorInterface $validator = null) - { + public function __construct( + HydratorInterface $hydrator, + ?ValidatorInterface $validator = null + ) { $this->hydrator = $hydrator; $this->validator = $validator; } From 6319e7f896ba30dddad47c83aa6a7e0e0ca78b3e Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Wed, 8 Feb 2023 01:32:58 +0100 Subject: [PATCH 054/180] v3 --- functions/get_debug_type.php | 2 + .../UnprocessableEntityException.php | 2 + .../KnownNameParameterResolver.php | 67 +++ .../KnownTypeParameterResolver.php | 6 +- .../RequestEntityParameterResolver.php | 1 + src/ReferenceResolverInterface.php | 4 + src/Route.php | 14 +- src/RouteInterface.php | 4 +- src/Router.php | 453 ++++++++++++------ 9 files changed, 392 insertions(+), 161 deletions(-) create mode 100644 src/ParameterResolver/KnownNameParameterResolver.php diff --git a/functions/get_debug_type.php b/functions/get_debug_type.php index ed2302a8..38d231bb 100644 --- a/functions/get_debug_type.php +++ b/functions/get_debug_type.php @@ -19,6 +19,8 @@ * @return string * * @since 3.0.0 + * + * @link https://www.php.net/get_debug_type */ function get_debug_type($value): string { diff --git a/src/Exception/UnprocessableEntityException.php b/src/Exception/UnprocessableEntityException.php index c7a88561..afe34b01 100644 --- a/src/Exception/UnprocessableEntityException.php +++ b/src/Exception/UnprocessableEntityException.php @@ -37,6 +37,8 @@ class UnprocessableEntityException extends HttpUnprocessableEntityException */ public function __construct(ConstraintViolationListInterface $violations) { + parent::__construct(); + $this->violations = $violations; } diff --git a/src/ParameterResolver/KnownNameParameterResolver.php b/src/ParameterResolver/KnownNameParameterResolver.php new file mode 100644 index 00000000..58f65db2 --- /dev/null +++ b/src/ParameterResolver/KnownNameParameterResolver.php @@ -0,0 +1,67 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ParameterResolver; + +/** + * Import classes + */ +use Sunrise\Http\Router\ParameterResolverInterface; +use ReflectionParameter; + +/** + * KnownNameParameterResolver + * + * @since 3.0.0 + */ +final class KnownNameParameterResolver implements ParameterResolverInterface +{ + + /** + * @var string + */ + private string $name; + + /** + * @var mixed + */ + private $value; + + /** + * @param string $name + * @param mixed $value + */ + public function __construct(string $name, $value) + { + $this->name = $name; + $this->value = $value; + } + + /** + * {@inheritdoc} + */ + public function supportsParameter(ReflectionParameter $parameter, $context): bool + { + if ($parameter->hasType()) { + return false; + } + + return $this->name === $parameter->getName(); + } + + /** + * {@inheritdoc} + */ + public function resolveParameter(ReflectionParameter $parameter, $context) + { + return $this->value; + } +} diff --git a/src/ParameterResolver/KnownTypeParameterResolver.php b/src/ParameterResolver/KnownTypeParameterResolver.php index bc5b1808..574b0329 100644 --- a/src/ParameterResolver/KnownTypeParameterResolver.php +++ b/src/ParameterResolver/KnownTypeParameterResolver.php @@ -74,11 +74,7 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo return false; } - if (!($parameter->getType()->getName() === $this->type)) { - return false; - } - - return true; + return $this->type === $parameter->getType()->getName(); } /** diff --git a/src/ParameterResolver/RequestEntityParameterResolver.php b/src/ParameterResolver/RequestEntityParameterResolver.php index ac819d91..e3215720 100644 --- a/src/ParameterResolver/RequestEntityParameterResolver.php +++ b/src/ParameterResolver/RequestEntityParameterResolver.php @@ -19,6 +19,7 @@ use Sunrise\Http\Router\Annotation\RequestEntity; use Sunrise\Http\Router\Exception\EntityNotFoundException; use Sunrise\Http\Router\Exception\ResolvingParameterException; +use Sunrise\Http\Router\ParameterResolverInterface; use ReflectionAttribute; use ReflectionMethod; use ReflectionNamedType; diff --git a/src/ReferenceResolverInterface.php b/src/ReferenceResolverInterface.php index 9df2cd07..a4f04b8a 100644 --- a/src/ReferenceResolverInterface.php +++ b/src/ReferenceResolverInterface.php @@ -35,6 +35,8 @@ interface ReferenceResolverInterface * * @throws ResolvingReferenceException * If the reference cannot be resolved to a request handler. + * + * @since 3.0.0 */ public function resolveRequestHandler($reference): RequestHandlerInterface; @@ -47,6 +49,8 @@ public function resolveRequestHandler($reference): RequestHandlerInterface; * * @throws ResolvingReferenceException * If the reference cannot be resolved to a middleware. + * + * @since 3.0.0 */ public function resolveMiddleware($reference): MiddlewareInterface; diff --git a/src/Route.php b/src/Route.php index 6358a15b..140d8819 100644 --- a/src/Route.php +++ b/src/Route.php @@ -26,7 +26,6 @@ /** * Import functions */ -use function array_values; use function rtrim; use function strtoupper; @@ -395,11 +394,18 @@ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterfa /** * {@inheritdoc} */ - public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): RouteInterface + public function addPriorityMiddleware(MiddlewareInterface ...$priorityMiddlewares): RouteInterface { - $middlewares = array_values($middlewares); + $previousMiddlewares = $this->middlewares; + $this->middlewares = []; + + foreach ($priorityMiddlewares as $middleware) { + $this->middlewares[] = $middleware; + } - $this->middlewares = [...$middlewares, ...$this->middlewares]; + foreach ($previousMiddlewares as $middleware) { + $this->middlewares[] = $middleware; + } return $this; } diff --git a/src/RouteInterface.php b/src/RouteInterface.php index 42a7620f..d35b6509 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -273,13 +273,13 @@ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterfa /** * Adds the given priority middleware(s) to the route * - * @param MiddlewareInterface ...$middlewares + * @param MiddlewareInterface ...$priorityMiddlewares * * @return RouteInterface * * @since 3.0.0 */ - public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): RouteInterface; + public function addPriorityMiddleware(MiddlewareInterface ...$priorityMiddlewares): RouteInterface; /** * Adds the given tag(s) to the route diff --git a/src/Router.php b/src/Router.php index bf5991e9..10ca2ebd 100644 --- a/src/Router.php +++ b/src/Router.php @@ -28,14 +28,13 @@ use Sunrise\Http\Router\Loader\LoaderInterface; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; use Sunrise\Http\Router\RequestHandler\UnsafeCallableRequestHandler; +use Generator; /** * Import functions */ use function Sunrise\Http\Router\path_build; use function Sunrise\Http\Router\path_match; -use function array_keys; -use function get_class; use function sprintf; /** @@ -51,7 +50,7 @@ class Router implements RequestHandlerInterface, RequestMethodInterface * * @since 2.9.0 */ - public static $patterns = [ + public static array $patterns = [ '@slug' => '[0-9a-z-]+', '@uuid' => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', ]; @@ -59,46 +58,93 @@ class Router implements RequestHandlerInterface, RequestMethodInterface /** * The router's host table * - * @var array - * The key is a host alias and values are hostnames. + * @var array> */ - private $hosts = []; + private array $hosts = []; /** * The router's routes * - * @var array - * The key is a route name. + * @var array> */ - private $routes = []; + private array $routes = []; /** * The router's middlewares * * @var list */ - private $middlewares = []; + private array $middlewares = []; /** * The router's matched route * * @var RouteInterface|null */ - private $matchedRoute = null; + private ?RouteInterface $matchedRoute = null; /** * The router's event dispatcher * * @var EventDispatcherInterface|null - * - * @since 2.13.0 */ private ?EventDispatcherInterface $eventDispatcher = null; + /** + * Adds the given patterns to the router + * + * @param array $patterns + * + * @return void + * + * @since 2.11.0 + */ + public function addPatterns(array $patterns): void + { + foreach ($patterns as $alias => $pattern) { + self::$patterns[$alias] = $pattern; + } + } + + /** + * Adds the given host to the router's host table + * + * @param string $alias + * @param string ...$hostnames + * + * @return void + * + * @since 2.6.0 + */ + public function addHost(string $alias, string ...$hostnames): void + { + foreach ($hostnames as $hostname) { + $this->hosts[$alias][] = $hostname; + } + } + + /** + * Adds the given hosts to the router's host table + * + * @param array> $hosts + * + * @return void + * + * @since 2.11.0 + */ + public function addHosts(array $hosts): void + { + foreach ($hosts as $alias => $hostnames) { + foreach ($hostnames as $hostname) { + $this->hosts[$alias][] = $hostname; + } + } + } + /** * Gets the router's host table * - * @return array + * @return array> * * @since 2.6.0 */ @@ -108,7 +154,7 @@ public function getHosts(): array } /** - * Resolves the given hostname + * Resolves the given hostname to its alias * * @param string $hostname * @@ -129,19 +175,53 @@ public function resolveHostname(string $hostname): ?string return null; } + /** + * Adds the given route(s) to the router + * + * @param RouteInterface ...$routes + * + * @return void + */ + public function addRoute(RouteInterface ...$routes): void + { + foreach ($routes as $route) { + $host = $route->getHost() ?? '*'; + $name = $route->getName(); + + $this->routes[$host][$name] = $route; + } + } + /** * Gets all routes * - * @return RouteInterface[] + * @return Generator */ - public function getRoutes(): array + public function getRoutes(): Generator { - $routes = []; - foreach ($this->routes as $route) { - $routes[] = $route; + foreach ($this->routes as $routes) { + foreach ($routes as $route) { + yield $route; + } } + } - return $routes; + /** + * Gets routes by the given host + * + * @param string $host + * + * @return Generator + * + * @since 3.0.0 + */ + public function getRoutesByHost(string $host): Generator + { + if (isset($this->routes[$host])) { + foreach ($this->routes[$host] as $route) { + yield $route; + } + } } /** @@ -149,150 +229,235 @@ public function getRoutes(): array * * @param string $hostname * - * @return RouteInterface[] + * @return Generator * * @since 2.14.0 */ - public function getRoutesByHostname(string $hostname): array + public function getRoutesByHostname(string $hostname): Generator { - // the hostname's alias. - $alias = $this->resolveHostname($hostname); - - $routes = []; - foreach ($this->routes as $route) { - $host = $route->getHost(); - if ($host === null || $host === $alias) { - $routes[] = $route; + $host = $this->resolveHostname($hostname); + + if (isset($host) && isset($this->routes[$host])) { + foreach ($this->routes[$host] as $route) { + yield $route; } } - return $routes; + if (isset($this->routes['*'])) { + foreach ($this->routes['*'] as $route) { + yield $route; + } + } } /** - * Gets the router's middlewares + * Gets a route by the given name + * + * @param string $name * - * @return MiddlewareInterface[] + * @return RouteInterface + * + * @throws RouteNotFoundException + * If a route wasn't found by the given name. */ - public function getMiddlewares(): array + public function getRoute(string $name): RouteInterface { - return $this->middlewares; + foreach ($this->routes as $routes) { + if (isset($routes[$name])) { + return $routes[$name]; + } + } + + throw new RouteNotFoundException(sprintf( + 'No route found for name "%s"', + $name + )); } /** - * Gets the router's matched route + * Gets a route by the given name and host * - * @return RouteInterface|null + * @param string $name + * @param string $host + * + * @return RouteInterface + * + * @throws RouteNotFoundException + * If a route wasn't found by the given name and host. + * + * @since 3.0.0 */ - public function getMatchedRoute(): ?RouteInterface + public function getNamedRouteByHost(string $name, string $host): RouteInterface { - return $this->matchedRoute; + if (isset($this->routes[$host][$name])) { + return $this->routes[$host][$name]; + } + + throw new RouteNotFoundException(sprintf( + 'No route found for name "%s" and host "%s"', + $name, + $host + )); } /** - * Gets the router's event dispatcher + * Gets a route by the given name and hostname * - * @return EventDispatcherInterface|null + * @param string $name + * @param string $hostname * - * @since 2.13.0 + * @return RouteInterface + * + * @throws RouteNotFoundException + * If a route wasn't found by the given name and hostname. + * + * @since 3.0.0 */ - public function getEventDispatcher(): ?EventDispatcherInterface + public function getNamedRouteByHostname(string $name, string $hostname): RouteInterface { - return $this->eventDispatcher; + $host = $this->resolveHostname($hostname); + + if (isset($host) && isset($this->routes[$host][$name])) { + return $this->routes[$host][$name]; + } + + if (isset($this->routes['*'][$name])) { + return $this->routes['*'][$name]; + } + + throw new RouteNotFoundException(sprintf( + 'No route found for name "%s" and hostname "%s"', + $name, + $hostname + )); } /** - * Adds the given patterns to the router - * - * ```php - * $router->addPatterns([ - * '@digit' => '\d+', - * '@word' => '\w+', - * ]); - * ``` - * - * ```php - * $route->setPath('/{foo<@digit>}/{bar<@word>}'); - * ``` - * - * @param array $patterns - * - * @return void + * Checks if a route exists by the given name * - * @since 2.11.0 + * @return bool */ - public function addPatterns(array $patterns): void + public function hasRoute(string $name): bool { - foreach ($patterns as $alias => $pattern) { - self::$patterns[$alias] = $pattern; + foreach ($this->routes as $routes) { + if (isset($routes[$name])) { + return true; + } } + + return false; } /** - * Adds aliases for hostnames to the router's host table + * Checks if a route exists by the given name and host * - * ```php - * $router->addHosts([ - * 'local' => ['127.0.0.1', 'localhost'], - * ]); - * ``` + * @return bool * - * ```php - * // will be available at 127.0.0.1 - * $route->setHost('local'); - * ``` + * @since 3.0.0 + */ + public function existsNamedRouteByHost(string $name, string $host): bool + { + if (isset($this->routes[$host][$name])) { + return true; + } + + return false; + } + + /** + * Checks if a route exists by the given name and hostname * - * @param array $hosts + * @return bool * - * @return void + * @since 3.0.0 + */ + public function existsNamedRouteByHostname(string $name, string $hostname): bool + { + if (isset($this->routes['*'][$name])) { + return true; + } + + $host = $this->resolveHostname($hostname); + + if (isset($host) && isset($this->routes[$host][$name])) { + return true; + } + + return false; + } + + /** + * Gets allowed methods * - * @since 2.11.0 + * @return list */ - public function addHosts(array $hosts): void + public function getAllowedMethods(): array { - foreach ($hosts as $alias => $hostnames) { - $this->addHost($alias, ...$hostnames); + $methods = []; + foreach ($this->routes as $routes) { + foreach ($routes as $route) { + foreach ($route->getMethods() as $method) { + $methods[$method] = $method; + } + } } + + return empty($methods) ? [] : \array_values($methods); } /** - * Adds the given alias for the given hostname(s) to the router's host table + * Gets allowed methods by the given host * - * @param string $alias - * @param string ...$hostnames + * @param string $host * - * @return void + * @return list * - * @since 2.6.0 + * @since 3.0.0 */ - public function addHost(string $alias, string ...$hostnames): void + public function getAllowedMethodsByHost(string $host): array { - $this->hosts[$alias] = $hostnames; + $methods = []; + if (isset($this->routes[$host])) { + foreach ($this->routes[$host] as $route) { + foreach ($route->getMethods() as $method) { + $methods[$method] = $method; + } + } + } + + return empty($methods) ? [] : \array_values($methods); } /** - * Adds the given route(s) to the router + * Gets allowed methods by the given hostname * - * @param RouteInterface ...$routes + * @param string $hostname * - * @return void + * @return list * - * @throws InvalidArgumentException - * if one of the given routes already exists. + * @since 3.0.0 */ - public function addRoute(RouteInterface ...$routes): void + public function getAllowedMethodsByHostname(string $hostname): array { - foreach ($routes as $route) { - $name = $route->getName(); - if (isset($this->routes[$name])) { - throw new InvalidArgumentException(sprintf( - 'The route "%s" already exists.', - $name - )); + $methods = []; + if (isset($this->routes['*'])) { + foreach ($this->routes['*'] as $route) { + foreach ($route->getMethods() as $method) { + $methods[$method] = $method; + } } + } - $this->routes[$name] = $route; + $host = $this->resolveHostname($hostname); + if (isset($host) && isset($this->routes[$host])) { + foreach ($this->routes[$host] as $route) { + foreach ($route->getMethods() as $method) { + $methods[$method] = $method; + } + } } + + return empty($methods) ? [] : \array_values($methods); } /** @@ -310,65 +475,49 @@ public function addMiddleware(MiddlewareInterface ...$middlewares): void } /** - * Sets the given event dispatcher to the router - * - * @param EventDispatcherInterface|null $eventDispatcher - * - * @return void + * Gets the router's middlewares * - * @since 2.13.0 + * @return list */ - public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher): void + public function getMiddlewares(): array { - $this->eventDispatcher = $eventDispatcher; + return $this->middlewares; } /** - * Gets allowed methods + * Gets the router's matched route * - * @return string[] + * @return RouteInterface|null */ - public function getAllowedMethods(): array + public function getMatchedRoute(): ?RouteInterface { - $methods = []; - foreach ($this->routes as $route) { - foreach ($route->getMethods() as $method) { - $methods[$method] = true; - } - } - - return array_keys($methods); + return $this->matchedRoute; } /** - * Checks if a route exists by the given name + * Sets the given event dispatcher to the router * - * @return bool + * @param EventDispatcherInterface|null $eventDispatcher + * + * @return void + * + * @since 2.13.0 */ - public function hasRoute(string $name): bool + public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher): void { - return isset($this->routes[$name]); + $this->eventDispatcher = $eventDispatcher; } /** - * Gets a route for the given name - * - * @param string $name + * Gets the router's event dispatcher * - * @return RouteInterface + * @return EventDispatcherInterface|null * - * @throws RouteNotFoundException + * @since 2.13.0 */ - public function getRoute(string $name): RouteInterface + public function getEventDispatcher(): ?EventDispatcherInterface { - if (!isset($this->routes[$name])) { - throw new RouteNotFoundException(sprintf( - 'No route found for the name "%s".', - $name - )); - } - - return $this->routes[$name]; + return $this->eventDispatcher; } /** @@ -381,7 +530,7 @@ public function getRoute(string $name): RouteInterface * @return string * * @throws RouteNotFoundException - * If the given named route wasn't found. + * If a route wasn't found by the given name. * * @throws Exception\RoutePathBuildException * If a required attribute value is not given, @@ -403,22 +552,26 @@ public function generateUri(string $name, array $attributes = [], bool $strict = * * @return RouteInterface * - * @throws MethodNotAllowedException * @throws PageNotFoundException + * If the request URI cannot be matched against any route. + * + * @throws MethodNotAllowedException + * If the request method isn't allowed. */ public function match(ServerRequestInterface $request): RouteInterface { - $currentUri = $request->getUri(); - $currentHost = $currentUri->getHost(); - $currentPath = $currentUri->getPath(); - $currentMethod = $request->getMethod(); + $requestUri = $request->getUri(); + $requestHost = $requestUri->getHost(); + $requestPath = $requestUri->getPath(); + $requestMethod = $request->getMethod(); $allowedMethods = []; - $currentHostRoutes = $this->getRoutesByHostname($currentHost); - foreach ($currentHostRoutes as $route) { + $routes = $this->getRoutesByHostname($requestHost); + + foreach ($routes as $route) { // https://github.com/sunrise-php/http-router/issues/50 // https://tools.ietf.org/html/rfc7231#section-6.5.5 - if (!path_match($route->getPath(), $currentPath, $attributes)) { + if (!path_match($route->getPath(), $requestPath, $attributes)) { continue; } @@ -428,7 +581,7 @@ public function match(ServerRequestInterface $request): RouteInterface $allowedMethods[$routeMethod] = $routeMethod; } - if (!isset($routeMethods[$currentMethod])) { + if (!isset($routeMethods[$requestMethod])) { continue; } @@ -438,7 +591,7 @@ public function match(ServerRequestInterface $request): RouteInterface } if (!empty($allowedMethods)) { - throw new MethodNotAllowedException($currentMethod, $allowedMethods); + throw new MethodNotAllowedException($requestMethod, $allowedMethods); } throw new PageNotFoundException(); From c0af5575b11817078f2700afad61e95b3f73b3ff Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Wed, 8 Feb 2023 02:22:50 +0100 Subject: [PATCH 055/180] v3 --- src/Loader/ConfigLoader.php | 41 ++++--------------- src/Loader/DescriptorLoader.php | 39 ++++-------------- src/Middleware/CallableMiddleware.php | 17 ++------ .../RequestEntityParameterResolver.php | 9 +++- src/RequestHandler/CallableRequestHandler.php | 16 ++------ src/Route.php | 15 +++---- src/RouteCollector.php | 36 +++------------- src/RouteInterface.php | 4 +- 8 files changed, 46 insertions(+), 131 deletions(-) diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 809323b3..b00acf26 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -14,10 +14,10 @@ /** * Import classes */ -use Psr\Container\ContainerInterface; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ParameterResolver\DependencyInjectionParameterResolver; +use Sunrise\Http\Router\ClassResolver; +use Sunrise\Http\Router\ClassResolverInterface; use Sunrise\Http\Router\ParameterResolutioner; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ParameterResolverInterface; @@ -85,13 +85,15 @@ final class ConfigLoader implements LoaderInterface * @param ReferenceResolverInterface|null $referenceResolver * @param ParameterResolutionerInterface|null $parameterResolutioner * @param ResponseResolutionerInterface|null $responseResolutioner + * @param ClassResolverInterface|null $classResolver */ public function __construct( ?RouteCollectionFactoryInterface $collectionFactory = null, ?RouteFactoryInterface $routeFactory = null, ?ReferenceResolverInterface $referenceResolver = null, ?ParameterResolutionerInterface $parameterResolutioner = null, - ?ResponseResolutionerInterface $responseResolutioner = null + ?ResponseResolutionerInterface $responseResolutioner = null, + ?ClassResolverInterface $classResolver = null ) { $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory(); $this->routeFactory = $routeFactory ?? new RouteFactory(); @@ -101,33 +103,8 @@ public function __construct( $this->referenceResolver = $referenceResolver ?? new ReferenceResolver( $this->parameterResolutioner ??= new ParameterResolutioner(), - $this->responseResolutioner ??= new ResponseResolutioner() - ); - } - - /** - * Sets the given container to the parameter resolutioner - * - * @param ContainerInterface $container - * - * @return void - * - * @throws LogicException - * If a custom reference resolver was setted - * and a parameter resolutioner was not passed. - */ - public function setContainer(ContainerInterface $container): void - { - if (!isset($this->parameterResolutioner)) { - throw new LogicException( - 'The config route loader cannot accept the container ' . - 'because a custom reference resolver was setted ' . - 'and a parameter resolutioner was not passed' - ); - } - - $this->parameterResolutioner->addResolver( - new DependencyInjectionParameterResolver($container) + $this->responseResolutioner ??= new ResponseResolutioner(), + $classResolver ?? new ClassResolver($this->parameterResolutioner) ); } @@ -148,7 +125,7 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): { if (!isset($this->parameterResolutioner)) { throw new LogicException( - 'The config route loader cannot accept the parameter resolver ' . + 'The config route loader cannot accept the parameter resolver(s) ' . 'because a custom reference resolver was setted ' . 'and a parameter resolutioner was not passed' ); @@ -174,7 +151,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo { if (!isset($this->responseResolutioner)) { throw new LogicException( - 'The config route loader cannot accept the response resolver ' . + 'The config route loader cannot accept the response resolver(s) ' . 'because a custom reference resolver was setted ' . 'and a response resolutioner was not passed' ); diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 554a52c4..c313aaa3 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -14,7 +14,6 @@ /** * Import classes */ -use Psr\Container\ContainerInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\SimpleCache\CacheInterface; use Sunrise\Http\Router\Annotation\Host; @@ -25,8 +24,9 @@ use Sunrise\Http\Router\Annotation\Tag; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ParameterResolver\DependencyInjectionParameterResolver; use Sunrise\Http\Router\AnnotationReader; +use Sunrise\Http\Router\ClassResolver; +use Sunrise\Http\Router\ClassResolverInterface; use Sunrise\Http\Router\ParameterResolutioner; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ParameterResolverInterface; @@ -118,6 +118,7 @@ final class DescriptorLoader implements LoaderInterface * @param ReferenceResolverInterface|null $referenceResolver * @param ParameterResolutionerInterface|null $parameterResolutioner * @param ResponseResolutionerInterface|null $responseResolutioner + * @param ClassResolverInterface|null $classResolver * @param \Doctrine\Common\Annotations\Reader|null $annotationReader */ public function __construct( @@ -126,6 +127,7 @@ public function __construct( ?ReferenceResolverInterface $referenceResolver = null, ?ParameterResolutionerInterface $parameterResolutioner = null, ?ResponseResolutionerInterface $responseResolutioner = null, + ?ClassResolverInterface $classResolver = null, ?\Doctrine\Common\Annotations\Reader $annotationReader = null ) { $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory(); @@ -136,7 +138,8 @@ public function __construct( $this->referenceResolver = $referenceResolver ?? new ReferenceResolver( $this->parameterResolutioner ??= new ParameterResolutioner(), - $this->responseResolutioner ??= new ResponseResolutioner() + $this->responseResolutioner ??= new ResponseResolutioner(), + $classResolver ?? new ClassResolver($this->parameterResolutioner) ); $this->annotationReader = new AnnotationReader(); @@ -148,32 +151,6 @@ public function __construct( } } - /** - * Sets the given container to the parameter resolutioner - * - * @param ContainerInterface $container - * - * @return void - * - * @throws LogicException - * If a custom reference resolver was setted - * and a parameter resolutioner was not passed. - */ - public function setContainer(ContainerInterface $container): void - { - if (!isset($this->parameterResolutioner)) { - throw new LogicException( - 'The descriptor route loader cannot accept the container ' . - 'because a custom reference resolver was setted ' . - 'and a parameter resolutioner was not passed' - ); - } - - $this->parameterResolutioner->addResolver( - new DependencyInjectionParameterResolver($container) - ); - } - /** * Adds the given parameter resolver(s) to the parameter resolutioner * @@ -191,7 +168,7 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): { if (!isset($this->parameterResolutioner)) { throw new LogicException( - 'The descriptor route loader cannot accept the parameter resolver ' . + 'The descriptor route loader cannot accept the parameter resolver(s) ' . 'because a custom reference resolver was setted ' . 'and a parameter resolutioner was not passed' ); @@ -217,7 +194,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo { if (!isset($this->responseResolutioner)) { throw new LogicException( - 'The descriptor route loader cannot accept the response resolver ' . + 'The descriptor route loader cannot accept the response resolver(s) ' . 'because a custom reference resolver was setted ' . 'and a response resolutioner was not passed' ); diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index fb2874e7..6b15b442 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -18,6 +18,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\ParameterResolver\KnownNameParameterResolver; use Sunrise\Http\Router\ParameterResolver\KnownTypeParameterResolver; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ResponseResolutionerInterface; @@ -89,18 +90,6 @@ public function getReflection(): ReflectionFunctionAbstract return reflect_callable($this->callback); } - /** - * Gets the callback's parameters - * - * @return list - * - * @since 3.0.0 - */ - public function getParameters(): array - { - return $this->getReflection()->getParameters(); - } - /** * {@inheritdoc} */ @@ -109,12 +98,14 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $parameterResolvers = [ new KnownTypeParameterResolver(ServerRequestInterface::class, $request), new KnownTypeParameterResolver(RequestHandlerInterface::class, $handler), + new KnownNameParameterResolver('request', $request), + new KnownNameParameterResolver('handler', $handler), ]; $arguments = $this->parameterResolutioner ->withContext($request) ->withPriorityResolver(...$parameterResolvers) - ->resolveParameters(...$this->getParameters()); + ->resolveParameters(...$this->getReflection()->getParameters()); /** @var mixed */ $response = ($this->callback)(...$arguments); diff --git a/src/ParameterResolver/RequestEntityParameterResolver.php b/src/ParameterResolver/RequestEntityParameterResolver.php index e3215720..ab839bc3 100644 --- a/src/ParameterResolver/RequestEntityParameterResolver.php +++ b/src/ParameterResolver/RequestEntityParameterResolver.php @@ -167,19 +167,24 @@ public function resolveParameter(ReflectionParameter $parameter, $context) */ private function stringifyParameter(ReflectionParameter $parameter): string { + /** @var ReflectionNamedType */ + $parameterType = $parameter->getType(); + if ($parameter->getDeclaringFunction() instanceof ReflectionMethod) { return sprintf( - '%s::%s($%s[%d])', + '%s::%s(%s $%s[%d])', $parameter->getDeclaringFunction()->getDeclaringClass()->getName(), $parameter->getDeclaringFunction()->getName(), + $parameterType->getName(), $parameter->getName(), $parameter->getPosition() ); } return sprintf( - '%s($%s[%d])', + '%s(%s $%s[%d])', $parameter->getDeclaringFunction()->getName(), + $parameterType->getName(), $parameter->getName(), $parameter->getPosition() ); diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index 8ea31019..e94df3c8 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -17,6 +17,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\ParameterResolver\KnownNameParameterResolver; use Sunrise\Http\Router\ParameterResolver\KnownTypeParameterResolver; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ResponseResolutionerInterface; @@ -86,18 +87,6 @@ public function getReflection(): ReflectionFunctionAbstract return reflect_callable($this->callback); } - /** - * Gets the callback's parameters - * - * @return list - * - * @since 3.0.0 - */ - public function getParameters(): array - { - return $this->getReflection()->getParameters(); - } - /** * {@inheritdoc} */ @@ -105,12 +94,13 @@ public function handle(ServerRequestInterface $request): ResponseInterface { $parameterResolvers = [ new KnownTypeParameterResolver(ServerRequestInterface::class, $request), + new KnownNameParameterResolver('request', $request), ]; $arguments = $this->parameterResolutioner ->withContext($request) ->withPriorityResolver(...$parameterResolvers) - ->resolveParameters(...$this->getParameters()); + ->resolveParameters(...$this->getReflection()->getParameters()); /** @var mixed */ $response = ($this->callback)(...$arguments); diff --git a/src/Route.php b/src/Route.php index 140d8819..9963cd96 100644 --- a/src/Route.php +++ b/src/Route.php @@ -394,19 +394,20 @@ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterfa /** * {@inheritdoc} */ - public function addPriorityMiddleware(MiddlewareInterface ...$priorityMiddlewares): RouteInterface + public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): RouteInterface { - $previousMiddlewares = $this->middlewares; - $this->middlewares = []; + $newValue = []; - foreach ($priorityMiddlewares as $middleware) { - $this->middlewares[] = $middleware; + foreach ($middlewares as $middleware) { + $newValue[] = $middleware; } - foreach ($previousMiddlewares as $middleware) { - $this->middlewares[] = $middleware; + foreach ($this->middlewares as $middleware) { + $newValue[] = $middleware; } + $this->middlewares = $newValue; + return $this; } diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 043fbbda..c6654eef 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -14,9 +14,7 @@ /** * Import classes */ -use Psr\Container\ContainerInterface; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ParameterResolver\DependencyInjectionParameterResolver; /** * RouteCollector @@ -70,6 +68,7 @@ class RouteCollector * @param ReferenceResolverInterface|null $referenceResolver * @param ParameterResolutionerInterface|null $parameterResolutioner * @param ResponseResolutionerInterface|null $responseResolutioner + * @param ClassResolverInterface|null $classResolver */ public function __construct( ?RouteCollectionFactoryInterface $collectionFactory = null, @@ -86,7 +85,8 @@ public function __construct( $this->referenceResolver = $referenceResolver ?? new ReferenceResolver( $this->parameterResolutioner ??= new ParameterResolutioner(), - $this->responseResolutioner ??= new ResponseResolutioner() + $this->responseResolutioner ??= new ResponseResolutioner(), + $classResolver ?? new ClassResolver($this->parameterResolutioner) ); $this->collection = $this->collectionFactory->createCollection(); @@ -112,32 +112,6 @@ public function getRoutes(): array return $this->collection->all(); } - /** - * Sets the given container to the parameter resolutioner - * - * @param ContainerInterface $container - * - * @return void - * - * @throws LogicException - * If a custom reference resolver was setted - * and a parameter resolutioner was not passed. - */ - public function setContainer(ContainerInterface $container): void - { - if (!isset($this->parameterResolutioner)) { - throw new LogicException( - 'The route collector cannot accept the container ' . - 'because a custom reference resolver was setted ' . - 'and a parameter resolutioner was not passed' - ); - } - - $this->parameterResolutioner->addResolver( - new DependencyInjectionParameterResolver($container) - ); - } - /** * Adds the given parameter resolver(s) to the parameter resolutioner * @@ -155,7 +129,7 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): { if (!isset($this->parameterResolutioner)) { throw new LogicException( - 'The route collector cannot accept the parameter resolver ' . + 'The route collector cannot accept the parameter resolver(s) ' . 'because a custom reference resolver was setted ' . 'and a parameter resolutioner was not passed' ); @@ -181,7 +155,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo { if (!isset($this->responseResolutioner)) { throw new LogicException( - 'The route collector cannot accept the response resolver ' . + 'The route collector cannot accept the response resolver(s) ' . 'because a custom reference resolver was setted ' . 'and a response resolutioner was not passed' ); diff --git a/src/RouteInterface.php b/src/RouteInterface.php index d35b6509..42a7620f 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -273,13 +273,13 @@ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterfa /** * Adds the given priority middleware(s) to the route * - * @param MiddlewareInterface ...$priorityMiddlewares + * @param MiddlewareInterface ...$middlewares * * @return RouteInterface * * @since 3.0.0 */ - public function addPriorityMiddleware(MiddlewareInterface ...$priorityMiddlewares): RouteInterface; + public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): RouteInterface; /** * Adds the given tag(s) to the route From e5c9f7d0ea1123588938c7689a737a10bc259d9b Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Thu, 9 Feb 2023 01:07:31 +0100 Subject: [PATCH 056/180] v3 --- src/Annotation/Consume.php | 52 +++ src/Annotation/Method.php | 52 +++ src/Annotation/Produce.php | 52 +++ src/Annotation/Route.php | 26 ++ src/AnnotationReader.php | 178 --------- src/Loader/ConfigLoader.php | 25 +- src/Loader/DescriptorLoader.php | 159 +++++--- src/Middleware/CallableMiddleware.php | 13 +- .../JsonPayloadDecodingMiddleware.php | 42 +-- ...er.php => KnownTypedParameterResolver.php} | 33 +- ....php => KnownUntypedParameterResolver.php} | 10 +- .../ServerRequestParameterResolver.php | 66 ++++ src/RequestHandler/CallableRequestHandler.php | 9 +- src/Route.php | 80 ++++ src/RouteCollection.php | 48 +++ src/RouteCollectionInterface.php | 44 +++ src/RouteCollector.php | 31 +- src/RouteInterface.php | 62 +++ src/Router.php | 3 + src/ServerRequest.php | 357 ++++++++++++++++++ 20 files changed, 998 insertions(+), 344 deletions(-) create mode 100644 src/Annotation/Consume.php create mode 100644 src/Annotation/Method.php create mode 100644 src/Annotation/Produce.php delete mode 100644 src/AnnotationReader.php rename src/ParameterResolver/{KnownTypeParameterResolver.php => KnownTypedParameterResolver.php} (63%) rename src/ParameterResolver/{KnownNameParameterResolver.php => KnownUntypedParameterResolver.php} (84%) create mode 100644 src/ParameterResolver/ServerRequestParameterResolver.php create mode 100644 src/ServerRequest.php diff --git a/src/Annotation/Consume.php b/src/Annotation/Consume.php new file mode 100644 index 00000000..7e26f8aa --- /dev/null +++ b/src/Annotation/Consume.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Annotation; + +/** + * Import classes + */ +use Attribute; + +/** + * @Annotation + * + * @Target({"CLASS", "METHOD"}) + * + * @NamedArgumentConstructor + * + * @Attributes({ + * @Attribute("value", type="string", required=true), + * }) + * + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)] +final class Consume +{ + + /** + * The attribute value + * + * @var string + */ + public string $value; + + /** + * Constructor of the class + * + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } +} diff --git a/src/Annotation/Method.php b/src/Annotation/Method.php new file mode 100644 index 00000000..e6ff17f2 --- /dev/null +++ b/src/Annotation/Method.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Annotation; + +/** + * Import classes + */ +use Attribute; + +/** + * @Annotation + * + * @Target({"CLASS", "METHOD"}) + * + * @NamedArgumentConstructor + * + * @Attributes({ + * @Attribute("value", type="string", required=true), + * }) + * + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)] +final class Method +{ + + /** + * The attribute value + * + * @var string + */ + public string $value; + + /** + * Constructor of the class + * + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } +} diff --git a/src/Annotation/Produce.php b/src/Annotation/Produce.php new file mode 100644 index 00000000..16be685c --- /dev/null +++ b/src/Annotation/Produce.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Annotation; + +/** + * Import classes + */ +use Attribute; + +/** + * @Annotation + * + * @Target({"CLASS", "METHOD"}) + * + * @NamedArgumentConstructor + * + * @Attributes({ + * @Attribute("value", type="string", required=true), + * }) + * + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)] +final class Produce +{ + + /** + * The attribute value + * + * @var string + */ + public string $value; + + /** + * Constructor of the class + * + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } +} diff --git a/src/Annotation/Route.php b/src/Annotation/Route.php index 9f0d25f4..2250b5c8 100644 --- a/src/Annotation/Route.php +++ b/src/Annotation/Route.php @@ -31,6 +31,8 @@ * @Attribute("path", type="string", required=true), * @Attribute("method", type="string"), * @Attribute("methods", type="array"), + * @Attribute("consumes", type="array"), + * @Attribute("produces", type="array"), * @Attribute("middlewares", type="array"), * @Attribute("attributes", type="array"), * @Attribute("summary", type="string"), @@ -80,6 +82,24 @@ final class Route implements RequestMethodInterface */ public array $methods; + /** + * The route's consumed content types + * + * @var list + * + * @since 3.0.0 + */ + public array $consumes; + + /** + * The route's produced content types + * + * @var list + * + * @since 3.0.0 + */ + public array $produces; + /** * The route middlewares * @@ -130,6 +150,8 @@ final class Route implements RequestMethodInterface * @param string $path The route path * @param string|null $method The route method * @param list $methods The route methods + * @param list $consumes The route's consumed content types + * @param list $produces The route's produced content types * @param list> $middlewares The route middlewares * @param array $attributes The route attributes * @param string $summary The route summary @@ -143,6 +165,8 @@ public function __construct( string $path = '/', ?string $method = null, array $methods = [], + array $consumes = [], + array $produces = [], array $middlewares = [], array $attributes = [], string $summary = '', @@ -164,6 +188,8 @@ public function __construct( $this->host = $host; $this->path = $path; $this->methods = $methods; + $this->consumes = $consumes; + $this->produces = $produces; $this->middlewares = $middlewares; $this->attributes = $attributes; $this->summary = $summary; diff --git a/src/AnnotationReader.php b/src/AnnotationReader.php deleted file mode 100644 index f20c850d..00000000 --- a/src/AnnotationReader.php +++ /dev/null @@ -1,178 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router; - -/** - * Import classes - */ -use Sunrise\Http\Router\Exception\InvalidArgumentException; -use Sunrise\Http\Router\Exception\LogicException; -use ReflectionAttribute; -use ReflectionClass; -use ReflectionMethod; -use Reflector; - -/** - * Import functions - */ -use function class_exists; -use function sprintf; - -/** - * Import constants - */ -use const PHP_MAJOR_VERSION; - -/** - * AnnotationReader - * - * @since 3.0.0 - */ -final class AnnotationReader -{ - - /** - * @var \Doctrine\Common\Annotations\Reader|null - */ - private ?\Doctrine\Common\Annotations\Reader $annotationReader = null; - - /** - * Sets the given annotation reader to the reader - * - * @param \Doctrine\Common\Annotations\Reader|null $annotationReader - * - * @return void - */ - public function setAnnotationReader(?\Doctrine\Common\Annotations\Reader $annotationReader): void - { - $this->annotationReader = $annotationReader; - } - - /** - * Uses the default annotation reader - * - * @return void - * - * @throws LogicException - * If the "doctrine/annotations" package isn't installed. - */ - public function useDefaultAnnotationReader(): void - { - if (!class_exists(\Doctrine\Common\Annotations\AnnotationReader::class)) { - throw new LogicException( - 'The annotations reading logic requires an uninstalled "doctrine/annotations" package, ' . - 'run the command "composer install doctrine/annotations" and try again' - ); - } - - $this->setAnnotationReader(new \Doctrine\Common\Annotations\AnnotationReader()); - } - - /** - * Gets annotations from the given class or method by the given annotation name - * - * @param Reflector $classOrMethod - * @param class-string $annotationName - * - * @return list - * - * @throws InvalidArgumentException - * If the given reflection isn't supported. - * - * @template T - */ - public function getClassOrMethodAnnotations(Reflector $classOrMethod, string $annotationName): array - { - if ($classOrMethod instanceof ReflectionClass) { - return $this->getClassAnnotations($classOrMethod, $annotationName); - } - - if ($classOrMethod instanceof ReflectionMethod) { - return $this->getMethodAnnotations($classOrMethod, $annotationName); - } - - throw new InvalidArgumentException(sprintf( - 'The %s method only handles class or method reflection', - __METHOD__ - )); - } - - /** - * Gets annotations from the given class by the given annotation name - * - * @param ReflectionClass $class - * @param class-string $annotationName - * - * @return list - * - * @template T - */ - public function getClassAnnotations(ReflectionClass $class, string $annotationName): array - { - $result = []; - - if (PHP_MAJOR_VERSION === 8) { - /** @var ReflectionAttribute[] */ - $attributes = $class->getAttributes($annotationName); - foreach ($attributes as $attribute) { - /** @var T */ - $result[] = $attribute->newInstance(); - } - } - - if (isset($this->annotationReader)) { - $annotations = $this->annotationReader->getClassAnnotations($class); - foreach ($annotations as $annotation) { - if ($annotation instanceof $annotationName) { - $result[] = $annotation; - } - } - } - - return $result; - } - - /** - * Gets annotations from the given method by the given annotation name - * - * @param ReflectionMethod $method - * @param class-string $annotationName - * - * @return list - * - * @template T - */ - public function getMethodAnnotations(ReflectionMethod $method, string $annotationName): array - { - $result = []; - - if (PHP_MAJOR_VERSION === 8) { - /** @var ReflectionAttribute[] */ - $attributes = $method->getAttributes($annotationName); - foreach ($attributes as $attribute) { - /** @var T */ - $result[] = $attribute->newInstance(); - } - } - - if (isset($this->annotationReader)) { - $annotations = $this->annotationReader->getMethodAnnotations($method); - foreach ($annotations as $annotation) { - if ($annotation instanceof $annotationName) { - $result[] = $annotation; - } - } - } - - return $result; - } -} diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index b00acf26..b746a240 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -16,8 +16,6 @@ */ use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ClassResolver; -use Sunrise\Http\Router\ClassResolverInterface; use Sunrise\Http\Router\ParameterResolutioner; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ParameterResolverInterface; @@ -70,12 +68,12 @@ final class ConfigLoader implements LoaderInterface /** * @var ParameterResolutionerInterface|null */ - private ?ParameterResolutionerInterface $parameterResolutioner = null; + private ?ParameterResolutionerInterface $parameterResolutioner; /** * @var ResponseResolutionerInterface|null */ - private ?ResponseResolutionerInterface $responseResolutioner = null; + private ?ResponseResolutionerInterface $responseResolutioner; /** * Constructor of the class @@ -85,15 +83,13 @@ final class ConfigLoader implements LoaderInterface * @param ReferenceResolverInterface|null $referenceResolver * @param ParameterResolutionerInterface|null $parameterResolutioner * @param ResponseResolutionerInterface|null $responseResolutioner - * @param ClassResolverInterface|null $classResolver */ public function __construct( ?RouteCollectionFactoryInterface $collectionFactory = null, ?RouteFactoryInterface $routeFactory = null, ?ReferenceResolverInterface $referenceResolver = null, ?ParameterResolutionerInterface $parameterResolutioner = null, - ?ResponseResolutionerInterface $responseResolutioner = null, - ?ClassResolverInterface $classResolver = null + ?ResponseResolutionerInterface $responseResolutioner = null ) { $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory(); $this->routeFactory = $routeFactory ?? new RouteFactory(); @@ -103,8 +99,7 @@ public function __construct( $this->referenceResolver = $referenceResolver ?? new ReferenceResolver( $this->parameterResolutioner ??= new ParameterResolutioner(), - $this->responseResolutioner ??= new ResponseResolutioner(), - $classResolver ?? new ClassResolver($this->parameterResolutioner) + $this->responseResolutioner ??= new ResponseResolutioner() ); } @@ -116,8 +111,7 @@ public function __construct( * @return void * * @throws LogicException - * If a custom reference resolver was setted - * and a parameter resolutioner was not passed. + * If a custom reference resolver was setted and a parameter resolutioner wasn't passed. * * @since 3.0.0 */ @@ -126,8 +120,7 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The config route loader cannot accept the parameter resolver(s) ' . - 'because a custom reference resolver was setted ' . - 'and a parameter resolutioner was not passed' + 'because a custom reference resolver was setted and a parameter resolutioner was not passed' ); } @@ -142,8 +135,7 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): * @return void * * @throws LogicException - * If a custom reference resolver was setted - * and a response resolutioner was not passed. + * If a custom reference resolver was setted and a response resolutioner wasn't passed. * * @since 3.0.0 */ @@ -152,8 +144,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo if (!isset($this->responseResolutioner)) { throw new LogicException( 'The config route loader cannot accept the response resolver(s) ' . - 'because a custom reference resolver was setted ' . - 'and a response resolutioner was not passed' + 'because a custom reference resolver was setted and a response resolutioner was not passed' ); } diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index c313aaa3..51c5b896 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -14,19 +14,21 @@ /** * Import classes */ +use Doctrine\Common\Annotations\AnnotationReader; +use Doctrine\Common\Annotations\Reader as AnnotationReaderInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\SimpleCache\CacheInterface; +use Sunrise\Http\Router\Annotation\Consume; use Sunrise\Http\Router\Annotation\Host; +use Sunrise\Http\Router\Annotation\Method; use Sunrise\Http\Router\Annotation\Middleware; use Sunrise\Http\Router\Annotation\Postfix; use Sunrise\Http\Router\Annotation\Prefix; +use Sunrise\Http\Router\Annotation\Produce; use Sunrise\Http\Router\Annotation\Route; use Sunrise\Http\Router\Annotation\Tag; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\AnnotationReader; -use Sunrise\Http\Router\ClassResolver; -use Sunrise\Http\Router\ClassResolverInterface; use Sunrise\Http\Router\ParameterResolutioner; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ParameterResolverInterface; @@ -40,6 +42,7 @@ use Sunrise\Http\Router\RouteCollectionInterface; use Sunrise\Http\Router\RouteFactory; use Sunrise\Http\Router\RouteFactoryInterface; +use ReflectionAttribute; use ReflectionClass; use ReflectionMethod; use Reflector; @@ -88,17 +91,17 @@ final class DescriptorLoader implements LoaderInterface /** * @var ParameterResolutionerInterface|null */ - private ?ParameterResolutionerInterface $parameterResolutioner = null; + private ?ParameterResolutionerInterface $parameterResolutioner; /** * @var ResponseResolutionerInterface|null */ - private ?ResponseResolutionerInterface $responseResolutioner = null; + private ?ResponseResolutionerInterface $responseResolutioner; /** - * @var AnnotationReader + * @var AnnotationReaderInterface|null */ - private AnnotationReader $annotationReader; + private ?AnnotationReaderInterface $annotationReader = null; /** * @var CacheInterface|null @@ -118,17 +121,13 @@ final class DescriptorLoader implements LoaderInterface * @param ReferenceResolverInterface|null $referenceResolver * @param ParameterResolutionerInterface|null $parameterResolutioner * @param ResponseResolutionerInterface|null $responseResolutioner - * @param ClassResolverInterface|null $classResolver - * @param \Doctrine\Common\Annotations\Reader|null $annotationReader */ public function __construct( ?RouteCollectionFactoryInterface $collectionFactory = null, ?RouteFactoryInterface $routeFactory = null, ?ReferenceResolverInterface $referenceResolver = null, ?ParameterResolutionerInterface $parameterResolutioner = null, - ?ResponseResolutionerInterface $responseResolutioner = null, - ?ClassResolverInterface $classResolver = null, - ?\Doctrine\Common\Annotations\Reader $annotationReader = null + ?ResponseResolutionerInterface $responseResolutioner = null ) { $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory(); $this->routeFactory = $routeFactory ?? new RouteFactory(); @@ -138,17 +137,8 @@ public function __construct( $this->referenceResolver = $referenceResolver ?? new ReferenceResolver( $this->parameterResolutioner ??= new ParameterResolutioner(), - $this->responseResolutioner ??= new ResponseResolutioner(), - $classResolver ?? new ClassResolver($this->parameterResolutioner) + $this->responseResolutioner ??= new ResponseResolutioner() ); - - $this->annotationReader = new AnnotationReader(); - - if (isset($annotationReader)) { - $this->annotationReader->setAnnotationReader($annotationReader); - } elseif (PHP_MAJOR_VERSION < 8) { - $this->annotationReader->useDefaultAnnotationReader(); - } } /** @@ -159,8 +149,7 @@ public function __construct( * @return void * * @throws LogicException - * If a custom reference resolver was setted - * and a parameter resolutioner was not passed. + * If a custom reference resolver was setted and a parameter resolutioner wasn't passed. * * @since 3.0.0 */ @@ -169,8 +158,7 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The descriptor route loader cannot accept the parameter resolver(s) ' . - 'because a custom reference resolver was setted ' . - 'and a parameter resolutioner was not passed' + 'because a custom reference resolver was setted and a parameter resolutioner was not passed' ); } @@ -185,8 +173,7 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): * @return void * * @throws LogicException - * If a custom reference resolver was setted - * and a response resolutioner was not passed. + * If a custom reference resolver was setted and a response resolutioner wasn't passed. * * @since 3.0.0 */ @@ -195,8 +182,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo if (!isset($this->responseResolutioner)) { throw new LogicException( 'The descriptor route loader cannot accept the response resolver(s) ' . - 'because a custom reference resolver was setted ' . - 'and a response resolutioner was not passed' + 'because a custom reference resolver was setted and a response resolutioner was not passed' ); } @@ -204,29 +190,39 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo } /** - * Sets the given annotation reader to the descriptor loader - * - * @param \Doctrine\Common\Annotations\Reader|null $annotationReader + * Uses the default annotation reader * * @return void * + * @throws LogicException + * If the "doctrine/annotations" package isn't installed. + * * @since 3.0.0 */ - public function setAnnotationReader(?\Doctrine\Common\Annotations\Reader $annotationReader): void + public function useDefaultAnnotationReader(): void { - $this->annotationReader->setAnnotationReader($annotationReader); + if (!class_exists(AnnotationReader::class)) { + throw new LogicException( + 'The annotations reading logic requires an uninstalled "doctrine/annotations" package, ' . + 'run the following command "composer install doctrine/annotations" and try again' + ); + } + + $this->annotationReader = new AnnotationReader(); } /** - * Uses the default annotation reader + * Sets the given annotation reader to the loader + * + * @param AnnotationReaderInterface|null $annotationReader * * @return void * * @since 3.0.0 */ - public function useDefaultAnnotationReader(): void + public function setAnnotationReader(?AnnotationReaderInterface $annotationReader): void { - $this->annotationReader->useDefaultAnnotationReader(); + $this->annotationReader = $annotationReader; } /** @@ -325,24 +321,28 @@ public function attachArray(array $resources): void */ public function load(): RouteCollectionInterface { - $routes = []; + $routes = $this->collectionFactory->createCollection(); + $descriptors = $this->getDescriptors(); foreach ($descriptors as $descriptor) { - $routes[] = $this->routeFactory->createRoute( + $route = $this->routeFactory->createRoute( $descriptor->name, $descriptor->path, $descriptor->methods, $this->referenceResolver->resolveRequestHandler($descriptor->holder), $this->referenceResolver->resolveMiddlewares($descriptor->middlewares), $descriptor->attributes - ) - ->setHost($descriptor->host) - ->setSummary($descriptor->summary) - ->setDescription($descriptor->description) - ->setTags(...$descriptor->tags); + ); + + $route->setHost($descriptor->host); + $route->setSummary($descriptor->summary); + $route->setDescription($descriptor->description); + $route->setTags(...$descriptor->tags); + + $routes->add($route); } - return $this->collectionFactory->createCollection(...$routes); + return $routes; } /** @@ -402,7 +402,7 @@ private function getClassDescriptors(ReflectionClass $class): array $result = []; if ($class->isSubclassOf(RequestHandlerInterface::class)) { - $annotations = $this->annotationReader->getClassAnnotations($class, Route::class); + $annotations = $this->getClassOrMethodAnnotations($class, Route::class); if (isset($annotations[0])) { $descriptor = $annotations[0]; $descriptor->holder = $class->getName(); @@ -417,7 +417,7 @@ private function getClassDescriptors(ReflectionClass $class): array continue; } - $annotations = $this->annotationReader->getMethodAnnotations($method, Route::class); + $annotations = $this->getClassOrMethodAnnotations($method, Route::class); if (isset($annotations[0])) { $descriptor = $annotations[0]; $descriptor->holder = [$class->getName(), $method->getName()]; @@ -430,6 +430,44 @@ private function getClassDescriptors(ReflectionClass $class): array return $result; } + /** + * Gets annotations from the given class or method + * + * @param ReflectionClass|ReflectionMethod $classOrMethod + * @param class-string $annotationName + * + * @return list + * + * @template T + */ + private function getClassOrMethodAnnotations(Reflector $classOrMethod, string $annotationName): array + { + $result = []; + + if (PHP_MAJOR_VERSION === 8) { + /** @var ReflectionAttribute[] */ + $attributes = $classOrMethod->getAttributes($annotationName); + foreach ($attributes as $attribute) { + /** @var T */ + $result[] = $attribute->newInstance(); + } + } + + if (isset($this->annotationReader)) { + $annotations = ($classOrMethod instanceof ReflectionClass) ? + $this->annotationReader->getClassAnnotations($classOrMethod) : + $this->annotationReader->getMethodAnnotations($classOrMethod); + + foreach ($annotations as $annotation) { + if ($annotation instanceof $annotationName) { + $result[] = $annotation; + } + } + } + + return $result; + } + /** * Supplements the given descriptor from the given class or method * @@ -440,27 +478,42 @@ private function getClassDescriptors(ReflectionClass $class): array */ private function supplementDescriptor(Route $descriptor, Reflector $classOrMethod): void { - $annotations = $this->annotationReader->getClassOrMethodAnnotations($classOrMethod, Host::class); + $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Host::class); if (isset($annotations[0])) { $descriptor->host = $annotations[0]->value; } - $annotations = $this->annotationReader->getClassOrMethodAnnotations($classOrMethod, Prefix::class); + $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Prefix::class); if (isset($annotations[0])) { $descriptor->path = $annotations[0]->value . $descriptor->path; } - $annotations = $this->annotationReader->getClassOrMethodAnnotations($classOrMethod, Postfix::class); + $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Postfix::class); if (isset($annotations[0])) { $descriptor->path = $descriptor->path . $annotations[0]->value; } - $annotations = $this->annotationReader->getClassOrMethodAnnotations($classOrMethod, Middleware::class); + $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Method::class); + foreach ($annotations as $annotation) { + $descriptor->methods[] = $annotation->value; + } + + $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Consume::class); + foreach ($annotations as $annotation) { + $descriptor->consumes[] = $annotation->value; + } + + $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Produce::class); + foreach ($annotations as $annotation) { + $descriptor->produces[] = $annotation->value; + } + + $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Middleware::class); foreach ($annotations as $annotation) { $descriptor->middlewares[] = $annotation->value; } - $annotations = $this->annotationReader->getClassOrMethodAnnotations($classOrMethod, Tag::class); + $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Tag::class); foreach ($annotations as $annotation) { $descriptor->tags[] = $annotation->value; } diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index 6b15b442..0e2e5502 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -18,14 +18,13 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\ParameterResolver\KnownNameParameterResolver; -use Sunrise\Http\Router\ParameterResolver\KnownTypeParameterResolver; +use Sunrise\Http\Router\ParameterResolver\KnownTypedParameterResolver; +use Sunrise\Http\Router\ParameterResolver\KnownUntypedParameterResolver; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ResponseResolutionerInterface; use ReflectionFunctionAbstract; use ReflectionFunction; use ReflectionMethod; -use ReflectionParameter; /** * Import functions @@ -96,10 +95,10 @@ public function getReflection(): ReflectionFunctionAbstract public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $parameterResolvers = [ - new KnownTypeParameterResolver(ServerRequestInterface::class, $request), - new KnownTypeParameterResolver(RequestHandlerInterface::class, $handler), - new KnownNameParameterResolver('request', $request), - new KnownNameParameterResolver('handler', $handler), + new KnownTypedParameterResolver(ServerRequestInterface::class, $request), + new KnownUntypedParameterResolver('request', $request), + new KnownTypedParameterResolver(RequestHandlerInterface::class, $handler), + new KnownUntypedParameterResolver('handler', $handler), ]; $arguments = $this->parameterResolutioner diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 2bf508bb..4e12e4a2 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -19,6 +19,7 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; +use Sunrise\Http\Router\ServerRequest; use JsonException; /** @@ -27,9 +28,6 @@ use function is_array; use function json_decode; use function sprintf; -use function strpos; -use function strstr; -use function trim; /** * Import constants @@ -52,10 +50,8 @@ final class JsonPayloadDecodingMiddleware implements MiddlewareInterface public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if ($this->supportsRequest($request)) { - return $handler->handle( - $request->withParsedBody( - $this->decodeRequestPayload($request) - ) + $request = $request->withParsedBody( + $this->decodeRequestPayload($request) ); } @@ -65,42 +61,17 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface /** * Checks if the given request is supported * - * @link https://datatracker.ietf.org/doc/html/rfc4627 - * * @param ServerRequestInterface $request * * @return bool */ private function supportsRequest(ServerRequestInterface $request): bool { - return $this->getRequestMediaType($request) === 'application/json'; - } - - /** - * Gets a media type from the given request - * - * Returns null if a media type cannot be retrieved. - * - * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1 - * - * @param ServerRequestInterface $request - * - * @return string|null - */ - private function getRequestMediaType(ServerRequestInterface $request): ?string - { - if (!$request->hasHeader('Content-Type')) { - return null; - } - - // type "/" subtype *( OWS ";" OWS parameter ) - $mediaType = $request->getHeaderLine('Content-Type'); - - if (false !== strpos($mediaType, ';')) { - $mediaType = strstr($mediaType, ';', true); + if (!($request instanceof ServerRequest)) { + $request = new ServerRequest($request); } - return trim($mediaType); + return $request->isJson(); } /** @@ -125,6 +96,7 @@ private function decodeRequestPayload(ServerRequestInterface $request): ?array throw new InvalidRequestPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); } + // according to PSR-7 the parsed body can be only an array or an object... return is_array($result) ? $result : null; } } diff --git a/src/ParameterResolver/KnownTypeParameterResolver.php b/src/ParameterResolver/KnownTypedParameterResolver.php similarity index 63% rename from src/ParameterResolver/KnownTypeParameterResolver.php rename to src/ParameterResolver/KnownTypedParameterResolver.php index 574b0329..59a42f3d 100644 --- a/src/ParameterResolver/KnownTypeParameterResolver.php +++ b/src/ParameterResolver/KnownTypedParameterResolver.php @@ -14,23 +14,16 @@ /** * Import classes */ -use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\ParameterResolverInterface; use ReflectionNamedType; use ReflectionParameter; /** - * Import classes - */ -use function get_class; -use function sprintf; - -/** - * KnownTypeParameterResolver + * KnownTypedParameterResolver * * @since 3.0.0 */ -final class KnownTypeParameterResolver implements ParameterResolverInterface +final class KnownTypedParameterResolver implements ParameterResolverInterface { /** @@ -46,21 +39,9 @@ final class KnownTypeParameterResolver implements ParameterResolverInterface /** * @param class-string $type * @param object $value - * - * @throws InvalidArgumentException - * If the given value is not an instance of the given type. */ public function __construct(string $type, object $value) { - if (!($value instanceof $type)) { - throw new InvalidArgumentException(sprintf( - 'The known type parameter resolver cannot accept the value "%s" ' . - 'because it is not an instance of the "%s"', - get_class($value), - $type - )); - } - $this->type = $type; $this->value = $value; } @@ -74,7 +55,15 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo return false; } - return $this->type === $parameter->getType()->getName(); + if ($parameter->getType()->isBuiltin()) { + return false; + } + + if (!($parameter->getType()->getName() === $this->type)) { + return false; + } + + return true; } /** diff --git a/src/ParameterResolver/KnownNameParameterResolver.php b/src/ParameterResolver/KnownUntypedParameterResolver.php similarity index 84% rename from src/ParameterResolver/KnownNameParameterResolver.php rename to src/ParameterResolver/KnownUntypedParameterResolver.php index 58f65db2..34c3ffe9 100644 --- a/src/ParameterResolver/KnownNameParameterResolver.php +++ b/src/ParameterResolver/KnownUntypedParameterResolver.php @@ -18,11 +18,11 @@ use ReflectionParameter; /** - * KnownNameParameterResolver + * KnownUntypedParameterResolver * * @since 3.0.0 */ -final class KnownNameParameterResolver implements ParameterResolverInterface +final class KnownUntypedParameterResolver implements ParameterResolverInterface { /** @@ -54,7 +54,11 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo return false; } - return $this->name === $parameter->getName(); + if (!($parameter->getName() === $this->name)) { + return false; + } + + return true; } /** diff --git a/src/ParameterResolver/ServerRequestParameterResolver.php b/src/ParameterResolver/ServerRequestParameterResolver.php new file mode 100644 index 00000000..f02d9b17 --- /dev/null +++ b/src/ParameterResolver/ServerRequestParameterResolver.php @@ -0,0 +1,66 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ParameterResolver; + +/** + * Import classes + */ +use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\ParameterResolverInterface; +use Sunrise\Http\Router\ServerRequest; +use ReflectionNamedType; +use ReflectionParameter; + +/** + * ServerRequestParameterResolver + * + * @since 3.0.0 + */ +final class ServerRequestParameterResolver implements ParameterResolverInterface +{ + + /** + * {@inheritdoc} + */ + public function supportsParameter(ReflectionParameter $parameter, $context): bool + { + if (!($context instanceof ServerRequestInterface)) { + return false; + } + + if (!($parameter->getType() instanceof ReflectionNamedType)) { + return false; + } + + if (!($parameter->getType()->getName() === ServerRequest::class)) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function resolveParameter(ReflectionParameter $parameter, $context) + { + /** @var ServerRequestInterface */ + $context = $context; + + if ($context instanceof ServerRequest) { + return $context; + } + + return new ServerRequest($context); + } +} + diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index e94df3c8..f655b6b4 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -17,14 +17,13 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\ParameterResolver\KnownNameParameterResolver; -use Sunrise\Http\Router\ParameterResolver\KnownTypeParameterResolver; +use Sunrise\Http\Router\ParameterResolver\KnownTypedParameterResolver; +use Sunrise\Http\Router\ParameterResolver\KnownUntypedParameterResolver; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ResponseResolutionerInterface; use ReflectionFunctionAbstract; use ReflectionFunction; use ReflectionMethod; -use ReflectionParameter; /** * Import functions @@ -93,8 +92,8 @@ public function getReflection(): ReflectionFunctionAbstract public function handle(ServerRequestInterface $request): ResponseInterface { $parameterResolvers = [ - new KnownTypeParameterResolver(ServerRequestInterface::class, $request), - new KnownNameParameterResolver('request', $request), + new KnownTypedParameterResolver(ServerRequestInterface::class, $request), + new KnownUntypedParameterResolver('request', $request), ]; $arguments = $this->parameterResolutioner diff --git a/src/Route.php b/src/Route.php index 9963cd96..47a86167 100644 --- a/src/Route.php +++ b/src/Route.php @@ -65,6 +65,20 @@ class Route implements RouteInterface */ private array $methods = []; + /** + * The route's consumed content types + * + * @var list + */ + private array $consumedContentTypes = []; + + /** + * The route's produced content types + * + * @var list + */ + private array $producedContentTypes = []; + /** * The route request handler * @@ -165,6 +179,22 @@ public function getMethods(): array return $this->methods; } + /** + * {@inheritdoc} + */ + public function getConsumedContentTypes(): array + { + return $this->consumedContentTypes; + } + + /** + * {@inheritdoc} + */ + public function getProducedContentTypes(): array + { + return $this->producedContentTypes; + } + /** * {@inheritdoc} */ @@ -268,6 +298,32 @@ public function setMethods(string ...$methods): RouteInterface return $this; } + /** + * {@inheritdoc} + */ + public function setConsumedContentTypes(string ...$contentTypes): RouteInterface + { + $this->consumedContentTypes = []; + foreach ($contentTypes as $contentType) { + $this->consumedContentTypes[] = $contentType; + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setProducedContentTypes(string ...$contentTypes): RouteInterface + { + $this->producedContentTypes = []; + foreach ($contentTypes as $contentType) { + $this->producedContentTypes[] = $contentType; + } + + return $this; + } + /** * {@inheritdoc} */ @@ -379,6 +435,30 @@ public function addMethod(string ...$methods): RouteInterface return $this; } + /** + * {@inheritdoc} + */ + public function addConsumedContentType(string ...$contentTypes): RouteInterface + { + foreach ($contentTypes as $contentType) { + $this->consumedContentTypes[] = $contentType; + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addProducedContentType(string ...$contentTypes): RouteInterface + { + foreach ($contentTypes as $contentType) { + $this->producedContentTypes[] = $contentType; + } + + return $this; + } + /** * {@inheritdoc} */ diff --git a/src/RouteCollection.php b/src/RouteCollection.php index c84cb11e..37bc8165 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -99,6 +99,30 @@ public function setHost(string $host): RouteCollectionInterface return $this; } + /** + * {@inheritdoc} + */ + public function setConsumedContentTypes(string ...$contentTypes): RouteCollectionInterface + { + foreach ($this->routes as $route) { + $route->setConsumedContentTypes(...$contentTypes); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setProducedContentTypes(string ...$contentTypes): RouteCollectionInterface + { + foreach ($this->routes as $route) { + $route->setProducedContentTypes(...$contentTypes); + } + + return $this; + } + /** * {@inheritdoc} */ @@ -147,6 +171,30 @@ public function addMethod(string ...$methods): RouteCollectionInterface return $this; } + /** + * {@inheritdoc} + */ + public function addConsumedContentType(string ...$contentTypes): RouteCollectionInterface + { + foreach ($this->routes as $route) { + $route->addConsumedContentType(...$contentTypes); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addProducedContentType(string ...$contentTypes): RouteCollectionInterface + { + foreach ($this->routes as $route) { + $route->addProducedContentType(...$contentTypes); + } + + return $this; + } + /** * {@inheritdoc} */ diff --git a/src/RouteCollectionInterface.php b/src/RouteCollectionInterface.php index 5b00c533..9809a0d7 100644 --- a/src/RouteCollectionInterface.php +++ b/src/RouteCollectionInterface.php @@ -72,6 +72,28 @@ public function add(RouteInterface ...$routes): RouteCollectionInterface; */ public function setHost(string $host): RouteCollectionInterface; + /** + * Sets the given consumed content type(s) to all routes in the collection + * + * @param string ...$contentTypes + * + * @return RouteCollectionInterface + * + * @since 3.0.0 + */ + public function setConsumedContentTypes(string ...$contentTypes): RouteCollectionInterface; + + /** + * Sets the given produced content type(s) to all routes in the collection + * + * @param string ...$contentTypes + * + * @return RouteCollectionInterface + * + * @since 3.0.0 + */ + public function setProducedContentTypes(string ...$contentTypes): RouteCollectionInterface; + /** * Sets the given attribute to all routes in the collection * @@ -117,6 +139,28 @@ public function addSuffix(string $suffix): RouteCollectionInterface; */ public function addMethod(string ...$methods): RouteCollectionInterface; + /** + * Adds the given consumed content type(s) to all routes in the collection + * + * @param string ...$contentTypes + * + * @return RouteCollectionInterface + * + * @since 3.0.0 + */ + public function addConsumedContentType(string ...$contentTypes): RouteCollectionInterface; + + /** + * Adds the given produced content type(s) to all routes in the collection + * + * @param string ...$contentTypes + * + * @return RouteCollectionInterface + * + * @since 3.0.0 + */ + public function addProducedContentType(string ...$contentTypes): RouteCollectionInterface; + /** * Adds the given middleware(s) to all routes in the collection * diff --git a/src/RouteCollector.php b/src/RouteCollector.php index c6654eef..e4fd9912 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -53,13 +53,12 @@ class RouteCollector /** * @var ParameterResolutionerInterface|null */ - private ?ParameterResolutionerInterface $parameterResolutioner = null; + private ?ParameterResolutionerInterface $parameterResolutioner; /** * @var ResponseResolutionerInterface|null */ - private ?ResponseResolutionerInterface $responseResolutioner = null; - + private ?ResponseResolutionerInterface $responseResolutioner; /** * Constructor of the class * @@ -68,7 +67,6 @@ class RouteCollector * @param ReferenceResolverInterface|null $referenceResolver * @param ParameterResolutionerInterface|null $parameterResolutioner * @param ResponseResolutionerInterface|null $responseResolutioner - * @param ClassResolverInterface|null $classResolver */ public function __construct( ?RouteCollectionFactoryInterface $collectionFactory = null, @@ -85,8 +83,7 @@ public function __construct( $this->referenceResolver = $referenceResolver ?? new ReferenceResolver( $this->parameterResolutioner ??= new ParameterResolutioner(), - $this->responseResolutioner ??= new ResponseResolutioner(), - $classResolver ?? new ClassResolver($this->parameterResolutioner) + $this->responseResolutioner ??= new ResponseResolutioner() ); $this->collection = $this->collectionFactory->createCollection(); @@ -102,16 +99,6 @@ public function getCollection(): RouteCollectionInterface return $this->collection; } - /** - * Gets the collector's routes - * - * @return list - */ - public function getRoutes(): array - { - return $this->collection->all(); - } - /** * Adds the given parameter resolver(s) to the parameter resolutioner * @@ -120,8 +107,7 @@ public function getRoutes(): array * @return void * * @throws LogicException - * If a custom reference resolver was setted - * and a parameter resolutioner was not passed. + * If a custom reference resolver was setted and a parameter resolutioner wasn't passed. * * @since 3.0.0 */ @@ -130,8 +116,7 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The route collector cannot accept the parameter resolver(s) ' . - 'because a custom reference resolver was setted ' . - 'and a parameter resolutioner was not passed' + 'because a custom reference resolver was setted and a parameter resolutioner was not passed' ); } @@ -146,8 +131,7 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): * @return void * * @throws LogicException - * If a custom reference resolver was setted - * and a response resolutioner was not passed. + * If a custom reference resolver was setted and a response resolutioner wasn't passed. * * @since 3.0.0 */ @@ -156,8 +140,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo if (!isset($this->responseResolutioner)) { throw new LogicException( 'The route collector cannot accept the response resolver(s) ' . - 'because a custom reference resolver was setted ' . - 'and a response resolutioner was not passed' + 'because a custom reference resolver was setted and a response resolutioner was not passed' ); } diff --git a/src/RouteInterface.php b/src/RouteInterface.php index 42a7620f..c1d71beb 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -67,6 +67,24 @@ public function getPath(): string; */ public function getMethods(): array; + /** + * Gets the route's consumed content types + * + * @return list + * + * @since 3.0.0 + */ + public function getConsumedContentTypes(): array; + + /** + * Gets the route's produced content types + * + * @return list + * + * @since 3.0.0 + */ + public function getProducedContentTypes(): array; + /** * Gets the route request handler * @@ -162,6 +180,28 @@ public function setPath(string $path): RouteInterface; */ public function setMethods(string ...$methods): RouteInterface; + /** + * Sets the given consumed content type(s) to the route + * + * @param string ...$contentTypes + * + * @return RouteInterface + * + * @since 3.0.0 + */ + public function setConsumedContentTypes(string ...$contentTypes): RouteInterface; + + /** + * Sets the given produced content type(s) to the route + * + * @param string ...$contentTypes + * + * @return RouteInterface + * + * @since 3.0.0 + */ + public function setProducedContentTypes(string ...$contentTypes): RouteInterface; + /** * Sets the given request handler to the route * @@ -261,6 +301,28 @@ public function addSuffix(string $suffix): RouteInterface; */ public function addMethod(string ...$methods): RouteInterface; + /** + * Adds the given consumed content type(s) to the route + * + * @param string ...$contentTypes + * + * @return RouteInterface + * + * @since 3.0.0 + */ + public function addConsumedContentType(string ...$contentTypes): RouteInterface; + + /** + * Adds the given produced content type(s) to the route + * + * @param string ...$contentTypes + * + * @return RouteInterface + * + * @since 3.0.0 + */ + public function addProducedContentType(string ...$contentTypes): RouteInterface; + /** * Adds the given middleware(s) to the route * diff --git a/src/Router.php b/src/Router.php index 10ca2ebd..5f24013e 100644 --- a/src/Router.php +++ b/src/Router.php @@ -585,6 +585,9 @@ public function match(ServerRequestInterface $request): RouteInterface continue; } + // $routeConsumedContentTypes = $route->getConsumedContentTypes(); + // $routeProducedContentTypes = $route->getProducedContentTypes(); + /** @var array $attributes */ return $route->withAddedAttributes($attributes); diff --git a/src/ServerRequest.php b/src/ServerRequest.php new file mode 100644 index 00000000..fbb3792f --- /dev/null +++ b/src/ServerRequest.php @@ -0,0 +1,357 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * Import classes + */ +use Psr\Http\Message\ServerRequestInterface; + +/** + * ServerRequest + * + * @since 3.0.0 + */ +final class ServerRequest implements ServerRequestInterface +{ + + /** + * @var ServerRequestInterface + */ + private ServerRequestInterface $request; + + /** + * Constructor of the class + * + * @param ServerRequestInterface $request + */ + public function __construct(ServerRequestInterface $request) + { + $this->request = $request; + } + + /** + * Checks if the request is JSON + * + * @link https://datatracker.ietf.org/doc/html/rfc4627 + * + * @return bool + */ + public function isJson(): bool + { + return $this->getContentType() === 'application/json'; + } + + /** + * Gets the request media type + * + * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1 + * + * @return string|null + */ + public function getContentType(): ?string + { + if (!$this->request->hasHeader('Content-Type')) { + return null; + } + + $result = $this->request->getHeaderLine('Content-Type'); + + if (false !== \strpos($result, ';')) { + $result = \strstr($result, ';', true); + } + + return \trim($result); + } + + /** + * {@inheritdoc} + */ + public function getProtocolVersion(): string + { + return $this->request->getProtocolVersion(); + } + + /** + * {@inheritdoc} + */ + public function withProtocolVersion($version) + { + $clone = clone $this; + $clone->request = $clone->request->withProtocolVersion($version); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function getHeaders(): array + { + return $this->request->getHeaders(); + } + + /** + * {@inheritdoc} + */ + public function hasHeader($name): bool + { + return $this->request->hasHeader($name); + } + + /** + * {@inheritdoc} + */ + public function getHeader($name): array + { + return $this->request->getHeader($name); + } + + /** + * {@inheritdoc} + */ + public function getHeaderLine($name): string + { + return $this->request->getHeaderLine($name); + } + + /** + * {@inheritdoc} + */ + public function withHeader($name, $value) + { + $clone = clone $this; + $clone->request = $clone->request->withHeader($name, $value); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function withAddedHeader($name, $value) + { + $clone = clone $this; + $clone->request = $clone->request->withAddedHeader($name, $value); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function withoutHeader($name) + { + $clone = clone $this; + $clone->request = $clone->request->withoutHeader($name); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function getBody(): \Psr\Http\Message\StreamInterface + { + return $this->request->getBody(); + } + + /** + * {@inheritdoc} + */ + public function withBody(\Psr\Http\Message\StreamInterface $body) + { + $clone = clone $this; + $clone->request = $clone->request->withBody($body); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function getMethod(): string + { + return $this->request->getMethod(); + } + + /** + * {@inheritdoc} + */ + public function withMethod($method) + { + $clone = clone $this; + $clone->request = $clone->request->withMethod($method); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function getUri(): \Psr\Http\Message\UriInterface + { + return $this->request->getUri(); + } + + /** + * {@inheritdoc} + */ + public function withUri(\Psr\Http\Message\UriInterface $uri, $preserveHost = false) + { + $clone = clone $this; + $clone->request = $clone->request->withUri($uri, $preserveHost); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function getRequestTarget(): string + { + return $this->request->getRequestTarget(); + } + + /** + * {@inheritdoc} + */ + public function withRequestTarget($requestTarget) + { + $clone = clone $this; + $clone->request = $clone->request->withRequestTarget($requestTarget); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function getServerParams(): array + { + return $this->request->getServerParams(); + } + + /** + * {@inheritdoc} + */ + public function getQueryParams(): array + { + return $this->request->getQueryParams(); + } + + /** + * {@inheritdoc} + */ + public function withQueryParams(array $query) + { + $clone = clone $this; + $clone->request = $clone->request->withQueryParams($query); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function getCookieParams(): array + { + return $this->request->getCookieParams(); + } + + /** + * {@inheritdoc} + */ + public function withCookieParams(array $cookies) + { + $clone = clone $this; + $clone->request = $clone->request->withCookieParams($cookies); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function getUploadedFiles(): array + { + return $this->request->getUploadedFiles(); + } + + /** + * {@inheritdoc} + */ + public function withUploadedFiles(array $uploadedFiles) + { + $clone = clone $this; + $clone->request = $clone->request->withUploadedFiles($uploadedFiles); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function getParsedBody() + { + return $this->request->getParsedBody(); + } + + /** + * {@inheritdoc} + */ + public function withParsedBody($data) + { + $clone = clone $this; + $clone->request = $clone->request->withParsedBody($data); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function getAttributes(): array + { + return $this->request->getAttributes(); + } + + /** + * {@inheritdoc} + */ + public function getAttribute($name, $default = null) + { + return $this->request->getAttribute($name, $default); + } + + /** + * {@inheritdoc} + */ + public function withAttribute($name, $value) + { + $clone = clone $this; + $clone->request = $clone->request->withAttribute($name, $value); + + return $clone; + } + + /** + * {@inheritdoc} + */ + public function withoutAttribute($name) + { + $clone = clone $this; + $clone->request = $clone->request->withoutAttribute($name); + + return $clone; + } +} From 2189b3aa70bafffb32d7e7c6f71c8c2e260b5c65 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Thu, 9 Feb 2023 01:12:26 +0100 Subject: [PATCH 057/180] v3 --- src/Router.php | 74 -------------------------------------------------- 1 file changed, 74 deletions(-) diff --git a/src/Router.php b/src/Router.php index 5f24013e..bd794545 100644 --- a/src/Router.php +++ b/src/Router.php @@ -386,80 +386,6 @@ public function existsNamedRouteByHostname(string $name, string $hostname): bool return false; } - /** - * Gets allowed methods - * - * @return list - */ - public function getAllowedMethods(): array - { - $methods = []; - foreach ($this->routes as $routes) { - foreach ($routes as $route) { - foreach ($route->getMethods() as $method) { - $methods[$method] = $method; - } - } - } - - return empty($methods) ? [] : \array_values($methods); - } - - /** - * Gets allowed methods by the given host - * - * @param string $host - * - * @return list - * - * @since 3.0.0 - */ - public function getAllowedMethodsByHost(string $host): array - { - $methods = []; - if (isset($this->routes[$host])) { - foreach ($this->routes[$host] as $route) { - foreach ($route->getMethods() as $method) { - $methods[$method] = $method; - } - } - } - - return empty($methods) ? [] : \array_values($methods); - } - - /** - * Gets allowed methods by the given hostname - * - * @param string $hostname - * - * @return list - * - * @since 3.0.0 - */ - public function getAllowedMethodsByHostname(string $hostname): array - { - $methods = []; - if (isset($this->routes['*'])) { - foreach ($this->routes['*'] as $route) { - foreach ($route->getMethods() as $method) { - $methods[$method] = $method; - } - } - } - - $host = $this->resolveHostname($hostname); - if (isset($host) && isset($this->routes[$host])) { - foreach ($this->routes[$host] as $route) { - foreach ($route->getMethods() as $method) { - $methods[$method] = $method; - } - } - } - - return empty($methods) ? [] : \array_values($methods); - } - /** * Adds the given middleware(s) to the router * From 576da85655310e2b9afe173f3bc53ac869bf0602 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Thu, 9 Feb 2023 01:16:03 +0100 Subject: [PATCH 058/180] v3 --- src/Loader/DescriptorLoader.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 51c5b896..3cde3e61 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -335,6 +335,8 @@ public function load(): RouteCollectionInterface ); $route->setHost($descriptor->host); + $route->setConsumedContentTypes(...$descriptor->consumes); + $route->setProducedContentTypes(...$descriptor->produces); $route->setSummary($descriptor->summary); $route->setDescription($descriptor->description); $route->setTags(...$descriptor->tags); From 34333e207f896cdea9c50fbcd012595afd33723e Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Thu, 9 Feb 2023 19:34:32 +0100 Subject: [PATCH 059/180] v3 --- src/Annotation/Description.php | 52 +++++++++++++++++++++++++++++++++ src/Annotation/Summary.php | 52 +++++++++++++++++++++++++++++++++ src/Loader/ConfigLoader.php | 1 + src/Loader/DescriptorLoader.php | 13 +++++++++ 4 files changed, 118 insertions(+) create mode 100644 src/Annotation/Description.php create mode 100644 src/Annotation/Summary.php diff --git a/src/Annotation/Description.php b/src/Annotation/Description.php new file mode 100644 index 00000000..0457e99c --- /dev/null +++ b/src/Annotation/Description.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Annotation; + +/** + * Import classes + */ +use Attribute; + +/** + * @Annotation + * + * @Target({"CLASS", "METHOD"}) + * + * @NamedArgumentConstructor + * + * @Attributes({ + * @Attribute("value", type="string", required=true), + * }) + * + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)] +final class Description +{ + + /** + * The attribute value + * + * @var string + */ + public string $value; + + /** + * Constructor of the class + * + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } +} diff --git a/src/Annotation/Summary.php b/src/Annotation/Summary.php new file mode 100644 index 00000000..c19eaaa0 --- /dev/null +++ b/src/Annotation/Summary.php @@ -0,0 +1,52 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Annotation; + +/** + * Import classes + */ +use Attribute; + +/** + * @Annotation + * + * @Target({"CLASS", "METHOD"}) + * + * @NamedArgumentConstructor + * + * @Attributes({ + * @Attribute("value", type="string", required=true), + * }) + * + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD|Attribute::IS_REPEATABLE)] +final class Summary +{ + + /** + * The attribute value + * + * @var string + */ + public string $value; + + /** + * Constructor of the class + * + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } +} diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index b746a240..9643b2fe 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -38,6 +38,7 @@ use function is_dir; use function is_file; use function is_string; +use function sprintf; /** * ConfigLoader diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 3cde3e61..13d6bc1d 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -19,6 +19,7 @@ use Psr\Http\Server\RequestHandlerInterface; use Psr\SimpleCache\CacheInterface; use Sunrise\Http\Router\Annotation\Consume; +use Sunrise\Http\Router\Annotation\Description; use Sunrise\Http\Router\Annotation\Host; use Sunrise\Http\Router\Annotation\Method; use Sunrise\Http\Router\Annotation\Middleware; @@ -26,6 +27,7 @@ use Sunrise\Http\Router\Annotation\Prefix; use Sunrise\Http\Router\Annotation\Produce; use Sunrise\Http\Router\Annotation\Route; +use Sunrise\Http\Router\Annotation\Summary; use Sunrise\Http\Router\Annotation\Tag; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; @@ -54,6 +56,7 @@ use function hash; use function is_dir; use function is_string; +use function sprintf; use function usort; use function Sunrise\Http\Router\get_dir_classes; @@ -515,6 +518,16 @@ private function supplementDescriptor(Route $descriptor, Reflector $classOrMetho $descriptor->middlewares[] = $annotation->value; } + $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Summary::class); + foreach ($annotations as $annotation) { + $descriptor->summary .= $annotation->value; + } + + $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Description::class); + foreach ($annotations as $annotation) { + $descriptor->description .= $annotation->value; + } + $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Tag::class); foreach ($annotations as $annotation) { $descriptor->tags[] = $annotation->value; From 4b5e5354dca7c10bb56b086a2cc9b15dfc03f0d1 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Fri, 10 Feb 2023 03:07:37 +0100 Subject: [PATCH 060/180] v3 --- .../Http/HttpMethodNotAllowedException.php | 20 - .../Http/HttpNotAcceptableException.php | 30 +- .../HttpUnsupportedMediaTypeException.php | 20 - src/Exception/MethodNotAllowedException.php | 24 -- ...on.php => RouteAlreadyExistsException.php} | 11 +- ...n.php => UnprocessableObjectException.php} | 4 +- .../UnprocessableRequestBodyException.php | 2 +- .../UnprocessableRequestQueryException.php | 2 +- src/HostTable.php | 77 ++++ src/Loader/DescriptorLoader.php | 4 +- src/Route.php | 44 +-- src/RouteCollection.php | 89 ++++- src/RouteCollectionInterface.php | 66 +++- src/RouteInterface.php | 32 +- src/Router.php | 346 +++--------------- src/ServerRequest.php | 136 ++++++- 16 files changed, 450 insertions(+), 457 deletions(-) delete mode 100644 src/Exception/MethodNotAllowedException.php rename src/Exception/{PageNotFoundException.php => RouteAlreadyExistsException.php} (69%) rename src/Exception/{UnprocessableEntityException.php => UnprocessableObjectException.php} (92%) create mode 100644 src/HostTable.php diff --git a/src/Exception/Http/HttpMethodNotAllowedException.php b/src/Exception/Http/HttpMethodNotAllowedException.php index 5c61c488..8dc9c424 100644 --- a/src/Exception/Http/HttpMethodNotAllowedException.php +++ b/src/Exception/Http/HttpMethodNotAllowedException.php @@ -34,13 +34,6 @@ class HttpMethodNotAllowedException extends HttpException { - /** - * Unallowed HTTP method - * - * @var string - */ - private string $unallowedMethod; - /** * Allowed HTTP methods * @@ -51,14 +44,12 @@ class HttpMethodNotAllowedException extends HttpException /** * Constructor of the class * - * @param string $unallowedMethod * @param string[] $allowedMethods * @param ?string $message * @param int $code * @param ?Throwable $previous */ public function __construct( - string $unallowedMethod, array $allowedMethods, ?string $message = null, int $code = 0, @@ -68,22 +59,11 @@ public function __construct( parent::__construct(self::STATUS_METHOD_NOT_ALLOWED, $message, $code, $previous); - $this->unallowedMethod = $unallowedMethod; foreach ($allowedMethods as $allowedMethod) { $this->allowedMethods[] = $allowedMethod; } } - /** - * Gets unallowed HTTP method - * - * @return string - */ - final public function getMethod(): string - { - return $this->unallowedMethod; - } - /** * Gets allowed HTTP methods * diff --git a/src/Exception/Http/HttpNotAcceptableException.php b/src/Exception/Http/HttpNotAcceptableException.php index b0eb0f19..66490101 100644 --- a/src/Exception/Http/HttpNotAcceptableException.php +++ b/src/Exception/Http/HttpNotAcceptableException.php @@ -29,17 +29,43 @@ class HttpNotAcceptableException extends HttpException { + /** + * Supported media types + * + * @var list + */ + private array $supportedMediaTypes = []; + /** * Constructor of the class * + * @param string[] $supportedMediaTypes * @param ?string $message * @param int $code * @param ?Throwable $previous */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { + public function __construct( + array $supportedMediaTypes, + ?string $message = null, + int $code = 0, + ?Throwable $previous = null + ) { $message ??= 'Not Acceptable'; parent::__construct(self::STATUS_NOT_ACCEPTABLE, $message, $code, $previous); + + foreach ($supportedMediaTypes as $supportedMediaType) { + $this->supportedMediaTypes[] = $supportedMediaType; + } + } + + /** + * Gets supported media types + * + * @return list + */ + final public function getSupportedTypes(): array + { + return $this->supportedMediaTypes; } } diff --git a/src/Exception/Http/HttpUnsupportedMediaTypeException.php b/src/Exception/Http/HttpUnsupportedMediaTypeException.php index 090887b7..de81ac2d 100644 --- a/src/Exception/Http/HttpUnsupportedMediaTypeException.php +++ b/src/Exception/Http/HttpUnsupportedMediaTypeException.php @@ -33,13 +33,6 @@ class HttpUnsupportedMediaTypeException extends HttpException { - /** - * Unsupported media type - * - * @var string - */ - private string $unsupportedMediaType; - /** * Supported media types * @@ -50,14 +43,12 @@ class HttpUnsupportedMediaTypeException extends HttpException /** * Constructor of the class * - * @param string $unsupportedMediaType * @param string[] $supportedMediaTypes * @param ?string $message * @param int $code * @param ?Throwable $previous */ public function __construct( - string $unsupportedMediaType, array $supportedMediaTypes, ?string $message = null, int $code = 0, @@ -67,22 +58,11 @@ public function __construct( parent::__construct(self::STATUS_UNSUPPORTED_MEDIA_TYPE, $message, $code, $previous); - $this->unsupportedMediaType = $unsupportedMediaType; foreach ($supportedMediaTypes as $supportedMediaType) { $this->supportedMediaTypes[] = $supportedMediaType; } } - /** - * Gets unsupported media type - * - * @return string - */ - final public function getType(): string - { - return $this->unsupportedMediaType; - } - /** * Gets supported media types * diff --git a/src/Exception/MethodNotAllowedException.php b/src/Exception/MethodNotAllowedException.php deleted file mode 100644 index b2edb2f4..00000000 --- a/src/Exception/MethodNotAllowedException.php +++ /dev/null @@ -1,24 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception; - -/** - * Import classes - */ -use Sunrise\Http\Router\Exception\Http\HttpMethodNotAllowedException; - -/** - * MethodNotAllowedException - */ -class MethodNotAllowedException extends HttpMethodNotAllowedException -{ -} diff --git a/src/Exception/PageNotFoundException.php b/src/Exception/RouteAlreadyExistsException.php similarity index 69% rename from src/Exception/PageNotFoundException.php rename to src/Exception/RouteAlreadyExistsException.php index 4a47e46a..33227f58 100644 --- a/src/Exception/PageNotFoundException.php +++ b/src/Exception/RouteAlreadyExistsException.php @@ -12,13 +12,10 @@ namespace Sunrise\Http\Router\Exception; /** - * Import classes - */ -use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; - -/** - * PageNotFoundException + * RouteAlreadyExistsException + * + * @since 3.0.0 */ -class PageNotFoundException extends HttpNotFoundException +class RouteAlreadyExistsException extends LogicException { } diff --git a/src/Exception/UnprocessableEntityException.php b/src/Exception/UnprocessableObjectException.php similarity index 92% rename from src/Exception/UnprocessableEntityException.php rename to src/Exception/UnprocessableObjectException.php index afe34b01..78bf3f50 100644 --- a/src/Exception/UnprocessableEntityException.php +++ b/src/Exception/UnprocessableObjectException.php @@ -18,11 +18,11 @@ use Symfony\Component\Validator\ConstraintViolationListInterface; /** - * UnprocessableEntityException + * UnprocessableObjectException * * @since 3.0.0 */ -class UnprocessableEntityException extends HttpUnprocessableEntityException +class UnprocessableObjectException extends HttpUnprocessableEntityException { /** diff --git a/src/Exception/UnprocessableRequestBodyException.php b/src/Exception/UnprocessableRequestBodyException.php index 5e06f180..8615eede 100644 --- a/src/Exception/UnprocessableRequestBodyException.php +++ b/src/Exception/UnprocessableRequestBodyException.php @@ -16,6 +16,6 @@ * * @since 3.0.0 */ -class UnprocessableRequestBodyException extends UnprocessableEntityException +class UnprocessableRequestBodyException extends UnprocessableObjectException { } diff --git a/src/Exception/UnprocessableRequestQueryException.php b/src/Exception/UnprocessableRequestQueryException.php index c1bae14a..6ab74b68 100644 --- a/src/Exception/UnprocessableRequestQueryException.php +++ b/src/Exception/UnprocessableRequestQueryException.php @@ -16,6 +16,6 @@ * * @since 3.0.0 */ -class UnprocessableRequestQueryException extends UnprocessableEntityException +class UnprocessableRequestQueryException extends UnprocessableObjectException { } diff --git a/src/HostTable.php b/src/HostTable.php new file mode 100644 index 00000000..8a8256c4 --- /dev/null +++ b/src/HostTable.php @@ -0,0 +1,77 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router; + +/** + * The router host table + * + * @since 3.0.0 + */ +final class HostTable +{ + + /** + * @var array> + */ + private array $hosts = []; + + /** + * Adds the given alias with its hostnames to the table + * + * @param string $alias + * @param string ...$hostnames + * + * @return void + */ + public function add(string $alias, string ...$hostnames): void + { + foreach ($hostnames as $hostname) { + $this->hosts[$alias][] = $hostname; + } + } + + /** + * Loads the given hosts to the table + * + * @param array> $hosts + * + * @return void + */ + public function load(array $hosts): void + { + foreach ($hosts as $alias => $hostnames) { + foreach ($hostnames as $hostname) { + $this->hosts[$alias][] = $hostname; + } + } + } + + /** + * Resolves the given hostname to its alias if it exists in the table otherwise returns null + * + * @param string $hostname + * + * @return string|null + */ + public function resolve(string $hostname): ?string + { + foreach ($this->hosts as $alias => $hostnames) { + foreach ($hostnames as $value) { + if ($hostname === $value) { + return $alias; + } + } + } + + return null; + } +} diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 13d6bc1d..858f8e16 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -338,8 +338,8 @@ public function load(): RouteCollectionInterface ); $route->setHost($descriptor->host); - $route->setConsumedContentTypes(...$descriptor->consumes); - $route->setProducedContentTypes(...$descriptor->produces); + $route->setConsumedMediaTypes(...$descriptor->consumes); + $route->setProducedMediaTypes(...$descriptor->produces); $route->setSummary($descriptor->summary); $route->setDescription($descriptor->description); $route->setTags(...$descriptor->tags); diff --git a/src/Route.php b/src/Route.php index 47a86167..fff78338 100644 --- a/src/Route.php +++ b/src/Route.php @@ -66,18 +66,18 @@ class Route implements RouteInterface private array $methods = []; /** - * The route's consumed content types + * The route's consumed media types * * @var list */ - private array $consumedContentTypes = []; + private array $consumedMediaTypes = []; /** - * The route's produced content types + * The route's produced media types * * @var list */ - private array $producedContentTypes = []; + private array $producedMediaTypes = []; /** * The route request handler @@ -182,17 +182,17 @@ public function getMethods(): array /** * {@inheritdoc} */ - public function getConsumedContentTypes(): array + public function getConsumedMediaTypes(): array { - return $this->consumedContentTypes; + return $this->consumedMediaTypes; } /** * {@inheritdoc} */ - public function getProducedContentTypes(): array + public function getProducedMediaTypes(): array { - return $this->producedContentTypes; + return $this->producedMediaTypes; } /** @@ -301,11 +301,11 @@ public function setMethods(string ...$methods): RouteInterface /** * {@inheritdoc} */ - public function setConsumedContentTypes(string ...$contentTypes): RouteInterface + public function setConsumedMediaTypes(string ...$mediaTypes): RouteInterface { - $this->consumedContentTypes = []; - foreach ($contentTypes as $contentType) { - $this->consumedContentTypes[] = $contentType; + $this->consumedMediaTypes = []; + foreach ($mediaTypes as $mediaType) { + $this->consumedMediaTypes[] = $mediaType; } return $this; @@ -314,11 +314,11 @@ public function setConsumedContentTypes(string ...$contentTypes): RouteInterface /** * {@inheritdoc} */ - public function setProducedContentTypes(string ...$contentTypes): RouteInterface + public function setProducedMediaTypes(string ...$mediaTypes): RouteInterface { - $this->producedContentTypes = []; - foreach ($contentTypes as $contentType) { - $this->producedContentTypes[] = $contentType; + $this->producedMediaTypes = []; + foreach ($mediaTypes as $mediaType) { + $this->producedMediaTypes[] = $mediaType; } return $this; @@ -438,10 +438,10 @@ public function addMethod(string ...$methods): RouteInterface /** * {@inheritdoc} */ - public function addConsumedContentType(string ...$contentTypes): RouteInterface + public function addConsumedMediaType(string ...$mediaTypes): RouteInterface { - foreach ($contentTypes as $contentType) { - $this->consumedContentTypes[] = $contentType; + foreach ($mediaTypes as $mediaType) { + $this->consumedMediaTypes[] = $mediaType; } return $this; @@ -450,10 +450,10 @@ public function addConsumedContentType(string ...$contentTypes): RouteInterface /** * {@inheritdoc} */ - public function addProducedContentType(string ...$contentTypes): RouteInterface + public function addProducedMediaType(string ...$mediaTypes): RouteInterface { - foreach ($contentTypes as $contentType) { - $this->producedContentTypes[] = $contentType; + foreach ($mediaTypes as $mediaType) { + $this->producedMediaTypes[] = $mediaType; } return $this; diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 37bc8165..37db2551 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -15,11 +15,15 @@ * Import classes */ use Psr\Http\Server\MiddlewareInterface; +use Sunrise\Http\Router\Exception\RouteAlreadyExistsException; +use Sunrise\Http\Router\Exception\RouteNotFoundException; +use Iterator; /** * Import functions */ use function count; +use function sprintf; /** * RouteCollection @@ -30,12 +34,20 @@ class RouteCollection implements RouteCollectionInterface { /** - * The collection routes - * + * @var string + */ + private const ANY_HOST = '*'; + + /** * @var array */ private array $routes = []; + /** + * @var array> + */ + private array $hostMap = []; + /** * Constructor of the class * @@ -49,22 +61,39 @@ public function __construct(RouteInterface ...$routes) /** * {@inheritdoc} */ - public function all(): array + public function getIterator(): Iterator { - $routes = []; foreach ($this->routes as $route) { - $routes[] = $route; + yield $route; } + } - return $routes; + /** + * {@inheritdoc} + */ + public function all(): Iterator + { + foreach ($this->routes as $route) { + yield $route; + } } /** * {@inheritdoc} */ - public function get(string $name): ?RouteInterface + public function allByHost(?string $host): Iterator { - return $this->routes[$name] ?? null; + if (isset($host, $this->hostMap[$host])) { + foreach ($this->hostMap[$host] as $name) { + yield $this->routes[$name]; + } + } + + if (isset($this->hostMap[self::ANY_HOST])) { + foreach ($this->hostMap[self::ANY_HOST] as $name) { + yield $this->routes[$name]; + } + } } /** @@ -75,13 +104,39 @@ public function has(string $name): bool return isset($this->routes[$name]); } + /** + * {@inheritdoc} + */ + public function get(string $name): RouteInterface + { + if (!isset($this->routes[$name])) { + throw new RouteNotFoundException(sprintf( + 'The collection does not contain a route with the name %s', + $name + )); + } + + return $this->routes[$name]; + } + /** * {@inheritdoc} */ public function add(RouteInterface ...$routes): RouteCollectionInterface { foreach ($routes as $route) { - $this->routes[$route->getName()] = $route; + $name = $route->getName(); + $host = $route->getHost() ?? self::ANY_HOST; + + if (isset($this->routes[$name])) { + throw new RouteAlreadyExistsException(sprintf( + 'The collection already contains a route with the name %s', + $name + )); + } + + $this->routes[$name] = $route; + $this->hostMap[$host][] = $name; } return $this; @@ -102,10 +157,10 @@ public function setHost(string $host): RouteCollectionInterface /** * {@inheritdoc} */ - public function setConsumedContentTypes(string ...$contentTypes): RouteCollectionInterface + public function setConsumedMediaTypes(string ...$mediaTypes): RouteCollectionInterface { foreach ($this->routes as $route) { - $route->setConsumedContentTypes(...$contentTypes); + $route->setConsumedMediaTypes(...$mediaTypes); } return $this; @@ -114,10 +169,10 @@ public function setConsumedContentTypes(string ...$contentTypes): RouteCollectio /** * {@inheritdoc} */ - public function setProducedContentTypes(string ...$contentTypes): RouteCollectionInterface + public function setProducedMediaTypes(string ...$mediaTypes): RouteCollectionInterface { foreach ($this->routes as $route) { - $route->setProducedContentTypes(...$contentTypes); + $route->setProducedMediaTypes(...$mediaTypes); } return $this; @@ -174,10 +229,10 @@ public function addMethod(string ...$methods): RouteCollectionInterface /** * {@inheritdoc} */ - public function addConsumedContentType(string ...$contentTypes): RouteCollectionInterface + public function addConsumedMediaType(string ...$mediaTypes): RouteCollectionInterface { foreach ($this->routes as $route) { - $route->addConsumedContentType(...$contentTypes); + $route->addConsumedMediaType(...$mediaTypes); } return $this; @@ -186,10 +241,10 @@ public function addConsumedContentType(string ...$contentTypes): RouteCollection /** * {@inheritdoc} */ - public function addProducedContentType(string ...$contentTypes): RouteCollectionInterface + public function addProducedMediaType(string ...$mediaTypes): RouteCollectionInterface { foreach ($this->routes as $route) { - $route->addProducedContentType(...$contentTypes); + $route->addProducedMediaType(...$mediaTypes); } return $this; diff --git a/src/RouteCollectionInterface.php b/src/RouteCollectionInterface.php index 9809a0d7..3c224262 100644 --- a/src/RouteCollectionInterface.php +++ b/src/RouteCollectionInterface.php @@ -15,31 +15,40 @@ * Import classes */ use Psr\Http\Server\MiddlewareInterface; +use Sunrise\Http\Router\Exception\RouteAlreadyExistsException; +use Sunrise\Http\Router\Exception\RouteNotFoundException; use Countable; +use Iterator; +use IteratorAggregate; /** * RouteCollectionInterface + * + * @extends IteratorAggregate */ -interface RouteCollectionInterface extends Countable +interface RouteCollectionInterface extends Countable, IteratorAggregate { /** * Gets all routes from the collection * - * @return list + * @return Iterator */ - public function all(): array; + public function all(): Iterator; /** - * Gets a route by the given name + * Gets all routes from the collection by the given host * - * @param string $name + * This method should first return all routes that are served on the given host, + * and then return all routes that are not bound to any host. * - * @return RouteInterface|null + * @param string|null $host * - * @since 2.10.0 + * @return Iterator + * + * @since 3.0.0 */ - public function get(string $name): ?RouteInterface; + public function allByHost(?string $host): Iterator; /** * Checks by the given name if a route exists in the collection @@ -52,12 +61,29 @@ public function get(string $name): ?RouteInterface; */ public function has(string $name): bool; + /** + * Gets a route by the given name + * + * @param string $name + * + * @return RouteInterface + * + * @throws RouteNotFoundException + * If the collection doesn't contain a route with the name. + * + * @since 2.10.0 + */ + public function get(string $name): RouteInterface; + /** * Adds the given route(s) to the collection * * @param RouteInterface ...$routes * * @return RouteCollectionInterface + * + * @throws RouteAlreadyExistsException + * If the collection already contains a route with the name. */ public function add(RouteInterface ...$routes): RouteCollectionInterface; @@ -73,26 +99,26 @@ public function add(RouteInterface ...$routes): RouteCollectionInterface; public function setHost(string $host): RouteCollectionInterface; /** - * Sets the given consumed content type(s) to all routes in the collection + * Sets the given consumed media type(s) to all routes in the collection * - * @param string ...$contentTypes + * @param string ...$mediaTypes * * @return RouteCollectionInterface * * @since 3.0.0 */ - public function setConsumedContentTypes(string ...$contentTypes): RouteCollectionInterface; + public function setConsumedMediaTypes(string ...$mediaTypes): RouteCollectionInterface; /** - * Sets the given produced content type(s) to all routes in the collection + * Sets the given produced media type(s) to all routes in the collection * - * @param string ...$contentTypes + * @param string ...$mediaTypes * * @return RouteCollectionInterface * * @since 3.0.0 */ - public function setProducedContentTypes(string ...$contentTypes): RouteCollectionInterface; + public function setProducedMediaTypes(string ...$mediaTypes): RouteCollectionInterface; /** * Sets the given attribute to all routes in the collection @@ -140,26 +166,26 @@ public function addSuffix(string $suffix): RouteCollectionInterface; public function addMethod(string ...$methods): RouteCollectionInterface; /** - * Adds the given consumed content type(s) to all routes in the collection + * Adds the given consumed media type(s) to all routes in the collection * - * @param string ...$contentTypes + * @param string ...$mediaTypes * * @return RouteCollectionInterface * * @since 3.0.0 */ - public function addConsumedContentType(string ...$contentTypes): RouteCollectionInterface; + public function addConsumedMediaType(string ...$mediaTypes): RouteCollectionInterface; /** - * Adds the given produced content type(s) to all routes in the collection + * Adds the given produced media type(s) to all routes in the collection * - * @param string ...$contentTypes + * @param string ...$mediaTypes * * @return RouteCollectionInterface * * @since 3.0.0 */ - public function addProducedContentType(string ...$contentTypes): RouteCollectionInterface; + public function addProducedMediaType(string ...$mediaTypes): RouteCollectionInterface; /** * Adds the given middleware(s) to all routes in the collection diff --git a/src/RouteInterface.php b/src/RouteInterface.php index c1d71beb..2baef204 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -68,22 +68,22 @@ public function getPath(): string; public function getMethods(): array; /** - * Gets the route's consumed content types + * Gets the route's consumed media types * * @return list * * @since 3.0.0 */ - public function getConsumedContentTypes(): array; + public function getConsumedMediaTypes(): array; /** - * Gets the route's produced content types + * Gets the route's produced media types * * @return list * * @since 3.0.0 */ - public function getProducedContentTypes(): array; + public function getProducedMediaTypes(): array; /** * Gets the route request handler @@ -181,26 +181,26 @@ public function setPath(string $path): RouteInterface; public function setMethods(string ...$methods): RouteInterface; /** - * Sets the given consumed content type(s) to the route + * Sets the given consumed media type(s) to the route * - * @param string ...$contentTypes + * @param string ...$mediaTypes * * @return RouteInterface * * @since 3.0.0 */ - public function setConsumedContentTypes(string ...$contentTypes): RouteInterface; + public function setConsumedMediaTypes(string ...$mediaTypes): RouteInterface; /** - * Sets the given produced content type(s) to the route + * Sets the given produced media type(s) to the route * - * @param string ...$contentTypes + * @param string ...$mediaTypes * * @return RouteInterface * * @since 3.0.0 */ - public function setProducedContentTypes(string ...$contentTypes): RouteInterface; + public function setProducedMediaTypes(string ...$mediaTypes): RouteInterface; /** * Sets the given request handler to the route @@ -302,26 +302,26 @@ public function addSuffix(string $suffix): RouteInterface; public function addMethod(string ...$methods): RouteInterface; /** - * Adds the given consumed content type(s) to the route + * Adds the given consumed media type(s) to the route * - * @param string ...$contentTypes + * @param string ...$mediaTypes * * @return RouteInterface * * @since 3.0.0 */ - public function addConsumedContentType(string ...$contentTypes): RouteInterface; + public function addConsumedMediaType(string ...$mediaTypes): RouteInterface; /** - * Adds the given produced content type(s) to the route + * Adds the given produced media type(s) to the route * - * @param string ...$contentTypes + * @param string ...$mediaTypes * * @return RouteInterface * * @since 3.0.0 */ - public function addProducedContentType(string ...$contentTypes): RouteInterface; + public function addProducedMediaType(string ...$mediaTypes): RouteInterface; /** * Adds the given middleware(s) to the route diff --git a/src/Router.php b/src/Router.php index bd794545..4ce6810e 100644 --- a/src/Router.php +++ b/src/Router.php @@ -21,21 +21,19 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\Event\RouteEvent; -use Sunrise\Http\Router\Exception\InvalidArgumentException; -use Sunrise\Http\Router\Exception\PageNotFoundException; -use Sunrise\Http\Router\Exception\RouteNotFoundException; -use Sunrise\Http\Router\Exception\MethodNotAllowedException; +use Sunrise\Http\Router\Exception\Http\HttpMethodNotAllowedException; +use Sunrise\Http\Router\Exception\Http\HttpNotAcceptableException; +use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; +use Sunrise\Http\Router\Exception\Http\HttpUnsupportedMediaTypeException; use Sunrise\Http\Router\Loader\LoaderInterface; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; use Sunrise\Http\Router\RequestHandler\UnsafeCallableRequestHandler; -use Generator; /** * Import functions */ use function Sunrise\Http\Router\path_build; use function Sunrise\Http\Router\path_match; -use function sprintf; /** * Router @@ -58,16 +56,16 @@ class Router implements RequestHandlerInterface, RequestMethodInterface /** * The router's host table * - * @var array> + * @var HostTable */ - private array $hosts = []; + private HostTable $hosts; /** * The router's routes * - * @var array> + * @var RouteCollectionInterface */ - private array $routes = []; + private RouteCollectionInterface $routes; /** * The router's middlewares @@ -76,13 +74,6 @@ class Router implements RequestHandlerInterface, RequestMethodInterface */ private array $middlewares = []; - /** - * The router's matched route - * - * @var RouteInterface|null - */ - private ?RouteInterface $matchedRoute = null; - /** * The router's event dispatcher * @@ -91,299 +82,64 @@ class Router implements RequestHandlerInterface, RequestMethodInterface private ?EventDispatcherInterface $eventDispatcher = null; /** - * Adds the given patterns to the router - * - * @param array $patterns - * - * @return void + * The router's matched route * - * @since 2.11.0 + * @var RouteInterface|null */ - public function addPatterns(array $patterns): void - { - foreach ($patterns as $alias => $pattern) { - self::$patterns[$alias] = $pattern; - } - } + private ?RouteInterface $matchedRoute = null; /** - * Adds the given host to the router's host table - * - * @param string $alias - * @param string ...$hostnames + * Constructor of the class * - * @return void + * @param HostTable|null $hosts + * @param RouteCollectionInterface|null $routes * - * @since 2.6.0 + * @since 3.0.0 */ - public function addHost(string $alias, string ...$hostnames): void - { - foreach ($hostnames as $hostname) { - $this->hosts[$alias][] = $hostname; - } + public function __construct( + ?HostTable $hosts = null, + ?RouteCollectionInterface $routes = null + ) { + $this->hosts = $hosts ?? new HostTable(); + $this->routes = $routes ?? new RouteCollection(); } /** - * Adds the given hosts to the router's host table + * Adds the given patterns to the router * - * @param array> $hosts + * @param array $patterns * * @return void * * @since 2.11.0 */ - public function addHosts(array $hosts): void + public function addPatterns(array $patterns): void { - foreach ($hosts as $alias => $hostnames) { - foreach ($hostnames as $hostname) { - $this->hosts[$alias][] = $hostname; - } + foreach ($patterns as $alias => $pattern) { + self::$patterns[$alias] = $pattern; } } /** * Gets the router's host table * - * @return array> + * @return HostTable * * @since 2.6.0 */ - public function getHosts(): array + public function getHosts(): HostTable { return $this->hosts; } /** - * Resolves the given hostname to its alias - * - * @param string $hostname - * - * @return string|null - * - * @since 2.14.0 - */ - public function resolveHostname(string $hostname): ?string - { - foreach ($this->hosts as $alias => $hostnames) { - foreach ($hostnames as $value) { - if ($hostname === $value) { - return $alias; - } - } - } - - return null; - } - - /** - * Adds the given route(s) to the router - * - * @param RouteInterface ...$routes - * - * @return void - */ - public function addRoute(RouteInterface ...$routes): void - { - foreach ($routes as $route) { - $host = $route->getHost() ?? '*'; - $name = $route->getName(); - - $this->routes[$host][$name] = $route; - } - } - - /** - * Gets all routes - * - * @return Generator - */ - public function getRoutes(): Generator - { - foreach ($this->routes as $routes) { - foreach ($routes as $route) { - yield $route; - } - } - } - - /** - * Gets routes by the given host - * - * @param string $host - * - * @return Generator - * - * @since 3.0.0 - */ - public function getRoutesByHost(string $host): Generator - { - if (isset($this->routes[$host])) { - foreach ($this->routes[$host] as $route) { - yield $route; - } - } - } - - /** - * Gets routes by the given hostname - * - * @param string $hostname - * - * @return Generator - * - * @since 2.14.0 - */ - public function getRoutesByHostname(string $hostname): Generator - { - $host = $this->resolveHostname($hostname); - - if (isset($host) && isset($this->routes[$host])) { - foreach ($this->routes[$host] as $route) { - yield $route; - } - } - - if (isset($this->routes['*'])) { - foreach ($this->routes['*'] as $route) { - yield $route; - } - } - } - - /** - * Gets a route by the given name - * - * @param string $name - * - * @return RouteInterface - * - * @throws RouteNotFoundException - * If a route wasn't found by the given name. - */ - public function getRoute(string $name): RouteInterface - { - foreach ($this->routes as $routes) { - if (isset($routes[$name])) { - return $routes[$name]; - } - } - - throw new RouteNotFoundException(sprintf( - 'No route found for name "%s"', - $name - )); - } - - /** - * Gets a route by the given name and host - * - * @param string $name - * @param string $host - * - * @return RouteInterface - * - * @throws RouteNotFoundException - * If a route wasn't found by the given name and host. - * - * @since 3.0.0 - */ - public function getNamedRouteByHost(string $name, string $host): RouteInterface - { - if (isset($this->routes[$host][$name])) { - return $this->routes[$host][$name]; - } - - throw new RouteNotFoundException(sprintf( - 'No route found for name "%s" and host "%s"', - $name, - $host - )); - } - - /** - * Gets a route by the given name and hostname - * - * @param string $name - * @param string $hostname - * - * @return RouteInterface - * - * @throws RouteNotFoundException - * If a route wasn't found by the given name and hostname. - * - * @since 3.0.0 - */ - public function getNamedRouteByHostname(string $name, string $hostname): RouteInterface - { - $host = $this->resolveHostname($hostname); - - if (isset($host) && isset($this->routes[$host][$name])) { - return $this->routes[$host][$name]; - } - - if (isset($this->routes['*'][$name])) { - return $this->routes['*'][$name]; - } - - throw new RouteNotFoundException(sprintf( - 'No route found for name "%s" and hostname "%s"', - $name, - $hostname - )); - } - - /** - * Checks if a route exists by the given name - * - * @return bool - */ - public function hasRoute(string $name): bool - { - foreach ($this->routes as $routes) { - if (isset($routes[$name])) { - return true; - } - } - - return false; - } - - /** - * Checks if a route exists by the given name and host - * - * @return bool - * - * @since 3.0.0 - */ - public function existsNamedRouteByHost(string $name, string $host): bool - { - if (isset($this->routes[$host][$name])) { - return true; - } - - return false; - } - - /** - * Checks if a route exists by the given name and hostname + * Gets the router's route collection * - * @return bool - * - * @since 3.0.0 + * @return RouteCollectionInterface */ - public function existsNamedRouteByHostname(string $name, string $hostname): bool + public function getRoutes(): RouteCollectionInterface { - if (isset($this->routes['*'][$name])) { - return true; - } - - $host = $this->resolveHostname($hostname); - - if (isset($host) && isset($this->routes[$host][$name])) { - return true; - } - - return false; + return $this->routes; } /** @@ -454,17 +210,10 @@ public function getEventDispatcher(): ?EventDispatcherInterface * @param bool $strict * * @return string - * - * @throws RouteNotFoundException - * If a route wasn't found by the given name. - * - * @throws Exception\RoutePathBuildException - * If a required attribute value is not given, - * or if an attribute value is not valid in strict mode. */ public function generateUri(string $name, array $attributes = [], bool $strict = false): string { - $route = $this->getRoute($name); + $route = $this->routes->get($name); $attributes += $route->getAttributes(); @@ -478,11 +227,15 @@ public function generateUri(string $name, array $attributes = [], bool $strict = * * @return RouteInterface * - * @throws PageNotFoundException + * @throws HttpNotFoundException * If the request URI cannot be matched against any route. * - * @throws MethodNotAllowedException + * @throws HttpMethodNotAllowedException * If the request method isn't allowed. + * + * @throws HttpUnsupportedMediaTypeException + * + * @throws HttpNotAcceptableException */ public function match(ServerRequestInterface $request): RouteInterface { @@ -492,7 +245,9 @@ public function match(ServerRequestInterface $request): RouteInterface $requestMethod = $request->getMethod(); $allowedMethods = []; - $routes = $this->getRoutesByHostname($requestHost); + $routes = $this->routes->allByHost($this->hosts->resolve($requestHost)); + + $request = new ServerRequest($request); foreach ($routes as $route) { // https://github.com/sunrise-php/http-router/issues/50 @@ -511,8 +266,15 @@ public function match(ServerRequestInterface $request): RouteInterface continue; } - // $routeConsumedContentTypes = $route->getConsumedContentTypes(); - // $routeProducedContentTypes = $route->getProducedContentTypes(); + $consumedMediaTypes = $route->getConsumedMediaTypes(); + if (!empty($consumedMediaTypes) && !$request->clientProducesMediaType($consumedMediaTypes)) { + throw new HttpUnsupportedMediaTypeException($consumedMediaTypes); + } + + $producedMediaTypes = $route->getProducedMediaTypes(); + if (!empty($producedMediaTypes) && !$request->clientConsumesMediaType($producedMediaTypes)) { + throw new HttpNotAcceptableException($producedMediaTypes); + } /** @var array $attributes */ @@ -520,10 +282,10 @@ public function match(ServerRequestInterface $request): RouteInterface } if (!empty($allowedMethods)) { - throw new MethodNotAllowedException($requestMethod, $allowedMethods); + throw new HttpMethodNotAllowedException($allowedMethods); } - throw new PageNotFoundException(); + throw new HttpNotFoundException(); } /** @@ -595,7 +357,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface public function load(LoaderInterface ...$loaders): void { foreach ($loaders as $loader) { - $this->addRoute(...$loader->load()->all()); + $this->routes->add(...$loader->load()->all()); } } } diff --git a/src/ServerRequest.php b/src/ServerRequest.php index fbb3792f..0dc4c75a 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -16,6 +16,15 @@ */ use Psr\Http\Message\ServerRequestInterface; +/** + * Import functions + */ +use function explode; +use function strpos; +use function strstr; +use function strtolower; +use function trim; + /** * ServerRequest * @@ -42,35 +51,140 @@ public function __construct(ServerRequestInterface $request) /** * Checks if the request is JSON * - * @link https://datatracker.ietf.org/doc/html/rfc4627 + * @link https://tools.ietf.org/html/rfc4627 * * @return bool */ public function isJson(): bool { - return $this->getContentType() === 'application/json'; + return $this->getClientProducedMediaType() === 'application/json'; } /** - * Gets the request media type + * Gets the client's produced media type * * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1 + * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.5 * - * @return string|null + * @return string */ - public function getContentType(): ?string + public function getClientProducedMediaType(): string { - if (!$this->request->hasHeader('Content-Type')) { - return null; + $header = $this->request->getHeaderLine('Content-Type'); + if ($header === '') { + return ''; + } + + if (strpos($header, ';') !== false) { + $header = strstr($header, ';', true); + } + + $header = trim($header); + if ($header === '') { + return ''; } - $result = $this->request->getHeaderLine('Content-Type'); + return strtolower($header); + } + + /** + * Gets the client's consumed media types + * + * @link https://tools.ietf.org/html/rfc7231#section-1.2 + * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1 + * @link https://tools.ietf.org/html/rfc7231#section-5.3.2 + * + * @return list + */ + public function getClientConsumedMediaTypes(): array + { + $header = $this->request->getHeaderLine('Accept'); + if ($header === '') { + return []; + } + + $result = []; + $accepts = explode(',', $header); + foreach ($accepts as $accept) { + if (strpos($accept, ';') !== false) { + $accept = strstr($accept, ';', true); + } + + $accept = trim($accept); + if ($accept === '') { + continue; + } + + if ($accept === '*/*') { + return []; + } + + $result[] = strtolower($accept); + } + + return $result; + } + + /** + * Checks if the client produces one of the given media types + * + * @param list $consumedMediaTypes + * + * @return bool + */ + public function clientProducesMediaType(array $consumedMediaTypes): bool + { + if ($consumedMediaTypes === []) { + return true; + } + + $producedMediaType = $this->getClientProducedMediaType(); + if ($producedMediaType === '') { + return false; + } + + foreach ($consumedMediaTypes as $consumedMediaType) { + if ($consumedMediaType === $producedMediaType) { + return true; + } + } + + return false; + } + + /** + * Checks if the client consumes one of the given media types + * + * @param list $producedMediaTypes + * + * @return bool + */ + public function clientConsumesMediaType(array $producedMediaTypes): bool + { + if ($producedMediaTypes === []) { + return true; + } + + $consumedMediaTypes = $this->getClientConsumedMediaTypes(); + if ($consumedMediaTypes === []) { + return true; + } + + foreach ($producedMediaTypes as $producedMediaType) { + if (strpos($producedMediaType, '/') !== false) { + $producedMediaTypes[] = strstr($producedMediaType, '/', true) . '/*'; + } + } - if (false !== \strpos($result, ';')) { - $result = \strstr($result, ';', true); + foreach ($consumedMediaTypes as $consumedMediaType) { + foreach ($producedMediaTypes as $producedMediaType) { + if ($consumedMediaType === $producedMediaType) { + return true; + } + } } - return \trim($result); + return false; } /** From bf51ff8a4c122533952a534236e39b190c2a28c8 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sat, 11 Feb 2023 04:52:40 +0100 Subject: [PATCH 061/180] v3 --- src/Command/RouteListCommand.php | 4 +- .../Http/HttpMethodNotAllowedException.php | 18 ++-- .../Http/HttpNotAcceptableException.php | 18 ++-- .../HttpUnsupportedMediaTypeException.php | 18 ++-- src/Exception/UnhydrableObjectException.php | 21 +++++ ...n.php => UnprocessableEntityException.php} | 4 +- .../UnprocessableRequestBodyException.php | 2 +- .../UnprocessableRequestQueryException.php | 2 +- src/Loader/ConfigLoader.php | 10 ++- src/Loader/DescriptorLoader.php | 22 ++--- src/Middleware/CallableMiddleware.php | 11 +-- .../JsonPayloadDecodingMiddleware.php | 22 +---- .../XmlPayloadDecodingMiddleware.php | 85 +++++++++++++++++++ .../DependencyInjectionParameterResolver.php | 4 +- .../KnownTypedParameterResolver.php | 10 ++- .../RequestBodyParameterResolver.php | 12 +-- .../RequestEntityParameterResolver.php | 2 +- .../RequestQueryParameterResolver.php | 12 +-- .../ServerRequestParameterResolver.php | 6 +- src/RequestHandler/CallableRequestHandler.php | 8 +- src/RouteCollector.php | 10 ++- src/ServerRequest.php | 81 +++++++++++++++--- 22 files changed, 255 insertions(+), 127 deletions(-) create mode 100644 src/Exception/UnhydrableObjectException.php rename src/Exception/{UnprocessableObjectException.php => UnprocessableEntityException.php} (92%) create mode 100644 src/Middleware/XmlPayloadDecodingMiddleware.php diff --git a/src/Command/RouteListCommand.php b/src/Command/RouteListCommand.php index 65052713..817aee44 100644 --- a/src/Command/RouteListCommand.php +++ b/src/Command/RouteListCommand.php @@ -74,7 +74,7 @@ protected function getRouter(): Router if (!isset($this->router)) { throw new LogicException(sprintf( 'The %2$s() method MUST return the %1$s class instance. ' . - 'Pass the %1$s class instance to the constructor, ' . + 'Pass the %1$s class instance to the constructor ' . 'or override the %2$s() method.', Router::class, __METHOD__ @@ -107,7 +107,7 @@ final protected function execute(InputInterface $input, OutputInterface $output) 'Verb', ]); - foreach ($this->getRouter()->getRoutes() as $route) { + foreach ($this->getRouter()->getRoutes()->all() as $route) { $table->addRow([ $route->getName(), $route->getHost() ?? 'ANY', diff --git a/src/Exception/Http/HttpMethodNotAllowedException.php b/src/Exception/Http/HttpMethodNotAllowedException.php index 8dc9c424..b2eae99e 100644 --- a/src/Exception/Http/HttpMethodNotAllowedException.php +++ b/src/Exception/Http/HttpMethodNotAllowedException.php @@ -44,23 +44,19 @@ class HttpMethodNotAllowedException extends HttpException /** * Constructor of the class * - * @param string[] $allowedMethods - * @param ?string $message + * @param array $allowed + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ - public function __construct( - array $allowedMethods, - ?string $message = null, - int $code = 0, - ?Throwable $previous = null - ) { + public function __construct(array $allowed, ?string $message = null, int $code = 0, ?Throwable $previous = null) + { $message ??= 'Method Not Allowed'; parent::__construct(self::STATUS_METHOD_NOT_ALLOWED, $message, $code, $previous); - foreach ($allowedMethods as $allowedMethod) { - $this->allowedMethods[] = $allowedMethod; + foreach ($allowed as $method) { + $this->allowedMethods[] = $method; } } diff --git a/src/Exception/Http/HttpNotAcceptableException.php b/src/Exception/Http/HttpNotAcceptableException.php index 66490101..e0bd105d 100644 --- a/src/Exception/Http/HttpNotAcceptableException.php +++ b/src/Exception/Http/HttpNotAcceptableException.php @@ -39,23 +39,19 @@ class HttpNotAcceptableException extends HttpException /** * Constructor of the class * - * @param string[] $supportedMediaTypes - * @param ?string $message + * @param array $supported + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ - public function __construct( - array $supportedMediaTypes, - ?string $message = null, - int $code = 0, - ?Throwable $previous = null - ) { + public function __construct(array $supported, ?string $message = null, int $code = 0, ?Throwable $previous = null) + { $message ??= 'Not Acceptable'; parent::__construct(self::STATUS_NOT_ACCEPTABLE, $message, $code, $previous); - foreach ($supportedMediaTypes as $supportedMediaType) { - $this->supportedMediaTypes[] = $supportedMediaType; + foreach ($supported as $mediaType) { + $this->supportedMediaTypes[] = $mediaType; } } diff --git a/src/Exception/Http/HttpUnsupportedMediaTypeException.php b/src/Exception/Http/HttpUnsupportedMediaTypeException.php index de81ac2d..9db05489 100644 --- a/src/Exception/Http/HttpUnsupportedMediaTypeException.php +++ b/src/Exception/Http/HttpUnsupportedMediaTypeException.php @@ -43,23 +43,19 @@ class HttpUnsupportedMediaTypeException extends HttpException /** * Constructor of the class * - * @param string[] $supportedMediaTypes - * @param ?string $message + * @param array $supported + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ - public function __construct( - array $supportedMediaTypes, - ?string $message = null, - int $code = 0, - ?Throwable $previous = null - ) { + public function __construct(array $supported, ?string $message = null, int $code = 0, ?Throwable $previous = null) + { $message ??= 'Unsupported Media Type'; parent::__construct(self::STATUS_UNSUPPORTED_MEDIA_TYPE, $message, $code, $previous); - foreach ($supportedMediaTypes as $supportedMediaType) { - $this->supportedMediaTypes[] = $supportedMediaType; + foreach ($supported as $mediaType) { + $this->supportedMediaTypes[] = $mediaType; } } diff --git a/src/Exception/UnhydrableObjectException.php b/src/Exception/UnhydrableObjectException.php new file mode 100644 index 00000000..23ba90a2 --- /dev/null +++ b/src/Exception/UnhydrableObjectException.php @@ -0,0 +1,21 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * UnhydrableObjectException + * + * @since 3.0.0 + */ +class UnhydrableObjectException extends LogicException +{ +} diff --git a/src/Exception/UnprocessableObjectException.php b/src/Exception/UnprocessableEntityException.php similarity index 92% rename from src/Exception/UnprocessableObjectException.php rename to src/Exception/UnprocessableEntityException.php index 78bf3f50..afe34b01 100644 --- a/src/Exception/UnprocessableObjectException.php +++ b/src/Exception/UnprocessableEntityException.php @@ -18,11 +18,11 @@ use Symfony\Component\Validator\ConstraintViolationListInterface; /** - * UnprocessableObjectException + * UnprocessableEntityException * * @since 3.0.0 */ -class UnprocessableObjectException extends HttpUnprocessableEntityException +class UnprocessableEntityException extends HttpUnprocessableEntityException { /** diff --git a/src/Exception/UnprocessableRequestBodyException.php b/src/Exception/UnprocessableRequestBodyException.php index 8615eede..5e06f180 100644 --- a/src/Exception/UnprocessableRequestBodyException.php +++ b/src/Exception/UnprocessableRequestBodyException.php @@ -16,6 +16,6 @@ * * @since 3.0.0 */ -class UnprocessableRequestBodyException extends UnprocessableObjectException +class UnprocessableRequestBodyException extends UnprocessableEntityException { } diff --git a/src/Exception/UnprocessableRequestQueryException.php b/src/Exception/UnprocessableRequestQueryException.php index 6ab74b68..c1bae14a 100644 --- a/src/Exception/UnprocessableRequestQueryException.php +++ b/src/Exception/UnprocessableRequestQueryException.php @@ -16,6 +16,6 @@ * * @since 3.0.0 */ -class UnprocessableRequestQueryException extends UnprocessableObjectException +class UnprocessableRequestQueryException extends UnprocessableEntityException { } diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 9643b2fe..1d4d6697 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -120,8 +120,9 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): { if (!isset($this->parameterResolutioner)) { throw new LogicException( - 'The config route loader cannot accept the parameter resolver(s) ' . - 'because a custom reference resolver was setted and a parameter resolutioner was not passed' + 'The config route loader cannot accept parameter resolvers ' . + 'because a custom reference resolver was setted ' . + 'and a parameter resolutioner was not passed' ); } @@ -144,8 +145,9 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo { if (!isset($this->responseResolutioner)) { throw new LogicException( - 'The config route loader cannot accept the response resolver(s) ' . - 'because a custom reference resolver was setted and a response resolutioner was not passed' + 'The config route loader cannot accept response resolvers ' . + 'because a custom reference resolver was setted ' . + 'and a response resolutioner was not passed' ); } diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 858f8e16..885dd4ee 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -160,8 +160,9 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): { if (!isset($this->parameterResolutioner)) { throw new LogicException( - 'The descriptor route loader cannot accept the parameter resolver(s) ' . - 'because a custom reference resolver was setted and a parameter resolutioner was not passed' + 'The descriptor route loader cannot accept parameter resolvers ' . + 'because a custom reference resolver was setted ' . + 'and a parameter resolutioner was not passed' ); } @@ -184,8 +185,9 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo { if (!isset($this->responseResolutioner)) { throw new LogicException( - 'The descriptor route loader cannot accept the response resolver(s) ' . - 'because a custom reference resolver was setted and a response resolutioner was not passed' + 'The descriptor route loader cannot accept response resolvers ' . + 'because a custom reference resolver was setted ' . + 'and a response resolutioner was not passed' ); } @@ -206,8 +208,9 @@ public function useDefaultAnnotationReader(): void { if (!class_exists(AnnotationReader::class)) { throw new LogicException( - 'The annotations reading logic requires an uninstalled "doctrine/annotations" package, ' . - 'run the following command "composer install doctrine/annotations" and try again' + 'The descriptor route loader cannot use the default annotation reader ' . + 'because the annotation reading logic requires the "doctrine/annotations" package, ' . + 'run the "composer install doctrine/annotations" command and try again' ); } @@ -324,8 +327,7 @@ public function attachArray(array $resources): void */ public function load(): RouteCollectionInterface { - $routes = $this->collectionFactory->createCollection(); - + $routes = []; $descriptors = $this->getDescriptors(); foreach ($descriptors as $descriptor) { $route = $this->routeFactory->createRoute( @@ -344,10 +346,10 @@ public function load(): RouteCollectionInterface $route->setDescription($descriptor->description); $route->setTags(...$descriptor->tags); - $routes->add($route); + $routes[] = $route; } - return $routes; + return $this->collectionFactory->createCollection(...$routes); } /** diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index 0e2e5502..18da1c0a 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -19,7 +19,6 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\ParameterResolver\KnownTypedParameterResolver; -use Sunrise\Http\Router\ParameterResolver\KnownUntypedParameterResolver; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ResponseResolutionerInterface; use ReflectionFunctionAbstract; @@ -94,16 +93,10 @@ public function getReflection(): ReflectionFunctionAbstract */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $parameterResolvers = [ - new KnownTypedParameterResolver(ServerRequestInterface::class, $request), - new KnownUntypedParameterResolver('request', $request), - new KnownTypedParameterResolver(RequestHandlerInterface::class, $handler), - new KnownUntypedParameterResolver('handler', $handler), - ]; - $arguments = $this->parameterResolutioner ->withContext($request) - ->withPriorityResolver(...$parameterResolvers) + ->withPriorityResolver(new KnownTypedParameterResolver(ServerRequestInterface::class, $request)) + ->withPriorityResolver(new KnownTypedParameterResolver(RequestHandlerInterface::class, $handler)) ->resolveParameters(...$this->getReflection()->getParameters()); /** @var mixed */ diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 4e12e4a2..9078474f 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -49,7 +49,7 @@ final class JsonPayloadDecodingMiddleware implements MiddlewareInterface */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if ($this->supportsRequest($request)) { + if (ServerRequest::from($request)->isJson()) { $request = $request->withParsedBody( $this->decodeRequestPayload($request) ); @@ -58,22 +58,6 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $handler->handle($request); } - /** - * Checks if the given request is supported - * - * @param ServerRequestInterface $request - * - * @return bool - */ - private function supportsRequest(ServerRequestInterface $request): bool - { - if (!($request instanceof ServerRequest)) { - $request = new ServerRequest($request); - } - - return $request->isJson(); - } - /** * Tries to decode the given request's payload * @@ -87,11 +71,11 @@ private function supportsRequest(ServerRequestInterface $request): bool private function decodeRequestPayload(ServerRequestInterface $request): ?array { // https://www.php.net/json.constants - $flags = JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY | JSON_THROW_ON_ERROR; + $options = JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY | JSON_THROW_ON_ERROR; try { /** @var mixed */ - $result = json_decode($request->getBody()->__toString(), null, 512, $flags); + $result = json_decode($request->getBody()->__toString(), null, 512, $options); } catch (JsonException $e) { throw new InvalidRequestPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); } diff --git a/src/Middleware/XmlPayloadDecodingMiddleware.php b/src/Middleware/XmlPayloadDecodingMiddleware.php new file mode 100644 index 00000000..4c716dd7 --- /dev/null +++ b/src/Middleware/XmlPayloadDecodingMiddleware.php @@ -0,0 +1,85 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Middleware; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; +use Sunrise\Http\Router\ServerRequest; +use SimpleXMLElement; +use Throwable; + +/** + * Import functions + */ +use function sprintf; + +/** + * Import constants + */ +use const LIBXML_COMPACT; +use const LIBXML_NONET; +use const LIBXML_NOERROR; +use const LIBXML_NOWARNING; +use const LIBXML_PARSEHUGE; + +/** + * XmlPayloadDecodingMiddleware + * + * @since 3.0.0 + */ +final class XmlPayloadDecodingMiddleware implements MiddlewareInterface +{ + + /** + * {@inheritdoc} + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if (ServerRequest::from($request)->isXml()) { + $request = $request->withParsedBody( + $this->decodeRequestPayload($request) + ); + } + + return $handler->handle($request); + } + + /** + * Tries to decode the given request's payload + * + * @param ServerRequestInterface $request + * + * @return SimpleXMLElement + * + * @throws InvalidRequestPayloadException + * If the request's payload cannot be decoded. + */ + private function decodeRequestPayload(ServerRequestInterface $request): SimpleXMLElement + { + // https://www.php.net/manual/en/libxml.constants.php + $options = LIBXML_COMPACT | LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_PARSEHUGE; + + try { + $result = new SimpleXMLElement($request->getBody()->__toString(), $options); + } catch (Throwable $e) { + throw new InvalidRequestPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); + } + + return $result; + } +} diff --git a/src/ParameterResolver/DependencyInjectionParameterResolver.php b/src/ParameterResolver/DependencyInjectionParameterResolver.php index 38eb7df1..8530be49 100644 --- a/src/ParameterResolver/DependencyInjectionParameterResolver.php +++ b/src/ParameterResolver/DependencyInjectionParameterResolver.php @@ -66,8 +66,8 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo public function resolveParameter(ReflectionParameter $parameter, $context) { /** @var ReflectionNamedType */ - $parameterType = $parameter->getType(); + $type = $parameter->getType(); - return $this->container->get($parameterType->getName()); + return $this->container->get($type->getName()); } } diff --git a/src/ParameterResolver/KnownTypedParameterResolver.php b/src/ParameterResolver/KnownTypedParameterResolver.php index 59a42f3d..9121c02c 100644 --- a/src/ParameterResolver/KnownTypedParameterResolver.php +++ b/src/ParameterResolver/KnownTypedParameterResolver.php @@ -21,24 +21,26 @@ /** * KnownTypedParameterResolver * + * @template T as object + * * @since 3.0.0 */ final class KnownTypedParameterResolver implements ParameterResolverInterface { /** - * @var class-string + * @var class-string */ private string $type; /** - * @var object + * @var T */ private object $value; /** - * @param class-string $type - * @param object $value + * @param class-string $type + * @param T $value */ public function __construct(string $type, object $value) { diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index 24ca56ee..dd231633 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -17,7 +17,7 @@ use Psr\Http\Message\ServerRequestInterface; use Sunrise\Http\Router\Annotation\RequestBody; use Sunrise\Http\Router\Exception\InvalidRequestBodyException; -use Sunrise\Http\Router\Exception\ResolvingParameterException; +use Sunrise\Http\Router\Exception\UnhydrableObjectException; use Sunrise\Http\Router\Exception\UnprocessableRequestBodyException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\RequestBodyInterface; @@ -102,8 +102,8 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} * - * @throws ResolvingParameterException - * If the object cannot be hydrated. + * @throws UnhydrableObjectException + * If an object isn't valid. * * @throws InvalidRequestBodyException * If the request body structure isn't valid. @@ -117,12 +117,12 @@ public function resolveParameter(ReflectionParameter $parameter, $context) $context = $context; /** @var ReflectionNamedType */ - $parameterType = $parameter->getType(); + $type = $parameter->getType(); try { - $object = $this->hydrator->hydrate($parameterType->getName(), (array) $context->getParsedBody()); + $object = $this->hydrator->hydrate($type->getName(), (array) $context->getParsedBody()); } catch (InvalidObjectException $e) { - throw new ResolvingParameterException($e->getMessage(), 0, $e); + throw new UnhydrableObjectException($e->getMessage(), 0, $e); } catch (InvalidValueException $e) { throw new InvalidRequestBodyException($e->getMessage(), 0, $e); } diff --git a/src/ParameterResolver/RequestEntityParameterResolver.php b/src/ParameterResolver/RequestEntityParameterResolver.php index ab839bc3..4ad6b936 100644 --- a/src/ParameterResolver/RequestEntityParameterResolver.php +++ b/src/ParameterResolver/RequestEntityParameterResolver.php @@ -97,7 +97,7 @@ public function resolveParameter(ReflectionParameter $parameter, $context) $entityId = $context->getAttribute($requestEntity->paramKey); if (!isset($entityId)) { throw new ResolvingParameterException(sprintf( - '{%s} Unable to get Entity ID (%s) by key %s', + '{%s} Unable to get Entity ID (%s) by key %s from the request attributes', $this->stringifyParameter($parameter), $requestEntity->findBy, $requestEntity->paramKey diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php index da5997a8..c59b5863 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -17,7 +17,7 @@ use Psr\Http\Message\ServerRequestInterface; use Sunrise\Http\Router\Annotation\RequestQuery; use Sunrise\Http\Router\Exception\InvalidRequestQueryException; -use Sunrise\Http\Router\Exception\ResolvingParameterException; +use Sunrise\Http\Router\Exception\UnhydrableObjectException; use Sunrise\Http\Router\Exception\UnprocessableRequestQueryException; use Sunrise\Http\Router\ParameterResolverInterface; use Sunrise\Http\Router\RequestQueryInterface; @@ -102,8 +102,8 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} * - * @throws ResolvingParameterException - * If the object cannot be hydrated. + * @throws UnhydrableObjectException + * If an object isn't valid. * * @throws InvalidRequestQueryException * If the request query structure isn't valid. @@ -117,12 +117,12 @@ public function resolveParameter(ReflectionParameter $parameter, $context) $context = $context; /** @var ReflectionNamedType */ - $parameterType = $parameter->getType(); + $type = $parameter->getType(); try { - $object = $this->hydrator->hydrate($parameterType->getName(), $context->getQueryParams()); + $object = $this->hydrator->hydrate($type->getName(), $context->getQueryParams()); } catch (InvalidObjectException $e) { - throw new ResolvingParameterException($e->getMessage(), 0, $e); + throw new UnhydrableObjectException($e->getMessage(), 0, $e); } catch (InvalidValueException $e) { throw new InvalidRequestQueryException($e->getMessage(), 0, $e); } diff --git a/src/ParameterResolver/ServerRequestParameterResolver.php b/src/ParameterResolver/ServerRequestParameterResolver.php index f02d9b17..8510933b 100644 --- a/src/ParameterResolver/ServerRequestParameterResolver.php +++ b/src/ParameterResolver/ServerRequestParameterResolver.php @@ -56,11 +56,7 @@ public function resolveParameter(ReflectionParameter $parameter, $context) /** @var ServerRequestInterface */ $context = $context; - if ($context instanceof ServerRequest) { - return $context; - } - - return new ServerRequest($context); + return ServerRequest::from($context); } } diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index f655b6b4..9fac178a 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -18,7 +18,6 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\ParameterResolver\KnownTypedParameterResolver; -use Sunrise\Http\Router\ParameterResolver\KnownUntypedParameterResolver; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ResponseResolutionerInterface; use ReflectionFunctionAbstract; @@ -91,14 +90,9 @@ public function getReflection(): ReflectionFunctionAbstract */ public function handle(ServerRequestInterface $request): ResponseInterface { - $parameterResolvers = [ - new KnownTypedParameterResolver(ServerRequestInterface::class, $request), - new KnownUntypedParameterResolver('request', $request), - ]; - $arguments = $this->parameterResolutioner ->withContext($request) - ->withPriorityResolver(...$parameterResolvers) + ->withPriorityResolver(new KnownTypedParameterResolver(ServerRequestInterface::class, $request)) ->resolveParameters(...$this->getReflection()->getParameters()); /** @var mixed */ diff --git a/src/RouteCollector.php b/src/RouteCollector.php index e4fd9912..380614f1 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -115,8 +115,9 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): { if (!isset($this->parameterResolutioner)) { throw new LogicException( - 'The route collector cannot accept the parameter resolver(s) ' . - 'because a custom reference resolver was setted and a parameter resolutioner was not passed' + 'The route collector cannot accept parameter resolvers ' . + 'because a custom reference resolver was setted ' . + 'and a parameter resolutioner was not passed' ); } @@ -139,8 +140,9 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo { if (!isset($this->responseResolutioner)) { throw new LogicException( - 'The route collector cannot accept the response resolver(s) ' . - 'because a custom reference resolver was setted and a response resolutioner was not passed' + 'The route collector cannot accept response resolvers ' . + 'because a custom reference resolver was setted ' . + 'and a response resolutioner was not passed' ); } diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 0dc4c75a..29ac7592 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -20,6 +20,7 @@ * Import functions */ use function explode; +use function strncmp; use function strpos; use function strstr; use function strtolower; @@ -48,6 +49,22 @@ public function __construct(ServerRequestInterface $request) $this->request = $request; } + /** + * Creates the class from the given request + * + * @param ServerRequestInterface $request + * + * @return self + */ + public static function from(ServerRequestInterface $request): self + { + if ($request instanceof self) { + return $request; + } + + return new self($request); + } + /** * Checks if the request is JSON * @@ -57,7 +74,24 @@ public function __construct(ServerRequestInterface $request) */ public function isJson(): bool { - return $this->getClientProducedMediaType() === 'application/json'; + return $this->clientProducesMediaType([ + 'application/json', + ]); + } + + /** + * Checks if the request is XML + * + * @link https://tools.ietf.org/html/rfc2376 + * + * @return bool + */ + public function isXml(): bool + { + return $this->clientProducesMediaType([ + 'application/xml', + 'text/xml', + ]); } /** @@ -144,7 +178,7 @@ public function clientProducesMediaType(array $consumedMediaTypes): bool } foreach ($consumedMediaTypes as $consumedMediaType) { - if ($consumedMediaType === $producedMediaType) { + if ($this->compareMediaTypes($consumedMediaType, $producedMediaType)) { return true; } } @@ -170,15 +204,9 @@ public function clientConsumesMediaType(array $producedMediaTypes): bool return true; } - foreach ($producedMediaTypes as $producedMediaType) { - if (strpos($producedMediaType, '/') !== false) { - $producedMediaTypes[] = strstr($producedMediaType, '/', true) . '/*'; - } - } - - foreach ($consumedMediaTypes as $consumedMediaType) { - foreach ($producedMediaTypes as $producedMediaType) { - if ($consumedMediaType === $producedMediaType) { + foreach ($producedMediaTypes as $a) { + foreach ($consumedMediaTypes as $b) { + if ($this->compareMediaTypes($a, $b)) { return true; } } @@ -187,6 +215,37 @@ public function clientConsumesMediaType(array $producedMediaTypes): bool return false; } + /** + * Compares the given media types + * + * @param string $a + * @param string $b + * + * @return bool + */ + public function compareMediaTypes(string $a, string $b): bool + { + if ($a === $b) { + return true; + } + + $slash = strpos($a, '/'); + if ($slash === false) { + return false; + } + + $star = $slash + 1; + if (!isset($a[$star], $b[$star])) { + return false; + } + + if (!($a[$star] === '*' || $b[$star] === '*')) { + return false; + } + + return strncmp($a, $b, $star) === 0; + } + /** * {@inheritdoc} */ From 945de828a2b08e090a2da35e03406ce9fbf1bbe8 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sat, 11 Feb 2023 04:53:48 +0100 Subject: [PATCH 062/180] v3 --- src/ServerRequest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 29ac7592..8d4113e4 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -178,7 +178,7 @@ public function clientProducesMediaType(array $consumedMediaTypes): bool } foreach ($consumedMediaTypes as $consumedMediaType) { - if ($this->compareMediaTypes($consumedMediaType, $producedMediaType)) { + if ($this->equalsMediaTypes($consumedMediaType, $producedMediaType)) { return true; } } @@ -206,7 +206,7 @@ public function clientConsumesMediaType(array $producedMediaTypes): bool foreach ($producedMediaTypes as $a) { foreach ($consumedMediaTypes as $b) { - if ($this->compareMediaTypes($a, $b)) { + if ($this->equalsMediaTypes($a, $b)) { return true; } } @@ -216,14 +216,14 @@ public function clientConsumesMediaType(array $producedMediaTypes): bool } /** - * Compares the given media types + * Checks if the given media types are equal * * @param string $a * @param string $b * * @return bool */ - public function compareMediaTypes(string $a, string $b): bool + public function equalsMediaTypes(string $a, string $b): bool { if ($a === $b) { return true; From 7a27390cef78516ea2a6640775356f31135a5acd Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sat, 11 Feb 2023 18:09:18 +0100 Subject: [PATCH 063/180] v3 --- src/Entity/IpAddress.php | 141 ++++++++++++++++++ src/Middleware/ClientIpAddressMiddleware.php | 55 +++++++ .../ClientIpAddressParameterResolver.php | 77 ++++++++++ src/ServerRequest.php | 36 +++++ 4 files changed, 309 insertions(+) create mode 100644 src/Entity/IpAddress.php create mode 100644 src/Middleware/ClientIpAddressMiddleware.php create mode 100644 src/ParameterResolver/ClientIpAddressParameterResolver.php diff --git a/src/Entity/IpAddress.php b/src/Entity/IpAddress.php new file mode 100644 index 00000000..59c6e2a5 --- /dev/null +++ b/src/Entity/IpAddress.php @@ -0,0 +1,141 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Entity; + +/** + * Import functions + */ +use function filter_var; + +/** + * Import constants + */ +use const FILTER_FLAG_GLOBAL_RANGE; +use const FILTER_FLAG_IPV4; +use const FILTER_FLAG_IPV6; +use const FILTER_FLAG_NO_PRIV_RANGE; +use const FILTER_FLAG_NO_RES_RANGE; +use const FILTER_VALIDATE_IP; + +/** + * IP address entity + * + * @since 3.0.0 + */ +final class IpAddress +{ + + /** + * The IP address value + * + * @var string + */ + private string $value; + + /** + * Constructor of the class + * + * @param string $value + */ + public function __construct(string $value) + { + $this->value = $value; + } + + /** + * Gets the IP address value + * + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + /** + * Checks if the IP address is valid + * + * @return bool + */ + public function isValid(): bool + { + return false !== filter_var($this->value, FILTER_VALIDATE_IP); + } + + /** + * Checks if the IP address is IPv4 + * + * @return bool + */ + public function isVersion4(): bool + { + return false !== filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); + } + + /** + * Checks if the IP address is IPv6 + * + * @return bool + */ + public function isVersion6(): bool + { + return false !== filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); + } + + /** + * Checks if the IP address is in the global range + * + * @return bool + */ + public function isInGlobalRange(): bool + { + return false !== filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE); + } + + /** + * Checks if the IP address is in the private range + * + * @return bool + */ + public function isInPrivateRange(): bool + { + if (!$this->isValid()) { + return false; + } + + return false === filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE); + } + + /** + * Checks if the IP address is in the reserved range + * + * @return bool + */ + public function isInReservedRange(): bool + { + if (!$this->isValid()) { + return false; + } + + return false === filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); + } + + /** + * Converts the object to a string + * + * @return string + */ + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Middleware/ClientIpAddressMiddleware.php b/src/Middleware/ClientIpAddressMiddleware.php new file mode 100644 index 00000000..b408548e --- /dev/null +++ b/src/Middleware/ClientIpAddressMiddleware.php @@ -0,0 +1,55 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Middleware; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\ServerRequest; + +/** + * ClientIpAddressMiddleware + * + * @since 3.0.0 + */ +final class ClientIpAddressMiddleware implements MiddlewareInterface +{ + + /** + * @var array + */ + private array $proxyChain; + + /** + * Constructor of the class + * + * @param array $proxyChain + */ + public function __construct(array $proxyChain = []) + { + $this->proxyChain = $proxyChain; + } + + /** + * {@inheritdoc} + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $clientIp = ServerRequest::from($request)->getClientIpAddress($this->proxyChain); + + return $handler->handle($request->withAttribute('@clientIp', $clientIp)); + } +} diff --git a/src/ParameterResolver/ClientIpAddressParameterResolver.php b/src/ParameterResolver/ClientIpAddressParameterResolver.php new file mode 100644 index 00000000..321e46fe --- /dev/null +++ b/src/ParameterResolver/ClientIpAddressParameterResolver.php @@ -0,0 +1,77 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\ParameterResolver; + +/** + * Import classes + */ +use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\Entity\IpAddress; +use Sunrise\Http\Router\ParameterResolverInterface; +use Sunrise\Http\Router\ServerRequest; +use ReflectionNamedType; +use ReflectionParameter; + +/** + * ClientIpAddressParameterResolver + * + * @since 3.0.0 + */ +final class ClientIpAddressParameterResolver implements ParameterResolverInterface +{ + + /** + * @var array + */ + private array $proxyChain; + + /** + * Constructor of the class + * + * @param array $proxyChain + */ + public function __construct(array $proxyChain = []) + { + $this->proxyChain = $proxyChain; + } + + /** + * {@inheritdoc} + */ + public function supportsParameter(ReflectionParameter $parameter, $context): bool + { + if (!($context instanceof ServerRequestInterface)) { + return false; + } + + if (!($parameter->getType() instanceof ReflectionNamedType)) { + return false; + } + + if (!($parameter->getType()->getName() === IpAddress::class)) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function resolveParameter(ReflectionParameter $parameter, $context) + { + /** @var ServerRequestInterface */ + $context = $context; + + return ServerRequest::from($context)->getClientIpAddress($this->proxyChain); + } +} diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 8d4113e4..af9dd4fc 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -15,6 +15,7 @@ * Import classes */ use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\Entity\IpAddress; /** * Import functions @@ -94,6 +95,41 @@ public function isXml(): bool ]); } + /** + * Gets the client's IP address + * + * @param array $proxyChain + * + * @return IpAddress + */ + public function getClientIpAddress(array $proxyChain = []): IpAddress + { + $env = $this->request->getServerParams(); + + /** @var string */ + $clientIp = $env['REMOTE_ADDR'] ?? '::1'; + + while (isset($proxyChain[$clientIp])) { + $trustedHeader = $proxyChain[$clientIp]; + unset($proxyChain[$clientIp]); + + // the chain can't be untangled... + if (!$this->request->hasHeader($trustedHeader)) { + break; + } + + // X-Forwarded-For: , , + $clientIp = $this->request->getHeaderLine($trustedHeader); + if (strpos($clientIp, ',') !== false) { + $clientIp = strstr($clientIp, ',', true); + } + + $clientIp = trim($clientIp); + } + + return new IpAddress($clientIp); + } + /** * Gets the client's produced media type * From e1046c921c3ce8dfa8bae2bd4d23747a9b5715df Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sat, 11 Feb 2023 18:17:08 +0100 Subject: [PATCH 064/180] v3 --- src/Entity/IpAddress.php | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/Entity/IpAddress.php b/src/Entity/IpAddress.php index 59c6e2a5..ce603b0b 100644 --- a/src/Entity/IpAddress.php +++ b/src/Entity/IpAddress.php @@ -19,7 +19,6 @@ /** * Import constants */ -use const FILTER_FLAG_GLOBAL_RANGE; use const FILTER_FLAG_IPV4; use const FILTER_FLAG_IPV6; use const FILTER_FLAG_NO_PRIV_RANGE; @@ -91,16 +90,6 @@ public function isVersion6(): bool return false !== filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); } - /** - * Checks if the IP address is in the global range - * - * @return bool - */ - public function isInGlobalRange(): bool - { - return false !== filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_GLOBAL_RANGE); - } - /** * Checks if the IP address is in the private range * From cd3ab2b51c8b56e8534ec036c30db185b714b6aa Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sat, 11 Feb 2023 23:40:52 +0100 Subject: [PATCH 065/180] v3 --- src/Annotation/RequestEntity.php | 4 +- src/Dto/ErrorDto.php | 21 ++++ src/Dto/ErrorsDto.php | 21 ++++ src/Entity/IpAddress.php | 20 ++++ .../MissingRequestParameterException.php | 26 ++++ .../CommittingEntityChangesMiddleware.php | 70 +++++++++++ .../JsonErrorHandlingMiddleware.php | 70 +++++++++++ .../DependencyInjectionParameterResolver.php | 4 +- .../RequestBodyParameterResolver.php | 10 +- .../RequestEntityParameterResolver.php | 112 +++++------------- .../RequestQueryParameterResolver.php | 10 +- .../ServerRequestParameterResolver.php | 1 - src/Router.php | 2 +- 13 files changed, 270 insertions(+), 101 deletions(-) create mode 100644 src/Dto/ErrorDto.php create mode 100644 src/Dto/ErrorsDto.php create mode 100644 src/Exception/MissingRequestParameterException.php create mode 100644 src/Middleware/CommittingEntityChangesMiddleware.php create mode 100644 src/Middleware/JsonErrorHandlingMiddleware.php diff --git a/src/Annotation/RequestEntity.php b/src/Annotation/RequestEntity.php index 56fac3de..562039b4 100644 --- a/src/Annotation/RequestEntity.php +++ b/src/Annotation/RequestEntity.php @@ -28,13 +28,13 @@ final class RequestEntity * * @param string|null $em * @param string $findBy - * @param string $paramKey + * @param string|null $paramKey * @param array $criteria */ public function __construct( public ?string $em = null, public string $findBy = 'id', - public string $paramKey = 'id', + public ?string $paramKey = null, public array $criteria = [] ) { } diff --git a/src/Dto/ErrorDto.php b/src/Dto/ErrorDto.php new file mode 100644 index 00000000..0e0b4a76 --- /dev/null +++ b/src/Dto/ErrorDto.php @@ -0,0 +1,21 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Dto; + +/** + * Error DTO + * + * @since 3.0.0 + */ +final class ErrorDto +{ +} diff --git a/src/Dto/ErrorsDto.php b/src/Dto/ErrorsDto.php new file mode 100644 index 00000000..14ec2696 --- /dev/null +++ b/src/Dto/ErrorsDto.php @@ -0,0 +1,21 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Dto; + +/** + * Errors DTO + * + * @since 3.0.0 + */ +final class ErrorsDto +{ +} diff --git a/src/Entity/IpAddress.php b/src/Entity/IpAddress.php index ce603b0b..0b6c6ec5 100644 --- a/src/Entity/IpAddress.php +++ b/src/Entity/IpAddress.php @@ -15,6 +15,7 @@ * Import functions */ use function filter_var; +use function ip2long; /** * Import constants @@ -118,6 +119,25 @@ public function isInReservedRange(): bool return false === filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); } + /** + * Converts the IP address to an integer if it possible + * + * @return int|null + */ + public function toLong(): ?int + { + if (!$this->isVersion4()) { + return null; + } + + $long = ip2long($this->value); + if ($long === false) { + return null; + } + + return $long; + } + /** * Converts the object to a string * diff --git a/src/Exception/MissingRequestParameterException.php b/src/Exception/MissingRequestParameterException.php new file mode 100644 index 00000000..343ec870 --- /dev/null +++ b/src/Exception/MissingRequestParameterException.php @@ -0,0 +1,26 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; + +/** + * MissingRequestParameterException + * + * @since 3.0.0 + */ +class MissingRequestParameterException extends HttpBadRequestException +{ +} diff --git a/src/Middleware/CommittingEntityChangesMiddleware.php b/src/Middleware/CommittingEntityChangesMiddleware.php new file mode 100644 index 00000000..9a27e6c0 --- /dev/null +++ b/src/Middleware/CommittingEntityChangesMiddleware.php @@ -0,0 +1,70 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Middleware; + +/** + * Import classes + */ +use Doctrine\Persistence\ManagerRegistry as EntityManagerRegistryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; + +/** + * CommittingEntityChangesMiddleware + * + * @since 3.0.0 + */ +final class CommittingEntityChangesMiddleware implements MiddlewareInterface +{ + + /** + * @var EntityManagerRegistryInterface + */ + private EntityManagerRegistryInterface $entityManagerRegistry; + + /** + * @var list + */ + private array $entityManagerNames; + + /** + * @param EntityManagerRegistryInterface $entityManagerRegistry + * @param list $entityManagerNames + */ + public function __construct( + EntityManagerRegistryInterface $entityManagerRegistry, + array $entityManagerNames = [] + ) { + $this->entityManagerRegistry = $entityManagerRegistry; + $this->entityManagerNames = $entityManagerNames; + } + + /** + * {@inheritdoc} + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $response = $handler->handle($request); + + if (empty($this->entityManagerNames)) { + $this->entityManagerRegistry->getManager()->flush(); + } + + foreach ($this->entityManagerNames as $entityManagerName) { + $this->entityManagerRegistry->getManager($entityManagerName)->flush(); + } + + return $response; + } +} diff --git a/src/Middleware/JsonErrorHandlingMiddleware.php b/src/Middleware/JsonErrorHandlingMiddleware.php new file mode 100644 index 00000000..66fab0c2 --- /dev/null +++ b/src/Middleware/JsonErrorHandlingMiddleware.php @@ -0,0 +1,70 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Middleware; + +/** + * Import classes + */ +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Psr\Log\LoggerInterface; +use Sunrise\Http\Router\Exception\Http\HttpExceptionInterface; + +/** + * JsonErrorHandlingMiddleware + * + * @since 3.0.0 + */ +final class JsonErrorHandlingMiddleware implements MiddlewareInterface +{ + + /** + * @var ResponseFactoryInterface + */ + private ResponseFactoryInterface $responseFactory; + + /** + * @var LoggerInterface + */ + private LoggerInterface $logger; + + /** + * @param ResponseFactoryInterface $responseFactory + * @param LoggerInterface $logger + */ + public function __construct(ResponseFactoryInterface $responseFactory, LoggerInterface $logger) + { + $this->responseFactory = $responseFactory; + $this->logger = $logger; + } + + /** + * {@inheritdoc} + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + try { + return $handler->handle($request); + } catch (HttpExceptionInterface $e) { + $this->logger->debug($e->getMessage()); + + return $this->responseFactory->createResponse($e->getStatusCode()); + } catch (Throwable $e) { + $this->logger->error($e->getMessage()); + + return $this->responseFactory->createResponse(500); + } + } +} diff --git a/src/ParameterResolver/DependencyInjectionParameterResolver.php b/src/ParameterResolver/DependencyInjectionParameterResolver.php index 8530be49..38eb7df1 100644 --- a/src/ParameterResolver/DependencyInjectionParameterResolver.php +++ b/src/ParameterResolver/DependencyInjectionParameterResolver.php @@ -66,8 +66,8 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo public function resolveParameter(ReflectionParameter $parameter, $context) { /** @var ReflectionNamedType */ - $type = $parameter->getType(); + $parameterType = $parameter->getType(); - return $this->container->get($type->getName()); + return $this->container->get($parameterType->getName()); } } diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index dd231633..b42ba750 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -102,14 +102,14 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} * - * @throws UnhydrableObjectException - * If an object isn't valid. - * * @throws InvalidRequestBodyException * If the request body structure isn't valid. * * @throws UnprocessableRequestBodyException * If the request body data isn't valid. + * + * @throws UnhydrableObjectException + * If an object isn't valid. */ public function resolveParameter(ReflectionParameter $parameter, $context) { @@ -117,10 +117,10 @@ public function resolveParameter(ReflectionParameter $parameter, $context) $context = $context; /** @var ReflectionNamedType */ - $type = $parameter->getType(); + $parameterType = $parameter->getType(); try { - $object = $this->hydrator->hydrate($type->getName(), (array) $context->getParsedBody()); + $object = $this->hydrator->hydrate($parameterType->getName(), (array) $context->getParsedBody()); } catch (InvalidObjectException $e) { throw new UnhydrableObjectException($e->getMessage(), 0, $e); } catch (InvalidValueException $e) { diff --git a/src/ParameterResolver/RequestEntityParameterResolver.php b/src/ParameterResolver/RequestEntityParameterResolver.php index 4ad6b936..6903cd01 100644 --- a/src/ParameterResolver/RequestEntityParameterResolver.php +++ b/src/ParameterResolver/RequestEntityParameterResolver.php @@ -18,17 +18,15 @@ use Psr\Http\Message\ServerRequestInterface; use Sunrise\Http\Router\Annotation\RequestEntity; use Sunrise\Http\Router\Exception\EntityNotFoundException; -use Sunrise\Http\Router\Exception\ResolvingParameterException; +use Sunrise\Http\Router\Exception\MissingRequestParameterException; use Sunrise\Http\Router\ParameterResolverInterface; use ReflectionAttribute; -use ReflectionMethod; use ReflectionNamedType; use ReflectionParameter; /** * Import functions */ -use function class_exists; use function sprintf; /** @@ -78,6 +76,12 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} + * + * @throws MissingRequestParameterException + * If an entity ID was not found in the request parameters. + * + * @throws EntityNotFoundException + * If an entity was not found. */ public function resolveParameter(ReflectionParameter $parameter, $context) { @@ -85,63 +89,36 @@ public function resolveParameter(ReflectionParameter $parameter, $context) $context = $context; /** @var ReflectionNamedType */ - $type = $parameter->getType(); + $parameterType = $parameter->getType(); - /** @var array{0: ReflectionAttribute} */ - $attributes = $parameter->getAttributes(RequestEntity::class); + /** @var non-empty-list */ + $parameterRequestEntityAttributes = $parameter->getAttributes(RequestEntity::class); /** @var RequestEntity */ - $requestEntity = $attributes[0]->newInstance(); + $requestEntity = $parameterRequestEntityAttributes[0]->newInstance(); - /** @var mixed */ - $entityId = $context->getAttribute($requestEntity->paramKey); - if (!isset($entityId)) { - throw new ResolvingParameterException(sprintf( - '{%s} Unable to get Entity ID (%s) by key %s from the request attributes', - $this->stringifyParameter($parameter), - $requestEntity->findBy, - $requestEntity->paramKey - )); - } + // if no request parameter key was assigned, the entity field name will be used... + $requestParameterKey = $requestEntity->paramKey ?? $requestEntity->findBy; - $entityName = $type->getName(); - if (!class_exists($entityName)) { - throw new ResolvingParameterException(sprintf( - '{%s} Entity %s does not exist', - $this->stringifyParameter($parameter), - $entityName - )); - } - - $entityManager = isset($requestEntity->em) ? - $this->entityManagerRegistry->getManager($requestEntity->em) : - $this->entityManagerRegistry->getManagerForClass($entityName); - - if (!isset($entityManager)) { - throw new ResolvingParameterException(sprintf( - '{%s} Unable to get Entity Manager for %s', - $this->stringifyParameter($parameter), - $entityName - )); - } + /** @var string|null */ + $entityId = $context->getAttribute($requestParameterKey); - $entityMetadata = $entityManager->getClassMetadata($entityName); - if (!$entityMetadata->hasField($requestEntity->findBy)) { - throw new ResolvingParameterException(sprintf( - '{%s} Entity %s does not contain field %s', - $this->stringifyParameter($parameter), - $entityName, - $requestEntity->findBy + if (!isset($entityId)) { + throw new MissingRequestParameterException(sprintf( + 'Missing the %s parameter in the request', + $requestParameterKey )); } - $criteria = [ - $requestEntity->findBy => $entityId, - ]; + $criteria = $requestEntity->criteria; + $criteria[$requestEntity->findBy] = $entityId; - $criteria += $requestEntity->criteria; + /** @var class-string */ + $entityName = $parameterType->getName(); - $entity = $entityManager->getRepository($entityName) + $entity = $this->entityManagerRegistry + ->getManager($requestEntity->em) + ->getRepository($entityName) ->findOneBy($criteria); if (isset($entity)) { @@ -152,41 +129,6 @@ public function resolveParameter(ReflectionParameter $parameter, $context) return null; } - throw new EntityNotFoundException(sprintf( - '%s Not Found', - $entityMetadata->getReflectionClass()->getShortName() - )); - } - - /** - * Stringifies the given parameter - * - * @param ReflectionParameter $parameter - * - * @return string - */ - private function stringifyParameter(ReflectionParameter $parameter): string - { - /** @var ReflectionNamedType */ - $parameterType = $parameter->getType(); - - if ($parameter->getDeclaringFunction() instanceof ReflectionMethod) { - return sprintf( - '%s::%s(%s $%s[%d])', - $parameter->getDeclaringFunction()->getDeclaringClass()->getName(), - $parameter->getDeclaringFunction()->getName(), - $parameterType->getName(), - $parameter->getName(), - $parameter->getPosition() - ); - } - - return sprintf( - '%s(%s $%s[%d])', - $parameter->getDeclaringFunction()->getName(), - $parameterType->getName(), - $parameter->getName(), - $parameter->getPosition() - ); + throw new EntityNotFoundException('Entity Not Found'); } } diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php index c59b5863..070daab4 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -102,14 +102,14 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} * - * @throws UnhydrableObjectException - * If an object isn't valid. - * * @throws InvalidRequestQueryException * If the request query structure isn't valid. * * @throws UnprocessableRequestQueryException * If the request query data isn't valid. + * + * @throws UnhydrableObjectException + * If an object isn't valid. */ public function resolveParameter(ReflectionParameter $parameter, $context) { @@ -117,10 +117,10 @@ public function resolveParameter(ReflectionParameter $parameter, $context) $context = $context; /** @var ReflectionNamedType */ - $type = $parameter->getType(); + $parameterType = $parameter->getType(); try { - $object = $this->hydrator->hydrate($type->getName(), $context->getQueryParams()); + $object = $this->hydrator->hydrate($parameterType->getName(), $context->getQueryParams()); } catch (InvalidObjectException $e) { throw new UnhydrableObjectException($e->getMessage(), 0, $e); } catch (InvalidValueException $e) { diff --git a/src/ParameterResolver/ServerRequestParameterResolver.php b/src/ParameterResolver/ServerRequestParameterResolver.php index 8510933b..58753289 100644 --- a/src/ParameterResolver/ServerRequestParameterResolver.php +++ b/src/ParameterResolver/ServerRequestParameterResolver.php @@ -59,4 +59,3 @@ public function resolveParameter(ReflectionParameter $parameter, $context) return ServerRequest::from($context); } } - diff --git a/src/Router.php b/src/Router.php index 4ce6810e..09b932b2 100644 --- a/src/Router.php +++ b/src/Router.php @@ -285,7 +285,7 @@ public function match(ServerRequestInterface $request): RouteInterface throw new HttpMethodNotAllowedException($allowedMethods); } - throw new HttpNotFoundException(); + throw new HttpNotFoundException('Page Not Found'); } /** From eaa4ec431c3eea63a2379e621f9a8d7a59db799e Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sat, 11 Feb 2023 23:50:42 +0100 Subject: [PATCH 066/180] v3 --- src/Dto/ErrorDto.php | 21 ------ src/Dto/ErrorsDto.php | 21 ------ .../JsonErrorHandlingMiddleware.php | 70 ------------------- 3 files changed, 112 deletions(-) delete mode 100644 src/Dto/ErrorDto.php delete mode 100644 src/Dto/ErrorsDto.php delete mode 100644 src/Middleware/JsonErrorHandlingMiddleware.php diff --git a/src/Dto/ErrorDto.php b/src/Dto/ErrorDto.php deleted file mode 100644 index 0e0b4a76..00000000 --- a/src/Dto/ErrorDto.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Dto; - -/** - * Error DTO - * - * @since 3.0.0 - */ -final class ErrorDto -{ -} diff --git a/src/Dto/ErrorsDto.php b/src/Dto/ErrorsDto.php deleted file mode 100644 index 14ec2696..00000000 --- a/src/Dto/ErrorsDto.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Dto; - -/** - * Errors DTO - * - * @since 3.0.0 - */ -final class ErrorsDto -{ -} diff --git a/src/Middleware/JsonErrorHandlingMiddleware.php b/src/Middleware/JsonErrorHandlingMiddleware.php deleted file mode 100644 index 66fab0c2..00000000 --- a/src/Middleware/JsonErrorHandlingMiddleware.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Middleware; - -/** - * Import classes - */ -use Psr\Http\Message\ResponseFactoryInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; -use Psr\Log\LoggerInterface; -use Sunrise\Http\Router\Exception\Http\HttpExceptionInterface; - -/** - * JsonErrorHandlingMiddleware - * - * @since 3.0.0 - */ -final class JsonErrorHandlingMiddleware implements MiddlewareInterface -{ - - /** - * @var ResponseFactoryInterface - */ - private ResponseFactoryInterface $responseFactory; - - /** - * @var LoggerInterface - */ - private LoggerInterface $logger; - - /** - * @param ResponseFactoryInterface $responseFactory - * @param LoggerInterface $logger - */ - public function __construct(ResponseFactoryInterface $responseFactory, LoggerInterface $logger) - { - $this->responseFactory = $responseFactory; - $this->logger = $logger; - } - - /** - * {@inheritdoc} - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - try { - return $handler->handle($request); - } catch (HttpExceptionInterface $e) { - $this->logger->debug($e->getMessage()); - - return $this->responseFactory->createResponse($e->getStatusCode()); - } catch (Throwable $e) { - $this->logger->error($e->getMessage()); - - return $this->responseFactory->createResponse(500); - } - } -} From 44b0aecf42ef8b2d1da27e8a161347b0c5772943 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 12 Feb 2023 01:59:26 +0100 Subject: [PATCH 067/180] v3 --- .../ClientNotConsumedMediaTypeException.php | 26 ++++++++ .../ClientNotProducedMediaTypeException.php | 26 ++++++++ src/Exception/MethodNotAllowedException.php | 24 +++++++ src/Exception/PageNotFoundException.php | 26 ++++++++ src/RouteCollection.php | 12 ++-- src/RouteCollectionInterface.php | 2 +- src/Router.php | 64 ++++++++++--------- 7 files changed, 142 insertions(+), 38 deletions(-) create mode 100644 src/Exception/ClientNotConsumedMediaTypeException.php create mode 100644 src/Exception/ClientNotProducedMediaTypeException.php create mode 100644 src/Exception/MethodNotAllowedException.php create mode 100644 src/Exception/PageNotFoundException.php diff --git a/src/Exception/ClientNotConsumedMediaTypeException.php b/src/Exception/ClientNotConsumedMediaTypeException.php new file mode 100644 index 00000000..ec11ce1f --- /dev/null +++ b/src/Exception/ClientNotConsumedMediaTypeException.php @@ -0,0 +1,26 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpNotAcceptableException; + +/** + * ClientNotConsumedMediaTypeException + * + * @since 3.0.0 + */ +class ClientNotConsumedMediaTypeException extends HttpNotAcceptableException +{ +} diff --git a/src/Exception/ClientNotProducedMediaTypeException.php b/src/Exception/ClientNotProducedMediaTypeException.php new file mode 100644 index 00000000..d6664161 --- /dev/null +++ b/src/Exception/ClientNotProducedMediaTypeException.php @@ -0,0 +1,26 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpUnsupportedMediaTypeException; + +/** + * ClientNotProducedMediaTypeException + * + * @since 3.0.0 + */ +class ClientNotProducedMediaTypeException extends HttpUnsupportedMediaTypeException +{ +} diff --git a/src/Exception/MethodNotAllowedException.php b/src/Exception/MethodNotAllowedException.php new file mode 100644 index 00000000..b2edb2f4 --- /dev/null +++ b/src/Exception/MethodNotAllowedException.php @@ -0,0 +1,24 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpMethodNotAllowedException; + +/** + * MethodNotAllowedException + */ +class MethodNotAllowedException extends HttpMethodNotAllowedException +{ +} diff --git a/src/Exception/PageNotFoundException.php b/src/Exception/PageNotFoundException.php new file mode 100644 index 00000000..0a8ad15d --- /dev/null +++ b/src/Exception/PageNotFoundException.php @@ -0,0 +1,26 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +namespace Sunrise\Http\Router\Exception; + +/** + * Import classes + */ +use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; + +/** + * PageNotFoundException + * + * @since 2.4.2 + */ +class PageNotFoundException extends HttpNotFoundException +{ +} diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 37db2551..53f42485 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -36,7 +36,7 @@ class RouteCollection implements RouteCollectionInterface /** * @var string */ - private const ANY_HOST = '*'; + private const ANY = '*'; /** * @var array @@ -81,16 +81,16 @@ public function all(): Iterator /** * {@inheritdoc} */ - public function allByHost(?string $host): Iterator + public function allOnHost(?string $host): Iterator { - if (isset($host, $this->hostMap[$host])) { + if (isset($host) && isset($this->hostMap[$host])) { foreach ($this->hostMap[$host] as $name) { yield $this->routes[$name]; } } - if (isset($this->hostMap[self::ANY_HOST])) { - foreach ($this->hostMap[self::ANY_HOST] as $name) { + if (isset($this->hostMap[self::ANY])) { + foreach ($this->hostMap[self::ANY] as $name) { yield $this->routes[$name]; } } @@ -126,7 +126,7 @@ public function add(RouteInterface ...$routes): RouteCollectionInterface { foreach ($routes as $route) { $name = $route->getName(); - $host = $route->getHost() ?? self::ANY_HOST; + $host = $route->getHost() ?? self::ANY; if (isset($this->routes[$name])) { throw new RouteAlreadyExistsException(sprintf( diff --git a/src/RouteCollectionInterface.php b/src/RouteCollectionInterface.php index 3c224262..554861e9 100644 --- a/src/RouteCollectionInterface.php +++ b/src/RouteCollectionInterface.php @@ -48,7 +48,7 @@ public function all(): Iterator; * * @since 3.0.0 */ - public function allByHost(?string $host): Iterator; + public function allOnHost(?string $host): Iterator; /** * Checks by the given name if a route exists in the collection diff --git a/src/Router.php b/src/Router.php index 09b932b2..6eae4458 100644 --- a/src/Router.php +++ b/src/Router.php @@ -21,10 +21,10 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\Event\RouteEvent; -use Sunrise\Http\Router\Exception\Http\HttpMethodNotAllowedException; -use Sunrise\Http\Router\Exception\Http\HttpNotAcceptableException; -use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; -use Sunrise\Http\Router\Exception\Http\HttpUnsupportedMediaTypeException; +use Sunrise\Http\Router\Exception\ClientNotConsumedMediaTypeException; +use Sunrise\Http\Router\Exception\ClientNotProducedMediaTypeException; +use Sunrise\Http\Router\Exception\MethodNotAllowedException; +use Sunrise\Http\Router\Exception\PageNotFoundException; use Sunrise\Http\Router\Loader\LoaderInterface; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; use Sunrise\Http\Router\RequestHandler\UnsafeCallableRequestHandler; @@ -227,65 +227,67 @@ public function generateUri(string $name, array $attributes = [], bool $strict = * * @return RouteInterface * - * @throws HttpNotFoundException - * If the request URI cannot be matched against any route. + * @throws PageNotFoundException + * If the request URI isn't served. * - * @throws HttpMethodNotAllowedException + * @throws MethodNotAllowedException * If the request method isn't allowed. * - * @throws HttpUnsupportedMediaTypeException + * @throws ClientNotProducedMediaTypeException + * If the client not produces required media types. * - * @throws HttpNotAcceptableException + * @throws ClientNotConsumedMediaTypeException + * If the client not consumed provided media types. */ public function match(ServerRequestInterface $request): RouteInterface { $requestUri = $request->getUri(); $requestHost = $requestUri->getHost(); $requestPath = $requestUri->getPath(); - $requestMethod = $request->getMethod(); - $allowedMethods = []; + $requestVerb = $request->getMethod(); + $allowedVerbs = []; - $routes = $this->routes->allByHost($this->hosts->resolve($requestHost)); - - $request = new ServerRequest($request); + $host = $this->hosts->resolve($requestHost); + $routes = $this->routes->allOnHost($host); + $request = ServerRequest::from($request); foreach ($routes as $route) { // https://github.com/sunrise-php/http-router/issues/50 // https://tools.ietf.org/html/rfc7231#section-6.5.5 - if (!path_match($route->getPath(), $requestPath, $attributes)) { + if (!path_match($route->getPath(), $requestPath, $routeAttributes)) { continue; } - $routeMethods = []; - foreach ($route->getMethods() as $routeMethod) { - $routeMethods[$routeMethod] = true; - $allowedMethods[$routeMethod] = $routeMethod; + $routeVerbs = []; + foreach ($route->getMethods() as $routeVerb) { + $routeVerbs[$routeVerb] = true; + $allowedVerbs[$routeVerb] = $routeVerb; } - if (!isset($routeMethods[$requestMethod])) { + if (!isset($routeVerbs[$requestVerb])) { continue; } - $consumedMediaTypes = $route->getConsumedMediaTypes(); - if (!empty($consumedMediaTypes) && !$request->clientProducesMediaType($consumedMediaTypes)) { - throw new HttpUnsupportedMediaTypeException($consumedMediaTypes); + $routeConsumed = $route->getConsumedMediaTypes(); + if (!empty($routeConsumed) && !$request->clientProducesMediaType($routeConsumed)) { + throw new ClientNotProducedMediaTypeException($routeConsumed); } - $producedMediaTypes = $route->getProducedMediaTypes(); - if (!empty($producedMediaTypes) && !$request->clientConsumesMediaType($producedMediaTypes)) { - throw new HttpNotAcceptableException($producedMediaTypes); + $routeProduced = $route->getProducedMediaTypes(); + if (!empty($routeProduced) && !$request->clientConsumesMediaType($routeProduced)) { + throw new ClientNotConsumedMediaTypeException($routeProduced); } - /** @var array $attributes */ + /** @var array $routeAttributes */ - return $route->withAddedAttributes($attributes); + return $route->withAddedAttributes($routeAttributes); } - if (!empty($allowedMethods)) { - throw new HttpMethodNotAllowedException($allowedMethods); + if (!empty($allowedVerbs)) { + throw new MethodNotAllowedException($allowedVerbs); } - throw new HttpNotFoundException('Page Not Found'); + throw new PageNotFoundException('Page Not Found'); } /** From e49f4c487f587606ed85899221fa7cee0c494c31 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 12 Feb 2023 06:39:13 +0100 Subject: [PATCH 068/180] v3 --- src/Router.php | 96 +++++++++++++++++------------------ src/ServerRequest.php | 113 +++++++++++++++++++++++++++++++++++------- 2 files changed, 144 insertions(+), 65 deletions(-) diff --git a/src/Router.php b/src/Router.php index 6eae4458..061e15c9 100644 --- a/src/Router.php +++ b/src/Router.php @@ -61,7 +61,7 @@ class Router implements RequestHandlerInterface, RequestMethodInterface private HostTable $hosts; /** - * The router's routes + * The router's route collection * * @var RouteCollectionInterface */ @@ -104,22 +104,6 @@ public function __construct( $this->routes = $routes ?? new RouteCollection(); } - /** - * Adds the given patterns to the router - * - * @param array $patterns - * - * @return void - * - * @since 2.11.0 - */ - public function addPatterns(array $patterns): void - { - foreach ($patterns as $alias => $pattern) { - self::$patterns[$alias] = $pattern; - } - } - /** * Gets the router's host table * @@ -143,33 +127,33 @@ public function getRoutes(): RouteCollectionInterface } /** - * Adds the given middleware(s) to the router - * - * @param MiddlewareInterface ...$middlewares + * Gets the router's middlewares * - * @return void + * @return list */ - public function addMiddleware(MiddlewareInterface ...$middlewares): void + public function getMiddlewares(): array { - foreach ($middlewares as $middleware) { - $this->middlewares[] = $middleware; - } + return $this->middlewares; } /** - * Gets the router's middlewares + * Gets the router's event dispatcher * - * @return list + * @return EventDispatcherInterface|null + * + * @since 2.13.0 */ - public function getMiddlewares(): array + public function getEventDispatcher(): ?EventDispatcherInterface { - return $this->middlewares; + return $this->eventDispatcher; } /** * Gets the router's matched route * * @return RouteInterface|null + * + * @since 2.12.0 */ public function getMatchedRoute(): ?RouteInterface { @@ -177,29 +161,47 @@ public function getMatchedRoute(): ?RouteInterface } /** - * Sets the given event dispatcher to the router + * Adds the given patterns to the router * - * @param EventDispatcherInterface|null $eventDispatcher + * @param array $patterns * * @return void * - * @since 2.13.0 + * @since 2.11.0 */ - public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher): void + public function addPatterns(array $patterns): void { - $this->eventDispatcher = $eventDispatcher; + foreach ($patterns as $alias => $pattern) { + self::$patterns[$alias] = $pattern; + } } /** - * Gets the router's event dispatcher + * Adds the given middleware(s) to the router * - * @return EventDispatcherInterface|null + * @param MiddlewareInterface ...$middlewares + * + * @return void + */ + public function addMiddleware(MiddlewareInterface ...$middlewares): void + { + foreach ($middlewares as $middleware) { + $this->middlewares[] = $middleware; + } + } + + /** + * Sets the given event dispatcher to the router + * + * @param EventDispatcherInterface|null $eventDispatcher + * + * @return void * * @since 2.13.0 */ - public function getEventDispatcher(): ?EventDispatcherInterface + public function setEventDispatcher(?EventDispatcherInterface $eventDispatcher): void { - return $this->eventDispatcher; + $this->eventDispatcher = $eventDispatcher; } /** @@ -254,7 +256,7 @@ public function match(ServerRequestInterface $request): RouteInterface foreach ($routes as $route) { // https://github.com/sunrise-php/http-router/issues/50 // https://tools.ietf.org/html/rfc7231#section-6.5.5 - if (!path_match($route->getPath(), $requestPath, $routeAttributes)) { + if (!path_match($route->getPath(), $requestPath, $attributes)) { continue; } @@ -268,19 +270,19 @@ public function match(ServerRequestInterface $request): RouteInterface continue; } - $routeConsumed = $route->getConsumedMediaTypes(); - if (!empty($routeConsumed) && !$request->clientProducesMediaType($routeConsumed)) { - throw new ClientNotProducedMediaTypeException($routeConsumed); + $routeConsumes = $route->getConsumedMediaTypes(); + if (!empty($routeConsumes) && !$request->clientProducesMediaType($routeConsumes)) { + throw new ClientNotProducedMediaTypeException($routeConsumes); } - $routeProduced = $route->getProducedMediaTypes(); - if (!empty($routeProduced) && !$request->clientConsumesMediaType($routeProduced)) { - throw new ClientNotConsumedMediaTypeException($routeProduced); + $routeProduces = $route->getProducedMediaTypes(); + if (!empty($routeProduces) && !$request->clientConsumesMediaType($routeProduces)) { + throw new ClientNotConsumedMediaTypeException($routeProduces); } - /** @var array $routeAttributes */ + /** @var array $attributes */ - return $route->withAddedAttributes($routeAttributes); + return $route->withAddedAttributes($attributes); } if (!empty($allowedVerbs)) { diff --git a/src/ServerRequest.php b/src/ServerRequest.php index af9dd4fc..f73996de 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -95,6 +95,16 @@ public function isXml(): bool ]); } + /** + * Checks if the request is XMLHttpRequest + * + * @return bool + */ + public function isXmlHttpRequest(): bool + { + return $this->request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; + } + /** * Gets the client's IP address * @@ -113,18 +123,22 @@ public function getClientIpAddress(array $proxyChain = []): IpAddress $trustedHeader = $proxyChain[$clientIp]; unset($proxyChain[$clientIp]); - // the chain can't be untangled... - if (!$this->request->hasHeader($trustedHeader)) { + $header = $this->request->getHeaderLine($trustedHeader); + if ($header === '') { break; } - // X-Forwarded-For: , , - $clientIp = $this->request->getHeaderLine($trustedHeader); - if (strpos($clientIp, ',') !== false) { - $clientIp = strstr($clientIp, ',', true); + $proxiedClientIp = strstr($header, ',', true); + if ($proxiedClientIp === false) { + $proxiedClientIp = $header; } - $clientIp = trim($clientIp); + $proxiedClientIp = trim($proxiedClientIp); + if ($proxiedClientIp === '') { + break; + } + + $clientIp = $proxiedClientIp; } return new IpAddress($clientIp); @@ -145,16 +159,17 @@ public function getClientProducedMediaType(): string return ''; } - if (strpos($header, ';') !== false) { - $header = strstr($header, ';', true); + $mediaType = strstr($header, ';', true); + if ($mediaType === false) { + $mediaType = $header; } - $header = trim($header); - if ($header === '') { + $mediaType = trim($mediaType); + if ($mediaType === '') { return ''; } - return strtolower($header); + return strtolower($mediaType); } /** @@ -176,20 +191,82 @@ public function getClientConsumedMediaTypes(): array $result = []; $accepts = explode(',', $header); foreach ($accepts as $accept) { - if (strpos($accept, ';') !== false) { - $accept = strstr($accept, ';', true); + $mediaType = strstr($accept, ';', true); + if ($mediaType === false) { + $mediaType = $accept; } - $accept = trim($accept); - if ($accept === '') { + $mediaType = trim($mediaType); + if ($mediaType === '') { continue; } - if ($accept === '*/*') { + if ($mediaType === '*/*') { return []; } - $result[] = strtolower($accept); + $result[] = strtolower($mediaType); + } + + return $result; + } + + /** + * Gets the client's consumed languages + * + * @return array + */ + public function getClientConsumedLanguages(): array + { + $header = $this->request->getHeaderLine('Accept-Language'); + if ($header === '') { + return []; + } + + $cursor = -1; + $inLanguage = true; + $inWeight = false; + $rows = []; + $i = 0; + + while (true) { + $char = $header[++$cursor] ?? null; + + if ($char === null) { + break; + } + if ($char === ' ') { + continue; + } + if ($char === ';') { + $inLanguage = false; + continue; + } + if ($char === '=') { + $inWeight = true; + continue; + } + if ($char === ',') { + $inLanguage = true; + $inWeight = false; + $i++; + continue; + } + if ($inLanguage) { + $rows[$i][0] ??= ''; + $rows[$i][0] .= $char; + continue; + } + if ($inWeight) { + $rows[$i][1] ??= ''; + $rows[$i][1] .= $char; + continue; + } + } + + $result = []; + foreach ($rows as $row) { + $result[$row[0]] = $row[1] ?? '1'; } return $result; From 686c335ff47d0e85c15d744dd16cce6e654d6cc1 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 12 Feb 2023 19:08:00 +0100 Subject: [PATCH 069/180] v3 --- composer.json | 5 ++-- .../StatusCodeResponseResolver.php | 2 +- src/ServerRequest.php | 25 +++++++++++++------ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/composer.json b/composer.json index 0682353f..bbe87bd1 100644 --- a/composer.json +++ b/composer.json @@ -37,14 +37,15 @@ "psr/simple-cache": "^1.0" }, "require-dev": { + "phpunit/phpunit": "^9.6", "doctrine/annotations": "^2.0", "doctrine/persistence": "^3.1", - "phpunit/phpunit": "~9.5.0", "sunrise/coding-standard": "~1.0.0", "sunrise/http-message": "^3.0", "sunrise/hydrator": "^2.7", "symfony/console": "^5.4", - "symfony/validator": "^5.4" + "symfony/validator": "^5.4", + "psr/log": "^1.1" }, "autoload": { "files": [ diff --git a/src/ResponseResolver/StatusCodeResponseResolver.php b/src/ResponseResolver/StatusCodeResponseResolver.php index c7b97262..064f1e23 100644 --- a/src/ResponseResolver/StatusCodeResponseResolver.php +++ b/src/ResponseResolver/StatusCodeResponseResolver.php @@ -57,7 +57,7 @@ public function supportsResponse($response, $context): bool */ public function resolveResponse($response, $context): ResponseInterface { - /** @var int $response */ + /** @var int<100, 599> $response */ return $this->responseFactory->createResponse($response); } diff --git a/src/ServerRequest.php b/src/ServerRequest.php index f73996de..d6035bc2 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -20,6 +20,7 @@ /** * Import functions */ +use function arsort; use function explode; use function strncmp; use function strpos; @@ -27,6 +28,11 @@ use function strtolower; use function trim; +/** + * Import constants + */ +use const SORT_NUMERIC; + /** * ServerRequest * @@ -214,7 +220,7 @@ public function getClientConsumedMediaTypes(): array /** * Gets the client's consumed languages * - * @return array + * @return array */ public function getClientConsumedLanguages(): array { @@ -226,7 +232,7 @@ public function getClientConsumedLanguages(): array $cursor = -1; $inLanguage = true; $inWeight = false; - $rows = []; + $data = []; $i = 0; while (true) { @@ -253,22 +259,25 @@ public function getClientConsumedLanguages(): array continue; } if ($inLanguage) { - $rows[$i][0] ??= ''; - $rows[$i][0] .= $char; + $data[$i][0] ??= ''; + $data[$i][0] .= $char; continue; } if ($inWeight) { - $rows[$i][1] ??= ''; - $rows[$i][1] .= $char; + $data[$i][1] ??= ''; + $data[$i][1] .= $char; continue; } } $result = []; - foreach ($rows as $row) { - $result[$row[0]] = $row[1] ?? '1'; + foreach ($data as $item) { + /** @var array{0: string, 1?: numeric-string} $item */ + $result[$item[0]] = (float) ($item[1] ?? 1.0); } + arsort($result, SORT_NUMERIC); + return $result; } From 8e3aab012f363da238f95be6a5c9dc7272c7cb3b Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Mon, 13 Feb 2023 02:37:17 +0100 Subject: [PATCH 070/180] v3 --- .../Http/HttpBadRequestException.php | 8 +- src/Exception/Http/HttpConflictException.php | 7 +- .../Http/HttpExpectationFailedException.php | 7 +- .../Http/HttpFailedDependencyException.php | 7 +- src/Exception/Http/HttpForbiddenException.php | 8 +- src/Exception/Http/HttpGoneException.php | 10 +-- .../Http/HttpLengthRequiredException.php | 7 +- src/Exception/Http/HttpLockedException.php | 7 +- .../Http/HttpMisdirectedRequestException.php | 8 +- .../Http/HttpNotAcceptableException.php | 18 ++-- src/Exception/Http/HttpNotFoundException.php | 10 +-- .../Http/HttpPayloadTooLargeException.php | 8 +- .../Http/HttpPaymentRequiredException.php | 8 +- .../Http/HttpPreconditionFailedException.php | 7 +- .../HttpPreconditionRequiredException.php | 9 +- ...tpProxyAuthenticationRequiredException.php | 7 +- .../Http/HttpRangeNotSatisfiableException.php | 8 +- ...tpRequestHeaderFieldsTooLargeException.php | 8 +- .../Http/HttpRequestTimeoutException.php | 10 +-- .../Http/HttpServiceUnavailableException.php | 11 +-- src/Exception/Http/HttpTooEarlyException.php | 7 +- .../Http/HttpTooManyRequestsException.php | 7 +- .../Http/HttpUnauthorizedException.php | 8 +- ...ttpUnavailableForLegalReasonsException.php | 8 +- .../Http/HttpUnprocessableEntityException.php | 7 +- .../HttpUnsupportedMediaTypeException.php | 39 +++------ .../Http/HttpUpgradeRequiredException.php | 9 +- .../Http/HttpUriTooLongException.php | 7 +- src/Exception/{Http => }/HttpException.php | 42 ++++++++-- .../{Http => }/HttpExceptionInterface.php | 12 ++- .../ServerRequestParameterResolver.php | 61 -------------- src/ServerRequest.php | 84 +------------------ 32 files changed, 146 insertions(+), 318 deletions(-) rename src/Exception/{Http => }/HttpException.php (52%) rename src/Exception/{Http => }/HttpExceptionInterface.php (74%) delete mode 100644 src/ParameterResolver/ServerRequestParameterResolver.php diff --git a/src/Exception/Http/HttpBadRequestException.php b/src/Exception/Http/HttpBadRequestException.php index efdd06bd..cbc905d9 100644 --- a/src/Exception/Http/HttpBadRequestException.php +++ b/src/Exception/Http/HttpBadRequestException.php @@ -14,14 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Bad Request Exception * - * The server cannot or will not process the request due to something that is perceived to be a client error (e.g., - * malformed request syntax, invalid request message framing, or deceptive request routing). - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/400 * * @since 3.0.0 @@ -32,9 +30,9 @@ class HttpBadRequestException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpConflictException.php b/src/Exception/Http/HttpConflictException.php index 6e5115fa..0996af75 100644 --- a/src/Exception/Http/HttpConflictException.php +++ b/src/Exception/Http/HttpConflictException.php @@ -14,13 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Conflict Exception * - * This response is sent when a request conflicts with the current state of the server. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409 * * @since 3.0.0 @@ -31,9 +30,9 @@ class HttpConflictException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpExpectationFailedException.php b/src/Exception/Http/HttpExpectationFailedException.php index 4d976867..2c0945eb 100644 --- a/src/Exception/Http/HttpExpectationFailedException.php +++ b/src/Exception/Http/HttpExpectationFailedException.php @@ -14,13 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Expectation Failed Exception * - * This response code means the expectation indicated by the Expect request header field cannot be met by the server. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417 * * @since 3.0.0 @@ -31,9 +30,9 @@ class HttpExpectationFailedException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpFailedDependencyException.php b/src/Exception/Http/HttpFailedDependencyException.php index c895fd6a..878ca548 100644 --- a/src/Exception/Http/HttpFailedDependencyException.php +++ b/src/Exception/Http/HttpFailedDependencyException.php @@ -14,12 +14,13 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Failed Dependency Exception * - * The request failed due to failure of a previous request. + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/424 * * @since 3.0.0 */ @@ -29,9 +30,9 @@ class HttpFailedDependencyException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpForbiddenException.php b/src/Exception/Http/HttpForbiddenException.php index a1b03951..b2f50362 100644 --- a/src/Exception/Http/HttpForbiddenException.php +++ b/src/Exception/Http/HttpForbiddenException.php @@ -14,14 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Forbidden Exception * - * The client does not have access rights to the content; that is, it is unauthorized, so the server is refusing to give - * the requested resource. Unlike 401 Unauthorized, the client's identity is known to the server. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403 * * @since 3.0.0 @@ -32,9 +30,9 @@ class HttpForbiddenException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpGoneException.php b/src/Exception/Http/HttpGoneException.php index 0eae223b..e7e7734a 100644 --- a/src/Exception/Http/HttpGoneException.php +++ b/src/Exception/Http/HttpGoneException.php @@ -14,16 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Gone Exception * - * This response is sent when the requested content has been permanently deleted from server, with no forwarding - * address. Clients are expected to remove their caches and links to the resource. The HTTP specification intends this - * status code to be used for "limited-time, promotional services". APIs should not feel compelled to indicate resources - * that have been deleted with this status code. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/410 * * @since 3.0.0 @@ -34,9 +30,9 @@ class HttpGoneException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpLengthRequiredException.php b/src/Exception/Http/HttpLengthRequiredException.php index bf1e58cf..622b5579 100644 --- a/src/Exception/Http/HttpLengthRequiredException.php +++ b/src/Exception/Http/HttpLengthRequiredException.php @@ -14,13 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Length Required Exception * - * Server rejected the request because the Content-Length header field is not defined and the server requires it. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411 * * @since 3.0.0 @@ -31,9 +30,9 @@ class HttpLengthRequiredException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpLockedException.php b/src/Exception/Http/HttpLockedException.php index 6b280521..d53dfdeb 100644 --- a/src/Exception/Http/HttpLockedException.php +++ b/src/Exception/Http/HttpLockedException.php @@ -14,13 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Locked Exception * - * The resource that is being accessed is locked. - * * @since 3.0.0 */ class HttpLockedException extends HttpException @@ -29,9 +28,9 @@ class HttpLockedException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpMisdirectedRequestException.php b/src/Exception/Http/HttpMisdirectedRequestException.php index 14228d90..03886cf2 100644 --- a/src/Exception/Http/HttpMisdirectedRequestException.php +++ b/src/Exception/Http/HttpMisdirectedRequestException.php @@ -14,14 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Misdirected Request Exception * - * The request was directed at a server that is not able to produce a response. This can be sent by a server that is not - * configured to produce responses for the combination of scheme and authority that are included in the request URI. - * * @since 3.0.0 */ class HttpMisdirectedRequestException extends HttpException @@ -30,9 +28,9 @@ class HttpMisdirectedRequestException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpNotAcceptableException.php b/src/Exception/Http/HttpNotAcceptableException.php index e0bd105d..4f775ee2 100644 --- a/src/Exception/Http/HttpNotAcceptableException.php +++ b/src/Exception/Http/HttpNotAcceptableException.php @@ -14,14 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Not Acceptable Exception * - * This response is sent when the web server, after performing server-driven content negotiation, doesn't find any - * content that conforms to the criteria given by the user agent. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406 * * @since 3.0.0 @@ -39,20 +37,22 @@ class HttpNotAcceptableException extends HttpException /** * Constructor of the class * - * @param array $supported + * @param list $supportedMediaTypes * @param string|null $message * @param int $code * @param Throwable|null $previous */ - public function __construct(array $supported, ?string $message = null, int $code = 0, ?Throwable $previous = null) - { + public function __construct( + array $supportedMediaTypes, + ?string $message = null, + int $code = 0, + ?Throwable $previous = null + ) { $message ??= 'Not Acceptable'; parent::__construct(self::STATUS_NOT_ACCEPTABLE, $message, $code, $previous); - foreach ($supported as $mediaType) { - $this->supportedMediaTypes[] = $mediaType; - } + $this->supportedMediaTypes = $supportedMediaTypes; } /** diff --git a/src/Exception/Http/HttpNotFoundException.php b/src/Exception/Http/HttpNotFoundException.php index 8ee9fada..ea27916c 100644 --- a/src/Exception/Http/HttpNotFoundException.php +++ b/src/Exception/Http/HttpNotFoundException.php @@ -14,16 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Not Found Exception * - * The server cannot find the requested resource. In the browser, this means the URL is not recognized. In an API, this - * can also mean that the endpoint is valid but the resource itself does not exist. Servers may also send this response - * instead of 403 Forbidden to hide the existence of a resource from an unauthorized client. This response code is - * probably the most well known due to its frequent occurrence on the web. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404 * * @since 3.0.0 @@ -34,9 +30,9 @@ class HttpNotFoundException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpPayloadTooLargeException.php b/src/Exception/Http/HttpPayloadTooLargeException.php index 6a411a43..1d838c34 100644 --- a/src/Exception/Http/HttpPayloadTooLargeException.php +++ b/src/Exception/Http/HttpPayloadTooLargeException.php @@ -14,14 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Payload Too Large Exception * - * Request entity is larger than limits defined by server. The server might close the connection or return an - * Retry-After header field. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/413 * * @since 3.0.0 @@ -32,9 +30,9 @@ class HttpPayloadTooLargeException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpPaymentRequiredException.php b/src/Exception/Http/HttpPaymentRequiredException.php index d5684ef7..23318337 100644 --- a/src/Exception/Http/HttpPaymentRequiredException.php +++ b/src/Exception/Http/HttpPaymentRequiredException.php @@ -14,14 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Payment Required Exception * - * This response code is reserved for future use. The initial aim for creating this code was using it for digital - * payment systems, however this status code is used very rarely and no standard convention exists. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402 * * @since 3.0.0 @@ -32,9 +30,9 @@ class HttpPaymentRequiredException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpPreconditionFailedException.php b/src/Exception/Http/HttpPreconditionFailedException.php index 18ab7ba3..79695132 100644 --- a/src/Exception/Http/HttpPreconditionFailedException.php +++ b/src/Exception/Http/HttpPreconditionFailedException.php @@ -14,13 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Precondition Failed Exception * - * The server does not meet one of the preconditions that the requester put on the request header fields. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412 * * @since 3.0.0 @@ -31,9 +30,9 @@ class HttpPreconditionFailedException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpPreconditionRequiredException.php b/src/Exception/Http/HttpPreconditionRequiredException.php index 44eb6c51..fbdf469e 100644 --- a/src/Exception/Http/HttpPreconditionRequiredException.php +++ b/src/Exception/Http/HttpPreconditionRequiredException.php @@ -14,15 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Precondition Required Exception * - * The origin server requires the request to be conditional. This response is intended to prevent the 'lost update' - * problem, where a client GETs a resource's state, modifies it and PUTs it back to the server, when meanwhile a third - * party has modified the state on the server, leading to a conflict. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428 * * @since 3.0.0 @@ -33,9 +30,9 @@ class HttpPreconditionRequiredException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpProxyAuthenticationRequiredException.php b/src/Exception/Http/HttpProxyAuthenticationRequiredException.php index 0fed38a5..48c2120d 100644 --- a/src/Exception/Http/HttpProxyAuthenticationRequiredException.php +++ b/src/Exception/Http/HttpProxyAuthenticationRequiredException.php @@ -14,13 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Proxy Authentication Required Exception * - * This is similar to 401 Unauthorized but authentication is needed to be done by a proxy. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407 * * @since 3.0.0 @@ -31,9 +30,9 @@ class HttpProxyAuthenticationRequiredException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpRangeNotSatisfiableException.php b/src/Exception/Http/HttpRangeNotSatisfiableException.php index 66bb81ad..05c7b47e 100644 --- a/src/Exception/Http/HttpRangeNotSatisfiableException.php +++ b/src/Exception/Http/HttpRangeNotSatisfiableException.php @@ -14,14 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Range Not Satisfiable Exception * - * The range specified by the Range header field in the request cannot be fulfilled. It's possible that the range is - * outside the size of the target URI's data. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416 * * @since 3.0.0 @@ -32,9 +30,9 @@ class HttpRangeNotSatisfiableException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php b/src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php index c0c31e8b..dfdfd852 100644 --- a/src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php +++ b/src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php @@ -14,14 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Request Header Fields Too Large Exception * - * The server is unwilling to process the request because its header fields are too large. The request may be - * resubmitted after reducing the size of the request header fields. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431 * * @since 3.0.0 @@ -32,9 +30,9 @@ class HttpRequestHeaderFieldsTooLargeException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpRequestTimeoutException.php b/src/Exception/Http/HttpRequestTimeoutException.php index f667cb26..86232169 100644 --- a/src/Exception/Http/HttpRequestTimeoutException.php +++ b/src/Exception/Http/HttpRequestTimeoutException.php @@ -14,16 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Request Timeout Exception * - * This response is sent on an idle connection by some servers, even without any previous request by the client. It - * means that the server would like to shut down this unused connection. This response is used much more since some - * browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection mechanisms to speed up surfing. Also note that - * some servers merely shut down the connection without sending this message. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408 * * @since 3.0.0 @@ -34,9 +30,9 @@ class HttpRequestTimeoutException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpServiceUnavailableException.php b/src/Exception/Http/HttpServiceUnavailableException.php index fc974870..0cdaf959 100644 --- a/src/Exception/Http/HttpServiceUnavailableException.php +++ b/src/Exception/Http/HttpServiceUnavailableException.php @@ -14,17 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Service Unavailable Exception * - * The server is not ready to handle the request. Common causes are a server that is down for maintenance or that is - * overloaded. Note that together with this response, a user-friendly page explaining the problem should be sent. This - * response should be used for temporary conditions and the Retry-After HTTP header should, if possible, contain the - * estimated time before the recovery of the service. The webmaster must also take care about the caching-related - * headers that are sent along with this response, as these temporary condition responses should usually not be cached. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503 * * @since 3.0.0 @@ -35,9 +30,9 @@ class HttpServiceUnavailableException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpTooEarlyException.php b/src/Exception/Http/HttpTooEarlyException.php index e2837cc2..e8415193 100644 --- a/src/Exception/Http/HttpTooEarlyException.php +++ b/src/Exception/Http/HttpTooEarlyException.php @@ -14,13 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Too Early Exception * - * Indicates that the server is unwilling to risk processing a request that might be replayed. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/425 * * @since 3.0.0 @@ -31,9 +30,9 @@ class HttpTooEarlyException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpTooManyRequestsException.php b/src/Exception/Http/HttpTooManyRequestsException.php index 812cb313..a30a3c30 100644 --- a/src/Exception/Http/HttpTooManyRequestsException.php +++ b/src/Exception/Http/HttpTooManyRequestsException.php @@ -14,13 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Too Many Requests Exception * - * The user has sent too many requests in a given amount of time ("rate limiting"). - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429 * * @since 3.0.0 @@ -31,9 +30,9 @@ class HttpTooManyRequestsException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpUnauthorizedException.php b/src/Exception/Http/HttpUnauthorizedException.php index c6e2a006..f236fd07 100644 --- a/src/Exception/Http/HttpUnauthorizedException.php +++ b/src/Exception/Http/HttpUnauthorizedException.php @@ -14,14 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Unauthorized Exception * - * Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated". That is, the - * client must authenticate itself to get the requested response. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401 * * @since 3.0.0 @@ -32,9 +30,9 @@ class HttpUnauthorizedException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpUnavailableForLegalReasonsException.php b/src/Exception/Http/HttpUnavailableForLegalReasonsException.php index 728e0bff..5c7e49c4 100644 --- a/src/Exception/Http/HttpUnavailableForLegalReasonsException.php +++ b/src/Exception/Http/HttpUnavailableForLegalReasonsException.php @@ -14,14 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Unavailable For Legal Reasons Exception * - * The server is unwilling to process the request because its header fields are too large. The request may be - * resubmitted after reducing the size of the request header fields. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/451 * * @since 3.0.0 @@ -32,9 +30,9 @@ class HttpUnavailableForLegalReasonsException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpUnprocessableEntityException.php b/src/Exception/Http/HttpUnprocessableEntityException.php index 34f8f121..543f906c 100644 --- a/src/Exception/Http/HttpUnprocessableEntityException.php +++ b/src/Exception/Http/HttpUnprocessableEntityException.php @@ -14,13 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Unprocessable Entity Exception * - * The request was well-formed but was unable to be followed due to semantic errors. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/422 * * @since 3.0.0 @@ -31,9 +30,9 @@ class HttpUnprocessableEntityException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpUnsupportedMediaTypeException.php b/src/Exception/Http/HttpUnsupportedMediaTypeException.php index 9db05489..b8b6f4f4 100644 --- a/src/Exception/Http/HttpUnsupportedMediaTypeException.php +++ b/src/Exception/Http/HttpUnsupportedMediaTypeException.php @@ -14,6 +14,7 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** @@ -24,8 +25,6 @@ /** * HTTP Unsupported Media Type Exception * - * The media format of the requested data is not supported by the server, so the server is rejecting the request. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/415 * * @since 3.0.0 @@ -43,20 +42,24 @@ class HttpUnsupportedMediaTypeException extends HttpException /** * Constructor of the class * - * @param array $supported + * @param list $supportedMediaTypes * @param string|null $message * @param int $code * @param Throwable|null $previous */ - public function __construct(array $supported, ?string $message = null, int $code = 0, ?Throwable $previous = null) - { + public function __construct( + array $supportedMediaTypes, + ?string $message = null, + int $code = 0, + ?Throwable $previous = null + ) { $message ??= 'Unsupported Media Type'; parent::__construct(self::STATUS_UNSUPPORTED_MEDIA_TYPE, $message, $code, $previous); - foreach ($supported as $mediaType) { - $this->supportedMediaTypes[] = $mediaType; - } + $this->supportedMediaTypes = $supportedMediaTypes; + + $this->addHeaderField('Accept', $this->getJoinedSupportedTypes()); } /** @@ -76,24 +79,6 @@ final public function getSupportedTypes(): array */ final public function getJoinedSupportedTypes(): string { - return join(',', $this->getSupportedTypes()); - } - - /** - * Gets arguments for an Accept header field - * - * Returns an array where key 0 contains the header name and key 1 contains its value. - * - * - * $response = $response - * ->withStatus($e->getStatusCode()) - * ->withHeader(...$e->getAcceptHeaderArguments()); - * - * - * @return array{0: string, 1: string} - */ - final public function getAcceptHeaderArguments(): array - { - return ['Accept', $this->getJoinedSupportedTypes()]; + return join(',', $this->supportedMediaTypes); } } diff --git a/src/Exception/Http/HttpUpgradeRequiredException.php b/src/Exception/Http/HttpUpgradeRequiredException.php index 4d0d4d88..02752367 100644 --- a/src/Exception/Http/HttpUpgradeRequiredException.php +++ b/src/Exception/Http/HttpUpgradeRequiredException.php @@ -14,15 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP Upgrade Required Exception * - * 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. The server sends an Upgrade header in a 426 response to indicate the required - * protocol(s). - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426 * * @since 3.0.0 @@ -33,9 +30,9 @@ class HttpUpgradeRequiredException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpUriTooLongException.php b/src/Exception/Http/HttpUriTooLongException.php index 5b92cce5..c0aaac09 100644 --- a/src/Exception/Http/HttpUriTooLongException.php +++ b/src/Exception/Http/HttpUriTooLongException.php @@ -14,13 +14,12 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** * HTTP URI Too Long Exception * - * The URI requested by the client is longer than the server is willing to interpret. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414 * * @since 3.0.0 @@ -31,9 +30,9 @@ class HttpUriTooLongException extends HttpException /** * Constructor of the class * - * @param ?string $message + * @param string|null $message * @param int $code - * @param ?Throwable $previous + * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { diff --git a/src/Exception/Http/HttpException.php b/src/Exception/HttpException.php similarity index 52% rename from src/Exception/Http/HttpException.php rename to src/Exception/HttpException.php index 9c01bcf1..9b982406 100644 --- a/src/Exception/Http/HttpException.php +++ b/src/Exception/HttpException.php @@ -9,7 +9,7 @@ * @link https://github.com/sunrise-php/http-router */ -namespace Sunrise\Http\Router\Exception\Http; +namespace Sunrise\Http\Router\Exception; /** * Import classes @@ -28,21 +28,28 @@ class HttpException extends RuntimeException implements HttpExceptionInterface /** * HTTP status code * - * @var int + * @var int<100, 599> */ private int $statusCode; + /** + * HTTP header fields + * + * @var list + */ + private array $headerFields = []; + /** * Constructor of the class * - * @param int $statusCode + * @param int<100, 599> $statusCode * @param string $message - * @param int $errorCode - * @param ?Throwable $previous + * @param int $code + * @param Throwable|null $previous */ - public function __construct(int $statusCode, string $message, int $errorCode = 0, ?Throwable $previous = null) + public function __construct(int $statusCode, string $message, int $code = 0, ?Throwable $previous = null) { - parent::__construct($message, $errorCode, $previous); + parent::__construct($message, $code, $previous); $this->statusCode = $statusCode; } @@ -54,4 +61,25 @@ final public function getStatusCode(): int { return $this->statusCode; } + + /** + * {@inheritdoc} + */ + final public function getHeaderFields(): array + { + return $this->headerFields; + } + + /** + * Adds the given header field to the exception + * + * @param string $fieldName + * @param string $fieldValue + * + * @return void + */ + final public function addHeaderField(string $fieldName, string $fieldValue): void + { + $this->headerFields[] = [$fieldName, $fieldValue]; + } } diff --git a/src/Exception/Http/HttpExceptionInterface.php b/src/Exception/HttpExceptionInterface.php similarity index 74% rename from src/Exception/Http/HttpExceptionInterface.php rename to src/Exception/HttpExceptionInterface.php index f56a38a7..364098b8 100644 --- a/src/Exception/Http/HttpExceptionInterface.php +++ b/src/Exception/HttpExceptionInterface.php @@ -9,13 +9,12 @@ * @link https://github.com/sunrise-php/http-router */ -namespace Sunrise\Http\Router\Exception\Http; +namespace Sunrise\Http\Router\Exception; /** * Import classes */ use Fig\Http\Message\StatusCodeInterface; -use Sunrise\Http\Router\Exception\ExceptionInterface; /** * Base HTTP exception interface @@ -28,7 +27,14 @@ interface HttpExceptionInterface extends ExceptionInterface, StatusCodeInterface /** * Gets HTTP status code * - * @return int + * @return int<100, 599> */ public function getStatusCode(): int; + + /** + * Gets HTTP header fields + * + * @return list + */ + public function getHeaderFields(): array; } diff --git a/src/ParameterResolver/ServerRequestParameterResolver.php b/src/ParameterResolver/ServerRequestParameterResolver.php deleted file mode 100644 index 58753289..00000000 --- a/src/ParameterResolver/ServerRequestParameterResolver.php +++ /dev/null @@ -1,61 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\ParameterResolver; - -/** - * Import classes - */ -use Psr\Http\Message\ServerRequestInterface; -use Sunrise\Http\Router\ParameterResolverInterface; -use Sunrise\Http\Router\ServerRequest; -use ReflectionNamedType; -use ReflectionParameter; - -/** - * ServerRequestParameterResolver - * - * @since 3.0.0 - */ -final class ServerRequestParameterResolver implements ParameterResolverInterface -{ - - /** - * {@inheritdoc} - */ - public function supportsParameter(ReflectionParameter $parameter, $context): bool - { - if (!($context instanceof ServerRequestInterface)) { - return false; - } - - if (!($parameter->getType() instanceof ReflectionNamedType)) { - return false; - } - - if (!($parameter->getType()->getName() === ServerRequest::class)) { - return false; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function resolveParameter(ReflectionParameter $parameter, $context) - { - /** @var ServerRequestInterface */ - $context = $context; - - return ServerRequest::from($context); - } -} diff --git a/src/ServerRequest.php b/src/ServerRequest.php index d6035bc2..1eb2bffe 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -20,7 +20,6 @@ /** * Import functions */ -use function arsort; use function explode; use function strncmp; use function strpos; @@ -28,11 +27,6 @@ use function strtolower; use function trim; -/** - * Import constants - */ -use const SORT_NUMERIC; - /** * ServerRequest * @@ -101,16 +95,6 @@ public function isXml(): bool ]); } - /** - * Checks if the request is XMLHttpRequest - * - * @return bool - */ - public function isXmlHttpRequest(): bool - { - return $this->request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; - } - /** * Gets the client's IP address * @@ -126,10 +110,10 @@ public function getClientIpAddress(array $proxyChain = []): IpAddress $clientIp = $env['REMOTE_ADDR'] ?? '::1'; while (isset($proxyChain[$clientIp])) { - $trustedHeader = $proxyChain[$clientIp]; + $proxyHeader = $proxyChain[$clientIp]; unset($proxyChain[$clientIp]); - $header = $this->request->getHeaderLine($trustedHeader); + $header = $this->request->getHeaderLine($proxyHeader); if ($header === '') { break; } @@ -217,70 +201,6 @@ public function getClientConsumedMediaTypes(): array return $result; } - /** - * Gets the client's consumed languages - * - * @return array - */ - public function getClientConsumedLanguages(): array - { - $header = $this->request->getHeaderLine('Accept-Language'); - if ($header === '') { - return []; - } - - $cursor = -1; - $inLanguage = true; - $inWeight = false; - $data = []; - $i = 0; - - while (true) { - $char = $header[++$cursor] ?? null; - - if ($char === null) { - break; - } - if ($char === ' ') { - continue; - } - if ($char === ';') { - $inLanguage = false; - continue; - } - if ($char === '=') { - $inWeight = true; - continue; - } - if ($char === ',') { - $inLanguage = true; - $inWeight = false; - $i++; - continue; - } - if ($inLanguage) { - $data[$i][0] ??= ''; - $data[$i][0] .= $char; - continue; - } - if ($inWeight) { - $data[$i][1] ??= ''; - $data[$i][1] .= $char; - continue; - } - } - - $result = []; - foreach ($data as $item) { - /** @var array{0: string, 1?: numeric-string} $item */ - $result[$item[0]] = (float) ($item[1] ?? 1.0); - } - - arsort($result, SORT_NUMERIC); - - return $result; - } - /** * Checks if the client produces one of the given media types * From 65098dd93e884a8f6ece38624ba45e97dd1da2a0 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Mon, 13 Feb 2023 02:37:25 +0100 Subject: [PATCH 071/180] v3 --- .../Http/HttpInternalServerErrorException.php | 44 ----------------- .../Http/HttpMethodNotAllowedException.php | 46 +++++------------ src/Router.php | 49 ++++++++++--------- 3 files changed, 39 insertions(+), 100 deletions(-) delete mode 100644 src/Exception/Http/HttpInternalServerErrorException.php diff --git a/src/Exception/Http/HttpInternalServerErrorException.php b/src/Exception/Http/HttpInternalServerErrorException.php deleted file mode 100644 index 9f43458e..00000000 --- a/src/Exception/Http/HttpInternalServerErrorException.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception\Http; - -/** - * Import classes - */ -use Throwable; - -/** - * HTTP Internal Server Error Exception - * - * The server has encountered a situation it does not know how to handle. - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 - * - * @since 3.0.0 - */ -class HttpInternalServerErrorException extends HttpException -{ - - /** - * Constructor of the class - * - * @param ?string $message - * @param int $code - * @param ?Throwable $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Internal Server Error'; - - parent::__construct(self::STATUS_INTERNAL_SERVER_ERROR, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpMethodNotAllowedException.php b/src/Exception/Http/HttpMethodNotAllowedException.php index b2eae99e..5f696179 100644 --- a/src/Exception/Http/HttpMethodNotAllowedException.php +++ b/src/Exception/Http/HttpMethodNotAllowedException.php @@ -14,6 +14,7 @@ /** * Import classes */ +use Sunrise\Http\Router\Exception\HttpException; use Throwable; /** @@ -24,9 +25,6 @@ /** * HTTP Method Not Allowed Exception * - * The request method is known by the server but is not supported by the target resource. For example, an API may not - * allow calling DELETE to remove a resource. - * * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/405 * * @since 3.0.0 @@ -39,25 +37,29 @@ class HttpMethodNotAllowedException extends HttpException * * @var list */ - private array $allowedMethods = []; + private array $allowedMethods; /** * Constructor of the class * - * @param array $allowed + * @param list $allowedMethods * @param string|null $message * @param int $code * @param Throwable|null $previous */ - public function __construct(array $allowed, ?string $message = null, int $code = 0, ?Throwable $previous = null) - { + public function __construct( + array $allowedMethods, + ?string $message = null, + int $code = 0, + ?Throwable $previous = null + ) { $message ??= 'Method Not Allowed'; parent::__construct(self::STATUS_METHOD_NOT_ALLOWED, $message, $code, $previous); - foreach ($allowed as $method) { - $this->allowedMethods[] = $method; - } + $this->allowedMethods = $allowedMethods; + + $this->addHeaderField('Allow', $this->getJoinedAllowedMethods()); } /** @@ -77,28 +79,6 @@ final public function getAllowedMethods(): array */ final public function getJoinedAllowedMethods(): string { - return join(',', $this->getAllowedMethods()); - } - - /** - * Gets arguments for an Allow header field - * - * The server must generate an Allow header field in a 405 status code response. - * - * The field must contain a list of methods that the target resource currently supports. - * - * Returns an array where key 0 contains the header name and key 1 contains its value. - * - * - * $response = $response - * ->withStatus($e->getStatusCode()) - * ->withHeader(...$e->getAllowHeaderArguments()); - * - * - * @return array{0: string, 1: string} - */ - final public function getAllowHeaderArguments(): array - { - return ['Allow', $this->getJoinedAllowedMethods()]; + return join(',', $this->allowedMethods); } } diff --git a/src/Router.php b/src/Router.php index 061e15c9..57a20dd0 100644 --- a/src/Router.php +++ b/src/Router.php @@ -32,6 +32,7 @@ /** * Import functions */ +use function array_keys; use function Sunrise\Http\Router\path_build; use function Sunrise\Http\Router\path_match; @@ -104,6 +105,20 @@ public function __construct( $this->routes = $routes ?? new RouteCollection(); } + /** + * Loads routes through the given loaders + * + * @param LoaderInterface ...$loaders + * + * @return void + */ + public function load(LoaderInterface ...$loaders): void + { + foreach ($loaders as $loader) { + $this->routes->add(...$loader->load()->all()); + } + } + /** * Gets the router's host table * @@ -246,8 +261,8 @@ public function match(ServerRequestInterface $request): RouteInterface $requestUri = $request->getUri(); $requestHost = $requestUri->getHost(); $requestPath = $requestUri->getPath(); - $requestVerb = $request->getMethod(); - $allowedVerbs = []; + $requestMethod = $request->getMethod(); + $allowedMethods = []; $host = $this->hosts->resolve($requestHost); $routes = $this->routes->allOnHost($host); @@ -260,13 +275,13 @@ public function match(ServerRequestInterface $request): RouteInterface continue; } - $routeVerbs = []; - foreach ($route->getMethods() as $routeVerb) { - $routeVerbs[$routeVerb] = true; - $allowedVerbs[$routeVerb] = $routeVerb; + $routeMethods = []; + foreach ($route->getMethods() as $routeMethod) { + $routeMethods[$routeMethod] = true; + $allowedMethods[$routeMethod] = true; } - if (!isset($routeVerbs[$requestVerb])) { + if (!isset($routeMethods[$requestMethod])) { continue; } @@ -285,8 +300,10 @@ public function match(ServerRequestInterface $request): RouteInterface return $route->withAddedAttributes($attributes); } - if (!empty($allowedVerbs)) { - throw new MethodNotAllowedException($allowedVerbs); + if (!empty($allowedMethods)) { + $allowedMethods = array_keys($allowedMethods); + + throw new MethodNotAllowedException($allowedMethods); } throw new PageNotFoundException('Page Not Found'); @@ -350,18 +367,4 @@ public function handle(ServerRequestInterface $request): ResponseInterface return $handler->handle($request); } - - /** - * Loads routes through the given loaders - * - * @param LoaderInterface ...$loaders - * - * @return void - */ - public function load(LoaderInterface ...$loaders): void - { - foreach ($loaders as $loader) { - $this->routes->add(...$loader->load()->all()); - } - } } From dc4e4e38d1f23e77042024cb0a4bc1f3ee081f7b Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 23 Jul 2023 06:31:53 +0200 Subject: [PATCH 072/180] v3 --- README.md | 4 +- composer.json | 20 +- functions/emit.php | 10 +- functions/get_debug_type.php | 49 --- functions/get_dir_classes.php | 53 --- functions/get_file_classes.php | 38 -- functions/parse_accept_like_header.php | 198 +++++++++ functions/path_build.php | 10 +- functions/path_match.php | 7 +- functions/path_parse.php | 10 +- functions/path_plain.php | 7 +- functions/path_regex.php | 7 +- functions/reflect_callable.php | 10 +- src/Annotation/Consume.php | 31 +- src/Annotation/Description.php | 31 +- src/Annotation/Host.php | 31 +- src/Annotation/Method.php | 34 +- src/Annotation/Middleware.php | 29 +- src/Annotation/Postfix.php | 31 +- src/Annotation/Prefix.php | 31 +- src/Annotation/Produce.php | 31 +- src/Annotation/RequestBody.php | 7 +- src/Annotation/RequestEntity.php | 41 -- src/Annotation/RequestQuery.php | 7 +- .../ResponseBody.php} | 13 +- src/Annotation/Route.php | 199 ++-------- src/Annotation/Summary.php | 31 +- src/Annotation/Tag.php | 31 +- src/Annotation/Version.php | 33 ++ src/ClassResolver.php | 14 +- src/ClassResolverInterface.php | 7 +- src/Command/RouteListCommand.php | 41 +- src/Entity/IpAddress.php | 121 +----- src/Entity/MediaType.php | 56 +++ src/Event/RouteEvent.php | 31 +- .../ClientNotConsumedMediaTypeException.php | 7 +- .../ClientNotProducedMediaTypeException.php | 7 +- src/Exception/EntityNotFoundException.php | 7 +- src/Exception/Exception.php | 7 +- src/Exception/ExceptionInterface.php | 7 +- .../Http/HttpBadRequestException.php | 7 +- src/Exception/Http/HttpConflictException.php | 7 +- .../Http/HttpExpectationFailedException.php | 7 +- .../Http/HttpFailedDependencyException.php | 7 +- src/Exception/Http/HttpForbiddenException.php | 7 +- src/Exception/Http/HttpGoneException.php | 7 +- .../Http/HttpLengthRequiredException.php | 7 +- src/Exception/Http/HttpLockedException.php | 7 +- .../Http/HttpMethodNotAllowedException.php | 10 +- .../Http/HttpMisdirectedRequestException.php | 7 +- .../Http/HttpNotAcceptableException.php | 7 +- src/Exception/Http/HttpNotFoundException.php | 7 +- .../Http/HttpPayloadTooLargeException.php | 7 +- .../Http/HttpPaymentRequiredException.php | 7 +- .../Http/HttpPreconditionFailedException.php | 7 +- .../HttpPreconditionRequiredException.php | 7 +- ...tpProxyAuthenticationRequiredException.php | 7 +- .../Http/HttpRangeNotSatisfiableException.php | 7 +- ...tpRequestHeaderFieldsTooLargeException.php | 7 +- .../Http/HttpRequestTimeoutException.php | 7 +- .../Http/HttpServiceUnavailableException.php | 7 +- src/Exception/Http/HttpTooEarlyException.php | 7 +- .../Http/HttpTooManyRequestsException.php | 7 +- .../Http/HttpUnauthorizedException.php | 7 +- ...ttpUnavailableForLegalReasonsException.php | 7 +- .../Http/HttpUnprocessableEntityException.php | 7 +- .../HttpUnsupportedMediaTypeException.php | 10 +- .../Http/HttpUpgradeRequiredException.php | 7 +- .../Http/HttpUriTooLongException.php | 7 +- src/Exception/HttpException.php | 7 +- src/Exception/HttpExceptionInterface.php | 7 +- src/Exception/InvalidArgumentException.php | 7 +- src/Exception/InvalidRequestBodyException.php | 26 -- .../InvalidRequestPayloadException.php | 7 +- .../InvalidRequestQueryException.php | 26 -- src/Exception/LogicException.php | 7 +- src/Exception/MethodNotAllowedException.php | 7 +- .../MissingRequestParameterException.php | 7 +- src/Exception/PageNotFoundException.php | 7 +- src/Exception/ResolvingParameterException.php | 4 +- src/Exception/ResolvingReferenceException.php | 4 +- src/Exception/ResolvingResponseException.php | 4 +- src/Exception/RouteAlreadyExistsException.php | 4 +- src/Exception/RouteNotFoundException.php | 4 +- src/Exception/RoutePathBuildException.php | 4 +- src/Exception/RoutePathParseException.php | 4 +- .../RoutePathProcessingException.php | 4 +- src/Exception/UnhydrableObjectException.php | 4 +- .../UnprocessableEntityException.php | 7 +- .../UnprocessableRequestBodyException.php | 4 +- .../UnprocessableRequestQueryException.php | 4 +- src/HostTable.php | 7 +- src/Loader/ConfigLoader.php | 74 ++-- src/Loader/DescriptorLoader.php | 375 +++++++++--------- src/Loader/LoaderInterface.php | 16 +- src/Middleware/CallableMiddleware.php | 76 +--- src/Middleware/CallbackMiddleware.php | 90 +++++ src/Middleware/ClientIpAddressMiddleware.php | 55 --- .../CommittingEntityChangesMiddleware.php | 70 ---- .../JsonPayloadDecodingMiddleware.php | 37 +- ...arsedBodyWhitespaceStrippingMiddleware.php | 62 --- src/Middleware/UnsafeCallableMiddleware.php | 56 --- .../XmlPayloadDecodingMiddleware.php | 85 ---- src/ParameterResolutioner.php | 32 +- src/ParameterResolutionerInterface.php | 23 +- .../ClientIpAddressParameterResolver.php | 77 ---- .../DependencyInjectionParameterResolver.php | 18 +- .../KnownUntypedParameterResolver.php | 71 ---- .../ParameterResolverInterface.php | 18 +- ...r.php => PresetTypedParameterResolver.php} | 43 +- .../RequestBodyParameterResolver.php | 80 +--- .../RequestEntityParameterResolver.php | 134 ------- .../RequestQueryParameterResolver.php | 56 +-- .../RequestRouteParameterResolver.php | 23 +- src/ReferenceResolver.php | 22 +- src/ReferenceResolverInterface.php | 7 +- src/RequestHandler/CallableRequestHandler.php | 27 +- .../QueueableRequestHandler.php | 16 +- .../UnsafeCallableRequestHandler.php | 9 +- src/RequestQueryInterface.php | 21 - src/ResponseResolutioner.php | 44 +- src/ResponseResolutionerInterface.php | 13 +- .../ObjectResponseResolver.php | 56 +++ .../ResponseResolverInterface.php | 13 +- .../RouteResponseResolver.php | 19 +- .../StatusCodeResponseResolver.php | 12 +- src/Route.php | 10 +- src/RouteCollection.php | 10 +- src/RouteCollectionFactory.php | 4 +- src/RouteCollectionFactoryInterface.php | 4 +- src/RouteCollectionInterface.php | 7 +- src/RouteCollector.php | 17 +- src/RouteFactory.php | 7 +- src/RouteFactoryInterface.php | 9 +- src/RouteInterface.php | 7 +- src/Router.php | 35 +- src/RouterBuilder.php | 7 +- src/ServerRequest.php | 216 +++++----- tests/Command/RouteListCommandTest.php | 3 - tests/Exception/BadRequestExceptionTest.php | 3 - tests/Exception/ExceptionTest.php | 3 - .../InvalidArgumentExceptionTest.php | 3 - .../InvalidAttributeValueExceptionTest.php | 3 - .../InvalidLoaderResourceExceptionTest.php | 3 - tests/Exception/InvalidPathExceptionTest.php | 3 - .../MethodNotAllowedExceptionTest.php | 6 - .../MissingAttributeValueExceptionTest.php | 3 - tests/Exception/PageNotFoundExceptionTest.php | 3 - .../Exception/RouteNotFoundExceptionTest.php | 3 - .../UnresolvableReferenceExceptionTest.php | 3 - .../UnsupportedMediaTypeExceptionTest.php | 6 - tests/Functions/FunctionPathBuildTest.php | 6 - tests/Functions/FunctionPathMatchTest.php | 6 - tests/Functions/FunctionPathParseTest.php | 6 - tests/Functions/FunctionPathPlainTest.php | 6 - tests/Functions/FunctionPathRegexTest.php | 6 - tests/Loader/ConfigLoaderTest.php | 3 - tests/Loader/DescriptorLoaderTest.php | 16 - tests/Middleware/CallableMiddlewareTest.php | 9 +- .../JsonPayloadDecodingMiddlewareTest.php | 3 - tests/ReferenceResolverTest.php | 3 - .../CallableRequestHandlerTest.php | 3 - .../QueueableRequestHandlerTest.php | 3 - tests/RouteCollectionFactoryTest.php | 3 - tests/RouteCollectionTest.php | 6 - tests/RouteCollectorTest.php | 3 - tests/RouteFactoryTest.php | 3 - tests/RouteTest.php | 6 - tests/RouterBuilderTest.php | 3 - tests/RouterTest.php | 12 +- 170 files changed, 1396 insertions(+), 2632 deletions(-) delete mode 100644 functions/get_debug_type.php delete mode 100644 functions/get_dir_classes.php delete mode 100644 functions/get_file_classes.php create mode 100644 functions/parse_accept_like_header.php delete mode 100644 src/Annotation/RequestEntity.php rename src/{RequestBodyInterface.php => Annotation/ResponseBody.php} (68%) create mode 100644 src/Annotation/Version.php create mode 100644 src/Entity/MediaType.php delete mode 100644 src/Exception/InvalidRequestBodyException.php delete mode 100644 src/Exception/InvalidRequestQueryException.php create mode 100644 src/Middleware/CallbackMiddleware.php delete mode 100644 src/Middleware/ClientIpAddressMiddleware.php delete mode 100644 src/Middleware/CommittingEntityChangesMiddleware.php delete mode 100644 src/Middleware/ParsedBodyWhitespaceStrippingMiddleware.php delete mode 100644 src/Middleware/UnsafeCallableMiddleware.php delete mode 100644 src/Middleware/XmlPayloadDecodingMiddleware.php delete mode 100644 src/ParameterResolver/ClientIpAddressParameterResolver.php delete mode 100644 src/ParameterResolver/KnownUntypedParameterResolver.php rename src/{ => ParameterResolver}/ParameterResolverInterface.php (78%) rename src/ParameterResolver/{KnownTypedParameterResolver.php => PresetTypedParameterResolver.php} (56%) delete mode 100644 src/ParameterResolver/RequestEntityParameterResolver.php delete mode 100644 src/RequestQueryInterface.php create mode 100644 src/ResponseResolver/ObjectResponseResolver.php rename src/{ => ResponseResolver}/ResponseResolverInterface.php (79%) diff --git a/README.md b/README.md index a20787bf..8ac4eb00 100644 --- a/README.md +++ b/README.md @@ -264,7 +264,7 @@ $response = $router->process($request, $handler); use Sunrise\Http\Message\ResponseFactory; use Sunrise\Http\Router\Exception\MethodNotAllowedException; use Sunrise\Http\Router\Exception\RouteNotFoundException; -use Sunrise\Http\Router\Middleware\CallableMiddleware; +use Sunrise\Http\Router\Middleware\CallbackMiddleware; use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use Sunrise\Http\Router\RouteCollector; use Sunrise\Http\Router\Router; @@ -281,7 +281,7 @@ $collector->get('home', '/', new CallableRequestHandler(function ($request) { $router = new Router(); $router->addRoute(...$collector->getCollection()->all()); -$router->addMiddleware(new CallableMiddleware(function ($request, $handler) { +$router->addMiddleware(new CallbackMiddleware(function ($request, $handler) { try { return $handler->handle($request); } catch (MethodNotAllowedException $e) { diff --git a/composer.json b/composer.json index bbe87bd1..e9823858 100644 --- a/composer.json +++ b/composer.json @@ -27,32 +27,27 @@ } ], "require": { - "php": ">=7.4", + "php": ">=8.0", "fig/http-message-util": "^1.1", "psr/container": "^1.0 || ^2.0", "psr/event-dispatcher": "^1.0", - "psr/http-message": "^1.0", + "psr/http-message": "^1.0 || ^2.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", - "psr/simple-cache": "^1.0" + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "phpunit/phpunit": "^9.6", - "doctrine/annotations": "^2.0", - "doctrine/persistence": "^3.1", - "sunrise/coding-standard": "~1.0.0", + "sunrise/coding-standard": "^1.0", "sunrise/http-message": "^3.0", - "sunrise/hydrator": "^2.7", + "sunrise/hydrator": "^3.0", "symfony/console": "^5.4", "symfony/validator": "^5.4", - "psr/log": "^1.1" + "vimeo/psalm": "^5.13" }, "autoload": { "files": [ "functions/emit.php", - "functions/get_debug_type.php", - "functions/get_dir_classes.php", - "functions/get_file_classes.php", "functions/path_build.php", "functions/path_match.php", "functions/path_parse.php", @@ -72,7 +67,8 @@ "scripts": { "test": [ "phpcs", - "psalm", + "psalm --no-cache", + "phpstan analyse src --level=5", "XDEBUG_MODE=coverage phpunit --coverage-text --colors=always" ], "build": [ diff --git a/functions/emit.php b/functions/emit.php index 38251649..50bf35c6 100644 --- a/functions/emit.php +++ b/functions/emit.php @@ -1,4 +1,4 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -if (!function_exists('get_debug_type')) { - - /** - * Polyfill for the get_debug_type function - * - * @param mixed $value - * - * @return string - * - * @since 3.0.0 - * - * @link https://www.php.net/get_debug_type - */ - function get_debug_type($value): string - { - if (null === $value) { - return 'null'; - } - - if (is_bool($value)) { - return 'bool'; - } - - if (is_int($value)) { - return 'int'; - } - - if (is_float($value)) { - return 'float'; - } - - if (is_object($value)) { - return get_class($value); - } - - return gettype($value); - } -} diff --git a/functions/get_dir_classes.php b/functions/get_dir_classes.php deleted file mode 100644 index c32ff6e6..00000000 --- a/functions/get_dir_classes.php +++ /dev/null @@ -1,53 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router; - -/** - * Import classes - */ -use Iterator; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; -use SplFileInfo; - -/** - * Scans the given directory and returns the found classes - * - * @param string $dirname - * - * @return class-string[] - * - * @since 3.0.0 - */ -function get_dir_classes(string $dirname): array -{ - /** @var Iterator */ - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($dirname) - ); - - $result = []; - - foreach ($files as $file) { - // only php files... - if ($file->getExtension() !== 'php') { - continue; - } - - $classnames = get_file_classes($file->getPathname()); - foreach ($classnames as $classname) { - $result[] = $classname; - } - } - - return $result; -} diff --git a/functions/get_file_classes.php b/functions/get_file_classes.php deleted file mode 100644 index 58b17e38..00000000 --- a/functions/get_file_classes.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router; - -/** - * Import functions - */ -use function array_diff; -use function get_declared_classes; - -/** - * Scans the given file and returns the found classes - * - * @param string $filename - * - * @return class-string[] - * - * @since 3.0.0 - * - * @todo https://www.php.net/manual/en/book.tokenizer.php - */ -function get_file_classes(string $filename): array -{ - $snapshot = get_declared_classes(); - - require_once $filename; - - return array_diff(get_declared_classes(), $snapshot); -} diff --git a/functions/parse_accept_like_header.php b/functions/parse_accept_like_header.php new file mode 100644 index 00000000..c8b41ee9 --- /dev/null +++ b/functions/parse_accept_like_header.php @@ -0,0 +1,198 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-message + */ + +namespace Sunrise\Http\Message; + +use function uasort; + +/** + * Parses the given accept-like header + * + * Returns null if the header isn't valid. + * + * @param string $header + * + * @return ?array> + */ +function parse_accept_header(string $header): ?array +{ + // OWS according to RFC-7230 + static $ows = [ + "\x09" => 1, "\x20" => 1, + ]; + + // token according to RFC-7230 + static $token = [ + "\x21" => 1, "\x23" => 1, "\x24" => 1, "\x25" => 1, "\x26" => 1, "\x27" => 1, "\x2a" => 1, "\x2b" => 1, + "\x2d" => 1, "\x2e" => 1, "\x30" => 1, "\x31" => 1, "\x32" => 1, "\x33" => 1, "\x34" => 1, "\x35" => 1, + "\x36" => 1, "\x37" => 1, "\x38" => 1, "\x39" => 1, "\x41" => 1, "\x42" => 1, "\x43" => 1, "\x44" => 1, + "\x45" => 1, "\x46" => 1, "\x47" => 1, "\x48" => 1, "\x49" => 1, "\x4a" => 1, "\x4b" => 1, "\x4c" => 1, + "\x4d" => 1, "\x4e" => 1, "\x4f" => 1, "\x50" => 1, "\x51" => 1, "\x52" => 1, "\x53" => 1, "\x54" => 1, + "\x55" => 1, "\x56" => 1, "\x57" => 1, "\x58" => 1, "\x59" => 1, "\x5a" => 1, "\x5e" => 1, "\x5f" => 1, + "\x60" => 1, "\x61" => 1, "\x62" => 1, "\x63" => 1, "\x64" => 1, "\x65" => 1, "\x66" => 1, "\x67" => 1, + "\x68" => 1, "\x69" => 1, "\x6a" => 1, "\x6b" => 1, "\x6c" => 1, "\x6d" => 1, "\x6e" => 1, "\x6f" => 1, + "\x70" => 1, "\x71" => 1, "\x72" => 1, "\x73" => 1, "\x74" => 1, "\x75" => 1, "\x76" => 1, "\x77" => 1, + "\x78" => 1, "\x79" => 1, "\x7a" => 1, "\x7c" => 1, "\x7e" => 1, + ]; + + // quoted-string according to RFC-7230 + static $quotedString = [ + "\x09" => 1, "\x20" => 1, "\x21" => 1, "\x23" => 1, "\x24" => 1, "\x25" => 1, "\x26" => 1, "\x27" => 1, + "\x28" => 1, "\x29" => 1, "\x2a" => 1, "\x2b" => 1, "\x2c" => 1, "\x2d" => 1, "\x2e" => 1, "\x2f" => 1, + "\x30" => 1, "\x31" => 1, "\x32" => 1, "\x33" => 1, "\x34" => 1, "\x35" => 1, "\x36" => 1, "\x37" => 1, + "\x38" => 1, "\x39" => 1, "\x3a" => 1, "\x3b" => 1, "\x3c" => 1, "\x3d" => 1, "\x3e" => 1, "\x3f" => 1, + "\x40" => 1, "\x41" => 1, "\x42" => 1, "\x43" => 1, "\x44" => 1, "\x45" => 1, "\x46" => 1, "\x47" => 1, + "\x48" => 1, "\x49" => 1, "\x4a" => 1, "\x4b" => 1, "\x4c" => 1, "\x4d" => 1, "\x4e" => 1, "\x4f" => 1, + "\x50" => 1, "\x51" => 1, "\x52" => 1, "\x53" => 1, "\x54" => 1, "\x55" => 1, "\x56" => 1, "\x57" => 1, + "\x58" => 1, "\x59" => 1, "\x5a" => 1, "\x5b" => 1, "\x5d" => 1, "\x5e" => 1, "\x5f" => 1, "\x60" => 1, + "\x61" => 1, "\x62" => 1, "\x63" => 1, "\x64" => 1, "\x65" => 1, "\x66" => 1, "\x67" => 1, "\x68" => 1, + "\x69" => 1, "\x6a" => 1, "\x6b" => 1, "\x6c" => 1, "\x6d" => 1, "\x6e" => 1, "\x6f" => 1, "\x70" => 1, + "\x71" => 1, "\x72" => 1, "\x73" => 1, "\x74" => 1, "\x75" => 1, "\x76" => 1, "\x77" => 1, "\x78" => 1, + "\x79" => 1, "\x7a" => 1, "\x7b" => 1, "\x7c" => 1, "\x7d" => 1, "\x7e" => 1, "\x80" => 1, "\x81" => 1, + "\x82" => 1, "\x83" => 1, "\x84" => 1, "\x85" => 1, "\x86" => 1, "\x87" => 1, "\x88" => 1, "\x89" => 1, + "\x8a" => 1, "\x8b" => 1, "\x8c" => 1, "\x8d" => 1, "\x8e" => 1, "\x8f" => 1, "\x90" => 1, "\x91" => 1, + "\x92" => 1, "\x93" => 1, "\x94" => 1, "\x95" => 1, "\x96" => 1, "\x97" => 1, "\x98" => 1, "\x99" => 1, + "\x9a" => 1, "\x9b" => 1, "\x9c" => 1, "\x9d" => 1, "\x9e" => 1, "\x9f" => 1, "\xa0" => 1, "\xa1" => 1, + "\xa2" => 1, "\xa3" => 1, "\xa4" => 1, "\xa5" => 1, "\xa6" => 1, "\xa7" => 1, "\xa8" => 1, "\xa9" => 1, + "\xaa" => 1, "\xab" => 1, "\xac" => 1, "\xad" => 1, "\xae" => 1, "\xaf" => 1, "\xb0" => 1, "\xb1" => 1, + "\xb2" => 1, "\xb3" => 1, "\xb4" => 1, "\xb5" => 1, "\xb6" => 1, "\xb7" => 1, "\xb8" => 1, "\xb9" => 1, + "\xba" => 1, "\xbb" => 1, "\xbc" => 1, "\xbd" => 1, "\xbe" => 1, "\xbf" => 1, "\xc0" => 1, "\xc1" => 1, + "\xc2" => 1, "\xc3" => 1, "\xc4" => 1, "\xc5" => 1, "\xc6" => 1, "\xc7" => 1, "\xc8" => 1, "\xc9" => 1, + "\xca" => 1, "\xcb" => 1, "\xcc" => 1, "\xcd" => 1, "\xce" => 1, "\xcf" => 1, "\xd0" => 1, "\xd1" => 1, + "\xd2" => 1, "\xd3" => 1, "\xd4" => 1, "\xd5" => 1, "\xd6" => 1, "\xd7" => 1, "\xd8" => 1, "\xd9" => 1, + "\xda" => 1, "\xdb" => 1, "\xdc" => 1, "\xdd" => 1, "\xde" => 1, "\xdf" => 1, "\xe0" => 1, "\xe1" => 1, + "\xe2" => 1, "\xe3" => 1, "\xe4" => 1, "\xe5" => 1, "\xe6" => 1, "\xe7" => 1, "\xe8" => 1, "\xe9" => 1, + "\xea" => 1, "\xeb" => 1, "\xec" => 1, "\xed" => 1, "\xee" => 1, "\xef" => 1, "\xf0" => 1, "\xf1" => 1, + "\xf2" => 1, "\xf3" => 1, "\xf4" => 1, "\xf5" => 1, "\xf6" => 1, "\xf7" => 1, "\xf8" => 1, "\xf9" => 1, + "\xfa" => 1, "\xfb" => 1, "\xfc" => 1, "\xfd" => 1, "\xfe" => 1, "\xff" => 1, + ]; + + $offset = -1; + + $inToken = true; + $inParamName = false; + $inParamValue = false; + $inQuotes = false; + + $data = []; + + $i = 0; + $p = 0; + + while (true) { + $char = $header[++$offset] ?? null; + + if (!isset($char)) { + break; + } + + $prev = $header[$offset-1] ?? null; + $next = $header[$offset+1] ?? null; + + if ($char === ',' && !$inQuotes) { + $inToken = true; + $inParamName = false; + $inParamValue = false; + $i++; + $p = 0; + continue; + } + if ($char === ';' && !$inQuotes) { + $inToken = false; + $inParamName = true; + $inParamValue = false; + $p++; + continue; + } + if ($char === '=' && !$inQuotes) { + $inToken = false; + $inParamName = false; + $inParamValue = true; + continue; + } + + // ignoring whitespaces around tokens... + if (isset($ows[$char]) && !$inQuotes) { + // en-GB[ ], ... + // ~~~~~~^~~~~~~ + if ($inToken && isset($data[$i][0])) { + $inToken = false; + } + // en-GB; q[ ]= 0.8, ... + // ~~~~~~~~~^~~~~~~~~~~~ + if ($inParamName && isset($data[$i][1][$p][0])) { + $inParamName = false; + } + // en-GB; q = 0.8[ ], ... + // ~~~~~~~~~~~~~~~^~~~~~~ + if ($inParamValue && isset($data[$i][1][$p][1])) { + $inParamValue = false; + } + + continue; + } + + // ignoring backslashes before double quotes in the quoted parameter value... + if ($char === '\\' && $next === '"' && $inQuotes) { + continue; + } + + if ($char === '"' && $inParamValue && !$inQuotes && !isset($data[$i][1][$p][1])) { + $inQuotes = true; + continue; + } + if ($char === '"' && $prev !== '\\' && $inQuotes) { + $inParamValue = false; + $inQuotes = false; + continue; + } + + // [en-GB]; q=0.8, ... + // ~^^^^^~~~~~~~~~~~~~ + if ($inToken && (isset($token[$char]) || $char === '/')) { + $data[$i][0] ??= ''; + $data[$i][0] .= $char; + continue; + } + // en-GB; [q]=0.8, ... + // ~~~~~~~~^~~~~~~~~~~ + if ($inParamName && isset($token[$char]) && isset($data[$i][0])) { + $data[$i][1][$p][0] ??= ''; + $data[$i][1][$p][0] .= $char; + continue; + } + // en-GB; q=[0.8], ... + // ~~~~~~~~~~^^^~~~~~~ + // phpcs:ignore Generic.Files.LineLength + if ($inParamValue && (isset($token[$char]) || ($inQuotes && (isset($quotedString[$char]) || ($prev . $char) === '\"'))) && isset($data[$i][1][$p][0])) { + $data[$i][1][$p][1] ??= ''; + $data[$i][1][$p][1] .= $char; + continue; + } + + // the header is invalid... + return null; + } + + $result = []; + foreach ($data as $item) { + $result[$item[0]] = []; + if (isset($item[1])) { + foreach ($item[1] as $param) { + $result[$item[0]][$param[0]] = $param[1] ?? ''; + } + } + } + + uasort($result, static fn(array $a, array $b): int => ($b['q'] ?? 1) <=> ($a['q'] ?? 1)); + + return $result; +} diff --git a/functions/path_build.php b/functions/path_build.php index 4ebff507..c9c5340d 100644 --- a/functions/path_build.php +++ b/functions/path_build.php @@ -1,4 +1,4 @@ -value = $value; } } diff --git a/src/Annotation/Description.php b/src/Annotation/Description.php index 0457e99c..32773539 100644 --- a/src/Annotation/Description.php +++ b/src/Annotation/Description.php @@ -1,4 +1,4 @@ -value = $value; } } diff --git a/src/Annotation/Host.php b/src/Annotation/Host.php index 4b4f9c8d..95f0a5c0 100644 --- a/src/Annotation/Host.php +++ b/src/Annotation/Host.php @@ -1,4 +1,4 @@ -value = $value; } } diff --git a/src/Annotation/Method.php b/src/Annotation/Method.php index e6ff17f2..f2544223 100644 --- a/src/Annotation/Method.php +++ b/src/Annotation/Method.php @@ -1,4 +1,4 @@ -value = $value; } } diff --git a/src/Annotation/Middleware.php b/src/Annotation/Middleware.php index 1d339476..980dc8ff 100644 --- a/src/Annotation/Middleware.php +++ b/src/Annotation/Middleware.php @@ -1,4 +1,4 @@ - - */ - public string $value; - /** * Constructor of the class * * @param class-string $value */ - public function __construct(string $value) + public function __construct(public string $value) { - $this->value = $value; } } diff --git a/src/Annotation/Postfix.php b/src/Annotation/Postfix.php index c2bb2a7d..48ff6d85 100644 --- a/src/Annotation/Postfix.php +++ b/src/Annotation/Postfix.php @@ -1,4 +1,4 @@ -value = $value; } } diff --git a/src/Annotation/Prefix.php b/src/Annotation/Prefix.php index 71f3baa8..aaccdeca 100644 --- a/src/Annotation/Prefix.php +++ b/src/Annotation/Prefix.php @@ -1,4 +1,4 @@ -value = $value; } } diff --git a/src/Annotation/Produce.php b/src/Annotation/Produce.php index 16be685c..ccac05ce 100644 --- a/src/Annotation/Produce.php +++ b/src/Annotation/Produce.php @@ -1,4 +1,4 @@ -value = $value; } } diff --git a/src/Annotation/RequestBody.php b/src/Annotation/RequestBody.php index 56ae949a..149c5d8f 100644 --- a/src/Annotation/RequestBody.php +++ b/src/Annotation/RequestBody.php @@ -1,4 +1,4 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Annotation; - -/** - * Import classes - */ -use Attribute; - -/** - * @since 3.0.0 - */ -#[Attribute(Attribute::TARGET_PARAMETER)] -final class RequestEntity -{ - - /** - * Constructor of the class - * - * @param string|null $em - * @param string $findBy - * @param string|null $paramKey - * @param array $criteria - */ - public function __construct( - public ?string $em = null, - public string $findBy = 'id', - public ?string $paramKey = null, - public array $criteria = [] - ) { - } -} diff --git a/src/Annotation/RequestQuery.php b/src/Annotation/RequestQuery.php index e2aa1e52..a064389b 100644 --- a/src/Annotation/RequestQuery.php +++ b/src/Annotation/RequestQuery.php @@ -1,4 +1,4 @@ -"), - * @Attribute("consumes", type="array"), - * @Attribute("produces", type="array"), - * @Attribute("middlewares", type="array"), - * @Attribute("attributes", type="array"), - * @Attribute("summary", type="string"), - * @Attribute("description", type="string"), - * @Attribute("tags", type="array"), - * @Attribute("priority", type="integer"), - * }) - */ -#[Attribute(Attribute::TARGET_CLASS|Attribute::TARGET_METHOD)] +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class Route implements RequestMethodInterface { /** - * The descriptor holder - * - * @var class-string|array{0: class-string, 1: non-empty-string}|null + * The annotation's holder * * @internal */ - public $holder = null; - - /** - * The route name - * - * @var string - */ - public string $name; - - /** - * The route host - * - * @var string|null - */ - public ?string $host; - - /** - * The route path - * - * @var string - */ - public string $path; - - /** - * The route methods - * - * @var list - */ - public array $methods; - - /** - * The route's consumed content types - * - * @var list - * - * @since 3.0.0 - */ - public array $consumes; - - /** - * The route's produced content types - * - * @var list - * - * @since 3.0.0 - */ - public array $produces; - - /** - * The route middlewares - * - * @var list> - */ - public array $middlewares; - - /** - * The route attributes - * - * @var array - */ - public array $attributes; - - /** - * The route summary - * - * @var string - */ - public string $summary; - - /** - * The route description - * - * @var string - */ - public string $description; - - /** - * The route tags - * - * @var list - */ - public array $tags; - - /** - * The route priority - * - * @var int - */ - public int $priority; + public mixed $holder = null; /** * Constructor of the class * - * @param string $name The route name - * @param string|null $host The route host - * @param string $path The route path - * @param string|null $method The route method - * @param list $methods The route methods - * @param list $consumes The route's consumed content types - * @param list $produces The route's produced content types - * @param list> $middlewares The route middlewares - * @param array $attributes The route attributes - * @param string $summary The route summary - * @param string $description The route description - * @param list $tags The route tags - * @param int $priority The route priority (default 0) + * @param non-empty-string $name The route's name + * @param non-empty-string|null $host The route's host + * @param non-empty-string $path The route's path + * @param non-empty-string|null $method The route's method + * @param list $methods The route's methods + * @param list $consumes The route's consumed media types + * @param list $produces The route's produced media types + * @param list> $middlewares The route's middlewares + * @param array $attributes The route's attributes + * @param string $summary The route's summary + * @param string $description The route's description + * @param list $tags The route's tags + * @param int $priority The route's priority (default 0) */ public function __construct( - string $name, - ?string $host = null, - string $path = '/', + public string $name, + public ?string $host = null, + public string $path = '/', ?string $method = null, - array $methods = [], - array $consumes = [], - array $produces = [], - array $middlewares = [], - array $attributes = [], - string $summary = '', - string $description = '', - array $tags = [], - int $priority = 0 + public array $methods = [], + public array $consumes = [], + public array $produces = [], + public array $middlewares = [], + public array $attributes = [], + public string $summary = '', + public string $description = '', + public array $tags = [], + public int $priority = 0, ) { if (isset($method)) { - $methods[] = $method; + $this->methods[] = $method; + } elseif (empty($this->methods)) { + $this->methods[] = self::METHOD_GET; } - - // if no methods are specified, - // such a route is a GET route. - if (empty($methods)) { - $methods[] = self::METHOD_GET; - } - - $this->name = $name; - $this->host = $host; - $this->path = $path; - $this->methods = $methods; - $this->consumes = $consumes; - $this->produces = $produces; - $this->middlewares = $middlewares; - $this->attributes = $attributes; - $this->summary = $summary; - $this->description = $description; - $this->tags = $tags; - $this->priority = $priority; } } diff --git a/src/Annotation/Summary.php b/src/Annotation/Summary.php index c19eaaa0..0e294cea 100644 --- a/src/Annotation/Summary.php +++ b/src/Annotation/Summary.php @@ -1,4 +1,4 @@ -value = $value; } } diff --git a/src/Annotation/Tag.php b/src/Annotation/Tag.php index 8af2e525..8cbe90f2 100644 --- a/src/Annotation/Tag.php +++ b/src/Annotation/Tag.php @@ -1,4 +1,4 @@ -value = $value; } } diff --git a/src/Annotation/Version.php b/src/Annotation/Version.php new file mode 100644 index 00000000..8d018ab9 --- /dev/null +++ b/src/Annotation/Version.php @@ -0,0 +1,33 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Annotation; + +use Attribute; + +/** + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] +final class Version +{ + + /** + * Constructor of the class + * + * @param non-empty-string $value + */ + public function __construct(public string $value) + { + } +} diff --git a/src/ClassResolver.php b/src/ClassResolver.php index e3d14e26..24e45e30 100644 --- a/src/ClassResolver.php +++ b/src/ClassResolver.php @@ -1,4 +1,4 @@ -, T> */ private array $resolvedClasses = []; /** - * The resolver's parameter resolutioner - * * @var ParameterResolutionerInterface */ private ParameterResolutionerInterface $parameterResolutioner; diff --git a/src/ClassResolverInterface.php b/src/ClassResolverInterface.php index c76bab7c..164b08d6 100644 --- a/src/ClassResolverInterface.php +++ b/src/ClassResolverInterface.php @@ -1,4 +1,4 @@ -router = $router; } /** @@ -72,12 +56,11 @@ public function __construct(?Router $router = null) protected function getRouter(): Router { if (!isset($this->router)) { - throw new LogicException(sprintf( + throw new LogicException(\sprintf( 'The %2$s() method MUST return the %1$s class instance. ' . - 'Pass the %1$s class instance to the constructor ' . - 'or override the %2$s() method.', + 'Pass the %1$s class instance to the constructor or override the %2$s() method.', Router::class, - __METHOD__ + __METHOD__, )); } @@ -110,14 +93,14 @@ final protected function execute(InputInterface $input, OutputInterface $output) foreach ($this->getRouter()->getRoutes()->all() as $route) { $table->addRow([ $route->getName(), - $route->getHost() ?? 'ANY', + $route->getHost() ?? '*', path_plain($route->getPath()), - join(', ', $route->getMethods()), + \join(', ', $route->getMethods()), ]); } $table->render(); - return 0; + return self::SUCCESS; } } diff --git a/src/Entity/IpAddress.php b/src/Entity/IpAddress.php index 0b6c6ec5..4e5477a8 100644 --- a/src/Entity/IpAddress.php +++ b/src/Entity/IpAddress.php @@ -1,4 +1,4 @@ - $proxies The list of proxies in front of this IP address */ - public function __construct(string $value) + public function __construct(private string $value, private array $proxies = []) { - $this->value = $value; } /** * Gets the IP address value * - * @return string + * @return non-empty-string */ public function getValue(): string { @@ -62,89 +42,12 @@ public function getValue(): string } /** - * Checks if the IP address is valid - * - * @return bool - */ - public function isValid(): bool - { - return false !== filter_var($this->value, FILTER_VALIDATE_IP); - } - - /** - * Checks if the IP address is IPv4 - * - * @return bool - */ - public function isVersion4(): bool - { - return false !== filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); - } - - /** - * Checks if the IP address is IPv6 - * - * @return bool - */ - public function isVersion6(): bool - { - return false !== filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6); - } - - /** - * Checks if the IP address is in the private range - * - * @return bool - */ - public function isInPrivateRange(): bool - { - if (!$this->isValid()) { - return false; - } - - return false === filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE); - } - - /** - * Checks if the IP address is in the reserved range - * - * @return bool - */ - public function isInReservedRange(): bool - { - if (!$this->isValid()) { - return false; - } - - return false === filter_var($this->value, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE); - } - - /** - * Converts the IP address to an integer if it possible + * Gets the list of proxies in front of this IP address * - * @return int|null + * @return list */ - public function toLong(): ?int + public function getProxies(): array { - if (!$this->isVersion4()) { - return null; - } - - $long = ip2long($this->value); - if ($long === false) { - return null; - } - - return $long; - } - - /** - * Converts the object to a string - * - * @return string - */ - public function __toString(): string - { - return $this->value; + return $this->proxies; } } diff --git a/src/Entity/MediaType.php b/src/Entity/MediaType.php new file mode 100644 index 00000000..87fea982 --- /dev/null +++ b/src/Entity/MediaType.php @@ -0,0 +1,56 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Entity; + +/** + * Media type + * + * @since 3.0.0 + */ +final class MediaType +{ + public const APPLICATION_JSON = 'application/json'; + public const APPLICATION_XML = 'application/xml'; + public const TEXT_XML = 'text/xml'; + + /** + * Constructor of the class + * + * @param non-empty-string $value + * @param array $parameters + */ + public function __construct(private string $value, private array $parameters = []) + { + } + + /** + * Gets the media type value + * + * @return non-empty-string + */ + public function getValue(): string + { + return $this->value; + } + + /** + * Gets the media type parameters + * + * @return array + */ + public function getParameters(): array + { + return $this->parameters; + } +} diff --git a/src/Event/RouteEvent.php b/src/Event/RouteEvent.php index 54942692..45c9b9c0 100644 --- a/src/Event/RouteEvent.php +++ b/src/Event/RouteEvent.php @@ -1,4 +1,4 @@ -route = $route; - $this->request = $request; } /** @@ -62,4 +49,14 @@ public function getRequest(): ServerRequestInterface { return $this->request; } + + /** + * @param ServerRequestInterface $request + * + * @return void + */ + public function setRequest(ServerRequestInterface $request): void + { + $this->request = $request; + } } diff --git a/src/Exception/ClientNotConsumedMediaTypeException.php b/src/Exception/ClientNotConsumedMediaTypeException.php index ec11ce1f..ab2f8af9 100644 --- a/src/Exception/ClientNotConsumedMediaTypeException.php +++ b/src/Exception/ClientNotConsumedMediaTypeException.php @@ -1,4 +1,4 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception; - -/** - * Import classes - */ -use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; - -/** - * InvalidRequestBodyException - * - * @since 3.0.0 - */ -class InvalidRequestBodyException extends HttpBadRequestException -{ -} diff --git a/src/Exception/InvalidRequestPayloadException.php b/src/Exception/InvalidRequestPayloadException.php index 24c8c478..4e6c96c9 100644 --- a/src/Exception/InvalidRequestPayloadException.php +++ b/src/Exception/InvalidRequestPayloadException.php @@ -1,4 +1,4 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Exception; - -/** - * Import classes - */ -use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; - -/** - * InvalidRequestQueryException - * - * @since 3.0.0 - */ -class InvalidRequestQueryException extends HttpBadRequestException -{ -} diff --git a/src/Exception/LogicException.php b/src/Exception/LogicException.php index 17c8238b..6070fa3c 100644 --- a/src/Exception/LogicException.php +++ b/src/Exception/LogicException.php @@ -1,4 +1,4 @@ - */ private array $resources = []; @@ -90,7 +90,7 @@ public function __construct( ?RouteFactoryInterface $routeFactory = null, ?ReferenceResolverInterface $referenceResolver = null, ?ParameterResolutionerInterface $parameterResolutioner = null, - ?ResponseResolutionerInterface $responseResolutioner = null + ?ResponseResolutionerInterface $responseResolutioner = null, ) { $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory(); $this->routeFactory = $routeFactory ?? new RouteFactory(); @@ -100,10 +100,28 @@ public function __construct( $this->referenceResolver = $referenceResolver ?? new ReferenceResolver( $this->parameterResolutioner ??= new ParameterResolutioner(), - $this->responseResolutioner ??= new ResponseResolutioner() + $this->responseResolutioner ??= new ResponseResolutioner(), ); } + /** + * Sets the given container to the loader + * + * @param ContainerInterface $container + * + * @return void + * + * @throws LogicException + * If a custom reference resolver has been set, + * but a parameter resolutioner has not been set. + * + * @since 2.9.0 + */ + public function setContainer(ContainerInterface $container): void + { + $this->addParameterResolver(new DependencyInjectionParameterResolver($container)); + } + /** * Adds the given parameter resolver(s) to the parameter resolutioner * @@ -112,7 +130,8 @@ public function __construct( * @return void * * @throws LogicException - * If a custom reference resolver was setted and a parameter resolutioner wasn't passed. + * If a custom reference resolver has been set, + * but a parameter resolutioner has not been set. * * @since 3.0.0 */ @@ -121,8 +140,8 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The config route loader cannot accept parameter resolvers ' . - 'because a custom reference resolver was setted ' . - 'and a parameter resolutioner was not passed' + 'because a custom reference resolver has been set, ' . + 'but a parameter resolutioner has not been set.' ); } @@ -137,7 +156,8 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): * @return void * * @throws LogicException - * If a custom reference resolver was setted and a response resolutioner wasn't passed. + * If a custom reference resolver has been set, + * but a response resolutioner has not been set. * * @since 3.0.0 */ @@ -146,8 +166,8 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo if (!isset($this->responseResolutioner)) { throw new LogicException( 'The config route loader cannot accept response resolvers ' . - 'because a custom reference resolver was setted ' . - 'and a response resolutioner was not passed' + 'because a custom reference resolver has been set, ' . + 'but a response resolutioner has not been set.' ); } @@ -156,12 +176,15 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo /** * {@inheritdoc} + * + * @throws InvalidArgumentException + * If the resource isn't valid. */ - public function attach($resource): void + public function attach(mixed $resource): void { if (!is_string($resource)) { throw new InvalidArgumentException( - 'The config route loader only handles string resources' + 'The config route loader only handles string resources.' ); } @@ -181,13 +204,16 @@ public function attach($resource): void throw new InvalidArgumentException(sprintf( 'The config route loader only handles file or directory paths, ' . - 'however the given resource "%s" is not one of them', - $resource + 'however the given resource "%s" is not one of them.', + $resource, )); } /** * {@inheritdoc} + * + * @throws InvalidArgumentException + * If one of the given resources isn't valid. */ public function attachArray(array $resources): void { @@ -205,14 +231,14 @@ public function load(): RouteCollectionInterface $collector = new RouteCollector( $this->collectionFactory, $this->routeFactory, - $this->referenceResolver + $this->referenceResolver, ); - foreach ($this->resources as $filename) { + foreach ($this->resources as $resource) { (function (string $filename): void { /** @psalm-suppress UnresolvableInclude */ require $filename; - })->call($collector, $filename); + })->call($collector, $resource); } return $collector->getCollection(); diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 885dd4ee..00dcfdf0 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -1,4 +1,4 @@ - + * List of classes or directories + * + * @var list */ private array $resources = []; @@ -101,18 +97,13 @@ final class DescriptorLoader implements LoaderInterface */ private ?ResponseResolutionerInterface $responseResolutioner; - /** - * @var AnnotationReaderInterface|null - */ - private ?AnnotationReaderInterface $annotationReader = null; - /** * @var CacheInterface|null */ - private ?CacheInterface $cache = null; + private ?CacheInterface $cache; /** - * @var string|null + * @var non-empty-string|null */ private ?string $cacheKey = null; @@ -124,13 +115,15 @@ final class DescriptorLoader implements LoaderInterface * @param ReferenceResolverInterface|null $referenceResolver * @param ParameterResolutionerInterface|null $parameterResolutioner * @param ResponseResolutionerInterface|null $responseResolutioner + * @param CacheInterface|null $cache */ public function __construct( ?RouteCollectionFactoryInterface $collectionFactory = null, ?RouteFactoryInterface $routeFactory = null, ?ReferenceResolverInterface $referenceResolver = null, ?ParameterResolutionerInterface $parameterResolutioner = null, - ?ResponseResolutionerInterface $responseResolutioner = null + ?ResponseResolutionerInterface $responseResolutioner = null, + ?CacheInterface $cache = null, ) { $this->collectionFactory = $collectionFactory ?? new RouteCollectionFactory(); $this->routeFactory = $routeFactory ?? new RouteFactory(); @@ -140,8 +133,26 @@ public function __construct( $this->referenceResolver = $referenceResolver ?? new ReferenceResolver( $this->parameterResolutioner ??= new ParameterResolutioner(), - $this->responseResolutioner ??= new ResponseResolutioner() + $this->responseResolutioner ??= new ResponseResolutioner(), ); + + $this->cache = $cache; + } + + /** + * Sets the given container to the loader + * + * @param ContainerInterface $container + * + * @return void + * + * @throws LogicException + * If a custom reference resolver has been set, + * but a parameter resolutioner has not been set. + */ + public function setContainer(ContainerInterface $container): void + { + $this->addParameterResolver(new DependencyInjectionParameterResolver($container)); } /** @@ -152,7 +163,8 @@ public function __construct( * @return void * * @throws LogicException - * If a custom reference resolver was setted and a parameter resolutioner wasn't passed. + * If a custom reference resolver has been set, + * but a parameter resolutioner has not been set. * * @since 3.0.0 */ @@ -161,8 +173,8 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The descriptor route loader cannot accept parameter resolvers ' . - 'because a custom reference resolver was setted ' . - 'and a parameter resolutioner was not passed' + 'because a custom reference resolver has been set, ' . + 'but a parameter resolutioner has not been set.' ); } @@ -177,7 +189,8 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): * @return void * * @throws LogicException - * If a custom reference resolver was setted and a response resolutioner wasn't passed. + * If a custom reference resolver has been set, + * but a response resolutioner has not been set. * * @since 3.0.0 */ @@ -186,8 +199,8 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo if (!isset($this->responseResolutioner)) { throw new LogicException( 'The descriptor route loader cannot accept response resolvers ' . - 'because a custom reference resolver was setted ' . - 'and a response resolutioner was not passed' + 'because a custom reference resolver has been set, ' . + 'but a response resolutioner has not been set.' ); } @@ -195,40 +208,15 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo } /** - * Uses the default annotation reader - * - * @return void - * - * @throws LogicException - * If the "doctrine/annotations" package isn't installed. - * - * @since 3.0.0 - */ - public function useDefaultAnnotationReader(): void - { - if (!class_exists(AnnotationReader::class)) { - throw new LogicException( - 'The descriptor route loader cannot use the default annotation reader ' . - 'because the annotation reading logic requires the "doctrine/annotations" package, ' . - 'run the "composer install doctrine/annotations" command and try again' - ); - } - - $this->annotationReader = new AnnotationReader(); - } - - /** - * Sets the given annotation reader to the loader + * Sets the given cache to the loader * - * @param AnnotationReaderInterface|null $annotationReader + * @param CacheInterface|null $cache * * @return void - * - * @since 3.0.0 */ - public function setAnnotationReader(?AnnotationReaderInterface $annotationReader): void + public function setCache(?CacheInterface $cache): void { - $this->annotationReader = $annotationReader; + $this->cache = $cache; } /** @@ -242,77 +230,61 @@ public function getCache(): ?CacheInterface } /** - * Sets the given cache to the loader + * Sets the given cache key to the loader * - * @param CacheInterface|null $cache + * @param non-empty-string|null $cacheKey * * @return void - */ - public function setCache(?CacheInterface $cache): void - { - $this->cache = $cache; - } - - /** - * Gets the loader cache key - * - * @return string * * @since 2.10.0 */ - public function getCacheKey(): string + public function setCacheKey(?string $cacheKey): void { - return $this->cacheKey ??= hash('md5', 'router:descriptors'); + $this->cacheKey = $cacheKey; } /** - * Sets the given cache key to the loader - * - * @param string|null $cacheKey + * Gets the loader cache key * - * @return void + * @return non-empty-string * * @since 2.10.0 */ - public function setCacheKey(?string $cacheKey): void + public function getCacheKey(): string { - $this->cacheKey = $cacheKey; + return $this->cacheKey ??= hash('md5', __METHOD__); } /** * {@inheritdoc} + * + * @throws InvalidArgumentException + * If the resource isn't valid. */ - public function attach($resource): void + public function attach(mixed $resource): void { if (!is_string($resource)) { throw new InvalidArgumentException( - 'The descriptor route loader only handles string resources' + 'The descriptor route loader only handles string resources.' ); } - if (is_dir($resource)) { - $classnames = get_dir_classes($resource); - foreach ($classnames as $classname) { - $this->resources[] = $classname; - } - - return; + if (!class_exists($resource) && !is_dir($resource)) { + throw new InvalidArgumentException(sprintf( + 'The descriptor route loader only handles class names or directory paths, ' . + 'however the given resource "%s" is not one of them.', + $resource, + )); } - if (class_exists($resource)) { - $this->resources[] = $resource; - return; - } - - throw new InvalidArgumentException(sprintf( - 'The descriptor route loader only handles class names or directory paths, ' . - 'however the given resource "%s" is not one of them', - $resource - )); + $this->resources[] = $resource; } /** * {@inheritdoc} + * + * @throws InvalidArgumentException + * If one of the given resources isn't valid. */ public function attachArray(array $resources): void { @@ -327,7 +299,7 @@ public function attachArray(array $resources): void */ public function load(): RouteCollectionInterface { - $routes = []; + $routes = $this->collectionFactory->createCollection(); $descriptors = $this->getDescriptors(); foreach ($descriptors as $descriptor) { $route = $this->routeFactory->createRoute( @@ -336,7 +308,7 @@ public function load(): RouteCollectionInterface $descriptor->methods, $this->referenceResolver->resolveRequestHandler($descriptor->holder), $this->referenceResolver->resolveMiddlewares($descriptor->middlewares), - $descriptor->attributes + $descriptor->attributes, ); $route->setHost($descriptor->host); @@ -346,193 +318,222 @@ public function load(): RouteCollectionInterface $route->setDescription($descriptor->description); $route->setTags(...$descriptor->tags); - $routes[] = $route; + $routes->add($route); } - return $this->collectionFactory->createCollection(...$routes); + return $routes; } /** * Gets descriptors from the cache if they are stored in it, * otherwise collects them from the loader resources, - * then tries to cache and return them + * then tries to cache and return them. * * @return list */ private function getDescriptors(): array { - $key = $this->getCacheKey(); + $cacheKey = $this->getCacheKey(); - if (isset($this->cache) && $this->cache->has($key)) { + if (isset($this->cache) && $this->cache->has($cacheKey)) { /** @var list */ - return $this->cache->get($key); + return $this->cache->get($cacheKey); } $result = []; - foreach ($this->resources as $resource) { - $descriptors = $this->getClassDescriptors( - new ReflectionClass($resource) - ); - + $descriptors = $this->getResourceDescriptors($resource); foreach ($descriptors as $descriptor) { $result[] = $descriptor; } } - usort($result, static function (Route $a, Route $b): int { - return $b->priority <=> $a->priority; - }); + usort($result, static fn(Route $a, Route $b): int => $b->priority <=> $a->priority); if (isset($this->cache)) { - $this->cache->set($key, $result); + $this->cache->set($cacheKey, $result); } return $result; } + /** + * Gets descriptors from the given resource + * + * @param string $resource + * + * @return iterable + */ + private function getResourceDescriptors(string $resource): iterable + { + if (class_exists($resource)) { + yield from $this->getClassDescriptors(new ReflectionClass($resource)); + } + + if (is_dir($resource)) { + foreach ($this->getDirectoryClasses($resource) as $class) { + yield from $this->getClassDescriptors($class); + } + } + } + /** * Gets descriptors from the given class * * @param ReflectionClass $class * - * @return list + * @return iterable */ - private function getClassDescriptors(ReflectionClass $class): array + private function getClassDescriptors(ReflectionClass $class): iterable { - // e.g., interfaces, traits, enums, abstract classes, - // classes with private constructor... if (!$class->isInstantiable()) { - return []; + return; } - $result = []; - if ($class->isSubclassOf(RequestHandlerInterface::class)) { - $annotations = $this->getClassOrMethodAnnotations($class, Route::class); + $annotations = $this->getAnnotations(Route::class, $class); if (isset($annotations[0])) { $descriptor = $annotations[0]; $descriptor->holder = $class->getName(); $this->supplementDescriptor($descriptor, $class); - $result[] = $descriptor; + yield $descriptor; } } - foreach ($class->getMethods() as $method) { - // ignore non-public methods... - if (!$method->isPublic()) { + foreach ($class->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + // Statical methods must be ignored... + if ($method->isStatic()) { continue; } - $annotations = $this->getClassOrMethodAnnotations($method, Route::class); + $annotations = $this->getAnnotations(Route::class, $method); if (isset($annotations[0])) { $descriptor = $annotations[0]; $descriptor->holder = [$class->getName(), $method->getName()]; $this->supplementDescriptor($descriptor, $class); $this->supplementDescriptor($descriptor, $method); - $result[] = $descriptor; + yield $descriptor; } } - - return $result; - } - - /** - * Gets annotations from the given class or method - * - * @param ReflectionClass|ReflectionMethod $classOrMethod - * @param class-string $annotationName - * - * @return list - * - * @template T - */ - private function getClassOrMethodAnnotations(Reflector $classOrMethod, string $annotationName): array - { - $result = []; - - if (PHP_MAJOR_VERSION === 8) { - /** @var ReflectionAttribute[] */ - $attributes = $classOrMethod->getAttributes($annotationName); - foreach ($attributes as $attribute) { - /** @var T */ - $result[] = $attribute->newInstance(); - } - } - - if (isset($this->annotationReader)) { - $annotations = ($classOrMethod instanceof ReflectionClass) ? - $this->annotationReader->getClassAnnotations($classOrMethod) : - $this->annotationReader->getMethodAnnotations($classOrMethod); - - foreach ($annotations as $annotation) { - if ($annotation instanceof $annotationName) { - $result[] = $annotation; - } - } - } - - return $result; } /** * Supplements the given descriptor from the given class or method * * @param Route $descriptor - * @param ReflectionClass|ReflectionMethod $classOrMethod + * @param ReflectionClass|ReflectionMethod $holder * * @return void */ - private function supplementDescriptor(Route $descriptor, Reflector $classOrMethod): void + private function supplementDescriptor(Route $descriptor, ReflectionClass|ReflectionMethod $holder): void { - $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Host::class); + $annotations = $this->getAnnotations(Host::class, $holder); if (isset($annotations[0])) { $descriptor->host = $annotations[0]->value; } - $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Prefix::class); + $annotations = $this->getAnnotations(Prefix::class, $holder); if (isset($annotations[0])) { $descriptor->path = $annotations[0]->value . $descriptor->path; } - $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Postfix::class); + $annotations = $this->getAnnotations(Postfix::class, $holder); if (isset($annotations[0])) { - $descriptor->path = $descriptor->path . $annotations[0]->value; + $descriptor->path .= $annotations[0]->value; } - $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Method::class); + $annotations = $this->getAnnotations(Method::class, $holder); foreach ($annotations as $annotation) { $descriptor->methods[] = $annotation->value; } - $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Consume::class); + $annotations = $this->getAnnotations(Consume::class, $holder); foreach ($annotations as $annotation) { $descriptor->consumes[] = $annotation->value; } - $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Produce::class); + $annotations = $this->getAnnotations(Produce::class, $holder); foreach ($annotations as $annotation) { $descriptor->produces[] = $annotation->value; } - $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Middleware::class); + $annotations = $this->getAnnotations(Middleware::class, $holder); foreach ($annotations as $annotation) { $descriptor->middlewares[] = $annotation->value; } - $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Summary::class); + $annotations = $this->getAnnotations(Summary::class, $holder); foreach ($annotations as $annotation) { $descriptor->summary .= $annotation->value; } - $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Description::class); + $annotations = $this->getAnnotations(Description::class, $holder); foreach ($annotations as $annotation) { $descriptor->description .= $annotation->value; } - $annotations = $this->getClassOrMethodAnnotations($classOrMethod, Tag::class); + $annotations = $this->getAnnotations(Tag::class, $holder); foreach ($annotations as $annotation) { $descriptor->tags[] = $annotation->value; } } + + /** + * Gets the named annotations from the given class or method + * + * @param class-string $name + * @param ReflectionClass|ReflectionMethod $source + * + * @return list + * + * @template T of object + */ + private function getAnnotations(string $name, ReflectionClass|ReflectionMethod $source): array + { + $result = []; + $attributes = $source->getAttributes($name); + foreach ($attributes as $attribute) { + $result[] = $attribute->newInstance(); + } + + return $result; + } + + /** + * Scans the given directory and returns the found classes + * + * @param string $dirname + * + * @return iterable + */ + private function getDirectoryClasses(string $dirname): iterable + { + /** @var array $filenames */ + $filenames = iterator_to_array( + new RegexIterator( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator( + $dirname, + FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_PATHNAME, + ) + ), + '/\.php$/', + ) + ); + + foreach ($filenames as $filename) { + (static function (string $filename): void { + /** @psalm-suppress UnresolvableInclude */ + require_once $filename; + })($filename); + } + + foreach (get_declared_classes() as $fqn) { + $class = new ReflectionClass($fqn); + $filename = $class->getFileName(); + if (isset($filenames[$filename])) { + yield $class; + } + } + } } diff --git a/src/Loader/LoaderInterface.php b/src/Loader/LoaderInterface.php index be6408b4..3659e862 100644 --- a/src/Loader/LoaderInterface.php +++ b/src/Loader/LoaderInterface.php @@ -1,4 +1,4 @@ - $resources * * @return void - * - * @throws InvalidArgumentException - * If one of the given resources isn't valid. */ public function attachArray(array $resources): void; diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index 18da1c0a..c42a2a38 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -1,4 +1,4 @@ -callback = $callback; - $this->parameterResolutioner = $parameterResolutioner; - $this->responseResolutioner = $responseResolutioner; - } - - /** - * Gets the callback's reflection - * - * @return ReflectionFunction|ReflectionMethod - * - * @since 3.0.0 + * @param T $callback */ - public function getReflection(): ReflectionFunctionAbstract + public function __construct(callable $callback) { - return reflect_callable($this->callback); + $this->callback = $callback; } /** @@ -93,17 +50,6 @@ public function getReflection(): ReflectionFunctionAbstract */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $arguments = $this->parameterResolutioner - ->withContext($request) - ->withPriorityResolver(new KnownTypedParameterResolver(ServerRequestInterface::class, $request)) - ->withPriorityResolver(new KnownTypedParameterResolver(RequestHandlerInterface::class, $handler)) - ->resolveParameters(...$this->getReflection()->getParameters()); - - /** @var mixed */ - $response = ($this->callback)(...$arguments); - - return $this->responseResolutioner - ->withContext($request) - ->resolveResponse($response); + return ($this->callback)($request, $handler); } } diff --git a/src/Middleware/CallbackMiddleware.php b/src/Middleware/CallbackMiddleware.php new file mode 100644 index 00000000..77e10b43 --- /dev/null +++ b/src/Middleware/CallbackMiddleware.php @@ -0,0 +1,90 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\ParameterResolutionerInterface; +use Sunrise\Http\Router\ParameterResolver\PresetTypedParameterResolver; +use Sunrise\Http\Router\ResponseResolutionerInterface; + +use function Sunrise\Http\Router\reflect_callable; + +/** + * CallbackMiddleware + * + * @since 3.0.0 + */ +final class CallbackMiddleware implements MiddlewareInterface +{ + + /** + * The middleware's callback + * + * @var callable + */ + private $callback; + + /** + * The callback's parameter resolutioner + * + * @var ParameterResolutionerInterface + */ + private ParameterResolutionerInterface $parameterResolutioner; + + /** + * The callback's response resolutioner + * + * @var ResponseResolutionerInterface + */ + private ResponseResolutionerInterface $responseResolutioner; + + /** + * Constructor of the class + * + * @param callable $callback + * @param ParameterResolutionerInterface $parameterResolutioner + * @param ResponseResolutionerInterface $responseResolutioner + */ + public function __construct( + callable $callback, + ParameterResolutionerInterface $parameterResolutioner, + ResponseResolutionerInterface $responseResolutioner, + ) { + $this->callback = $callback; + $this->parameterResolutioner = $parameterResolutioner; + $this->responseResolutioner = $responseResolutioner; + } + + /** + * {@inheritdoc} + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + $args = $this->parameterResolutioner + ->withRequest($request) + ->withPriorityResolver( + new PresetTypedParameterResolver(ServerRequestInterface::class, $request), + new PresetTypedParameterResolver(RequestHandlerInterface::class, $handler), + ) + ->resolveParameters(...reflect_callable($this->callback)->getParameters()); + + /** @var mixed $response */ + $response = ($this->callback)(...$args); + + return $this->responseResolutioner->withRequest($request)->resolveResponse($response); + } +} diff --git a/src/Middleware/ClientIpAddressMiddleware.php b/src/Middleware/ClientIpAddressMiddleware.php deleted file mode 100644 index b408548e..00000000 --- a/src/Middleware/ClientIpAddressMiddleware.php +++ /dev/null @@ -1,55 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Middleware; - -/** - * Import classes - */ -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\ServerRequest; - -/** - * ClientIpAddressMiddleware - * - * @since 3.0.0 - */ -final class ClientIpAddressMiddleware implements MiddlewareInterface -{ - - /** - * @var array - */ - private array $proxyChain; - - /** - * Constructor of the class - * - * @param array $proxyChain - */ - public function __construct(array $proxyChain = []) - { - $this->proxyChain = $proxyChain; - } - - /** - * {@inheritdoc} - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - $clientIp = ServerRequest::from($request)->getClientIpAddress($this->proxyChain); - - return $handler->handle($request->withAttribute('@clientIp', $clientIp)); - } -} diff --git a/src/Middleware/CommittingEntityChangesMiddleware.php b/src/Middleware/CommittingEntityChangesMiddleware.php deleted file mode 100644 index 9a27e6c0..00000000 --- a/src/Middleware/CommittingEntityChangesMiddleware.php +++ /dev/null @@ -1,70 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Middleware; - -/** - * Import classes - */ -use Doctrine\Persistence\ManagerRegistry as EntityManagerRegistryInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; - -/** - * CommittingEntityChangesMiddleware - * - * @since 3.0.0 - */ -final class CommittingEntityChangesMiddleware implements MiddlewareInterface -{ - - /** - * @var EntityManagerRegistryInterface - */ - private EntityManagerRegistryInterface $entityManagerRegistry; - - /** - * @var list - */ - private array $entityManagerNames; - - /** - * @param EntityManagerRegistryInterface $entityManagerRegistry - * @param list $entityManagerNames - */ - public function __construct( - EntityManagerRegistryInterface $entityManagerRegistry, - array $entityManagerNames = [] - ) { - $this->entityManagerRegistry = $entityManagerRegistry; - $this->entityManagerNames = $entityManagerNames; - } - - /** - * {@inheritdoc} - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - $response = $handler->handle($request); - - if (empty($this->entityManagerNames)) { - $this->entityManagerRegistry->getManager()->flush(); - } - - foreach ($this->entityManagerNames as $entityManagerName) { - $this->entityManagerRegistry->getManager($entityManagerName)->flush(); - } - - return $response; - } -} diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 9078474f..b7eba786 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -1,4 +1,4 @@ - * * @throws InvalidRequestPayloadException * If the request's payload cannot be decoded. */ - private function decodeRequestPayload(ServerRequestInterface $request): ?array + private function decodeRequestPayload(ServerRequestInterface $request): array { - // https://www.php.net/json.constants - $options = JSON_BIGINT_AS_STRING | JSON_OBJECT_AS_ARRAY | JSON_THROW_ON_ERROR; + $json = $request->getBody()->__toString(); try { - /** @var mixed */ - $result = json_decode($request->getBody()->__toString(), null, 512, $options); + $data = json_decode($json, true, 512, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR); } catch (JsonException $e) { throw new InvalidRequestPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); } - // according to PSR-7 the parsed body can be only an array or an object... - return is_array($result) ? $result : null; + if (!is_array($data)) { + throw new InvalidRequestPayloadException('Unexpected JSON: Expects an array or object.'); + } + + return $data; } } diff --git a/src/Middleware/ParsedBodyWhitespaceStrippingMiddleware.php b/src/Middleware/ParsedBodyWhitespaceStrippingMiddleware.php deleted file mode 100644 index 656cec85..00000000 --- a/src/Middleware/ParsedBodyWhitespaceStrippingMiddleware.php +++ /dev/null @@ -1,62 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Middleware; - -/** - * Import classes - */ -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; - -/** - * Import functions - */ -use function array_walk_recursive; -use function is_array; -use function is_string; -use function trim; - -/** - * ParsedBodyWhitespaceStrippingMiddleware - * - * @since 3.0.0 - */ -final class ParsedBodyWhitespaceStrippingMiddleware implements MiddlewareInterface -{ - - /** - * {@inheritdoc} - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - $parsedBody = $request->getParsedBody(); - - if (empty($parsedBody) || !is_array($parsedBody)) { - return $handler->handle($request); - } - - /** @psalm-suppress MissingClosureParamType, MixedAssignment */ - $walker = static function (&$value): void { - if (is_string($value)) { - $value = trim($value); - } - }; - - array_walk_recursive($parsedBody, $walker); - - return $handler->handle( - $request->withParsedBody($parsedBody) - ); - } -} diff --git a/src/Middleware/UnsafeCallableMiddleware.php b/src/Middleware/UnsafeCallableMiddleware.php deleted file mode 100644 index 430f0149..00000000 --- a/src/Middleware/UnsafeCallableMiddleware.php +++ /dev/null @@ -1,56 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Middleware; - -/** - * Import classes - */ -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; - -/** - * UnsafeCallableMiddleware - * - * @since 3.0.0 - * - * @template T as callable(ServerRequestInterface=, RequestHandlerInterface=): ResponseInterface - */ -final class UnsafeCallableMiddleware implements MiddlewareInterface -{ - - /** - * The middleware callback - * - * @var T - */ - private $callback; - - /** - * Constructor of the class - * - * @param T $callback - */ - public function __construct(callable $callback) - { - $this->callback = $callback; - } - - /** - * {@inheritdoc} - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - return ($this->callback)($request, $handler); - } -} diff --git a/src/Middleware/XmlPayloadDecodingMiddleware.php b/src/Middleware/XmlPayloadDecodingMiddleware.php deleted file mode 100644 index 4c716dd7..00000000 --- a/src/Middleware/XmlPayloadDecodingMiddleware.php +++ /dev/null @@ -1,85 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\Middleware; - -/** - * Import classes - */ -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; -use Sunrise\Http\Router\ServerRequest; -use SimpleXMLElement; -use Throwable; - -/** - * Import functions - */ -use function sprintf; - -/** - * Import constants - */ -use const LIBXML_COMPACT; -use const LIBXML_NONET; -use const LIBXML_NOERROR; -use const LIBXML_NOWARNING; -use const LIBXML_PARSEHUGE; - -/** - * XmlPayloadDecodingMiddleware - * - * @since 3.0.0 - */ -final class XmlPayloadDecodingMiddleware implements MiddlewareInterface -{ - - /** - * {@inheritdoc} - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - if (ServerRequest::from($request)->isXml()) { - $request = $request->withParsedBody( - $this->decodeRequestPayload($request) - ); - } - - return $handler->handle($request); - } - - /** - * Tries to decode the given request's payload - * - * @param ServerRequestInterface $request - * - * @return SimpleXMLElement - * - * @throws InvalidRequestPayloadException - * If the request's payload cannot be decoded. - */ - private function decodeRequestPayload(ServerRequestInterface $request): SimpleXMLElement - { - // https://www.php.net/manual/en/libxml.constants.php - $options = LIBXML_COMPACT | LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING | LIBXML_PARSEHUGE; - - try { - $result = new SimpleXMLElement($request->getBody()->__toString(), $options); - } catch (Throwable $e) { - throw new InvalidRequestPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); - } - - return $result; - } -} diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index 45682616..4c0a3cd9 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -1,4 +1,4 @@ - */ private array $resolvers = []; @@ -48,10 +42,10 @@ final class ParameterResolutioner implements ParameterResolutionerInterface /** * {@inheritdoc} */ - public function withContext($context): ParameterResolutionerInterface + public function withRequest(RequestInterface $request): static { $clone = clone $this; - $clone->context = $context; + $clone->request = $request; return $clone; } @@ -59,7 +53,7 @@ public function withContext($context): ParameterResolutionerInterface /** * {@inheritdoc} */ - public function withPriorityResolver(ParameterResolverInterface ...$resolvers): ParameterResolutionerInterface + public function withPriorityResolver(ParameterResolverInterface ...$resolvers): static { $clone = clone $this; $clone->resolvers = []; @@ -112,8 +106,8 @@ public function resolveParameters(ReflectionParameter ...$parameters): array private function resolveParameter(ReflectionParameter $parameter) { foreach ($this->resolvers as $resolver) { - if ($resolver->supportsParameter($parameter, $this->context)) { - return $resolver->resolveParameter($parameter, $this->context); + if ($resolver->supportsParameter($parameter, $this->request)) { + return $resolver->resolveParameter($parameter, $this->request); } } diff --git a/src/ParameterResolutionerInterface.php b/src/ParameterResolutionerInterface.php index 1d04ea3e..255eca11 100644 --- a/src/ParameterResolutionerInterface.php +++ b/src/ParameterResolutionerInterface.php @@ -1,4 +1,4 @@ - - * List of ready-to-pass arguments. + * @return list List of ready-to-pass arguments. * - * @throws ResolvingParameterException - * If one of the parameters cannot be resolved to an argument. + * @throws ResolvingParameterException If one of the parameters cannot be resolved to an argument. */ public function resolveParameters(ReflectionParameter ...$parameters): array; } diff --git a/src/ParameterResolver/ClientIpAddressParameterResolver.php b/src/ParameterResolver/ClientIpAddressParameterResolver.php deleted file mode 100644 index 321e46fe..00000000 --- a/src/ParameterResolver/ClientIpAddressParameterResolver.php +++ /dev/null @@ -1,77 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\ParameterResolver; - -/** - * Import classes - */ -use Psr\Http\Message\ServerRequestInterface; -use Sunrise\Http\Router\Entity\IpAddress; -use Sunrise\Http\Router\ParameterResolverInterface; -use Sunrise\Http\Router\ServerRequest; -use ReflectionNamedType; -use ReflectionParameter; - -/** - * ClientIpAddressParameterResolver - * - * @since 3.0.0 - */ -final class ClientIpAddressParameterResolver implements ParameterResolverInterface -{ - - /** - * @var array - */ - private array $proxyChain; - - /** - * Constructor of the class - * - * @param array $proxyChain - */ - public function __construct(array $proxyChain = []) - { - $this->proxyChain = $proxyChain; - } - - /** - * {@inheritdoc} - */ - public function supportsParameter(ReflectionParameter $parameter, $context): bool - { - if (!($context instanceof ServerRequestInterface)) { - return false; - } - - if (!($parameter->getType() instanceof ReflectionNamedType)) { - return false; - } - - if (!($parameter->getType()->getName() === IpAddress::class)) { - return false; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function resolveParameter(ReflectionParameter $parameter, $context) - { - /** @var ServerRequestInterface */ - $context = $context; - - return ServerRequest::from($context)->getClientIpAddress($this->proxyChain); - } -} diff --git a/src/ParameterResolver/DependencyInjectionParameterResolver.php b/src/ParameterResolver/DependencyInjectionParameterResolver.php index 38eb7df1..39aa2c2e 100644 --- a/src/ParameterResolver/DependencyInjectionParameterResolver.php +++ b/src/ParameterResolver/DependencyInjectionParameterResolver.php @@ -1,4 +1,4 @@ -getType() instanceof ReflectionNamedType)) { return false; @@ -63,11 +62,8 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} */ - public function resolveParameter(ReflectionParameter $parameter, $context) + public function resolveParameter(ReflectionParameter $parameter, RequestInterface $request): mixed { - /** @var ReflectionNamedType */ - $parameterType = $parameter->getType(); - - return $this->container->get($parameterType->getName()); + return $this->container->get($parameter->getType()->getName()); } } diff --git a/src/ParameterResolver/KnownUntypedParameterResolver.php b/src/ParameterResolver/KnownUntypedParameterResolver.php deleted file mode 100644 index 34c3ffe9..00000000 --- a/src/ParameterResolver/KnownUntypedParameterResolver.php +++ /dev/null @@ -1,71 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\ParameterResolver; - -/** - * Import classes - */ -use Sunrise\Http\Router\ParameterResolverInterface; -use ReflectionParameter; - -/** - * KnownUntypedParameterResolver - * - * @since 3.0.0 - */ -final class KnownUntypedParameterResolver implements ParameterResolverInterface -{ - - /** - * @var string - */ - private string $name; - - /** - * @var mixed - */ - private $value; - - /** - * @param string $name - * @param mixed $value - */ - public function __construct(string $name, $value) - { - $this->name = $name; - $this->value = $value; - } - - /** - * {@inheritdoc} - */ - public function supportsParameter(ReflectionParameter $parameter, $context): bool - { - if ($parameter->hasType()) { - return false; - } - - if (!($parameter->getName() === $this->name)) { - return false; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function resolveParameter(ReflectionParameter $parameter, $context) - { - return $this->value; - } -} diff --git a/src/ParameterResolverInterface.php b/src/ParameterResolver/ParameterResolverInterface.php similarity index 78% rename from src/ParameterResolverInterface.php rename to src/ParameterResolver/ParameterResolverInterface.php index 510d63f7..581631a0 100644 --- a/src/ParameterResolverInterface.php +++ b/src/ParameterResolver/ParameterResolverInterface.php @@ -1,4 +1,4 @@ - - */ - private string $type; - - /** - * @var T - */ - private object $value; - /** * @param class-string $type * @param T $value + * + * @template T of object */ - public function __construct(string $type, object $value) + public function __construct(private string $type, private object $value) { - $this->type = $type; - $this->value = $value; } /** * {@inheritdoc} */ - public function supportsParameter(ReflectionParameter $parameter, $context): bool + public function supportsParameter(ReflectionParameter $parameter, RequestInterface $request): bool { - if (!($parameter->getType() instanceof ReflectionNamedType)) { + $type = $parameter->getType(); + + if (!($type instanceof ReflectionNamedType)) { return false; } - if ($parameter->getType()->isBuiltin()) { + if ($type->isBuiltin()) { return false; } - if (!($parameter->getType()->getName() === $this->type)) { + if (!($type->getName() === $this->type)) { return false; } @@ -71,7 +62,7 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} */ - public function resolveParameter(ReflectionParameter $parameter, $context) + public function resolveParameter(ReflectionParameter $parameter, RequestInterface $request): mixed { return $this->value; } diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index b42ba750..b5061a91 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -1,4 +1,4 @@ -hydrator = $hydrator; - $this->validator = $validator; + public function __construct(private HydratorInterface $hydrator, private ?ValidatorInterface $validator = null) + { } /** * {@inheritdoc} */ - public function supportsParameter(ReflectionParameter $parameter, $context): bool + public function supportsParameter(ReflectionParameter $parameter, RequestInterface $request): bool { - if (!($context instanceof ServerRequestInterface)) { - return false; - } - if (!($parameter->getType() instanceof ReflectionNamedType)) { return false; } @@ -88,11 +57,7 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo return false; } - if (8 === PHP_MAJOR_VERSION && $parameter->getAttributes(RequestBody::class)) { - return true; - } - - if (is_subclass_of($parameter->getType()->getName(), RequestBodyInterface::class)) { + if ($parameter->getAttributes(RequestBody::class)) { return true; } @@ -102,29 +67,26 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} * - * @throws InvalidRequestBodyException - * If the request body structure isn't valid. + * @throws UnhydrableObjectException + * If an object isn't valid. * * @throws UnprocessableRequestBodyException * If the request body data isn't valid. - * - * @throws UnhydrableObjectException - * If an object isn't valid. */ - public function resolveParameter(ReflectionParameter $parameter, $context) + public function resolveParameter(ReflectionParameter $parameter, RequestInterface $request): mixed { - /** @var ServerRequestInterface */ - $context = $context; + /** @var ReflectionNamedType $type */ + $type = $parameter->getType(); - /** @var ReflectionNamedType */ - $parameterType = $parameter->getType(); + /** @var class-string $typeName */ + $typeName = $type->getName(); try { - $object = $this->hydrator->hydrate($parameterType->getName(), (array) $context->getParsedBody()); + $object = $this->hydrator->hydrate($typeName, (array) $request->getParsedBody()); } catch (InvalidObjectException $e) { throw new UnhydrableObjectException($e->getMessage(), 0, $e); - } catch (InvalidValueException $e) { - throw new InvalidRequestBodyException($e->getMessage(), 0, $e); + } catch (InvalidDataException $e) { + throw new UnprocessableRequestBodyException($e->getViolations()); } if (isset($this->validator)) { diff --git a/src/ParameterResolver/RequestEntityParameterResolver.php b/src/ParameterResolver/RequestEntityParameterResolver.php deleted file mode 100644 index 6903cd01..00000000 --- a/src/ParameterResolver/RequestEntityParameterResolver.php +++ /dev/null @@ -1,134 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router\ParameterResolver; - -/** - * Import classes - */ -use Doctrine\Persistence\ManagerRegistry as EntityManagerRegistryInterface; -use Psr\Http\Message\ServerRequestInterface; -use Sunrise\Http\Router\Annotation\RequestEntity; -use Sunrise\Http\Router\Exception\EntityNotFoundException; -use Sunrise\Http\Router\Exception\MissingRequestParameterException; -use Sunrise\Http\Router\ParameterResolverInterface; -use ReflectionAttribute; -use ReflectionNamedType; -use ReflectionParameter; - -/** - * Import functions - */ -use function sprintf; - -/** - * RequestEntityParameterResolver - * - * @since 3.0.0 - */ -final class RequestEntityParameterResolver implements ParameterResolverInterface -{ - - /** - * @var EntityManagerRegistryInterface - */ - private EntityManagerRegistryInterface $entityManagerRegistry; - - /** - * @param EntityManagerRegistryInterface $entityManagerRegistry - */ - public function __construct(EntityManagerRegistryInterface $entityManagerRegistry) - { - $this->entityManagerRegistry = $entityManagerRegistry; - } - - /** - * {@inheritdoc} - */ - public function supportsParameter(ReflectionParameter $parameter, $context): bool - { - if (!($context instanceof ServerRequestInterface)) { - return false; - } - - if (!($parameter->getType() instanceof ReflectionNamedType)) { - return false; - } - - if ($parameter->getType()->isBuiltin()) { - return false; - } - - if (8 === PHP_MAJOR_VERSION && $parameter->getAttributes(RequestEntity::class)) { - return true; - } - - return false; - } - - /** - * {@inheritdoc} - * - * @throws MissingRequestParameterException - * If an entity ID was not found in the request parameters. - * - * @throws EntityNotFoundException - * If an entity was not found. - */ - public function resolveParameter(ReflectionParameter $parameter, $context) - { - /** @var ServerRequestInterface */ - $context = $context; - - /** @var ReflectionNamedType */ - $parameterType = $parameter->getType(); - - /** @var non-empty-list */ - $parameterRequestEntityAttributes = $parameter->getAttributes(RequestEntity::class); - - /** @var RequestEntity */ - $requestEntity = $parameterRequestEntityAttributes[0]->newInstance(); - - // if no request parameter key was assigned, the entity field name will be used... - $requestParameterKey = $requestEntity->paramKey ?? $requestEntity->findBy; - - /** @var string|null */ - $entityId = $context->getAttribute($requestParameterKey); - - if (!isset($entityId)) { - throw new MissingRequestParameterException(sprintf( - 'Missing the %s parameter in the request', - $requestParameterKey - )); - } - - $criteria = $requestEntity->criteria; - $criteria[$requestEntity->findBy] = $entityId; - - /** @var class-string */ - $entityName = $parameterType->getName(); - - $entity = $this->entityManagerRegistry - ->getManager($requestEntity->em) - ->getRepository($entityName) - ->findOneBy($criteria); - - if (isset($entity)) { - return $entity; - } - - if ($parameter->allowsNull()) { - return null; - } - - throw new EntityNotFoundException('Entity Not Found'); - } -} diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php index 070daab4..14be1470 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -1,4 +1,4 @@ -hydrator = $hydrator; $this->validator = $validator; } @@ -74,9 +61,9 @@ public function __construct( /** * {@inheritdoc} */ - public function supportsParameter(ReflectionParameter $parameter, $context): bool + public function supportsParameter(ReflectionParameter $parameter, $request): bool { - if (!($context instanceof ServerRequestInterface)) { + if (!($request instanceof ServerRequestInterface)) { return false; } @@ -88,7 +75,7 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo return false; } - if (8 === PHP_MAJOR_VERSION && $parameter->getAttributes(RequestQuery::class)) { + if (PHP_MAJOR_VERSION >= 8 && $parameter->getAttributes(RequestQuery::class)) { return true; } @@ -102,29 +89,22 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} * - * @throws InvalidRequestQueryException - * If the request query structure isn't valid. + * @throws UnhydrableObjectException + * If an object isn't valid. * * @throws UnprocessableRequestQueryException * If the request query data isn't valid. - * - * @throws UnhydrableObjectException - * If an object isn't valid. */ - public function resolveParameter(ReflectionParameter $parameter, $context) + public function resolveParameter(ReflectionParameter $parameter, $request) { - /** @var ServerRequestInterface */ - $context = $context; - - /** @var ReflectionNamedType */ - $parameterType = $parameter->getType(); + /** @var ServerRequestInterface $request */ try { - $object = $this->hydrator->hydrate($parameterType->getName(), $context->getQueryParams()); + $object = $this->hydrator->hydrate($parameter->getType()->getName(), $request->getQueryParams()); } catch (InvalidObjectException $e) { throw new UnhydrableObjectException($e->getMessage(), 0, $e); - } catch (InvalidValueException $e) { - throw new InvalidRequestQueryException($e->getMessage(), 0, $e); + } catch (InvalidDataException $e) { + throw new UnprocessableRequestQueryException($e->getViolations()); } if (isset($this->validator)) { diff --git a/src/ParameterResolver/RequestRouteParameterResolver.php b/src/ParameterResolver/RequestRouteParameterResolver.php index 3db1a9f9..b084b1f6 100644 --- a/src/ParameterResolver/RequestRouteParameterResolver.php +++ b/src/ParameterResolver/RequestRouteParameterResolver.php @@ -1,4 +1,4 @@ -getAttribute(RouteInterface::ATTR_ROUTE) instanceof RouteInterface)) { + if (!($request->getAttribute(RouteInterface::ATTR_ROUTE) instanceof RouteInterface)) { return false; } @@ -55,11 +53,10 @@ public function supportsParameter(ReflectionParameter $parameter, $context): boo /** * {@inheritdoc} */ - public function resolveParameter(ReflectionParameter $parameter, $context) + public function resolveParameter(ReflectionParameter $parameter, $request) { - /** @var ServerRequestInterface */ - $context = $context; + /** @var ServerRequestInterface $request */ - return $context->getAttribute(RouteInterface::ATTR_ROUTE); + return $request->getAttribute(RouteInterface::ATTR_ROUTE); } } diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index f76d2c0e..f9dc58b4 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -1,4 +1,4 @@ -parameterResolutioner, $this->responseResolutioner); + return new CallbackMiddleware($reference, $this->parameterResolutioner, $this->responseResolutioner); } if (is_string($reference) && is_subclass_of($reference, MiddlewareInterface::class)) { @@ -147,7 +137,7 @@ public function resolveMiddleware($reference): MiddlewareInterface $reference[0] = $this->classResolver->resolveClass($reference[0]); } - return new CallableMiddleware( + return new CallbackMiddleware( [$reference[0], $reference[1]], $this->parameterResolutioner, $this->responseResolutioner diff --git a/src/ReferenceResolverInterface.php b/src/ReferenceResolverInterface.php index a4f04b8a..deef0e97 100644 --- a/src/ReferenceResolverInterface.php +++ b/src/ReferenceResolverInterface.php @@ -1,4 +1,4 @@ -callback); } @@ -91,15 +86,13 @@ public function getReflection(): ReflectionFunctionAbstract public function handle(ServerRequestInterface $request): ResponseInterface { $arguments = $this->parameterResolutioner - ->withContext($request) - ->withPriorityResolver(new KnownTypedParameterResolver(ServerRequestInterface::class, $request)) + ->withRequest($request) + ->withPriorityResolver(new PresetTypedParameterResolver(ServerRequestInterface::class, $request)) ->resolveParameters(...$this->getReflection()->getParameters()); - /** @var mixed */ + /** @var mixed $response */ $response = ($this->callback)(...$arguments); - return $this->responseResolutioner - ->withContext($request) - ->resolveResponse($response); + return $this->responseResolutioner->withRequest($request)->resolveResponse($response); } } diff --git a/src/RequestHandler/QueueableRequestHandler.php b/src/RequestHandler/QueueableRequestHandler.php index 00282bff..4d9fab1e 100644 --- a/src/RequestHandler/QueueableRequestHandler.php +++ b/src/RequestHandler/QueueableRequestHandler.php @@ -1,4 +1,4 @@ - */ private SplQueue $queue; /** - * The request handler endpoint - * * @var RequestHandlerInterface */ private RequestHandlerInterface $endpoint; @@ -47,10 +42,7 @@ final class QueueableRequestHandler implements RequestHandlerInterface */ public function __construct(RequestHandlerInterface $endpoint) { - /** @var SplQueue */ - $queue = new SplQueue(); - - $this->queue = $queue; + $this->queue = new SplQueue(); $this->endpoint = $endpoint; } diff --git a/src/RequestHandler/UnsafeCallableRequestHandler.php b/src/RequestHandler/UnsafeCallableRequestHandler.php index 2c08f3be..22e4da2a 100644 --- a/src/RequestHandler/UnsafeCallableRequestHandler.php +++ b/src/RequestHandler/UnsafeCallableRequestHandler.php @@ -1,4 +1,4 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -namespace Sunrise\Http\Router; - -/** - * RequestQueryInterface - * - * @since 3.0.0 - */ -interface RequestQueryInterface -{ -} diff --git a/src/ResponseResolutioner.php b/src/ResponseResolutioner.php index 6422c26f..310f12fb 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolutioner.php @@ -1,4 +1,4 @@ - */ private array $resolvers = []; @@ -48,10 +42,10 @@ final class ResponseResolutioner implements ResponseResolutionerInterface /** * {@inheritdoc} */ - public function withContext($context): ResponseResolutionerInterface + public function withRequest(RequestInterface $context): static { $clone = clone $this; - $clone->context = $context; + $clone->request = $context; return $clone; } @@ -76,28 +70,14 @@ public function resolveResponse($response): ResponseInterface } foreach ($this->resolvers as $resolver) { - if ($resolver->supportsResponse($response, $this->context)) { - return $resolver->resolveResponse($response, $this->context); + if ($resolver->supportsResponse($response, $this->request)) { + return $resolver->resolveResponse($response, $this->request); } } throw new ResolvingResponseException(sprintf( 'Unable to resolve the response {%s}', - $this->stringifyResponse($response) + get_debug_type($response), )); } - - /** - * Stringifies the given raw response - * - * @param mixed $response - * - * @return string - * - * @todo Think about how to display the responder... - */ - private function stringifyResponse($response): string - { - return get_debug_type($response); - } } diff --git a/src/ResponseResolutionerInterface.php b/src/ResponseResolutionerInterface.php index e56ced98..6818d72c 100644 --- a/src/ResponseResolutionerInterface.php +++ b/src/ResponseResolutionerInterface.php @@ -1,4 +1,4 @@ - + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ResponseResolver; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Sunrise\Http\Router\Annotation\ResponseBody; +use ReflectionClass; + +use function is_object; + +/** + * ObjectResponseResolver + * + * @since 3.0.0 + */ +final class ObjectResponseResolver implements ResponseResolverInterface +{ + public function __construct( + private ResponseFactoryInterface $responseFactory, + ) { + } + + /** + * {@inheritdoc} + */ + public function supportsResponse($response, $context): bool + { + if (!is_object($response)) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function resolveResponse($response, $context): ResponseInterface + { + $attributes = (new ReflectionClass($response))->getAttributes(ResponseBody::class); + + return $this->responseFactory->createResponse(200); + } +} diff --git a/src/ResponseResolverInterface.php b/src/ResponseResolver/ResponseResolverInterface.php similarity index 79% rename from src/ResponseResolverInterface.php rename to src/ResponseResolver/ResponseResolverInterface.php index fa18d042..f332668e 100644 --- a/src/ResponseResolverInterface.php +++ b/src/ResponseResolver/ResponseResolverInterface.php @@ -1,4 +1,4 @@ -handle($context); } diff --git a/src/ResponseResolver/StatusCodeResponseResolver.php b/src/ResponseResolver/StatusCodeResponseResolver.php index 064f1e23..7eee9be1 100644 --- a/src/ResponseResolver/StatusCodeResponseResolver.php +++ b/src/ResponseResolver/StatusCodeResponseResolver.php @@ -1,4 +1,4 @@ - * * @since 2.9.0 @@ -55,36 +50,26 @@ class Router implements RequestHandlerInterface, RequestMethodInterface ]; /** - * The router's host table - * * @var HostTable */ private HostTable $hosts; /** - * The router's route collection - * * @var RouteCollectionInterface */ private RouteCollectionInterface $routes; /** - * The router's middlewares - * * @var list */ private array $middlewares = []; /** - * The router's event dispatcher - * * @var EventDispatcherInterface|null */ private ?EventDispatcherInterface $eventDispatcher = null; /** - * The router's matched route - * * @var RouteInterface|null */ private ?RouteInterface $matchedRoute = null; @@ -326,9 +311,9 @@ function (ServerRequestInterface $request): ResponseInterface { $this->matchedRoute = $this->match($request); if (isset($this->eventDispatcher)) { - $this->eventDispatcher->dispatch( - new RouteEvent($this->matchedRoute, $request) - ); + $event = new RouteEvent($this->matchedRoute, $request); + $this->eventDispatcher->dispatch($event); + $request = $event->getRequest(); } return $this->matchedRoute->handle($request); @@ -353,9 +338,9 @@ public function handle(ServerRequestInterface $request): ResponseInterface $this->matchedRoute = $this->match($request); if (isset($this->eventDispatcher)) { - $this->eventDispatcher->dispatch( - new RouteEvent($this->matchedRoute, $request) - ); + $event = new RouteEvent($this->matchedRoute, $request); + $this->eventDispatcher->dispatch($event); + $request = $event->getRequest(); } if (empty($this->middlewares)) { diff --git a/src/RouterBuilder.php b/src/RouterBuilder.php index 94edd9bb..4ac1e5cb 100644 --- a/src/RouterBuilder.php +++ b/src/RouterBuilder.php @@ -1,4 +1,4 @@ -request = $request; } /** - * Creates the class from the given request + * Creates the proxy from the given request * * @param ServerRequestInterface $request * @@ -76,7 +74,7 @@ public static function from(ServerRequestInterface $request): self public function isJson(): bool { return $this->clientProducesMediaType([ - 'application/json', + MediaType::APPLICATION_JSON, ]); } @@ -90,48 +88,52 @@ public function isJson(): bool public function isXml(): bool { return $this->clientProducesMediaType([ - 'application/xml', - 'text/xml', + MediaType::APPLICATION_XML, + MediaType::TEXT_XML, ]); } /** * Gets the client's IP address * - * @param array $proxyChain + * @param array $proxyChain * * @return IpAddress */ public function getClientIpAddress(array $proxyChain = []): IpAddress { - $env = $this->request->getServerParams(); + $serverParams = $this->request->getServerParams(); + + /** @var non-empty-string $clientAddress */ + $clientAddress = $serverParams['REMOTE_ADDR'] ?? '::1'; - /** @var string */ - $clientIp = $env['REMOTE_ADDR'] ?? '::1'; + /** @var list $proxyAddresses */ + $proxyAddresses = []; - while (isset($proxyChain[$clientIp])) { - $proxyHeader = $proxyChain[$clientIp]; - unset($proxyChain[$clientIp]); + while (isset($proxyChain[$clientAddress])) { + $proxyHeader = $proxyChain[$clientAddress]; + unset($proxyChain[$clientAddress]); $header = $this->request->getHeaderLine($proxyHeader); if ($header === '') { break; } - $proxiedClientIp = strstr($header, ',', true); - if ($proxiedClientIp === false) { - $proxiedClientIp = $header; + /** @var list $addresses */ + $addresses = preg_split('/\s*,\s*/', $header, -1, PREG_SPLIT_NO_EMPTY); + if ($addresses === []) { + break; } - $proxiedClientIp = trim($proxiedClientIp); - if ($proxiedClientIp === '') { - break; + $clientAddress = array_shift($addresses); + if ($addresses === []) { + continue; } - $clientIp = $proxiedClientIp; + $proxyAddresses = array_merge($proxyAddresses, $addresses); } - return new IpAddress($clientIp); + return new IpAddress($clientAddress, $proxyAddresses); } /** @@ -149,17 +151,17 @@ public function getClientProducedMediaType(): string return ''; } - $mediaType = strstr($header, ';', true); - if ($mediaType === false) { - $mediaType = $header; + $result = strstr($header, ';', true); + if ($result === false) { + $result = $header; } - $mediaType = trim($mediaType); - if ($mediaType === '') { + $result = trim($result); + if ($result === '') { return ''; } - return strtolower($mediaType); + return strtolower($result); } /** @@ -169,7 +171,7 @@ public function getClientProducedMediaType(): string * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1 * @link https://tools.ietf.org/html/rfc7231#section-5.3.2 * - * @return list + * @return array> */ public function getClientConsumedMediaTypes(): array { @@ -178,49 +180,79 @@ public function getClientConsumedMediaTypes(): array return []; } + $accepts = header_accept_like_parse($header); + if (empty($accepts)) { + return []; + } + $result = []; - $accepts = explode(',', $header); - foreach ($accepts as $accept) { - $mediaType = strstr($accept, ';', true); - if ($mediaType === false) { - $mediaType = $accept; - } + foreach ($accepts as $type => $params) { + $result[strtolower($type)] = $params; + } - $mediaType = trim($mediaType); - if ($mediaType === '') { - continue; - } + return $result; + } - if ($mediaType === '*/*') { - return []; - } + /** + * Gets the client's consumed encodings + * + * @return array> + */ + public function getClientConsumedEncodings(): array + { + $header = $this->request->getHeaderLine('Accept-Encoding'); + if ($header === '') { + return []; + } - $result[] = strtolower($mediaType); + $accepts = header_accept_like_parse($header); + if (empty($accepts)) { + return []; } - return $result; + return $accepts; + } + + /** + * Gets the client's consumed languages + * + * @return array> + */ + public function getClientConsumedLanguages(): array + { + $header = $this->request->getHeaderLine('Accept-Language'); + if ($header === '') { + return []; + } + + $accepts = header_accept_like_parse($header); + if (empty($accepts)) { + return []; + } + + return $accepts; } /** * Checks if the client produces one of the given media types * - * @param list $consumedMediaTypes + * @param list $consumes * * @return bool */ - public function clientProducesMediaType(array $consumedMediaTypes): bool + public function clientProducesMediaType(array $consumes): bool { - if ($consumedMediaTypes === []) { + if ($consumes === []) { return true; } - $producedMediaType = $this->getClientProducedMediaType(); - if ($producedMediaType === '') { + $produced = $this->getClientProducedMediaType(); + if ($produced === '') { return false; } - foreach ($consumedMediaTypes as $consumedMediaType) { - if ($this->equalsMediaTypes($consumedMediaType, $producedMediaType)) { + foreach ($consumes as $consumed) { + if (media_types_compare($consumed, $produced)) { return true; } } @@ -231,24 +263,28 @@ public function clientProducesMediaType(array $consumedMediaTypes): bool /** * Checks if the client consumes one of the given media types * - * @param list $producedMediaTypes + * @param list $produces * * @return bool */ - public function clientConsumesMediaType(array $producedMediaTypes): bool + public function clientConsumesMediaType(array $produces): bool { - if ($producedMediaTypes === []) { + if ($produces === []) { + return true; + } + + $consumes = $this->getClientConsumedMediaTypes(); + if ($consumes === []) { return true; } - $consumedMediaTypes = $this->getClientConsumedMediaTypes(); - if ($consumedMediaTypes === []) { + if (isset($consumes['*/*'])) { return true; } - foreach ($producedMediaTypes as $a) { - foreach ($consumedMediaTypes as $b) { - if ($this->equalsMediaTypes($a, $b)) { + foreach ($produces as $a) { + foreach ($consumes as $b => $_) { + if (media_types_compare($a, $b)) { return true; } } @@ -272,7 +308,7 @@ public function equalsMediaTypes(string $a, string $b): bool } $slash = strpos($a, '/'); - if ($slash === false) { + if ($slash === false || !isset($b[$slash]) || $b[$slash] !== '/') { return false; } @@ -299,7 +335,7 @@ public function getProtocolVersion(): string /** * {@inheritdoc} */ - public function withProtocolVersion($version) + public function withProtocolVersion($version): self { $clone = clone $this; $clone->request = $clone->request->withProtocolVersion($version); @@ -342,7 +378,7 @@ public function getHeaderLine($name): string /** * {@inheritdoc} */ - public function withHeader($name, $value) + public function withHeader($name, $value): self { $clone = clone $this; $clone->request = $clone->request->withHeader($name, $value); @@ -353,7 +389,7 @@ public function withHeader($name, $value) /** * {@inheritdoc} */ - public function withAddedHeader($name, $value) + public function withAddedHeader($name, $value): self { $clone = clone $this; $clone->request = $clone->request->withAddedHeader($name, $value); @@ -364,7 +400,7 @@ public function withAddedHeader($name, $value) /** * {@inheritdoc} */ - public function withoutHeader($name) + public function withoutHeader($name): self { $clone = clone $this; $clone->request = $clone->request->withoutHeader($name); @@ -375,7 +411,7 @@ public function withoutHeader($name) /** * {@inheritdoc} */ - public function getBody(): \Psr\Http\Message\StreamInterface + public function getBody(): StreamInterface { return $this->request->getBody(); } @@ -383,7 +419,7 @@ public function getBody(): \Psr\Http\Message\StreamInterface /** * {@inheritdoc} */ - public function withBody(\Psr\Http\Message\StreamInterface $body) + public function withBody(StreamInterface $body): self { $clone = clone $this; $clone->request = $clone->request->withBody($body); @@ -402,7 +438,7 @@ public function getMethod(): string /** * {@inheritdoc} */ - public function withMethod($method) + public function withMethod($method): self { $clone = clone $this; $clone->request = $clone->request->withMethod($method); @@ -413,7 +449,7 @@ public function withMethod($method) /** * {@inheritdoc} */ - public function getUri(): \Psr\Http\Message\UriInterface + public function getUri(): UriInterface { return $this->request->getUri(); } @@ -421,7 +457,7 @@ public function getUri(): \Psr\Http\Message\UriInterface /** * {@inheritdoc} */ - public function withUri(\Psr\Http\Message\UriInterface $uri, $preserveHost = false) + public function withUri(UriInterface $uri, $preserveHost = false): self { $clone = clone $this; $clone->request = $clone->request->withUri($uri, $preserveHost); @@ -440,7 +476,7 @@ public function getRequestTarget(): string /** * {@inheritdoc} */ - public function withRequestTarget($requestTarget) + public function withRequestTarget($requestTarget): self { $clone = clone $this; $clone->request = $clone->request->withRequestTarget($requestTarget); @@ -467,7 +503,7 @@ public function getQueryParams(): array /** * {@inheritdoc} */ - public function withQueryParams(array $query) + public function withQueryParams(array $query): self { $clone = clone $this; $clone->request = $clone->request->withQueryParams($query); @@ -486,7 +522,7 @@ public function getCookieParams(): array /** * {@inheritdoc} */ - public function withCookieParams(array $cookies) + public function withCookieParams(array $cookies): self { $clone = clone $this; $clone->request = $clone->request->withCookieParams($cookies); @@ -505,7 +541,7 @@ public function getUploadedFiles(): array /** * {@inheritdoc} */ - public function withUploadedFiles(array $uploadedFiles) + public function withUploadedFiles(array $uploadedFiles): self { $clone = clone $this; $clone->request = $clone->request->withUploadedFiles($uploadedFiles); @@ -516,7 +552,7 @@ public function withUploadedFiles(array $uploadedFiles) /** * {@inheritdoc} */ - public function getParsedBody() + public function getParsedBody(): mixed { return $this->request->getParsedBody(); } @@ -524,7 +560,7 @@ public function getParsedBody() /** * {@inheritdoc} */ - public function withParsedBody($data) + public function withParsedBody($data): self { $clone = clone $this; $clone->request = $clone->request->withParsedBody($data); @@ -543,7 +579,7 @@ public function getAttributes(): array /** * {@inheritdoc} */ - public function getAttribute($name, $default = null) + public function getAttribute($name, $default = null): mixed { return $this->request->getAttribute($name, $default); } @@ -551,7 +587,7 @@ public function getAttribute($name, $default = null) /** * {@inheritdoc} */ - public function withAttribute($name, $value) + public function withAttribute($name, $value): self { $clone = clone $this; $clone->request = $clone->request->withAttribute($name, $value); @@ -562,7 +598,7 @@ public function withAttribute($name, $value) /** * {@inheritdoc} */ - public function withoutAttribute($name) + public function withoutAttribute($name): self { $clone = clone $this; $clone->request = $clone->request->withoutAttribute($name); diff --git a/tests/Command/RouteListCommandTest.php b/tests/Command/RouteListCommandTest.php index deab9ebf..848b49a3 100644 --- a/tests/Command/RouteListCommandTest.php +++ b/tests/Command/RouteListCommandTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Command; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Command\RouteListCommand; use Sunrise\Http\Router\Router; diff --git a/tests/Exception/BadRequestExceptionTest.php b/tests/Exception/BadRequestExceptionTest.php index 1eb50213..2ab92b5d 100644 --- a/tests/Exception/BadRequestExceptionTest.php +++ b/tests/Exception/BadRequestExceptionTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\Exception; use Sunrise\Http\Router\Exception\BadRequestException; diff --git a/tests/Exception/ExceptionTest.php b/tests/Exception/ExceptionTest.php index 38804ab2..98f513a3 100644 --- a/tests/Exception/ExceptionTest.php +++ b/tests/Exception/ExceptionTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\Exception; use Sunrise\Http\Router\Exception\ExceptionInterface; diff --git a/tests/Exception/InvalidArgumentExceptionTest.php b/tests/Exception/InvalidArgumentExceptionTest.php index 900ff25c..cedf93ee 100644 --- a/tests/Exception/InvalidArgumentExceptionTest.php +++ b/tests/Exception/InvalidArgumentExceptionTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\Exception; use Sunrise\Http\Router\Exception\InvalidArgumentException; diff --git a/tests/Exception/InvalidAttributeValueExceptionTest.php b/tests/Exception/InvalidAttributeValueExceptionTest.php index 93501e05..02bf80e2 100644 --- a/tests/Exception/InvalidAttributeValueExceptionTest.php +++ b/tests/Exception/InvalidAttributeValueExceptionTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\Exception; use Sunrise\Http\Router\Exception\InvalidAttributeValueException; diff --git a/tests/Exception/InvalidLoaderResourceExceptionTest.php b/tests/Exception/InvalidLoaderResourceExceptionTest.php index 098d36dc..72b5e07a 100644 --- a/tests/Exception/InvalidLoaderResourceExceptionTest.php +++ b/tests/Exception/InvalidLoaderResourceExceptionTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\Exception; use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; diff --git a/tests/Exception/InvalidPathExceptionTest.php b/tests/Exception/InvalidPathExceptionTest.php index 4eb999ed..71bfbc73 100644 --- a/tests/Exception/InvalidPathExceptionTest.php +++ b/tests/Exception/InvalidPathExceptionTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\Exception; use Sunrise\Http\Router\Exception\InvalidPathException; diff --git a/tests/Exception/MethodNotAllowedExceptionTest.php b/tests/Exception/MethodNotAllowedExceptionTest.php index dcc651e3..34930bba 100644 --- a/tests/Exception/MethodNotAllowedExceptionTest.php +++ b/tests/Exception/MethodNotAllowedExceptionTest.php @@ -2,16 +2,10 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\Exception; use Sunrise\Http\Router\Exception\MethodNotAllowedException; -/** - * Import functions - */ use function implode; /** diff --git a/tests/Exception/MissingAttributeValueExceptionTest.php b/tests/Exception/MissingAttributeValueExceptionTest.php index 57d1b7df..5380358b 100644 --- a/tests/Exception/MissingAttributeValueExceptionTest.php +++ b/tests/Exception/MissingAttributeValueExceptionTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\Exception; use Sunrise\Http\Router\Exception\MissingAttributeValueException; diff --git a/tests/Exception/PageNotFoundExceptionTest.php b/tests/Exception/PageNotFoundExceptionTest.php index c824a541..7aed6f9e 100644 --- a/tests/Exception/PageNotFoundExceptionTest.php +++ b/tests/Exception/PageNotFoundExceptionTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\PageNotFoundException; use Sunrise\Http\Router\Exception\RouteNotFoundException; diff --git a/tests/Exception/RouteNotFoundExceptionTest.php b/tests/Exception/RouteNotFoundExceptionTest.php index 04164112..1295debf 100644 --- a/tests/Exception/RouteNotFoundExceptionTest.php +++ b/tests/Exception/RouteNotFoundExceptionTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\Exception; use Sunrise\Http\Router\Exception\RouteNotFoundException; diff --git a/tests/Exception/UnresolvableReferenceExceptionTest.php b/tests/Exception/UnresolvableReferenceExceptionTest.php index dd414b4d..c844afa5 100644 --- a/tests/Exception/UnresolvableReferenceExceptionTest.php +++ b/tests/Exception/UnresolvableReferenceExceptionTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\Exception; use Sunrise\Http\Router\Exception\UnresolvableReferenceException; diff --git a/tests/Exception/UnsupportedMediaTypeExceptionTest.php b/tests/Exception/UnsupportedMediaTypeExceptionTest.php index c81ec6ef..375d8141 100644 --- a/tests/Exception/UnsupportedMediaTypeExceptionTest.php +++ b/tests/Exception/UnsupportedMediaTypeExceptionTest.php @@ -2,16 +2,10 @@ namespace Sunrise\Http\Router\Tests\Exception; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\Exception; use Sunrise\Http\Router\Exception\UnsupportedMediaTypeException; -/** - * Import functions - */ use function implode; /** diff --git a/tests/Functions/FunctionPathBuildTest.php b/tests/Functions/FunctionPathBuildTest.php index 7dfd1615..b2c4d303 100644 --- a/tests/Functions/FunctionPathBuildTest.php +++ b/tests/Functions/FunctionPathBuildTest.php @@ -2,16 +2,10 @@ namespace Sunrise\Http\Router\Tests\Functions; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\InvalidAttributeValueException; use Sunrise\Http\Router\Exception\MissingAttributeValueException; -/** - * Import functions - */ use function array_keys; use function Sunrise\Http\Router\path_build; diff --git a/tests/Functions/FunctionPathMatchTest.php b/tests/Functions/FunctionPathMatchTest.php index d5ec5763..bbff7847 100644 --- a/tests/Functions/FunctionPathMatchTest.php +++ b/tests/Functions/FunctionPathMatchTest.php @@ -2,14 +2,8 @@ namespace Sunrise\Http\Router\Tests\Functions; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; -/** - * Import functions - */ use function Sunrise\Http\Router\path_match; /** diff --git a/tests/Functions/FunctionPathParseTest.php b/tests/Functions/FunctionPathParseTest.php index 581c505d..bd24454e 100644 --- a/tests/Functions/FunctionPathParseTest.php +++ b/tests/Functions/FunctionPathParseTest.php @@ -2,16 +2,10 @@ namespace Sunrise\Http\Router\Tests\Functions; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\InvalidPathException; use Sunrise\Http\Router\Router; -/** - * Import functions - */ use function Sunrise\Http\Router\path_parse; use function chr; diff --git a/tests/Functions/FunctionPathPlainTest.php b/tests/Functions/FunctionPathPlainTest.php index 37228af8..7cd4e862 100644 --- a/tests/Functions/FunctionPathPlainTest.php +++ b/tests/Functions/FunctionPathPlainTest.php @@ -2,14 +2,8 @@ namespace Sunrise\Http\Router\Tests\Functions; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; -/** - * Import functions - */ use function Sunrise\Http\Router\path_plain; /** diff --git a/tests/Functions/FunctionPathRegexTest.php b/tests/Functions/FunctionPathRegexTest.php index a31c0fc3..b47e6dcd 100644 --- a/tests/Functions/FunctionPathRegexTest.php +++ b/tests/Functions/FunctionPathRegexTest.php @@ -2,14 +2,8 @@ namespace Sunrise\Http\Router\Tests\Functions; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; -/** - * Import functions - */ use function Sunrise\Http\Router\path_regex; /** diff --git a/tests/Loader/ConfigLoaderTest.php b/tests/Loader/ConfigLoaderTest.php index fa114f86..b3d44c9d 100644 --- a/tests/Loader/ConfigLoaderTest.php +++ b/tests/Loader/ConfigLoaderTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Loader; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\InvalidLoaderResourceException; use Sunrise\Http\Router\Loader\ConfigLoader; diff --git a/tests/Loader/DescriptorLoaderTest.php b/tests/Loader/DescriptorLoaderTest.php index 929e29f9..c897210a 100644 --- a/tests/Loader/DescriptorLoaderTest.php +++ b/tests/Loader/DescriptorLoaderTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Loader; -/** - * Import classes - */ use Doctrine\Common\Annotations\AnnotationRegistry; use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Annotation\Route; @@ -14,9 +11,6 @@ use Sunrise\Http\Router\Tests\Fixtures; use ReflectionClass; -/** - * Import functions - */ use function array_map; use function class_exists; @@ -33,16 +27,6 @@ class DescriptorLoaderTest extends TestCase use Fixtures\CacheAwareTrait; use Fixtures\ContainerAwareTrait; - /** - * {@inheritdoc} - */ - protected function setUp() : void - { - if (class_exists(AnnotationRegistry::class)) { - /** @scrutinizer ignore-deprecated */ AnnotationRegistry::registerLoader('class_exists'); - } - } - /** * @return void */ diff --git a/tests/Middleware/CallableMiddlewareTest.php b/tests/Middleware/CallableMiddlewareTest.php index f1e6b029..5f31f972 100644 --- a/tests/Middleware/CallableMiddlewareTest.php +++ b/tests/Middleware/CallableMiddlewareTest.php @@ -2,13 +2,10 @@ namespace Sunrise\Http\Router\Tests\Middleware; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; -use Sunrise\Http\Router\Middleware\CallableMiddleware; +use Sunrise\Http\Router\Middleware\CallbackMiddleware; use Sunrise\Http\Router\Tests\Fixtures; /** @@ -23,7 +20,7 @@ class CallableMiddlewareTest extends TestCase public function testContracts() : void { $callback = new Fixtures\Middlewares\BlankMiddleware(); - $middleware = new CallableMiddleware($callback); + $middleware = new CallbackMiddleware($callback); $this->assertInstanceOf(MiddlewareInterface::class, $middleware); } @@ -34,7 +31,7 @@ public function testContracts() : void public function testRun() : void { $callback = new Fixtures\Middlewares\BlankMiddleware(); - $middleware = new CallableMiddleware($callback); + $middleware = new CallbackMiddleware($callback); $this->assertSame($callback, $middleware->getCallback()); diff --git a/tests/Middleware/JsonPayloadDecodingMiddlewareTest.php b/tests/Middleware/JsonPayloadDecodingMiddlewareTest.php index 7d4fa750..d3e1e8cc 100644 --- a/tests/Middleware/JsonPayloadDecodingMiddlewareTest.php +++ b/tests/Middleware/JsonPayloadDecodingMiddlewareTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\Middleware; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Psr\Http\Server\MiddlewareInterface; use Sunrise\Http\Router\Exception\UndecodablePayloadException; diff --git a/tests/ReferenceResolverTest.php b/tests/ReferenceResolverTest.php index e3d606de..50f9526a 100644 --- a/tests/ReferenceResolverTest.php +++ b/tests/ReferenceResolverTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\UnresolvableReferenceException; use Sunrise\Http\Router\ReferenceResolver; diff --git a/tests/RequestHandler/CallableRequestHandlerTest.php b/tests/RequestHandler/CallableRequestHandlerTest.php index 5ae1ac19..cfe1791e 100644 --- a/tests/RequestHandler/CallableRequestHandlerTest.php +++ b/tests/RequestHandler/CallableRequestHandlerTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\RequestHandler; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; diff --git a/tests/RequestHandler/QueueableRequestHandlerTest.php b/tests/RequestHandler/QueueableRequestHandlerTest.php index 395e3f89..dddfbcb4 100644 --- a/tests/RequestHandler/QueueableRequestHandlerTest.php +++ b/tests/RequestHandler/QueueableRequestHandlerTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests\RequestHandler; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; diff --git a/tests/RouteCollectionFactoryTest.php b/tests/RouteCollectionFactoryTest.php index 780ed6be..f64349df 100644 --- a/tests/RouteCollectionFactoryTest.php +++ b/tests/RouteCollectionFactoryTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\RouteCollectionFactory; use Sunrise\Http\Router\RouteCollectionFactoryInterface; diff --git a/tests/RouteCollectionTest.php b/tests/RouteCollectionTest.php index 5b17b3c3..aa45725b 100644 --- a/tests/RouteCollectionTest.php +++ b/tests/RouteCollectionTest.php @@ -2,16 +2,10 @@ namespace Sunrise\Http\Router\Tests; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\RouteCollection; use Sunrise\Http\Router\RouteCollectionInterface; -/** - * Import functions - */ use function array_merge; /** diff --git a/tests/RouteCollectorTest.php b/tests/RouteCollectorTest.php index 5247b360..7fc18cbd 100644 --- a/tests/RouteCollectorTest.php +++ b/tests/RouteCollectorTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\RouteCollectionInterface; use Sunrise\Http\Router\RouteCollector; diff --git a/tests/RouteFactoryTest.php b/tests/RouteFactoryTest.php index 804940ef..48730cff 100644 --- a/tests/RouteFactoryTest.php +++ b/tests/RouteFactoryTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\RouteFactory; use Sunrise\Http\Router\RouteFactoryInterface; diff --git a/tests/RouteTest.php b/tests/RouteTest.php index e87dbabb..f5fdd706 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -2,18 +2,12 @@ namespace Sunrise\Http\Router\Tests; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use Sunrise\Http\Router\Route; use Sunrise\Http\Router\RouteInterface; use Sunrise\Http\ServerRequest\ServerRequestFactory; -/** - * Import functions - */ use function array_merge; /** diff --git a/tests/RouterBuilderTest.php b/tests/RouterBuilderTest.php index 953da79d..ef46f86e 100644 --- a/tests/RouterBuilderTest.php +++ b/tests/RouterBuilderTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Sunrise\Http\Router\Exception\RouteNotFoundException; use Sunrise\Http\Router\Router; diff --git a/tests/RouterTest.php b/tests/RouterTest.php index 9870565c..8fa81167 100644 --- a/tests/RouterTest.php +++ b/tests/RouterTest.php @@ -2,9 +2,6 @@ namespace Sunrise\Http\Router\Tests; -/** - * Import classes - */ use PHPUnit\Framework\TestCase; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; @@ -13,7 +10,7 @@ use Sunrise\Http\Router\Exception\MethodNotAllowedException; use Sunrise\Http\Router\Exception\PageNotFoundException; use Sunrise\Http\Router\Exception\RouteNotFoundException; -use Sunrise\Http\Router\Middleware\CallableMiddleware; +use Sunrise\Http\Router\Middleware\CallbackMiddleware; use Sunrise\Http\Router\Loader\LoaderInterface; use Sunrise\Http\Router\Route; use Sunrise\Http\Router\RouteCollection; @@ -22,9 +19,6 @@ use Sunrise\Http\ServerRequest\ServerRequestFactory; use Symfony\Component\EventDispatcher\EventDispatcher; -/** - * Import functions - */ use function array_merge; use function array_unique; @@ -449,7 +443,7 @@ public function testRunWithUnallowedMethod() : void $router = new Router(); $router->addRoute(...$routes); - $router->addMiddleware(new CallableMiddleware(function ($request, $handler) { + $router->addMiddleware(new CallbackMiddleware(function ($request, $handler) { try { return $handler->handle($request); } catch (MethodNotAllowedException $e) { @@ -477,7 +471,7 @@ public function testRunWithUndefinedRoute() : void $router = new Router(); $router->addRoute(...$routes); - $router->addMiddleware(new CallableMiddleware(function ($request, $handler) { + $router->addMiddleware(new CallbackMiddleware(function ($request, $handler) { try { return $handler->handle($request); } catch (RouteNotFoundException $e) { From 8d44899fea536e821086efc5376fbf0a1e79be28 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Mon, 24 Jul 2023 05:18:56 +0200 Subject: [PATCH 073/180] v3 --- composer.json | 10 +- src/ClassResolver.php | 2 +- src/Command/RouteListCommand.php | 4 +- src/Exception/HttpException.php | 4 +- src/Loader/ConfigLoader.php | 6 +- src/Loader/DescriptorLoader.php | 6 +- src/Middleware/CallableMiddleware.php | 2 +- src/Middleware/CallbackMiddleware.php | 8 +- .../JsonPayloadDecodingMiddleware.php | 41 +++++--- .../SimdjsonPayloadDecodingMiddleware.php | 97 +++++++++++++++++++ src/ParameterResolutioner.php | 10 +- .../DependencyInjectionParameterResolver.php | 37 +++---- .../ParameterResolverInterface.php | 10 +- .../RequestBodyParameterResolver.php | 35 ++++--- .../RequestQueryParameterResolver.php | 59 +++++------ .../RequestRouteParameterResolver.php | 29 +++--- ...Resolver.php => TypeParameterResolver.php} | 47 ++++----- src/ReferenceResolver.php | 6 +- src/RequestHandler/CallableRequestHandler.php | 6 +- .../QueueableRequestHandler.php | 2 +- .../UnsafeCallableRequestHandler.php | 2 +- src/ResponseResolutioner.php | 6 +- .../ObjectResponseResolver.php | 4 +- .../RouteResponseResolver.php | 4 +- .../StatusCodeResponseResolver.php | 4 +- src/Route.php | 72 +++++++------- src/RouteCollection.php | 38 ++++---- src/RouteCollectionFactory.php | 2 +- src/RouteFactory.php | 2 +- src/Router.php | 2 +- src/ServerRequest.php | 60 ++++++------ 31 files changed, 358 insertions(+), 259 deletions(-) create mode 100644 src/Middleware/SimdjsonPayloadDecodingMiddleware.php rename src/ParameterResolver/{PresetTypedParameterResolver.php => TypeParameterResolver.php} (50%) diff --git a/composer.json b/composer.json index e9823858..36f25c53 100644 --- a/composer.json +++ b/composer.json @@ -34,16 +34,16 @@ "psr/http-message": "^1.0 || ^2.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "sunrise/hydrator": "^3.0", + "symfony/validator": ">=6.0" }, "require-dev": { "phpunit/phpunit": "^9.6", + "vimeo/psalm": "^5.13", "sunrise/coding-standard": "^1.0", "sunrise/http-message": "^3.0", - "sunrise/hydrator": "^3.0", - "symfony/console": "^5.4", - "symfony/validator": "^5.4", - "vimeo/psalm": "^5.13" + "symfony/console": "^5.4" }, "autoload": { "files": [ diff --git a/src/ClassResolver.php b/src/ClassResolver.php index 24e45e30..96319547 100644 --- a/src/ClassResolver.php +++ b/src/ClassResolver.php @@ -53,7 +53,7 @@ public function __construct(ParameterResolutionerInterface $parameterResolutione } /** - * {@inheritdoc} + * @inheritDoc */ public function resolveClass(string $className): object { diff --git a/src/Command/RouteListCommand.php b/src/Command/RouteListCommand.php index 54135066..bc073ca5 100644 --- a/src/Command/RouteListCommand.php +++ b/src/Command/RouteListCommand.php @@ -68,7 +68,7 @@ protected function getRouter(): Router } /** - * {@inheritdoc} + * @inheritDoc */ protected function configure(): void { @@ -77,7 +77,7 @@ protected function configure(): void } /** - * {@inheritdoc} + * @inheritDoc */ final protected function execute(InputInterface $input, OutputInterface $output): int { diff --git a/src/Exception/HttpException.php b/src/Exception/HttpException.php index e5fb74e1..ced1091f 100644 --- a/src/Exception/HttpException.php +++ b/src/Exception/HttpException.php @@ -54,7 +54,7 @@ public function __construct(int $statusCode, string $message, int $code = 0, ?Th } /** - * {@inheritdoc} + * @inheritDoc */ final public function getStatusCode(): int { @@ -62,7 +62,7 @@ final public function getStatusCode(): int } /** - * {@inheritdoc} + * @inheritDoc */ final public function getHeaderFields(): array { diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 5aef3a7f..69d0f9ae 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -175,7 +175,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo } /** - * {@inheritdoc} + * @inheritDoc * * @throws InvalidArgumentException * If the resource isn't valid. @@ -210,7 +210,7 @@ public function attach(mixed $resource): void } /** - * {@inheritdoc} + * @inheritDoc * * @throws InvalidArgumentException * If one of the given resources isn't valid. @@ -224,7 +224,7 @@ public function attachArray(array $resources): void } /** - * {@inheritdoc} + * @inheritDoc */ public function load(): RouteCollectionInterface { diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 00dcfdf0..183c2b3a 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -256,7 +256,7 @@ public function getCacheKey(): string } /** - * {@inheritdoc} + * @inheritDoc * * @throws InvalidArgumentException * If the resource isn't valid. @@ -281,7 +281,7 @@ public function attach(mixed $resource): void } /** - * {@inheritdoc} + * @inheritDoc * * @throws InvalidArgumentException * If one of the given resources isn't valid. @@ -295,7 +295,7 @@ public function attachArray(array $resources): void } /** - * {@inheritdoc} + * @inheritDoc */ public function load(): RouteCollectionInterface { diff --git a/src/Middleware/CallableMiddleware.php b/src/Middleware/CallableMiddleware.php index c42a2a38..b05f696c 100644 --- a/src/Middleware/CallableMiddleware.php +++ b/src/Middleware/CallableMiddleware.php @@ -46,7 +46,7 @@ public function __construct(callable $callback) } /** - * {@inheritdoc} + * @inheritDoc */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { diff --git a/src/Middleware/CallbackMiddleware.php b/src/Middleware/CallbackMiddleware.php index 77e10b43..963d286f 100644 --- a/src/Middleware/CallbackMiddleware.php +++ b/src/Middleware/CallbackMiddleware.php @@ -18,7 +18,7 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\ParameterResolutionerInterface; -use Sunrise\Http\Router\ParameterResolver\PresetTypedParameterResolver; +use Sunrise\Http\Router\ParameterResolver\TypeParameterResolver; use Sunrise\Http\Router\ResponseResolutionerInterface; use function Sunrise\Http\Router\reflect_callable; @@ -70,15 +70,15 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $args = $this->parameterResolutioner ->withRequest($request) ->withPriorityResolver( - new PresetTypedParameterResolver(ServerRequestInterface::class, $request), - new PresetTypedParameterResolver(RequestHandlerInterface::class, $handler), + new TypeParameterResolver(ServerRequestInterface::class, $request), + new TypeParameterResolver(RequestHandlerInterface::class, $handler), ) ->resolveParameters(...reflect_callable($this->callback)->getParameters()); diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index b7eba786..8cf4c17a 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -13,14 +13,16 @@ namespace Sunrise\Http\Router\Middleware; -use JsonException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; +use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ServerRequest; +use JsonException; +use function extension_loaded; use function is_array; use function json_decode; use function sprintf; @@ -29,7 +31,7 @@ use const JSON_THROW_ON_ERROR; /** - * Middleware for JSON payload decoding + * Middleware for JSON payload decoding using the JSON extension * * @since 2.15.0 * @@ -39,13 +41,30 @@ final class JsonPayloadDecodingMiddleware implements MiddlewareInterface { /** - * {@inheritdoc} + * Constructor of the class + * + * @throws LogicException + * If the JSON extension isn't loaded. + */ + public function __construct() + { + if (!extension_loaded('json')) { + throw new LogicException( + 'The JSON extension is required, run the `pecl install json` command to resolve it.' + ); + } + } + + /** + * @inheritDoc */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (ServerRequest::from($request)->isJson()) { $request = $request->withParsedBody( - $this->decodeRequestPayload($request) + $this->decodeJsonPayload( + $request->getBody()->__toString() + ) ); } @@ -53,25 +72,25 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface } /** - * Tries to decode the given request's payload + * Tries to decode the given JSON payload * - * @param ServerRequestInterface $request + * @param string $json * * @return array * * @throws InvalidRequestPayloadException - * If the request's payload cannot be decoded. + * If the JSON payload cannot be decoded. */ - private function decodeRequestPayload(ServerRequestInterface $request): array + private function decodeJsonPayload(string $json): array { - $json = $request->getBody()->__toString(); - try { $data = json_decode($json, true, 512, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR); } catch (JsonException $e) { - throw new InvalidRequestPayloadException(sprintf('Invalid Payload: %s', $e->getMessage()), 0, $e); + throw new InvalidRequestPayloadException(sprintf('Invalid JSON: %s', $e->getMessage()), 0, $e); } + // According to PSR-7, the data must be an array because + // we're using the 'associative' option when decoding the JSON. if (!is_array($data)) { throw new InvalidRequestPayloadException('Unexpected JSON: Expects an array or object.'); } diff --git a/src/Middleware/SimdjsonPayloadDecodingMiddleware.php b/src/Middleware/SimdjsonPayloadDecodingMiddleware.php new file mode 100644 index 00000000..802ac74f --- /dev/null +++ b/src/Middleware/SimdjsonPayloadDecodingMiddleware.php @@ -0,0 +1,97 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Middleware; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ServerRequest; +use RuntimeException; + +use function extension_loaded; +use function is_array; +use function simdjson_decode; +use function sprintf; + +/** + * Middleware for JSON payload decoding using the Simdjson extension + * + * @since 3.0.0 + * + * @link https://www.php.net/manual/en/book.simdjson.php + */ +final class SimdjsonPayloadDecodingMiddleware implements MiddlewareInterface +{ + + /** + * Constructor of the class + * + * @throws LogicException + * If the Simdjson extension isn't loaded. + */ + public function __construct() + { + if (!extension_loaded('simdjson')) { + throw new LogicException( + 'The Simdjson extension is required, run the `pecl install simdjson` command to resolve it.' + ); + } + } + + /** + * @inheritDoc + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + if (ServerRequest::from($request)->isJson()) { + $request = $request->withParsedBody( + $this->decodeJsonPayload( + $request->getBody()->__toString() + ) + ); + } + + return $handler->handle($request); + } + + /** + * Tries to decode the given JSON payload + * + * @param string $json + * + * @return array + * + * @throws InvalidRequestPayloadException + * If the JSON payload cannot be decoded. + */ + private function decodeJsonPayload(string $json): array + { + try { + $data = simdjson_decode($json, true, 512); + } catch (RuntimeException $e) { + throw new InvalidRequestPayloadException(sprintf('Invalid JSON: %s', $e->getMessage()), 0, $e); + } + + // According to PSR-7, the data must be an array because + // we're using the 'associative' option when decoding the JSON. + if (!is_array($data)) { + throw new InvalidRequestPayloadException('Unexpected JSON: Expects an array or object.'); + } + + return $data; + } +} diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index 4c0a3cd9..44842a30 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -40,7 +40,7 @@ final class ParameterResolutioner implements ParameterResolutionerInterface private array $resolvers = []; /** - * {@inheritdoc} + * @inheritDoc */ public function withRequest(RequestInterface $request): static { @@ -51,7 +51,7 @@ public function withRequest(RequestInterface $request): static } /** - * {@inheritdoc} + * @inheritDoc */ public function withPriorityResolver(ParameterResolverInterface ...$resolvers): static { @@ -70,7 +70,7 @@ public function withPriorityResolver(ParameterResolverInterface ...$resolvers): } /** - * {@inheritdoc} + * @inheritDoc */ public function addResolver(ParameterResolverInterface ...$resolvers): void { @@ -80,7 +80,7 @@ public function addResolver(ParameterResolverInterface ...$resolvers): void } /** - * {@inheritdoc} + * @inheritDoc */ public function resolveParameters(ReflectionParameter ...$parameters): array { @@ -103,7 +103,7 @@ public function resolveParameters(ReflectionParameter ...$parameters): array * @throws ResolvingParameterException * If the parameter cannot be resolved to an argument. */ - private function resolveParameter(ReflectionParameter $parameter) + private function resolveParameter(ReflectionParameter $parameter): mixed { foreach ($this->resolvers as $resolver) { if ($resolver->supportsParameter($parameter, $this->request)) { diff --git a/src/ParameterResolver/DependencyInjectionParameterResolver.php b/src/ParameterResolver/DependencyInjectionParameterResolver.php index 39aa2c2e..fbe69553 100644 --- a/src/ParameterResolver/DependencyInjectionParameterResolver.php +++ b/src/ParameterResolver/DependencyInjectionParameterResolver.php @@ -14,7 +14,7 @@ namespace Sunrise\Http\Router\ParameterResolver; use Psr\Container\ContainerInterface; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use ReflectionNamedType; use ReflectionParameter; @@ -27,43 +27,36 @@ final class DependencyInjectionParameterResolver implements ParameterResolverInt { /** - * @var ContainerInterface - */ - private ContainerInterface $container; - - /** + * Constructor of the class + * * @param ContainerInterface $container */ - public function __construct(ContainerInterface $container) + public function __construct(private ContainerInterface $container) { - $this->container = $container; } /** - * {@inheritdoc} + * @inheritDoc */ - public function supportsParameter(ReflectionParameter $parameter, RequestInterface $request): bool + public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool { - if (!($parameter->getType() instanceof ReflectionNamedType)) { - return false; - } + $type = $parameter->getType(); - if ($parameter->getType()->isBuiltin()) { + if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { return false; } - if (!$this->container->has($parameter->getType()->getName())) { - return false; - } - - return true; + return $this->container->has($type->getName()); } /** - * {@inheritdoc} + * @inheritDoc */ - public function resolveParameter(ReflectionParameter $parameter, RequestInterface $request): mixed + public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed { - return $this->container->get($parameter->getType()->getName()); + /** @var ReflectionNamedType $type */ + $type = $parameter->getType(); + + return $this->container->get($type->getName()); } } diff --git a/src/ParameterResolver/ParameterResolverInterface.php b/src/ParameterResolver/ParameterResolverInterface.php index 581631a0..39b1e0a4 100644 --- a/src/ParameterResolver/ParameterResolverInterface.php +++ b/src/ParameterResolver/ParameterResolverInterface.php @@ -13,7 +13,7 @@ namespace Sunrise\Http\Router\ParameterResolver; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Sunrise\Http\Router\Exception\ResolvingParameterException; use ReflectionParameter; @@ -29,22 +29,22 @@ interface ParameterResolverInterface * Checks if the given parameter is supported * * @param ReflectionParameter $parameter - * @param RequestInterface $request + * @param ServerRequestInterface|null $request * * @return bool */ - public function supportsParameter(ReflectionParameter $parameter, RequestInterface $request): bool; + public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool; /** * Resolves the given parameter to an argument * * @param ReflectionParameter $parameter - * @param RequestInterface $request + * @param ServerRequestInterface|null $request * * @return mixed * * @throws ResolvingParameterException * If the parameter cannot be resolved to an argument. */ - public function resolveParameter(ReflectionParameter $parameter, RequestInterface $request): mixed; + public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed; } diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index b5061a91..0e24286c 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -13,10 +13,7 @@ namespace Sunrise\Http\Router\ParameterResolver; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ServerRequestInterface; -use ReflectionNamedType; -use ReflectionParameter; use Sunrise\Http\Router\Annotation\RequestBody; use Sunrise\Http\Router\Exception\UnhydrableObjectException; use Sunrise\Http\Router\Exception\UnprocessableRequestBodyException; @@ -24,6 +21,8 @@ use Sunrise\Hydrator\Exception\InvalidObjectException; use Sunrise\Hydrator\HydratorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +use ReflectionNamedType; +use ReflectionParameter; /** * RequestBodyParameterResolver @@ -37,6 +36,8 @@ final class RequestBodyParameterResolver implements ParameterResolverInterface { /** + * Constructor of the class + * * @param HydratorInterface $hydrator * @param ValidatorInterface|null $validator */ @@ -45,44 +46,46 @@ public function __construct(private HydratorInterface $hydrator, private ?Valida } /** - * {@inheritdoc} + * @inheritDoc */ - public function supportsParameter(ReflectionParameter $parameter, RequestInterface $request): bool + public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool { - if (!($parameter->getType() instanceof ReflectionNamedType)) { + if ($request === null) { return false; } - if ($parameter->getType()->isBuiltin()) { + $type = $parameter->getType(); + + if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { return false; } - if ($parameter->getAttributes(RequestBody::class)) { - return true; + if ($parameter->getAttributes(RequestBody::class) === []) { + return false; } - return false; + return true; } /** - * {@inheritdoc} + * @inheritDoc * * @throws UnhydrableObjectException * If an object isn't valid. * * @throws UnprocessableRequestBodyException - * If the request body data isn't valid. + * If the request's parsed body isn't valid. */ - public function resolveParameter(ReflectionParameter $parameter, RequestInterface $request): mixed + public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed { /** @var ReflectionNamedType $type */ $type = $parameter->getType(); - /** @var class-string $typeName */ - $typeName = $type->getName(); + /** @var class-string $fqn */ + $fqn = $type->getName(); try { - $object = $this->hydrator->hydrate($typeName, (array) $request->getParsedBody()); + $object = $this->hydrator->hydrate($fqn, (array) $request?->getParsedBody()); } catch (InvalidObjectException $e) { throw new UnhydrableObjectException($e->getMessage(), 0, $e); } catch (InvalidDataException $e) { diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php index 14be1470..a36e1042 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -14,18 +14,15 @@ namespace Sunrise\Http\Router\ParameterResolver; use Psr\Http\Message\ServerRequestInterface; -use ReflectionNamedType; -use ReflectionParameter; use Sunrise\Http\Router\Annotation\RequestQuery; use Sunrise\Http\Router\Exception\UnhydrableObjectException; use Sunrise\Http\Router\Exception\UnprocessableRequestQueryException; -use Sunrise\Http\Router\RequestQueryInterface; use Sunrise\Hydrator\Exception\InvalidDataException; use Sunrise\Hydrator\Exception\InvalidObjectException; use Sunrise\Hydrator\HydratorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; -use function is_subclass_of; -use const PHP_MAJOR_VERSION; +use ReflectionNamedType; +use ReflectionParameter; /** * RequestQueryParameterResolver @@ -39,68 +36,56 @@ final class RequestQueryParameterResolver implements ParameterResolverInterface { /** - * @var HydratorInterface - */ - private HydratorInterface $hydrator; - - /** - * @var ValidatorInterface|null - */ - private ?ValidatorInterface $validator; - - /** + * Constructor of the class + * * @param HydratorInterface $hydrator * @param ValidatorInterface|null $validator */ - public function __construct(HydratorInterface $hydrator, ?ValidatorInterface $validator = null) + public function __construct(private HydratorInterface $hydrator, private ?ValidatorInterface $validator = null) { - $this->hydrator = $hydrator; - $this->validator = $validator; } /** - * {@inheritdoc} + * @inheritDoc */ - public function supportsParameter(ReflectionParameter $parameter, $request): bool + public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool { - if (!($request instanceof ServerRequestInterface)) { + if ($request === null) { return false; } - if (!($parameter->getType() instanceof ReflectionNamedType)) { - return false; - } + $type = $parameter->getType(); - if ($parameter->getType()->isBuiltin()) { + if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { return false; } - if (PHP_MAJOR_VERSION >= 8 && $parameter->getAttributes(RequestQuery::class)) { - return true; - } - - if (is_subclass_of($parameter->getType()->getName(), RequestQueryInterface::class)) { - return true; + if ($parameter->getAttributes(RequestQuery::class) === []) { + return false; } - return false; + return true; } /** - * {@inheritdoc} + * @inheritDoc * * @throws UnhydrableObjectException * If an object isn't valid. * * @throws UnprocessableRequestQueryException - * If the request query data isn't valid. + * If the request's query parameters isn't valid. */ - public function resolveParameter(ReflectionParameter $parameter, $request) + public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed { - /** @var ServerRequestInterface $request */ + /** @var ReflectionNamedType $type */ + $type = $parameter->getType(); + + /** @var class-string $fqn */ + $fqn = $type->getName(); try { - $object = $this->hydrator->hydrate($parameter->getType()->getName(), $request->getQueryParams()); + $object = $this->hydrator->hydrate($fqn, (array) $request?->getQueryParams()); } catch (InvalidObjectException $e) { throw new UnhydrableObjectException($e->getMessage(), 0, $e); } catch (InvalidDataException $e) { diff --git a/src/ParameterResolver/RequestRouteParameterResolver.php b/src/ParameterResolver/RequestRouteParameterResolver.php index b084b1f6..fe64e2ac 100644 --- a/src/ParameterResolver/RequestRouteParameterResolver.php +++ b/src/ParameterResolver/RequestRouteParameterResolver.php @@ -14,9 +14,9 @@ namespace Sunrise\Http\Router\ParameterResolver; use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\RouteInterface; use ReflectionNamedType; use ReflectionParameter; -use Sunrise\Http\Router\RouteInterface; /** * RequestRouteParameterResolver @@ -27,36 +27,35 @@ final class RequestRouteParameterResolver implements ParameterResolverInterface { /** - * {@inheritdoc} + * @inheritDoc */ - public function supportsParameter(ReflectionParameter $parameter, $request): bool + public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool { - if (!($request instanceof ServerRequestInterface)) { + if ($request === null) { return false; } - if (!($parameter->getType() instanceof ReflectionNamedType)) { - return false; - } + $type = $parameter->getType(); - if (!($parameter->getType()->getName() === RouteInterface::class)) { + if (! $type instanceof ReflectionNamedType) { return false; } - if (!($request->getAttribute(RouteInterface::ATTR_ROUTE) instanceof RouteInterface)) { + if (! ($type->getName() === RouteInterface::class)) { return false; } - return true; + /** @var RouteInterface|null $route */ + $route = $request->getAttribute('@route'); + + return isset($route) || $type->allowsNull(); } /** - * {@inheritdoc} + * @inheritDoc */ - public function resolveParameter(ReflectionParameter $parameter, $request) + public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed { - /** @var ServerRequestInterface $request */ - - return $request->getAttribute(RouteInterface::ATTR_ROUTE); + return $request?->getAttribute('@route'); } } diff --git a/src/ParameterResolver/PresetTypedParameterResolver.php b/src/ParameterResolver/TypeParameterResolver.php similarity index 50% rename from src/ParameterResolver/PresetTypedParameterResolver.php rename to src/ParameterResolver/TypeParameterResolver.php index 1188a2ea..9c4b3858 100644 --- a/src/ParameterResolver/PresetTypedParameterResolver.php +++ b/src/ParameterResolver/TypeParameterResolver.php @@ -13,56 +13,59 @@ namespace Sunrise\Http\Router\ParameterResolver; -use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\Exception\LogicException; use ReflectionNamedType; use ReflectionParameter; +use function sprintf; + /** - * PresetTypedParameterResolver - * - * @template T of object + * TypeParameterResolver * * @since 3.0.0 */ -final class PresetTypedParameterResolver implements ParameterResolverInterface +final class TypeParameterResolver implements ParameterResolverInterface { /** - * @param class-string $type - * @param T $value + * Constructor of the class + * + * @param string $type + * @param object $value * - * @template T of object + * @throws LogicException + * If the value isn't an instance of the type. */ public function __construct(private string $type, private object $value) { + if (! $this->value instanceof $this->type) { + throw new LogicException(sprintf( + 'The %1$s value must be an instance of %2$s.', + $this->value::class, + $this->type, + )); + } } /** - * {@inheritdoc} + * @inheritDoc */ - public function supportsParameter(ReflectionParameter $parameter, RequestInterface $request): bool + public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool { $type = $parameter->getType(); - if (!($type instanceof ReflectionNamedType)) { - return false; - } - - if ($type->isBuiltin()) { - return false; - } - - if (!($type->getName() === $this->type)) { + if (! $type instanceof ReflectionNamedType) { return false; } - return true; + return $type->getName() === $this->type; } /** - * {@inheritdoc} + * @inheritDoc */ - public function resolveParameter(ReflectionParameter $parameter, RequestInterface $request): mixed + public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed { return $this->value; } diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index f9dc58b4..ca27e657 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -71,7 +71,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ public function resolveRequestHandler($reference): RequestHandlerInterface { @@ -111,7 +111,7 @@ public function resolveRequestHandler($reference): RequestHandlerInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function resolveMiddleware($reference): MiddlewareInterface { @@ -151,7 +151,7 @@ public function resolveMiddleware($reference): MiddlewareInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function resolveMiddlewares(array $references): array { diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index e61e66f0..a958a179 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -16,7 +16,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\ParameterResolver\PresetTypedParameterResolver; +use Sunrise\Http\Router\ParameterResolver\TypeParameterResolver; use Sunrise\Http\Router\ParameterResolutionerInterface; use Sunrise\Http\Router\ResponseResolutionerInterface; use ReflectionFunction; @@ -81,13 +81,13 @@ public function getReflection(): ReflectionFunction|ReflectionMethod } /** - * {@inheritdoc} + * @inheritDoc */ public function handle(ServerRequestInterface $request): ResponseInterface { $arguments = $this->parameterResolutioner ->withRequest($request) - ->withPriorityResolver(new PresetTypedParameterResolver(ServerRequestInterface::class, $request)) + ->withPriorityResolver(new TypeParameterResolver(ServerRequestInterface::class, $request)) ->resolveParameters(...$this->getReflection()->getParameters()); /** @var mixed $response */ diff --git a/src/RequestHandler/QueueableRequestHandler.php b/src/RequestHandler/QueueableRequestHandler.php index 4d9fab1e..68c59ddf 100644 --- a/src/RequestHandler/QueueableRequestHandler.php +++ b/src/RequestHandler/QueueableRequestHandler.php @@ -61,7 +61,7 @@ public function add(MiddlewareInterface ...$middlewares): void } /** - * {@inheritdoc} + * @inheritDoc */ public function handle(ServerRequestInterface $request): ResponseInterface { diff --git a/src/RequestHandler/UnsafeCallableRequestHandler.php b/src/RequestHandler/UnsafeCallableRequestHandler.php index 22e4da2a..8e44a261 100644 --- a/src/RequestHandler/UnsafeCallableRequestHandler.php +++ b/src/RequestHandler/UnsafeCallableRequestHandler.php @@ -45,7 +45,7 @@ public function __construct(callable $callback) } /** - * {@inheritdoc} + * @inheritDoc */ public function handle(ServerRequestInterface $request): ResponseInterface { diff --git a/src/ResponseResolutioner.php b/src/ResponseResolutioner.php index 310f12fb..f3aac200 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolutioner.php @@ -40,7 +40,7 @@ final class ResponseResolutioner implements ResponseResolutionerInterface private array $resolvers = []; /** - * {@inheritdoc} + * @inheritDoc */ public function withRequest(RequestInterface $context): static { @@ -51,7 +51,7 @@ public function withRequest(RequestInterface $context): static } /** - * {@inheritdoc} + * @inheritDoc */ public function addResolver(ResponseResolverInterface ...$resolvers): void { @@ -61,7 +61,7 @@ public function addResolver(ResponseResolverInterface ...$resolvers): void } /** - * {@inheritdoc} + * @inheritDoc */ public function resolveResponse($response): ResponseInterface { diff --git a/src/ResponseResolver/ObjectResponseResolver.php b/src/ResponseResolver/ObjectResponseResolver.php index d74bf244..bea2558b 100644 --- a/src/ResponseResolver/ObjectResponseResolver.php +++ b/src/ResponseResolver/ObjectResponseResolver.php @@ -33,7 +33,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ public function supportsResponse($response, $context): bool { @@ -45,7 +45,7 @@ public function supportsResponse($response, $context): bool } /** - * {@inheritdoc} + * @inheritDoc */ public function resolveResponse($response, $context): ResponseInterface { diff --git a/src/ResponseResolver/RouteResponseResolver.php b/src/ResponseResolver/RouteResponseResolver.php index 8901e890..adc6bd7d 100644 --- a/src/ResponseResolver/RouteResponseResolver.php +++ b/src/ResponseResolver/RouteResponseResolver.php @@ -26,7 +26,7 @@ final class RouteResponseResolver implements ResponseResolverInterface { /** - * {@inheritdoc} + * @inheritDoc */ public function supportsResponse(mixed $response, mixed $context): bool { @@ -42,7 +42,7 @@ public function supportsResponse(mixed $response, mixed $context): bool } /** - * {@inheritdoc} + * @inheritDoc */ public function resolveResponse(mixed $response, mixed $context): ResponseInterface { diff --git a/src/ResponseResolver/StatusCodeResponseResolver.php b/src/ResponseResolver/StatusCodeResponseResolver.php index 7eee9be1..8f725123 100644 --- a/src/ResponseResolver/StatusCodeResponseResolver.php +++ b/src/ResponseResolver/StatusCodeResponseResolver.php @@ -39,7 +39,7 @@ public function __construct(ResponseFactoryInterface $responseFactory) } /** - * {@inheritdoc} + * @inheritDoc */ public function supportsResponse($response, $context): bool { @@ -47,7 +47,7 @@ public function supportsResponse($response, $context): bool } /** - * {@inheritdoc} + * @inheritDoc */ public function resolveResponse($response, $context): ResponseInterface { diff --git a/src/Route.php b/src/Route.php index ef869f15..388dcee4 100644 --- a/src/Route.php +++ b/src/Route.php @@ -144,7 +144,7 @@ public function __construct( } /** - * {@inheritdoc} + * @inheritDoc */ public function getName(): string { @@ -152,7 +152,7 @@ public function getName(): string } /** - * {@inheritdoc} + * @inheritDoc */ public function getHost(): ?string { @@ -160,7 +160,7 @@ public function getHost(): ?string } /** - * {@inheritdoc} + * @inheritDoc */ public function getPath(): string { @@ -168,7 +168,7 @@ public function getPath(): string } /** - * {@inheritdoc} + * @inheritDoc */ public function getMethods(): array { @@ -176,7 +176,7 @@ public function getMethods(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function getConsumedMediaTypes(): array { @@ -184,7 +184,7 @@ public function getConsumedMediaTypes(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function getProducedMediaTypes(): array { @@ -192,7 +192,7 @@ public function getProducedMediaTypes(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function getRequestHandler(): RequestHandlerInterface { @@ -200,7 +200,7 @@ public function getRequestHandler(): RequestHandlerInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function getMiddlewares(): array { @@ -208,7 +208,7 @@ public function getMiddlewares(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function getAttributes(): array { @@ -216,7 +216,7 @@ public function getAttributes(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function getSummary(): string { @@ -224,7 +224,7 @@ public function getSummary(): string } /** - * {@inheritdoc} + * @inheritDoc */ public function getDescription(): string { @@ -232,7 +232,7 @@ public function getDescription(): string } /** - * {@inheritdoc} + * @inheritDoc */ public function getTags(): array { @@ -240,7 +240,7 @@ public function getTags(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function getHolder(): Reflector { @@ -252,7 +252,7 @@ public function getHolder(): Reflector } /** - * {@inheritdoc} + * @inheritDoc */ public function setName(string $name): RouteInterface { @@ -262,7 +262,7 @@ public function setName(string $name): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setHost(?string $host): RouteInterface { @@ -272,7 +272,7 @@ public function setHost(?string $host): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setPath(string $path): RouteInterface { @@ -282,7 +282,7 @@ public function setPath(string $path): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setMethods(string ...$methods): RouteInterface { @@ -295,7 +295,7 @@ public function setMethods(string ...$methods): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setConsumedMediaTypes(string ...$mediaTypes): RouteInterface { @@ -308,7 +308,7 @@ public function setConsumedMediaTypes(string ...$mediaTypes): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setProducedMediaTypes(string ...$mediaTypes): RouteInterface { @@ -321,7 +321,7 @@ public function setProducedMediaTypes(string ...$mediaTypes): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setRequestHandler(RequestHandlerInterface $requestHandler): RouteInterface { @@ -331,7 +331,7 @@ public function setRequestHandler(RequestHandlerInterface $requestHandler): Rout } /** - * {@inheritdoc} + * @inheritDoc */ public function setMiddlewares(MiddlewareInterface ...$middlewares): RouteInterface { @@ -344,7 +344,7 @@ public function setMiddlewares(MiddlewareInterface ...$middlewares): RouteInterf } /** - * {@inheritdoc} + * @inheritDoc */ public function setAttributes(array $attributes): RouteInterface { @@ -354,7 +354,7 @@ public function setAttributes(array $attributes): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setAttribute(string $name, $value): RouteInterface { @@ -364,7 +364,7 @@ public function setAttribute(string $name, $value): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setSummary(string $summary): RouteInterface { @@ -374,7 +374,7 @@ public function setSummary(string $summary): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setDescription(string $description): RouteInterface { @@ -384,7 +384,7 @@ public function setDescription(string $description): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setTags(string ...$tags): RouteInterface { @@ -397,7 +397,7 @@ public function setTags(string ...$tags): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function addPrefix(string $prefix): RouteInterface { @@ -410,7 +410,7 @@ public function addPrefix(string $prefix): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function addSuffix(string $suffix): RouteInterface { @@ -420,7 +420,7 @@ public function addSuffix(string $suffix): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function addMethod(string ...$methods): RouteInterface { @@ -432,7 +432,7 @@ public function addMethod(string ...$methods): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function addConsumedMediaType(string ...$mediaTypes): RouteInterface { @@ -444,7 +444,7 @@ public function addConsumedMediaType(string ...$mediaTypes): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function addProducedMediaType(string ...$mediaTypes): RouteInterface { @@ -456,7 +456,7 @@ public function addProducedMediaType(string ...$mediaTypes): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterface { @@ -468,7 +468,7 @@ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteInterfa } /** - * {@inheritdoc} + * @inheritDoc */ public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): RouteInterface { @@ -488,7 +488,7 @@ public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): Rout } /** - * {@inheritdoc} + * @inheritDoc */ public function addTag(string ...$tags): RouteInterface { @@ -500,7 +500,7 @@ public function addTag(string ...$tags): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function withAddedAttributes(array $attributes): RouteInterface { @@ -515,7 +515,7 @@ public function withAddedAttributes(array $attributes): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function handle(ServerRequestInterface $request): ResponseInterface { diff --git a/src/RouteCollection.php b/src/RouteCollection.php index f32e913c..730bd99c 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -55,7 +55,7 @@ public function __construct(RouteInterface ...$routes) } /** - * {@inheritdoc} + * @inheritDoc */ public function getIterator(): Iterator { @@ -65,7 +65,7 @@ public function getIterator(): Iterator } /** - * {@inheritdoc} + * @inheritDoc */ public function all(): Iterator { @@ -75,7 +75,7 @@ public function all(): Iterator } /** - * {@inheritdoc} + * @inheritDoc */ public function allOnHost(?string $host): Iterator { @@ -93,7 +93,7 @@ public function allOnHost(?string $host): Iterator } /** - * {@inheritdoc} + * @inheritDoc */ public function has(string $name): bool { @@ -101,7 +101,7 @@ public function has(string $name): bool } /** - * {@inheritdoc} + * @inheritDoc */ public function get(string $name): RouteInterface { @@ -116,7 +116,7 @@ public function get(string $name): RouteInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function add(RouteInterface ...$routes): RouteCollectionInterface { @@ -139,7 +139,7 @@ public function add(RouteInterface ...$routes): RouteCollectionInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setHost(string $host): RouteCollectionInterface { @@ -151,7 +151,7 @@ public function setHost(string $host): RouteCollectionInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function setConsumedMediaTypes(string ...$mediaTypes): RouteCollectionInterface { @@ -163,7 +163,7 @@ public function setConsumedMediaTypes(string ...$mediaTypes): RouteCollectionInt } /** - * {@inheritdoc} + * @inheritDoc */ public function setProducedMediaTypes(string ...$mediaTypes): RouteCollectionInterface { @@ -175,7 +175,7 @@ public function setProducedMediaTypes(string ...$mediaTypes): RouteCollectionInt } /** - * {@inheritdoc} + * @inheritDoc */ public function setAttribute(string $name, $value): RouteCollectionInterface { @@ -187,7 +187,7 @@ public function setAttribute(string $name, $value): RouteCollectionInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function addPrefix(string $prefix): RouteCollectionInterface { @@ -199,7 +199,7 @@ public function addPrefix(string $prefix): RouteCollectionInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function addSuffix(string $suffix): RouteCollectionInterface { @@ -211,7 +211,7 @@ public function addSuffix(string $suffix): RouteCollectionInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function addMethod(string ...$methods): RouteCollectionInterface { @@ -223,7 +223,7 @@ public function addMethod(string ...$methods): RouteCollectionInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function addConsumedMediaType(string ...$mediaTypes): RouteCollectionInterface { @@ -235,7 +235,7 @@ public function addConsumedMediaType(string ...$mediaTypes): RouteCollectionInte } /** - * {@inheritdoc} + * @inheritDoc */ public function addProducedMediaType(string ...$mediaTypes): RouteCollectionInterface { @@ -247,7 +247,7 @@ public function addProducedMediaType(string ...$mediaTypes): RouteCollectionInte } /** - * {@inheritdoc} + * @inheritDoc */ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface { @@ -259,7 +259,7 @@ public function addMiddleware(MiddlewareInterface ...$middlewares): RouteCollect } /** - * {@inheritdoc} + * @inheritDoc */ public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): RouteCollectionInterface { @@ -271,7 +271,7 @@ public function addPriorityMiddleware(MiddlewareInterface ...$middlewares): Rout } /** - * {@inheritdoc} + * @inheritDoc */ public function addTag(string ...$tags): RouteCollectionInterface { @@ -283,7 +283,7 @@ public function addTag(string ...$tags): RouteCollectionInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function count(): int { diff --git a/src/RouteCollectionFactory.php b/src/RouteCollectionFactory.php index c42a6019..3d596070 100644 --- a/src/RouteCollectionFactory.php +++ b/src/RouteCollectionFactory.php @@ -20,7 +20,7 @@ class RouteCollectionFactory implements RouteCollectionFactoryInterface { /** - * {@inheritdoc} + * @inheritDoc */ public function createCollection(RouteInterface ...$routes): RouteCollectionInterface { diff --git a/src/RouteFactory.php b/src/RouteFactory.php index b8a0ec5a..c7c8362b 100644 --- a/src/RouteFactory.php +++ b/src/RouteFactory.php @@ -22,7 +22,7 @@ class RouteFactory implements RouteFactoryInterface { /** - * {@inheritdoc} + * @inheritDoc */ public function createRoute( string $name, diff --git a/src/Router.php b/src/Router.php index 5d29bc27..1d7da240 100644 --- a/src/Router.php +++ b/src/Router.php @@ -331,7 +331,7 @@ function (ServerRequestInterface $request): ResponseInterface { } /** - * {@inheritdoc} + * @inheritDoc */ public function handle(ServerRequestInterface $request): ResponseInterface { diff --git a/src/ServerRequest.php b/src/ServerRequest.php index c4abff4e..afa4fcd1 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -325,7 +325,7 @@ public function equalsMediaTypes(string $a, string $b): bool } /** - * {@inheritdoc} + * @inheritDoc */ public function getProtocolVersion(): string { @@ -333,7 +333,7 @@ public function getProtocolVersion(): string } /** - * {@inheritdoc} + * @inheritDoc */ public function withProtocolVersion($version): self { @@ -344,7 +344,7 @@ public function withProtocolVersion($version): self } /** - * {@inheritdoc} + * @inheritDoc */ public function getHeaders(): array { @@ -352,7 +352,7 @@ public function getHeaders(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function hasHeader($name): bool { @@ -360,7 +360,7 @@ public function hasHeader($name): bool } /** - * {@inheritdoc} + * @inheritDoc */ public function getHeader($name): array { @@ -368,7 +368,7 @@ public function getHeader($name): array } /** - * {@inheritdoc} + * @inheritDoc */ public function getHeaderLine($name): string { @@ -376,7 +376,7 @@ public function getHeaderLine($name): string } /** - * {@inheritdoc} + * @inheritDoc */ public function withHeader($name, $value): self { @@ -387,7 +387,7 @@ public function withHeader($name, $value): self } /** - * {@inheritdoc} + * @inheritDoc */ public function withAddedHeader($name, $value): self { @@ -398,7 +398,7 @@ public function withAddedHeader($name, $value): self } /** - * {@inheritdoc} + * @inheritDoc */ public function withoutHeader($name): self { @@ -409,7 +409,7 @@ public function withoutHeader($name): self } /** - * {@inheritdoc} + * @inheritDoc */ public function getBody(): StreamInterface { @@ -417,7 +417,7 @@ public function getBody(): StreamInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function withBody(StreamInterface $body): self { @@ -428,7 +428,7 @@ public function withBody(StreamInterface $body): self } /** - * {@inheritdoc} + * @inheritDoc */ public function getMethod(): string { @@ -436,7 +436,7 @@ public function getMethod(): string } /** - * {@inheritdoc} + * @inheritDoc */ public function withMethod($method): self { @@ -447,7 +447,7 @@ public function withMethod($method): self } /** - * {@inheritdoc} + * @inheritDoc */ public function getUri(): UriInterface { @@ -455,7 +455,7 @@ public function getUri(): UriInterface } /** - * {@inheritdoc} + * @inheritDoc */ public function withUri(UriInterface $uri, $preserveHost = false): self { @@ -466,7 +466,7 @@ public function withUri(UriInterface $uri, $preserveHost = false): self } /** - * {@inheritdoc} + * @inheritDoc */ public function getRequestTarget(): string { @@ -474,7 +474,7 @@ public function getRequestTarget(): string } /** - * {@inheritdoc} + * @inheritDoc */ public function withRequestTarget($requestTarget): self { @@ -485,7 +485,7 @@ public function withRequestTarget($requestTarget): self } /** - * {@inheritdoc} + * @inheritDoc */ public function getServerParams(): array { @@ -493,7 +493,7 @@ public function getServerParams(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function getQueryParams(): array { @@ -501,7 +501,7 @@ public function getQueryParams(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function withQueryParams(array $query): self { @@ -512,7 +512,7 @@ public function withQueryParams(array $query): self } /** - * {@inheritdoc} + * @inheritDoc */ public function getCookieParams(): array { @@ -520,7 +520,7 @@ public function getCookieParams(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function withCookieParams(array $cookies): self { @@ -531,7 +531,7 @@ public function withCookieParams(array $cookies): self } /** - * {@inheritdoc} + * @inheritDoc */ public function getUploadedFiles(): array { @@ -539,7 +539,7 @@ public function getUploadedFiles(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function withUploadedFiles(array $uploadedFiles): self { @@ -550,7 +550,7 @@ public function withUploadedFiles(array $uploadedFiles): self } /** - * {@inheritdoc} + * @inheritDoc */ public function getParsedBody(): mixed { @@ -558,7 +558,7 @@ public function getParsedBody(): mixed } /** - * {@inheritdoc} + * @inheritDoc */ public function withParsedBody($data): self { @@ -569,7 +569,7 @@ public function withParsedBody($data): self } /** - * {@inheritdoc} + * @inheritDoc */ public function getAttributes(): array { @@ -577,7 +577,7 @@ public function getAttributes(): array } /** - * {@inheritdoc} + * @inheritDoc */ public function getAttribute($name, $default = null): mixed { @@ -585,7 +585,7 @@ public function getAttribute($name, $default = null): mixed } /** - * {@inheritdoc} + * @inheritDoc */ public function withAttribute($name, $value): self { @@ -596,7 +596,7 @@ public function withAttribute($name, $value): self } /** - * {@inheritdoc} + * @inheritDoc */ public function withoutAttribute($name): self { From d8821df9ee83f7f8984d7cbd23a0cc0f3811f8c7 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Mon, 24 Jul 2023 05:19:25 +0200 Subject: [PATCH 074/180] v3 --- src/Annotation/RequestRouteAttribute.php | 33 ++++++++++ ...RequestRouteAttributeParameterResolver.php | 66 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 src/Annotation/RequestRouteAttribute.php create mode 100644 src/ParameterResolver/RequestRouteAttributeParameterResolver.php diff --git a/src/Annotation/RequestRouteAttribute.php b/src/Annotation/RequestRouteAttribute.php new file mode 100644 index 00000000..0a701a83 --- /dev/null +++ b/src/Annotation/RequestRouteAttribute.php @@ -0,0 +1,33 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Annotation; + +use Attribute; + +/** + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_PARAMETER)] +final class RequestRouteAttribute +{ + + /** + * Constructor of the class + * + * @param non-empty-string|null $name + */ + public function __construct(public ?string $name = null) + { + } +} diff --git a/src/ParameterResolver/RequestRouteAttributeParameterResolver.php b/src/ParameterResolver/RequestRouteAttributeParameterResolver.php new file mode 100644 index 00000000..c4755cd2 --- /dev/null +++ b/src/ParameterResolver/RequestRouteAttributeParameterResolver.php @@ -0,0 +1,66 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ParameterResolver; + +use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\Annotation\RequestRouteAttribute; +use Sunrise\Http\Router\RouteInterface; +use ReflectionAttribute; +use ReflectionNamedType; +use ReflectionParameter; + +final class RequestRouteAttributeParameterResolver implements ParameterResolverInterface +{ + + /** + * @inheritDoc + */ + public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool + { + if ($request === null) { + return false; + } + + $type = $parameter->getType(); + + if (! $type instanceof ReflectionNamedType || ! $type->isBuiltin()) { + return false; + } + + if ($parameter->getAttributes(RequestRouteAttribute::class) === []) { + return false; + } + + return true; + } + + /** + * @inheritDoc + */ + public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed + { + /** @var non-empty-list> $annotations */ + $annotations = $parameter->getAttributes(RequestRouteAttribute::class); + + $key = $annotations[0]->newInstance()->name ?? $parameter->getName(); + + /** @var RouteInterface $route */ + $route = $request->getAttribute('@route'); + + /** @var mixed $value */ + $value = $route->getAttributes()[$key] ?? null; + + return $value; + } +} From d559726be833779a291dead75cab377f0bc0da9c Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 30 Jul 2023 10:49:06 +0200 Subject: [PATCH 075/180] v3 --- README.md | 4 +- composer.json | 3 +- functions/reflect_callable.php | 3 +- ...outeAttribute.php => RequestAttribute.php} | 6 +- src/Annotation/ResponseBody.php | 19 ++++ src/Entity/MediaType.php | 2 + src/Exception/ResolvingParameterException.php | 23 ---- src/Exception/ResolvingResponseException.php | 23 ---- src/Loader/ConfigLoader.php | 10 +- src/Loader/DescriptorLoader.php | 12 +-- src/Middleware/CallbackMiddleware.php | 28 +++-- .../JsonPayloadDecodingMiddleware.php | 18 ++-- ... => SimdJsonPayloadDecodingMiddleware.php} | 24 ++--- src/ParameterResolutioner.php | 42 ++++---- src/ParameterResolutionerInterface.php | 17 ++- .../DependencyInjectionParameterResolver.php | 21 ++-- .../DirectInjectionParameterResolver.php | 54 ++++++++++ .../ParameterResolverInterface.php | 24 +---- .../RequestAttributeParameterResolver.php | 56 ++++++++++ .../RequestBodyParameterResolver.php | 67 ++++++------ .../RequestQueryParameterResolver.php | 67 ++++++------ ...RequestRouteAttributeParameterResolver.php | 66 ------------ .../RequestRouteParameterResolver.php | 43 ++++---- .../TypeParameterResolver.php | 72 ------------- src/ReferenceResolver.php | 6 +- src/RequestHandler/CallableRequestHandler.php | 60 ++--------- src/RequestHandler/CallbackRequestHandler.php | 100 ++++++++++++++++++ .../QueueableRequestHandler.php | 28 +++-- .../UnsafeCallableRequestHandler.php | 54 ---------- src/ResponseResolutioner.php | 38 +++---- src/ResponseResolutionerInterface.php | 23 +--- src/ResponseResolver/NullResponseResolver.php | 47 ++++++++ .../ObjectResponseResolver.php | 56 ---------- .../ResponseBodyResponseResolver.php | 88 +++++++++++++++ .../ResponseResolverInterface.php | 22 +--- .../RouteResponseResolver.php | 29 +++-- .../StatusCodeResponseResolver.php | 27 ++--- src/ResponseResolver/UriResponseResolver.php | 49 +++++++++ src/Route.php | 4 +- src/Router.php | 4 +- .../CallableRequestHandlerTest.php | 6 +- tests/RouteTest.php | 6 +- 42 files changed, 671 insertions(+), 680 deletions(-) rename src/Annotation/{RequestRouteAttribute.php => RequestAttribute.php} (79%) delete mode 100644 src/Exception/ResolvingParameterException.php delete mode 100644 src/Exception/ResolvingResponseException.php rename src/Middleware/{SimdjsonPayloadDecodingMiddleware.php => SimdJsonPayloadDecodingMiddleware.php} (76%) create mode 100644 src/ParameterResolver/DirectInjectionParameterResolver.php create mode 100644 src/ParameterResolver/RequestAttributeParameterResolver.php delete mode 100644 src/ParameterResolver/RequestRouteAttributeParameterResolver.php delete mode 100644 src/ParameterResolver/TypeParameterResolver.php create mode 100644 src/RequestHandler/CallbackRequestHandler.php delete mode 100644 src/RequestHandler/UnsafeCallableRequestHandler.php create mode 100644 src/ResponseResolver/NullResponseResolver.php delete mode 100644 src/ResponseResolver/ObjectResponseResolver.php create mode 100644 src/ResponseResolver/ResponseBodyResponseResolver.php create mode 100644 src/ResponseResolver/UriResponseResolver.php diff --git a/README.md b/README.md index 8ac4eb00..b2207e5b 100644 --- a/README.md +++ b/README.md @@ -265,7 +265,7 @@ use Sunrise\Http\Message\ResponseFactory; use Sunrise\Http\Router\Exception\MethodNotAllowedException; use Sunrise\Http\Router\Exception\RouteNotFoundException; use Sunrise\Http\Router\Middleware\CallbackMiddleware; -use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; +use Sunrise\Http\Router\RequestHandler\CallbackRequestHandler; use Sunrise\Http\Router\RouteCollector; use Sunrise\Http\Router\Router; use Sunrise\Http\ServerRequest\ServerRequestFactory; @@ -274,7 +274,7 @@ use function Sunrise\Http\Router\emit; $collector = new RouteCollector(); -$collector->get('home', '/', new CallableRequestHandler(function ($request) { +$collector->get('home', '/', new CallbackRequestHandler(function ($request) { return (new ResponseFactory)->createJsonResponse(200); })); diff --git a/composer.json b/composer.json index 36f25c53..3b8c4b5e 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,7 @@ "psr/http-server-middleware": "^1.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", "sunrise/hydrator": "^3.0", + "symfony/serializer": ">=6.0", "symfony/validator": ">=6.0" }, "require-dev": { @@ -43,7 +44,7 @@ "vimeo/psalm": "^5.13", "sunrise/coding-standard": "^1.0", "sunrise/http-message": "^3.0", - "symfony/console": "^5.4" + "symfony/console": "^6.0" }, "autoload": { "files": [ diff --git a/functions/reflect_callable.php b/functions/reflect_callable.php index 4751be85..e563cd43 100644 --- a/functions/reflect_callable.php +++ b/functions/reflect_callable.php @@ -15,7 +15,6 @@ use Closure; use InvalidArgumentException; -use ReflectionFunctionAbstract; use ReflectionFunction; use ReflectionMethod; @@ -38,7 +37,7 @@ * * @since 3.0.0 */ -function reflect_callable(callable $callback): ReflectionFunctionAbstract +function reflect_callable(callable $callback): ReflectionFunction|ReflectionMethod { if ($callback instanceof Closure) { return new ReflectionFunction($callback); diff --git a/src/Annotation/RequestRouteAttribute.php b/src/Annotation/RequestAttribute.php similarity index 79% rename from src/Annotation/RequestRouteAttribute.php rename to src/Annotation/RequestAttribute.php index 0a701a83..80a02a59 100644 --- a/src/Annotation/RequestRouteAttribute.php +++ b/src/Annotation/RequestAttribute.php @@ -19,15 +19,15 @@ * @since 3.0.0 */ #[Attribute(Attribute::TARGET_PARAMETER)] -final class RequestRouteAttribute +final class RequestAttribute { /** * Constructor of the class * - * @param non-empty-string|null $name + * @param non-empty-string|null $key */ - public function __construct(public ?string $name = null) + public function __construct(public ?string $key = null) { } } diff --git a/src/Annotation/ResponseBody.php b/src/Annotation/ResponseBody.php index dd699375..6b62561e 100644 --- a/src/Annotation/ResponseBody.php +++ b/src/Annotation/ResponseBody.php @@ -14,6 +14,7 @@ namespace Sunrise\Http\Router\Annotation; use Attribute; +use Fig\Http\Message\StatusCodeInterface; /** * @since 3.0.0 @@ -21,4 +22,22 @@ #[Attribute(Attribute::TARGET_CLASS)] final class ResponseBody { + public const FORMAT_CSV = 'csv'; + public const FORMAT_JSON = 'json'; + public const FORMAT_XML = 'xml'; + public const FORMAT_YAML = 'yaml'; + + /** + * Constructor of the class + * + * @param int<100, 599> $statusCode + * @param array> $headers + * @param non-empty-string $format + */ + public function __construct( + public int $statusCode = StatusCodeInterface::STATUS_OK, + public array $headers = [], + public string $format = self::FORMAT_JSON, + ) { + } } diff --git a/src/Entity/MediaType.php b/src/Entity/MediaType.php index 87fea982..80e5a137 100644 --- a/src/Entity/MediaType.php +++ b/src/Entity/MediaType.php @@ -22,6 +22,8 @@ final class MediaType { public const APPLICATION_JSON = 'application/json'; public const APPLICATION_XML = 'application/xml'; + public const APPLICATION_YAML = 'application/yaml'; + public const TEXT_CSV = 'text/csv'; public const TEXT_XML = 'text/xml'; /** diff --git a/src/Exception/ResolvingParameterException.php b/src/Exception/ResolvingParameterException.php deleted file mode 100644 index 8054205e..00000000 --- a/src/Exception/ResolvingParameterException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -/** - * ResolvingParameterException - * - * @since 3.0.0 - */ -class ResolvingParameterException extends LogicException -{ -} diff --git a/src/Exception/ResolvingResponseException.php b/src/Exception/ResolvingResponseException.php deleted file mode 100644 index 67a6e0fb..00000000 --- a/src/Exception/ResolvingResponseException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -/** - * ResolvingResponseException - * - * @since 3.0.0 - */ -class ResolvingResponseException extends LogicException -{ -} diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 69d0f9ae..b8ab7444 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -188,6 +188,11 @@ public function attach(mixed $resource): void ); } + if (is_file($resource)) { + $this->resources[] = $resource; + return; + } + if (is_dir($resource)) { $filenames = glob($resource . '/*.php'); foreach ($filenames as $filename) { @@ -197,11 +202,6 @@ public function attach(mixed $resource): void return; } - if (is_file($resource)) { - $this->resources[] = $resource; - return; - } - throw new InvalidArgumentException(sprintf( 'The config route loader only handles file or directory paths, ' . 'however the given resource "%s" is not one of them.', diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 183c2b3a..82a783d5 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -13,9 +13,15 @@ namespace Sunrise\Http\Router\Loader; +use FilesystemIterator; use Psr\Container\ContainerInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\SimpleCache\CacheInterface; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; +use ReflectionClass; +use ReflectionMethod; +use RegexIterator; use Sunrise\Http\Router\Annotation\Consume; use Sunrise\Http\Router\Annotation\Description; use Sunrise\Http\Router\Annotation\Host; @@ -43,12 +49,6 @@ use Sunrise\Http\Router\RouteCollectionInterface; use Sunrise\Http\Router\RouteFactory; use Sunrise\Http\Router\RouteFactoryInterface; -use FilesystemIterator; -use RecursiveDirectoryIterator; -use RecursiveIteratorIterator; -use ReflectionClass; -use ReflectionMethod; -use RegexIterator; use function class_exists; use function get_declared_classes; diff --git a/src/Middleware/CallbackMiddleware.php b/src/Middleware/CallbackMiddleware.php index 963d286f..204433be 100644 --- a/src/Middleware/CallbackMiddleware.php +++ b/src/Middleware/CallbackMiddleware.php @@ -17,8 +17,10 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use ReflectionFunction; +use ReflectionMethod; use Sunrise\Http\Router\ParameterResolutionerInterface; -use Sunrise\Http\Router\ParameterResolver\TypeParameterResolver; +use Sunrise\Http\Router\ParameterResolver\DirectInjectionParameterResolver; use Sunrise\Http\Router\ResponseResolutionerInterface; use function Sunrise\Http\Router\reflect_callable; @@ -69,22 +71,32 @@ public function __construct( $this->responseResolutioner = $responseResolutioner; } + /** + * Gets the callback's reflection + * + * @return ReflectionFunction|ReflectionMethod + */ + public function getReflection(): ReflectionFunction|ReflectionMethod + { + return reflect_callable($this->callback); + } + /** * @inheritDoc */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $args = $this->parameterResolutioner - ->withRequest($request) + $arguments = $this->parameterResolutioner + ->withContext($request) ->withPriorityResolver( - new TypeParameterResolver(ServerRequestInterface::class, $request), - new TypeParameterResolver(RequestHandlerInterface::class, $handler), + new DirectInjectionParameterResolver($request), + new DirectInjectionParameterResolver($handler), ) - ->resolveParameters(...reflect_callable($this->callback)->getParameters()); + ->resolveParameters(...$this->getReflection()->getParameters()); /** @var mixed $response */ - $response = ($this->callback)(...$args); + $response = ($this->callback)(...$arguments); - return $this->responseResolutioner->withRequest($request)->resolveResponse($response); + return $this->responseResolutioner->resolveResponse($response, $request); } } diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 8cf4c17a..db020f27 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -13,6 +13,7 @@ namespace Sunrise\Http\Router\Middleware; +use JsonException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; @@ -20,7 +21,6 @@ use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ServerRequest; -use JsonException; use function extension_loaded; use function is_array; @@ -61,11 +61,7 @@ public function __construct() public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (ServerRequest::from($request)->isJson()) { - $request = $request->withParsedBody( - $this->decodeJsonPayload( - $request->getBody()->__toString() - ) - ); + $request = $request->withParsedBody($this->decodePayload($request->getBody()->__toString())); } return $handler->handle($request); @@ -74,23 +70,23 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface /** * Tries to decode the given JSON payload * - * @param string $json + * @param string $payload * * @return array * * @throws InvalidRequestPayloadException * If the JSON payload cannot be decoded. */ - private function decodeJsonPayload(string $json): array + private function decodePayload(string $payload): array { try { - $data = json_decode($json, true, 512, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR); + $data = json_decode($payload, true, 512, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR); } catch (JsonException $e) { throw new InvalidRequestPayloadException(sprintf('Invalid JSON: %s', $e->getMessage()), 0, $e); } - // According to PSR-7, the data must be an array because - // we're using the 'associative' option when decoding the JSON. + // According to PSR-7, the data must be an array + // because we're using the 'associative' option when decoding the JSON. if (!is_array($data)) { throw new InvalidRequestPayloadException('Unexpected JSON: Expects an array or object.'); } diff --git a/src/Middleware/SimdjsonPayloadDecodingMiddleware.php b/src/Middleware/SimdJsonPayloadDecodingMiddleware.php similarity index 76% rename from src/Middleware/SimdjsonPayloadDecodingMiddleware.php rename to src/Middleware/SimdJsonPayloadDecodingMiddleware.php index 802ac74f..168fd009 100644 --- a/src/Middleware/SimdjsonPayloadDecodingMiddleware.php +++ b/src/Middleware/SimdJsonPayloadDecodingMiddleware.php @@ -13,6 +13,7 @@ namespace Sunrise\Http\Router\Middleware; +use SimdJsonException; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; @@ -20,7 +21,6 @@ use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ServerRequest; -use RuntimeException; use function extension_loaded; use function is_array; @@ -34,7 +34,7 @@ * * @link https://www.php.net/manual/en/book.simdjson.php */ -final class SimdjsonPayloadDecodingMiddleware implements MiddlewareInterface +final class SimdJsonPayloadDecodingMiddleware implements MiddlewareInterface { /** @@ -47,7 +47,7 @@ public function __construct() { if (!extension_loaded('simdjson')) { throw new LogicException( - 'The Simdjson extension is required, run the `pecl install simdjson` command to resolve it.' + 'The SimdJson extension is required, run the `pecl install simdjson` command to resolve it.' ); } } @@ -58,11 +58,7 @@ public function __construct() public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (ServerRequest::from($request)->isJson()) { - $request = $request->withParsedBody( - $this->decodeJsonPayload( - $request->getBody()->__toString() - ) - ); + $request = $request->withParsedBody($this->decodePayload($request->getBody()->__toString())); } return $handler->handle($request); @@ -71,23 +67,23 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface /** * Tries to decode the given JSON payload * - * @param string $json + * @param string $payload * * @return array * * @throws InvalidRequestPayloadException * If the JSON payload cannot be decoded. */ - private function decodeJsonPayload(string $json): array + private function decodePayload(string $payload): array { try { - $data = simdjson_decode($json, true, 512); - } catch (RuntimeException $e) { + $data = simdjson_decode($payload, true, 512); + } catch (SimdJsonException $e) { throw new InvalidRequestPayloadException(sprintf('Invalid JSON: %s', $e->getMessage()), 0, $e); } - // According to PSR-7, the data must be an array because - // we're using the 'associative' option when decoding the JSON. + // According to PSR-7, the data must be an array + // because we're using the 'associative' option when decoding the JSON. if (!is_array($data)) { throw new InvalidRequestPayloadException('Unexpected JSON: Expects an array or object.'); } diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index 44842a30..431f1d27 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -13,10 +13,10 @@ namespace Sunrise\Http\Router; -use Psr\Http\Message\RequestInterface; +use Generator; use ReflectionMethod; use ReflectionParameter; -use Sunrise\Http\Router\Exception\ResolvingParameterException; +use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ParameterResolver\ParameterResolverInterface; use function sprintf; @@ -30,9 +30,9 @@ final class ParameterResolutioner implements ParameterResolutionerInterface { /** - * @var RequestInterface|null + * @var mixed */ - private ?RequestInterface $request = null; + private mixed $context = null; /** * @var list @@ -42,10 +42,10 @@ final class ParameterResolutioner implements ParameterResolutionerInterface /** * @inheritDoc */ - public function withRequest(RequestInterface $request): static + public function withContext(mixed $context): static { $clone = clone $this; - $clone->request = $request; + $clone->context = $context; return $clone; } @@ -81,42 +81,40 @@ public function addResolver(ParameterResolverInterface ...$resolvers): void /** * @inheritDoc + * + * @throws LogicException If one of the parameters cannot be resolved to an argument(s). */ - public function resolveParameters(ReflectionParameter ...$parameters): array + public function resolveParameters(ReflectionParameter ...$parameters): Generator { - $arguments = []; foreach ($parameters as $parameter) { - /** @var mixed */ - $arguments[] = $this->resolveParameter($parameter); + yield from $this->resolveParameter($parameter); } - - return $arguments; } /** - * Tries to resolve the given parameter to an argument + * Tries to resolve the given parameter to an argument(s) * * @param ReflectionParameter $parameter * - * @return mixed + * @return Generator * - * @throws ResolvingParameterException - * If the parameter cannot be resolved to an argument. + * @throws LogicException If the parameter cannot be resolved to an argument(s). */ - private function resolveParameter(ReflectionParameter $parameter): mixed + private function resolveParameter(ReflectionParameter $parameter): Generator { foreach ($this->resolvers as $resolver) { - if ($resolver->supportsParameter($parameter, $this->request)) { - return $resolver->resolveParameter($parameter, $this->request); + $arguments = $resolver->resolveParameter($parameter, $this->context); + if ($arguments->valid()) { + return yield from $arguments; } } if ($parameter->isDefaultValueAvailable()) { - return $parameter->getDefaultValue(); + return yield $parameter->getDefaultValue(); } - throw new ResolvingParameterException(sprintf( - 'Unable to resolve the parameter {%s}', + throw new LogicException(sprintf( + 'Unable to resolve the parameter {%s}.', $this->stringifyParameter($parameter) )); } diff --git a/src/ParameterResolutionerInterface.php b/src/ParameterResolutionerInterface.php index 255eca11..bfbd04ea 100644 --- a/src/ParameterResolutionerInterface.php +++ b/src/ParameterResolutionerInterface.php @@ -13,10 +13,9 @@ namespace Sunrise\Http\Router; -use Psr\Http\Message\RequestInterface; -use Sunrise\Http\Router\Exception\ResolvingParameterException; -use Sunrise\Http\Router\ParameterResolver\ParameterResolverInterface; +use Generator; use ReflectionParameter; +use Sunrise\Http\Router\ParameterResolver\ParameterResolverInterface; /** * ParameterResolutionerInterface @@ -27,15 +26,15 @@ interface ParameterResolutionerInterface { /** - * Creates a new instance of the resolutioner with the given current request + * Creates a new instance of the resolutioner with the given context * * Please note that this method MUST NOT change the object state. * - * @param RequestInterface $request + * @param mixed $context * * @return static */ - public function withRequest(RequestInterface $request): static; + public function withContext(mixed $context): static; /** * Creates a new instance of the resolutioner with the given priority parameter resolver(s) @@ -62,9 +61,7 @@ public function addResolver(ParameterResolverInterface ...$resolvers): void; * * @param ReflectionParameter ...$parameters * - * @return list List of ready-to-pass arguments. - * - * @throws ResolvingParameterException If one of the parameters cannot be resolved to an argument. + * @return Generator List of ready-to-pass arguments. */ - public function resolveParameters(ReflectionParameter ...$parameters): array; + public function resolveParameters(ReflectionParameter ...$parameters): Generator; } diff --git a/src/ParameterResolver/DependencyInjectionParameterResolver.php b/src/ParameterResolver/DependencyInjectionParameterResolver.php index fbe69553..ba6db34d 100644 --- a/src/ParameterResolver/DependencyInjectionParameterResolver.php +++ b/src/ParameterResolver/DependencyInjectionParameterResolver.php @@ -13,8 +13,8 @@ namespace Sunrise\Http\Router\ParameterResolver; +use Generator; use Psr\Container\ContainerInterface; -use Psr\Http\Message\ServerRequestInterface; use ReflectionNamedType; use ReflectionParameter; @@ -38,25 +38,16 @@ public function __construct(private ContainerInterface $container) /** * @inheritDoc */ - public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator { $type = $parameter->getType(); if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { - return false; + return; } - return $this->container->has($type->getName()); - } - - /** - * @inheritDoc - */ - public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed - { - /** @var ReflectionNamedType $type */ - $type = $parameter->getType(); - - return $this->container->get($type->getName()); + if ($this->container->has($type->getName())) { + yield $this->container->get($type->getName()); + } } } diff --git a/src/ParameterResolver/DirectInjectionParameterResolver.php b/src/ParameterResolver/DirectInjectionParameterResolver.php new file mode 100644 index 00000000..f285e48d --- /dev/null +++ b/src/ParameterResolver/DirectInjectionParameterResolver.php @@ -0,0 +1,54 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ParameterResolver; + +use Generator; +use ReflectionNamedType; +use ReflectionParameter; + +use function is_a; + +/** + * ObjectParameterResolver + * + * @since 3.0.0 + */ +final class DirectInjectionParameterResolver implements ParameterResolverInterface +{ + + /** + * Constructor of the class + * + * @param object $object + */ + public function __construct(private object $object) + { + } + + /** + * @inheritDoc + */ + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator + { + $type = $parameter->getType(); + + if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { + return; + } + + if (is_a($this->object, $type->getName())) { + yield $this->object; + } + } +} diff --git a/src/ParameterResolver/ParameterResolverInterface.php b/src/ParameterResolver/ParameterResolverInterface.php index 39b1e0a4..6f29aaa7 100644 --- a/src/ParameterResolver/ParameterResolverInterface.php +++ b/src/ParameterResolver/ParameterResolverInterface.php @@ -13,8 +13,7 @@ namespace Sunrise\Http\Router\ParameterResolver; -use Psr\Http\Message\ServerRequestInterface; -use Sunrise\Http\Router\Exception\ResolvingParameterException; +use Generator; use ReflectionParameter; /** @@ -26,25 +25,12 @@ interface ParameterResolverInterface { /** - * Checks if the given parameter is supported + * Resolves the given parameter to an argument(s) * * @param ReflectionParameter $parameter - * @param ServerRequestInterface|null $request + * @param mixed $context * - * @return bool + * @return Generator */ - public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool; - - /** - * Resolves the given parameter to an argument - * - * @param ReflectionParameter $parameter - * @param ServerRequestInterface|null $request - * - * @return mixed - * - * @throws ResolvingParameterException - * If the parameter cannot be resolved to an argument. - */ - public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed; + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator; } diff --git a/src/ParameterResolver/RequestAttributeParameterResolver.php b/src/ParameterResolver/RequestAttributeParameterResolver.php new file mode 100644 index 00000000..5abf9040 --- /dev/null +++ b/src/ParameterResolver/RequestAttributeParameterResolver.php @@ -0,0 +1,56 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ParameterResolver; + +use Generator; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionParameter; +use Sunrise\Http\Router\Annotation\RequestAttribute; +use Sunrise\Http\Router\Exception\LogicException; + +/** + * RequestAttributeParameterResolver + * + * @since 3.0.0 + */ +final class RequestAttributeParameterResolver implements ParameterResolverInterface +{ + + /** + * @inheritDoc + */ + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator + { + $attributes = $parameter->getAttributes(RequestAttribute::class); + if ($attributes === []) { + return; + } + + if (! $context instanceof ServerRequestInterface) { + throw new LogicException( + 'At this level of the application, any operations with the request are not possible.' + ); + } + + /** + * @var RequestAttribute $attribute + * @psalm-suppress UnnecessaryVarAnnotation + */ + $attribute = $attributes[0]->newInstance(); + + $value = $context->getAttribute($attribute->key); + + yield $value; + } +} diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index 0e24286c..de28740a 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -13,16 +13,18 @@ namespace Sunrise\Http\Router\ParameterResolver; +use Generator; use Psr\Http\Message\ServerRequestInterface; +use ReflectionNamedType; +use ReflectionParameter; use Sunrise\Http\Router\Annotation\RequestBody; +use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\Exception\UnhydrableObjectException; use Sunrise\Http\Router\Exception\UnprocessableRequestBodyException; use Sunrise\Hydrator\Exception\InvalidDataException; use Sunrise\Hydrator\Exception\InvalidObjectException; use Sunrise\Hydrator\HydratorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; -use ReflectionNamedType; -use ReflectionParameter; /** * RequestBodyParameterResolver @@ -39,32 +41,10 @@ final class RequestBodyParameterResolver implements ParameterResolverInterface * Constructor of the class * * @param HydratorInterface $hydrator - * @param ValidatorInterface|null $validator - */ - public function __construct(private HydratorInterface $hydrator, private ?ValidatorInterface $validator = null) - { - } - - /** - * @inheritDoc + * @param ValidatorInterface $validator */ - public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool + public function __construct(private HydratorInterface $hydrator, private ValidatorInterface $validator) { - if ($request === null) { - return false; - } - - $type = $parameter->getType(); - - if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { - return false; - } - - if ($parameter->getAttributes(RequestBody::class) === []) { - return false; - } - - return true; } /** @@ -75,30 +55,43 @@ public function supportsParameter(ReflectionParameter $parameter, ?ServerRequest * * @throws UnprocessableRequestBodyException * If the request's parsed body isn't valid. + * + * @throws LogicException + * If the resolver is used incorrectly. */ - public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator { - /** @var ReflectionNamedType $type */ + if ($parameter->getAttributes(RequestBody::class) === []) { + return; + } + $type = $parameter->getType(); - /** @var class-string $fqn */ - $fqn = $type->getName(); + if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { + throw new LogicException( + 'To use the #[RequestBody] attribute, the parameter must be typed with a DTO.' + ); + } + + if (! $context instanceof ServerRequestInterface) { + throw new LogicException( + 'At this level of the application, any operations with the request are not possible.' + ); + } try { - $object = $this->hydrator->hydrate($fqn, (array) $request?->getParsedBody()); + $object = $this->hydrator->hydrate($type->getName(), (array) $context->getParsedBody()); } catch (InvalidObjectException $e) { throw new UnhydrableObjectException($e->getMessage(), 0, $e); } catch (InvalidDataException $e) { throw new UnprocessableRequestBodyException($e->getViolations()); } - if (isset($this->validator)) { - $violations = $this->validator->validate($object); - if ($violations->count() > 0) { - throw new UnprocessableRequestBodyException($violations); - } + $violations = $this->validator->validate($object); + if ($violations->count() > 0) { + throw new UnprocessableRequestBodyException($violations); } - return $object; + yield $object; } } diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php index a36e1042..50a2cfec 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -13,16 +13,18 @@ namespace Sunrise\Http\Router\ParameterResolver; +use Generator; use Psr\Http\Message\ServerRequestInterface; +use ReflectionNamedType; +use ReflectionParameter; use Sunrise\Http\Router\Annotation\RequestQuery; +use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\Exception\UnhydrableObjectException; use Sunrise\Http\Router\Exception\UnprocessableRequestQueryException; use Sunrise\Hydrator\Exception\InvalidDataException; use Sunrise\Hydrator\Exception\InvalidObjectException; use Sunrise\Hydrator\HydratorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; -use ReflectionNamedType; -use ReflectionParameter; /** * RequestQueryParameterResolver @@ -39,32 +41,10 @@ final class RequestQueryParameterResolver implements ParameterResolverInterface * Constructor of the class * * @param HydratorInterface $hydrator - * @param ValidatorInterface|null $validator - */ - public function __construct(private HydratorInterface $hydrator, private ?ValidatorInterface $validator = null) - { - } - - /** - * @inheritDoc + * @param ValidatorInterface $validator */ - public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool + public function __construct(private HydratorInterface $hydrator, private ValidatorInterface $validator) { - if ($request === null) { - return false; - } - - $type = $parameter->getType(); - - if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { - return false; - } - - if ($parameter->getAttributes(RequestQuery::class) === []) { - return false; - } - - return true; } /** @@ -75,30 +55,43 @@ public function supportsParameter(ReflectionParameter $parameter, ?ServerRequest * * @throws UnprocessableRequestQueryException * If the request's query parameters isn't valid. + * + * @throws LogicException + * If the resolver is used incorrectly. */ - public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator { - /** @var ReflectionNamedType $type */ + if ($parameter->getAttributes(RequestQuery::class) === []) { + return; + } + $type = $parameter->getType(); - /** @var class-string $fqn */ - $fqn = $type->getName(); + if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { + throw new LogicException( + 'To use the #[RequestQuery] attribute, the parameter must be typed with a DTO.' + ); + } + + if (! $context instanceof ServerRequestInterface) { + throw new LogicException( + 'At this level of the application, any operations with the request are not possible.' + ); + } try { - $object = $this->hydrator->hydrate($fqn, (array) $request?->getQueryParams()); + $object = $this->hydrator->hydrate($type->getName(), $context->getQueryParams()); } catch (InvalidObjectException $e) { throw new UnhydrableObjectException($e->getMessage(), 0, $e); } catch (InvalidDataException $e) { throw new UnprocessableRequestQueryException($e->getViolations()); } - if (isset($this->validator)) { - $violations = $this->validator->validate($object); - if ($violations->count() > 0) { - throw new UnprocessableRequestQueryException($violations); - } + $violations = $this->validator->validate($object); + if ($violations->count() > 0) { + throw new UnprocessableRequestQueryException($violations); } - return $object; + yield $object; } } diff --git a/src/ParameterResolver/RequestRouteAttributeParameterResolver.php b/src/ParameterResolver/RequestRouteAttributeParameterResolver.php deleted file mode 100644 index c4755cd2..00000000 --- a/src/ParameterResolver/RequestRouteAttributeParameterResolver.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\ParameterResolver; - -use Psr\Http\Message\ServerRequestInterface; -use Sunrise\Http\Router\Annotation\RequestRouteAttribute; -use Sunrise\Http\Router\RouteInterface; -use ReflectionAttribute; -use ReflectionNamedType; -use ReflectionParameter; - -final class RequestRouteAttributeParameterResolver implements ParameterResolverInterface -{ - - /** - * @inheritDoc - */ - public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool - { - if ($request === null) { - return false; - } - - $type = $parameter->getType(); - - if (! $type instanceof ReflectionNamedType || ! $type->isBuiltin()) { - return false; - } - - if ($parameter->getAttributes(RequestRouteAttribute::class) === []) { - return false; - } - - return true; - } - - /** - * @inheritDoc - */ - public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed - { - /** @var non-empty-list> $annotations */ - $annotations = $parameter->getAttributes(RequestRouteAttribute::class); - - $key = $annotations[0]->newInstance()->name ?? $parameter->getName(); - - /** @var RouteInterface $route */ - $route = $request->getAttribute('@route'); - - /** @var mixed $value */ - $value = $route->getAttributes()[$key] ?? null; - - return $value; - } -} diff --git a/src/ParameterResolver/RequestRouteParameterResolver.php b/src/ParameterResolver/RequestRouteParameterResolver.php index fe64e2ac..5d1eb423 100644 --- a/src/ParameterResolver/RequestRouteParameterResolver.php +++ b/src/ParameterResolver/RequestRouteParameterResolver.php @@ -13,10 +13,12 @@ namespace Sunrise\Http\Router\ParameterResolver; +use Generator; use Psr\Http\Message\ServerRequestInterface; -use Sunrise\Http\Router\RouteInterface; use ReflectionNamedType; use ReflectionParameter; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\RouteInterface; /** * RequestRouteParameterResolver @@ -28,34 +30,37 @@ final class RequestRouteParameterResolver implements ParameterResolverInterface /** * @inheritDoc + * + * @throws LogicException + * If the resolver is used incorrectly. */ - public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator { - if ($request === null) { - return false; - } - $type = $parameter->getType(); - if (! $type instanceof ReflectionNamedType) { - return false; + if ( + ! ($type instanceof ReflectionNamedType) || + ! ($type->getName() === RouteInterface::class) + ) { + return; } - if (! ($type->getName() === RouteInterface::class)) { - return false; + if (! $context instanceof ServerRequestInterface) { + throw new LogicException( + 'At this level of the application, any operations with the request are not possible.' + ); } /** @var RouteInterface|null $route */ - $route = $request->getAttribute('@route'); + $route = $context->getAttribute('@route'); - return isset($route) || $type->allowsNull(); - } + if ($route === null && !$parameter->allowsNull()) { + throw new LogicException( + 'At this level of the application, the current request does not contain a route. ' . + 'To suppress this error, the parameter should be nullable.' + ); + } - /** - * @inheritDoc - */ - public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed - { - return $request?->getAttribute('@route'); + yield $route; } } diff --git a/src/ParameterResolver/TypeParameterResolver.php b/src/ParameterResolver/TypeParameterResolver.php deleted file mode 100644 index 9c4b3858..00000000 --- a/src/ParameterResolver/TypeParameterResolver.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\ParameterResolver; - -use Psr\Http\Message\ServerRequestInterface; -use Sunrise\Http\Router\Exception\LogicException; -use ReflectionNamedType; -use ReflectionParameter; - -use function sprintf; - -/** - * TypeParameterResolver - * - * @since 3.0.0 - */ -final class TypeParameterResolver implements ParameterResolverInterface -{ - - /** - * Constructor of the class - * - * @param string $type - * @param object $value - * - * @throws LogicException - * If the value isn't an instance of the type. - */ - public function __construct(private string $type, private object $value) - { - if (! $this->value instanceof $this->type) { - throw new LogicException(sprintf( - 'The %1$s value must be an instance of %2$s.', - $this->value::class, - $this->type, - )); - } - } - - /** - * @inheritDoc - */ - public function supportsParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): bool - { - $type = $parameter->getType(); - - if (! $type instanceof ReflectionNamedType) { - return false; - } - - return $type->getName() === $this->type; - } - - /** - * @inheritDoc - */ - public function resolveParameter(ReflectionParameter $parameter, ?ServerRequestInterface $request): mixed - { - return $this->value; - } -} diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index ca27e657..998ebfa7 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -17,7 +17,7 @@ use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\Exception\ResolvingReferenceException; use Sunrise\Http\Router\Middleware\CallbackMiddleware; -use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; +use Sunrise\Http\Router\RequestHandler\CallbackRequestHandler; use Closure; use function get_debug_type; @@ -80,7 +80,7 @@ public function resolveRequestHandler($reference): RequestHandlerInterface } if ($reference instanceof Closure) { - return new CallableRequestHandler($reference, $this->parameterResolutioner, $this->responseResolutioner); + return new CallbackRequestHandler($reference, $this->parameterResolutioner, $this->responseResolutioner); } if (is_string($reference) && is_subclass_of($reference, RequestHandlerInterface::class)) { @@ -97,7 +97,7 @@ public function resolveRequestHandler($reference): RequestHandlerInterface $reference[0] = $this->classResolver->resolveClass($reference[0]); } - return new CallableRequestHandler( + return new CallbackRequestHandler( [$reference[0], $reference[1]], $this->parameterResolutioner, $this->responseResolutioner diff --git a/src/RequestHandler/CallableRequestHandler.php b/src/RequestHandler/CallableRequestHandler.php index a958a179..265a57a3 100644 --- a/src/RequestHandler/CallableRequestHandler.php +++ b/src/RequestHandler/CallableRequestHandler.php @@ -16,16 +16,11 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\ParameterResolver\TypeParameterResolver; -use Sunrise\Http\Router\ParameterResolutionerInterface; -use Sunrise\Http\Router\ResponseResolutionerInterface; -use ReflectionFunction; -use ReflectionMethod; - -use function Sunrise\Http\Router\reflect_callable; /** * CallableRequestHandler + * + * @template T as callable(ServerRequestInterface=): ResponseInterface */ final class CallableRequestHandler implements RequestHandlerInterface { @@ -33,51 +28,18 @@ final class CallableRequestHandler implements RequestHandlerInterface /** * The request handler's callback * - * @var callable + * @var T */ private $callback; - /** - * The callback's parameter resolutioner - * - * @var ParameterResolutionerInterface - */ - private ParameterResolutionerInterface $parameterResolutioner; - - /** - * The callback's response resolutioner - * - * @var ResponseResolutionerInterface - */ - private ResponseResolutionerInterface $responseResolutioner; - /** * Constructor of the class * - * @param callable $callback - * @param ParameterResolutionerInterface $parameterResolutioner - * @param ResponseResolutionerInterface $responseResolutioner + * @param T $callback */ - public function __construct( - callable $callback, - ParameterResolutionerInterface $parameterResolutioner, - ResponseResolutionerInterface $responseResolutioner - ) { - $this->callback = $callback; - $this->parameterResolutioner = $parameterResolutioner; - $this->responseResolutioner = $responseResolutioner; - } - - /** - * Gets the callback's reflection - * - * @return ReflectionFunction|ReflectionMethod - * - * @since 3.0.0 - */ - public function getReflection(): ReflectionFunction|ReflectionMethod + public function __construct(callable $callback) { - return reflect_callable($this->callback); + $this->callback = $callback; } /** @@ -85,14 +47,6 @@ public function getReflection(): ReflectionFunction|ReflectionMethod */ public function handle(ServerRequestInterface $request): ResponseInterface { - $arguments = $this->parameterResolutioner - ->withRequest($request) - ->withPriorityResolver(new TypeParameterResolver(ServerRequestInterface::class, $request)) - ->resolveParameters(...$this->getReflection()->getParameters()); - - /** @var mixed $response */ - $response = ($this->callback)(...$arguments); - - return $this->responseResolutioner->withRequest($request)->resolveResponse($response); + return ($this->callback)($request); } } diff --git a/src/RequestHandler/CallbackRequestHandler.php b/src/RequestHandler/CallbackRequestHandler.php new file mode 100644 index 00000000..ea009077 --- /dev/null +++ b/src/RequestHandler/CallbackRequestHandler.php @@ -0,0 +1,100 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\RequestHandler; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\RequestHandlerInterface; +use ReflectionFunction; +use ReflectionMethod; +use Sunrise\Http\Router\ParameterResolutionerInterface; +use Sunrise\Http\Router\ParameterResolver\DirectInjectionParameterResolver; +use Sunrise\Http\Router\ResponseResolutionerInterface; + +use function Sunrise\Http\Router\reflect_callable; + +/** + * CallbackRequestHandler + * + * @since 3.0.0 + */ +final class CallbackRequestHandler implements RequestHandlerInterface +{ + + /** + * The request handler's callback + * + * @var callable + */ + private $callback; + + /** + * The callback's parameter resolutioner + * + * @var ParameterResolutionerInterface + */ + private ParameterResolutionerInterface $parameterResolutioner; + + /** + * The callback's response resolutioner + * + * @var ResponseResolutionerInterface + */ + private ResponseResolutionerInterface $responseResolutioner; + + /** + * Constructor of the class + * + * @param callable $callback + * @param ParameterResolutionerInterface $parameterResolutioner + * @param ResponseResolutionerInterface $responseResolutioner + */ + public function __construct( + callable $callback, + ParameterResolutionerInterface $parameterResolutioner, + ResponseResolutionerInterface $responseResolutioner + ) { + $this->callback = $callback; + $this->parameterResolutioner = $parameterResolutioner; + $this->responseResolutioner = $responseResolutioner; + } + + /** + * Gets the callback's reflection + * + * @return ReflectionFunction|ReflectionMethod + */ + public function getReflection(): ReflectionFunction|ReflectionMethod + { + return reflect_callable($this->callback); + } + + /** + * @inheritDoc + */ + public function handle(ServerRequestInterface $request): ResponseInterface + { + $arguments = $this->parameterResolutioner + ->withContext($request) + ->withPriorityResolver( + new DirectInjectionParameterResolver($request), + ) + ->resolveParameters(...$this->getReflection()->getParameters()); + + /** @var mixed $response */ + $response = ($this->callback)(...$arguments); + + return $this->responseResolutioner->resolveResponse($response, $request); + } +} diff --git a/src/RequestHandler/QueueableRequestHandler.php b/src/RequestHandler/QueueableRequestHandler.php index 68c59ddf..3d036b60 100644 --- a/src/RequestHandler/QueueableRequestHandler.php +++ b/src/RequestHandler/QueueableRequestHandler.php @@ -26,28 +26,34 @@ final class QueueableRequestHandler implements RequestHandlerInterface { /** + * The request handler's middleware queue + * * @var SplQueue */ - private SplQueue $queue; + private SplQueue $middlewareQueue; /** + * The request handler's request handler + * * @var RequestHandlerInterface */ - private RequestHandlerInterface $endpoint; + private RequestHandlerInterface $requestHandler; /** * Constructor of the class * - * @param RequestHandlerInterface $endpoint + * @param RequestHandlerInterface $requestHandler */ - public function __construct(RequestHandlerInterface $endpoint) + public function __construct(RequestHandlerInterface $requestHandler) { - $this->queue = new SplQueue(); - $this->endpoint = $endpoint; + /** @var SplQueue */ + $this->middlewareQueue = new SplQueue(); + + $this->requestHandler = $requestHandler; } /** - * Adds the given middleware(s) to the request handler queue + * Adds the given middleware(s) to the request handler's middleware queue * * @param MiddlewareInterface ...$middlewares * @@ -56,7 +62,7 @@ public function __construct(RequestHandlerInterface $endpoint) public function add(MiddlewareInterface ...$middlewares): void { foreach ($middlewares as $middleware) { - $this->queue->enqueue($middleware); + $this->middlewareQueue->enqueue($middleware); } } @@ -65,10 +71,10 @@ public function add(MiddlewareInterface ...$middlewares): void */ public function handle(ServerRequestInterface $request): ResponseInterface { - if (!$this->queue->isEmpty()) { - return $this->queue->dequeue()->process($request, $this); + if (!$this->middlewareQueue->isEmpty()) { + return $this->middlewareQueue->dequeue()->process($request, $this); } - return $this->endpoint->handle($request); + return $this->requestHandler->handle($request); } } diff --git a/src/RequestHandler/UnsafeCallableRequestHandler.php b/src/RequestHandler/UnsafeCallableRequestHandler.php deleted file mode 100644 index 8e44a261..00000000 --- a/src/RequestHandler/UnsafeCallableRequestHandler.php +++ /dev/null @@ -1,54 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\RequestHandler; - -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\RequestHandlerInterface; - -/** - * UnsafeCallableRequestHandler - * - * @since 3.0.0 - * - * @template T as callable(ServerRequestInterface=): ResponseInterface - */ -final class UnsafeCallableRequestHandler implements RequestHandlerInterface -{ - - /** - * The request handler's callback - * - * @var T - */ - private $callback; - - /** - * Constructor of the class - * - * @param T $callback - */ - public function __construct(callable $callback) - { - $this->callback = $callback; - } - - /** - * @inheritDoc - */ - public function handle(ServerRequestInterface $request): ResponseInterface - { - return ($this->callback)($request); - } -} diff --git a/src/ResponseResolutioner.php b/src/ResponseResolutioner.php index f3aac200..7408518f 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolutioner.php @@ -13,9 +13,8 @@ namespace Sunrise\Http\Router; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Exception\ResolvingResponseException; +use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ResponseResolver\ResponseResolverInterface; use function get_debug_type; @@ -29,27 +28,11 @@ final class ResponseResolutioner implements ResponseResolutionerInterface { - /** - * @var RequestInterface|null - */ - private ?RequestInterface $request = null; - /** * @var list */ private array $resolvers = []; - /** - * @inheritDoc - */ - public function withRequest(RequestInterface $context): static - { - $clone = clone $this; - $clone->request = $context; - - return $clone; - } - /** * @inheritDoc */ @@ -62,22 +45,25 @@ public function addResolver(ResponseResolverInterface ...$resolvers): void /** * @inheritDoc + * + * @throws LogicException If the value cannot be resolved to PSR-7 response. */ - public function resolveResponse($response): ResponseInterface + public function resolveResponse(mixed $value, mixed $context): ResponseInterface { - if ($response instanceof ResponseInterface) { - return $response; + if ($value instanceof ResponseInterface) { + return $value; } foreach ($this->resolvers as $resolver) { - if ($resolver->supportsResponse($response, $this->request)) { - return $resolver->resolveResponse($response, $this->request); + $response = $resolver->resolveResponse($value, $context); + if ($response instanceof ResponseInterface) { + return $response; } } - throw new ResolvingResponseException(sprintf( - 'Unable to resolve the response {%s}', - get_debug_type($response), + throw new LogicException(sprintf( + 'Unable to resolve the value {%s} to PSR-7 response.', + get_debug_type($value), )); } } diff --git a/src/ResponseResolutionerInterface.php b/src/ResponseResolutionerInterface.php index 6818d72c..681967fb 100644 --- a/src/ResponseResolutionerInterface.php +++ b/src/ResponseResolutionerInterface.php @@ -13,9 +13,7 @@ namespace Sunrise\Http\Router; -use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Exception\ResolvingResponseException; use Sunrise\Http\Router\ResponseResolver\ResponseResolverInterface; /** @@ -26,17 +24,6 @@ interface ResponseResolutionerInterface { - /** - * Creates a new instance of the resolutioner with the given current context - * - * Please note that this method MUST NOT change the object state. - * - * @param RequestInterface $context - * - * @return static - */ - public function withRequest(RequestInterface $context): static; - /** * Adds the given response resolver(s) to the resolutioner * @@ -47,14 +34,12 @@ public function withRequest(RequestInterface $context): static; public function addResolver(ResponseResolverInterface ...$resolvers): void; /** - * Resolves the given raw response to the object + * Resolves the given value to PSR-7 response * - * @param mixed $response + * @param mixed $value + * @param mixed $context * * @return ResponseInterface - * - * @throws ResolvingResponseException - * If the raw response cannot be resolved to the object. */ - public function resolveResponse($response): ResponseInterface; + public function resolveResponse(mixed $value, mixed $context): ResponseInterface; } diff --git a/src/ResponseResolver/NullResponseResolver.php b/src/ResponseResolver/NullResponseResolver.php new file mode 100644 index 00000000..2332a0f3 --- /dev/null +++ b/src/ResponseResolver/NullResponseResolver.php @@ -0,0 +1,47 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ResponseResolver; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * NullResponseResolver + * + * @since 3.0.0 + */ +final class NullResponseResolver implements ResponseResolverInterface +{ + + /** + * Constructor of the class + * + * @param ResponseFactoryInterface $responseFactory + */ + public function __construct(private ResponseFactoryInterface $responseFactory) + { + } + + /** + * @inheritDoc + */ + public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface + { + if ($value === null) { + return $this->responseFactory->createResponse(204); + } + + return null; + } +} diff --git a/src/ResponseResolver/ObjectResponseResolver.php b/src/ResponseResolver/ObjectResponseResolver.php deleted file mode 100644 index bea2558b..00000000 --- a/src/ResponseResolver/ObjectResponseResolver.php +++ /dev/null @@ -1,56 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\ResponseResolver; - -use Psr\Http\Message\ResponseFactoryInterface; -use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Annotation\ResponseBody; -use ReflectionClass; - -use function is_object; - -/** - * ObjectResponseResolver - * - * @since 3.0.0 - */ -final class ObjectResponseResolver implements ResponseResolverInterface -{ - public function __construct( - private ResponseFactoryInterface $responseFactory, - ) { - } - - /** - * @inheritDoc - */ - public function supportsResponse($response, $context): bool - { - if (!is_object($response)) { - return false; - } - - return true; - } - - /** - * @inheritDoc - */ - public function resolveResponse($response, $context): ResponseInterface - { - $attributes = (new ReflectionClass($response))->getAttributes(ResponseBody::class); - - return $this->responseFactory->createResponse(200); - } -} diff --git a/src/ResponseResolver/ResponseBodyResponseResolver.php b/src/ResponseResolver/ResponseBodyResponseResolver.php new file mode 100644 index 00000000..ad751d92 --- /dev/null +++ b/src/ResponseResolver/ResponseBodyResponseResolver.php @@ -0,0 +1,88 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ResponseResolver; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use ReflectionClass; +use Sunrise\Http\Router\Annotation\ResponseBody; +use Sunrise\Http\Router\Entity\MediaType; +use Symfony\Component\Serializer\SerializerInterface; + +use function is_object; + +/** + * ResponseBodyResponseResolver + * + * @since 3.0.0 + */ +final class ResponseBodyResponseResolver implements ResponseResolverInterface +{ + + /** + * Constructor of the class + * + * @param SerializerInterface $serializer + * @param ResponseFactoryInterface $responseFactory + */ + public function __construct( + private SerializerInterface $serializer, + private ResponseFactoryInterface $responseFactory, + ) { + } + + /** + * @inheritDoc + */ + public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface + { + if (!is_object($value)) { + return null; + } + + $class = new ReflectionClass($value::class); + $attributes = $class->getAttributes(ResponseBody::class); + if ($attributes === []) { + return null; + } + + /** + * @var ResponseBody $responseBody + * @psalm-suppress UnnecessaryVarAnnotation + */ + $responseBody = $attributes[0]->newInstance(); + + $response = $this->responseFactory->createResponse($responseBody->statusCode); + + $mediaType = match ($responseBody->format) { + ResponseBody::FORMAT_CSV => MediaType::TEXT_CSV, + ResponseBody::FORMAT_JSON => MediaType::APPLICATION_JSON, + ResponseBody::FORMAT_XML => MediaType::APPLICATION_XML, + ResponseBody::FORMAT_YAML => MediaType::APPLICATION_YAML, + default => null, + }; + + if (isset($mediaType)) { + $response = $response->withHeader('Content-Type', $mediaType); + } + + foreach ($responseBody->headers as $name => $header) { + $response = $response->withHeader($name, $header); + } + + $response->getBody()->write($this->serializer->serialize($value, $responseBody->format)); + + return $response; + } +} diff --git a/src/ResponseResolver/ResponseResolverInterface.php b/src/ResponseResolver/ResponseResolverInterface.php index f332668e..49c32860 100644 --- a/src/ResponseResolver/ResponseResolverInterface.php +++ b/src/ResponseResolver/ResponseResolverInterface.php @@ -14,7 +14,6 @@ namespace Sunrise\Http\Router\ResponseResolver; use Psr\Http\Message\ResponseInterface; -use Sunrise\Http\Router\Exception\ResolvingResponseException; /** * ResponseResolverInterface @@ -25,25 +24,12 @@ interface ResponseResolverInterface { /** - * Checks if the given raw response is supported + * Resolves the given value to PSR-7 response * - * @param mixed $response + * @param mixed $value * @param mixed $context * - * @return bool + * @return ResponseInterface|null */ - public function supportsResponse(mixed $response, mixed $context): bool; - - /** - * Resolves the given raw response to the object - * - * @param mixed $response - * @param mixed $context - * - * @return ResponseInterface - * - * @throws ResolvingResponseException - * If the raw response cannot be resolved to the object. - */ - public function resolveResponse(mixed $response, mixed $context): ResponseInterface; + public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface; } diff --git a/src/ResponseResolver/RouteResponseResolver.php b/src/ResponseResolver/RouteResponseResolver.php index adc6bd7d..ca38ee0e 100644 --- a/src/ResponseResolver/RouteResponseResolver.php +++ b/src/ResponseResolver/RouteResponseResolver.php @@ -15,6 +15,7 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\RouteInterface; /** @@ -27,28 +28,22 @@ final class RouteResponseResolver implements ResponseResolverInterface /** * @inheritDoc + * + * @throws LogicException + * If the resolver is used incorrectly. */ - public function supportsResponse(mixed $response, mixed $context): bool + public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface { - if (!($context instanceof ServerRequestInterface)) { - return false; + if (! $value instanceof RouteInterface) { + return null; } - if (!($response instanceof RouteInterface)) { - return false; + if (! $context instanceof ServerRequestInterface) { + throw new LogicException( + 'At this level of the application, any operations with the request are not possible.' + ); } - return true; - } - - /** - * @inheritDoc - */ - public function resolveResponse(mixed $response, mixed $context): ResponseInterface - { - /** @var RouteInterface $response */ - /** @var ServerRequestInterface $context */ - - return $response->handle($context); + return $value->handle($context); } } diff --git a/src/ResponseResolver/StatusCodeResponseResolver.php b/src/ResponseResolver/StatusCodeResponseResolver.php index 8f725123..24e6f66a 100644 --- a/src/ResponseResolver/StatusCodeResponseResolver.php +++ b/src/ResponseResolver/StatusCodeResponseResolver.php @@ -15,6 +15,7 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; + use function is_int; /** @@ -26,33 +27,23 @@ final class StatusCodeResponseResolver implements ResponseResolverInterface { /** - * @var ResponseFactoryInterface - */ - private ResponseFactoryInterface $responseFactory; - - /** + * Constructor of the class + * * @param ResponseFactoryInterface $responseFactory */ - public function __construct(ResponseFactoryInterface $responseFactory) - { - $this->responseFactory = $responseFactory; - } - - /** - * @inheritDoc - */ - public function supportsResponse($response, $context): bool + public function __construct(private ResponseFactoryInterface $responseFactory) { - return is_int($response) && $response >= 100 && $response <= 599; } /** * @inheritDoc */ - public function resolveResponse($response, $context): ResponseInterface + public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface { - /** @var int<100, 599> $response */ + if (is_int($value) && $value >= 100 && $value <= 599) { + return $this->responseFactory->createResponse($value); + } - return $this->responseFactory->createResponse($response); + return null; } } diff --git a/src/ResponseResolver/UriResponseResolver.php b/src/ResponseResolver/UriResponseResolver.php new file mode 100644 index 00000000..5638a5f7 --- /dev/null +++ b/src/ResponseResolver/UriResponseResolver.php @@ -0,0 +1,49 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ResponseResolver; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\UriInterface; + +/** + * UriResponseResolver + * + * @since 3.0.0 + */ +final class UriResponseResolver implements ResponseResolverInterface +{ + + /** + * Constructor of the class + * + * @param ResponseFactoryInterface $responseFactory + */ + public function __construct(private ResponseFactoryInterface $responseFactory) + { + } + + /** + * @inheritDoc + */ + public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface + { + if (! $value instanceof UriInterface) { + return null; + } + + return $this->responseFactory->createResponse(302) + ->withHeader('Location', $value->__toString()); + } +} diff --git a/src/Route.php b/src/Route.php index 388dcee4..af224fa5 100644 --- a/src/Route.php +++ b/src/Route.php @@ -17,7 +17,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; +use Sunrise\Http\Router\RequestHandler\CallbackRequestHandler; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; use ReflectionClass; use Reflector; @@ -244,7 +244,7 @@ public function getTags(): array */ public function getHolder(): Reflector { - if ($this->requestHandler instanceof CallableRequestHandler) { + if ($this->requestHandler instanceof CallbackRequestHandler) { return $this->requestHandler->getReflection(); } diff --git a/src/Router.php b/src/Router.php index 1d7da240..540fe8a2 100644 --- a/src/Router.php +++ b/src/Router.php @@ -27,7 +27,7 @@ use Sunrise\Http\Router\Exception\PageNotFoundException; use Sunrise\Http\Router\Loader\LoaderInterface; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; -use Sunrise\Http\Router\RequestHandler\UnsafeCallableRequestHandler; +use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use function array_keys; use function Sunrise\Http\Router\path_build; @@ -306,7 +306,7 @@ public function match(ServerRequestInterface $request): RouteInterface public function run(ServerRequestInterface $request): ResponseInterface { // lazy resolving of the given request... - $routing = new UnsafeCallableRequestHandler( + $routing = new CallableRequestHandler( function (ServerRequestInterface $request): ResponseInterface { $this->matchedRoute = $this->match($request); diff --git a/tests/RequestHandler/CallableRequestHandlerTest.php b/tests/RequestHandler/CallableRequestHandlerTest.php index cfe1791e..9cbdba6f 100644 --- a/tests/RequestHandler/CallableRequestHandlerTest.php +++ b/tests/RequestHandler/CallableRequestHandlerTest.php @@ -5,7 +5,7 @@ use PHPUnit\Framework\TestCase; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; +use Sunrise\Http\Router\RequestHandler\CallbackRequestHandler; use Sunrise\Http\Router\Tests\Fixtures; /** @@ -20,7 +20,7 @@ class CallableRequestHandlerTest extends TestCase public function testContracts() : void { $callback = new Fixtures\Controllers\BlankController(); - $requestHandler = new CallableRequestHandler($callback); + $requestHandler = new CallbackRequestHandler($callback); $this->assertInstanceOf(RequestHandlerInterface::class, $requestHandler); } @@ -31,7 +31,7 @@ public function testContracts() : void public function testRun() : void { $callback = new Fixtures\Controllers\BlankController(); - $requestHandler = new CallableRequestHandler($callback); + $requestHandler = new CallbackRequestHandler($callback); $this->assertSame($callback, $requestHandler->getCallback()); diff --git a/tests/RouteTest.php b/tests/RouteTest.php index f5fdd706..20fe0a05 100644 --- a/tests/RouteTest.php +++ b/tests/RouteTest.php @@ -3,7 +3,7 @@ namespace Sunrise\Http\Router\Tests; use PHPUnit\Framework\TestCase; -use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; +use Sunrise\Http\Router\RequestHandler\CallbackRequestHandler; use Sunrise\Http\Router\Route; use Sunrise\Http\Router\RouteInterface; use Sunrise\Http\ServerRequest\ServerRequestFactory; @@ -383,7 +383,7 @@ public function testGetClosureHolder() : void $callback = function () { }; - $route = new Route('foo', '/foo', [], new CallableRequestHandler($callback)); + $route = new Route('foo', '/foo', [], new CallbackRequestHandler($callback)); $holder = $route->getHolder(); $this->assertInstanceOf(\ReflectionFunction::class, $holder); @@ -398,7 +398,7 @@ public function testGetMethodHolder() : void $class = new Fixtures\Controllers\BlankController(); $method = '__invoke'; - $route = new Route('foo', '/foo', [], new CallableRequestHandler([$class, $method])); + $route = new Route('foo', '/foo', [], new CallbackRequestHandler([$class, $method])); $holder = $route->getHolder(); $this->assertInstanceOf(\ReflectionMethod::class, $holder); From 181775f8d094d4786f65c0ae90aba7dc08d728ea Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Wed, 2 Aug 2023 03:32:03 +0200 Subject: [PATCH 076/180] v3 --- composer.json | 2 +- ...ike_header.php => parse_accept_header.php} | 0 src/Annotation/ResponseBody.php | 21 +--- src/Annotation/ResponseHeader.php | 34 ++++++ ...equestAttribute.php => ResponseStatus.php} | 9 +- src/Command/RouteListCommand.php | 3 +- src/Entity/MediaType.php | 2 + src/Loader/ConfigLoader.php | 6 +- src/Loader/DescriptorLoader.php | 8 +- src/Middleware/CallbackMiddleware.php | 12 +- .../JsonPayloadDecodingMiddleware.php | 12 +- .../SimdJsonPayloadDecodingMiddleware.php | 12 +- src/ParameterResolutioner.php | 6 +- ...p => ObjectInjectionParameterResolver.php} | 4 +- .../RequestAttributeParameterResolver.php | 56 --------- .../RequestBodyParameterResolver.php | 21 ++-- .../RequestQueryParameterResolver.php | 21 ++-- .../RequestRouteParameterResolver.php | 15 ++- src/ReferenceResolver.php | 37 +++--- src/ReferenceResolverInterface.php | 19 +--- src/RequestHandler/CallbackRequestHandler.php | 10 +- .../QueueableRequestHandler.php | 1 - src/ResponseResolutioner.php | 84 ++++++++++++-- src/ResponseResolutionerInterface.php | 16 ++- ...Resolver.php => EmptyResponseResolver.php} | 16 ++- .../ResponseBodyResponseResolver.php | 106 ++++++++++++------ .../ResponseResolverInterface.php | 16 ++- .../RouteResponseResolver.php | 25 ++--- ...esolver.php => StreamResponseResolver.php} | 22 ++-- src/ResponseResolver/UriResponseResolver.php | 18 ++- src/Route.php | 4 +- src/RouteCollector.php | 5 +- src/RouteInterface.php | 2 +- src/ServerRequest.php | 2 +- 34 files changed, 364 insertions(+), 263 deletions(-) rename functions/{parse_accept_like_header.php => parse_accept_header.php} (100%) create mode 100644 src/Annotation/ResponseHeader.php rename src/Annotation/{RequestAttribute.php => ResponseStatus.php} (65%) rename src/ParameterResolver/{DirectInjectionParameterResolver.php => ObjectInjectionParameterResolver.php} (91%) delete mode 100644 src/ParameterResolver/RequestAttributeParameterResolver.php rename src/ResponseResolver/{NullResponseResolver.php => EmptyResponseResolver.php} (67%) rename src/ResponseResolver/{StatusCodeResponseResolver.php => StreamResponseResolver.php} (58%) diff --git a/composer.json b/composer.json index 3b8c4b5e..736e4d2c 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ }, "require-dev": { "phpunit/phpunit": "^9.6", - "vimeo/psalm": "^5.13", + "vimeo/psalm": "^5.14", "sunrise/coding-standard": "^1.0", "sunrise/http-message": "^3.0", "symfony/console": "^6.0" diff --git a/functions/parse_accept_like_header.php b/functions/parse_accept_header.php similarity index 100% rename from functions/parse_accept_like_header.php rename to functions/parse_accept_header.php diff --git a/src/Annotation/ResponseBody.php b/src/Annotation/ResponseBody.php index 6b62561e..bc866170 100644 --- a/src/Annotation/ResponseBody.php +++ b/src/Annotation/ResponseBody.php @@ -14,30 +14,11 @@ namespace Sunrise\Http\Router\Annotation; use Attribute; -use Fig\Http\Message\StatusCodeInterface; /** * @since 3.0.0 */ -#[Attribute(Attribute::TARGET_CLASS)] +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)] final class ResponseBody { - public const FORMAT_CSV = 'csv'; - public const FORMAT_JSON = 'json'; - public const FORMAT_XML = 'xml'; - public const FORMAT_YAML = 'yaml'; - - /** - * Constructor of the class - * - * @param int<100, 599> $statusCode - * @param array> $headers - * @param non-empty-string $format - */ - public function __construct( - public int $statusCode = StatusCodeInterface::STATUS_OK, - public array $headers = [], - public string $format = self::FORMAT_JSON, - ) { - } } diff --git a/src/Annotation/ResponseHeader.php b/src/Annotation/ResponseHeader.php new file mode 100644 index 00000000..b640211b --- /dev/null +++ b/src/Annotation/ResponseHeader.php @@ -0,0 +1,34 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Annotation; + +use Attribute; + +/** + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION | Attribute::IS_REPEATABLE)] +final class ResponseHeader +{ + + /** + * Constructor of the class + * + * @param non-empty-string $name + * @param string $value + */ + public function __construct(public string $name, public string $value) + { + } +} diff --git a/src/Annotation/RequestAttribute.php b/src/Annotation/ResponseStatus.php similarity index 65% rename from src/Annotation/RequestAttribute.php rename to src/Annotation/ResponseStatus.php index 80a02a59..09c57642 100644 --- a/src/Annotation/RequestAttribute.php +++ b/src/Annotation/ResponseStatus.php @@ -14,20 +14,21 @@ namespace Sunrise\Http\Router\Annotation; use Attribute; +use Fig\Http\Message\StatusCodeInterface; /** * @since 3.0.0 */ -#[Attribute(Attribute::TARGET_PARAMETER)] -final class RequestAttribute +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)] +final class ResponseStatus implements StatusCodeInterface { /** * Constructor of the class * - * @param non-empty-string|null $key + * @param int<100, 599> $code */ - public function __construct(public ?string $key = null) + public function __construct(public int $code) { } } diff --git a/src/Command/RouteListCommand.php b/src/Command/RouteListCommand.php index bc073ca5..fb4b9a63 100644 --- a/src/Command/RouteListCommand.php +++ b/src/Command/RouteListCommand.php @@ -48,8 +48,7 @@ public function __construct(private ?Router $router = null) * * @return Router * - * @throws LogicException - * If the command doesn't contain the router instance. + * @throws LogicException If the command doesn't contain the router instance. * * @since 2.11.0 */ diff --git a/src/Entity/MediaType.php b/src/Entity/MediaType.php index 80e5a137..289b1440 100644 --- a/src/Entity/MediaType.php +++ b/src/Entity/MediaType.php @@ -24,6 +24,8 @@ final class MediaType public const APPLICATION_XML = 'application/xml'; public const APPLICATION_YAML = 'application/yaml'; public const TEXT_CSV = 'text/csv'; + public const TEXT_HTML = 'text/html'; + public const TEXT_PLAIN = 'text/plain'; public const TEXT_XML = 'text/xml'; /** diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index b8ab7444..85677f98 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -177,8 +177,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo /** * @inheritDoc * - * @throws InvalidArgumentException - * If the resource isn't valid. + * @throws InvalidArgumentException If the resource isn't valid. */ public function attach(mixed $resource): void { @@ -212,8 +211,7 @@ public function attach(mixed $resource): void /** * @inheritDoc * - * @throws InvalidArgumentException - * If one of the given resources isn't valid. + * @throws InvalidArgumentException If one of the given resources isn't valid. */ public function attachArray(array $resources): void { diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 82a783d5..0354769a 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -258,8 +258,7 @@ public function getCacheKey(): string /** * @inheritDoc * - * @throws InvalidArgumentException - * If the resource isn't valid. + * @throws InvalidArgumentException If the resource isn't valid. */ public function attach(mixed $resource): void { @@ -283,8 +282,7 @@ public function attach(mixed $resource): void /** * @inheritDoc * - * @throws InvalidArgumentException - * If one of the given resources isn't valid. + * @throws InvalidArgumentException If one of the given resources isn't valid. */ public function attachArray(array $resources): void { @@ -307,7 +305,7 @@ public function load(): RouteCollectionInterface $descriptor->path, $descriptor->methods, $this->referenceResolver->resolveRequestHandler($descriptor->holder), - $this->referenceResolver->resolveMiddlewares($descriptor->middlewares), + [...$this->referenceResolver->resolveMiddlewares($descriptor->middlewares)], $descriptor->attributes, ); diff --git a/src/Middleware/CallbackMiddleware.php b/src/Middleware/CallbackMiddleware.php index 204433be..d68d1b58 100644 --- a/src/Middleware/CallbackMiddleware.php +++ b/src/Middleware/CallbackMiddleware.php @@ -20,7 +20,7 @@ use ReflectionFunction; use ReflectionMethod; use Sunrise\Http\Router\ParameterResolutionerInterface; -use Sunrise\Http\Router\ParameterResolver\DirectInjectionParameterResolver; +use Sunrise\Http\Router\ParameterResolver\ObjectInjectionParameterResolver; use Sunrise\Http\Router\ResponseResolutionerInterface; use function Sunrise\Http\Router\reflect_callable; @@ -86,17 +86,19 @@ public function getReflection(): ReflectionFunction|ReflectionMethod */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { + $source = $this->getReflection(); + $arguments = $this->parameterResolutioner ->withContext($request) ->withPriorityResolver( - new DirectInjectionParameterResolver($request), - new DirectInjectionParameterResolver($handler), + new ObjectInjectionParameterResolver($request), + new ObjectInjectionParameterResolver($handler), ) - ->resolveParameters(...$this->getReflection()->getParameters()); + ->resolveParameters(...$source->getParameters()); /** @var mixed $response */ $response = ($this->callback)(...$arguments); - return $this->responseResolutioner->resolveResponse($response, $request); + return $this->responseResolutioner->resolveResponse($response, $request, $source); } } diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index db020f27..3d1d362d 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -43,8 +43,7 @@ final class JsonPayloadDecodingMiddleware implements MiddlewareInterface /** * Constructor of the class * - * @throws LogicException - * If the JSON extension isn't loaded. + * @throws LogicException If the JSON extension isn't loaded. */ public function __construct() { @@ -61,7 +60,11 @@ public function __construct() public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (ServerRequest::from($request)->isJson()) { - $request = $request->withParsedBody($this->decodePayload($request->getBody()->__toString())); + $request = $request->withParsedBody( + $this->decodePayload( + $request->getBody()->__toString() + ) + ); } return $handler->handle($request); @@ -74,8 +77,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface * * @return array * - * @throws InvalidRequestPayloadException - * If the JSON payload cannot be decoded. + * @throws InvalidRequestPayloadException If the JSON payload cannot be decoded. */ private function decodePayload(string $payload): array { diff --git a/src/Middleware/SimdJsonPayloadDecodingMiddleware.php b/src/Middleware/SimdJsonPayloadDecodingMiddleware.php index 168fd009..a24da152 100644 --- a/src/Middleware/SimdJsonPayloadDecodingMiddleware.php +++ b/src/Middleware/SimdJsonPayloadDecodingMiddleware.php @@ -40,8 +40,7 @@ final class SimdJsonPayloadDecodingMiddleware implements MiddlewareInterface /** * Constructor of the class * - * @throws LogicException - * If the Simdjson extension isn't loaded. + * @throws LogicException If the Simdjson extension isn't loaded. */ public function __construct() { @@ -58,7 +57,11 @@ public function __construct() public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { if (ServerRequest::from($request)->isJson()) { - $request = $request->withParsedBody($this->decodePayload($request->getBody()->__toString())); + $request = $request->withParsedBody( + $this->decodePayload( + $request->getBody()->__toString() + ) + ); } return $handler->handle($request); @@ -71,8 +74,7 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface * * @return array * - * @throws InvalidRequestPayloadException - * If the JSON payload cannot be decoded. + * @throws InvalidRequestPayloadException If the JSON payload cannot be decoded. */ private function decodePayload(string $payload): array { diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index 431f1d27..87148307 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -115,7 +115,7 @@ private function resolveParameter(ReflectionParameter $parameter): Generator throw new LogicException(sprintf( 'Unable to resolve the parameter {%s}.', - $this->stringifyParameter($parameter) + self::stringifyParameter($parameter) )); } @@ -124,9 +124,9 @@ private function resolveParameter(ReflectionParameter $parameter): Generator * * @param ReflectionParameter $parameter * - * @return string + * @return non-empty-string */ - private function stringifyParameter(ReflectionParameter $parameter): string + public static function stringifyParameter(ReflectionParameter $parameter): string { if ($parameter->getDeclaringFunction() instanceof ReflectionMethod) { return sprintf( diff --git a/src/ParameterResolver/DirectInjectionParameterResolver.php b/src/ParameterResolver/ObjectInjectionParameterResolver.php similarity index 91% rename from src/ParameterResolver/DirectInjectionParameterResolver.php rename to src/ParameterResolver/ObjectInjectionParameterResolver.php index f285e48d..dd3087e9 100644 --- a/src/ParameterResolver/DirectInjectionParameterResolver.php +++ b/src/ParameterResolver/ObjectInjectionParameterResolver.php @@ -20,11 +20,11 @@ use function is_a; /** - * ObjectParameterResolver + * ObjectInjectionParameterResolver * * @since 3.0.0 */ -final class DirectInjectionParameterResolver implements ParameterResolverInterface +final class ObjectInjectionParameterResolver implements ParameterResolverInterface { /** diff --git a/src/ParameterResolver/RequestAttributeParameterResolver.php b/src/ParameterResolver/RequestAttributeParameterResolver.php deleted file mode 100644 index 5abf9040..00000000 --- a/src/ParameterResolver/RequestAttributeParameterResolver.php +++ /dev/null @@ -1,56 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\ParameterResolver; - -use Generator; -use Psr\Http\Message\ServerRequestInterface; -use ReflectionParameter; -use Sunrise\Http\Router\Annotation\RequestAttribute; -use Sunrise\Http\Router\Exception\LogicException; - -/** - * RequestAttributeParameterResolver - * - * @since 3.0.0 - */ -final class RequestAttributeParameterResolver implements ParameterResolverInterface -{ - - /** - * @inheritDoc - */ - public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator - { - $attributes = $parameter->getAttributes(RequestAttribute::class); - if ($attributes === []) { - return; - } - - if (! $context instanceof ServerRequestInterface) { - throw new LogicException( - 'At this level of the application, any operations with the request are not possible.' - ); - } - - /** - * @var RequestAttribute $attribute - * @psalm-suppress UnnecessaryVarAnnotation - */ - $attribute = $attributes[0]->newInstance(); - - $value = $context->getAttribute($attribute->key); - - yield $value; - } -} diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolver/RequestBodyParameterResolver.php index de28740a..78081a2c 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolver/RequestBodyParameterResolver.php @@ -21,11 +21,14 @@ use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\Exception\UnhydrableObjectException; use Sunrise\Http\Router\Exception\UnprocessableRequestBodyException; +use Sunrise\Http\Router\ParameterResolutioner; use Sunrise\Hydrator\Exception\InvalidDataException; use Sunrise\Hydrator\Exception\InvalidObjectException; use Sunrise\Hydrator\HydratorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +use function sprintf; + /** * RequestBodyParameterResolver * @@ -50,14 +53,9 @@ public function __construct(private HydratorInterface $hydrator, private Validat /** * @inheritDoc * - * @throws UnhydrableObjectException - * If an object isn't valid. - * - * @throws UnprocessableRequestBodyException - * If the request's parsed body isn't valid. - * - * @throws LogicException - * If the resolver is used incorrectly. + * @throws UnhydrableObjectException If an object isn't valid. + * @throws UnprocessableRequestBodyException If the request's parsed body isn't valid. + * @throws LogicException If the resolver is used incorrectly. */ public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator { @@ -68,9 +66,10 @@ public function resolveParameter(ReflectionParameter $parameter, mixed $context) $type = $parameter->getType(); if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { - throw new LogicException( - 'To use the #[RequestBody] attribute, the parameter must be typed with a DTO.' - ); + throw new LogicException(sprintf( + 'To use the #[RequestBody] attribute, the parameter {%s} must be typed with an object.', + ParameterResolutioner::stringifyParameter($parameter), + )); } if (! $context instanceof ServerRequestInterface) { diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolver/RequestQueryParameterResolver.php index 50a2cfec..9aea6c02 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolver/RequestQueryParameterResolver.php @@ -21,11 +21,14 @@ use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\Exception\UnhydrableObjectException; use Sunrise\Http\Router\Exception\UnprocessableRequestQueryException; +use Sunrise\Http\Router\ParameterResolutioner; use Sunrise\Hydrator\Exception\InvalidDataException; use Sunrise\Hydrator\Exception\InvalidObjectException; use Sunrise\Hydrator\HydratorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +use function sprintf; + /** * RequestQueryParameterResolver * @@ -50,14 +53,9 @@ public function __construct(private HydratorInterface $hydrator, private Validat /** * @inheritDoc * - * @throws UnhydrableObjectException - * If an object isn't valid. - * - * @throws UnprocessableRequestQueryException - * If the request's query parameters isn't valid. - * - * @throws LogicException - * If the resolver is used incorrectly. + * @throws UnhydrableObjectException If an object isn't valid. + * @throws UnprocessableRequestQueryException If the request's query parameters isn't valid. + * @throws LogicException If the resolver is used incorrectly. */ public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator { @@ -68,9 +66,10 @@ public function resolveParameter(ReflectionParameter $parameter, mixed $context) $type = $parameter->getType(); if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { - throw new LogicException( - 'To use the #[RequestQuery] attribute, the parameter must be typed with a DTO.' - ); + throw new LogicException(sprintf( + 'To use the #[RequestQuery] attribute, the parameter {%s} must be typed with an object.', + ParameterResolutioner::stringifyParameter($parameter), + )); } if (! $context instanceof ServerRequestInterface) { diff --git a/src/ParameterResolver/RequestRouteParameterResolver.php b/src/ParameterResolver/RequestRouteParameterResolver.php index 5d1eb423..df975527 100644 --- a/src/ParameterResolver/RequestRouteParameterResolver.php +++ b/src/ParameterResolver/RequestRouteParameterResolver.php @@ -18,8 +18,11 @@ use ReflectionNamedType; use ReflectionParameter; use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ParameterResolutioner; use Sunrise\Http\Router\RouteInterface; +use function sprintf; + /** * RequestRouteParameterResolver * @@ -31,8 +34,7 @@ final class RequestRouteParameterResolver implements ParameterResolverInterface /** * @inheritDoc * - * @throws LogicException - * If the resolver is used incorrectly. + * @throws LogicException If the resolver is used incorrectly. */ public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator { @@ -54,11 +56,12 @@ public function resolveParameter(ReflectionParameter $parameter, mixed $context) /** @var RouteInterface|null $route */ $route = $context->getAttribute('@route'); - if ($route === null && !$parameter->allowsNull()) { - throw new LogicException( + if (! $route instanceof RouteInterface && !$parameter->allowsNull()) { + throw new LogicException(sprintf( 'At this level of the application, the current request does not contain a route. ' . - 'To suppress this error, the parameter should be nullable.' - ); + 'To suppress this error, the parameter {%s} should be nullable.', + ParameterResolutioner::stringifyParameter($parameter), + )); } yield $route; diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index 998ebfa7..373fed5b 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -13,6 +13,7 @@ namespace Sunrise\Http\Router; +use Generator; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\Exception\ResolvingReferenceException; @@ -20,6 +21,7 @@ use Sunrise\Http\Router\RequestHandler\CallbackRequestHandler; use Closure; +use function class_exists; use function get_debug_type; use function is_array; use function is_callable; @@ -73,7 +75,7 @@ public function __construct( /** * @inheritDoc */ - public function resolveRequestHandler($reference): RequestHandlerInterface + public function resolveRequestHandler(mixed $reference): RequestHandlerInterface { if ($reference instanceof RequestHandlerInterface) { return $reference; @@ -83,9 +85,19 @@ public function resolveRequestHandler($reference): RequestHandlerInterface return new CallbackRequestHandler($reference, $this->parameterResolutioner, $this->responseResolutioner); } - if (is_string($reference) && is_subclass_of($reference, RequestHandlerInterface::class)) { - /** @var RequestHandlerInterface */ - return $this->classResolver->resolveClass($reference); + if (is_string($reference) && class_exists($reference)) { + if (is_subclass_of($reference, RequestHandlerInterface::class)) { + /** @var RequestHandlerInterface */ + return $this->classResolver->resolveClass($reference); + } + + if (method_exists($reference, '__invoke')) { + return new CallbackRequestHandler( + $this->classResolver->resolveClass($reference), + $this->parameterResolutioner, + $this->responseResolutioner + ); + } } // https://github.com/php/php-src/blob/3ed526441400060aa4e618b91b3352371fcd02a8/Zend/zend_API.c#L3884-L3932 @@ -105,15 +117,15 @@ public function resolveRequestHandler($reference): RequestHandlerInterface } throw new ResolvingReferenceException(sprintf( - 'Unable to resolve the reference {%s}', - $this->stringifyReference($reference) + 'Unable to resolve the reference {%s}.', + self::stringifyReference($reference) )); } /** * @inheritDoc */ - public function resolveMiddleware($reference): MiddlewareInterface + public function resolveMiddleware(mixed $reference): MiddlewareInterface { if ($reference instanceof MiddlewareInterface) { return $reference; @@ -146,22 +158,19 @@ public function resolveMiddleware($reference): MiddlewareInterface throw new ResolvingReferenceException(sprintf( 'Unable to resolve the reference {%s}', - $this->stringifyReference($reference) + self::stringifyReference($reference) )); } /** * @inheritDoc */ - public function resolveMiddlewares(array $references): array + public function resolveMiddlewares(array $references): Generator { - $middlewares = []; /** @psalm-suppress MixedAssignment */ foreach ($references as $reference) { - $middlewares[] = $this->resolveMiddleware($reference); + yield $this->resolveMiddleware($reference); } - - return $middlewares; } /** @@ -171,7 +180,7 @@ public function resolveMiddlewares(array $references): array * * @return string */ - private function stringifyReference($reference): string + public static function stringifyReference(mixed $reference): string { if (is_array($reference) && is_callable($reference, true, $stringReference)) { return $stringReference; diff --git a/src/ReferenceResolverInterface.php b/src/ReferenceResolverInterface.php index deef0e97..904b1423 100644 --- a/src/ReferenceResolverInterface.php +++ b/src/ReferenceResolverInterface.php @@ -13,9 +13,9 @@ namespace Sunrise\Http\Router; +use Generator; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\ResolvingReferenceException; /** * ReferenceResolverInterface @@ -32,12 +32,9 @@ interface ReferenceResolverInterface * * @return RequestHandlerInterface * - * @throws ResolvingReferenceException - * If the reference cannot be resolved to a request handler. - * * @since 3.0.0 */ - public function resolveRequestHandler($reference): RequestHandlerInterface; + public function resolveRequestHandler(mixed $reference): RequestHandlerInterface; /** * Resolves the given reference to a middleware @@ -46,24 +43,18 @@ public function resolveRequestHandler($reference): RequestHandlerInterface; * * @return MiddlewareInterface * - * @throws ResolvingReferenceException - * If the reference cannot be resolved to a middleware. - * * @since 3.0.0 */ - public function resolveMiddleware($reference): MiddlewareInterface; + public function resolveMiddleware(mixed $reference): MiddlewareInterface; /** * Resolves the given references to middlewares * * @param array $references * - * @return list - * - * @throws ResolvingReferenceException - * If one of the references cannot be resolved to a middleware. + * @return Generator * * @since 3.0.0 */ - public function resolveMiddlewares(array $references): array; + public function resolveMiddlewares(array $references): Generator; } diff --git a/src/RequestHandler/CallbackRequestHandler.php b/src/RequestHandler/CallbackRequestHandler.php index ea009077..c6ecf8e7 100644 --- a/src/RequestHandler/CallbackRequestHandler.php +++ b/src/RequestHandler/CallbackRequestHandler.php @@ -19,7 +19,7 @@ use ReflectionFunction; use ReflectionMethod; use Sunrise\Http\Router\ParameterResolutionerInterface; -use Sunrise\Http\Router\ParameterResolver\DirectInjectionParameterResolver; +use Sunrise\Http\Router\ParameterResolver\ObjectInjectionParameterResolver; use Sunrise\Http\Router\ResponseResolutionerInterface; use function Sunrise\Http\Router\reflect_callable; @@ -85,16 +85,18 @@ public function getReflection(): ReflectionFunction|ReflectionMethod */ public function handle(ServerRequestInterface $request): ResponseInterface { + $source = $this->getReflection(); + $arguments = $this->parameterResolutioner ->withContext($request) ->withPriorityResolver( - new DirectInjectionParameterResolver($request), + new ObjectInjectionParameterResolver($request), ) - ->resolveParameters(...$this->getReflection()->getParameters()); + ->resolveParameters(...$source->getParameters()); /** @var mixed $response */ $response = ($this->callback)(...$arguments); - return $this->responseResolutioner->resolveResponse($response, $request); + return $this->responseResolutioner->resolveResponse($response, $request, $source); } } diff --git a/src/RequestHandler/QueueableRequestHandler.php b/src/RequestHandler/QueueableRequestHandler.php index 3d036b60..476dde45 100644 --- a/src/RequestHandler/QueueableRequestHandler.php +++ b/src/RequestHandler/QueueableRequestHandler.php @@ -48,7 +48,6 @@ public function __construct(RequestHandlerInterface $requestHandler) { /** @var SplQueue */ $this->middlewareQueue = new SplQueue(); - $this->requestHandler = $requestHandler; } diff --git a/src/ResponseResolutioner.php b/src/ResponseResolutioner.php index 7408518f..f5c3b8c6 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolutioner.php @@ -14,6 +14,12 @@ namespace Sunrise\Http\Router; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionAttribute; +use ReflectionFunction; +use ReflectionMethod; +use Sunrise\Http\Router\Annotation\ResponseHeader; +use Sunrise\Http\Router\Annotation\ResponseStatus; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ResponseResolver\ResponseResolverInterface; @@ -46,24 +52,82 @@ public function addResolver(ResponseResolverInterface ...$resolvers): void /** * @inheritDoc * - * @throws LogicException If the value cannot be resolved to PSR-7 response. + * @throws LogicException If the response cannot be resolved to PSR-7 response. */ - public function resolveResponse(mixed $value, mixed $context): ResponseInterface - { - if ($value instanceof ResponseInterface) { - return $value; + public function resolveResponse( + mixed $response, + ServerRequestInterface $request, + ReflectionFunction|ReflectionMethod $source, + ) : ResponseInterface { + if ($response instanceof ResponseInterface) { + return $response; } foreach ($this->resolvers as $resolver) { - $response = $resolver->resolveResponse($value, $context); - if ($response instanceof ResponseInterface) { - return $response; + $result = $resolver->resolveResponse($response, $request, $source); + if ($result instanceof ResponseInterface) { + return $this->handleResponse($result, $source); } } throw new LogicException(sprintf( - 'Unable to resolve the value {%s} to PSR-7 response.', - get_debug_type($value), + 'Unable to resolve the response {%s} to PSR-7 response.', + self::stringifyResponse($response, $source), )); } + + /** + * Handles the given response + * + * @param ResponseInterface $response + * @param ReflectionFunction|ReflectionMethod $source + * + * @return ResponseInterface + */ + private function handleResponse( + ResponseInterface $response, + ReflectionFunction|ReflectionMethod $source, + ) : ResponseInterface { + /** @var list> $attributes */ + $attributes = $source->getAttributes(ResponseStatus::class); + if (isset($attributes[0])) { + $status = $attributes[0]->newInstance(); + $response = $response->withStatus($status->code); + } + + /** @var list> $attributes */ + $attributes = $source->getAttributes(ResponseHeader::class); + foreach ($attributes as $attribute) { + $header = $attribute->newInstance(); + $response = $response->withHeader($header->name, $header->value); + } + + return $response; + } + + /** + * Stringifies the given raw response + * + * @param mixed $response + * @param ReflectionFunction|ReflectionMethod $source + * + * @return non-empty-string + */ + public static function stringifyResponse(mixed $response, ReflectionFunction|ReflectionMethod $source): string + { + if ($source instanceof ReflectionMethod) { + return sprintf( + '%s::%s():$%s', + $source->getDeclaringClass()->getName(), + $source->getName(), + get_debug_type($response), + ); + } + + return sprintf( + '%s():$%s', + $source->getName(), + get_debug_type($response), + ); + } } diff --git a/src/ResponseResolutionerInterface.php b/src/ResponseResolutionerInterface.php index 681967fb..78baeb1f 100644 --- a/src/ResponseResolutionerInterface.php +++ b/src/ResponseResolutionerInterface.php @@ -14,6 +14,9 @@ namespace Sunrise\Http\Router; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionFunction; +use ReflectionMethod; use Sunrise\Http\Router\ResponseResolver\ResponseResolverInterface; /** @@ -34,12 +37,17 @@ interface ResponseResolutionerInterface public function addResolver(ResponseResolverInterface ...$resolvers): void; /** - * Resolves the given value to PSR-7 response + * Resolves the given response to PSR-7 response * - * @param mixed $value - * @param mixed $context + * @param mixed $response + * @param ServerRequestInterface $request + * @param ReflectionFunction|ReflectionMethod $source * * @return ResponseInterface */ - public function resolveResponse(mixed $value, mixed $context): ResponseInterface; + public function resolveResponse( + mixed $response, + ServerRequestInterface $request, + ReflectionFunction|ReflectionMethod $source, + ) : ResponseInterface; } diff --git a/src/ResponseResolver/NullResponseResolver.php b/src/ResponseResolver/EmptyResponseResolver.php similarity index 67% rename from src/ResponseResolver/NullResponseResolver.php rename to src/ResponseResolver/EmptyResponseResolver.php index 2332a0f3..cd193807 100644 --- a/src/ResponseResolver/NullResponseResolver.php +++ b/src/ResponseResolver/EmptyResponseResolver.php @@ -15,13 +15,16 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionFunction; +use ReflectionMethod; /** - * NullResponseResolver + * EmptyResponseResolver * * @since 3.0.0 */ -final class NullResponseResolver implements ResponseResolverInterface +final class EmptyResponseResolver implements ResponseResolverInterface { /** @@ -36,9 +39,12 @@ public function __construct(private ResponseFactoryInterface $responseFactory) /** * @inheritDoc */ - public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface - { - if ($value === null) { + public function resolveResponse( + mixed $response, + ServerRequestInterface $request, + ReflectionFunction|ReflectionMethod $source, + ) : ?ResponseInterface { + if ($response === null) { return $this->responseFactory->createResponse(204); } diff --git a/src/ResponseResolver/ResponseBodyResponseResolver.php b/src/ResponseResolver/ResponseBodyResponseResolver.php index ad751d92..f47e2750 100644 --- a/src/ResponseResolver/ResponseBodyResponseResolver.php +++ b/src/ResponseResolver/ResponseBodyResponseResolver.php @@ -15,12 +15,20 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; -use ReflectionClass; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionFunction; +use ReflectionMethod; use Sunrise\Http\Router\Annotation\ResponseBody; use Sunrise\Http\Router\Entity\MediaType; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ResponseResolutioner; +use Sunrise\Http\Router\RouteInterface; +use Sunrise\Http\Router\ServerRequest; use Symfony\Component\Serializer\SerializerInterface; -use function is_object; +use function next; +use function reset; +use function sprintf; /** * ResponseBodyResponseResolver @@ -30,59 +38,91 @@ final class ResponseBodyResponseResolver implements ResponseResolverInterface { + /** + * @var array + */ + public const MEDIA_TYPE_FORMATS = [ + MediaType::APPLICATION_JSON => 'json', + MediaType::APPLICATION_XML => 'xml', + MediaType::APPLICATION_YAML => 'yaml', + ]; + /** * Constructor of the class * - * @param SerializerInterface $serializer * @param ResponseFactoryInterface $responseFactory + * @param SerializerInterface $serializer + * @param non-empty-string $defaultMediaType + * @param array $mediaTypeFormats + * @param array $serializationContext */ public function __construct( - private SerializerInterface $serializer, private ResponseFactoryInterface $responseFactory, + private SerializerInterface $serializer, + private string $defaultMediaType = MediaType::APPLICATION_JSON, + private array $mediaTypeFormats = self::MEDIA_TYPE_FORMATS, + private array $serializationContext = [], ) { } /** * @inheritDoc + * + * @throws LogicException If the resolver is used incorrectly. */ - public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface - { - if (!is_object($value)) { + public function resolveResponse( + mixed $response, + ServerRequestInterface $request, + ReflectionFunction|ReflectionMethod $source, + ) : ?ResponseInterface { + if ($source->getAttributes(ResponseBody::class) === []) { return null; } - $class = new ReflectionClass($value::class); - $attributes = $class->getAttributes(ResponseBody::class); - if ($attributes === []) { - return null; + $mediaType = $this->defaultMediaType; + + /** @var RouteInterface|null $route */ + $route = $request->getAttribute('@route'); + if ($route instanceof RouteInterface) { + $producedMediaTypes = $route->getProducedMediaTypes(); + if (!empty($producedMediaTypes)) { + /** @var non-empty-string $mediaType */ + $mediaType = reset($producedMediaTypes); + $requestProxy = ServerRequest::from($request); + $consumedMediaTypes = $requestProxy->getClientConsumedMediaTypes(); + while ($producedMediaType = next($producedMediaTypes)) { + if (isset($consumedMediaTypes[$producedMediaType])) { + /** @var non-empty-string $mediaType */ + $mediaType = $consumedMediaTypes[$producedMediaType]; + break; + } + } + } } - /** - * @var ResponseBody $responseBody - * @psalm-suppress UnnecessaryVarAnnotation - */ - $responseBody = $attributes[0]->newInstance(); - - $response = $this->responseFactory->createResponse($responseBody->statusCode); + if (!isset($this->mediaTypeFormats[$mediaType])) { + throw new LogicException(sprintf( + 'The response {%s} cannot be serialized. ' . + 'Make sure that the resolver %s is configured correctly, ' . + 'and also ensure that the current route provides media types known to the resolver.', + ResponseResolutioner::stringifyResponse($response, $source), + ResponseBodyResponseResolver::class, + )); + } - $mediaType = match ($responseBody->format) { - ResponseBody::FORMAT_CSV => MediaType::TEXT_CSV, - ResponseBody::FORMAT_JSON => MediaType::APPLICATION_JSON, - ResponseBody::FORMAT_XML => MediaType::APPLICATION_XML, - ResponseBody::FORMAT_YAML => MediaType::APPLICATION_YAML, - default => null, - }; + $payload = $this->serializer->serialize( + $response, + $this->mediaTypeFormats[$mediaType], + $this->serializationContext, + ); - if (isset($mediaType)) { - $response = $response->withHeader('Content-Type', $mediaType); - } + $contentType = sprintf('%s; charset=UTF-8', $mediaType); - foreach ($responseBody->headers as $name => $header) { - $response = $response->withHeader($name, $header); - } + $result = $this->responseFactory->createResponse(200) + ->withHeader('Content-Type', $contentType); - $response->getBody()->write($this->serializer->serialize($value, $responseBody->format)); + $result->getBody()->write($payload); - return $response; + return $result; } } diff --git a/src/ResponseResolver/ResponseResolverInterface.php b/src/ResponseResolver/ResponseResolverInterface.php index 49c32860..e6d28126 100644 --- a/src/ResponseResolver/ResponseResolverInterface.php +++ b/src/ResponseResolver/ResponseResolverInterface.php @@ -14,6 +14,9 @@ namespace Sunrise\Http\Router\ResponseResolver; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionFunction; +use ReflectionMethod; /** * ResponseResolverInterface @@ -24,12 +27,17 @@ interface ResponseResolverInterface { /** - * Resolves the given value to PSR-7 response + * Resolves the given response to PSR-7 response * - * @param mixed $value - * @param mixed $context + * @param mixed $response + * @param ServerRequestInterface $request + * @param ReflectionFunction|ReflectionMethod $source * * @return ResponseInterface|null */ - public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface; + public function resolveResponse( + mixed $response, + ServerRequestInterface $request, + ReflectionFunction|ReflectionMethod $source, + ) : ?ResponseInterface; } diff --git a/src/ResponseResolver/RouteResponseResolver.php b/src/ResponseResolver/RouteResponseResolver.php index ca38ee0e..a0e4d0b3 100644 --- a/src/ResponseResolver/RouteResponseResolver.php +++ b/src/ResponseResolver/RouteResponseResolver.php @@ -15,7 +15,8 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use Sunrise\Http\Router\Exception\LogicException; +use ReflectionFunction; +use ReflectionMethod; use Sunrise\Http\Router\RouteInterface; /** @@ -28,22 +29,16 @@ final class RouteResponseResolver implements ResponseResolverInterface /** * @inheritDoc - * - * @throws LogicException - * If the resolver is used incorrectly. */ - public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface - { - if (! $value instanceof RouteInterface) { - return null; + public function resolveResponse( + mixed $response, + ServerRequestInterface $request, + ReflectionFunction|ReflectionMethod $source, + ) : ?ResponseInterface { + if ($response instanceof RouteInterface) { + return $response->handle($request); } - if (! $context instanceof ServerRequestInterface) { - throw new LogicException( - 'At this level of the application, any operations with the request are not possible.' - ); - } - - return $value->handle($context); + return null; } } diff --git a/src/ResponseResolver/StatusCodeResponseResolver.php b/src/ResponseResolver/StreamResponseResolver.php similarity index 58% rename from src/ResponseResolver/StatusCodeResponseResolver.php rename to src/ResponseResolver/StreamResponseResolver.php index 24e6f66a..b06d7a2a 100644 --- a/src/ResponseResolver/StatusCodeResponseResolver.php +++ b/src/ResponseResolver/StreamResponseResolver.php @@ -15,15 +15,17 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; - -use function is_int; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Message\StreamInterface; +use ReflectionFunction; +use ReflectionMethod; /** - * StatusCodeResponseResolver + * StreamResponseResolver * * @since 3.0.0 */ -final class StatusCodeResponseResolver implements ResponseResolverInterface +final class StreamResponseResolver implements ResponseResolverInterface { /** @@ -38,10 +40,14 @@ public function __construct(private ResponseFactoryInterface $responseFactory) /** * @inheritDoc */ - public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface - { - if (is_int($value) && $value >= 100 && $value <= 599) { - return $this->responseFactory->createResponse($value); + public function resolveResponse( + mixed $response, + ServerRequestInterface $request, + ReflectionFunction|ReflectionMethod $source, + ) : ?ResponseInterface { + if ($response instanceof StreamInterface) { + return $this->responseFactory->createResponse(200) + ->withBody($response); } return null; diff --git a/src/ResponseResolver/UriResponseResolver.php b/src/ResponseResolver/UriResponseResolver.php index 5638a5f7..b21ad271 100644 --- a/src/ResponseResolver/UriResponseResolver.php +++ b/src/ResponseResolver/UriResponseResolver.php @@ -15,7 +15,10 @@ use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\UriInterface; +use ReflectionFunction; +use ReflectionMethod; /** * UriResponseResolver @@ -37,13 +40,16 @@ public function __construct(private ResponseFactoryInterface $responseFactory) /** * @inheritDoc */ - public function resolveResponse(mixed $value, mixed $context): ?ResponseInterface - { - if (! $value instanceof UriInterface) { - return null; + public function resolveResponse( + mixed $response, + ServerRequestInterface $request, + ReflectionFunction|ReflectionMethod $source, + ) : ?ResponseInterface { + if ($response instanceof UriInterface) { + return $this->responseFactory->createResponse(302) + ->withHeader('Location', $response->__toString()); } - return $this->responseFactory->createResponse(302) - ->withHeader('Location', $value->__toString()); + return null; } } diff --git a/src/Route.php b/src/Route.php index af224fa5..376e34f0 100644 --- a/src/Route.php +++ b/src/Route.php @@ -17,6 +17,8 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use ReflectionFunction; +use ReflectionMethod; use Sunrise\Http\Router\RequestHandler\CallbackRequestHandler; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; use ReflectionClass; @@ -242,7 +244,7 @@ public function getTags(): array /** * @inheritDoc */ - public function getHolder(): Reflector + public function getHolder(): ReflectionClass|ReflectionMethod|ReflectionFunction { if ($this->requestHandler instanceof CallbackRequestHandler) { return $this->requestHandler->getReflection(); diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 64012cb7..2b77f784 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -13,6 +13,7 @@ namespace Sunrise\Http\Router; +use Fig\Http\Message\RequestMethodInterface; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ParameterResolver\ParameterResolverInterface; use Sunrise\Http\Router\ResponseResolver\ResponseResolverInterface; @@ -167,7 +168,7 @@ public function route( $path, $methods, $this->referenceResolver->resolveRequestHandler($requestHandler), - $this->referenceResolver->resolveMiddlewares($middlewares), + [...$this->referenceResolver->resolveMiddlewares($middlewares)], $attributes ); @@ -365,7 +366,7 @@ public function purge( return $this->route( $name, $path, - [RouteInterface::METHOD_PURGE], + [RequestMethodInterface::METHOD_PURGE], $requestHandler, $middlewares, $attributes diff --git a/src/RouteInterface.php b/src/RouteInterface.php index 7833a663..a1e32cd9 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -139,7 +139,7 @@ public function getTags(): array; * * @since 2.14.0 */ - public function getHolder(): Reflector; + public function getHolder(): ReflectionClass|ReflectionMethod|ReflectionFunction; /** * Sets the given name to the route diff --git a/src/ServerRequest.php b/src/ServerRequest.php index afa4fcd1..0e754838 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -180,7 +180,7 @@ public function getClientConsumedMediaTypes(): array return []; } - $accepts = header_accept_like_parse($header); + $accepts = parse_accept_header($header); if (empty($accepts)) { return []; } From f09d6a1c38df2e177c9d80666c3ade8ab8128879 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Wed, 2 Aug 2023 12:40:32 +0200 Subject: [PATCH 077/180] v3 --- src/Annotation/Middleware.php | 5 +- src/Annotation/Route.php | 29 ++++---- src/ClassResolver.php | 30 +++++---- src/ClassResolverInterface.php | 13 +--- src/ReferenceResolver.php | 119 ++++++++++++++++++++------------- 5 files changed, 108 insertions(+), 88 deletions(-) diff --git a/src/Annotation/Middleware.php b/src/Annotation/Middleware.php index 980dc8ff..19dd49ed 100644 --- a/src/Annotation/Middleware.php +++ b/src/Annotation/Middleware.php @@ -14,7 +14,6 @@ namespace Sunrise\Http\Router\Annotation; use Attribute; -use Psr\Http\Server\MiddlewareInterface; /** * @since 2.11.0 @@ -26,9 +25,9 @@ final class Middleware /** * Constructor of the class * - * @param class-string $value + * @param mixed $value */ - public function __construct(public string $value) + public function __construct(public mixed $value) { } } diff --git a/src/Annotation/Route.php b/src/Annotation/Route.php index 43408464..5b6ea783 100644 --- a/src/Annotation/Route.php +++ b/src/Annotation/Route.php @@ -15,7 +15,6 @@ use Attribute; use Fig\Http\Message\RequestMethodInterface; -use Psr\Http\Server\MiddlewareInterface; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class Route implements RequestMethodInterface @@ -24,6 +23,8 @@ final class Route implements RequestMethodInterface /** * The annotation's holder * + * @var mixed + * * @internal */ public mixed $holder = null; @@ -31,19 +32,19 @@ final class Route implements RequestMethodInterface /** * Constructor of the class * - * @param non-empty-string $name The route's name - * @param non-empty-string|null $host The route's host - * @param non-empty-string $path The route's path - * @param non-empty-string|null $method The route's method - * @param list $methods The route's methods - * @param list $consumes The route's consumed media types - * @param list $produces The route's produced media types - * @param list> $middlewares The route's middlewares - * @param array $attributes The route's attributes - * @param string $summary The route's summary - * @param string $description The route's description - * @param list $tags The route's tags - * @param int $priority The route's priority (default 0) + * @param non-empty-string $name The route's name + * @param non-empty-string|null $host The route's host + * @param non-empty-string $path The route's path + * @param non-empty-string|null $method The route's method + * @param list $methods The route's methods + * @param list $consumes The route's consumed media types + * @param list $produces The route's produced media types + * @param list $middlewares The route's middlewares + * @param array $attributes The route's attributes + * @param string $summary The route's summary + * @param string $description The route's description + * @param list $tags The route's tags + * @param int $priority The route's priority (default 0) */ public function __construct( public string $name, diff --git a/src/ClassResolver.php b/src/ClassResolver.php index 96319547..d457a173 100644 --- a/src/ClassResolver.php +++ b/src/ClassResolver.php @@ -18,6 +18,7 @@ use ReflectionClass; use function class_exists; +use function is_object; use function sprintf; /** @@ -54,39 +55,44 @@ public function __construct(ParameterResolutionerInterface $parameterResolutione /** * @inheritDoc + * + * @throws InvalidArgumentException If the class doesn't exist. + * @throws LogicException If the class cannot be resolved. */ - public function resolveClass(string $className): object + public function resolveClass(string $fqn): object { - if (isset($this->resolvedClasses[$className])) { - return $this->resolvedClasses[$className]; + if (isset($this->resolvedClasses[$fqn])) { + return $this->resolvedClasses[$fqn]; } - if (!class_exists($className)) { + if (!class_exists($fqn)) { throw new InvalidArgumentException(sprintf( 'Class %s does not exist', - $className + $fqn )); } - $reflection = new ReflectionClass($className); - if (!$reflection->isInstantiable()) { + $class = new ReflectionClass($fqn); + if (!$class->isInstantiable()) { throw new LogicException(sprintf( 'Class %s cannot be initialized', - $className + $fqn )); } $arguments = []; - $constructor = $reflection->getConstructor(); + $constructor = $class->getConstructor(); if (isset($constructor) && $constructor->getNumberOfParameters() > 0) { $arguments = $this->parameterResolutioner->resolveParameters( ...$constructor->getParameters() ); } - /** @var T */ - $this->resolvedClasses[$className] = $reflection->newInstance(...$arguments); + /** @var T $instance */ + $instance = new $class(...$arguments); + + $this->resolvedClasses[$fqn] = $instance; - return $this->resolvedClasses[$className]; + return $this->resolvedClasses[$fqn]; } } diff --git a/src/ClassResolverInterface.php b/src/ClassResolverInterface.php index 164b08d6..c20ef413 100644 --- a/src/ClassResolverInterface.php +++ b/src/ClassResolverInterface.php @@ -13,9 +13,6 @@ namespace Sunrise\Http\Router; -use Sunrise\Http\Router\Exception\InvalidArgumentException; -use Sunrise\Http\Router\Exception\LogicException; - /** * ClassResolverInterface * @@ -29,15 +26,9 @@ interface ClassResolverInterface /** * Resolves the given named class * - * @param class-string $className + * @param class-string $fqn * * @return T - * - * @throws InvalidArgumentException - * If the class doesn't exist. - * - * @throws LogicException - * If the class cannot be resolved. */ - public function resolveClass(string $className): object; + public function resolveClass(string $fqn): object; } diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index 373fed5b..073a8319 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -13,13 +13,13 @@ namespace Sunrise\Http\Router; +use Closure; use Generator; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Exception\ResolvingReferenceException; +use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\Middleware\CallbackMiddleware; use Sunrise\Http\Router\RequestHandler\CallbackRequestHandler; -use Closure; use function class_exists; use function get_debug_type; @@ -38,11 +38,6 @@ final class ReferenceResolver implements ReferenceResolverInterface { - /** - * @var ClassResolverInterface - */ - private ClassResolverInterface $classResolver; - /** * @var ParameterResolutionerInterface */ @@ -53,6 +48,11 @@ final class ReferenceResolver implements ReferenceResolverInterface */ private ResponseResolutionerInterface $responseResolutioner; + /** + * @var ClassResolverInterface + */ + private ClassResolverInterface $classResolver; + /** * Constructor of the class * @@ -67,13 +67,15 @@ public function __construct( ) { $classResolver ??= new ClassResolver($parameterResolutioner); - $this->classResolver = $classResolver; $this->parameterResolutioner = $parameterResolutioner; $this->responseResolutioner = $responseResolutioner; + $this->classResolver = $classResolver; } /** * @inheritDoc + * + * @throws LogicException If the reference cannot be resolved. */ public function resolveRequestHandler(mixed $reference): RequestHandlerInterface { @@ -82,7 +84,28 @@ public function resolveRequestHandler(mixed $reference): RequestHandlerInterface } if ($reference instanceof Closure) { - return new CallbackRequestHandler($reference, $this->parameterResolutioner, $this->responseResolutioner); + return new CallbackRequestHandler( + $reference, + $this->parameterResolutioner, + $this->responseResolutioner, + ); + } + + // https://github.com/php/php-src/blob/3ed526441400060aa4e618b91b3352371fcd02a8/Zend/zend_API.c#L3884-L3932 + if (is_array($reference) && is_callable($reference, true)) { + /** @var array{0: class-string|object, 1: non-empty-string} $reference */ + + if (is_string($reference[0])) { + $reference[0] = $this->classResolver->resolveClass($reference[0]); + } + + if (is_callable($reference)) { + return new CallbackRequestHandler( + $reference, + $this->parameterResolutioner, + $this->responseResolutioner, + ); + } } if (is_string($reference) && class_exists($reference)) { @@ -95,35 +118,21 @@ public function resolveRequestHandler(mixed $reference): RequestHandlerInterface return new CallbackRequestHandler( $this->classResolver->resolveClass($reference), $this->parameterResolutioner, - $this->responseResolutioner + $this->responseResolutioner, ); } } - // https://github.com/php/php-src/blob/3ed526441400060aa4e618b91b3352371fcd02a8/Zend/zend_API.c#L3884-L3932 - /** @psalm-suppress MixedArgument */ - if (is_array($reference) && is_callable($reference, true) && method_exists($reference[0], $reference[1])) { - /** @var array{0: class-string|object, 1: non-empty-string} $reference */ - - if (is_string($reference[0])) { - $reference[0] = $this->classResolver->resolveClass($reference[0]); - } - - return new CallbackRequestHandler( - [$reference[0], $reference[1]], - $this->parameterResolutioner, - $this->responseResolutioner - ); - } - - throw new ResolvingReferenceException(sprintf( + throw new LogicException(sprintf( 'Unable to resolve the reference {%s}.', - self::stringifyReference($reference) + self::stringifyReference($reference), )); } /** * @inheritDoc + * + * @throws LogicException If the reference cannot be resolved. */ public function resolveMiddleware(mixed $reference): MiddlewareInterface { @@ -132,38 +141,55 @@ public function resolveMiddleware(mixed $reference): MiddlewareInterface } if ($reference instanceof Closure) { - return new CallbackMiddleware($reference, $this->parameterResolutioner, $this->responseResolutioner); + return new CallbackMiddleware( + $reference, + $this->parameterResolutioner, + $this->responseResolutioner, + ); } - if (is_string($reference) && is_subclass_of($reference, MiddlewareInterface::class)) { - /** @var MiddlewareInterface */ - return $this->classResolver->resolveClass($reference); + if (is_string($reference) && class_exists($reference)) { + if (is_subclass_of($reference, MiddlewareInterface::class)) { + /** @var MiddlewareInterface */ + return $this->classResolver->resolveClass($reference); + } + + if (method_exists($reference, '__invoke')) { + return new CallbackMiddleware( + $this->classResolver->resolveClass($reference), + $this->parameterResolutioner, + $this->responseResolutioner, + ); + } } // https://github.com/php/php-src/blob/3ed526441400060aa4e618b91b3352371fcd02a8/Zend/zend_API.c#L3884-L3932 - /** @psalm-suppress MixedArgument */ - if (is_array($reference) && is_callable($reference, true) && method_exists($reference[0], $reference[1])) { + if (is_array($reference) && is_callable($reference, true)) { /** @var array{0: class-string|object, 1: non-empty-string} $reference */ if (is_string($reference[0])) { $reference[0] = $this->classResolver->resolveClass($reference[0]); } - return new CallbackMiddleware( - [$reference[0], $reference[1]], - $this->parameterResolutioner, - $this->responseResolutioner - ); + if (is_callable($reference)) { + return new CallbackMiddleware( + $reference, + $this->parameterResolutioner, + $this->responseResolutioner, + ); + } } - throw new ResolvingReferenceException(sprintf( - 'Unable to resolve the reference {%s}', - self::stringifyReference($reference) + throw new LogicException(sprintf( + 'Unable to resolve the reference {%s}.', + self::stringifyReference($reference), )); } /** * @inheritDoc + * + * @throws LogicException If one of the references cannot be resolved. */ public function resolveMiddlewares(array $references): Generator { @@ -182,12 +208,9 @@ public function resolveMiddlewares(array $references): Generator */ public static function stringifyReference(mixed $reference): string { - if (is_array($reference) && is_callable($reference, true, $stringReference)) { - return $stringReference; - } - - if (is_string($reference)) { - return $reference; + // https://github.com/php/php-src/blob/3ed526441400060aa4e618b91b3352371fcd02a8/Zend/zend_API.c#L3884-L3932 + if (is_callable($reference, true, $result)) { + return $result; } return get_debug_type($reference); From ee61aa946514f913a8ba0587a3255610fdd92476 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Wed, 2 Aug 2023 12:59:51 +0200 Subject: [PATCH 078/180] v3 --- src/ResponseResolver/ResponseBodyResponseResolver.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/ResponseResolver/ResponseBodyResponseResolver.php b/src/ResponseResolver/ResponseBodyResponseResolver.php index f47e2750..b4b04305 100644 --- a/src/ResponseResolver/ResponseBodyResponseResolver.php +++ b/src/ResponseResolver/ResponseBodyResponseResolver.php @@ -86,14 +86,11 @@ public function resolveResponse( if ($route instanceof RouteInterface) { $producedMediaTypes = $route->getProducedMediaTypes(); if (!empty($producedMediaTypes)) { - /** @var non-empty-string $mediaType */ $mediaType = reset($producedMediaTypes); - $requestProxy = ServerRequest::from($request); - $consumedMediaTypes = $requestProxy->getClientConsumedMediaTypes(); + $consumedMediaTypes = ServerRequest::from($request)->getClientConsumedMediaTypes(); while ($producedMediaType = next($producedMediaTypes)) { if (isset($consumedMediaTypes[$producedMediaType])) { - /** @var non-empty-string $mediaType */ - $mediaType = $consumedMediaTypes[$producedMediaType]; + $mediaType = $producedMediaType; break; } } From ca227476ba82495e37043e2b9582fbf67fab5060 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 6 Aug 2023 09:09:45 +0200 Subject: [PATCH 079/180] v3 --- composer.json | 7 +- functions/emit.php | 8 +- functions/parse_accept_header.php | 198 -------------- functions/parse_header.php | 178 +++++++++++++ functions/parse_header_with_media_type.php | 91 +++++++ ...lect_callable.php => reflect_callback.php} | 5 +- src/Annotation/Consume.php | 5 +- src/Annotation/Produce.php | 5 +- src/Annotation/ProxyChain.php | 36 +++ src/Annotation/Route.php | 19 +- src/ClassResolver.php | 13 +- src/Dictionary/Charset.php | 72 ++++++ ...{IpAddress.php => ClientRemoteAddress.php} | 22 +- src/Entity/MediaType.php | 85 +++++- src/Loader/DescriptorLoader.php | 40 ++- src/Middleware/CallbackMiddleware.php | 4 +- .../JsonPayloadDecodingMiddleware.php | 20 +- .../SimdJsonPayloadDecodingMiddleware.php | 20 +- src/ParameterResolutioner.php | 4 +- .../ClientRemoteAddressParameterResolver.php | 75 ++++++ .../RequestRouteParameterResolver.php | 6 +- src/ReferenceResolver.php | 4 +- src/RequestHandler/CallbackRequestHandler.php | 4 +- src/ResponseResolutioner.php | 2 +- .../ResponseBodyResponseResolver.php | 59 ++--- src/Route.php | 42 +-- src/RouteCollection.php | 17 +- src/RouteCollectionInterface.php | 25 +- src/RouteInterface.php | 38 +-- src/Router.php | 7 +- src/ServerRequest.php | 241 +++++------------- 31 files changed, 781 insertions(+), 571 deletions(-) delete mode 100644 functions/parse_accept_header.php create mode 100644 functions/parse_header.php create mode 100644 functions/parse_header_with_media_type.php rename functions/{reflect_callable.php => reflect_callback.php} (90%) create mode 100644 src/Annotation/ProxyChain.php create mode 100644 src/Dictionary/Charset.php rename src/Entity/{IpAddress.php => ClientRemoteAddress.php} (70%) create mode 100644 src/ParameterResolver/ClientRemoteAddressParameterResolver.php diff --git a/composer.json b/composer.json index 736e4d2c..bab7bb85 100644 --- a/composer.json +++ b/composer.json @@ -44,17 +44,20 @@ "vimeo/psalm": "^5.14", "sunrise/coding-standard": "^1.0", "sunrise/http-message": "^3.0", - "symfony/console": "^6.0" + "symfony/console": "^6.0", + "symfony/http-foundation": "^6.3" }, "autoload": { "files": [ "functions/emit.php", + "functions/parse_header.php", + "functions/parse_header_with_media_type.php", "functions/path_build.php", "functions/path_match.php", "functions/path_parse.php", "functions/path_plain.php", "functions/path_regex.php", - "functions/reflect_callable.php" + "functions/reflect_callback.php" ], "psr-4": { "Sunrise\\Http\\Router\\": "src/" diff --git a/functions/emit.php b/functions/emit.php index 50bf35c6..643229f9 100644 --- a/functions/emit.php +++ b/functions/emit.php @@ -32,15 +32,11 @@ function emit(ResponseInterface $response): void $response->getProtocolVersion(), $response->getStatusCode(), $response->getReasonPhrase() - ), true); + )); foreach ($response->getHeaders() as $name => $values) { foreach ($values as $value) { - header(sprintf( - '%s: %s', - $name, - $value - ), false); + header(sprintf('%s: %s', $name, $value), replace: false); } } diff --git a/functions/parse_accept_header.php b/functions/parse_accept_header.php deleted file mode 100644 index c8b41ee9..00000000 --- a/functions/parse_accept_header.php +++ /dev/null @@ -1,198 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-message - */ - -namespace Sunrise\Http\Message; - -use function uasort; - -/** - * Parses the given accept-like header - * - * Returns null if the header isn't valid. - * - * @param string $header - * - * @return ?array> - */ -function parse_accept_header(string $header): ?array -{ - // OWS according to RFC-7230 - static $ows = [ - "\x09" => 1, "\x20" => 1, - ]; - - // token according to RFC-7230 - static $token = [ - "\x21" => 1, "\x23" => 1, "\x24" => 1, "\x25" => 1, "\x26" => 1, "\x27" => 1, "\x2a" => 1, "\x2b" => 1, - "\x2d" => 1, "\x2e" => 1, "\x30" => 1, "\x31" => 1, "\x32" => 1, "\x33" => 1, "\x34" => 1, "\x35" => 1, - "\x36" => 1, "\x37" => 1, "\x38" => 1, "\x39" => 1, "\x41" => 1, "\x42" => 1, "\x43" => 1, "\x44" => 1, - "\x45" => 1, "\x46" => 1, "\x47" => 1, "\x48" => 1, "\x49" => 1, "\x4a" => 1, "\x4b" => 1, "\x4c" => 1, - "\x4d" => 1, "\x4e" => 1, "\x4f" => 1, "\x50" => 1, "\x51" => 1, "\x52" => 1, "\x53" => 1, "\x54" => 1, - "\x55" => 1, "\x56" => 1, "\x57" => 1, "\x58" => 1, "\x59" => 1, "\x5a" => 1, "\x5e" => 1, "\x5f" => 1, - "\x60" => 1, "\x61" => 1, "\x62" => 1, "\x63" => 1, "\x64" => 1, "\x65" => 1, "\x66" => 1, "\x67" => 1, - "\x68" => 1, "\x69" => 1, "\x6a" => 1, "\x6b" => 1, "\x6c" => 1, "\x6d" => 1, "\x6e" => 1, "\x6f" => 1, - "\x70" => 1, "\x71" => 1, "\x72" => 1, "\x73" => 1, "\x74" => 1, "\x75" => 1, "\x76" => 1, "\x77" => 1, - "\x78" => 1, "\x79" => 1, "\x7a" => 1, "\x7c" => 1, "\x7e" => 1, - ]; - - // quoted-string according to RFC-7230 - static $quotedString = [ - "\x09" => 1, "\x20" => 1, "\x21" => 1, "\x23" => 1, "\x24" => 1, "\x25" => 1, "\x26" => 1, "\x27" => 1, - "\x28" => 1, "\x29" => 1, "\x2a" => 1, "\x2b" => 1, "\x2c" => 1, "\x2d" => 1, "\x2e" => 1, "\x2f" => 1, - "\x30" => 1, "\x31" => 1, "\x32" => 1, "\x33" => 1, "\x34" => 1, "\x35" => 1, "\x36" => 1, "\x37" => 1, - "\x38" => 1, "\x39" => 1, "\x3a" => 1, "\x3b" => 1, "\x3c" => 1, "\x3d" => 1, "\x3e" => 1, "\x3f" => 1, - "\x40" => 1, "\x41" => 1, "\x42" => 1, "\x43" => 1, "\x44" => 1, "\x45" => 1, "\x46" => 1, "\x47" => 1, - "\x48" => 1, "\x49" => 1, "\x4a" => 1, "\x4b" => 1, "\x4c" => 1, "\x4d" => 1, "\x4e" => 1, "\x4f" => 1, - "\x50" => 1, "\x51" => 1, "\x52" => 1, "\x53" => 1, "\x54" => 1, "\x55" => 1, "\x56" => 1, "\x57" => 1, - "\x58" => 1, "\x59" => 1, "\x5a" => 1, "\x5b" => 1, "\x5d" => 1, "\x5e" => 1, "\x5f" => 1, "\x60" => 1, - "\x61" => 1, "\x62" => 1, "\x63" => 1, "\x64" => 1, "\x65" => 1, "\x66" => 1, "\x67" => 1, "\x68" => 1, - "\x69" => 1, "\x6a" => 1, "\x6b" => 1, "\x6c" => 1, "\x6d" => 1, "\x6e" => 1, "\x6f" => 1, "\x70" => 1, - "\x71" => 1, "\x72" => 1, "\x73" => 1, "\x74" => 1, "\x75" => 1, "\x76" => 1, "\x77" => 1, "\x78" => 1, - "\x79" => 1, "\x7a" => 1, "\x7b" => 1, "\x7c" => 1, "\x7d" => 1, "\x7e" => 1, "\x80" => 1, "\x81" => 1, - "\x82" => 1, "\x83" => 1, "\x84" => 1, "\x85" => 1, "\x86" => 1, "\x87" => 1, "\x88" => 1, "\x89" => 1, - "\x8a" => 1, "\x8b" => 1, "\x8c" => 1, "\x8d" => 1, "\x8e" => 1, "\x8f" => 1, "\x90" => 1, "\x91" => 1, - "\x92" => 1, "\x93" => 1, "\x94" => 1, "\x95" => 1, "\x96" => 1, "\x97" => 1, "\x98" => 1, "\x99" => 1, - "\x9a" => 1, "\x9b" => 1, "\x9c" => 1, "\x9d" => 1, "\x9e" => 1, "\x9f" => 1, "\xa0" => 1, "\xa1" => 1, - "\xa2" => 1, "\xa3" => 1, "\xa4" => 1, "\xa5" => 1, "\xa6" => 1, "\xa7" => 1, "\xa8" => 1, "\xa9" => 1, - "\xaa" => 1, "\xab" => 1, "\xac" => 1, "\xad" => 1, "\xae" => 1, "\xaf" => 1, "\xb0" => 1, "\xb1" => 1, - "\xb2" => 1, "\xb3" => 1, "\xb4" => 1, "\xb5" => 1, "\xb6" => 1, "\xb7" => 1, "\xb8" => 1, "\xb9" => 1, - "\xba" => 1, "\xbb" => 1, "\xbc" => 1, "\xbd" => 1, "\xbe" => 1, "\xbf" => 1, "\xc0" => 1, "\xc1" => 1, - "\xc2" => 1, "\xc3" => 1, "\xc4" => 1, "\xc5" => 1, "\xc6" => 1, "\xc7" => 1, "\xc8" => 1, "\xc9" => 1, - "\xca" => 1, "\xcb" => 1, "\xcc" => 1, "\xcd" => 1, "\xce" => 1, "\xcf" => 1, "\xd0" => 1, "\xd1" => 1, - "\xd2" => 1, "\xd3" => 1, "\xd4" => 1, "\xd5" => 1, "\xd6" => 1, "\xd7" => 1, "\xd8" => 1, "\xd9" => 1, - "\xda" => 1, "\xdb" => 1, "\xdc" => 1, "\xdd" => 1, "\xde" => 1, "\xdf" => 1, "\xe0" => 1, "\xe1" => 1, - "\xe2" => 1, "\xe3" => 1, "\xe4" => 1, "\xe5" => 1, "\xe6" => 1, "\xe7" => 1, "\xe8" => 1, "\xe9" => 1, - "\xea" => 1, "\xeb" => 1, "\xec" => 1, "\xed" => 1, "\xee" => 1, "\xef" => 1, "\xf0" => 1, "\xf1" => 1, - "\xf2" => 1, "\xf3" => 1, "\xf4" => 1, "\xf5" => 1, "\xf6" => 1, "\xf7" => 1, "\xf8" => 1, "\xf9" => 1, - "\xfa" => 1, "\xfb" => 1, "\xfc" => 1, "\xfd" => 1, "\xfe" => 1, "\xff" => 1, - ]; - - $offset = -1; - - $inToken = true; - $inParamName = false; - $inParamValue = false; - $inQuotes = false; - - $data = []; - - $i = 0; - $p = 0; - - while (true) { - $char = $header[++$offset] ?? null; - - if (!isset($char)) { - break; - } - - $prev = $header[$offset-1] ?? null; - $next = $header[$offset+1] ?? null; - - if ($char === ',' && !$inQuotes) { - $inToken = true; - $inParamName = false; - $inParamValue = false; - $i++; - $p = 0; - continue; - } - if ($char === ';' && !$inQuotes) { - $inToken = false; - $inParamName = true; - $inParamValue = false; - $p++; - continue; - } - if ($char === '=' && !$inQuotes) { - $inToken = false; - $inParamName = false; - $inParamValue = true; - continue; - } - - // ignoring whitespaces around tokens... - if (isset($ows[$char]) && !$inQuotes) { - // en-GB[ ], ... - // ~~~~~~^~~~~~~ - if ($inToken && isset($data[$i][0])) { - $inToken = false; - } - // en-GB; q[ ]= 0.8, ... - // ~~~~~~~~~^~~~~~~~~~~~ - if ($inParamName && isset($data[$i][1][$p][0])) { - $inParamName = false; - } - // en-GB; q = 0.8[ ], ... - // ~~~~~~~~~~~~~~~^~~~~~~ - if ($inParamValue && isset($data[$i][1][$p][1])) { - $inParamValue = false; - } - - continue; - } - - // ignoring backslashes before double quotes in the quoted parameter value... - if ($char === '\\' && $next === '"' && $inQuotes) { - continue; - } - - if ($char === '"' && $inParamValue && !$inQuotes && !isset($data[$i][1][$p][1])) { - $inQuotes = true; - continue; - } - if ($char === '"' && $prev !== '\\' && $inQuotes) { - $inParamValue = false; - $inQuotes = false; - continue; - } - - // [en-GB]; q=0.8, ... - // ~^^^^^~~~~~~~~~~~~~ - if ($inToken && (isset($token[$char]) || $char === '/')) { - $data[$i][0] ??= ''; - $data[$i][0] .= $char; - continue; - } - // en-GB; [q]=0.8, ... - // ~~~~~~~~^~~~~~~~~~~ - if ($inParamName && isset($token[$char]) && isset($data[$i][0])) { - $data[$i][1][$p][0] ??= ''; - $data[$i][1][$p][0] .= $char; - continue; - } - // en-GB; q=[0.8], ... - // ~~~~~~~~~~^^^~~~~~~ - // phpcs:ignore Generic.Files.LineLength - if ($inParamValue && (isset($token[$char]) || ($inQuotes && (isset($quotedString[$char]) || ($prev . $char) === '\"'))) && isset($data[$i][1][$p][0])) { - $data[$i][1][$p][1] ??= ''; - $data[$i][1][$p][1] .= $char; - continue; - } - - // the header is invalid... - return null; - } - - $result = []; - foreach ($data as $item) { - $result[$item[0]] = []; - if (isset($item[1])) { - foreach ($item[1] as $param) { - $result[$item[0]][$param[0]] = $param[1] ?? ''; - } - } - } - - uasort($result, static fn(array $a, array $b): int => ($b['q'] ?? 1) <=> ($a['q'] ?? 1)); - - return $result; -} diff --git a/functions/parse_header.php b/functions/parse_header.php new file mode 100644 index 00000000..50fe3335 --- /dev/null +++ b/functions/parse_header.php @@ -0,0 +1,178 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router; + +use Sunrise\Http\Router\Dictionary\Charset; +use Sunrise\Http\Router\Exception\InvalidArgumentException; + +use function sprintf; + +/** + * Parses the given header + * + * @param string $header + * + * @return T + * + * @throws InvalidArgumentException If the given header is invalid. + * + * @template T as list}> + * + * @since 3.0.0 + */ +function parse_header(string $header): array +{ + /** @var list}> $matches */ + $matches = []; + + $offset = -1; + $inToken = true; + $inParamName = false; + $inParamValue = false; + $inQuotedString = false; + $isQuotedChar = false; + + $tokenIndex = 0; + $paramIndex = 0; + + while (true) { + $offset++; + + $char = $header[$offset] ?? null; + if ($char === null) { + break; + } + + // en-GB; q=0.8[,] ... + // ~~~~~~~~~~~~~^~~~~~ + if (!$inQuotedString && $char === ',') { + $inToken = true; + $inParamName = false; + $inParamValue = false; + $tokenIndex++; + $paramIndex = 0; + continue; + } + // en-GB[;] q=0.8, ... + // ~~~~~~^~~~~~~~~~~~~ + if (!$inQuotedString && $char === ';') { + $inToken = false; + $inParamName = true; + $inParamValue = false; + $paramIndex++; + continue; + } + // en-GB; q[=]0.8, ... + // ~~~~~~~~~^~~~~~~~~~ + if (!$inQuotedString && $char === '=') { + $inToken = false; + $inParamName = false; + $inParamValue = true; + continue; + } + + // en-GB[ ]; q=0.8, ... + // ~~~~~~^~~~~~~~~~~~~~ + if (!$inQuotedString && $inToken && isset(Charset::RFC7230_OWS[$char]) && isset($matches[$tokenIndex][0][0])) { + // The cursor is no longer inside the token, as it cannot contain OWS characters... + $inToken = false; + continue; + } + // en-GB; q[ ]=0.8, ... + // ~~~~~~~~~^~~~~~~~~~~ + // phpcs:ignore Generic.Files.LineLength + if (!$inQuotedString && $inParamName && isset(Charset::RFC7230_OWS[$char]) && isset($matches[$tokenIndex][1][$paramIndex][0][0])) { + // The cursor is no longer inside the parameter name, as it cannot contain OWS characters... + $inParamName = false; + continue; + } + // en-GB; q=0.8[ ], ... + // ~~~~~~~~~~~~~^~~~~~~ + // phpcs:ignore Generic.Files.LineLength + if (!$inQuotedString && $inParamValue && isset(Charset::RFC7230_OWS[$char]) && isset($matches[$tokenIndex][1][$paramIndex][1][0])) { + // The cursor is no longer inside the parameter value, as it cannot contain OWS characters... + $inParamValue = false; + continue; + } + + // Ignore any OWS characters outside the quoted string... + if (!$inQuotedString && isset(Charset::RFC7230_OWS[$char])) { + continue; + } + + // en-GB; q=["]0.8", ... + // ~~~~~~~~~~^~~~~~~~~~~ + if (!$inQuotedString && $inParamValue && $char === '"' && !isset($matches[$tokenIndex][1][$paramIndex][1][0])) { + $inQuotedString = true; + continue; + } + // en-GB; q="0.8["], ... + // ~~~~~~~~~~~~~~^~~~~~~ + if ($inQuotedString && $inParamValue && !$isQuotedChar && $char === '"') { + $inParamValue = false; + $inQuotedString = false; + continue; + } + + // en-GB; param="foo [\]"bar[\]" [\]\0", ... + // ~~~~~~~~~~~~~~~~~~~^~~~~~^~~~~~^~~~~~~~~~ + if ($inQuotedString && !$isQuotedChar && $char === '\\') { + $nextChar = $header[$offset + 1] ?? null; + // phpcs:ignore Generic.Files.LineLength + if (isset($nextChar) && ($nextChar === '\\' || $nextChar === '"' || isset(Charset::RFC7230_QUOTED_STRING[$nextChar]))) { + $isQuotedChar = true; + continue; + } + } + + // [en-GB]; q=0.8, ... + // ~^^^^^~~~~~~~~~~~~~ + if ($inToken) { // AS IS + $matches[$tokenIndex][0] ??= ''; + $matches[$tokenIndex][0] .= $char; + continue; + } + // en-GB; [q]=0.8, ... + // ~~~~~~~~^~~~~~~~~~~ + if ($inParamName && isset(Charset::RFC7230_TOKEN[$char]) && isset($matches[$tokenIndex][0][0])) { + $matches[$tokenIndex][1][$paramIndex][0] ??= ''; + $matches[$tokenIndex][1][$paramIndex][0] .= $char; + continue; + } + // en-GB; q=[0.8], ... + // ~~~~~~~~~~^^^~~~~~~ + // phpcs:ignore Generic.Files.LineLength + if ($inParamValue && (isset(Charset::RFC7230_TOKEN[$char]) || ($inQuotedString && isset(Charset::RFC7230_QUOTED_STRING[$char])) || $isQuotedChar) && isset($matches[$tokenIndex][1][$paramIndex][0][0])) { + $matches[$tokenIndex][1][$paramIndex][1] ??= ''; + $matches[$tokenIndex][1][$paramIndex][1] .= $char; + $isQuotedChar = false; + continue; + } + + throw new InvalidArgumentException(sprintf('Unexpected character at position %d', $offset)); + } + + $result = []; + foreach ($matches as $index => $match) { + $result[$index] = [$match[0], []]; + if (isset($match[1])) { + foreach ($match[1] as $param) { + $result[$index][1][$param[0]] = $param[1] ?? null; + } + } + } + + /** @var T */ + return $result; +} diff --git a/functions/parse_header_with_media_type.php b/functions/parse_header_with_media_type.php new file mode 100644 index 00000000..fbdb526a --- /dev/null +++ b/functions/parse_header_with_media_type.php @@ -0,0 +1,91 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router; + +use Generator; +use Sunrise\Http\Router\Dictionary\Charset; +use Sunrise\Http\Router\Entity\MediaType; +use Sunrise\Http\Router\Exception\InvalidArgumentException; + +use function sprintf; +use function strtolower; + +/** + * Parses the given header that contains media types + * + * @param string $header + * + * @return Generator, MediaType> + * + * @throws InvalidArgumentException If one of the media types is invalid. + * + * @since 3.0.0 + */ +function parse_header_with_media_type(string $header): Generator +{ + $matches = parse_header($header); + if ($matches === []) { + return; + } + + foreach ($matches as $index => [$token, $parameters]) { + $offset = -1; + + $inType = true; + $inSubtype = false; + + $type = null; + $subtype = null; + + while (true) { + $offset++; + + $char = $token[$offset] ?? null; + if ($char === null) { + break; + } + + if ($char === '/' && $inSubtype === false) { + $inType = false; + $inSubtype = true; + continue; + } + + if ($inType && isset(Charset::RFC7230_TOKEN[$char])) { + $type .= $char; + continue; + } + + if ($inSubtype && isset(Charset::RFC7230_TOKEN[$char])) { + $subtype .= $char; + continue; + } + + throw new InvalidArgumentException(sprintf( + 'Unexpected character at position %d inside media type with index %d.', + $offset, + $index, + )); + } + + if ($subtype === null) { + throw new InvalidArgumentException(sprintf( + 'Missing subtype for media type with index %d.', + $index, + )); + } + + yield $index => new MediaType(strtolower($type), strtolower($subtype), $parameters); + } +} diff --git a/functions/reflect_callable.php b/functions/reflect_callback.php similarity index 90% rename from functions/reflect_callable.php rename to functions/reflect_callback.php index e563cd43..be81733b 100644 --- a/functions/reflect_callable.php +++ b/functions/reflect_callback.php @@ -32,12 +32,11 @@ * * @return ReflectionFunction|ReflectionMethod * - * @throws InvalidArgumentException - * If the given callback cannot be reflected. + * @throws InvalidArgumentException If the given callback cannot be reflected. * * @since 3.0.0 */ -function reflect_callable(callable $callback): ReflectionFunction|ReflectionMethod +function reflect_callback(callable $callback): ReflectionFunction|ReflectionMethod { if ($callback instanceof Closure) { return new ReflectionFunction($callback); diff --git a/src/Annotation/Consume.php b/src/Annotation/Consume.php index 8eaf7d85..bcbf45a4 100644 --- a/src/Annotation/Consume.php +++ b/src/Annotation/Consume.php @@ -14,6 +14,7 @@ namespace Sunrise\Http\Router\Annotation; use Attribute; +use Sunrise\Http\Router\Entity\MediaType; /** * @since 3.0.0 @@ -25,9 +26,9 @@ final class Consume /** * Constructor of the class * - * @param non-empty-string $value + * @param MediaType|non-empty-string $value */ - public function __construct(public string $value) + public function __construct(public MediaType|string $value) { } } diff --git a/src/Annotation/Produce.php b/src/Annotation/Produce.php index ccac05ce..407b911e 100644 --- a/src/Annotation/Produce.php +++ b/src/Annotation/Produce.php @@ -14,6 +14,7 @@ namespace Sunrise\Http\Router\Annotation; use Attribute; +use Sunrise\Http\Router\Entity\MediaType; /** * @since 3.0.0 @@ -25,9 +26,9 @@ final class Produce /** * Constructor of the class * - * @param non-empty-string $value + * @param MediaType|non-empty-string $value */ - public function __construct(public string $value) + public function __construct(public MediaType|string $value) { } } diff --git a/src/Annotation/ProxyChain.php b/src/Annotation/ProxyChain.php new file mode 100644 index 00000000..ff449937 --- /dev/null +++ b/src/Annotation/ProxyChain.php @@ -0,0 +1,36 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Annotation; + +use Attribute; + +/** + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_PARAMETER)] +final class ProxyChain +{ + + /** + * Constructor of the class + * + * @param array $value + * + * @template TKey as non-empty-string Proxy address + * @template TValue as non-empty-string Trusted header + */ + public function __construct(public array $value) + { + } +} diff --git a/src/Annotation/Route.php b/src/Annotation/Route.php index 5b6ea783..4bb47742 100644 --- a/src/Annotation/Route.php +++ b/src/Annotation/Route.php @@ -15,6 +15,7 @@ use Attribute; use Fig\Http\Message\RequestMethodInterface; +use Sunrise\Http\Router\Entity\MediaType; #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] final class Route implements RequestMethodInterface @@ -29,6 +30,20 @@ final class Route implements RequestMethodInterface */ public mixed $holder = null; + /** + * The route's consumes media types + * + * @var list + */ + public array $consumes = []; + + /** + * The route's produces media types + * + * @var list + */ + public array $produces = []; + /** * Constructor of the class * @@ -37,8 +52,6 @@ final class Route implements RequestMethodInterface * @param non-empty-string $path The route's path * @param non-empty-string|null $method The route's method * @param list $methods The route's methods - * @param list $consumes The route's consumed media types - * @param list $produces The route's produced media types * @param list $middlewares The route's middlewares * @param array $attributes The route's attributes * @param string $summary The route's summary @@ -52,8 +65,6 @@ public function __construct( public string $path = '/', ?string $method = null, public array $methods = [], - public array $consumes = [], - public array $produces = [], public array $middlewares = [], public array $attributes = [], public string $summary = '', diff --git a/src/ClassResolver.php b/src/ClassResolver.php index d457a173..88522cd1 100644 --- a/src/ClassResolver.php +++ b/src/ClassResolver.php @@ -13,12 +13,11 @@ namespace Sunrise\Http\Router; +use ReflectionClass; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; -use ReflectionClass; use function class_exists; -use function is_object; use function sprintf; /** @@ -66,18 +65,12 @@ public function resolveClass(string $fqn): object } if (!class_exists($fqn)) { - throw new InvalidArgumentException(sprintf( - 'Class %s does not exist', - $fqn - )); + throw new InvalidArgumentException(sprintf('The class %s does not exist', $fqn)); } $class = new ReflectionClass($fqn); if (!$class->isInstantiable()) { - throw new LogicException(sprintf( - 'Class %s cannot be initialized', - $fqn - )); + throw new LogicException(sprintf('The class %s cannot be initialized', $fqn)); } $arguments = []; diff --git a/src/Dictionary/Charset.php b/src/Dictionary/Charset.php new file mode 100644 index 00000000..05e975e9 --- /dev/null +++ b/src/Dictionary/Charset.php @@ -0,0 +1,72 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Dictionary; + +/** + * Charset + * + * @since 3.0.0 + */ +final class Charset +{ + public const WILDCARD = '*'; + + public const RFC7230_OWS = [ + "\x09" => 1, "\x20" => 1, + ]; + + public const RFC7230_TOKEN = [ + "\x21" => 1, "\x23" => 1, "\x24" => 1, "\x25" => 1, "\x26" => 1, "\x27" => 1, "\x2a" => 1, "\x2b" => 1, + "\x2d" => 1, "\x2e" => 1, "\x5e" => 1, "\x5f" => 1, "\x60" => 1, "\x7c" => 1, "\x7e" => 1, "\x30" => 1, + "\x31" => 1, "\x32" => 1, "\x33" => 1, "\x34" => 1, "\x35" => 1, "\x36" => 1, "\x37" => 1, "\x38" => 1, + "\x39" => 1, "\x41" => 1, "\x42" => 1, "\x43" => 1, "\x44" => 1, "\x45" => 1, "\x46" => 1, "\x47" => 1, + "\x48" => 1, "\x49" => 1, "\x4a" => 1, "\x4b" => 1, "\x4c" => 1, "\x4d" => 1, "\x4e" => 1, "\x4f" => 1, + "\x50" => 1, "\x51" => 1, "\x52" => 1, "\x53" => 1, "\x54" => 1, "\x55" => 1, "\x56" => 1, "\x57" => 1, + "\x58" => 1, "\x59" => 1, "\x5a" => 1, "\x61" => 1, "\x62" => 1, "\x63" => 1, "\x64" => 1, "\x65" => 1, + "\x66" => 1, "\x67" => 1, "\x68" => 1, "\x69" => 1, "\x6a" => 1, "\x6b" => 1, "\x6c" => 1, "\x6d" => 1, + "\x6e" => 1, "\x6f" => 1, "\x70" => 1, "\x71" => 1, "\x72" => 1, "\x73" => 1, "\x74" => 1, "\x75" => 1, + "\x76" => 1, "\x77" => 1, "\x78" => 1, "\x79" => 1, "\x7a" => 1, + ]; + + public const RFC7230_QUOTED_STRING = [ + "\x09" => 1, "\x20" => 1, "\x21" => 1, "\x23" => 1, "\x24" => 1, "\x25" => 1, "\x26" => 1, "\x27" => 1, + "\x28" => 1, "\x29" => 1, "\x2a" => 1, "\x2b" => 1, "\x2c" => 1, "\x2d" => 1, "\x2e" => 1, "\x2f" => 1, + "\x30" => 1, "\x31" => 1, "\x32" => 1, "\x33" => 1, "\x34" => 1, "\x35" => 1, "\x36" => 1, "\x37" => 1, + "\x38" => 1, "\x39" => 1, "\x3a" => 1, "\x3b" => 1, "\x3c" => 1, "\x3d" => 1, "\x3e" => 1, "\x3f" => 1, + "\x40" => 1, "\x41" => 1, "\x42" => 1, "\x43" => 1, "\x44" => 1, "\x45" => 1, "\x46" => 1, "\x47" => 1, + "\x48" => 1, "\x49" => 1, "\x4a" => 1, "\x4b" => 1, "\x4c" => 1, "\x4d" => 1, "\x4e" => 1, "\x4f" => 1, + "\x50" => 1, "\x51" => 1, "\x52" => 1, "\x53" => 1, "\x54" => 1, "\x55" => 1, "\x56" => 1, "\x57" => 1, + "\x58" => 1, "\x59" => 1, "\x5a" => 1, "\x5b" => 1, "\x5d" => 1, "\x5e" => 1, "\x5f" => 1, "\x60" => 1, + "\x61" => 1, "\x62" => 1, "\x63" => 1, "\x64" => 1, "\x65" => 1, "\x66" => 1, "\x67" => 1, "\x68" => 1, + "\x69" => 1, "\x6a" => 1, "\x6b" => 1, "\x6c" => 1, "\x6d" => 1, "\x6e" => 1, "\x6f" => 1, "\x70" => 1, + "\x71" => 1, "\x72" => 1, "\x73" => 1, "\x74" => 1, "\x75" => 1, "\x76" => 1, "\x77" => 1, "\x78" => 1, + "\x79" => 1, "\x7a" => 1, "\x7b" => 1, "\x7c" => 1, "\x7d" => 1, "\x7e" => 1, "\x80" => 1, "\x81" => 1, + "\x82" => 1, "\x83" => 1, "\x84" => 1, "\x85" => 1, "\x86" => 1, "\x87" => 1, "\x88" => 1, "\x89" => 1, + "\x8a" => 1, "\x8b" => 1, "\x8c" => 1, "\x8d" => 1, "\x8e" => 1, "\x8f" => 1, "\x90" => 1, "\x91" => 1, + "\x92" => 1, "\x93" => 1, "\x94" => 1, "\x95" => 1, "\x96" => 1, "\x97" => 1, "\x98" => 1, "\x99" => 1, + "\x9a" => 1, "\x9b" => 1, "\x9c" => 1, "\x9d" => 1, "\x9e" => 1, "\x9f" => 1, "\xa0" => 1, "\xa1" => 1, + "\xa2" => 1, "\xa3" => 1, "\xa4" => 1, "\xa5" => 1, "\xa6" => 1, "\xa7" => 1, "\xa8" => 1, "\xa9" => 1, + "\xaa" => 1, "\xab" => 1, "\xac" => 1, "\xad" => 1, "\xae" => 1, "\xaf" => 1, "\xb0" => 1, "\xb1" => 1, + "\xb2" => 1, "\xb3" => 1, "\xb4" => 1, "\xb5" => 1, "\xb6" => 1, "\xb7" => 1, "\xb8" => 1, "\xb9" => 1, + "\xba" => 1, "\xbb" => 1, "\xbc" => 1, "\xbd" => 1, "\xbe" => 1, "\xbf" => 1, "\xc0" => 1, "\xc1" => 1, + "\xc2" => 1, "\xc3" => 1, "\xc4" => 1, "\xc5" => 1, "\xc6" => 1, "\xc7" => 1, "\xc8" => 1, "\xc9" => 1, + "\xca" => 1, "\xcb" => 1, "\xcc" => 1, "\xcd" => 1, "\xce" => 1, "\xcf" => 1, "\xd0" => 1, "\xd1" => 1, + "\xd2" => 1, "\xd3" => 1, "\xd4" => 1, "\xd5" => 1, "\xd6" => 1, "\xd7" => 1, "\xd8" => 1, "\xd9" => 1, + "\xda" => 1, "\xdb" => 1, "\xdc" => 1, "\xdd" => 1, "\xde" => 1, "\xdf" => 1, "\xe0" => 1, "\xe1" => 1, + "\xe2" => 1, "\xe3" => 1, "\xe4" => 1, "\xe5" => 1, "\xe6" => 1, "\xe7" => 1, "\xe8" => 1, "\xe9" => 1, + "\xea" => 1, "\xeb" => 1, "\xec" => 1, "\xed" => 1, "\xee" => 1, "\xef" => 1, "\xf0" => 1, "\xf1" => 1, + "\xf2" => 1, "\xf3" => 1, "\xf4" => 1, "\xf5" => 1, "\xf6" => 1, "\xf7" => 1, "\xf8" => 1, "\xf9" => 1, + "\xfa" => 1, "\xfb" => 1, "\xfc" => 1, "\xfd" => 1, "\xfe" => 1, "\xff" => 1, + ]; +} diff --git a/src/Entity/IpAddress.php b/src/Entity/ClientRemoteAddress.php similarity index 70% rename from src/Entity/IpAddress.php rename to src/Entity/ClientRemoteAddress.php index 4e5477a8..2d67b9dc 100644 --- a/src/Entity/IpAddress.php +++ b/src/Entity/ClientRemoteAddress.php @@ -13,26 +13,28 @@ namespace Sunrise\Http\Router\Entity; +use Stringable; + /** - * IP address + * Client remote address * * @since 3.0.0 */ -final class IpAddress +final class ClientRemoteAddress implements Stringable { /** * Constructor of the class * - * @param non-empty-string $value The IP address value - * @param list $proxies The list of proxies in front of this IP address + * @param non-empty-string $value The address value + * @param list $proxies The list of proxies in front of this address */ public function __construct(private string $value, private array $proxies = []) { } /** - * Gets the IP address value + * Gets the address value * * @return non-empty-string */ @@ -42,7 +44,7 @@ public function getValue(): string } /** - * Gets the list of proxies in front of this IP address + * Gets the list of proxies in front of this address * * @return list */ @@ -50,4 +52,12 @@ public function getProxies(): array { return $this->proxies; } + + /** + * @inheritDoc + */ + public function __toString(): string + { + return $this->value; + } } diff --git a/src/Entity/MediaType.php b/src/Entity/MediaType.php index 289b1440..2ed04d60 100644 --- a/src/Entity/MediaType.php +++ b/src/Entity/MediaType.php @@ -13,6 +13,8 @@ namespace Sunrise\Http\Router\Entity; +use Sunrise\Http\Router\Dictionary\Charset; + /** * Media type * @@ -20,36 +22,40 @@ */ final class MediaType { - public const APPLICATION_JSON = 'application/json'; - public const APPLICATION_XML = 'application/xml'; - public const APPLICATION_YAML = 'application/yaml'; - public const TEXT_CSV = 'text/csv'; - public const TEXT_HTML = 'text/html'; - public const TEXT_PLAIN = 'text/plain'; - public const TEXT_XML = 'text/xml'; /** * Constructor of the class * - * @param non-empty-string $value + * @param non-empty-string $type + * @param non-empty-string $subtype * @param array $parameters */ - public function __construct(private string $value, private array $parameters = []) + public function __construct(private string $type, private string $subtype, private array $parameters = []) + { + } + + /** + * Gets the type of the media type + * + * @return non-empty-string + */ + public function getType(): string { + return $this->type; } /** - * Gets the media type value + * Gets the subtype of the media type * * @return non-empty-string */ - public function getValue(): string + public function getSubtype(): string { - return $this->value; + return $this->subtype; } /** - * Gets the media type parameters + * Gets the parameters of the media type * * @return array */ @@ -57,4 +63,57 @@ public function getParameters(): array { return $this->parameters; } + + /** + * Gets the media range of the media type + * + * @return non-empty-string + */ + public function getMediaRange(): string + { + return $this->type . '/' . $this->subtype; + } + + /** + * Gets the quality factor of the media type + * + * @return float + */ + public function getQualityFactor(): float + { + return (float) ($this->parameters['q'] ?? 1.); + } + + /** + * Checks if the type of the media type is wildcard + * + * @return bool + */ + public function isWildcardType(): bool + { + return $this->type === Charset::WILDCARD; + } + + /** + * Checks if the subtype of the media type is wildcard + * + * @return bool + */ + public function isWildcardSubtype(): bool + { + return $this->subtype === Charset::WILDCARD; + } + + /** + * Checks if this media type equals to the given media type + * + * @param MediaType $other + * + * @return bool + */ + public function equals(MediaType $other): bool + { + return ($this->isWildcardType() || $other->isWildcardType() || $this->getType() === $other->getType()) + && ($this->isWildcardSubtype() || $other->isWildcardSubtype() || $this->getType() === $other->getSubtype()); + } } diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 0354769a..e823f85d 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -14,6 +14,7 @@ namespace Sunrise\Http\Router\Loader; use FilesystemIterator; +use Generator; use Psr\Container\ContainerInterface; use Psr\Http\Server\RequestHandlerInterface; use Psr\SimpleCache\CacheInterface; @@ -33,6 +34,7 @@ use Sunrise\Http\Router\Annotation\Route; use Sunrise\Http\Router\Annotation\Summary; use Sunrise\Http\Router\Annotation\Tag; +use Sunrise\Http\Router\Entity\MediaType; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ParameterResolutioner; @@ -59,6 +61,8 @@ use function sprintf; use function usort; +use function Sunrise\Http\Router\parse_header_with_media_type; + /** * DescriptorLoader */ @@ -310,8 +314,8 @@ public function load(): RouteCollectionInterface ); $route->setHost($descriptor->host); - $route->setConsumedMediaTypes(...$descriptor->consumes); - $route->setProducedMediaTypes(...$descriptor->produces); + $route->setConsumesMediaTypes(...$descriptor->consumes); + $route->setProducesMediaTypes(...$descriptor->produces); $route->setSummary($descriptor->summary); $route->setDescription($descriptor->description); $route->setTags(...$descriptor->tags); @@ -360,9 +364,9 @@ private function getDescriptors(): array * * @param string $resource * - * @return iterable + * @return Generator */ - private function getResourceDescriptors(string $resource): iterable + private function getResourceDescriptors(string $resource): Generator { if (class_exists($resource)) { yield from $this->getClassDescriptors(new ReflectionClass($resource)); @@ -380,9 +384,9 @@ private function getResourceDescriptors(string $resource): iterable * * @param ReflectionClass $class * - * @return iterable + * @return Generator */ - private function getClassDescriptors(ReflectionClass $class): iterable + private function getClassDescriptors(ReflectionClass $class): Generator { if (!$class->isInstantiable()) { return; @@ -447,12 +451,28 @@ private function supplementDescriptor(Route $descriptor, ReflectionClass|Reflect $annotations = $this->getAnnotations(Consume::class, $holder); foreach ($annotations as $annotation) { - $descriptor->consumes[] = $annotation->value; + if ($annotation->value instanceof MediaType) { + $descriptor->consumes[] = $annotation->value; + continue; + } + + $consumes = parse_header_with_media_type($annotation->value); + foreach ($consumes as $consume) { + $descriptor->consumes[] = $consume; + } } $annotations = $this->getAnnotations(Produce::class, $holder); foreach ($annotations as $annotation) { - $descriptor->produces[] = $annotation->value; + if ($annotation->value instanceof MediaType) { + $descriptor->produces[] = $annotation->value; + continue; + } + + $produces = parse_header_with_media_type($annotation->value); + foreach ($produces as $produce) { + $descriptor->produces[] = $produce; + } } $annotations = $this->getAnnotations(Middleware::class, $holder); @@ -502,9 +522,9 @@ private function getAnnotations(string $name, ReflectionClass|ReflectionMethod $ * * @param string $dirname * - * @return iterable + * @return Generator */ - private function getDirectoryClasses(string $dirname): iterable + private function getDirectoryClasses(string $dirname): Generator { /** @var array $filenames */ $filenames = iterator_to_array( diff --git a/src/Middleware/CallbackMiddleware.php b/src/Middleware/CallbackMiddleware.php index d68d1b58..8fad2684 100644 --- a/src/Middleware/CallbackMiddleware.php +++ b/src/Middleware/CallbackMiddleware.php @@ -23,7 +23,7 @@ use Sunrise\Http\Router\ParameterResolver\ObjectInjectionParameterResolver; use Sunrise\Http\Router\ResponseResolutionerInterface; -use function Sunrise\Http\Router\reflect_callable; +use function Sunrise\Http\Router\reflect_callback; /** * CallbackMiddleware @@ -78,7 +78,7 @@ public function __construct( */ public function getReflection(): ReflectionFunction|ReflectionMethod { - return reflect_callable($this->callback); + return reflect_callback($this->callback); } /** diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 3d1d362d..f249fb98 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -18,6 +18,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\Entity\MediaType; use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ServerRequest; @@ -59,12 +60,17 @@ public function __construct() */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (ServerRequest::from($request)->isJson()) { - $request = $request->withParsedBody( - $this->decodePayload( - $request->getBody()->__toString() - ) - ); + $requestProxy = ServerRequest::from($request); + $clientProducedMediaType = $requestProxy->getClientProducedMediaType(); + if (isset($clientProducedMediaType)) { + $serverConsumesMediaType = new MediaType('application', 'json'); + if ($serverConsumesMediaType->equals($clientProducedMediaType)) { + $request = $request->withParsedBody( + $this->decodePayload( + $request->getBody()->__toString() + ) + ); + } } return $handler->handle($request); @@ -84,7 +90,7 @@ private function decodePayload(string $payload): array try { $data = json_decode($payload, true, 512, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR); } catch (JsonException $e) { - throw new InvalidRequestPayloadException(sprintf('Invalid JSON: %s', $e->getMessage()), 0, $e); + throw new InvalidRequestPayloadException(sprintf('Invalid JSON payload: %s', $e->getMessage()), 0, $e); } // According to PSR-7, the data must be an array diff --git a/src/Middleware/SimdJsonPayloadDecodingMiddleware.php b/src/Middleware/SimdJsonPayloadDecodingMiddleware.php index a24da152..7309ff99 100644 --- a/src/Middleware/SimdJsonPayloadDecodingMiddleware.php +++ b/src/Middleware/SimdJsonPayloadDecodingMiddleware.php @@ -18,6 +18,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; +use Sunrise\Http\Router\Entity\MediaType; use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ServerRequest; @@ -56,12 +57,17 @@ public function __construct() */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - if (ServerRequest::from($request)->isJson()) { - $request = $request->withParsedBody( - $this->decodePayload( - $request->getBody()->__toString() - ) - ); + $requestProxy = ServerRequest::from($request); + $clientProducedMediaType = $requestProxy->getClientProducedMediaType(); + if (isset($clientProducedMediaType)) { + $serverConsumesMediaType = new MediaType('application', 'json'); + if ($serverConsumesMediaType->equals($clientProducedMediaType)) { + $request = $request->withParsedBody( + $this->decodePayload( + $request->getBody()->__toString() + ) + ); + } } return $handler->handle($request); @@ -81,7 +87,7 @@ private function decodePayload(string $payload): array try { $data = simdjson_decode($payload, true, 512); } catch (SimdJsonException $e) { - throw new InvalidRequestPayloadException(sprintf('Invalid JSON: %s', $e->getMessage()), 0, $e); + throw new InvalidRequestPayloadException(sprintf('Invalid JSON payload: %s', $e->getMessage()), 0, $e); } // According to PSR-7, the data must be an array diff --git a/src/ParameterResolutioner.php b/src/ParameterResolutioner.php index 87148307..3d9ed969 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolutioner.php @@ -110,11 +110,11 @@ private function resolveParameter(ReflectionParameter $parameter): Generator } if ($parameter->isDefaultValueAvailable()) { - return yield $parameter->getDefaultValue(); + return yield $parameter->getDefaultValue(); } throw new LogicException(sprintf( - 'Unable to resolve the parameter {%s}.', + 'Unable to resolve the parameter {%s}', self::stringifyParameter($parameter) )); } diff --git a/src/ParameterResolver/ClientRemoteAddressParameterResolver.php b/src/ParameterResolver/ClientRemoteAddressParameterResolver.php new file mode 100644 index 00000000..27bd8ebc --- /dev/null +++ b/src/ParameterResolver/ClientRemoteAddressParameterResolver.php @@ -0,0 +1,75 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ParameterResolver; + +use Generator; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionAttribute; +use ReflectionNamedType; +use ReflectionParameter; +use Sunrise\Http\Router\Annotation\ProxyChain; +use Sunrise\Http\Router\Entity\ClientRemoteAddress; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ServerRequest; + +/** + * ClientRemoteAddressParameterResolver + * + * @since 3.0.0 + */ +final class ClientRemoteAddressParameterResolver implements ParameterResolverInterface +{ + + /** + * Constructor of th class + * + * @param array $proxyChain + * + * @template TKey as non-empty-string Proxy address + * @template TValue as non-empty-string Trusted header + */ + public function __construct(private array $proxyChain = []) + { + } + + /** + * @inheritDoc + * + * @throws LogicException If the resolver is used incorrectly. + */ + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator + { + $type = $parameter->getType(); + + if (! ($type instanceof ReflectionNamedType) || + ! ($type->getName() === ClientRemoteAddress::class)) { + return; + } + + if (! $context instanceof ServerRequestInterface) { + throw new LogicException( + 'At this level of the application, any operations with the request are not possible.' + ); + } + + $proxyChain = $this->proxyChain; + /** @var list> $attributes */ + $attributes = $parameter->getAttributes(ProxyChain::class); + if (isset($attributes[0])) { + $proxyChain = $attributes[0]->newInstance()->value; + } + + yield ServerRequest::from($context)->getClientRemoteAddress($proxyChain); + } +} diff --git a/src/ParameterResolver/RequestRouteParameterResolver.php b/src/ParameterResolver/RequestRouteParameterResolver.php index df975527..0d919b51 100644 --- a/src/ParameterResolver/RequestRouteParameterResolver.php +++ b/src/ParameterResolver/RequestRouteParameterResolver.php @@ -40,10 +40,8 @@ public function resolveParameter(ReflectionParameter $parameter, mixed $context) { $type = $parameter->getType(); - if ( - ! ($type instanceof ReflectionNamedType) || - ! ($type->getName() === RouteInterface::class) - ) { + if (! ($type instanceof ReflectionNamedType) || + ! ($type->getName() === RouteInterface::class)) { return; } diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index 073a8319..954e391a 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -124,7 +124,7 @@ public function resolveRequestHandler(mixed $reference): RequestHandlerInterface } throw new LogicException(sprintf( - 'Unable to resolve the reference {%s}.', + 'Unable to resolve the reference {%s}', self::stringifyReference($reference), )); } @@ -181,7 +181,7 @@ public function resolveMiddleware(mixed $reference): MiddlewareInterface } throw new LogicException(sprintf( - 'Unable to resolve the reference {%s}.', + 'Unable to resolve the reference {%s}', self::stringifyReference($reference), )); } diff --git a/src/RequestHandler/CallbackRequestHandler.php b/src/RequestHandler/CallbackRequestHandler.php index c6ecf8e7..572c3458 100644 --- a/src/RequestHandler/CallbackRequestHandler.php +++ b/src/RequestHandler/CallbackRequestHandler.php @@ -22,7 +22,7 @@ use Sunrise\Http\Router\ParameterResolver\ObjectInjectionParameterResolver; use Sunrise\Http\Router\ResponseResolutionerInterface; -use function Sunrise\Http\Router\reflect_callable; +use function Sunrise\Http\Router\reflect_callback; /** * CallbackRequestHandler @@ -77,7 +77,7 @@ public function __construct( */ public function getReflection(): ReflectionFunction|ReflectionMethod { - return reflect_callable($this->callback); + return reflect_callback($this->callback); } /** diff --git a/src/ResponseResolutioner.php b/src/ResponseResolutioner.php index f5c3b8c6..b3d81403 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolutioner.php @@ -71,7 +71,7 @@ public function resolveResponse( } throw new LogicException(sprintf( - 'Unable to resolve the response {%s} to PSR-7 response.', + 'Unable to resolve the response {%s} to PSR-7 response', self::stringifyResponse($response, $source), )); } diff --git a/src/ResponseResolver/ResponseBodyResponseResolver.php b/src/ResponseResolver/ResponseBodyResponseResolver.php index b4b04305..fd6ad771 100644 --- a/src/ResponseResolver/ResponseBodyResponseResolver.php +++ b/src/ResponseResolver/ResponseBodyResponseResolver.php @@ -26,7 +26,7 @@ use Sunrise\Http\Router\ServerRequest; use Symfony\Component\Serializer\SerializerInterface; -use function next; +use function in_array; use function reset; use function sprintf; @@ -38,30 +38,21 @@ final class ResponseBodyResponseResolver implements ResponseResolverInterface { - /** - * @var array - */ - public const MEDIA_TYPE_FORMATS = [ - MediaType::APPLICATION_JSON => 'json', - MediaType::APPLICATION_XML => 'xml', - MediaType::APPLICATION_YAML => 'yaml', - ]; - /** * Constructor of the class * * @param ResponseFactoryInterface $responseFactory * @param SerializerInterface $serializer - * @param non-empty-string $defaultMediaType - * @param array $mediaTypeFormats + * @param non-empty-string $defaultFormat + * @param array $formats * @param array $serializationContext */ public function __construct( private ResponseFactoryInterface $responseFactory, - private SerializerInterface $serializer, - private string $defaultMediaType = MediaType::APPLICATION_JSON, - private array $mediaTypeFormats = self::MEDIA_TYPE_FORMATS, - private array $serializationContext = [], + private SerializerInterface $serializer, + private string $defaultFormat = 'json', + private array $formats = self::MEDIA_FORMATS, + private array $serializationContext = [], ) { } @@ -79,41 +70,23 @@ public function resolveResponse( return null; } - $mediaType = $this->defaultMediaType; + $format = $this->defaultFormat; /** @var RouteInterface|null $route */ $route = $request->getAttribute('@route'); if ($route instanceof RouteInterface) { - $producedMediaTypes = $route->getProducedMediaTypes(); - if (!empty($producedMediaTypes)) { - $mediaType = reset($producedMediaTypes); - $consumedMediaTypes = ServerRequest::from($request)->getClientConsumedMediaTypes(); - while ($producedMediaType = next($producedMediaTypes)) { - if (isset($consumedMediaTypes[$producedMediaType])) { - $mediaType = $producedMediaType; - break; - } - } - } - } + $mediaRange = ServerRequest::from($request) + ->getClientPreferredMediaType(...$route->getProducesMediaTypes()) + ?->getMediaRange(); - if (!isset($this->mediaTypeFormats[$mediaType])) { - throw new LogicException(sprintf( - 'The response {%s} cannot be serialized. ' . - 'Make sure that the resolver %s is configured correctly, ' . - 'and also ensure that the current route provides media types known to the resolver.', - ResponseResolutioner::stringifyResponse($response, $source), - ResponseBodyResponseResolver::class, - )); + if (isset($mediaRange) && isset($this->formats[$mediaRange])) { + $format = $this->formats[$mediaRange]; + } } - $payload = $this->serializer->serialize( - $response, - $this->mediaTypeFormats[$mediaType], - $this->serializationContext, - ); + $payload = $this->serializer->serialize($response, $format, $this->serializationContext); - $contentType = sprintf('%s; charset=UTF-8', $mediaType); + $contentType = sprintf('%s; charset=UTF-8', $mediaType->getMediaRange()); $result = $this->responseFactory->createResponse(200) ->withHeader('Content-Type', $contentType); diff --git a/src/Route.php b/src/Route.php index 376e34f0..4cf4dcb3 100644 --- a/src/Route.php +++ b/src/Route.php @@ -19,10 +19,10 @@ use Psr\Http\Server\RequestHandlerInterface; use ReflectionFunction; use ReflectionMethod; +use Sunrise\Http\Router\Entity\MediaType; use Sunrise\Http\Router\RequestHandler\CallbackRequestHandler; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; use ReflectionClass; -use Reflector; use function rtrim; use function strtoupper; @@ -64,18 +64,18 @@ class Route implements RouteInterface private array $methods = []; /** - * The route's consumed media types + * The route's consumes media types * - * @var list + * @var list */ - private array $consumedMediaTypes = []; + private array $consumesMediaTypes = []; /** - * The route's produced media types + * The route's produces media types * - * @var list + * @var list */ - private array $producedMediaTypes = []; + private array $producesMediaTypes = []; /** * The route request handler @@ -180,17 +180,17 @@ public function getMethods(): array /** * @inheritDoc */ - public function getConsumedMediaTypes(): array + public function getConsumesMediaTypes(): array { - return $this->consumedMediaTypes; + return $this->consumesMediaTypes; } /** * @inheritDoc */ - public function getProducedMediaTypes(): array + public function getProducesMediaTypes(): array { - return $this->producedMediaTypes; + return $this->producesMediaTypes; } /** @@ -299,11 +299,11 @@ public function setMethods(string ...$methods): RouteInterface /** * @inheritDoc */ - public function setConsumedMediaTypes(string ...$mediaTypes): RouteInterface + public function setConsumesMediaTypes(MediaType ...$mediaTypes): RouteInterface { - $this->consumedMediaTypes = []; + $this->consumesMediaTypes = []; foreach ($mediaTypes as $mediaType) { - $this->consumedMediaTypes[] = $mediaType; + $this->consumesMediaTypes[] = $mediaType; } return $this; @@ -312,11 +312,11 @@ public function setConsumedMediaTypes(string ...$mediaTypes): RouteInterface /** * @inheritDoc */ - public function setProducedMediaTypes(string ...$mediaTypes): RouteInterface + public function setProducesMediaTypes(MediaType ...$mediaTypes): RouteInterface { - $this->producedMediaTypes = []; + $this->producesMediaTypes = []; foreach ($mediaTypes as $mediaType) { - $this->producedMediaTypes[] = $mediaType; + $this->producesMediaTypes[] = $mediaType; } return $this; @@ -436,10 +436,10 @@ public function addMethod(string ...$methods): RouteInterface /** * @inheritDoc */ - public function addConsumedMediaType(string ...$mediaTypes): RouteInterface + public function addConsumesMediaType(MediaType ...$mediaTypes): RouteInterface { foreach ($mediaTypes as $mediaType) { - $this->consumedMediaTypes[] = $mediaType; + $this->consumesMediaTypes[] = $mediaType; } return $this; @@ -448,10 +448,10 @@ public function addConsumedMediaType(string ...$mediaTypes): RouteInterface /** * @inheritDoc */ - public function addProducedMediaType(string ...$mediaTypes): RouteInterface + public function addProducesMediaType(MediaType ...$mediaTypes): RouteInterface { foreach ($mediaTypes as $mediaType) { - $this->producedMediaTypes[] = $mediaType; + $this->producesMediaTypes[] = $mediaType; } return $this; diff --git a/src/RouteCollection.php b/src/RouteCollection.php index 730bd99c..11ac5830 100644 --- a/src/RouteCollection.php +++ b/src/RouteCollection.php @@ -14,6 +14,7 @@ namespace Sunrise\Http\Router; use Psr\Http\Server\MiddlewareInterface; +use Sunrise\Http\Router\Entity\MediaType; use Sunrise\Http\Router\Exception\RouteAlreadyExistsException; use Sunrise\Http\Router\Exception\RouteNotFoundException; use Iterator; @@ -153,10 +154,10 @@ public function setHost(string $host): RouteCollectionInterface /** * @inheritDoc */ - public function setConsumedMediaTypes(string ...$mediaTypes): RouteCollectionInterface + public function setConsumesMediaTypes(MediaType ...$mediaTypes): RouteCollectionInterface { foreach ($this->routes as $route) { - $route->setConsumedMediaTypes(...$mediaTypes); + $route->setConsumesMediaTypes(...$mediaTypes); } return $this; @@ -165,10 +166,10 @@ public function setConsumedMediaTypes(string ...$mediaTypes): RouteCollectionInt /** * @inheritDoc */ - public function setProducedMediaTypes(string ...$mediaTypes): RouteCollectionInterface + public function setProducesMediaTypes(MediaType ...$mediaTypes): RouteCollectionInterface { foreach ($this->routes as $route) { - $route->setProducedMediaTypes(...$mediaTypes); + $route->setProducesMediaTypes(...$mediaTypes); } return $this; @@ -225,10 +226,10 @@ public function addMethod(string ...$methods): RouteCollectionInterface /** * @inheritDoc */ - public function addConsumedMediaType(string ...$mediaTypes): RouteCollectionInterface + public function addConsumesMediaType(MediaType ...$mediaTypes): RouteCollectionInterface { foreach ($this->routes as $route) { - $route->addConsumedMediaType(...$mediaTypes); + $route->addConsumesMediaType(...$mediaTypes); } return $this; @@ -237,10 +238,10 @@ public function addConsumedMediaType(string ...$mediaTypes): RouteCollectionInte /** * @inheritDoc */ - public function addProducedMediaType(string ...$mediaTypes): RouteCollectionInterface + public function addProducesMediaType(MediaType ...$mediaTypes): RouteCollectionInterface { foreach ($this->routes as $route) { - $route->addProducedMediaType(...$mediaTypes); + $route->addProducesMediaType(...$mediaTypes); } return $this; diff --git a/src/RouteCollectionInterface.php b/src/RouteCollectionInterface.php index 51a88cf4..2ae51da0 100644 --- a/src/RouteCollectionInterface.php +++ b/src/RouteCollectionInterface.php @@ -14,6 +14,7 @@ namespace Sunrise\Http\Router; use Psr\Http\Server\MiddlewareInterface; +use Sunrise\Http\Router\Entity\MediaType; use Sunrise\Http\Router\Exception\RouteAlreadyExistsException; use Sunrise\Http\Router\Exception\RouteNotFoundException; use Countable; @@ -98,26 +99,26 @@ public function add(RouteInterface ...$routes): RouteCollectionInterface; public function setHost(string $host): RouteCollectionInterface; /** - * Sets the given consumed media type(s) to all routes in the collection + * Sets the given consumes media type(s) to all routes in the collection * - * @param string ...$mediaTypes + * @param MediaType ...$mediaTypes * * @return RouteCollectionInterface * * @since 3.0.0 */ - public function setConsumedMediaTypes(string ...$mediaTypes): RouteCollectionInterface; + public function setConsumesMediaTypes(MediaType ...$mediaTypes): RouteCollectionInterface; /** - * Sets the given produced media type(s) to all routes in the collection + * Sets the given produces media type(s) to all routes in the collection * - * @param string ...$mediaTypes + * @param MediaType ...$mediaTypes * * @return RouteCollectionInterface * * @since 3.0.0 */ - public function setProducedMediaTypes(string ...$mediaTypes): RouteCollectionInterface; + public function setProducesMediaTypes(MediaType ...$mediaTypes): RouteCollectionInterface; /** * Sets the given attribute to all routes in the collection @@ -165,26 +166,26 @@ public function addSuffix(string $suffix): RouteCollectionInterface; public function addMethod(string ...$methods): RouteCollectionInterface; /** - * Adds the given consumed media type(s) to all routes in the collection + * Adds the given consumes media type(s) to all routes in the collection * - * @param string ...$mediaTypes + * @param MediaType ...$mediaTypes * * @return RouteCollectionInterface * * @since 3.0.0 */ - public function addConsumedMediaType(string ...$mediaTypes): RouteCollectionInterface; + public function addConsumesMediaType(MediaType ...$mediaTypes): RouteCollectionInterface; /** - * Adds the given produced media type(s) to all routes in the collection + * Adds the given produces media type(s) to all routes in the collection * - * @param string ...$mediaTypes + * @param MediaType ...$mediaTypes * * @return RouteCollectionInterface * * @since 3.0.0 */ - public function addProducedMediaType(string ...$mediaTypes): RouteCollectionInterface; + public function addProducesMediaType(MediaType ...$mediaTypes): RouteCollectionInterface; /** * Adds the given middleware(s) to all routes in the collection diff --git a/src/RouteInterface.php b/src/RouteInterface.php index a1e32cd9..83d8c861 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -19,7 +19,7 @@ use ReflectionClass; use ReflectionMethod; use ReflectionFunction; -use Reflector; +use Sunrise\Http\Router\Entity\MediaType; /** * RouteInterface @@ -67,22 +67,22 @@ public function getPath(): string; public function getMethods(): array; /** - * Gets the route's consumed media types + * Gets the route's consumes media types * - * @return list + * @return list * * @since 3.0.0 */ - public function getConsumedMediaTypes(): array; + public function getConsumesMediaTypes(): array; /** - * Gets the route's produced media types + * Gets the route's produces media types * - * @return list + * @return list * * @since 3.0.0 */ - public function getProducedMediaTypes(): array; + public function getProducesMediaTypes(): array; /** * Gets the route request handler @@ -180,26 +180,26 @@ public function setPath(string $path): RouteInterface; public function setMethods(string ...$methods): RouteInterface; /** - * Sets the given consumed media type(s) to the route + * Sets the given consumes media type(s) to the route * - * @param string ...$mediaTypes + * @param MediaType ...$mediaTypes * * @return RouteInterface * * @since 3.0.0 */ - public function setConsumedMediaTypes(string ...$mediaTypes): RouteInterface; + public function setConsumesMediaTypes(MediaType ...$mediaTypes): RouteInterface; /** - * Sets the given produced media type(s) to the route + * Sets the given produces media type(s) to the route * - * @param string ...$mediaTypes + * @param MediaType ...$mediaTypes * * @return RouteInterface * * @since 3.0.0 */ - public function setProducedMediaTypes(string ...$mediaTypes): RouteInterface; + public function setProducesMediaTypes(MediaType ...$mediaTypes): RouteInterface; /** * Sets the given request handler to the route @@ -301,26 +301,26 @@ public function addSuffix(string $suffix): RouteInterface; public function addMethod(string ...$methods): RouteInterface; /** - * Adds the given consumed media type(s) to the route + * Adds the given consumes media type(s) to the route * - * @param string ...$mediaTypes + * @param MediaType ...$mediaTypes * * @return RouteInterface * * @since 3.0.0 */ - public function addConsumedMediaType(string ...$mediaTypes): RouteInterface; + public function addConsumesMediaType(MediaType ...$mediaTypes): RouteInterface; /** - * Adds the given produced media type(s) to the route + * Adds the given produces media type(s) to the route * - * @param string ...$mediaTypes + * @param MediaType ...$mediaTypes * * @return RouteInterface * * @since 3.0.0 */ - public function addProducedMediaType(string ...$mediaTypes): RouteInterface; + public function addProducesMediaType(MediaType ...$mediaTypes): RouteInterface; /** * Adds the given middleware(s) to the route diff --git a/src/Router.php b/src/Router.php index 540fe8a2..4d3919e5 100644 --- a/src/Router.php +++ b/src/Router.php @@ -270,16 +270,11 @@ public function match(ServerRequestInterface $request): RouteInterface continue; } - $routeConsumes = $route->getConsumedMediaTypes(); + $routeConsumes = $route->getConsumesMediaTypes(); if (!empty($routeConsumes) && !$request->clientProducesMediaType($routeConsumes)) { throw new ClientNotProducedMediaTypeException($routeConsumes); } - $routeProduces = $route->getProducedMediaTypes(); - if (!empty($routeProduces) && !$request->clientConsumesMediaType($routeProduces)) { - throw new ClientNotConsumedMediaTypeException($routeProduces); - } - /** @var array $attributes */ return $route->withAddedAttributes($attributes); diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 0e754838..14c2e001 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -14,22 +14,25 @@ namespace Sunrise\Http\Router; use Fig\Http\Message\RequestMethodInterface; +use Generator; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; -use Sunrise\Http\Router\Entity\IpAddress; - +use Sunrise\Http\Router\Entity\ClientRemoteAddress; use Sunrise\Http\Router\Entity\MediaType; -use function array_merge; +use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; +use Sunrise\Http\Router\Exception\InvalidArgumentException; + +use Throwable; +use function current; use function explode; use function key; use function preg_split; -use function reset; +use function sprintf; use function strncmp; use function strpos; -use function strstr; use function strtolower; -use function trim; +use function usort; /** * ServerRequest @@ -65,178 +68,122 @@ public static function from(ServerRequestInterface $request): self } /** - * Checks if the request is JSON + * Gets the client's remote address * - * @link https://tools.ietf.org/html/rfc4627 + * @param array $proxyChain * - * @return bool - */ - public function isJson(): bool - { - return $this->clientProducesMediaType([ - MediaType::APPLICATION_JSON, - ]); - } - - /** - * Checks if the request is XML + * @return ClientRemoteAddress * - * @link https://tools.ietf.org/html/rfc2376 + * @template TKey as non-empty-string Proxy address; e.g., 127.0.0.1 + * @template TValue as non-empty-string Trusted header; e.g., X-Forwarded-For * - * @return bool + * @link https://www.rfc-editor.org/rfc/rfc7239.html#section-5.2 */ - public function isXml(): bool - { - return $this->clientProducesMediaType([ - MediaType::APPLICATION_XML, - MediaType::TEXT_XML, - ]); - } - - /** - * Gets the client's IP address - * - * @param array $proxyChain - * - * @return IpAddress - */ - public function getClientIpAddress(array $proxyChain = []): IpAddress + public function getClientRemoteAddress(array $proxyChain = []): ClientRemoteAddress { $serverParams = $this->request->getServerParams(); - /** @var non-empty-string $clientAddress */ $clientAddress = $serverParams['REMOTE_ADDR'] ?? '::1'; - /** @var list $proxyAddresses */ $proxyAddresses = []; while (isset($proxyChain[$clientAddress])) { - $proxyHeader = $proxyChain[$clientAddress]; + $trustedHeader = $proxyChain[$clientAddress]; unset($proxyChain[$clientAddress]); - $header = $this->request->getHeaderLine($proxyHeader); + $header = $this->request->getHeaderLine($trustedHeader); if ($header === '') { break; } /** @var list $addresses */ $addresses = preg_split('/\s*,\s*/', $header, -1, PREG_SPLIT_NO_EMPTY); - if ($addresses === []) { + if (empty($addresses)) { break; } - $clientAddress = array_shift($addresses); - if ($addresses === []) { - continue; - } + $clientAddress = $addresses[0]; + unset($addresses[0]); - $proxyAddresses = array_merge($proxyAddresses, $addresses); + foreach ($addresses as $address) { + $proxyAddresses[] = $address; + } } - return new IpAddress($clientAddress, $proxyAddresses); + return new ClientRemoteAddress($clientAddress, $proxyAddresses); } /** - * Gets the client's produced media type - * - * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1 - * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.5 + * Gets the media type that the client produced * - * @return string + * @return MediaType|null */ - public function getClientProducedMediaType(): string + public function getClientProducedMediaType(): ?MediaType { $header = $this->request->getHeaderLine('Content-Type'); - if ($header === '') { - return ''; - } - - $result = strstr($header, ';', true); - if ($result === false) { - $result = $header; - } - $result = trim($result); - if ($result === '') { - return ''; + try { + return parse_header_with_media_type($header)->current(); + } catch (InvalidArgumentException $e) { + throw new HttpBadRequestException(sprintf('The Content-Type header is invalid: %s', $e->getMessage())); } - - return strtolower($result); } /** - * Gets the client's consumed media types - * - * @link https://tools.ietf.org/html/rfc7231#section-1.2 - * @link https://tools.ietf.org/html/rfc7231#section-3.1.1.1 - * @link https://tools.ietf.org/html/rfc7231#section-5.3.2 + * Gets the media types that the client consumes * - * @return array> + * @return Generator, MediaType> */ - public function getClientConsumedMediaTypes(): array + public function getClientConsumesMediaTypes(): Generator { $header = $this->request->getHeaderLine('Accept'); - if ($header === '') { - return []; - } - $accepts = parse_accept_header($header); - if (empty($accepts)) { - return []; + try { + yield from parse_header_with_media_type($header); + } catch (InvalidArgumentException $e) { + throw new HttpBadRequestException(sprintf('The Accept header is invalid: %s', $e->getMessage())); } - - $result = []; - foreach ($accepts as $type => $params) { - $result[strtolower($type)] = $params; - } - - return $result; } /** - * Gets the client's consumed encodings + * Gets the client's preferred media type * - * @return array> + * @param MediaType ...$serverProducesMediaTypes + * + * @return MediaType|null */ - public function getClientConsumedEncodings(): array + public function getClientPreferredMediaType(MediaType ...$serverProducesMediaTypes): ?MediaType { - $header = $this->request->getHeaderLine('Accept-Encoding'); - if ($header === '') { - return []; + if ($serverProducesMediaTypes === []) { + return null; } - $accepts = header_accept_like_parse($header); - if (empty($accepts)) { - return []; - } + /** @var list $clientConsumesMediaTypes */ + $clientConsumesMediaTypes = [...$this->getClientConsumesMediaTypes()]; - return $accepts; - } - - /** - * Gets the client's consumed languages - * - * @return array> - */ - public function getClientConsumedLanguages(): array - { - $header = $this->request->getHeaderLine('Accept-Language'); - if ($header === '') { - return []; + if ($clientConsumesMediaTypes === []) { + return current($serverProducesMediaTypes); } - $accepts = header_accept_like_parse($header); - if (empty($accepts)) { - return []; + usort($clientConsumesMediaTypes, static fn (MediaType $a, MediaType $b): int => ( + $b->getQualityFactor() <=> $a->getQualityFactor() + )); + + foreach ($clientConsumesMediaTypes as $clientConsumesMediaType) { + foreach ($serverProducesMediaTypes as $serverProducesMediaType) { + if ($serverProducesMediaType->equals($clientConsumesMediaType)) { + return $serverProducesMediaType; + } + } } - return $accepts; + return current($serverProducesMediaTypes); } /** * Checks if the client produces one of the given media types * - * @param list $consumes + * @param MediaType ...$consumes * * @return bool */ @@ -260,70 +207,6 @@ public function clientProducesMediaType(array $consumes): bool return false; } - /** - * Checks if the client consumes one of the given media types - * - * @param list $produces - * - * @return bool - */ - public function clientConsumesMediaType(array $produces): bool - { - if ($produces === []) { - return true; - } - - $consumes = $this->getClientConsumedMediaTypes(); - if ($consumes === []) { - return true; - } - - if (isset($consumes['*/*'])) { - return true; - } - - foreach ($produces as $a) { - foreach ($consumes as $b => $_) { - if (media_types_compare($a, $b)) { - return true; - } - } - } - - return false; - } - - /** - * Checks if the given media types are equal - * - * @param string $a - * @param string $b - * - * @return bool - */ - public function equalsMediaTypes(string $a, string $b): bool - { - if ($a === $b) { - return true; - } - - $slash = strpos($a, '/'); - if ($slash === false || !isset($b[$slash]) || $b[$slash] !== '/') { - return false; - } - - $star = $slash + 1; - if (!isset($a[$star], $b[$star])) { - return false; - } - - if (!($a[$star] === '*' || $b[$star] === '*')) { - return false; - } - - return strncmp($a, $b, $star) === 0; - } - /** * @inheritDoc */ From 3eb6a530a177292179adf0245e8cf646bf26b69f Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 20 Aug 2023 08:04:28 +0200 Subject: [PATCH 080/180] v3 --- composer.json | 7 +- functions/parse_header.php | 19 +-- functions/parse_header_with_media_type.php | 26 +++- src/Annotation/{Version.php => Consumes.php} | 4 +- .../{Produce.php => JsonResponseBody.php} | 10 +- src/Annotation/{Consume.php => Produces.php} | 7 +- src/Annotation/RequestEntity.php | 40 ++++++ .../{ResponseBody.php => RequestHeader.php} | 4 +- ...ProxyChain.php => RequestPathVariable.php} | 14 +- src/Annotation/ResponseHeader.php | 2 +- src/ClassResolver.php | 3 +- src/Dictionary/Charset.php | 2 - src/Entity/ClientRemoteAddress.php | 16 +-- src/Entity/MediaType.php | 91 +++++++----- src/Loader/DescriptorLoader.php | 108 ++++++-------- src/Middleware/CallbackMiddleware.php | 6 +- .../JsonPayloadDecodingMiddleware.php | 16 +-- .../SimdJsonPayloadDecodingMiddleware.php | 101 -------------- .../ClientRemoteAddressParameterResolver.php | 15 +- ....php => PresetObjectParameterResolver.php} | 10 +- .../RequestEntityParameterResolver.php | 132 ++++++++++++++++++ .../RequestHeaderParameterResolver.php | 37 +++++ .../RequestPathVariableParameterResolver.php | 37 +++++ src/RequestHandler/CallbackRequestHandler.php | 6 +- .../QueueableRequestHandler.php | 39 +----- src/ResponseResolutioner.php | 29 ++-- .../EmptyResponseResolver.php | 3 +- .../ExceptionResponseResolver.php | 96 +++++++++++++ .../JsonResponseBodyResponseResolver.php | 106 ++++++++++++++ .../ResponseBodyResponseResolver.php | 98 ------------- .../StreamResponseResolver.php | 3 +- src/ResponseResolver/UriResponseResolver.php | 3 +- src/Route.php | 5 +- src/Router.php | 17 +-- src/ServerRequest.php | 99 ++++++------- 35 files changed, 700 insertions(+), 511 deletions(-) rename src/Annotation/{Version.php => Consumes.php} (83%) rename src/Annotation/{Produce.php => JsonResponseBody.php} (62%) rename src/Annotation/{Consume.php => Produces.php} (76%) create mode 100644 src/Annotation/RequestEntity.php rename src/Annotation/{ResponseBody.php => RequestHeader.php} (81%) rename src/Annotation/{ProxyChain.php => RequestPathVariable.php} (60%) delete mode 100644 src/Middleware/SimdJsonPayloadDecodingMiddleware.php rename src/ParameterResolver/{ObjectInjectionParameterResolver.php => PresetObjectParameterResolver.php} (81%) create mode 100644 src/ParameterResolver/RequestEntityParameterResolver.php create mode 100644 src/ParameterResolver/RequestHeaderParameterResolver.php create mode 100644 src/ParameterResolver/RequestPathVariableParameterResolver.php create mode 100644 src/ResponseResolver/ExceptionResponseResolver.php create mode 100644 src/ResponseResolver/JsonResponseBodyResponseResolver.php delete mode 100644 src/ResponseResolver/ResponseBodyResponseResolver.php diff --git a/composer.json b/composer.json index bab7bb85..2e51a6c9 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,9 @@ "sunrise/coding-standard": "^1.0", "sunrise/http-message": "^3.0", "symfony/console": "^6.0", - "symfony/http-foundation": "^6.3" + "symfony/http-foundation": "^6.3", + "doctrine/orm": "^2.16", + "filp/whoops": "^2.15" }, "autoload": { "files": [ @@ -60,7 +62,8 @@ "functions/reflect_callback.php" ], "psr-4": { - "Sunrise\\Http\\Router\\": "src/" + "Sunrise\\Http\\Router\\": "src/", + "App\\": "example/" } }, "autoload-dev": { diff --git a/functions/parse_header.php b/functions/parse_header.php index 50fe3335..7d91e500 100644 --- a/functions/parse_header.php +++ b/functions/parse_header.php @@ -27,13 +27,13 @@ * * @throws InvalidArgumentException If the given header is invalid. * - * @template T as list}> + * @template T as list}> * * @since 3.0.0 */ function parse_header(string $header): array { - /** @var list}> $matches */ + /** @var T $matches */ $matches = []; $offset = -1; @@ -160,19 +160,8 @@ function parse_header(string $header): array continue; } - throw new InvalidArgumentException(sprintf('Unexpected character at position %d', $offset)); + throw new InvalidArgumentException(sprintf('Unexpected character at position %d.', $offset)); } - $result = []; - foreach ($matches as $index => $match) { - $result[$index] = [$match[0], []]; - if (isset($match[1])) { - foreach ($match[1] as $param) { - $result[$index][1][$param[0]] = $param[1] ?? null; - } - } - } - - /** @var T */ - return $result; + return $matches; } diff --git a/functions/parse_header_with_media_type.php b/functions/parse_header_with_media_type.php index fbdb526a..cd87f751 100644 --- a/functions/parse_header_with_media_type.php +++ b/functions/parse_header_with_media_type.php @@ -19,14 +19,13 @@ use Sunrise\Http\Router\Exception\InvalidArgumentException; use function sprintf; -use function strtolower; /** * Parses the given header that contains media types * * @param string $header * - * @return Generator, MediaType> + * @return Generator * * @throws InvalidArgumentException If one of the media types is invalid. * @@ -39,7 +38,7 @@ function parse_header_with_media_type(string $header): Generator return; } - foreach ($matches as $index => [$token, $parameters]) { + foreach ($matches as $index => $match) { $offset = -1; $inType = true; @@ -51,12 +50,12 @@ function parse_header_with_media_type(string $header): Generator while (true) { $offset++; - $char = $token[$offset] ?? null; + $char = $match[0][$offset] ?? null; if ($char === null) { break; } - if ($char === '/' && $inSubtype === false) { + if ($inType && $char === '/') { $inType = false; $inSubtype = true; continue; @@ -66,7 +65,6 @@ function parse_header_with_media_type(string $header): Generator $type .= $char; continue; } - if ($inSubtype && isset(Charset::RFC7230_TOKEN[$char])) { $subtype .= $char; continue; @@ -86,6 +84,20 @@ function parse_header_with_media_type(string $header): Generator )); } - yield $index => new MediaType(strtolower($type), strtolower($subtype), $parameters); + if ($type === '*' && $subtype !== '*') { + throw new InvalidArgumentException(sprintf( + 'Subtype "*" expected for media type with index %d.', + $index, + )); + } + + $parameters = []; + if (isset($match[1])) { + foreach ($match[1] as $param) { + $parameters[$param[0]] = $param[1] ?? null; + } + } + + yield new MediaType($type, $subtype, $parameters); } } diff --git a/src/Annotation/Version.php b/src/Annotation/Consumes.php similarity index 83% rename from src/Annotation/Version.php rename to src/Annotation/Consumes.php index 8d018ab9..6cd0a05b 100644 --- a/src/Annotation/Version.php +++ b/src/Annotation/Consumes.php @@ -18,8 +18,8 @@ /** * @since 3.0.0 */ -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)] -final class Version +#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] +final class Consumes { /** diff --git a/src/Annotation/Produce.php b/src/Annotation/JsonResponseBody.php similarity index 62% rename from src/Annotation/Produce.php rename to src/Annotation/JsonResponseBody.php index 407b911e..ee1c32ed 100644 --- a/src/Annotation/Produce.php +++ b/src/Annotation/JsonResponseBody.php @@ -14,21 +14,21 @@ namespace Sunrise\Http\Router\Annotation; use Attribute; -use Sunrise\Http\Router\Entity\MediaType; /** * @since 3.0.0 */ -#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -final class Produce +#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)] +final class JsonResponseBody { /** * Constructor of the class * - * @param MediaType|non-empty-string $value + * @param int|null $jsonEncodingFlags + * @param int|null $jsonEncodingDepth */ - public function __construct(public MediaType|string $value) + public function __construct(public ?int $jsonEncodingFlags = null, public ?int $jsonEncodingDepth = null) { } } diff --git a/src/Annotation/Consume.php b/src/Annotation/Produces.php similarity index 76% rename from src/Annotation/Consume.php rename to src/Annotation/Produces.php index bcbf45a4..5b467587 100644 --- a/src/Annotation/Consume.php +++ b/src/Annotation/Produces.php @@ -14,21 +14,20 @@ namespace Sunrise\Http\Router\Annotation; use Attribute; -use Sunrise\Http\Router\Entity\MediaType; /** * @since 3.0.0 */ #[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)] -final class Consume +final class Produces { /** * Constructor of the class * - * @param MediaType|non-empty-string $value + * @param non-empty-string $value */ - public function __construct(public MediaType|string $value) + public function __construct(public string $value) { } } diff --git a/src/Annotation/RequestEntity.php b/src/Annotation/RequestEntity.php new file mode 100644 index 00000000..2b6313a9 --- /dev/null +++ b/src/Annotation/RequestEntity.php @@ -0,0 +1,40 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Annotation; + +use Attribute; + +/** + * @since 3.0.0 + */ +#[Attribute(Attribute::TARGET_PARAMETER)] +final class RequestEntity +{ + + /** + * Constructor of the class + * + * @param non-empty-string|null $em + * @param non-empty-string|null $findBy + * @param non-empty-string|null $valueKey + * @param array $criteria An entity's additional search criteria + */ + public function __construct( + public string|null $em = null, + public string|null $findBy = null, + public string|null $valueKey = null, + public array $criteria = [], + ) { + } +} diff --git a/src/Annotation/ResponseBody.php b/src/Annotation/RequestHeader.php similarity index 81% rename from src/Annotation/ResponseBody.php rename to src/Annotation/RequestHeader.php index bc866170..3703e1e8 100644 --- a/src/Annotation/ResponseBody.php +++ b/src/Annotation/RequestHeader.php @@ -18,7 +18,7 @@ /** * @since 3.0.0 */ -#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)] -final class ResponseBody +#[Attribute(Attribute::TARGET_PARAMETER)] +final class RequestHeader { } diff --git a/src/Annotation/ProxyChain.php b/src/Annotation/RequestPathVariable.php similarity index 60% rename from src/Annotation/ProxyChain.php rename to src/Annotation/RequestPathVariable.php index ff449937..9938a654 100644 --- a/src/Annotation/ProxyChain.php +++ b/src/Annotation/RequestPathVariable.php @@ -19,18 +19,6 @@ * @since 3.0.0 */ #[Attribute(Attribute::TARGET_PARAMETER)] -final class ProxyChain +final class RequestPathVariable { - - /** - * Constructor of the class - * - * @param array $value - * - * @template TKey as non-empty-string Proxy address - * @template TValue as non-empty-string Trusted header - */ - public function __construct(public array $value) - { - } } diff --git a/src/Annotation/ResponseHeader.php b/src/Annotation/ResponseHeader.php index b640211b..0e5d020b 100644 --- a/src/Annotation/ResponseHeader.php +++ b/src/Annotation/ResponseHeader.php @@ -26,7 +26,7 @@ final class ResponseHeader * Constructor of the class * * @param non-empty-string $name - * @param string $value + * @param non-empty-string $value */ public function __construct(public string $name, public string $value) { diff --git a/src/ClassResolver.php b/src/ClassResolver.php index 88522cd1..daf676c3 100644 --- a/src/ClassResolver.php +++ b/src/ClassResolver.php @@ -19,6 +19,7 @@ use function class_exists; use function sprintf; +use function var_dump; /** * ClassResolver @@ -82,7 +83,7 @@ public function resolveClass(string $fqn): object } /** @var T $instance */ - $instance = new $class(...$arguments); + $instance = new $fqn(...$arguments); $this->resolvedClasses[$fqn] = $instance; diff --git a/src/Dictionary/Charset.php b/src/Dictionary/Charset.php index 05e975e9..720e0c9c 100644 --- a/src/Dictionary/Charset.php +++ b/src/Dictionary/Charset.php @@ -20,8 +20,6 @@ */ final class Charset { - public const WILDCARD = '*'; - public const RFC7230_OWS = [ "\x09" => 1, "\x20" => 1, ]; diff --git a/src/Entity/ClientRemoteAddress.php b/src/Entity/ClientRemoteAddress.php index 2d67b9dc..691f33af 100644 --- a/src/Entity/ClientRemoteAddress.php +++ b/src/Entity/ClientRemoteAddress.php @@ -16,7 +16,7 @@ use Stringable; /** - * Client remote address + * Client Remote Address * * @since 3.0.0 */ @@ -26,21 +26,21 @@ final class ClientRemoteAddress implements Stringable /** * Constructor of the class * - * @param non-empty-string $value The address value - * @param list $proxies The list of proxies in front of this address + * @param non-empty-string $address + * @param list $proxies */ - public function __construct(private string $value, private array $proxies = []) + public function __construct(private string $address, private array $proxies = []) { } /** - * Gets the address value + * Gets the client's remote address * * @return non-empty-string */ - public function getValue(): string + public function getAddress(): string { - return $this->value; + return $this->address; } /** @@ -58,6 +58,6 @@ public function getProxies(): array */ public function __toString(): string { - return $this->value; + return $this->address; } } diff --git a/src/Entity/MediaType.php b/src/Entity/MediaType.php index 2ed04d60..b2ce333c 100644 --- a/src/Entity/MediaType.php +++ b/src/Entity/MediaType.php @@ -13,14 +13,16 @@ namespace Sunrise\Http\Router\Entity; -use Sunrise\Http\Router\Dictionary\Charset; +use Stringable; + +use function sprintf; /** - * Media type + * Media Type * * @since 3.0.0 */ -final class MediaType +final class MediaType implements Stringable { /** @@ -28,80 +30,88 @@ final class MediaType * * @param non-empty-string $type * @param non-empty-string $subtype - * @param array $parameters + * @param array $parameters */ public function __construct(private string $type, private string $subtype, private array $parameters = []) { } /** - * Gets the type of the media type + * Creates the json media type * - * @return non-empty-string + * @param array $parameters + * + * @return self */ - public function getType(): string + public static function json(array $parameters = []): self { - return $this->type; + return new self('application', 'json', $parameters); } /** - * Gets the subtype of the media type + * Creates the xml media type * - * @return non-empty-string + * @param array $parameters + * + * @return self */ - public function getSubtype(): string + public static function xml(array $parameters = []): self { - return $this->subtype; + return new self('application', 'xml', $parameters); } /** - * Gets the parameters of the media type + * Creates the yaml media type + * + * @param array $parameters * - * @return array + * @return self */ - public function getParameters(): array + public static function yaml(array $parameters = []): self { - return $this->parameters; + return new self('application', 'yaml', $parameters); } /** - * Gets the media range of the media type + * Creates the html media type * - * @return non-empty-string + * @param array $parameters + * + * @return self */ - public function getMediaRange(): string + public static function html(array $parameters = []): self { - return $this->type . '/' . $this->subtype; + return new self('text', 'html', $parameters); } /** - * Gets the quality factor of the media type + * Gets the media range type * - * @return float + * @return non-empty-string */ - public function getQualityFactor(): float + public function getType(): string { - return (float) ($this->parameters['q'] ?? 1.); + return $this->type; } /** - * Checks if the type of the media type is wildcard + * Gets the media range subtype * - * @return bool + * @return non-empty-string */ - public function isWildcardType(): bool + public function getSubtype(): string { - return $this->type === Charset::WILDCARD; + return $this->subtype; } /** - * Checks if the subtype of the media type is wildcard + * Gets the media range parameters * - * @return bool + * @return array */ - public function isWildcardSubtype(): bool + public function getParameters(): array { - return $this->subtype === Charset::WILDCARD; + return $this->parameters; } /** @@ -113,7 +123,20 @@ public function isWildcardSubtype(): bool */ public function equals(MediaType $other): bool { - return ($this->isWildcardType() || $other->isWildcardType() || $this->getType() === $other->getType()) - && ($this->isWildcardSubtype() || $other->isWildcardSubtype() || $this->getType() === $other->getSubtype()); + return ($this->type === '*' || $other->type === '*' || $this->type === $other->type) + && ($this->subtype === '*' || $other->subtype === '*' || $this->subtype === $other->subtype); + } + + /** + * @inheritDoc + */ + public function __toString(): string + { + $result = sprintf('%s/%s', $this->type, $this->subtype); + foreach ($this->parameters as $name => $value) { + $result .= sprintf('; %s="%s"', $name, $value); + } + + return $result; } } diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index e823f85d..7f716f36 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -23,18 +23,17 @@ use ReflectionClass; use ReflectionMethod; use RegexIterator; -use Sunrise\Http\Router\Annotation\Consume; +use Sunrise\Http\Router\Annotation\Consumes; use Sunrise\Http\Router\Annotation\Description; use Sunrise\Http\Router\Annotation\Host; use Sunrise\Http\Router\Annotation\Method; use Sunrise\Http\Router\Annotation\Middleware; use Sunrise\Http\Router\Annotation\Postfix; use Sunrise\Http\Router\Annotation\Prefix; -use Sunrise\Http\Router\Annotation\Produce; +use Sunrise\Http\Router\Annotation\Produces; use Sunrise\Http\Router\Annotation\Route; use Sunrise\Http\Router\Annotation\Summary; use Sunrise\Http\Router\Annotation\Tag; -use Sunrise\Http\Router\Entity\MediaType; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ParameterResolutioner; @@ -57,12 +56,9 @@ use function hash; use function is_dir; use function is_string; -use function iterator_to_array; use function sprintf; use function usort; -use function Sunrise\Http\Router\parse_header_with_media_type; - /** * DescriptorLoader */ @@ -394,8 +390,8 @@ private function getClassDescriptors(ReflectionClass $class): Generator if ($class->isSubclassOf(RequestHandlerInterface::class)) { $annotations = $this->getAnnotations(Route::class, $class); - if (isset($annotations[0])) { - $descriptor = $annotations[0]; + if ($annotations->valid()) { + $descriptor = $annotations->current(); $descriptor->holder = $class->getName(); $this->supplementDescriptor($descriptor, $class); yield $descriptor; @@ -409,8 +405,8 @@ private function getClassDescriptors(ReflectionClass $class): Generator } $annotations = $this->getAnnotations(Route::class, $method); - if (isset($annotations[0])) { - $descriptor = $annotations[0]; + if ($annotations->valid()) { + $descriptor = $annotations->current(); $descriptor->holder = [$class->getName(), $method->getName()]; $this->supplementDescriptor($descriptor, $class); $this->supplementDescriptor($descriptor, $method); @@ -430,18 +426,18 @@ private function getClassDescriptors(ReflectionClass $class): Generator private function supplementDescriptor(Route $descriptor, ReflectionClass|ReflectionMethod $holder): void { $annotations = $this->getAnnotations(Host::class, $holder); - if (isset($annotations[0])) { - $descriptor->host = $annotations[0]->value; + if ($annotations->valid()) { + $descriptor->host = $annotations->current()->value; } $annotations = $this->getAnnotations(Prefix::class, $holder); - if (isset($annotations[0])) { - $descriptor->path = $annotations[0]->value . $descriptor->path; + if ($annotations->valid()) { + $descriptor->path = $annotations->current()->value . $descriptor->path; } $annotations = $this->getAnnotations(Postfix::class, $holder); - if (isset($annotations[0])) { - $descriptor->path .= $annotations[0]->value; + if ($annotations->valid()) { + $descriptor->path .= $annotations->current()->value; } $annotations = $this->getAnnotations(Method::class, $holder); @@ -449,29 +445,19 @@ private function supplementDescriptor(Route $descriptor, ReflectionClass|Reflect $descriptor->methods[] = $annotation->value; } - $annotations = $this->getAnnotations(Consume::class, $holder); + $annotations = $this->getAnnotations(Consumes::class, $holder); foreach ($annotations as $annotation) { - if ($annotation->value instanceof MediaType) { - $descriptor->consumes[] = $annotation->value; - continue; - } - - $consumes = parse_header_with_media_type($annotation->value); - foreach ($consumes as $consume) { - $descriptor->consumes[] = $consume; + $consumesMediaTypes = \Sunrise\Http\Router\parse_header_with_media_type($annotation->value); + foreach ($consumesMediaTypes as $consumesMediaType) { + $descriptor->consumes[] = $consumesMediaType; } } - $annotations = $this->getAnnotations(Produce::class, $holder); + $annotations = $this->getAnnotations(Produces::class, $holder); foreach ($annotations as $annotation) { - if ($annotation->value instanceof MediaType) { - $descriptor->produces[] = $annotation->value; - continue; - } - - $produces = parse_header_with_media_type($annotation->value); - foreach ($produces as $produce) { - $descriptor->produces[] = $produce; + $producesMediaTypes = \Sunrise\Http\Router\parse_header_with_media_type($annotation->value); + foreach ($producesMediaTypes as $producesMediaType) { + $descriptor->produces[] = $producesMediaType; } } @@ -496,27 +482,6 @@ private function supplementDescriptor(Route $descriptor, ReflectionClass|Reflect } } - /** - * Gets the named annotations from the given class or method - * - * @param class-string $name - * @param ReflectionClass|ReflectionMethod $source - * - * @return list - * - * @template T of object - */ - private function getAnnotations(string $name, ReflectionClass|ReflectionMethod $source): array - { - $result = []; - $attributes = $source->getAttributes($name); - foreach ($attributes as $attribute) { - $result[] = $attribute->newInstance(); - } - - return $result; - } - /** * Scans the given directory and returns the found classes * @@ -527,7 +492,7 @@ private function getAnnotations(string $name, ReflectionClass|ReflectionMethod $ private function getDirectoryClasses(string $dirname): Generator { /** @var array $filenames */ - $filenames = iterator_to_array( + $filenames = [...( new RegexIterator( new RecursiveIteratorIterator( new RecursiveDirectoryIterator( @@ -535,9 +500,9 @@ private function getDirectoryClasses(string $dirname): Generator FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_PATHNAME, ) ), - '/\.php$/', + pattern: '/\.php$/', ) - ); + )]; foreach ($filenames as $filename) { (static function (string $filename): void { @@ -546,12 +511,29 @@ private function getDirectoryClasses(string $dirname): Generator })($filename); } - foreach (get_declared_classes() as $fqn) { - $class = new ReflectionClass($fqn); - $filename = $class->getFileName(); - if (isset($filenames[$filename])) { - yield $class; + foreach (get_declared_classes() as $className) { + $classReflection = new ReflectionClass($className); + if (isset($filenames[$classReflection->getFileName()])) { + yield $classReflection; } } } + + /** + * Gets the named annotations from the given class or method + * + * @param class-string $name + * @param ReflectionClass|ReflectionMethod $source + * + * @return Generator + * + * @template T of object + */ + private function getAnnotations(string $name, ReflectionClass|ReflectionMethod $source): Generator + { + $attributes = $source->getAttributes($name); + foreach ($attributes as $attribute) { + yield $attribute->newInstance(); + } + } } diff --git a/src/Middleware/CallbackMiddleware.php b/src/Middleware/CallbackMiddleware.php index 8fad2684..64d83c57 100644 --- a/src/Middleware/CallbackMiddleware.php +++ b/src/Middleware/CallbackMiddleware.php @@ -20,7 +20,7 @@ use ReflectionFunction; use ReflectionMethod; use Sunrise\Http\Router\ParameterResolutionerInterface; -use Sunrise\Http\Router\ParameterResolver\ObjectInjectionParameterResolver; +use Sunrise\Http\Router\ParameterResolver\PresetObjectParameterResolver; use Sunrise\Http\Router\ResponseResolutionerInterface; use function Sunrise\Http\Router\reflect_callback; @@ -91,8 +91,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $arguments = $this->parameterResolutioner ->withContext($request) ->withPriorityResolver( - new ObjectInjectionParameterResolver($request), - new ObjectInjectionParameterResolver($handler), + new PresetObjectParameterResolver($request), + new PresetObjectParameterResolver($handler), ) ->resolveParameters(...$source->getParameters()); diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index f249fb98..6dbd6d97 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -60,17 +60,8 @@ public function __construct() */ public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { - $requestProxy = ServerRequest::from($request); - $clientProducedMediaType = $requestProxy->getClientProducedMediaType(); - if (isset($clientProducedMediaType)) { - $serverConsumesMediaType = new MediaType('application', 'json'); - if ($serverConsumesMediaType->equals($clientProducedMediaType)) { - $request = $request->withParsedBody( - $this->decodePayload( - $request->getBody()->__toString() - ) - ); - } + if (ServerRequest::from($request)->clientProducesMediaType(MediaType::json())) { + $request = $request->withParsedBody($this->decodePayload($request->getBody()->__toString())); } return $handler->handle($request); @@ -93,8 +84,7 @@ private function decodePayload(string $payload): array throw new InvalidRequestPayloadException(sprintf('Invalid JSON payload: %s', $e->getMessage()), 0, $e); } - // According to PSR-7, the data must be an array - // because we're using the 'associative' option when decoding the JSON. + // According to PSR-7, the data must be an array... if (!is_array($data)) { throw new InvalidRequestPayloadException('Unexpected JSON: Expects an array or object.'); } diff --git a/src/Middleware/SimdJsonPayloadDecodingMiddleware.php b/src/Middleware/SimdJsonPayloadDecodingMiddleware.php deleted file mode 100644 index 7309ff99..00000000 --- a/src/Middleware/SimdJsonPayloadDecodingMiddleware.php +++ /dev/null @@ -1,101 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Middleware; - -use SimdJsonException; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Server\MiddlewareInterface; -use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Router\Entity\MediaType; -use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; -use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ServerRequest; - -use function extension_loaded; -use function is_array; -use function simdjson_decode; -use function sprintf; - -/** - * Middleware for JSON payload decoding using the Simdjson extension - * - * @since 3.0.0 - * - * @link https://www.php.net/manual/en/book.simdjson.php - */ -final class SimdJsonPayloadDecodingMiddleware implements MiddlewareInterface -{ - - /** - * Constructor of the class - * - * @throws LogicException If the Simdjson extension isn't loaded. - */ - public function __construct() - { - if (!extension_loaded('simdjson')) { - throw new LogicException( - 'The SimdJson extension is required, run the `pecl install simdjson` command to resolve it.' - ); - } - } - - /** - * @inheritDoc - */ - public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface - { - $requestProxy = ServerRequest::from($request); - $clientProducedMediaType = $requestProxy->getClientProducedMediaType(); - if (isset($clientProducedMediaType)) { - $serverConsumesMediaType = new MediaType('application', 'json'); - if ($serverConsumesMediaType->equals($clientProducedMediaType)) { - $request = $request->withParsedBody( - $this->decodePayload( - $request->getBody()->__toString() - ) - ); - } - } - - return $handler->handle($request); - } - - /** - * Tries to decode the given JSON payload - * - * @param string $payload - * - * @return array - * - * @throws InvalidRequestPayloadException If the JSON payload cannot be decoded. - */ - private function decodePayload(string $payload): array - { - try { - $data = simdjson_decode($payload, true, 512); - } catch (SimdJsonException $e) { - throw new InvalidRequestPayloadException(sprintf('Invalid JSON payload: %s', $e->getMessage()), 0, $e); - } - - // According to PSR-7, the data must be an array - // because we're using the 'associative' option when decoding the JSON. - if (!is_array($data)) { - throw new InvalidRequestPayloadException('Unexpected JSON: Expects an array or object.'); - } - - return $data; - } -} diff --git a/src/ParameterResolver/ClientRemoteAddressParameterResolver.php b/src/ParameterResolver/ClientRemoteAddressParameterResolver.php index 27bd8ebc..e1e00e80 100644 --- a/src/ParameterResolver/ClientRemoteAddressParameterResolver.php +++ b/src/ParameterResolver/ClientRemoteAddressParameterResolver.php @@ -15,10 +15,8 @@ use Generator; use Psr\Http\Message\ServerRequestInterface; -use ReflectionAttribute; use ReflectionNamedType; use ReflectionParameter; -use Sunrise\Http\Router\Annotation\ProxyChain; use Sunrise\Http\Router\Entity\ClientRemoteAddress; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\ServerRequest; @@ -36,8 +34,8 @@ final class ClientRemoteAddressParameterResolver implements ParameterResolverInt * * @param array $proxyChain * - * @template TKey as non-empty-string Proxy address - * @template TValue as non-empty-string Trusted header + * @template TKey as non-empty-string Proxy address; e.g., 127.0.0.1 + * @template TValue as non-empty-string Trusted header; e.g., X-Forwarded-For, X-Real-IP, etc. */ public function __construct(private array $proxyChain = []) { @@ -63,13 +61,6 @@ public function resolveParameter(ReflectionParameter $parameter, mixed $context) ); } - $proxyChain = $this->proxyChain; - /** @var list> $attributes */ - $attributes = $parameter->getAttributes(ProxyChain::class); - if (isset($attributes[0])) { - $proxyChain = $attributes[0]->newInstance()->value; - } - - yield ServerRequest::from($context)->getClientRemoteAddress($proxyChain); + yield ServerRequest::from($context)->getClientRemoteAddress($this->proxyChain); } } diff --git a/src/ParameterResolver/ObjectInjectionParameterResolver.php b/src/ParameterResolver/PresetObjectParameterResolver.php similarity index 81% rename from src/ParameterResolver/ObjectInjectionParameterResolver.php rename to src/ParameterResolver/PresetObjectParameterResolver.php index dd3087e9..91e16fd7 100644 --- a/src/ParameterResolver/ObjectInjectionParameterResolver.php +++ b/src/ParameterResolver/PresetObjectParameterResolver.php @@ -20,11 +20,11 @@ use function is_a; /** - * ObjectInjectionParameterResolver + * PresetObjectParameterResolver * * @since 3.0.0 */ -final class ObjectInjectionParameterResolver implements ParameterResolverInterface +final class PresetObjectParameterResolver implements ParameterResolverInterface { /** @@ -47,8 +47,10 @@ public function resolveParameter(ReflectionParameter $parameter, mixed $context) return; } - if (is_a($this->object, $type->getName())) { - yield $this->object; + if (! is_a($this->object, $type->getName())) { + return; } + + yield $this->object; } } diff --git a/src/ParameterResolver/RequestEntityParameterResolver.php b/src/ParameterResolver/RequestEntityParameterResolver.php new file mode 100644 index 00000000..ed844278 --- /dev/null +++ b/src/ParameterResolver/RequestEntityParameterResolver.php @@ -0,0 +1,132 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ParameterResolver; + +use Doctrine\Persistence\ManagerRegistry as EntityManagerRegistryInterface; +use Generator; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionAttribute; +use ReflectionNamedType; +use ReflectionParameter; +use Sunrise\Http\Router\Annotation\RequestEntity; +use Sunrise\Http\Router\Exception\EntityNotFoundException; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ParameterResolutioner; + +use function count; +use function current; +use function sprintf; + +/** + * RequestEntityParameterResolver + * + * @since 3.0.0 + */ +final class RequestEntityParameterResolver implements ParameterResolverInterface +{ + + /** + * Constructor of the class + * + * @param EntityManagerRegistryInterface $entityManagerRegistry + * @param non-empty-string|null $defaultEntityManagerName + */ + public function __construct( + private EntityManagerRegistryInterface $entityManagerRegistry, + private string|null $defaultEntityManagerName = null, + ) { + } + + /** + * @inheritDoc + * + * @throws EntityNotFoundException If an entity wasn't found. + * @throws LogicException If the resolver is used incorrectly. + */ + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator + { + /** @var list> $attributes */ + $attributes = $parameter->getAttributes(RequestEntity::class); + if ($attributes === []) { + return; + } + + $type = $parameter->getType(); + + if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { + throw new LogicException(sprintf( + 'To use the #[RequestEntity] attribute, the parameter {%s} must be typed with an entity.', + ParameterResolutioner::stringifyParameter($parameter), + )); + } + + if (! $context instanceof ServerRequestInterface) { + throw new LogicException( + 'At this level of the application, any operations with the request are not possible.' + ); + } + + $requestEntity = $attributes[0]->newInstance(); + + $entityManagerName = $requestEntity->em ?? $this->defaultEntityManagerName; + $entityManager = $this->entityManagerRegistry->getManager($entityManagerName); + + $entityIdentificationFieldName = $requestEntity->findBy; + if ($entityIdentificationFieldName === null) { + $entityMetadata = $entityManager->getClassMetadata($type->getName()); + $entityIdentificationFieldNames = $entityMetadata->getIdentifier(); + if (empty($entityIdentificationFieldNames) || + count($entityIdentificationFieldNames) > 1) { + throw new LogicException(sprintf( + 'To use the #[RequestEntity] attribute with the parameter {%s}, ' . + 'it is necessary to explicitly set the "findBy" parameter within it, ' . + 'as the entity {%s} either has a composite identifier or does not have one at all.', + ParameterResolutioner::stringifyParameter($parameter), + $type->getName(), + )); + } + + $entityIdentificationFieldName = current($entityIdentificationFieldNames); + } + + $requestParameterName = $requestEntity->valueKey ?? $entityIdentificationFieldName; + $entityIdentificationFieldValue = $context->getAttribute($requestParameterName); + if ($entityIdentificationFieldValue === null) { + throw new LogicException(sprintf( + 'To use the #[RequestEntity] attribute with the parameter {%s}, ' . + 'it might be necessary to explicitly set the "valueKey" parameter within it, ' . + 'as the attribute with the name "%s" was not found in the current request.', + ParameterResolutioner::stringifyParameter($parameter), + $requestParameterName, + )); + } + + $entity = $entityManager->getRepository($type->getName())->findOneBy([ + $entityIdentificationFieldName => $entityIdentificationFieldValue, + ...$requestEntity->criteria, + ]); + + if (isset($entity)) { + yield $entity; + return; + } + + if ($parameter->allowsNull()) { + yield null; + return; + } + + throw new EntityNotFoundException(); + } +} diff --git a/src/ParameterResolver/RequestHeaderParameterResolver.php b/src/ParameterResolver/RequestHeaderParameterResolver.php new file mode 100644 index 00000000..014c4f31 --- /dev/null +++ b/src/ParameterResolver/RequestHeaderParameterResolver.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ParameterResolver; + +use Generator; +use ReflectionAttribute; +use ReflectionParameter; +use Sunrise\Http\Router\Annotation\RequestHeader; + +final class RequestHeaderParameterResolver implements ParameterResolverInterface +{ + + /** + * @inheritDoc + */ + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator + { + /** @var list> $attributes */ + $attributes = $parameter->getAttributes(RequestHeader::class); + if ($attributes === []) { + return; + } + + // TODO: Implement the method... + } +} diff --git a/src/ParameterResolver/RequestPathVariableParameterResolver.php b/src/ParameterResolver/RequestPathVariableParameterResolver.php new file mode 100644 index 00000000..4db162e1 --- /dev/null +++ b/src/ParameterResolver/RequestPathVariableParameterResolver.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ParameterResolver; + +use Generator; +use ReflectionAttribute; +use ReflectionParameter; +use Sunrise\Http\Router\Annotation\RequestPathVariable; + +final class RequestPathVariableParameterResolver implements ParameterResolverInterface +{ + + /** + * @inheritDoc + */ + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator + { + /** @var list> $attributes */ + $attributes = $parameter->getAttributes(RequestPathVariable::class); + if ($attributes === []) { + return; + } + + // TODO: Implement the method... + } +} diff --git a/src/RequestHandler/CallbackRequestHandler.php b/src/RequestHandler/CallbackRequestHandler.php index 572c3458..3b52a405 100644 --- a/src/RequestHandler/CallbackRequestHandler.php +++ b/src/RequestHandler/CallbackRequestHandler.php @@ -19,7 +19,7 @@ use ReflectionFunction; use ReflectionMethod; use Sunrise\Http\Router\ParameterResolutionerInterface; -use Sunrise\Http\Router\ParameterResolver\ObjectInjectionParameterResolver; +use Sunrise\Http\Router\ParameterResolver\PresetObjectParameterResolver; use Sunrise\Http\Router\ResponseResolutionerInterface; use function Sunrise\Http\Router\reflect_callback; @@ -63,7 +63,7 @@ final class CallbackRequestHandler implements RequestHandlerInterface public function __construct( callable $callback, ParameterResolutionerInterface $parameterResolutioner, - ResponseResolutionerInterface $responseResolutioner + ResponseResolutionerInterface $responseResolutioner, ) { $this->callback = $callback; $this->parameterResolutioner = $parameterResolutioner; @@ -90,7 +90,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $arguments = $this->parameterResolutioner ->withContext($request) ->withPriorityResolver( - new ObjectInjectionParameterResolver($request), + new PresetObjectParameterResolver($request), ) ->resolveParameters(...$source->getParameters()); diff --git a/src/RequestHandler/QueueableRequestHandler.php b/src/RequestHandler/QueueableRequestHandler.php index 476dde45..83079a32 100644 --- a/src/RequestHandler/QueueableRequestHandler.php +++ b/src/RequestHandler/QueueableRequestHandler.php @@ -21,47 +21,22 @@ /** * QueueableRequestHandler + * + * @extends SplQueue */ -final class QueueableRequestHandler implements RequestHandlerInterface +final class QueueableRequestHandler extends SplQueue implements RequestHandlerInterface { - /** - * The request handler's middleware queue - * - * @var SplQueue - */ - private SplQueue $middlewareQueue; - - /** - * The request handler's request handler - * - * @var RequestHandlerInterface - */ - private RequestHandlerInterface $requestHandler; - /** * Constructor of the class * * @param RequestHandlerInterface $requestHandler - */ - public function __construct(RequestHandlerInterface $requestHandler) - { - /** @var SplQueue */ - $this->middlewareQueue = new SplQueue(); - $this->requestHandler = $requestHandler; - } - - /** - * Adds the given middleware(s) to the request handler's middleware queue - * * @param MiddlewareInterface ...$middlewares - * - * @return void */ - public function add(MiddlewareInterface ...$middlewares): void + public function __construct(private RequestHandlerInterface $requestHandler, MiddlewareInterface ...$middlewares) { foreach ($middlewares as $middleware) { - $this->middlewareQueue->enqueue($middleware); + $this->enqueue($middleware); } } @@ -70,8 +45,8 @@ public function add(MiddlewareInterface ...$middlewares): void */ public function handle(ServerRequestInterface $request): ResponseInterface { - if (!$this->middlewareQueue->isEmpty()) { - return $this->middlewareQueue->dequeue()->process($request, $this); + if (! $this->isEmpty()) { + return ($clone = clone $this)->dequeue()->process($request, $clone); } return $this->requestHandler->handle($request); diff --git a/src/ResponseResolutioner.php b/src/ResponseResolutioner.php index b3d81403..6313adb0 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolutioner.php @@ -66,25 +66,26 @@ public function resolveResponse( foreach ($this->resolvers as $resolver) { $result = $resolver->resolveResponse($response, $request, $source); if ($result instanceof ResponseInterface) { - return $this->handleResponse($result, $source); + return $this->supplementResponse($result, $source); } } throw new LogicException(sprintf( - 'Unable to resolve the response {%s} to PSR-7 response', - self::stringifyResponse($response, $source), + 'Unable to resolve the response {%s->%s} to PSR-7 response', + self::stringifySource($source), + get_debug_type($response), )); } /** - * Handles the given response + * Supplements the given response * * @param ResponseInterface $response * @param ReflectionFunction|ReflectionMethod $source * * @return ResponseInterface */ - private function handleResponse( + private function supplementResponse( ResponseInterface $response, ReflectionFunction|ReflectionMethod $source, ) : ResponseInterface { @@ -106,28 +107,18 @@ private function handleResponse( } /** - * Stringifies the given raw response + * Stringifies the given source of a response * - * @param mixed $response * @param ReflectionFunction|ReflectionMethod $source * * @return non-empty-string */ - public static function stringifyResponse(mixed $response, ReflectionFunction|ReflectionMethod $source): string + public static function stringifySource(ReflectionFunction|ReflectionMethod $source): string { if ($source instanceof ReflectionMethod) { - return sprintf( - '%s::%s():$%s', - $source->getDeclaringClass()->getName(), - $source->getName(), - get_debug_type($response), - ); + return sprintf('%s::%s()', $source->getDeclaringClass()->getName(), $source->getName()); } - return sprintf( - '%s():$%s', - $source->getName(), - get_debug_type($response), - ); + return sprintf('%s()', $source->getName()); } } diff --git a/src/ResponseResolver/EmptyResponseResolver.php b/src/ResponseResolver/EmptyResponseResolver.php index cd193807..595c8e60 100644 --- a/src/ResponseResolver/EmptyResponseResolver.php +++ b/src/ResponseResolver/EmptyResponseResolver.php @@ -13,6 +13,7 @@ namespace Sunrise\Http\Router\ResponseResolver; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -45,7 +46,7 @@ public function resolveResponse( ReflectionFunction|ReflectionMethod $source, ) : ?ResponseInterface { if ($response === null) { - return $this->responseFactory->createResponse(204); + return $this->responseFactory->createResponse(StatusCodeInterface::STATUS_NO_CONTENT); } return null; diff --git a/src/ResponseResolver/ExceptionResponseResolver.php b/src/ResponseResolver/ExceptionResponseResolver.php new file mode 100644 index 00000000..bead398f --- /dev/null +++ b/src/ResponseResolver/ExceptionResponseResolver.php @@ -0,0 +1,96 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ResponseResolver; + +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionFunction; +use ReflectionMethod; +use Sunrise\Http\Router\Entity\MediaType; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ServerRequest; +use Throwable; +use Whoops\Run as Whoops; + +use function class_exists; + +/** + * ExceptionResponseResolver + * + * @since 3.0.0 + * + * @link https://github.com/filp/whoops + */ +final class ExceptionResponseResolver implements ResponseResolverInterface +{ + + /** + * Constructor of the class + * + * @param ResponseFactoryInterface $responseFactory + * @param int<100, 599> $defaultResponseStatusCode + * + * @throws LogicException If the whoops package isn't installed. + */ + public function __construct( + private ResponseFactoryInterface $responseFactory, + private int $defaultResponseStatusCode = 500, + ) { + if (!class_exists(Whoops::class)) { + throw new LogicException( + 'The whoops package is required, run the `composer require filp/whoops` command to resolve it.' + ); + } + } + + /** + * @inheritDoc + */ + public function resolveResponse( + mixed $response, + ServerRequestInterface $request, + ReflectionMethod|ReflectionFunction $source, + ): ?ResponseInterface { + if (! $response instanceof Throwable) { + return null; + } + + $whoops = new Whoops(); + $whoops->allowQuit(false); + $whoops->writeToOutput(false); + + $clientPreferredMediaType = ServerRequest::from($request) + ->getClientPreferredMediaType( + MediaType::html(), + MediaType::json(), + MediaType::xml(), + ); + + if ($clientPreferredMediaType->equals(MediaType::html())) { + $whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler()); + } elseif ($clientPreferredMediaType->equals(MediaType::json())) { + $whoops->pushHandler(new \Whoops\Handler\JsonResponseHandler()); + } elseif ($clientPreferredMediaType->equals(MediaType::xml())) { + $whoops->pushHandler(new \Whoops\Handler\XmlResponseHandler()); + } + + $result = $this->responseFactory->createResponse($this->defaultResponseStatusCode) + ->withHeader('Content-Type', $clientPreferredMediaType->__toString()); + + $result->getBody()->write($whoops->handleException($response)); + + return $result; + } +} diff --git a/src/ResponseResolver/JsonResponseBodyResponseResolver.php b/src/ResponseResolver/JsonResponseBodyResponseResolver.php new file mode 100644 index 00000000..a0144b91 --- /dev/null +++ b/src/ResponseResolver/JsonResponseBodyResponseResolver.php @@ -0,0 +1,106 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ResponseResolver; + +use Fig\Http\Message\StatusCodeInterface; +use JsonException; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionAttribute; +use ReflectionFunction; +use ReflectionMethod; +use Sunrise\Http\Router\Annotation\JsonResponseBody; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ResponseResolutioner; + +use function extension_loaded; +use function json_encode; +use function sprintf; + +use const JSON_PRESERVE_ZERO_FRACTION; +use const JSON_THROW_ON_ERROR; +use const JSON_UNESCAPED_SLASHES; +use const JSON_UNESCAPED_UNICODE; + +/** + * JsonResponseBodyResponseResolver + * + * @since 3.0.0 + * + * @link https://www.php.net/manual/en/book.json.php + */ +final class JsonResponseBodyResponseResolver implements ResponseResolverInterface +{ + public const DEFAULT_JSON_ENCODING_FLAGS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION; + public const DEFAULT_JSON_ENCODING_DEPTH = 512; + + /** + * Constructor of the class + * + * @param ResponseFactoryInterface $responseFactory + * @param int|null $jsonEncodingFlags + * @param int|null $jsonEncodingDepth + */ + public function __construct( + private ResponseFactoryInterface $responseFactory, + private ?int $jsonEncodingFlags = null, + private ?int $jsonEncodingDepth = null, + ) { + if (!extension_loaded('json')) { + throw new LogicException( + 'The JSON extension is required, run the `pecl install json` command to resolve it.' + ); + } + } + + /** + * @inheritDoc + * + * @throws LogicException If the resolver is used incorrectly. + */ + public function resolveResponse( + mixed $response, + ServerRequestInterface $request, + ReflectionFunction|ReflectionMethod $source, + ) : ?ResponseInterface { + /** @var list> $attributes */ + $attributes = $source->getAttributes(JsonResponseBody::class); + if ($attributes === []) { + return null; + } + + $jsonResponseBody = $attributes[0]->newInstance(); + + $jsonEncodingFlags = $jsonResponseBody->jsonEncodingFlags ?? $this->jsonEncodingFlags ?? self::DEFAULT_JSON_ENCODING_FLAGS; + $jsonEncodingDepth = $jsonResponseBody->jsonEncodingDepth ?? $this->jsonEncodingDepth ?? self::DEFAULT_JSON_ENCODING_DEPTH; + + try { + $payload = json_encode($response, $jsonEncodingFlags | JSON_THROW_ON_ERROR, $jsonEncodingDepth); + } catch (JsonException $e) { + throw new LogicException(sprintf( + 'Unable to encode a response from the source {%s} due to: %s', + ResponseResolutioner::stringifySource($source), + $e->getMessage(), + ), 0, $e); + } + + $result = $this->responseFactory->createResponse(StatusCodeInterface::STATUS_OK) + ->withHeader('Content-Type', 'application/json; charset=UTF-8'); + + $result->getBody()->write($payload); + + return $result; + } +} diff --git a/src/ResponseResolver/ResponseBodyResponseResolver.php b/src/ResponseResolver/ResponseBodyResponseResolver.php deleted file mode 100644 index fd6ad771..00000000 --- a/src/ResponseResolver/ResponseBodyResponseResolver.php +++ /dev/null @@ -1,98 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\ResponseResolver; - -use Psr\Http\Message\ResponseFactoryInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use ReflectionFunction; -use ReflectionMethod; -use Sunrise\Http\Router\Annotation\ResponseBody; -use Sunrise\Http\Router\Entity\MediaType; -use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ResponseResolutioner; -use Sunrise\Http\Router\RouteInterface; -use Sunrise\Http\Router\ServerRequest; -use Symfony\Component\Serializer\SerializerInterface; - -use function in_array; -use function reset; -use function sprintf; - -/** - * ResponseBodyResponseResolver - * - * @since 3.0.0 - */ -final class ResponseBodyResponseResolver implements ResponseResolverInterface -{ - - /** - * Constructor of the class - * - * @param ResponseFactoryInterface $responseFactory - * @param SerializerInterface $serializer - * @param non-empty-string $defaultFormat - * @param array $formats - * @param array $serializationContext - */ - public function __construct( - private ResponseFactoryInterface $responseFactory, - private SerializerInterface $serializer, - private string $defaultFormat = 'json', - private array $formats = self::MEDIA_FORMATS, - private array $serializationContext = [], - ) { - } - - /** - * @inheritDoc - * - * @throws LogicException If the resolver is used incorrectly. - */ - public function resolveResponse( - mixed $response, - ServerRequestInterface $request, - ReflectionFunction|ReflectionMethod $source, - ) : ?ResponseInterface { - if ($source->getAttributes(ResponseBody::class) === []) { - return null; - } - - $format = $this->defaultFormat; - - /** @var RouteInterface|null $route */ - $route = $request->getAttribute('@route'); - if ($route instanceof RouteInterface) { - $mediaRange = ServerRequest::from($request) - ->getClientPreferredMediaType(...$route->getProducesMediaTypes()) - ?->getMediaRange(); - - if (isset($mediaRange) && isset($this->formats[$mediaRange])) { - $format = $this->formats[$mediaRange]; - } - } - - $payload = $this->serializer->serialize($response, $format, $this->serializationContext); - - $contentType = sprintf('%s; charset=UTF-8', $mediaType->getMediaRange()); - - $result = $this->responseFactory->createResponse(200) - ->withHeader('Content-Type', $contentType); - - $result->getBody()->write($payload); - - return $result; - } -} diff --git a/src/ResponseResolver/StreamResponseResolver.php b/src/ResponseResolver/StreamResponseResolver.php index b06d7a2a..a014f6b3 100644 --- a/src/ResponseResolver/StreamResponseResolver.php +++ b/src/ResponseResolver/StreamResponseResolver.php @@ -13,6 +13,7 @@ namespace Sunrise\Http\Router\ResponseResolver; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -46,7 +47,7 @@ public function resolveResponse( ReflectionFunction|ReflectionMethod $source, ) : ?ResponseInterface { if ($response instanceof StreamInterface) { - return $this->responseFactory->createResponse(200) + return $this->responseFactory->createResponse(StatusCodeInterface::STATUS_OK) ->withBody($response); } diff --git a/src/ResponseResolver/UriResponseResolver.php b/src/ResponseResolver/UriResponseResolver.php index b21ad271..9f553445 100644 --- a/src/ResponseResolver/UriResponseResolver.php +++ b/src/ResponseResolver/UriResponseResolver.php @@ -13,6 +13,7 @@ namespace Sunrise\Http\Router\ResponseResolver; +use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -46,7 +47,7 @@ public function resolveResponse( ReflectionFunction|ReflectionMethod $source, ) : ?ResponseInterface { if ($response instanceof UriInterface) { - return $this->responseFactory->createResponse(302) + return $this->responseFactory->createResponse(StatusCodeInterface::STATUS_FOUND) ->withHeader('Location', $response->__toString()); } diff --git a/src/Route.php b/src/Route.php index 4cf4dcb3..434be829 100644 --- a/src/Route.php +++ b/src/Route.php @@ -532,9 +532,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface return $this->requestHandler->handle($request); } - $handler = new QueueableRequestHandler($this->requestHandler); - $handler->add(...$this->middlewares); - - return $handler->handle($request); + return (new QueueableRequestHandler($this->requestHandler, ...$this->middlewares))->handle($request); } } diff --git a/src/Router.php b/src/Router.php index 4d3919e5..318fe458 100644 --- a/src/Router.php +++ b/src/Router.php @@ -243,6 +243,7 @@ public function generateUri(string $name, array $attributes = [], bool $strict = */ public function match(ServerRequestInterface $request): RouteInterface { + $request = ServerRequest::from($request); $requestUri = $request->getUri(); $requestHost = $requestUri->getHost(); $requestPath = $requestUri->getPath(); @@ -251,7 +252,6 @@ public function match(ServerRequestInterface $request): RouteInterface $host = $this->hosts->resolve($requestHost); $routes = $this->routes->allOnHost($host); - $request = ServerRequest::from($request); foreach ($routes as $route) { // https://github.com/sunrise-php/http-router/issues/50 @@ -270,9 +270,8 @@ public function match(ServerRequestInterface $request): RouteInterface continue; } - $routeConsumes = $route->getConsumesMediaTypes(); - if (!empty($routeConsumes) && !$request->clientProducesMediaType($routeConsumes)) { - throw new ClientNotProducedMediaTypeException($routeConsumes); + if (!$request->clientProducesMediaType(...$route->getConsumesMediaTypes())) { + throw new ClientNotProducedMediaTypeException($route->getConsumesMediaTypes()); } /** @var array $attributes */ @@ -319,10 +318,7 @@ function (ServerRequestInterface $request): ResponseInterface { return $routing->handle($request); } - $handler = new QueueableRequestHandler($routing); - $handler->add(...$this->middlewares); - - return $handler->handle($request); + return (new QueueableRequestHandler($routing, ...$this->middlewares))->handle($request); } /** @@ -342,9 +338,6 @@ public function handle(ServerRequestInterface $request): ResponseInterface return $this->matchedRoute->handle($request); } - $handler = new QueueableRequestHandler($this->matchedRoute); - $handler->add(...$this->middlewares); - - return $handler->handle($request); + return (new QueueableRequestHandler($this->matchedRoute, ...$this->middlewares))->handle($request); } } diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 14c2e001..e4b81617 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -13,7 +13,6 @@ namespace Sunrise\Http\Router; -use Fig\Http\Message\RequestMethodInterface; use Generator; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; @@ -22,16 +21,11 @@ use Sunrise\Http\Router\Entity\MediaType; use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; use Sunrise\Http\Router\Exception\InvalidArgumentException; +use Sunrise\Http\Router\Exception\LogicException; -use Throwable; -use function current; -use function explode; -use function key; use function preg_split; +use function reset; use function sprintf; -use function strncmp; -use function strpos; -use function strtolower; use function usort; /** @@ -39,7 +33,7 @@ * * @since 3.0.0 */ -final class ServerRequest implements ServerRequestInterface, RequestMethodInterface +final class ServerRequest implements ServerRequestInterface { /** @@ -67,6 +61,16 @@ public static function from(ServerRequestInterface $request): self return new self($request); } + /** + * Gets the original request + * + * @return ServerRequestInterface + */ + public function getOriginalRequest(): ServerRequestInterface + { + return $this->request; + } + /** * Gets the client's remote address * @@ -75,46 +79,41 @@ public static function from(ServerRequestInterface $request): self * @return ClientRemoteAddress * * @template TKey as non-empty-string Proxy address; e.g., 127.0.0.1 - * @template TValue as non-empty-string Trusted header; e.g., X-Forwarded-For + * @template TValue as non-empty-string Trusted header; e.g., X-Forwarded-For, X-Real-IP, etc. * + * @link https://www.rfc-editor.org/rfc/rfc3875#section-4.1.8 * @link https://www.rfc-editor.org/rfc/rfc7239.html#section-5.2 */ public function getClientRemoteAddress(array $proxyChain = []): ClientRemoteAddress { $serverParams = $this->request->getServerParams(); - /** @var non-empty-string $clientAddress */ - $clientAddress = $serverParams['REMOTE_ADDR'] ?? '::1'; - /** @var list $proxyAddresses */ - $proxyAddresses = []; + $clientRemoteAddress = $serverParams['REMOTE_ADDR'] ?? '::1'; + $clientRemoteAddressChain = [$clientRemoteAddress]; - while (isset($proxyChain[$clientAddress])) { - $trustedHeader = $proxyChain[$clientAddress]; - unset($proxyChain[$clientAddress]); + while (isset($proxyChain[$clientRemoteAddressChain[0]])) { + $trustedHeader = $proxyChain[$clientRemoteAddressChain[0]]; + unset($proxyChain[$clientRemoteAddressChain[0]]); $header = $this->request->getHeaderLine($trustedHeader); - if ($header === '') { + $addresses = preg_split('/\s*,\s*/', $header, flags: \PREG_SPLIT_NO_EMPTY); + if ($addresses === []) { break; } - /** @var list $addresses */ - $addresses = preg_split('/\s*,\s*/', $header, -1, PREG_SPLIT_NO_EMPTY); - if (empty($addresses)) { - break; - } + $clientRemoteAddressChain = [...$addresses, ...$clientRemoteAddressChain]; + } - $clientAddress = $addresses[0]; - unset($addresses[0]); + $address = $clientRemoteAddressChain[0]; + unset($clientRemoteAddressChain[0]); - foreach ($addresses as $address) { - $proxyAddresses[] = $address; - } - } + /** @var list $proxies */ + $proxies = [...$clientRemoteAddressChain]; - return new ClientRemoteAddress($clientAddress, $proxyAddresses); + return new ClientRemoteAddress($address, $proxies); } /** - * Gets the media type that the client produced + * Gets a media type that the client produced * * @return MediaType|null */ @@ -130,9 +129,9 @@ public function getClientProducedMediaType(): ?MediaType } /** - * Gets the media types that the client consumes + * Gets media types that the client consumes * - * @return Generator, MediaType> + * @return Generator */ public function getClientConsumesMediaTypes(): Generator { @@ -150,23 +149,27 @@ public function getClientConsumesMediaTypes(): Generator * * @param MediaType ...$serverProducesMediaTypes * - * @return MediaType|null + * @return MediaType + * + * @throws LogicException If the list of produces media types is empty. */ - public function getClientPreferredMediaType(MediaType ...$serverProducesMediaTypes): ?MediaType + public function getClientPreferredMediaType(MediaType ...$serverProducesMediaTypes): MediaType { if ($serverProducesMediaTypes === []) { - return null; + throw new LogicException( + 'The list of media types produced by the server is empty, ' . + 'making it impossible to determine the preferred media type for the client.' + ); } /** @var list $clientConsumesMediaTypes */ $clientConsumesMediaTypes = [...$this->getClientConsumesMediaTypes()]; - if ($clientConsumesMediaTypes === []) { - return current($serverProducesMediaTypes); + return reset($serverProducesMediaTypes); } - usort($clientConsumesMediaTypes, static fn (MediaType $a, MediaType $b): int => ( - $b->getQualityFactor() <=> $a->getQualityFactor() + usort($clientConsumesMediaTypes, static fn(MediaType $a, MediaType $b): int => ( + ($b->getParameters()['q'] ?? 1.) <=> ($a->getParameters()['q'] ?? 1.) )); foreach ($clientConsumesMediaTypes as $clientConsumesMediaType) { @@ -177,29 +180,29 @@ public function getClientPreferredMediaType(MediaType ...$serverProducesMediaTyp } } - return current($serverProducesMediaTypes); + return reset($serverProducesMediaTypes); } /** * Checks if the client produces one of the given media types * - * @param MediaType ...$consumes + * @param MediaType ...$serverConsumesMediaTypes * * @return bool */ - public function clientProducesMediaType(array $consumes): bool + public function clientProducesMediaType(MediaType ...$serverConsumesMediaTypes): bool { - if ($consumes === []) { + if ($serverConsumesMediaTypes === []) { return true; } - $produced = $this->getClientProducedMediaType(); - if ($produced === '') { + $clientProducedMediaType = $this->getClientProducedMediaType(); + if ($clientProducedMediaType === null) { return false; } - foreach ($consumes as $consumed) { - if (media_types_compare($consumed, $produced)) { + foreach ($serverConsumesMediaTypes as $serverConsumesMediaType) { + if ($clientProducedMediaType->equals($serverConsumesMediaType)) { return true; } } From 0f8015fc33a3ab554622c1bf3902858bc826b591 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sat, 26 Aug 2023 09:43:10 +0200 Subject: [PATCH 081/180] v3 --- composer.json | 14 +- functions/parse_header_with_media_type.php | 2 +- resources/views/error.phtml | 85 +++++++++ src/Annotation/Consumes.php | 5 +- src/Annotation/JsonResponseBody.php | 5 +- ...questPathVariable.php => PathVariable.php} | 11 +- src/Annotation/Produces.php | 6 +- src/Annotation/RequestEntity.php | 40 ---- src/Annotation/RequestHeader.php | 9 + src/Annotation/Route.php | 4 + src/ClassResolver.php | 11 +- .../ErrorDto.php} | 18 +- src/Dto/ViolationDto.php | 62 ++++++ src/Entity/ClientRemoteAddress.php | 63 ------- src/Entity/MediaType.php | 28 ++- src/Event/ErrorEvent.php | 75 ++++++++ .../ClientNotConsumedMediaTypeException.php | 25 --- .../ClientNotProducedMediaTypeException.php | 25 --- .../Http/HttpBadRequestException.php | 6 +- src/Exception/Http/HttpConflictException.php | 6 +- .../Http/HttpExpectationFailedException.php | 42 ----- .../Http/HttpFailedDependencyException.php | 6 +- src/Exception/Http/HttpForbiddenException.php | 6 +- src/Exception/Http/HttpGoneException.php | 6 +- ...p => HttpInternalServerErrorException.php} | 14 +- .../Http/HttpLengthRequiredException.php | 42 ----- src/Exception/Http/HttpLockedException.php | 8 +- .../Http/HttpMethodNotAllowedException.php | 44 +---- .../Http/HttpMisdirectedRequestException.php | 40 ---- .../Http/HttpNotAcceptableException.php | 66 ------- src/Exception/Http/HttpNotFoundException.php | 6 +- .../Http/HttpPayloadTooLargeException.php | 6 +- .../Http/HttpPaymentRequiredException.php | 42 ----- .../Http/HttpPreconditionFailedException.php | 42 ----- ...tpProxyAuthenticationRequiredException.php | 42 ----- .../Http/HttpRangeNotSatisfiableException.php | 42 ----- ...tpRequestHeaderFieldsTooLargeException.php | 42 ----- .../Http/HttpRequestTimeoutException.php | 42 ----- .../Http/HttpServiceUnavailableException.php | 7 +- src/Exception/Http/HttpTooEarlyException.php | 42 ----- .../Http/HttpTooManyRequestsException.php | 6 +- .../Http/HttpUnauthorizedException.php | 6 +- ...ttpUnavailableForLegalReasonsException.php | 6 +- .../Http/HttpUnprocessableEntityException.php | 6 +- .../HttpUnsupportedMediaTypeException.php | 45 ++--- .../Http/HttpUpgradeRequiredException.php | 42 ----- .../Http/HttpUriTooLongException.php | 42 ----- src/Exception/HttpException.php | 155 +++++++++++++-- src/Exception/HttpExceptionInterface.php | 30 ++- .../InvalidRequestPayloadException.php | 25 --- src/Exception/MethodNotAllowedException.php | 23 --- .../MissingRequestParameterException.php | 25 --- src/Exception/PageNotFoundException.php | 25 --- src/Exception/ResolvingReferenceException.php | 23 --- src/Exception/UnhydrableObjectException.php | 23 --- .../UnprocessableEntityException.php | 53 ------ .../UnprocessableRequestBodyException.php | 23 --- .../UnprocessableRequestQueryException.php | 23 --- src/Loader/ConfigLoader.php | 14 +- src/Loader/DescriptorLoader.php | 94 ++++++---- src/Middleware/CallbackMiddleware.php | 10 +- src/Middleware/ErrorHandlingMiddleware.php | 177 ++++++++++++++++++ .../JsonPayloadDecodingMiddleware.php | 32 +--- .../ClientRemoteAddressParameterResolver.php | 66 ------- .../RequestEntityParameterResolver.php | 132 ------------- .../RequestHeaderParameterResolver.php | 37 ---- .../RequestPathVariableParameterResolver.php | 37 ---- .../ParameterResolutioner.php | 12 +- .../ParameterResolutionerInterface.php | 6 +- .../DependencyInjectionParameterResolver.php | 2 +- .../ObjectInjectionParameterResolver.php} | 8 +- .../ParameterResolverInterface.php | 4 +- .../PathVariableParameterResolver.php | 117 ++++++++++++ .../RequestBodyParameterResolver.php | 29 +-- .../RequestHeaderParameterResolver.php | 100 ++++++++++ .../RequestQueryParameterResolver.php | 29 +-- .../RequestRouteParameterResolver.php | 4 +- src/ReferenceResolver.php | 8 +- src/RequestHandler/CallbackRequestHandler.php | 8 +- .../QueueableRequestHandler.php | 6 +- .../ExceptionResponseResolver.php | 96 ---------- .../ResponseResolutioner.php | 8 +- .../ResponseResolutionerInterface.php | 4 +- .../EmptyResponseResolver.php | 5 +- .../JsonResponseBodyResponseResolver.php | 33 +--- .../ResponseResolverInterface.php | 2 +- .../RouteResponseResolver.php | 2 +- .../StreamResponseResolver.php | 5 +- .../ResponseResolver/UriResponseResolver.php | 5 +- src/Route.php | 8 + src/RouteCollector.php | 12 +- src/RouteInterface.php | 10 + src/Router.php | 33 ++-- src/ServerRequest.php | 104 +++------- src/TypeConversion/TypeConversioner.php | 64 +++++++ .../TypeConversionerInterface.php | 46 +++++ .../TypeConverter/BackedEnumTypeConverter.php | 122 ++++++++++++ .../TypeConverter/BoolTypeConverter.php | 65 +++++++ .../TypeConverter/DateTimeTypeConverter.php | 83 ++++++++ .../TypeConverter/FloatTypeConverter.php | 70 +++++++ .../TypeConverter/IntTypeConverter.php | 66 +++++++ .../TypeConverter/StringTypeConverter.php | 46 +++++ .../TypeConverter/TypeConverterInterface.php | 37 ++++ 103 files changed, 1785 insertions(+), 1741 deletions(-) create mode 100644 resources/views/error.phtml rename src/Annotation/{RequestPathVariable.php => PathVariable.php} (70%) delete mode 100644 src/Annotation/RequestEntity.php rename src/{Exception/EntityNotFoundException.php => Dto/ErrorDto.php} (55%) create mode 100644 src/Dto/ViolationDto.php delete mode 100644 src/Entity/ClientRemoteAddress.php create mode 100644 src/Event/ErrorEvent.php delete mode 100644 src/Exception/ClientNotConsumedMediaTypeException.php delete mode 100644 src/Exception/ClientNotProducedMediaTypeException.php delete mode 100644 src/Exception/Http/HttpExpectationFailedException.php rename src/Exception/Http/{HttpPreconditionRequiredException.php => HttpInternalServerErrorException.php} (67%) delete mode 100644 src/Exception/Http/HttpLengthRequiredException.php delete mode 100644 src/Exception/Http/HttpMisdirectedRequestException.php delete mode 100644 src/Exception/Http/HttpNotAcceptableException.php delete mode 100644 src/Exception/Http/HttpPaymentRequiredException.php delete mode 100644 src/Exception/Http/HttpPreconditionFailedException.php delete mode 100644 src/Exception/Http/HttpProxyAuthenticationRequiredException.php delete mode 100644 src/Exception/Http/HttpRangeNotSatisfiableException.php delete mode 100644 src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php delete mode 100644 src/Exception/Http/HttpRequestTimeoutException.php delete mode 100644 src/Exception/Http/HttpTooEarlyException.php delete mode 100644 src/Exception/Http/HttpUpgradeRequiredException.php delete mode 100644 src/Exception/Http/HttpUriTooLongException.php delete mode 100644 src/Exception/InvalidRequestPayloadException.php delete mode 100644 src/Exception/MethodNotAllowedException.php delete mode 100644 src/Exception/MissingRequestParameterException.php delete mode 100644 src/Exception/PageNotFoundException.php delete mode 100644 src/Exception/ResolvingReferenceException.php delete mode 100644 src/Exception/UnhydrableObjectException.php delete mode 100644 src/Exception/UnprocessableEntityException.php delete mode 100644 src/Exception/UnprocessableRequestBodyException.php delete mode 100644 src/Exception/UnprocessableRequestQueryException.php create mode 100644 src/Middleware/ErrorHandlingMiddleware.php delete mode 100644 src/ParameterResolver/ClientRemoteAddressParameterResolver.php delete mode 100644 src/ParameterResolver/RequestEntityParameterResolver.php delete mode 100644 src/ParameterResolver/RequestHeaderParameterResolver.php delete mode 100644 src/ParameterResolver/RequestPathVariableParameterResolver.php rename src/{ => ParameterResolving}/ParameterResolutioner.php (89%) rename src/{ => ParameterResolving}/ParameterResolutionerInterface.php (88%) rename src/{ => ParameterResolving}/ParameterResolver/DependencyInjectionParameterResolver.php (94%) rename src/{ParameterResolver/PresetObjectParameterResolver.php => ParameterResolving/ParameterResolver/ObjectInjectionParameterResolver.php} (79%) rename src/{ => ParameterResolving}/ParameterResolver/ParameterResolverInterface.php (87%) create mode 100644 src/ParameterResolving/ParameterResolver/PathVariableParameterResolver.php rename src/{ => ParameterResolving}/ParameterResolver/RequestBodyParameterResolver.php (73%) create mode 100644 src/ParameterResolving/ParameterResolver/RequestHeaderParameterResolver.php rename src/{ => ParameterResolving}/ParameterResolver/RequestQueryParameterResolver.php (72%) rename src/{ => ParameterResolving}/ParameterResolver/RequestRouteParameterResolver.php (93%) delete mode 100644 src/ResponseResolver/ExceptionResponseResolver.php rename src/{ => ResponseResolving}/ResponseResolutioner.php (93%) rename src/{ => ResponseResolving}/ResponseResolutionerInterface.php (90%) rename src/{ => ResponseResolving}/ResponseResolver/EmptyResponseResolver.php (85%) rename src/{ => ResponseResolving}/ResponseResolver/JsonResponseBodyResponseResolver.php (60%) rename src/{ => ResponseResolving}/ResponseResolver/ResponseResolverInterface.php (93%) rename src/{ => ResponseResolving}/ResponseResolver/RouteResponseResolver.php (93%) rename src/{ => ResponseResolving}/ResponseResolver/StreamResponseResolver.php (87%) rename src/{ => ResponseResolving}/ResponseResolver/UriResponseResolver.php (87%) create mode 100644 src/TypeConversion/TypeConversioner.php create mode 100644 src/TypeConversion/TypeConversionerInterface.php create mode 100644 src/TypeConversion/TypeConverter/BackedEnumTypeConverter.php create mode 100644 src/TypeConversion/TypeConverter/BoolTypeConverter.php create mode 100644 src/TypeConversion/TypeConverter/DateTimeTypeConverter.php create mode 100644 src/TypeConversion/TypeConverter/FloatTypeConverter.php create mode 100644 src/TypeConversion/TypeConverter/IntTypeConverter.php create mode 100644 src/TypeConversion/TypeConverter/StringTypeConverter.php create mode 100644 src/TypeConversion/TypeConverter/TypeConverterInterface.php diff --git a/composer.json b/composer.json index 2e51a6c9..b03dee91 100644 --- a/composer.json +++ b/composer.json @@ -34,20 +34,18 @@ "psr/http-message": "^1.0 || ^2.0", "psr/http-server-handler": "^1.0", "psr/http-server-middleware": "^1.0", - "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", - "sunrise/hydrator": "^3.0", - "symfony/serializer": ">=6.0", - "symfony/validator": ">=6.0" + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "phpunit/phpunit": "^9.6", - "vimeo/psalm": "^5.14", + "vimeo/psalm": "^5.15", "sunrise/coding-standard": "^1.0", "sunrise/http-message": "^3.0", + "sunrise/hydrator": "^3.0", "symfony/console": "^6.0", - "symfony/http-foundation": "^6.3", - "doctrine/orm": "^2.16", - "filp/whoops": "^2.15" + "symfony/validator": "^6.0", + "monolog/monolog": "^3.4" }, "autoload": { "files": [ diff --git a/functions/parse_header_with_media_type.php b/functions/parse_header_with_media_type.php index cd87f751..c53587fd 100644 --- a/functions/parse_header_with_media_type.php +++ b/functions/parse_header_with_media_type.php @@ -25,7 +25,7 @@ * * @param string $header * - * @return Generator + * @return Generator * * @throws InvalidArgumentException If one of the media types is invalid. * diff --git a/resources/views/error.phtml b/resources/views/error.phtml new file mode 100644 index 00000000..9e604094 --- /dev/null +++ b/resources/views/error.phtml @@ -0,0 +1,85 @@ + + + + + + + <?= sprintf('%d | %s', $this->getStatusCode(), $this->getReasonPhrase()) ?> + + + +
+

getReasonPhrase() ?>

+

getMessage() ?>

+
+ getViolations() as $violation): ?> +

source ?> message ?>

+ +
+ +
+ + diff --git a/src/Annotation/Consumes.php b/src/Annotation/Consumes.php index 6cd0a05b..c2f4905a 100644 --- a/src/Annotation/Consumes.php +++ b/src/Annotation/Consumes.php @@ -25,9 +25,10 @@ final class Consumes /** * Constructor of the class * - * @param non-empty-string $value + * @param non-empty-string $type + * @param non-empty-string $subtype */ - public function __construct(public string $value) + public function __construct(public string $type, public string $subtype) { } } diff --git a/src/Annotation/JsonResponseBody.php b/src/Annotation/JsonResponseBody.php index ee1c32ed..86dc0857 100644 --- a/src/Annotation/JsonResponseBody.php +++ b/src/Annotation/JsonResponseBody.php @@ -25,10 +25,9 @@ final class JsonResponseBody /** * Constructor of the class * - * @param int|null $jsonEncodingFlags - * @param int|null $jsonEncodingDepth + * @param int $options */ - public function __construct(public ?int $jsonEncodingFlags = null, public ?int $jsonEncodingDepth = null) + public function __construct(public int $options = 0) { } } diff --git a/src/Annotation/RequestPathVariable.php b/src/Annotation/PathVariable.php similarity index 70% rename from src/Annotation/RequestPathVariable.php rename to src/Annotation/PathVariable.php index 9938a654..9f180e4a 100644 --- a/src/Annotation/RequestPathVariable.php +++ b/src/Annotation/PathVariable.php @@ -19,6 +19,15 @@ * @since 3.0.0 */ #[Attribute(Attribute::TARGET_PARAMETER)] -final class RequestPathVariable +final class PathVariable { + + /** + * Constructor of the class + * + * @param non-empty-string|null $name + */ + public function __construct(public ?string $name = null) + { + } } diff --git a/src/Annotation/Produces.php b/src/Annotation/Produces.php index 5b467587..a121a4ed 100644 --- a/src/Annotation/Produces.php +++ b/src/Annotation/Produces.php @@ -25,9 +25,11 @@ final class Produces /** * Constructor of the class * - * @param non-empty-string $value + * @param non-empty-string $type + * @param non-empty-string $subtype + * @param array $parameters */ - public function __construct(public string $value) + public function __construct(public string $type, public string $subtype, public array $parameters = []) { } } diff --git a/src/Annotation/RequestEntity.php b/src/Annotation/RequestEntity.php deleted file mode 100644 index 2b6313a9..00000000 --- a/src/Annotation/RequestEntity.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Annotation; - -use Attribute; - -/** - * @since 3.0.0 - */ -#[Attribute(Attribute::TARGET_PARAMETER)] -final class RequestEntity -{ - - /** - * Constructor of the class - * - * @param non-empty-string|null $em - * @param non-empty-string|null $findBy - * @param non-empty-string|null $valueKey - * @param array $criteria An entity's additional search criteria - */ - public function __construct( - public string|null $em = null, - public string|null $findBy = null, - public string|null $valueKey = null, - public array $criteria = [], - ) { - } -} diff --git a/src/Annotation/RequestHeader.php b/src/Annotation/RequestHeader.php index 3703e1e8..1dd0bb43 100644 --- a/src/Annotation/RequestHeader.php +++ b/src/Annotation/RequestHeader.php @@ -21,4 +21,13 @@ #[Attribute(Attribute::TARGET_PARAMETER)] final class RequestHeader { + + /** + * Constructor of the class + * + * @param non-empty-string $name + */ + public function __construct(public string $name) + { + } } diff --git a/src/Annotation/Route.php b/src/Annotation/Route.php index 4bb47742..9bec9b38 100644 --- a/src/Annotation/Route.php +++ b/src/Annotation/Route.php @@ -34,6 +34,8 @@ final class Route implements RequestMethodInterface * The route's consumes media types * * @var list + * + * @internal */ public array $consumes = []; @@ -41,6 +43,8 @@ final class Route implements RequestMethodInterface * The route's produces media types * * @var list + * + * @internal */ public array $produces = []; diff --git a/src/ClassResolver.php b/src/ClassResolver.php index daf676c3..111dcbca 100644 --- a/src/ClassResolver.php +++ b/src/ClassResolver.php @@ -16,10 +16,10 @@ use ReflectionClass; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutionerInterface; use function class_exists; use function sprintf; -use function var_dump; /** * ClassResolver @@ -57,7 +57,8 @@ public function __construct(ParameterResolutionerInterface $parameterResolutione * @inheritDoc * * @throws InvalidArgumentException If the class doesn't exist. - * @throws LogicException If the class cannot be resolved. + * + * @throws LogicException If the class couldn't be resolved. */ public function resolveClass(string $fqn): object { @@ -66,12 +67,12 @@ public function resolveClass(string $fqn): object } if (!class_exists($fqn)) { - throw new InvalidArgumentException(sprintf('The class %s does not exist', $fqn)); + throw new InvalidArgumentException(sprintf('The class %s does not exist.', $fqn)); } $class = new ReflectionClass($fqn); if (!$class->isInstantiable()) { - throw new LogicException(sprintf('The class %s cannot be initialized', $fqn)); + throw new LogicException(sprintf('The class %s cannot be initialized.', $fqn)); } $arguments = []; @@ -83,7 +84,7 @@ public function resolveClass(string $fqn): object } /** @var T $instance */ - $instance = new $fqn(...$arguments); + $instance = $class->newInstance(...$arguments); $this->resolvedClasses[$fqn] = $instance; diff --git a/src/Exception/EntityNotFoundException.php b/src/Dto/ErrorDto.php similarity index 55% rename from src/Exception/EntityNotFoundException.php rename to src/Dto/ErrorDto.php index e5f490b0..33135fa5 100644 --- a/src/Exception/EntityNotFoundException.php +++ b/src/Dto/ErrorDto.php @@ -11,15 +11,21 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\Exception; - -use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; +namespace Sunrise\Http\Router\Dto; /** - * EntityNotFoundException - * * @since 3.0.0 */ -class EntityNotFoundException extends HttpNotFoundException +final class ErrorDto { + + /** + * Constructor of the class + * + * @param non-empty-string $message + * @param list $violations + */ + public function __construct(public string $message, public array $violations = []) + { + } } diff --git a/src/Dto/ViolationDto.php b/src/Dto/ViolationDto.php new file mode 100644 index 00000000..fa4e71c8 --- /dev/null +++ b/src/Dto/ViolationDto.php @@ -0,0 +1,62 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Dto; + +use Sunrise\Hydrator\Exception\InvalidValueException; +use Symfony\Component\Validator\ConstraintViolationInterface; + +/** + * @since 3.0.0 + */ +final class ViolationDto +{ + public function __construct( + public string $message, + public string $source, + public string $code, + ) { + } + + /** + * Creates the violation from the given hydrator violation + * + * @param InvalidValueException $violation + * + * @return self + */ + public static function fromHydratorViolation(InvalidValueException $violation): self + { + return new self( + $violation->getMessage(), + $violation->getPropertyPath(), + $violation->getErrorCode(), + ); + } + + /** + * Creates the violation from the given validator violation + * + * @param ConstraintViolationInterface $violation + * + * @return self + */ + public static function fromValidatorViolation(ConstraintViolationInterface $violation): self + { + return new self( + (string) $violation->getMessage(), + $violation->getPropertyPath(), + $violation->getCode() ?? '00000000-0000-0000-0000-000000000000', + ); + } +} diff --git a/src/Entity/ClientRemoteAddress.php b/src/Entity/ClientRemoteAddress.php deleted file mode 100644 index 691f33af..00000000 --- a/src/Entity/ClientRemoteAddress.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Entity; - -use Stringable; - -/** - * Client Remote Address - * - * @since 3.0.0 - */ -final class ClientRemoteAddress implements Stringable -{ - - /** - * Constructor of the class - * - * @param non-empty-string $address - * @param list $proxies - */ - public function __construct(private string $address, private array $proxies = []) - { - } - - /** - * Gets the client's remote address - * - * @return non-empty-string - */ - public function getAddress(): string - { - return $this->address; - } - - /** - * Gets the list of proxies in front of this address - * - * @return list - */ - public function getProxies(): array - { - return $this->proxies; - } - - /** - * @inheritDoc - */ - public function __toString(): string - { - return $this->address; - } -} diff --git a/src/Entity/MediaType.php b/src/Entity/MediaType.php index b2ce333c..419aac47 100644 --- a/src/Entity/MediaType.php +++ b/src/Entity/MediaType.php @@ -84,6 +84,30 @@ public static function html(array $parameters = []): self return new self('text', 'html', $parameters); } + /** + * Creates the text media type + * + * @param array $parameters + * + * @return self + */ + public static function text(array $parameters = []): self + { + return new self('text', 'plain', $parameters); + } + + /** + * Creates the image media range + * + * @param array $parameters + * + * @return self + */ + public static function image(array $parameters = []): self + { + return new self('image', '*', $parameters); + } + /** * Gets the media range type * @@ -105,7 +129,7 @@ public function getSubtype(): string } /** - * Gets the media range parameters + * Gets the media type parameters * * @return array */ @@ -134,7 +158,7 @@ public function __toString(): string { $result = sprintf('%s/%s', $this->type, $this->subtype); foreach ($this->parameters as $name => $value) { - $result .= sprintf('; %s="%s"', $name, $value); + $result .= sprintf('; %s="%s"', $name, (string) $value); } return $result; diff --git a/src/Event/ErrorEvent.php b/src/Event/ErrorEvent.php new file mode 100644 index 00000000..cbd9a478 --- /dev/null +++ b/src/Event/ErrorEvent.php @@ -0,0 +1,75 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Event; + +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Throwable; + +/** + * ErrorEvent + * + * @since 3.0.0 + */ +final class ErrorEvent +{ + + /** + * Constructor of the class + * + * @param Throwable $error + * @param ServerRequestInterface $request + * @param ResponseInterface $response + */ + public function __construct( + private Throwable $error, + private ServerRequestInterface $request, + private ResponseInterface $response, + ) { + } + + /** + * @return Throwable + */ + public function getError(): Throwable + { + return $this->error; + } + + /** + * @return ServerRequestInterface + */ + public function getRequest(): ServerRequestInterface + { + return $this->request; + } + + /** + * @return ResponseInterface + */ + public function getResponse(): ResponseInterface + { + return $this->response; + } + + /** + * @param ResponseInterface $response + * + * @return void + */ + public function setResponse(ResponseInterface $response): void + { + $this->response = $response; + } +} diff --git a/src/Exception/ClientNotConsumedMediaTypeException.php b/src/Exception/ClientNotConsumedMediaTypeException.php deleted file mode 100644 index ab2f8af9..00000000 --- a/src/Exception/ClientNotConsumedMediaTypeException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -use Sunrise\Http\Router\Exception\Http\HttpNotAcceptableException; - -/** - * ClientNotConsumedMediaTypeException - * - * @since 3.0.0 - */ -class ClientNotConsumedMediaTypeException extends HttpNotAcceptableException -{ -} diff --git a/src/Exception/ClientNotProducedMediaTypeException.php b/src/Exception/ClientNotProducedMediaTypeException.php deleted file mode 100644 index c6c746d2..00000000 --- a/src/Exception/ClientNotProducedMediaTypeException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -use Sunrise\Http\Router\Exception\Http\HttpUnsupportedMediaTypeException; - -/** - * ClientNotProducedMediaTypeException - * - * @since 3.0.0 - */ -class ClientNotProducedMediaTypeException extends HttpUnsupportedMediaTypeException -{ -} diff --git a/src/Exception/Http/HttpBadRequestException.php b/src/Exception/Http/HttpBadRequestException.php index 9dc1dfef..e9e74862 100644 --- a/src/Exception/Http/HttpBadRequestException.php +++ b/src/Exception/Http/HttpBadRequestException.php @@ -29,14 +29,16 @@ class HttpBadRequestException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Bad Request'; + $message ??= 'The request couldn‘t be processed due to malformed syntax or invalid parameters.'; parent::__construct(self::STATUS_BAD_REQUEST, $message, $code, $previous); + + $this->setReasonPhrase('Bad Request'); } } diff --git a/src/Exception/Http/HttpConflictException.php b/src/Exception/Http/HttpConflictException.php index b657b250..885fe754 100644 --- a/src/Exception/Http/HttpConflictException.php +++ b/src/Exception/Http/HttpConflictException.php @@ -29,14 +29,16 @@ class HttpConflictException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Conflict'; + $message ??= 'The request couldn‘t be processed due to conflicts with the resource‘s current state.'; parent::__construct(self::STATUS_CONFLICT, $message, $code, $previous); + + $this->setReasonPhrase('Conflict'); } } diff --git a/src/Exception/Http/HttpExpectationFailedException.php b/src/Exception/Http/HttpExpectationFailedException.php deleted file mode 100644 index 08cab2ca..00000000 --- a/src/Exception/Http/HttpExpectationFailedException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Expectation Failed Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417 - * - * @since 3.0.0 - */ -class HttpExpectationFailedException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Expectation Failed'; - - parent::__construct(self::STATUS_EXPECTATION_FAILED, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpFailedDependencyException.php b/src/Exception/Http/HttpFailedDependencyException.php index e107c724..65f57092 100644 --- a/src/Exception/Http/HttpFailedDependencyException.php +++ b/src/Exception/Http/HttpFailedDependencyException.php @@ -29,14 +29,16 @@ class HttpFailedDependencyException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Failed Dependency'; + $message ??= 'The request couldn‘t be processed due to failures with the resource‘s dependencies.'; parent::__construct(self::STATUS_FAILED_DEPENDENCY, $message, $code, $previous); + + $this->setReasonPhrase('Failed Dependency'); } } diff --git a/src/Exception/Http/HttpForbiddenException.php b/src/Exception/Http/HttpForbiddenException.php index 3103c39a..2b5bc2cd 100644 --- a/src/Exception/Http/HttpForbiddenException.php +++ b/src/Exception/Http/HttpForbiddenException.php @@ -29,14 +29,16 @@ class HttpForbiddenException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Forbidden'; + $message ??= 'The request couldn‘t be processed due to insufficient permissions for the resource.'; parent::__construct(self::STATUS_FORBIDDEN, $message, $code, $previous); + + $this->setReasonPhrase('Forbidden'); } } diff --git a/src/Exception/Http/HttpGoneException.php b/src/Exception/Http/HttpGoneException.php index 91ef1e0d..503799cf 100644 --- a/src/Exception/Http/HttpGoneException.php +++ b/src/Exception/Http/HttpGoneException.php @@ -29,14 +29,16 @@ class HttpGoneException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Gone'; + $message ??= 'The resource is no longer available.'; parent::__construct(self::STATUS_GONE, $message, $code, $previous); + + $this->setReasonPhrase('Gone'); } } diff --git a/src/Exception/Http/HttpPreconditionRequiredException.php b/src/Exception/Http/HttpInternalServerErrorException.php similarity index 67% rename from src/Exception/Http/HttpPreconditionRequiredException.php rename to src/Exception/Http/HttpInternalServerErrorException.php index 026df707..0bc04a19 100644 --- a/src/Exception/Http/HttpPreconditionRequiredException.php +++ b/src/Exception/Http/HttpInternalServerErrorException.php @@ -17,26 +17,28 @@ use Throwable; /** - * HTTP Precondition Required Exception + * HTTP Internal Server Error Exception * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/428 + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500 * * @since 3.0.0 */ -class HttpPreconditionRequiredException extends HttpException +class HttpInternalServerErrorException extends HttpException { /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Precondition Required'; + $message ??= 'The server encountered an unexpected condition that prevented it from fulfilling the request.'; - parent::__construct(self::STATUS_PRECONDITION_REQUIRED, $message, $code, $previous); + parent::__construct(self::STATUS_INTERNAL_SERVER_ERROR, $message, $code, $previous); + + $this->setReasonPhrase('Internal Server Error'); } } diff --git a/src/Exception/Http/HttpLengthRequiredException.php b/src/Exception/Http/HttpLengthRequiredException.php deleted file mode 100644 index 96c826fd..00000000 --- a/src/Exception/Http/HttpLengthRequiredException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Length Required Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/411 - * - * @since 3.0.0 - */ -class HttpLengthRequiredException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Length Required'; - - parent::__construct(self::STATUS_LENGTH_REQUIRED, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpLockedException.php b/src/Exception/Http/HttpLockedException.php index 030170cd..afb842e4 100644 --- a/src/Exception/Http/HttpLockedException.php +++ b/src/Exception/Http/HttpLockedException.php @@ -19,6 +19,8 @@ /** * HTTP Locked Exception * + * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/423 + * * @since 3.0.0 */ class HttpLockedException extends HttpException @@ -27,14 +29,16 @@ class HttpLockedException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Locked'; + $message ??= 'The request couldn‘t be processed due to the resource‘s temporary lock and inaccessibility.'; parent::__construct(self::STATUS_LOCKED, $message, $code, $previous); + + $this->setReasonPhrase('Locked'); } } diff --git a/src/Exception/Http/HttpMethodNotAllowedException.php b/src/Exception/Http/HttpMethodNotAllowedException.php index 13b5d681..a9ba4440 100644 --- a/src/Exception/Http/HttpMethodNotAllowedException.php +++ b/src/Exception/Http/HttpMethodNotAllowedException.php @@ -16,8 +16,6 @@ use Sunrise\Http\Router\Exception\HttpException; use Throwable; -use function join; - /** * HTTP Method Not Allowed Exception * @@ -28,53 +26,31 @@ class HttpMethodNotAllowedException extends HttpException { - /** - * Allowed HTTP methods - * - * @var list - */ - private array $allowedMethods; - /** * Constructor of the class * - * @param list $allowedMethods - * @param string|null $message + * @param list $allowedMethods + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ - public function __construct( - array $allowedMethods, - ?string $message = null, - int $code = 0, - ?Throwable $previous = null - ) { - $message ??= 'Method Not Allowed'; + // phpcs:ignore Generic.Files.LineLength + public function __construct(private array $allowedMethods, ?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'The request couldn‘t be processed using the requested HTTP method for the resource.'; parent::__construct(self::STATUS_METHOD_NOT_ALLOWED, $message, $code, $previous); - $this->allowedMethods = $allowedMethods; + $this->setReasonPhrase('Method Not Allowed'); - $this->addHeaderField('Allow', $this->getJoinedAllowedMethods()); + $this->addHeader('Allow', ...$allowedMethods); } /** - * Gets allowed HTTP methods - * - * @return list + * @return list */ - final public function getAllowedMethods(): array + public function getAllowedMethods(): array { return $this->allowedMethods; } - - /** - * Gets joined allowed HTTP methods - * - * @return string - */ - final public function getJoinedAllowedMethods(): string - { - return join(',', $this->allowedMethods); - } } diff --git a/src/Exception/Http/HttpMisdirectedRequestException.php b/src/Exception/Http/HttpMisdirectedRequestException.php deleted file mode 100644 index 4987747f..00000000 --- a/src/Exception/Http/HttpMisdirectedRequestException.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Misdirected Request Exception - * - * @since 3.0.0 - */ -class HttpMisdirectedRequestException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Misdirected Request'; - - parent::__construct(self::STATUS_MISDIRECTED_REQUEST, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpNotAcceptableException.php b/src/Exception/Http/HttpNotAcceptableException.php deleted file mode 100644 index 5204ff13..00000000 --- a/src/Exception/Http/HttpNotAcceptableException.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Not Acceptable Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/406 - * - * @since 3.0.0 - */ -class HttpNotAcceptableException extends HttpException -{ - - /** - * Supported media types - * - * @var list - */ - private array $supportedMediaTypes = []; - - /** - * Constructor of the class - * - * @param list $supportedMediaTypes - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct( - array $supportedMediaTypes, - ?string $message = null, - int $code = 0, - ?Throwable $previous = null - ) { - $message ??= 'Not Acceptable'; - - parent::__construct(self::STATUS_NOT_ACCEPTABLE, $message, $code, $previous); - - $this->supportedMediaTypes = $supportedMediaTypes; - } - - /** - * Gets supported media types - * - * @return list - */ - final public function getSupportedTypes(): array - { - return $this->supportedMediaTypes; - } -} diff --git a/src/Exception/Http/HttpNotFoundException.php b/src/Exception/Http/HttpNotFoundException.php index 4b2c300e..d8e98fcd 100644 --- a/src/Exception/Http/HttpNotFoundException.php +++ b/src/Exception/Http/HttpNotFoundException.php @@ -29,14 +29,16 @@ class HttpNotFoundException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Not Found'; + $message ??= 'The requested page or resource could not be found.'; parent::__construct(self::STATUS_NOT_FOUND, $message, $code, $previous); + + $this->setReasonPhrase('Not Found'); } } diff --git a/src/Exception/Http/HttpPayloadTooLargeException.php b/src/Exception/Http/HttpPayloadTooLargeException.php index 46556afc..26e9d184 100644 --- a/src/Exception/Http/HttpPayloadTooLargeException.php +++ b/src/Exception/Http/HttpPayloadTooLargeException.php @@ -29,14 +29,16 @@ class HttpPayloadTooLargeException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Payload Too Large'; + $message ??= 'The request couldn‘t be processed due to the payload size exceeding limits.'; parent::__construct(self::STATUS_PAYLOAD_TOO_LARGE, $message, $code, $previous); + + $this->setReasonPhrase('Payload Too Large'); } } diff --git a/src/Exception/Http/HttpPaymentRequiredException.php b/src/Exception/Http/HttpPaymentRequiredException.php deleted file mode 100644 index 523de368..00000000 --- a/src/Exception/Http/HttpPaymentRequiredException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Payment Required Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/402 - * - * @since 3.0.0 - */ -class HttpPaymentRequiredException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Payment Required'; - - parent::__construct(self::STATUS_PAYMENT_REQUIRED, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpPreconditionFailedException.php b/src/Exception/Http/HttpPreconditionFailedException.php deleted file mode 100644 index d5c631cd..00000000 --- a/src/Exception/Http/HttpPreconditionFailedException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Precondition Failed Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412 - * - * @since 3.0.0 - */ -class HttpPreconditionFailedException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Precondition Failed'; - - parent::__construct(self::STATUS_PRECONDITION_FAILED, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpProxyAuthenticationRequiredException.php b/src/Exception/Http/HttpProxyAuthenticationRequiredException.php deleted file mode 100644 index 78e216e5..00000000 --- a/src/Exception/Http/HttpProxyAuthenticationRequiredException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Proxy Authentication Required Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/407 - * - * @since 3.0.0 - */ -class HttpProxyAuthenticationRequiredException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Proxy Authentication Required'; - - parent::__construct(self::STATUS_PROXY_AUTHENTICATION_REQUIRED, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpRangeNotSatisfiableException.php b/src/Exception/Http/HttpRangeNotSatisfiableException.php deleted file mode 100644 index 9a495c79..00000000 --- a/src/Exception/Http/HttpRangeNotSatisfiableException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Range Not Satisfiable Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/416 - * - * @since 3.0.0 - */ -class HttpRangeNotSatisfiableException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Range Not Satisfiable'; - - parent::__construct(self::STATUS_RANGE_NOT_SATISFIABLE, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php b/src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php deleted file mode 100644 index 095dbbbe..00000000 --- a/src/Exception/Http/HttpRequestHeaderFieldsTooLargeException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Request Header Fields Too Large Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/431 - * - * @since 3.0.0 - */ -class HttpRequestHeaderFieldsTooLargeException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Request Header Fields Too Large'; - - parent::__construct(self::STATUS_REQUEST_HEADER_FIELDS_TOO_LARGE, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpRequestTimeoutException.php b/src/Exception/Http/HttpRequestTimeoutException.php deleted file mode 100644 index 48a39295..00000000 --- a/src/Exception/Http/HttpRequestTimeoutException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Request Timeout Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408 - * - * @since 3.0.0 - */ -class HttpRequestTimeoutException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Request Timeout'; - - parent::__construct(self::STATUS_REQUEST_TIMEOUT, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpServiceUnavailableException.php b/src/Exception/Http/HttpServiceUnavailableException.php index f30b1822..ff7986db 100644 --- a/src/Exception/Http/HttpServiceUnavailableException.php +++ b/src/Exception/Http/HttpServiceUnavailableException.php @@ -29,14 +29,17 @@ class HttpServiceUnavailableException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Service Unavailable'; + // phpcs:ignore Generic.Files.LineLength + $message ??= 'The server is currently unable to handle the request due to temporary overloading or maintenance.'; parent::__construct(self::STATUS_SERVICE_UNAVAILABLE, $message, $code, $previous); + + $this->setReasonPhrase('Service Unavailable'); } } diff --git a/src/Exception/Http/HttpTooEarlyException.php b/src/Exception/Http/HttpTooEarlyException.php deleted file mode 100644 index 3652bebf..00000000 --- a/src/Exception/Http/HttpTooEarlyException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Too Early Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/425 - * - * @since 3.0.0 - */ -class HttpTooEarlyException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Too Early'; - - parent::__construct(self::STATUS_TOO_EARLY, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpTooManyRequestsException.php b/src/Exception/Http/HttpTooManyRequestsException.php index cbf9c022..4e81b91a 100644 --- a/src/Exception/Http/HttpTooManyRequestsException.php +++ b/src/Exception/Http/HttpTooManyRequestsException.php @@ -29,14 +29,16 @@ class HttpTooManyRequestsException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Too Many Requests'; + $message ??= 'The request couldn‘t be processed due to exceeding rate-limiting thresholds.'; parent::__construct(self::STATUS_TOO_MANY_REQUESTS, $message, $code, $previous); + + $this->setReasonPhrase('Too Many Requests'); } } diff --git a/src/Exception/Http/HttpUnauthorizedException.php b/src/Exception/Http/HttpUnauthorizedException.php index d496e992..d90910db 100644 --- a/src/Exception/Http/HttpUnauthorizedException.php +++ b/src/Exception/Http/HttpUnauthorizedException.php @@ -29,14 +29,16 @@ class HttpUnauthorizedException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Unauthorized'; + $message ??= 'The request couldn‘t processed due to insufficient or invalid authentication credentials.'; parent::__construct(self::STATUS_UNAUTHORIZED, $message, $code, $previous); + + $this->setReasonPhrase('Unauthorized'); } } diff --git a/src/Exception/Http/HttpUnavailableForLegalReasonsException.php b/src/Exception/Http/HttpUnavailableForLegalReasonsException.php index fcdd1550..a7cbb14f 100644 --- a/src/Exception/Http/HttpUnavailableForLegalReasonsException.php +++ b/src/Exception/Http/HttpUnavailableForLegalReasonsException.php @@ -29,14 +29,16 @@ class HttpUnavailableForLegalReasonsException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Unavailable For Legal Reasons'; + $message ??= 'The request couldn‘t be processed due to legal restrictions on the resource.'; parent::__construct(self::STATUS_UNAVAILABLE_FOR_LEGAL_REASONS, $message, $code, $previous); + + $this->setReasonPhrase('Unavailable For Legal Reasons'); } } diff --git a/src/Exception/Http/HttpUnprocessableEntityException.php b/src/Exception/Http/HttpUnprocessableEntityException.php index ba950687..b26c9ebf 100644 --- a/src/Exception/Http/HttpUnprocessableEntityException.php +++ b/src/Exception/Http/HttpUnprocessableEntityException.php @@ -29,14 +29,16 @@ class HttpUnprocessableEntityException extends HttpException /** * Constructor of the class * - * @param string|null $message + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) { - $message ??= 'Unprocessable Entity'; + $message ??= 'The request couldn‘t be processed due to semantic violations.'; parent::__construct(self::STATUS_UNPROCESSABLE_ENTITY, $message, $code, $previous); + + $this->setReasonPhrase('Unprocessable Entity'); } } diff --git a/src/Exception/Http/HttpUnsupportedMediaTypeException.php b/src/Exception/Http/HttpUnsupportedMediaTypeException.php index 449ce440..bf046b8e 100644 --- a/src/Exception/Http/HttpUnsupportedMediaTypeException.php +++ b/src/Exception/Http/HttpUnsupportedMediaTypeException.php @@ -13,11 +13,10 @@ namespace Sunrise\Http\Router\Exception\Http; +use Sunrise\Http\Router\Entity\MediaType; use Sunrise\Http\Router\Exception\HttpException; use Throwable; -use function join; - /** * HTTP Unsupported Media Type Exception * @@ -28,53 +27,31 @@ class HttpUnsupportedMediaTypeException extends HttpException { - /** - * Supported media types - * - * @var list - */ - private array $supportedMediaTypes = []; - /** * Constructor of the class * - * @param list $supportedMediaTypes - * @param string|null $message + * @param list $supportedMediaTypes + * @param non-empty-string|null $message * @param int $code * @param Throwable|null $previous */ - public function __construct( - array $supportedMediaTypes, - ?string $message = null, - int $code = 0, - ?Throwable $previous = null - ) { - $message ??= 'Unsupported Media Type'; + // phpcs:ignore Generic.Files.LineLength + public function __construct(private array $supportedMediaTypes, ?string $message = null, int $code = 0, ?Throwable $previous = null) + { + $message ??= 'The request couldn‘t be processed due to an unsupported format of the request payload.'; parent::__construct(self::STATUS_UNSUPPORTED_MEDIA_TYPE, $message, $code, $previous); - $this->supportedMediaTypes = $supportedMediaTypes; + $this->setReasonPhrase('Unsupported Media Type'); - $this->addHeaderField('Accept', $this->getJoinedSupportedTypes()); + $this->addHeader('Accept', ...$supportedMediaTypes); } /** - * Gets supported media types - * - * @return list + * @return list */ - final public function getSupportedTypes(): array + public function getSupportedMediaTypes(): array { return $this->supportedMediaTypes; } - - /** - * Gets joined supported media types - * - * @return string - */ - final public function getJoinedSupportedTypes(): string - { - return join(',', $this->supportedMediaTypes); - } } diff --git a/src/Exception/Http/HttpUpgradeRequiredException.php b/src/Exception/Http/HttpUpgradeRequiredException.php deleted file mode 100644 index f2b56bb5..00000000 --- a/src/Exception/Http/HttpUpgradeRequiredException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP Upgrade Required Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/426 - * - * @since 3.0.0 - */ -class HttpUpgradeRequiredException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'Upgrade Required'; - - parent::__construct(self::STATUS_UPGRADE_REQUIRED, $message, $code, $previous); - } -} diff --git a/src/Exception/Http/HttpUriTooLongException.php b/src/Exception/Http/HttpUriTooLongException.php deleted file mode 100644 index 81691ca5..00000000 --- a/src/Exception/Http/HttpUriTooLongException.php +++ /dev/null @@ -1,42 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception\Http; - -use Sunrise\Http\Router\Exception\HttpException; -use Throwable; - -/** - * HTTP URI Too Long Exception - * - * @link https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/414 - * - * @since 3.0.0 - */ -class HttpUriTooLongException extends HttpException -{ - - /** - * Constructor of the class - * - * @param string|null $message - * @param int $code - * @param Throwable|null $previous - */ - public function __construct(?string $message = null, int $code = 0, ?Throwable $previous = null) - { - $message ??= 'URI Too Long'; - - parent::__construct(self::STATUS_URI_TOO_LONG, $message, $code, $previous); - } -} diff --git a/src/Exception/HttpException.php b/src/Exception/HttpException.php index ced1091f..11add4f8 100644 --- a/src/Exception/HttpException.php +++ b/src/Exception/HttpException.php @@ -14,8 +14,14 @@ namespace Sunrise\Http\Router\Exception; use RuntimeException; +use Stringable; +use Sunrise\Http\Router\Dto\ViolationDto; +use Sunrise\Hydrator\Exception\InvalidValueException; +use Symfony\Component\Validator\ConstraintViolationInterface; use Throwable; +use function join; + /** * Base HTTP exception * @@ -25,18 +31,32 @@ class HttpException extends RuntimeException implements HttpExceptionInterface { /** - * HTTP status code + * The error's reason phrase + * + * @var non-empty-string + */ + private string $reasonPhrase = 'Something went wrong'; + + /** + * HTTP status code that will be sent to the client * * @var int<100, 599> */ private int $statusCode; /** - * HTTP header fields + * HTTP header fields that will be sent to the client * - * @var list + * @var list */ - private array $headerFields = []; + private array $headers = []; + + /** + * The list of violations associated with the error + * + * @var list + */ + private array $violations = []; /** * Constructor of the class @@ -53,6 +73,14 @@ public function __construct(int $statusCode, string $message, int $code = 0, ?Th $this->statusCode = $statusCode; } + /** + * @inheritDoc + */ + final public function getReasonPhrase(): string + { + return $this->reasonPhrase; + } + /** * @inheritDoc */ @@ -64,21 +92,124 @@ final public function getStatusCode(): int /** * @inheritDoc */ - final public function getHeaderFields(): array + final public function getHeaders(): array { - return $this->headerFields; + return $this->headers; } /** - * Adds the given header field to the exception + * @inheritDoc + */ + final public function getViolations(): array + { + return $this->violations; + } + + /** + * Sets the given message to the error + * + * @param non-empty-string $message + * + * @return static + */ + final public function setMessage(string $message): static + { + $this->message = $message; + + return $this; + } + + /** + * Sets the given reason phrase to the error * - * @param string $fieldName - * @param string $fieldValue + * @param non-empty-string $reasonPhrase * - * @return void + * @return static */ - final public function addHeaderField(string $fieldName, string $fieldValue): void + final public function setReasonPhrase(string $reasonPhrase): static { - $this->headerFields[] = [$fieldName, $fieldValue]; + $this->reasonPhrase = $reasonPhrase; + + return $this; + } + + /** + * Sets the given HTTP status code that will be sent to the client + * + * @param int<100, 599> $statusCode + * + * @return static + */ + final public function setStatusCode(int $statusCode): static + { + $this->statusCode = $statusCode; + + return $this; + } + + /** + * Adds the given HTTP header field that will be sent to the client + * + * @param non-empty-string $fieldName + * @param Stringable|non-empty-string ...$fieldValues + * + * @return static + */ + final public function addHeader(string $fieldName, Stringable|string ...$fieldValues): static + { + /** @var non-empty-string $fieldValue */ + $fieldValue = join(', ', $fieldValues); + + $this->headers[] = [$fieldName, $fieldValue]; + + return $this; + } + + /** + * Adds the given violation(s) associated with the error + * + * @param ViolationDto ...$violations + * + * @return static + */ + final public function addViolation(ViolationDto ...$violations): static + { + foreach ($violations as $violation) { + $this->violations[] = $violation; + } + + return $this; + } + + /** + * Adds the given hydrator violation(s) associated with the error + * + * @param InvalidValueException ...$violations + * + * @return static + */ + final public function addHydratorViolation(InvalidValueException ...$violations): static + { + foreach ($violations as $violation) { + $this->violations[] = ViolationDto::fromHydratorViolation($violation); + } + + return $this; + } + + /** + * Adds the given validator violation(s) associated with the error + * + * @param ConstraintViolationInterface ...$violations + * + * @return static + */ + final public function addValidatorViolation(ConstraintViolationInterface ...$violations): static + { + foreach ($violations as $violation) { + $this->violations[] = ViolationDto::fromValidatorViolation($violation); + } + + return $this; } } diff --git a/src/Exception/HttpExceptionInterface.php b/src/Exception/HttpExceptionInterface.php index 903c93b7..70a5e7c0 100644 --- a/src/Exception/HttpExceptionInterface.php +++ b/src/Exception/HttpExceptionInterface.php @@ -14,6 +14,7 @@ namespace Sunrise\Http\Router\Exception; use Fig\Http\Message\StatusCodeInterface; +use Sunrise\Http\Router\Dto\ViolationDto; /** * Base HTTP exception interface @@ -24,16 +25,37 @@ interface HttpExceptionInterface extends ExceptionInterface, StatusCodeInterface { /** - * Gets HTTP status code + * {@inheritDoc} + * + * @return non-empty-string + */ + public function getMessage(): string; + + /** + * Gets the error's reason phrase + * + * @return non-empty-string + */ + public function getReasonPhrase(): string; + + /** + * Gets HTTP status code that will be sent to the client * * @return int<100, 599> */ public function getStatusCode(): int; /** - * Gets HTTP header fields + * Gets HTTP header fields that will be sent to the client + * + * @return list + */ + public function getHeaders(): array; + + /** + * Gets the list of violations associated with the error * - * @return list + * @return list */ - public function getHeaderFields(): array; + public function getViolations(): array; } diff --git a/src/Exception/InvalidRequestPayloadException.php b/src/Exception/InvalidRequestPayloadException.php deleted file mode 100644 index 4e6c96c9..00000000 --- a/src/Exception/InvalidRequestPayloadException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; - -/** - * InvalidRequestPayloadException - * - * @since 3.0.0 - */ -class InvalidRequestPayloadException extends HttpBadRequestException -{ -} diff --git a/src/Exception/MethodNotAllowedException.php b/src/Exception/MethodNotAllowedException.php deleted file mode 100644 index 8d15989e..00000000 --- a/src/Exception/MethodNotAllowedException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -use Sunrise\Http\Router\Exception\Http\HttpMethodNotAllowedException; - -/** - * MethodNotAllowedException - */ -class MethodNotAllowedException extends HttpMethodNotAllowedException -{ -} diff --git a/src/Exception/MissingRequestParameterException.php b/src/Exception/MissingRequestParameterException.php deleted file mode 100644 index ebdb33de..00000000 --- a/src/Exception/MissingRequestParameterException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; - -/** - * MissingRequestParameterException - * - * @since 3.0.0 - */ -class MissingRequestParameterException extends HttpBadRequestException -{ -} diff --git a/src/Exception/PageNotFoundException.php b/src/Exception/PageNotFoundException.php deleted file mode 100644 index 744e661f..00000000 --- a/src/Exception/PageNotFoundException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; - -/** - * PageNotFoundException - * - * @since 2.4.2 - */ -class PageNotFoundException extends HttpNotFoundException -{ -} diff --git a/src/Exception/ResolvingReferenceException.php b/src/Exception/ResolvingReferenceException.php deleted file mode 100644 index 2b21ef63..00000000 --- a/src/Exception/ResolvingReferenceException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -/** - * ResolvingReferenceException - * - * @since 3.0.0 - */ -class ResolvingReferenceException extends LogicException -{ -} diff --git a/src/Exception/UnhydrableObjectException.php b/src/Exception/UnhydrableObjectException.php deleted file mode 100644 index d35f2b71..00000000 --- a/src/Exception/UnhydrableObjectException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -/** - * UnhydrableObjectException - * - * @since 3.0.0 - */ -class UnhydrableObjectException extends LogicException -{ -} diff --git a/src/Exception/UnprocessableEntityException.php b/src/Exception/UnprocessableEntityException.php deleted file mode 100644 index 345a2bee..00000000 --- a/src/Exception/UnprocessableEntityException.php +++ /dev/null @@ -1,53 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; -use Symfony\Component\Validator\ConstraintViolationListInterface; - -/** - * UnprocessableEntityException - * - * @since 3.0.0 - */ -class UnprocessableEntityException extends HttpUnprocessableEntityException -{ - - /** - * @var ConstraintViolationListInterface - */ - private ConstraintViolationListInterface $violations; - - /** - * Constructor of the class - * - * @param ConstraintViolationListInterface $violations - */ - public function __construct(ConstraintViolationListInterface $violations) - { - parent::__construct(); - - $this->violations = $violations; - } - - /** - * Gets the violations list - * - * @return ConstraintViolationListInterface - */ - final public function getViolations(): ConstraintViolationListInterface - { - return $this->violations; - } -} diff --git a/src/Exception/UnprocessableRequestBodyException.php b/src/Exception/UnprocessableRequestBodyException.php deleted file mode 100644 index 3ba6a2dd..00000000 --- a/src/Exception/UnprocessableRequestBodyException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -/** - * UnprocessableRequestBodyException - * - * @since 3.0.0 - */ -class UnprocessableRequestBodyException extends UnprocessableEntityException -{ -} diff --git a/src/Exception/UnprocessableRequestQueryException.php b/src/Exception/UnprocessableRequestQueryException.php deleted file mode 100644 index 403ed813..00000000 --- a/src/Exception/UnprocessableRequestQueryException.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\Exception; - -/** - * UnprocessableRequestQueryException - * - * @since 3.0.0 - */ -class UnprocessableRequestQueryException extends UnprocessableEntityException -{ -} diff --git a/src/Loader/ConfigLoader.php b/src/Loader/ConfigLoader.php index 85677f98..b06b980e 100644 --- a/src/Loader/ConfigLoader.php +++ b/src/Loader/ConfigLoader.php @@ -16,15 +16,15 @@ use Psr\Container\ContainerInterface; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ParameterResolutioner; -use Sunrise\Http\Router\ParameterResolutionerInterface; -use Sunrise\Http\Router\ParameterResolver\DependencyInjectionParameterResolver; -use Sunrise\Http\Router\ParameterResolver\ParameterResolverInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutioner; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutionerInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolver\DependencyInjectionParameterResolver; +use Sunrise\Http\Router\ParameterResolving\ParameterResolver\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; -use Sunrise\Http\Router\ResponseResolutioner; -use Sunrise\Http\Router\ResponseResolutionerInterface; -use Sunrise\Http\Router\ResponseResolver\ResponseResolverInterface; +use Sunrise\Http\Router\ResponseResolving\ResponseResolutioner; +use Sunrise\Http\Router\ResponseResolving\ResponseResolutionerInterface; +use Sunrise\Http\Router\ResponseResolving\ResponseResolver\ResponseResolverInterface; use Sunrise\Http\Router\RouteCollectionFactory; use Sunrise\Http\Router\RouteCollectionFactoryInterface; use Sunrise\Http\Router\RouteCollectionInterface; diff --git a/src/Loader/DescriptorLoader.php b/src/Loader/DescriptorLoader.php index 7f716f36..0d65c2c3 100644 --- a/src/Loader/DescriptorLoader.php +++ b/src/Loader/DescriptorLoader.php @@ -34,17 +34,18 @@ use Sunrise\Http\Router\Annotation\Route; use Sunrise\Http\Router\Annotation\Summary; use Sunrise\Http\Router\Annotation\Tag; +use Sunrise\Http\Router\Entity\MediaType; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ParameterResolutioner; -use Sunrise\Http\Router\ParameterResolutionerInterface; -use Sunrise\Http\Router\ParameterResolver\DependencyInjectionParameterResolver; -use Sunrise\Http\Router\ParameterResolver\ParameterResolverInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutioner; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutionerInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolver\DependencyInjectionParameterResolver; +use Sunrise\Http\Router\ParameterResolving\ParameterResolver\ParameterResolverInterface; use Sunrise\Http\Router\ReferenceResolver; use Sunrise\Http\Router\ReferenceResolverInterface; -use Sunrise\Http\Router\ResponseResolutioner; -use Sunrise\Http\Router\ResponseResolutionerInterface; -use Sunrise\Http\Router\ResponseResolver\ResponseResolverInterface; +use Sunrise\Http\Router\ResponseResolving\ResponseResolutioner; +use Sunrise\Http\Router\ResponseResolving\ResponseResolutionerInterface; +use Sunrise\Http\Router\ResponseResolving\ResponseResolver\ResponseResolverInterface; use Sunrise\Http\Router\RouteCollectionFactory; use Sunrise\Http\Router\RouteCollectionFactoryInterface; use Sunrise\Http\Router\RouteCollectionInterface; @@ -56,6 +57,7 @@ use function hash; use function is_dir; use function is_string; +use function iterator_to_array; use function sprintf; use function usort; @@ -252,7 +254,7 @@ public function setCacheKey(?string $cacheKey): void */ public function getCacheKey(): string { - return $this->cacheKey ??= hash('md5', __METHOD__); + return $this->cacheKey ??= hash('md5', $this::class); } /** @@ -360,7 +362,7 @@ private function getDescriptors(): array * * @param string $resource * - * @return Generator + * @return Generator */ private function getResourceDescriptors(string $resource): Generator { @@ -380,7 +382,7 @@ private function getResourceDescriptors(string $resource): Generator * * @param ReflectionClass $class * - * @return Generator + * @return Generator */ private function getClassDescriptors(ReflectionClass $class): Generator { @@ -393,7 +395,8 @@ private function getClassDescriptors(ReflectionClass $class): Generator if ($annotations->valid()) { $descriptor = $annotations->current(); $descriptor->holder = $class->getName(); - $this->supplementDescriptor($descriptor, $class); + $this->supplementDescriptorFromParentClasses($descriptor, $class); + $this->supplementDescriptorFromClassOrMethod($descriptor, $class); yield $descriptor; } } @@ -408,13 +411,29 @@ private function getClassDescriptors(ReflectionClass $class): Generator if ($annotations->valid()) { $descriptor = $annotations->current(); $descriptor->holder = [$class->getName(), $method->getName()]; - $this->supplementDescriptor($descriptor, $class); - $this->supplementDescriptor($descriptor, $method); + $this->supplementDescriptorFromParentClasses($descriptor, $class); + $this->supplementDescriptorFromClassOrMethod($descriptor, $class); + $this->supplementDescriptorFromClassOrMethod($descriptor, $method); yield $descriptor; } } } + /** + * Supplements the given descriptor from parent classes of the given class + * + * @param Route $descriptor + * @param ReflectionClass $holder + * + * @return void + */ + private function supplementDescriptorFromParentClasses(Route $descriptor, ReflectionClass $holder): void + { + foreach ($this->getClassParents($holder) as $parent) { + $this->supplementDescriptorFromClassOrMethod($descriptor, $parent); + } + } + /** * Supplements the given descriptor from the given class or method * @@ -423,7 +442,8 @@ private function getClassDescriptors(ReflectionClass $class): Generator * * @return void */ - private function supplementDescriptor(Route $descriptor, ReflectionClass|ReflectionMethod $holder): void + // phpcs:ignore Generic.Files.LineLength + private function supplementDescriptorFromClassOrMethod(Route $descriptor, ReflectionClass|ReflectionMethod $holder): void { $annotations = $this->getAnnotations(Host::class, $holder); if ($annotations->valid()) { @@ -447,18 +467,12 @@ private function supplementDescriptor(Route $descriptor, ReflectionClass|Reflect $annotations = $this->getAnnotations(Consumes::class, $holder); foreach ($annotations as $annotation) { - $consumesMediaTypes = \Sunrise\Http\Router\parse_header_with_media_type($annotation->value); - foreach ($consumesMediaTypes as $consumesMediaType) { - $descriptor->consumes[] = $consumesMediaType; - } + $descriptor->consumes[] = new MediaType($annotation->type, $annotation->subtype); } $annotations = $this->getAnnotations(Produces::class, $holder); foreach ($annotations as $annotation) { - $producesMediaTypes = \Sunrise\Http\Router\parse_header_with_media_type($annotation->value); - foreach ($producesMediaTypes as $producesMediaType) { - $descriptor->produces[] = $producesMediaType; - } + $descriptor->produces[] = new MediaType($annotation->type, $annotation->subtype, $annotation->parameters); } $annotations = $this->getAnnotations(Middleware::class, $holder); @@ -487,22 +501,15 @@ private function supplementDescriptor(Route $descriptor, ReflectionClass|Reflect * * @param string $dirname * - * @return Generator + * @return Generator */ private function getDirectoryClasses(string $dirname): Generator { + $flags = FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_PATHNAME; + /** @var array $filenames */ - $filenames = [...( - new RegexIterator( - new RecursiveIteratorIterator( - new RecursiveDirectoryIterator( - $dirname, - FilesystemIterator::KEY_AS_PATHNAME | FilesystemIterator::CURRENT_AS_PATHNAME, - ) - ), - pattern: '/\.php$/', - ) - )]; + // phpcs:ignore Generic.Files.LineLength + $filenames = iterator_to_array(new RegexIterator(new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dirname, $flags)), '/\.php$/')); foreach ($filenames as $filename) { (static function (string $filename): void { @@ -519,13 +526,30 @@ private function getDirectoryClasses(string $dirname): Generator } } + /** + * Gets parents of the given class + * + * @param ReflectionClass $child + * + * @return Generator + */ + private function getClassParents(ReflectionClass $child): Generator + { + $ancestors = []; + while ($child = $child->getParentClass()) { + $ancestors = [$child, ...$ancestors]; + } + + yield from $ancestors; + } + /** * Gets the named annotations from the given class or method * * @param class-string $name * @param ReflectionClass|ReflectionMethod $source * - * @return Generator + * @return Generator * * @template T of object */ diff --git a/src/Middleware/CallbackMiddleware.php b/src/Middleware/CallbackMiddleware.php index 64d83c57..0dd32e3e 100644 --- a/src/Middleware/CallbackMiddleware.php +++ b/src/Middleware/CallbackMiddleware.php @@ -19,9 +19,9 @@ use Psr\Http\Server\RequestHandlerInterface; use ReflectionFunction; use ReflectionMethod; -use Sunrise\Http\Router\ParameterResolutionerInterface; -use Sunrise\Http\Router\ParameterResolver\PresetObjectParameterResolver; -use Sunrise\Http\Router\ResponseResolutionerInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutionerInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolver\ObjectInjectionParameterResolver; +use Sunrise\Http\Router\ResponseResolving\ResponseResolutionerInterface; use function Sunrise\Http\Router\reflect_callback; @@ -91,8 +91,8 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface $arguments = $this->parameterResolutioner ->withContext($request) ->withPriorityResolver( - new PresetObjectParameterResolver($request), - new PresetObjectParameterResolver($handler), + new ObjectInjectionParameterResolver($request), + new ObjectInjectionParameterResolver($handler), ) ->resolveParameters(...$source->getParameters()); diff --git a/src/Middleware/ErrorHandlingMiddleware.php b/src/Middleware/ErrorHandlingMiddleware.php new file mode 100644 index 00000000..bd77dc37 --- /dev/null +++ b/src/Middleware/ErrorHandlingMiddleware.php @@ -0,0 +1,177 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\Middleware; + +use Psr\EventDispatcher\EventDispatcherInterface; +use Psr\Http\Message\ResponseFactoryInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use Psr\Http\Server\MiddlewareInterface; +use Psr\Http\Server\RequestHandlerInterface; +use Psr\Log\LoggerInterface; +use SimpleXMLElement; +use Sunrise\Http\Router\Dto\ErrorDto; +use Sunrise\Http\Router\Entity\MediaType; +use Sunrise\Http\Router\Event\ErrorEvent; +use Sunrise\Http\Router\Exception\Http\HttpInternalServerErrorException; +use Sunrise\Http\Router\Exception\HttpExceptionInterface; +use Sunrise\Http\Router\ServerRequest; +use Throwable; + +use function extension_loaded; +use function json_encode; +use function ob_end_clean; +use function ob_get_clean; +use function ob_start; + +use const JSON_PARTIAL_OUTPUT_ON_ERROR; +use const LIBXML_COMPACT; +use const LIBXML_NOERROR; +use const LIBXML_NOWARNING; + +/** + * @since 3.0.0 + */ +final class ErrorHandlingMiddleware implements MiddlewareInterface +{ + + /** + * Constructor of the class + * + * @param ResponseFactoryInterface $responseFactory + * @param EventDispatcherInterface|null $eventDispatcher + * @param LoggerInterface|null $logger + */ + public function __construct( + private ResponseFactoryInterface $responseFactory, + private ?EventDispatcherInterface $eventDispatcher = null, + private ?LoggerInterface $logger = null, + ) { + } + + /** + * @inheritDoc + */ + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + try { + return $handler->handle($request); + } catch (HttpExceptionInterface $error) { + return $this->handleHttpError($error, $request); + } catch (Throwable $error) { + return $this->handleFatalError($error, $request); + } + } + + private function handleHttpError(HttpExceptionInterface $error, ServerRequestInterface $request): ResponseInterface + { + $serverProducesMediaTypes = [MediaType::html(), MediaType::json()]; + + if (extension_loaded('simplexml')) { + $serverProducesMediaTypes[] = MediaType::xml(); + } + + $clientPreferredMediaType = ServerRequest::from($request) + ->getClientPreferredMediaType(...$serverProducesMediaTypes); + + $response = $this->responseFactory->createResponse($error->getStatusCode()) + ->withHeader('Content-Type', $clientPreferredMediaType->__toString()); + + foreach ($error->getHeaders() as [$fieldName, $fieldValue]) { + $response = $response->withHeader($fieldName, $fieldValue); + } + + if ($clientPreferredMediaType->equals(MediaType::html())) { + $response->getBody()->write($this->renderHtmlError($error)); + } elseif ($clientPreferredMediaType->equals(MediaType::json())) { + $response->getBody()->write($this->renderJsonError($error)); + } elseif ($clientPreferredMediaType->equals(MediaType::xml())) { + $response->getBody()->write($this->renderXmlError($error)); + } + + if (isset($this->eventDispatcher)) { + $event = new ErrorEvent($error, $request, $response); + $this->eventDispatcher->dispatch($event); + $response = $event->getResponse(); + } + + return $response; + } + + private function handleFatalError(Throwable $error, ServerRequestInterface $request): ResponseInterface + { + $this->logger?->error($error->getMessage(), ['error' => $error]); + + $httpError = new HttpInternalServerErrorException(previous: $error); + + return $this->handleHttpError($httpError, $request); + } + + private function renderHtmlError(HttpExceptionInterface $error): string + { + $view = __DIR__ . '/../../resources/views/error.phtml'; + + return $this->loadView($view, $error); + } + + private function renderJsonError(HttpExceptionInterface $error): string + { + $view = new ErrorDto( + $error->getMessage(), + $error->getViolations(), + ); + + return json_encode($view, flags: JSON_PARTIAL_OUTPUT_ON_ERROR); + } + + private function renderXmlError(HttpExceptionInterface $error): string + { + $xmlBlank = ''; + $xmlOptions = LIBXML_COMPACT | LIBXML_NOERROR | LIBXML_NOWARNING; + + $errorChild = new SimpleXMLElement($xmlBlank, $xmlOptions); + $errorChild->addChild('message', $error->getMessage()); + + foreach ($error->getViolations() as $violation) { + /** @var SimpleXMLElement $violationsChild */ + $violationsChild = $errorChild->addChild('violations'); + $violationsChild->addChild('message', $violation->message); + $violationsChild->addChild('source', $violation->source); + $violationsChild->addChild('code', $violation->code); + } + + /** @var string */ + return $errorChild->asXML(); + } + + private function loadView(string $filename, HttpExceptionInterface $error): string + { + try { + ob_start(); + + (function (string $filename): void { + /** @psalm-suppress UnresolvableInclude */ + include $filename; + })->call($error, $filename); + + return ob_get_clean(); + } catch (Throwable $exception) { + while (ob_get_level()) { + ob_end_clean(); + } + + throw $exception; + } + } +} diff --git a/src/Middleware/JsonPayloadDecodingMiddleware.php b/src/Middleware/JsonPayloadDecodingMiddleware.php index 6dbd6d97..cd850ee3 100644 --- a/src/Middleware/JsonPayloadDecodingMiddleware.php +++ b/src/Middleware/JsonPayloadDecodingMiddleware.php @@ -19,14 +19,11 @@ use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\Entity\MediaType; -use Sunrise\Http\Router\Exception\InvalidRequestPayloadException; -use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; use Sunrise\Http\Router\ServerRequest; -use function extension_loaded; use function is_array; use function json_decode; -use function sprintf; use const JSON_BIGINT_AS_STRING; use const JSON_THROW_ON_ERROR; @@ -41,20 +38,6 @@ final class JsonPayloadDecodingMiddleware implements MiddlewareInterface { - /** - * Constructor of the class - * - * @throws LogicException If the JSON extension isn't loaded. - */ - public function __construct() - { - if (!extension_loaded('json')) { - throw new LogicException( - 'The JSON extension is required, run the `pecl install json` command to resolve it.' - ); - } - } - /** * @inheritDoc */ @@ -74,19 +57,22 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface * * @return array * - * @throws InvalidRequestPayloadException If the JSON payload cannot be decoded. + * @throws HttpBadRequestException If the JSON payload couldn't be decoded. */ private function decodePayload(string $payload): array { + if ($payload === '') { + throw new HttpBadRequestException('JSON payload cannot be empty.'); + } + try { $data = json_decode($payload, true, 512, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR); } catch (JsonException $e) { - throw new InvalidRequestPayloadException(sprintf('Invalid JSON payload: %s', $e->getMessage()), 0, $e); + throw new HttpBadRequestException('The JSON payload is invalid and could not be decoded.', previous: $e); } - // According to PSR-7, the data must be an array... - if (!is_array($data)) { - throw new InvalidRequestPayloadException('Unexpected JSON: Expects an array or object.'); + if (is_array($data) === false) { + throw new HttpBadRequestException('The JSON payload must be in the form of an array or an object.'); } return $data; diff --git a/src/ParameterResolver/ClientRemoteAddressParameterResolver.php b/src/ParameterResolver/ClientRemoteAddressParameterResolver.php deleted file mode 100644 index e1e00e80..00000000 --- a/src/ParameterResolver/ClientRemoteAddressParameterResolver.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\ParameterResolver; - -use Generator; -use Psr\Http\Message\ServerRequestInterface; -use ReflectionNamedType; -use ReflectionParameter; -use Sunrise\Http\Router\Entity\ClientRemoteAddress; -use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ServerRequest; - -/** - * ClientRemoteAddressParameterResolver - * - * @since 3.0.0 - */ -final class ClientRemoteAddressParameterResolver implements ParameterResolverInterface -{ - - /** - * Constructor of th class - * - * @param array $proxyChain - * - * @template TKey as non-empty-string Proxy address; e.g., 127.0.0.1 - * @template TValue as non-empty-string Trusted header; e.g., X-Forwarded-For, X-Real-IP, etc. - */ - public function __construct(private array $proxyChain = []) - { - } - - /** - * @inheritDoc - * - * @throws LogicException If the resolver is used incorrectly. - */ - public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator - { - $type = $parameter->getType(); - - if (! ($type instanceof ReflectionNamedType) || - ! ($type->getName() === ClientRemoteAddress::class)) { - return; - } - - if (! $context instanceof ServerRequestInterface) { - throw new LogicException( - 'At this level of the application, any operations with the request are not possible.' - ); - } - - yield ServerRequest::from($context)->getClientRemoteAddress($this->proxyChain); - } -} diff --git a/src/ParameterResolver/RequestEntityParameterResolver.php b/src/ParameterResolver/RequestEntityParameterResolver.php deleted file mode 100644 index ed844278..00000000 --- a/src/ParameterResolver/RequestEntityParameterResolver.php +++ /dev/null @@ -1,132 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\ParameterResolver; - -use Doctrine\Persistence\ManagerRegistry as EntityManagerRegistryInterface; -use Generator; -use Psr\Http\Message\ServerRequestInterface; -use ReflectionAttribute; -use ReflectionNamedType; -use ReflectionParameter; -use Sunrise\Http\Router\Annotation\RequestEntity; -use Sunrise\Http\Router\Exception\EntityNotFoundException; -use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ParameterResolutioner; - -use function count; -use function current; -use function sprintf; - -/** - * RequestEntityParameterResolver - * - * @since 3.0.0 - */ -final class RequestEntityParameterResolver implements ParameterResolverInterface -{ - - /** - * Constructor of the class - * - * @param EntityManagerRegistryInterface $entityManagerRegistry - * @param non-empty-string|null $defaultEntityManagerName - */ - public function __construct( - private EntityManagerRegistryInterface $entityManagerRegistry, - private string|null $defaultEntityManagerName = null, - ) { - } - - /** - * @inheritDoc - * - * @throws EntityNotFoundException If an entity wasn't found. - * @throws LogicException If the resolver is used incorrectly. - */ - public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator - { - /** @var list> $attributes */ - $attributes = $parameter->getAttributes(RequestEntity::class); - if ($attributes === []) { - return; - } - - $type = $parameter->getType(); - - if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) { - throw new LogicException(sprintf( - 'To use the #[RequestEntity] attribute, the parameter {%s} must be typed with an entity.', - ParameterResolutioner::stringifyParameter($parameter), - )); - } - - if (! $context instanceof ServerRequestInterface) { - throw new LogicException( - 'At this level of the application, any operations with the request are not possible.' - ); - } - - $requestEntity = $attributes[0]->newInstance(); - - $entityManagerName = $requestEntity->em ?? $this->defaultEntityManagerName; - $entityManager = $this->entityManagerRegistry->getManager($entityManagerName); - - $entityIdentificationFieldName = $requestEntity->findBy; - if ($entityIdentificationFieldName === null) { - $entityMetadata = $entityManager->getClassMetadata($type->getName()); - $entityIdentificationFieldNames = $entityMetadata->getIdentifier(); - if (empty($entityIdentificationFieldNames) || - count($entityIdentificationFieldNames) > 1) { - throw new LogicException(sprintf( - 'To use the #[RequestEntity] attribute with the parameter {%s}, ' . - 'it is necessary to explicitly set the "findBy" parameter within it, ' . - 'as the entity {%s} either has a composite identifier or does not have one at all.', - ParameterResolutioner::stringifyParameter($parameter), - $type->getName(), - )); - } - - $entityIdentificationFieldName = current($entityIdentificationFieldNames); - } - - $requestParameterName = $requestEntity->valueKey ?? $entityIdentificationFieldName; - $entityIdentificationFieldValue = $context->getAttribute($requestParameterName); - if ($entityIdentificationFieldValue === null) { - throw new LogicException(sprintf( - 'To use the #[RequestEntity] attribute with the parameter {%s}, ' . - 'it might be necessary to explicitly set the "valueKey" parameter within it, ' . - 'as the attribute with the name "%s" was not found in the current request.', - ParameterResolutioner::stringifyParameter($parameter), - $requestParameterName, - )); - } - - $entity = $entityManager->getRepository($type->getName())->findOneBy([ - $entityIdentificationFieldName => $entityIdentificationFieldValue, - ...$requestEntity->criteria, - ]); - - if (isset($entity)) { - yield $entity; - return; - } - - if ($parameter->allowsNull()) { - yield null; - return; - } - - throw new EntityNotFoundException(); - } -} diff --git a/src/ParameterResolver/RequestHeaderParameterResolver.php b/src/ParameterResolver/RequestHeaderParameterResolver.php deleted file mode 100644 index 014c4f31..00000000 --- a/src/ParameterResolver/RequestHeaderParameterResolver.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\ParameterResolver; - -use Generator; -use ReflectionAttribute; -use ReflectionParameter; -use Sunrise\Http\Router\Annotation\RequestHeader; - -final class RequestHeaderParameterResolver implements ParameterResolverInterface -{ - - /** - * @inheritDoc - */ - public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator - { - /** @var list> $attributes */ - $attributes = $parameter->getAttributes(RequestHeader::class); - if ($attributes === []) { - return; - } - - // TODO: Implement the method... - } -} diff --git a/src/ParameterResolver/RequestPathVariableParameterResolver.php b/src/ParameterResolver/RequestPathVariableParameterResolver.php deleted file mode 100644 index 4db162e1..00000000 --- a/src/ParameterResolver/RequestPathVariableParameterResolver.php +++ /dev/null @@ -1,37 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\ParameterResolver; - -use Generator; -use ReflectionAttribute; -use ReflectionParameter; -use Sunrise\Http\Router\Annotation\RequestPathVariable; - -final class RequestPathVariableParameterResolver implements ParameterResolverInterface -{ - - /** - * @inheritDoc - */ - public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator - { - /** @var list> $attributes */ - $attributes = $parameter->getAttributes(RequestPathVariable::class); - if ($attributes === []) { - return; - } - - // TODO: Implement the method... - } -} diff --git a/src/ParameterResolutioner.php b/src/ParameterResolving/ParameterResolutioner.php similarity index 89% rename from src/ParameterResolutioner.php rename to src/ParameterResolving/ParameterResolutioner.php index 3d9ed969..0a0a404b 100644 --- a/src/ParameterResolutioner.php +++ b/src/ParameterResolving/ParameterResolutioner.php @@ -11,13 +11,13 @@ declare(strict_types=1); -namespace Sunrise\Http\Router; +namespace Sunrise\Http\Router\ParameterResolving; use Generator; use ReflectionMethod; use ReflectionParameter; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ParameterResolver\ParameterResolverInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolver\ParameterResolverInterface; use function sprintf; @@ -82,7 +82,7 @@ public function addResolver(ParameterResolverInterface ...$resolvers): void /** * @inheritDoc * - * @throws LogicException If one of the parameters cannot be resolved to an argument(s). + * @throws LogicException If one of the parameters couldn't be resolved to an argument(s). */ public function resolveParameters(ReflectionParameter ...$parameters): Generator { @@ -96,9 +96,9 @@ public function resolveParameters(ReflectionParameter ...$parameters): Generator * * @param ReflectionParameter $parameter * - * @return Generator + * @return Generator * - * @throws LogicException If the parameter cannot be resolved to an argument(s). + * @throws LogicException If the parameter couldn't be resolved to an argument(s). */ private function resolveParameter(ReflectionParameter $parameter): Generator { @@ -114,7 +114,7 @@ private function resolveParameter(ReflectionParameter $parameter): Generator } throw new LogicException(sprintf( - 'Unable to resolve the parameter {%s}', + 'Unable to resolve the parameter {%s}.', self::stringifyParameter($parameter) )); } diff --git a/src/ParameterResolutionerInterface.php b/src/ParameterResolving/ParameterResolutionerInterface.php similarity index 88% rename from src/ParameterResolutionerInterface.php rename to src/ParameterResolving/ParameterResolutionerInterface.php index bfbd04ea..97a0a1a6 100644 --- a/src/ParameterResolutionerInterface.php +++ b/src/ParameterResolving/ParameterResolutionerInterface.php @@ -11,11 +11,11 @@ declare(strict_types=1); -namespace Sunrise\Http\Router; +namespace Sunrise\Http\Router\ParameterResolving; use Generator; use ReflectionParameter; -use Sunrise\Http\Router\ParameterResolver\ParameterResolverInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolver\ParameterResolverInterface; /** * ParameterResolutionerInterface @@ -61,7 +61,7 @@ public function addResolver(ParameterResolverInterface ...$resolvers): void; * * @param ReflectionParameter ...$parameters * - * @return Generator List of ready-to-pass arguments. + * @return Generator List of ready-to-pass arguments. */ public function resolveParameters(ReflectionParameter ...$parameters): Generator; } diff --git a/src/ParameterResolver/DependencyInjectionParameterResolver.php b/src/ParameterResolving/ParameterResolver/DependencyInjectionParameterResolver.php similarity index 94% rename from src/ParameterResolver/DependencyInjectionParameterResolver.php rename to src/ParameterResolving/ParameterResolver/DependencyInjectionParameterResolver.php index ba6db34d..252b837d 100644 --- a/src/ParameterResolver/DependencyInjectionParameterResolver.php +++ b/src/ParameterResolving/ParameterResolver/DependencyInjectionParameterResolver.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ParameterResolver; +namespace Sunrise\Http\Router\ParameterResolving\ParameterResolver; use Generator; use Psr\Container\ContainerInterface; diff --git a/src/ParameterResolver/PresetObjectParameterResolver.php b/src/ParameterResolving/ParameterResolver/ObjectInjectionParameterResolver.php similarity index 79% rename from src/ParameterResolver/PresetObjectParameterResolver.php rename to src/ParameterResolving/ParameterResolver/ObjectInjectionParameterResolver.php index 91e16fd7..f7692281 100644 --- a/src/ParameterResolver/PresetObjectParameterResolver.php +++ b/src/ParameterResolving/ParameterResolver/ObjectInjectionParameterResolver.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ParameterResolver; +namespace Sunrise\Http\Router\ParameterResolving\ParameterResolver; use Generator; use ReflectionNamedType; @@ -20,11 +20,11 @@ use function is_a; /** - * PresetObjectParameterResolver + * ObjectInjectionParameterResolver * * @since 3.0.0 */ -final class PresetObjectParameterResolver implements ParameterResolverInterface +final class ObjectInjectionParameterResolver implements ParameterResolverInterface { /** @@ -47,7 +47,7 @@ public function resolveParameter(ReflectionParameter $parameter, mixed $context) return; } - if (! is_a($this->object, $type->getName())) { + if (!is_a($this->object, $type->getName())) { return; } diff --git a/src/ParameterResolver/ParameterResolverInterface.php b/src/ParameterResolving/ParameterResolver/ParameterResolverInterface.php similarity index 87% rename from src/ParameterResolver/ParameterResolverInterface.php rename to src/ParameterResolving/ParameterResolver/ParameterResolverInterface.php index 6f29aaa7..05989698 100644 --- a/src/ParameterResolver/ParameterResolverInterface.php +++ b/src/ParameterResolving/ParameterResolver/ParameterResolverInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ParameterResolver; +namespace Sunrise\Http\Router\ParameterResolving\ParameterResolver; use Generator; use ReflectionParameter; @@ -30,7 +30,7 @@ interface ParameterResolverInterface * @param ReflectionParameter $parameter * @param mixed $context * - * @return Generator + * @return Generator */ public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator; } diff --git a/src/ParameterResolving/ParameterResolver/PathVariableParameterResolver.php b/src/ParameterResolving/ParameterResolver/PathVariableParameterResolver.php new file mode 100644 index 00000000..5aa2f61a --- /dev/null +++ b/src/ParameterResolving/ParameterResolver/PathVariableParameterResolver.php @@ -0,0 +1,117 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ParameterResolving\ParameterResolver; + +use Generator; +use InvalidArgumentException; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionAttribute; +use ReflectionParameter; +use Sunrise\Http\Router\Annotation\PathVariable; +use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutioner; +use Sunrise\Http\Router\RouteInterface; +use Sunrise\Http\Router\TypeConversion\TypeConversionerInterface; + +use function sprintf; + +/** + * PathVariableParameterResolver + * + * @since 3.0.0 + */ +final class PathVariableParameterResolver implements ParameterResolverInterface +{ + + /** + * Constructor of the class + * + * @param TypeConversionerInterface $typeConversioner + */ + public function __construct(private TypeConversionerInterface $typeConversioner) + { + } + + /** + * @inheritDoc + * + * @throws LogicException If the resolver is used incorrectly. + * + * @throws HttpNotFoundException If the request's path variable isn't valid. + */ + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator + { + /** @var list> $attributes */ + $attributes = $parameter->getAttributes(PathVariable::class); + if ($attributes === []) { + return; + } + + if (!$parameter->hasType()) { + throw new LogicException(sprintf( + 'To use the #[PathVariable] attribute, the parameter {%s} must be typed.', + ParameterResolutioner::stringifyParameter($parameter), + )); + } + + if (! $context instanceof ServerRequestInterface) { + throw new LogicException( + 'At this level of the application, any operations with the request are not possible.' + ); + } + + $route = $context->getAttribute('@route'); + + if (! $route instanceof RouteInterface) { + throw new LogicException( + 'The #[PathVariable] attribute cannot be applied to the parameter {%s}, ' . + 'because the request does not contain information about the requested route, ' . + 'at least at this level of the application.' + ); + } + + $variable = $attributes[0]->newInstance(); + $variableName = $variable->name ?? $parameter->getName(); + /** @var mixed $variableValue */ + $variableValue = $route->getAttribute($variableName); + + if ($variableValue === null) { + if ($parameter->isDefaultValueAvailable()) { + return yield $parameter->getDefaultValue(); + } elseif ($parameter->allowsNull()) { + return yield; + } + + throw new LogicException(sprintf( + 'The parameter {%1$s} expects the value of the variable "%2$s" from the route "%3$s", ' . + 'which is missing in the request, most likely, because this variable is optional. ' . + 'To resolve this issue, make this parameter nullable or assign it a default value.', + ParameterResolutioner::stringifyParameter($parameter), + $variableName, + $route->getName(), + )); + } + + try { + yield $this->typeConversioner->castValue($variableValue, $parameter->getType()); + } catch (InvalidArgumentException $violation) { + throw new HttpNotFoundException(sprintf( + 'The request cannot be completed with an invalid "%s". %s', + $variableName, + $violation->getMessage(), + ), previous: $violation); + } + } +} diff --git a/src/ParameterResolver/RequestBodyParameterResolver.php b/src/ParameterResolving/ParameterResolver/RequestBodyParameterResolver.php similarity index 73% rename from src/ParameterResolver/RequestBodyParameterResolver.php rename to src/ParameterResolving/ParameterResolver/RequestBodyParameterResolver.php index 78081a2c..b558c861 100644 --- a/src/ParameterResolver/RequestBodyParameterResolver.php +++ b/src/ParameterResolving/ParameterResolver/RequestBodyParameterResolver.php @@ -11,17 +11,16 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ParameterResolver; +namespace Sunrise\Http\Router\ParameterResolving\ParameterResolver; use Generator; use Psr\Http\Message\ServerRequestInterface; use ReflectionNamedType; use ReflectionParameter; use Sunrise\Http\Router\Annotation\RequestBody; +use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\Exception\UnhydrableObjectException; -use Sunrise\Http\Router\Exception\UnprocessableRequestBodyException; -use Sunrise\Http\Router\ParameterResolutioner; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutioner; use Sunrise\Hydrator\Exception\InvalidDataException; use Sunrise\Hydrator\Exception\InvalidObjectException; use Sunrise\Hydrator\HydratorInterface; @@ -44,18 +43,18 @@ final class RequestBodyParameterResolver implements ParameterResolverInterface * Constructor of the class * * @param HydratorInterface $hydrator - * @param ValidatorInterface $validator + * @param ValidatorInterface|null $validator */ - public function __construct(private HydratorInterface $hydrator, private ValidatorInterface $validator) + public function __construct(private HydratorInterface $hydrator, private ?ValidatorInterface $validator = null) { } /** * @inheritDoc * - * @throws UnhydrableObjectException If an object isn't valid. - * @throws UnprocessableRequestBodyException If the request's parsed body isn't valid. - * @throws LogicException If the resolver is used incorrectly. + * @throws LogicException If the resolver is used incorrectly or if an object isn't valid. + * + * @throws HttpUnprocessableEntityException If the request's parsed body isn't valid. */ public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator { @@ -81,14 +80,16 @@ public function resolveParameter(ReflectionParameter $parameter, mixed $context) try { $object = $this->hydrator->hydrate($type->getName(), (array) $context->getParsedBody()); } catch (InvalidObjectException $e) { - throw new UnhydrableObjectException($e->getMessage(), 0, $e); + throw new LogicException($e->getMessage(), 0, $e); } catch (InvalidDataException $e) { - throw new UnprocessableRequestBodyException($e->getViolations()); + throw (new HttpUnprocessableEntityException)->addHydratorViolation(...$e->getExceptions()); } - $violations = $this->validator->validate($object); - if ($violations->count() > 0) { - throw new UnprocessableRequestBodyException($violations); + if (isset($this->validator)) { + $violations = $this->validator->validate($object); + if ($violations->count() > 0) { + throw (new HttpUnprocessableEntityException)->addValidatorViolation(...$violations); + } } yield $object; diff --git a/src/ParameterResolving/ParameterResolver/RequestHeaderParameterResolver.php b/src/ParameterResolving/ParameterResolver/RequestHeaderParameterResolver.php new file mode 100644 index 00000000..9341e222 --- /dev/null +++ b/src/ParameterResolving/ParameterResolver/RequestHeaderParameterResolver.php @@ -0,0 +1,100 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\ParameterResolving\ParameterResolver; + +use Generator; +use InvalidArgumentException; +use Psr\Http\Message\ServerRequestInterface; +use ReflectionAttribute; +use ReflectionParameter; +use Sunrise\Http\Router\Annotation\RequestHeader; +use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutioner; +use Sunrise\Http\Router\TypeConversion\TypeConversionerInterface; + +use function sprintf; + +/** + * RequestHeaderParameterResolver + * + * @since 3.0.0 + */ +final class RequestHeaderParameterResolver implements ParameterResolverInterface +{ + + /** + * Constructor of the class + * + * @param TypeConversionerInterface $typeConversioner + */ + public function __construct(private TypeConversionerInterface $typeConversioner) + { + } + + /** + * @inheritDoc + * + * @throws LogicException If the resolver is used incorrectly. + */ + public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator + { + /** @var list> $attributes */ + $attributes = $parameter->getAttributes(RequestHeader::class); + if ($attributes === []) { + return; + } + + if (!$parameter->hasType()) { + throw new LogicException(sprintf( + 'To use the #[RequestHeader] attribute, the parameter {%s} must be typed.', + ParameterResolutioner::stringifyParameter($parameter), + )); + } + + if (! $context instanceof ServerRequestInterface) { + throw new LogicException( + 'At this level of the application, any operations with the request are not possible.' + ); + } + + $header = $attributes[0]->newInstance(); + + if (!$context->hasHeader($header->name)) { + if ($parameter->isDefaultValueAvailable()) { + return yield $parameter->getDefaultValue(); + } elseif ($parameter->allowsNull()) { + return yield; + } + + throw new HttpBadRequestException(sprintf( + 'The HTTP header %s must be provided.', + $header->name, + )); + } + + try { + yield $this->typeConversioner->castValue( + $context->getHeaderLine($header->name), + $parameter->getType(), + ); + } catch (InvalidArgumentException $violation) { + throw new HttpBadRequestException(sprintf( + 'The value of the HTTP header %s is not valid. %s', + $header->name, + $violation->getMessage(), + ), previous: $violation); + } + } +} diff --git a/src/ParameterResolver/RequestQueryParameterResolver.php b/src/ParameterResolving/ParameterResolver/RequestQueryParameterResolver.php similarity index 72% rename from src/ParameterResolver/RequestQueryParameterResolver.php rename to src/ParameterResolving/ParameterResolver/RequestQueryParameterResolver.php index 9aea6c02..6274c282 100644 --- a/src/ParameterResolver/RequestQueryParameterResolver.php +++ b/src/ParameterResolving/ParameterResolver/RequestQueryParameterResolver.php @@ -11,17 +11,16 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ParameterResolver; +namespace Sunrise\Http\Router\ParameterResolving\ParameterResolver; use Generator; use Psr\Http\Message\ServerRequestInterface; use ReflectionNamedType; use ReflectionParameter; use Sunrise\Http\Router\Annotation\RequestQuery; +use Sunrise\Http\Router\Exception\Http\HttpUnprocessableEntityException; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\Exception\UnhydrableObjectException; -use Sunrise\Http\Router\Exception\UnprocessableRequestQueryException; -use Sunrise\Http\Router\ParameterResolutioner; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutioner; use Sunrise\Hydrator\Exception\InvalidDataException; use Sunrise\Hydrator\Exception\InvalidObjectException; use Sunrise\Hydrator\HydratorInterface; @@ -44,18 +43,18 @@ final class RequestQueryParameterResolver implements ParameterResolverInterface * Constructor of the class * * @param HydratorInterface $hydrator - * @param ValidatorInterface $validator + * @param ValidatorInterface|null $validator */ - public function __construct(private HydratorInterface $hydrator, private ValidatorInterface $validator) + public function __construct(private HydratorInterface $hydrator, private ?ValidatorInterface $validator = null) { } /** * @inheritDoc * - * @throws UnhydrableObjectException If an object isn't valid. - * @throws UnprocessableRequestQueryException If the request's query parameters isn't valid. - * @throws LogicException If the resolver is used incorrectly. + * @throws LogicException If the resolver is used incorrectly or if an object isn't valid. + * + * @throws HttpUnprocessableEntityException If the request's query parameters isn't valid. */ public function resolveParameter(ReflectionParameter $parameter, mixed $context): Generator { @@ -81,14 +80,16 @@ public function resolveParameter(ReflectionParameter $parameter, mixed $context) try { $object = $this->hydrator->hydrate($type->getName(), $context->getQueryParams()); } catch (InvalidObjectException $e) { - throw new UnhydrableObjectException($e->getMessage(), 0, $e); + throw new LogicException($e->getMessage(), 0, $e); } catch (InvalidDataException $e) { - throw new UnprocessableRequestQueryException($e->getViolations()); + throw (new HttpUnprocessableEntityException)->addHydratorViolation(...$e->getExceptions()); } - $violations = $this->validator->validate($object); - if ($violations->count() > 0) { - throw new UnprocessableRequestQueryException($violations); + if (isset($this->validator)) { + $violations = $this->validator->validate($object); + if ($violations->count() > 0) { + throw (new HttpUnprocessableEntityException)->addValidatorViolation(...$violations); + } } yield $object; diff --git a/src/ParameterResolver/RequestRouteParameterResolver.php b/src/ParameterResolving/ParameterResolver/RequestRouteParameterResolver.php similarity index 93% rename from src/ParameterResolver/RequestRouteParameterResolver.php rename to src/ParameterResolving/ParameterResolver/RequestRouteParameterResolver.php index 0d919b51..25c6283c 100644 --- a/src/ParameterResolver/RequestRouteParameterResolver.php +++ b/src/ParameterResolving/ParameterResolver/RequestRouteParameterResolver.php @@ -11,14 +11,14 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ParameterResolver; +namespace Sunrise\Http\Router\ParameterResolving\ParameterResolver; use Generator; use Psr\Http\Message\ServerRequestInterface; use ReflectionNamedType; use ReflectionParameter; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ParameterResolutioner; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutioner; use Sunrise\Http\Router\RouteInterface; use function sprintf; diff --git a/src/ReferenceResolver.php b/src/ReferenceResolver.php index 954e391a..50f77cab 100644 --- a/src/ReferenceResolver.php +++ b/src/ReferenceResolver.php @@ -19,7 +19,9 @@ use Psr\Http\Server\RequestHandlerInterface; use Sunrise\Http\Router\Exception\LogicException; use Sunrise\Http\Router\Middleware\CallbackMiddleware; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutionerInterface; use Sunrise\Http\Router\RequestHandler\CallbackRequestHandler; +use Sunrise\Http\Router\ResponseResolving\ResponseResolutionerInterface; use function class_exists; use function get_debug_type; @@ -75,7 +77,7 @@ public function __construct( /** * @inheritDoc * - * @throws LogicException If the reference cannot be resolved. + * @throws LogicException If the reference couldn't be resolved. */ public function resolveRequestHandler(mixed $reference): RequestHandlerInterface { @@ -132,7 +134,7 @@ public function resolveRequestHandler(mixed $reference): RequestHandlerInterface /** * @inheritDoc * - * @throws LogicException If the reference cannot be resolved. + * @throws LogicException If the reference couldn't be resolved. */ public function resolveMiddleware(mixed $reference): MiddlewareInterface { @@ -189,7 +191,7 @@ public function resolveMiddleware(mixed $reference): MiddlewareInterface /** * @inheritDoc * - * @throws LogicException If one of the references cannot be resolved. + * @throws LogicException If one of the references couldn't be resolved. */ public function resolveMiddlewares(array $references): Generator { diff --git a/src/RequestHandler/CallbackRequestHandler.php b/src/RequestHandler/CallbackRequestHandler.php index 3b52a405..960f258b 100644 --- a/src/RequestHandler/CallbackRequestHandler.php +++ b/src/RequestHandler/CallbackRequestHandler.php @@ -18,9 +18,9 @@ use Psr\Http\Server\RequestHandlerInterface; use ReflectionFunction; use ReflectionMethod; -use Sunrise\Http\Router\ParameterResolutionerInterface; -use Sunrise\Http\Router\ParameterResolver\PresetObjectParameterResolver; -use Sunrise\Http\Router\ResponseResolutionerInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutionerInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolver\ObjectInjectionParameterResolver; +use Sunrise\Http\Router\ResponseResolving\ResponseResolutionerInterface; use function Sunrise\Http\Router\reflect_callback; @@ -90,7 +90,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface $arguments = $this->parameterResolutioner ->withContext($request) ->withPriorityResolver( - new PresetObjectParameterResolver($request), + new ObjectInjectionParameterResolver($request), ) ->resolveParameters(...$source->getParameters()); diff --git a/src/RequestHandler/QueueableRequestHandler.php b/src/RequestHandler/QueueableRequestHandler.php index 83079a32..a3883af3 100644 --- a/src/RequestHandler/QueueableRequestHandler.php +++ b/src/RequestHandler/QueueableRequestHandler.php @@ -45,10 +45,10 @@ public function __construct(private RequestHandlerInterface $requestHandler, Mid */ public function handle(ServerRequestInterface $request): ResponseInterface { - if (! $this->isEmpty()) { - return ($clone = clone $this)->dequeue()->process($request, $clone); + if ($this->isEmpty()) { + return $this->requestHandler->handle($request); } - return $this->requestHandler->handle($request); + return ($clone = clone $this)->dequeue()->process($request, $clone); } } diff --git a/src/ResponseResolver/ExceptionResponseResolver.php b/src/ResponseResolver/ExceptionResponseResolver.php deleted file mode 100644 index bead398f..00000000 --- a/src/ResponseResolver/ExceptionResponseResolver.php +++ /dev/null @@ -1,96 +0,0 @@ - - * @copyright Copyright (c) 2018, Anatoly Nekhay - * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE - * @link https://github.com/sunrise-php/http-router - */ - -declare(strict_types=1); - -namespace Sunrise\Http\Router\ResponseResolver; - -use Psr\Http\Message\ResponseFactoryInterface; -use Psr\Http\Message\ResponseInterface; -use Psr\Http\Message\ServerRequestInterface; -use ReflectionFunction; -use ReflectionMethod; -use Sunrise\Http\Router\Entity\MediaType; -use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ServerRequest; -use Throwable; -use Whoops\Run as Whoops; - -use function class_exists; - -/** - * ExceptionResponseResolver - * - * @since 3.0.0 - * - * @link https://github.com/filp/whoops - */ -final class ExceptionResponseResolver implements ResponseResolverInterface -{ - - /** - * Constructor of the class - * - * @param ResponseFactoryInterface $responseFactory - * @param int<100, 599> $defaultResponseStatusCode - * - * @throws LogicException If the whoops package isn't installed. - */ - public function __construct( - private ResponseFactoryInterface $responseFactory, - private int $defaultResponseStatusCode = 500, - ) { - if (!class_exists(Whoops::class)) { - throw new LogicException( - 'The whoops package is required, run the `composer require filp/whoops` command to resolve it.' - ); - } - } - - /** - * @inheritDoc - */ - public function resolveResponse( - mixed $response, - ServerRequestInterface $request, - ReflectionMethod|ReflectionFunction $source, - ): ?ResponseInterface { - if (! $response instanceof Throwable) { - return null; - } - - $whoops = new Whoops(); - $whoops->allowQuit(false); - $whoops->writeToOutput(false); - - $clientPreferredMediaType = ServerRequest::from($request) - ->getClientPreferredMediaType( - MediaType::html(), - MediaType::json(), - MediaType::xml(), - ); - - if ($clientPreferredMediaType->equals(MediaType::html())) { - $whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler()); - } elseif ($clientPreferredMediaType->equals(MediaType::json())) { - $whoops->pushHandler(new \Whoops\Handler\JsonResponseHandler()); - } elseif ($clientPreferredMediaType->equals(MediaType::xml())) { - $whoops->pushHandler(new \Whoops\Handler\XmlResponseHandler()); - } - - $result = $this->responseFactory->createResponse($this->defaultResponseStatusCode) - ->withHeader('Content-Type', $clientPreferredMediaType->__toString()); - - $result->getBody()->write($whoops->handleException($response)); - - return $result; - } -} diff --git a/src/ResponseResolutioner.php b/src/ResponseResolving/ResponseResolutioner.php similarity index 93% rename from src/ResponseResolutioner.php rename to src/ResponseResolving/ResponseResolutioner.php index 6313adb0..54785f9c 100644 --- a/src/ResponseResolutioner.php +++ b/src/ResponseResolving/ResponseResolutioner.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Sunrise\Http\Router; +namespace Sunrise\Http\Router\ResponseResolving; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -21,7 +21,7 @@ use Sunrise\Http\Router\Annotation\ResponseHeader; use Sunrise\Http\Router\Annotation\ResponseStatus; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ResponseResolver\ResponseResolverInterface; +use Sunrise\Http\Router\ResponseResolving\ResponseResolver\ResponseResolverInterface; use function get_debug_type; use function sprintf; @@ -52,7 +52,7 @@ public function addResolver(ResponseResolverInterface ...$resolvers): void /** * @inheritDoc * - * @throws LogicException If the response cannot be resolved to PSR-7 response. + * @throws LogicException If the response couldn't be resolved to PSR-7 response. */ public function resolveResponse( mixed $response, @@ -71,7 +71,7 @@ public function resolveResponse( } throw new LogicException(sprintf( - 'Unable to resolve the response {%s->%s} to PSR-7 response', + 'Unable to resolve the response {%s->%s} to PSR-7 response.', self::stringifySource($source), get_debug_type($response), )); diff --git a/src/ResponseResolutionerInterface.php b/src/ResponseResolving/ResponseResolutionerInterface.php similarity index 90% rename from src/ResponseResolutionerInterface.php rename to src/ResponseResolving/ResponseResolutionerInterface.php index 78baeb1f..5061d287 100644 --- a/src/ResponseResolutionerInterface.php +++ b/src/ResponseResolving/ResponseResolutionerInterface.php @@ -11,13 +11,13 @@ declare(strict_types=1); -namespace Sunrise\Http\Router; +namespace Sunrise\Http\Router\ResponseResolving; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use ReflectionFunction; use ReflectionMethod; -use Sunrise\Http\Router\ResponseResolver\ResponseResolverInterface; +use Sunrise\Http\Router\ResponseResolving\ResponseResolver\ResponseResolverInterface; /** * ResponseResolutionerInterface diff --git a/src/ResponseResolver/EmptyResponseResolver.php b/src/ResponseResolving/ResponseResolver/EmptyResponseResolver.php similarity index 85% rename from src/ResponseResolver/EmptyResponseResolver.php rename to src/ResponseResolving/ResponseResolver/EmptyResponseResolver.php index 595c8e60..a5cdf81a 100644 --- a/src/ResponseResolver/EmptyResponseResolver.php +++ b/src/ResponseResolving/ResponseResolver/EmptyResponseResolver.php @@ -11,9 +11,8 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ResponseResolver; +namespace Sunrise\Http\Router\ResponseResolving\ResponseResolver; -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -46,7 +45,7 @@ public function resolveResponse( ReflectionFunction|ReflectionMethod $source, ) : ?ResponseInterface { if ($response === null) { - return $this->responseFactory->createResponse(StatusCodeInterface::STATUS_NO_CONTENT); + return $this->responseFactory->createResponse(204); } return null; diff --git a/src/ResponseResolver/JsonResponseBodyResponseResolver.php b/src/ResponseResolving/ResponseResolver/JsonResponseBodyResponseResolver.php similarity index 60% rename from src/ResponseResolver/JsonResponseBodyResponseResolver.php rename to src/ResponseResolving/ResponseResolver/JsonResponseBodyResponseResolver.php index a0144b91..b11fc0e0 100644 --- a/src/ResponseResolver/JsonResponseBodyResponseResolver.php +++ b/src/ResponseResolving/ResponseResolver/JsonResponseBodyResponseResolver.php @@ -11,9 +11,8 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ResponseResolver; +namespace Sunrise\Http\Router\ResponseResolving\ResponseResolver; -use Fig\Http\Message\StatusCodeInterface; use JsonException; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; @@ -23,16 +22,12 @@ use ReflectionMethod; use Sunrise\Http\Router\Annotation\JsonResponseBody; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ResponseResolutioner; +use Sunrise\Http\Router\ResponseResolving\ResponseResolutioner; -use function extension_loaded; use function json_encode; use function sprintf; -use const JSON_PRESERVE_ZERO_FRACTION; use const JSON_THROW_ON_ERROR; -use const JSON_UNESCAPED_SLASHES; -use const JSON_UNESCAPED_UNICODE; /** * JsonResponseBodyResponseResolver @@ -43,26 +38,17 @@ */ final class JsonResponseBodyResponseResolver implements ResponseResolverInterface { - public const DEFAULT_JSON_ENCODING_FLAGS = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_PRESERVE_ZERO_FRACTION; - public const DEFAULT_JSON_ENCODING_DEPTH = 512; /** * Constructor of the class * * @param ResponseFactoryInterface $responseFactory - * @param int|null $jsonEncodingFlags - * @param int|null $jsonEncodingDepth + * @param int $options */ public function __construct( private ResponseFactoryInterface $responseFactory, - private ?int $jsonEncodingFlags = null, - private ?int $jsonEncodingDepth = null, + private int $options = 0, ) { - if (!extension_loaded('json')) { - throw new LogicException( - 'The JSON extension is required, run the `pecl install json` command to resolve it.' - ); - } } /** @@ -81,22 +67,19 @@ public function resolveResponse( return null; } - $jsonResponseBody = $attributes[0]->newInstance(); - - $jsonEncodingFlags = $jsonResponseBody->jsonEncodingFlags ?? $this->jsonEncodingFlags ?? self::DEFAULT_JSON_ENCODING_FLAGS; - $jsonEncodingDepth = $jsonResponseBody->jsonEncodingDepth ?? $this->jsonEncodingDepth ?? self::DEFAULT_JSON_ENCODING_DEPTH; + $attribute = $attributes[0]->newInstance(); try { - $payload = json_encode($response, $jsonEncodingFlags | JSON_THROW_ON_ERROR, $jsonEncodingDepth); + $payload = json_encode($response, $this->options | $attribute->options | JSON_THROW_ON_ERROR); } catch (JsonException $e) { throw new LogicException(sprintf( 'Unable to encode a response from the source {%s} due to: %s', ResponseResolutioner::stringifySource($source), $e->getMessage(), - ), 0, $e); + ), previous: $e); } - $result = $this->responseFactory->createResponse(StatusCodeInterface::STATUS_OK) + $result = $this->responseFactory->createResponse(200) ->withHeader('Content-Type', 'application/json; charset=UTF-8'); $result->getBody()->write($payload); diff --git a/src/ResponseResolver/ResponseResolverInterface.php b/src/ResponseResolving/ResponseResolver/ResponseResolverInterface.php similarity index 93% rename from src/ResponseResolver/ResponseResolverInterface.php rename to src/ResponseResolving/ResponseResolver/ResponseResolverInterface.php index e6d28126..05597232 100644 --- a/src/ResponseResolver/ResponseResolverInterface.php +++ b/src/ResponseResolving/ResponseResolver/ResponseResolverInterface.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ResponseResolver; +namespace Sunrise\Http\Router\ResponseResolving\ResponseResolver; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; diff --git a/src/ResponseResolver/RouteResponseResolver.php b/src/ResponseResolving/ResponseResolver/RouteResponseResolver.php similarity index 93% rename from src/ResponseResolver/RouteResponseResolver.php rename to src/ResponseResolving/ResponseResolver/RouteResponseResolver.php index a0e4d0b3..1b2cfb37 100644 --- a/src/ResponseResolver/RouteResponseResolver.php +++ b/src/ResponseResolving/ResponseResolver/RouteResponseResolver.php @@ -11,7 +11,7 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ResponseResolver; +namespace Sunrise\Http\Router\ResponseResolving\ResponseResolver; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; diff --git a/src/ResponseResolver/StreamResponseResolver.php b/src/ResponseResolving/ResponseResolver/StreamResponseResolver.php similarity index 87% rename from src/ResponseResolver/StreamResponseResolver.php rename to src/ResponseResolving/ResponseResolver/StreamResponseResolver.php index a014f6b3..8462d4ec 100644 --- a/src/ResponseResolver/StreamResponseResolver.php +++ b/src/ResponseResolving/ResponseResolver/StreamResponseResolver.php @@ -11,9 +11,8 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ResponseResolver; +namespace Sunrise\Http\Router\ResponseResolving\ResponseResolver; -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -47,7 +46,7 @@ public function resolveResponse( ReflectionFunction|ReflectionMethod $source, ) : ?ResponseInterface { if ($response instanceof StreamInterface) { - return $this->responseFactory->createResponse(StatusCodeInterface::STATUS_OK) + return $this->responseFactory->createResponse(200) ->withBody($response); } diff --git a/src/ResponseResolver/UriResponseResolver.php b/src/ResponseResolving/ResponseResolver/UriResponseResolver.php similarity index 87% rename from src/ResponseResolver/UriResponseResolver.php rename to src/ResponseResolving/ResponseResolver/UriResponseResolver.php index 9f553445..9c2dae92 100644 --- a/src/ResponseResolver/UriResponseResolver.php +++ b/src/ResponseResolving/ResponseResolver/UriResponseResolver.php @@ -11,9 +11,8 @@ declare(strict_types=1); -namespace Sunrise\Http\Router\ResponseResolver; +namespace Sunrise\Http\Router\ResponseResolving\ResponseResolver; -use Fig\Http\Message\StatusCodeInterface; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; @@ -47,7 +46,7 @@ public function resolveResponse( ReflectionFunction|ReflectionMethod $source, ) : ?ResponseInterface { if ($response instanceof UriInterface) { - return $this->responseFactory->createResponse(StatusCodeInterface::STATUS_FOUND) + return $this->responseFactory->createResponse(302) ->withHeader('Location', $response->__toString()); } diff --git a/src/Route.php b/src/Route.php index 434be829..5663dde1 100644 --- a/src/Route.php +++ b/src/Route.php @@ -217,6 +217,14 @@ public function getAttributes(): array return $this->attributes; } + /** + * @inheritDoc + */ + public function getAttribute(string $name, mixed $default = null): mixed + { + return $this->attributes[$name] ?? $default; + } + /** * @inheritDoc */ diff --git a/src/RouteCollector.php b/src/RouteCollector.php index 2b77f784..1be0d8d8 100644 --- a/src/RouteCollector.php +++ b/src/RouteCollector.php @@ -15,8 +15,12 @@ use Fig\Http\Message\RequestMethodInterface; use Sunrise\Http\Router\Exception\LogicException; -use Sunrise\Http\Router\ParameterResolver\ParameterResolverInterface; -use Sunrise\Http\Router\ResponseResolver\ResponseResolverInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutioner; +use Sunrise\Http\Router\ParameterResolving\ParameterResolutionerInterface; +use Sunrise\Http\Router\ParameterResolving\ParameterResolver\ParameterResolverInterface; +use Sunrise\Http\Router\ResponseResolving\ResponseResolutioner; +use Sunrise\Http\Router\ResponseResolving\ResponseResolutionerInterface; +use Sunrise\Http\Router\ResponseResolving\ResponseResolver\ResponseResolverInterface; /** * RouteCollector @@ -110,7 +114,7 @@ public function addParameterResolver(ParameterResolverInterface ...$resolvers): if (!isset($this->parameterResolutioner)) { throw new LogicException( 'The route collector cannot accept parameter resolvers ' . - 'because a custom reference resolver was setted ' . + 'because a custom reference resolver was set ' . 'and a parameter resolutioner was not passed' ); } @@ -135,7 +139,7 @@ public function addResponseResolver(ResponseResolverInterface ...$resolvers): vo if (!isset($this->responseResolutioner)) { throw new LogicException( 'The route collector cannot accept response resolvers ' . - 'because a custom reference resolver was setted ' . + 'because a custom reference resolver was set ' . 'and a response resolutioner was not passed' ); } diff --git a/src/RouteInterface.php b/src/RouteInterface.php index 83d8c861..79543e64 100644 --- a/src/RouteInterface.php +++ b/src/RouteInterface.php @@ -105,6 +105,16 @@ public function getMiddlewares(): array; */ public function getAttributes(): array; + /** + * Gets the route attribute by its given name + * + * @param non-empty-string $name + * @param mixed $default + * + * @return mixed + */ + public function getAttribute(string $name, mixed $default = null): mixed; + /** * Gets the route summary * diff --git a/src/Router.php b/src/Router.php index 318fe458..6ec211c3 100644 --- a/src/Router.php +++ b/src/Router.php @@ -19,19 +19,15 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use Sunrise\Http\Message\ServerRequestProxy; use Sunrise\Http\Router\Event\RouteEvent; -use Sunrise\Http\Router\Exception\ClientNotConsumedMediaTypeException; -use Sunrise\Http\Router\Exception\ClientNotProducedMediaTypeException; -use Sunrise\Http\Router\Exception\MethodNotAllowedException; -use Sunrise\Http\Router\Exception\PageNotFoundException; +use Sunrise\Http\Router\Exception\Http\HttpMethodNotAllowedException; +use Sunrise\Http\Router\Exception\Http\HttpNotFoundException; +use Sunrise\Http\Router\Exception\Http\HttpUnsupportedMediaTypeException; use Sunrise\Http\Router\Loader\LoaderInterface; use Sunrise\Http\Router\RequestHandler\QueueableRequestHandler; use Sunrise\Http\Router\RequestHandler\CallableRequestHandler; use function array_keys; -use function Sunrise\Http\Router\path_build; -use function Sunrise\Http\Router\path_match; /** * Router @@ -229,17 +225,14 @@ public function generateUri(string $name, array $attributes = [], bool $strict = * * @return RouteInterface * - * @throws PageNotFoundException + * @throws HttpNotFoundException * If the request URI isn't served. * - * @throws MethodNotAllowedException + * @throws HttpMethodNotAllowedException * If the request method isn't allowed. * - * @throws ClientNotProducedMediaTypeException + * @throws HttpUnsupportedMediaTypeException * If the client not produces required media types. - * - * @throws ClientNotConsumedMediaTypeException - * If the client not consumed provided media types. */ public function match(ServerRequestInterface $request): RouteInterface { @@ -271,7 +264,7 @@ public function match(ServerRequestInterface $request): RouteInterface } if (!$request->clientProducesMediaType(...$route->getConsumesMediaTypes())) { - throw new ClientNotProducedMediaTypeException($route->getConsumesMediaTypes()); + throw new HttpUnsupportedMediaTypeException($route->getConsumesMediaTypes()); } /** @var array $attributes */ @@ -282,10 +275,10 @@ public function match(ServerRequestInterface $request): RouteInterface if (!empty($allowedMethods)) { $allowedMethods = array_keys($allowedMethods); - throw new MethodNotAllowedException($allowedMethods); + throw new HttpMethodNotAllowedException($allowedMethods); } - throw new PageNotFoundException('Page Not Found'); + throw new HttpNotFoundException(); } /** @@ -299,6 +292,8 @@ public function match(ServerRequestInterface $request): RouteInterface */ public function run(ServerRequestInterface $request): ResponseInterface { + // TODO: request event + // lazy resolving of the given request... $routing = new CallableRequestHandler( function (ServerRequestInterface $request): ResponseInterface { @@ -318,7 +313,11 @@ function (ServerRequestInterface $request): ResponseInterface { return $routing->handle($request); } - return (new QueueableRequestHandler($routing, ...$this->middlewares))->handle($request); + $response = (new QueueableRequestHandler($routing, ...$this->middlewares))->handle($request); + + // TODO: response event + + return $response; } /** diff --git a/src/ServerRequest.php b/src/ServerRequest.php index e4b81617..1c19f07d 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -17,15 +17,12 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; -use Sunrise\Http\Router\Entity\ClientRemoteAddress; use Sunrise\Http\Router\Entity\MediaType; use Sunrise\Http\Router\Exception\Http\HttpBadRequestException; use Sunrise\Http\Router\Exception\InvalidArgumentException; use Sunrise\Http\Router\Exception\LogicException; -use function preg_split; use function reset; -use function sprintf; use function usort; /** @@ -71,47 +68,6 @@ public function getOriginalRequest(): ServerRequestInterface return $this->request; } - /** - * Gets the client's remote address - * - * @param array $proxyChain - * - * @return ClientRemoteAddress - * - * @template TKey as non-empty-string Proxy address; e.g., 127.0.0.1 - * @template TValue as non-empty-string Trusted header; e.g., X-Forwarded-For, X-Real-IP, etc. - * - * @link https://www.rfc-editor.org/rfc/rfc3875#section-4.1.8 - * @link https://www.rfc-editor.org/rfc/rfc7239.html#section-5.2 - */ - public function getClientRemoteAddress(array $proxyChain = []): ClientRemoteAddress - { - $serverParams = $this->request->getServerParams(); - $clientRemoteAddress = $serverParams['REMOTE_ADDR'] ?? '::1'; - $clientRemoteAddressChain = [$clientRemoteAddress]; - - while (isset($proxyChain[$clientRemoteAddressChain[0]])) { - $trustedHeader = $proxyChain[$clientRemoteAddressChain[0]]; - unset($proxyChain[$clientRemoteAddressChain[0]]); - - $header = $this->request->getHeaderLine($trustedHeader); - $addresses = preg_split('/\s*,\s*/', $header, flags: \PREG_SPLIT_NO_EMPTY); - if ($addresses === []) { - break; - } - - $clientRemoteAddressChain = [...$addresses, ...$clientRemoteAddressChain]; - } - - $address = $clientRemoteAddressChain[0]; - unset($clientRemoteAddressChain[0]); - - /** @var list $proxies */ - $proxies = [...$clientRemoteAddressChain]; - - return new ClientRemoteAddress($address, $proxies); - } - /** * Gets a media type that the client produced * @@ -124,14 +80,14 @@ public function getClientProducedMediaType(): ?MediaType try { return parse_header_with_media_type($header)->current(); } catch (InvalidArgumentException $e) { - throw new HttpBadRequestException(sprintf('The Content-Type header is invalid: %s', $e->getMessage())); + throw new HttpBadRequestException('The Content-Type header is invalid.', previous: $e); } } /** * Gets media types that the client consumes * - * @return Generator + * @return Generator */ public function getClientConsumesMediaTypes(): Generator { @@ -140,10 +96,37 @@ public function getClientConsumesMediaTypes(): Generator try { yield from parse_header_with_media_type($header); } catch (InvalidArgumentException $e) { - throw new HttpBadRequestException(sprintf('The Accept header is invalid: %s', $e->getMessage())); + throw new HttpBadRequestException('The Accept header is invalid', previous: $e); } } + /** + * Checks if the client produces one of the given media types + * + * @param MediaType ...$serverConsumesMediaTypes + * + * @return bool + */ + public function clientProducesMediaType(MediaType ...$serverConsumesMediaTypes): bool + { + if ($serverConsumesMediaTypes === []) { + return true; + } + + $clientProducedMediaType = $this->getClientProducedMediaType(); + if ($clientProducedMediaType === null) { + return false; + } + + foreach ($serverConsumesMediaTypes as $serverConsumesMediaType) { + if ($clientProducedMediaType->equals($serverConsumesMediaType)) { + return true; + } + } + + return false; + } + /** * Gets the client's preferred media type * @@ -183,33 +166,6 @@ public function getClientPreferredMediaType(MediaType ...$serverProducesMediaTyp return reset($serverProducesMediaTypes); } - /** - * Checks if the client produces one of the given media types - * - * @param MediaType ...$serverConsumesMediaTypes - * - * @return bool - */ - public function clientProducesMediaType(MediaType ...$serverConsumesMediaTypes): bool - { - if ($serverConsumesMediaTypes === []) { - return true; - } - - $clientProducedMediaType = $this->getClientProducedMediaType(); - if ($clientProducedMediaType === null) { - return false; - } - - foreach ($serverConsumesMediaTypes as $serverConsumesMediaType) { - if ($clientProducedMediaType->equals($serverConsumesMediaType)) { - return true; - } - } - - return false; - } - /** * @inheritDoc */ diff --git a/src/TypeConversion/TypeConversioner.php b/src/TypeConversion/TypeConversioner.php new file mode 100644 index 00000000..8c559a8d --- /dev/null +++ b/src/TypeConversion/TypeConversioner.php @@ -0,0 +1,64 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\TypeConversion; + +use ReflectionType; +use Sunrise\Http\Router\Exception\LogicException; +use Sunrise\Http\Router\TypeConversion\TypeConverter\TypeConverterInterface; + +use function get_debug_type; +use function sprintf; + +/** + * @since 3.0.0 + */ +final class TypeConversioner implements TypeConversionerInterface +{ + + /** + * @var list + */ + private array $converters = []; + + /** + * @inheritDoc + */ + public function addConverter(TypeConverterInterface ...$converters): void + { + foreach ($converters as $converter) { + $this->converters[] = $converter; + } + } + + /** + * @inheritDoc + * + * @throws LogicException If the type isn't supported or cannot be applied to the value. + */ + public function castValue(mixed $value, ReflectionType $type): mixed + { + foreach ($this->converters as $converter) { + $result = $converter->castValue($value, $type); + if ($result->valid()) { + return $result->current(); + } + } + + throw new LogicException(sprintf( + 'Unable to convert the value {%s} to the type %s.', + get_debug_type($value), + (string) $type, + )); + } +} diff --git a/src/TypeConversion/TypeConversionerInterface.php b/src/TypeConversion/TypeConversionerInterface.php new file mode 100644 index 00000000..095f0fcd --- /dev/null +++ b/src/TypeConversion/TypeConversionerInterface.php @@ -0,0 +1,46 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\TypeConversion; + +use LogicException; +use ReflectionType; +use Sunrise\Http\Router\TypeConversion\TypeConverter\TypeConverterInterface; + +/** + * @since 3.0.0 + */ +interface TypeConversionerInterface +{ + + /** + * Adds the given type converter(s) to the conversioner + * + * @param TypeConverterInterface ...$converters + * + * @return void + */ + public function addConverter(TypeConverterInterface ...$converters): void; + + /** + * Trying to cast the given value to the given type + * + * @param mixed $value + * @param ReflectionType $type + * + * @return mixed + * + * @throws LogicException If the type isn't supported. + */ + public function castValue(mixed $value, ReflectionType $type): mixed; +} diff --git a/src/TypeConversion/TypeConverter/BackedEnumTypeConverter.php b/src/TypeConversion/TypeConverter/BackedEnumTypeConverter.php new file mode 100644 index 00000000..a762b1d5 --- /dev/null +++ b/src/TypeConversion/TypeConverter/BackedEnumTypeConverter.php @@ -0,0 +1,122 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\TypeConversion\TypeConverter; + +use BackedEnum; +use Generator; +use ReflectionEnum; +use ReflectionNamedType; +use ReflectionType; +use Sunrise\Http\Router\Exception\InvalidArgumentException; +use ValueError; + +use function filter_var; +use function is_int; +use function is_string; +use function is_subclass_of; +use function join; +use function sprintf; +use function trim; + +use const FILTER_NULL_ON_FAILURE; +use const FILTER_VALIDATE_INT; +use const PHP_VERSION_ID; + +/** + * @since 3.0.0 + */ +final class BackedEnumTypeConverter implements TypeConverterInterface +{ + + /** + * @inheritDoc + * + * @throws InvalidArgumentException If the value isn't valid. + */ + public function castValue(mixed $value, ReflectionType $type): Generator + { + if (PHP_VERSION_ID < 80100) { + return; + } + + if (! $type instanceof ReflectionNamedType) { + return; + } + + $enumName = $type->getName(); + + if (!is_subclass_of($enumName, BackedEnum::class)) { + return; + } + + /** @var ReflectionNamedType $enumType */ + $enumType = (new ReflectionEnum($enumName))->getBackingType(); + + /** @var 'int'|'string' $enumTypeName */ + $enumTypeName = $enumType->getName(); + + if (is_string($value)) { + // As part of the support for HTML forms and other untyped data sources, + // an instance of BackedEnum should not be created from an empty string, + // therefore, such values should be treated as NULL. + if (trim($value) === '') { + // phpcs:ignore Generic.Files.LineLength + return $type->allowsNull() ? yield : throw new InvalidArgumentException('This value must not be empty.'); + } + + if ($enumTypeName === 'int') { + // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L94 + // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L197 + $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE); + } + } + + if ($enumTypeName === 'int' && !is_int($value)) { + throw new InvalidArgumentException('This value must be of type integer.'); + } + if ($enumTypeName === 'string' && !is_string($value)) { + throw new InvalidArgumentException('This value must be of type string.'); + } + + /** @var int|string $value */ + + try { + yield $enumName::from($value); + } catch (ValueError $e) { + $choices = [...$this->getBackedEnumChoices($enumName)]; + + throw new InvalidArgumentException(sprintf( + 'This value must be one of: %s.', + join(', ', $choices) + ), previous: $e); + } + } + + /** + * Gets choices from the given backed enum + * + * @param class-string $enumName + * + * @return Generator + */ + private function getBackedEnumChoices(string $enumName): Generator + { + /** @var list $cases */ + $cases = $enumName::cases(); + + foreach ($cases as $case) { + yield $case->value; + } + } +} diff --git a/src/TypeConversion/TypeConverter/BoolTypeConverter.php b/src/TypeConversion/TypeConverter/BoolTypeConverter.php new file mode 100644 index 00000000..f13f5579 --- /dev/null +++ b/src/TypeConversion/TypeConverter/BoolTypeConverter.php @@ -0,0 +1,65 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\TypeConversion\TypeConverter; + +use Generator; +use ReflectionNamedType; +use ReflectionType; +use Sunrise\Http\Router\Exception\InvalidArgumentException; + +use function filter_var; +use function is_bool; +use function is_string; +use function trim; + +use const FILTER_NULL_ON_FAILURE; +use const FILTER_VALIDATE_BOOL; + +/** + * @since 3.0.0 + */ +final class BoolTypeConverter implements TypeConverterInterface +{ + + /** + * @inheritDoc + * + * @throws InvalidArgumentException If the value isn't valid. + */ + public function castValue(mixed $value, ReflectionType $type): Generator + { + if (! $type instanceof ReflectionNamedType || $type->getName() !== 'bool') { + return; + } + + if (is_string($value)) { + // As part of the support for HTML forms and other untyped data sources, + // an empty string should not be cast to a boolean type, + // therefore, such values should be treated as NULL. + if (trim($value) === '') { + // phpcs:ignore Generic.Files.LineLength + return $type->allowsNull() ? yield : throw new InvalidArgumentException('This value must not be empty.'); + } + + // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L273 + $value = filter_var($value, FILTER_VALIDATE_BOOL, FILTER_NULL_ON_FAILURE); + } + + if (!is_bool($value)) { + throw new InvalidArgumentException('This value must be of type boolean.'); + } + + yield $value; + } +} diff --git a/src/TypeConversion/TypeConverter/DateTimeTypeConverter.php b/src/TypeConversion/TypeConverter/DateTimeTypeConverter.php new file mode 100644 index 00000000..b849c1ed --- /dev/null +++ b/src/TypeConversion/TypeConverter/DateTimeTypeConverter.php @@ -0,0 +1,83 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\TypeConversion\TypeConverter; + +use DateTimeImmutable; +use DateTimeInterface; +use Exception; +use Generator; +use ReflectionNamedType; +use ReflectionType; +use Sunrise\Http\Router\Exception\InvalidArgumentException; + +use function ctype_digit; +use function is_a; +use function is_int; +use function is_string; +use function trim; + +/** + * @since 3.0.0 + */ +final class DateTimeTypeConverter implements TypeConverterInterface +{ + + /** + * @inheritDoc + * + * @throws InvalidArgumentException If the value isn't valid. + */ + public function castValue(mixed $value, ReflectionType $type): Generator + { + if (! $type instanceof ReflectionNamedType) { + return; + } + + $className = $type->getName(); + + if (!is_a($className, DateTimeInterface::class, true)) { + return; + } + + if (is_int($value)) { + $value = '@' . $value; + } + + if (!is_string($value)) { + throw new InvalidArgumentException('This value must be of a string or an integer type.'); + } + + // As part of the support for HTML forms and other untyped data sources, + // an instance of DateTime* should not be created from an empty string, + // therefore, such values should be treated as NULL. + if (trim($value) === '') { + return $type->allowsNull() ? yield : throw new InvalidArgumentException('This value must not be empty.'); + } + + if (ctype_digit($value)) { + $value = '@' . $value; + } + + // It is recommended to use only DateTimeImmutable... + if ($className === DateTimeInterface::class) { + $className = DateTimeImmutable::class; + } + + try { + yield new $className($value); + } catch (Exception $e) { + throw new InvalidArgumentException('This value is not a valid timestamp.', previous: $e); + } + } +} diff --git a/src/TypeConversion/TypeConverter/FloatTypeConverter.php b/src/TypeConversion/TypeConverter/FloatTypeConverter.php new file mode 100644 index 00000000..3ad75f68 --- /dev/null +++ b/src/TypeConversion/TypeConverter/FloatTypeConverter.php @@ -0,0 +1,70 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\TypeConversion\TypeConverter; + +use Generator; +use ReflectionNamedType; +use ReflectionType; +use Sunrise\Http\Router\Exception\InvalidArgumentException; + +use function filter_var; +use function is_float; +use function is_int; +use function is_string; +use function trim; + +use const FILTER_NULL_ON_FAILURE; +use const FILTER_VALIDATE_FLOAT; + +/** + * @since 3.0.0 + */ +final class FloatTypeConverter implements TypeConverterInterface +{ + + /** + * @inheritDoc + * + * @throws InvalidArgumentException If the value isn't valid. + */ + public function castValue(mixed $value, ReflectionType $type): Generator + { + if (! $type instanceof ReflectionNamedType || $type->getName() !== 'float') { + return; + } + + if (is_int($value)) { + return yield $value + .0; + } + + if (is_string($value)) { + // As part of the support for HTML forms and other untyped data sources, + // an empty string should not be cast to a number type, + // therefore, such values should be treated as NULL. + if (trim($value) === '') { + // phpcs:ignore Generic.Files.LineLength + return $type->allowsNull() ? yield : throw new InvalidArgumentException('This value must not be empty.'); + } + + // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L342 + $value = filter_var($value, FILTER_VALIDATE_FLOAT, FILTER_NULL_ON_FAILURE); + } + + if (!is_float($value)) { + throw new InvalidArgumentException('This value must be of type number.'); + } + + yield $value; + } +} diff --git a/src/TypeConversion/TypeConverter/IntTypeConverter.php b/src/TypeConversion/TypeConverter/IntTypeConverter.php new file mode 100644 index 00000000..9c266013 --- /dev/null +++ b/src/TypeConversion/TypeConverter/IntTypeConverter.php @@ -0,0 +1,66 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\TypeConversion\TypeConverter; + +use Generator; +use ReflectionNamedType; +use ReflectionType; +use Sunrise\Http\Router\Exception\InvalidArgumentException; + +use function filter_var; +use function is_int; +use function is_string; +use function trim; + +use const FILTER_NULL_ON_FAILURE; +use const FILTER_VALIDATE_INT; + +/** + * @since 3.0.0 + */ +final class IntTypeConverter implements TypeConverterInterface +{ + + /** + * @inheritDoc + * + * @throws InvalidArgumentException If the value isn't valid. + */ + public function castValue(mixed $value, ReflectionType $type): Generator + { + if (! $type instanceof ReflectionNamedType || $type->getName() !== 'int') { + return; + } + + if (is_string($value)) { + // As part of the support for HTML forms and other untyped data sources, + // an empty string should not be cast to an integer type, + // therefore, such values should be treated as NULL. + if (trim($value) === '') { + // phpcs:ignore Generic.Files.LineLength + return $type->allowsNull() ? yield : throw new InvalidArgumentException('This value must not be empty.'); + } + + // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L94 + // https://github.com/php/php-src/blob/b7d90f09d4a1688f2692f2fa9067d0a07f78cc7d/ext/filter/logical_filters.c#L197 + $value = filter_var($value, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE); + } + + if (!is_int($value)) { + throw new InvalidArgumentException('This value must be of type integer.'); + } + + yield $value; + } +} diff --git a/src/TypeConversion/TypeConverter/StringTypeConverter.php b/src/TypeConversion/TypeConverter/StringTypeConverter.php new file mode 100644 index 00000000..87bc1b9b --- /dev/null +++ b/src/TypeConversion/TypeConverter/StringTypeConverter.php @@ -0,0 +1,46 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\TypeConversion\TypeConverter; + +use Generator; +use ReflectionNamedType; +use ReflectionType; +use Sunrise\Http\Router\Exception\InvalidArgumentException; + +use function is_string; + +/** + * @since 3.0.0 + */ +final class StringTypeConverter implements TypeConverterInterface +{ + + /** + * @inheritDoc + * + * @throws InvalidArgumentException If the value isn't valid. + */ + public function castValue(mixed $value, ReflectionType $type): Generator + { + if (! $type instanceof ReflectionNamedType || $type->getName() !== 'string') { + return; + } + + if (!is_string($value)) { + throw new InvalidArgumentException('This value must be of type string.'); + } + + yield $value; + } +} diff --git a/src/TypeConversion/TypeConverter/TypeConverterInterface.php b/src/TypeConversion/TypeConverter/TypeConverterInterface.php new file mode 100644 index 00000000..77b2ca53 --- /dev/null +++ b/src/TypeConversion/TypeConverter/TypeConverterInterface.php @@ -0,0 +1,37 @@ + + * @copyright Copyright (c) 2018, Anatoly Nekhay + * @license https://github.com/sunrise-php/http-router/blob/master/LICENSE + * @link https://github.com/sunrise-php/http-router + */ + +declare(strict_types=1); + +namespace Sunrise\Http\Router\TypeConversion\TypeConverter; + +use Generator; +use InvalidArgumentException; +use ReflectionType; + +/** + * @since 3.0.0 + */ +interface TypeConverterInterface +{ + + /** + * Tries to cast the given value to the given type + * + * @param mixed $value + * @param ReflectionType $type + * + * @return Generator + * + * @throws InvalidArgumentException If the value isn't valid. + */ + public function castValue(mixed $value, ReflectionType $type): Generator; +} From 4739c77c106b1ac888f77cc1c85c5b62d00dc359 Mon Sep 17 00:00:00 2001 From: Anatoly Nekhay Date: Sun, 27 Aug 2023 20:03:15 +0200 Subject: [PATCH 082/180] v3 --- .../views/{error.phtml => error.html.php} | 4 +- src/Annotation/Commit.php | 33 ++++ src/Annotation/{Host.php => ProvideRoute.php} | 10 +- src/Annotation/RequestCookie.php | 33 ++++ src/Annotation/RequestEntity.php | 40 +++++ src/Annotation/Route.php | 2 - src/Command/RouteListCommand.php | 2 - src/Event/AbstractEvent.php | 48 ++++++ src/Event/ErrorEvent.php | 2 +- src/Event/ResponseResolvedEvent.php | 76 +++++++++ src/Event/RouteEvent.php | 8 +- src/EventListener/CommitEventListener.php | 75 +++++++++ src/EventListener/WhoopsEventListener.php | 99 ++++++++++++ .../Http/HttpInternalServerErrorException.php | 17 +- src/Helper/ViewLoader.php | 72 +++++++++ src/HostTable.php | 80 ---------- src/Loader/DescriptorLoader.php | 8 +- src/Middleware/CallbackMiddleware.php | 16 +- .../CrossOriginResourceSharingMiddleware.php | 95 ++++++++++++ src/Middleware/ErrorHandlingMiddleware.php | 32 +--- .../PathVariableParameterResolver.php | 9 +- .../ProvideRouteParameterResolver.php | 71 +++++++++ .../RequestCookieParameterResolver.php | 103 ++++++++++++ .../RequestEntityParameterResolver.php | 146 ++++++++++++++++++ .../RequestHeaderParameterResolver.php | 2 + src/ReferenceResolver.php | 4 +- src/RequestHandler/CallbackRequestHandler.php | 16 +- .../ResponseResolutioner.php | 39 +++-- .../ResponseResolutionerInterface.php | 8 +- .../EmptyResponseResolver.php | 4 +- .../JsonResponseBodyResponseResolver.php | 4 +- .../ResponseResolverInterface.php | 8 +- .../RouteResponseResolver.php | 4 +- .../StreamResponseResolver.php | 4 +- .../ResponseResolver/UriResponseResolver.php | 4 +- src/Route.php | 48 +----- src/RouteCollection.php | 54 +------ src/RouteCollectionInterface.php | 25 --- src/RouteInterface.php | 32 ---- src/Router.php | 56 +++---- src/RouterBuilder.php | 23 --- src/TypeConversion/TypeConversioner.php | 2 +- .../TypeConverter/BackedEnumTypeConverter.php | 4 +- .../TypeConverter/DateTimeTypeConverter.php | 2 +- tests/Exception/BadRequestExceptionTest.php | 78 ---------- tests/Exception/ExceptionTest.php | 96 ------------ .../InvalidArgumentExceptionTest.php | 24 --- .../InvalidAttributeValueExceptionTest.php | 24 --- .../InvalidLoaderResourceExceptionTest.php | 24 --- tests/Exception/InvalidPathExceptionTest.php | 24 --- .../MethodNotAllowedExceptionTest.php | 100 ------------ .../MissingAttributeValueExceptionTest.php | 24 --- tests/Exception/PageNotFoundExceptionTest.php | 24 --- .../Exception/RouteNotFoundExceptionTest.php | 24 --- .../UnresolvableReferenceExceptionTest.php | 24 --- .../UnsupportedMediaTypeExceptionTest.php | 103 ------------ 56 files changed, 1012 insertions(+), 981 deletions(-) rename resources/views/{error.phtml => error.html.php} (95%) create mode 100644 src/Annotation/Commit.php rename src/Annotation/{Host.php => ProvideRoute.php} (71%) create mode 100644 src/Annotation/RequestCookie.php create mode 100644 src/Annotation/RequestEntity.php create mode 100644 src/Event/AbstractEvent.php create mode 100644 src/Event/ResponseResolvedEvent.php create mode 100644 src/EventListener/CommitEventListener.php create mode 100644 src/EventListener/WhoopsEventListener.php create mode 100644 src/Helper/ViewLoader.php delete mode 100644 src/HostTable.php create mode 100644 src/Middleware/CrossOriginResourceSharingMiddleware.php create mode 100644 src/ParameterResolving/ParameterResolver/ProvideRouteParameterResolver.php create mode 100644 src/ParameterResolving/ParameterResolver/RequestCookieParameterResolver.php create mode 100644 src/ParameterResolving/ParameterResolver/RequestEntityParameterResolver.php delete mode 100644 tests/Exception/BadRequestExceptionTest.php delete mode 100644 tests/Exception/ExceptionTest.php delete mode 100644 tests/Exception/InvalidArgumentExceptionTest.php delete mode 100644 tests/Exception/InvalidAttributeValueExceptionTest.php delete mode 100644 tests/Exception/InvalidLoaderResourceExceptionTest.php delete mode 100644 tests/Exception/InvalidPathExceptionTest.php delete mode 100644 tests/Exception/MethodNotAllowedExceptionTest.php delete mode 100644 tests/Exception/MissingAttributeValueExceptionTest.php delete mode 100644 tests/Exception/PageNotFoundExceptionTest.php delete mode 100644 tests/Exception/RouteNotFoundExceptionTest.php delete mode 100644 tests/Exception/UnresolvableReferenceExceptionTest.php delete mode 100644 tests/Exception/UnsupportedMediaTypeExceptionTest.php diff --git a/resources/views/error.phtml b/resources/views/error.html.php similarity index 95% rename from resources/views/error.phtml rename to resources/views/error.html.php index 9e604094..2d436886 100644 --- a/resources/views/error.phtml +++ b/resources/views/error.html.php @@ -68,10 +68,10 @@

getReasonPhrase() ?>

-

getMessage() ?>

+

getMessage()) ?>

getViolations() as $violation): ?> -

source ?> message ?>

+

source ?> message) ?>