├── LICENSE ├── README.md ├── composer.json └── src ├── Exception └── JsonApiRequestException.php ├── Middleware ├── JsonApiDispatcherMiddleware.php ├── JsonApiExceptionHandlerMiddleware.php ├── JsonApiRequestValidatorMiddleware.php ├── JsonApiResponseValidatorMiddleware.php └── YinCompatibilityMiddleware.php └── Utils └── JsonApiMessageValidator.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Woohoo Labs. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Woohoo Labs. Yin Middleware 2 | 3 | [![Latest Version on Packagist][ico-version]][link-version] 4 | [![Software License][ico-license]](LICENSE) 5 | [![Build Status][ico-build]][link-build] 6 | [![Coverage Status][ico-coverage]][link-coverage] 7 | [![Quality Score][ico-code-quality]][link-code-quality] 8 | [![Total Downloads][ico-downloads]][link-downloads] 9 | [![Gitter][ico-support]][link-support] 10 | 11 | **Woohoo Labs. Yin Middleware is a collection of middleware which helps you to integrate 12 | [Woohoo Labs. Yin](https://github.com/woohoolabs/yin) into your PHP applications.** 13 | 14 | ## Table of Contents 15 | 16 | * [Introduction](#introduction) 17 | * [Install](#install) 18 | * [Basic Usage](#basic-usage) 19 | * [Versioning](#versioning) 20 | * [Change Log](#change-log) 21 | * [Testing](#testing) 22 | * [Contributing](#contributing) 23 | * [Support](#support) 24 | * [Credits](#credits) 25 | * [License](#license) 26 | 27 | ## Introduction 28 | 29 | ### Features 30 | 31 | - 100% [PSR-15](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-15-request-handlers.md) compatibility 32 | - 100% [PSR-7](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md) compatibility 33 | - Validation of requests against the JSON schema 34 | - Validation of responses against the JSON and JSON:API schema 35 | - Dispatching of JSON:API-aware controllers 36 | - JSON:API exception handling 37 | 38 | ## Install 39 | 40 | The only thing you need before getting started is [Composer](https://getcomposer.org). 41 | 42 | ### Install a PSR-7 implementation: 43 | 44 | Because Yin Middleware requires a PSR-7 implementation (a package which provides the `psr/http-message-implementation` virtual 45 | package), you must install one first. You may use [Zend Diactoros](https://github.com/zendframework/zend-diactoros) or 46 | any other library of your preference: 47 | 48 | ```bash 49 | $ composer require zendframework/zend-diactoros 50 | ``` 51 | 52 | ### Install Yin Middleware: 53 | 54 | To install the latest version of this library, run the command below: 55 | 56 | ```bash 57 | $ composer require woohoolabs/yin-middleware 58 | ``` 59 | 60 | > Note: The tests and examples won't be downloaded by default. You have to use `composer require woohoolabs/yin-middleware --prefer-source` 61 | or clone the repository if you need them. 62 | 63 | Yin Middleware 4.1 requires PHP 7.4 at least, but you may use Yin Middleware 4.0.0 for PHP 7.1. 64 | 65 | ### Install the optional dependencies: 66 | 67 | If you want to use `JsonApiRequestValidatorMiddleware` and `JsonApiResponseValidatorMiddleware` from the default middleware stack 68 | then you have to require the following dependencies too: 69 | 70 | ```bash 71 | $ composer require seld/jsonlint 72 | $ composer require justinrainbow/json-schema 73 | ``` 74 | 75 | ## Basic Usage 76 | 77 | ### Supported middleware interface design 78 | 79 | The interface design of Yin-Middleware is based on the [PSR-15](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-15-request-handlers.md) de-facto standard. 80 | That's why it is compatible with [Woohoo Labs. Harmony](https://github.com/woohoolabs/harmony), 81 | [Zend-Stratigility](https://github.com/zendframework/zend-stratigility/), [Zend-Expressive](https://github.com/zendframework/zend-expressive/) 82 | and many other frameworks. 83 | 84 | The following sections will guide you through how to use and configure the provided middleware. 85 | 86 | > Note: When passing a `ServerRequestInterface` instance to your middleware dispatcher, a 87 | `WoohooLabs\Yin\JsonApi\Request\JsonApiRequestInterface` instance must be used in fact (the `WoohooLabs\Yin\JsonApi\Request\JsonApiRequest` 88 | class possibly), otherwise the `JsonApiDispatcherMiddleware` and the `JsonApiExceptionHandlerMiddleware` will throw an 89 | exception. 90 | 91 | ### YinCompatibilityMiddleware 92 | 93 | This middleware facilitates the usage of Yin and Yin-Middleware in other frameworks. It does so by upgrading a basic PSR-7 94 | request object to `JsonApiRequest`, which is suitable for working with Yin. Please keep in mind, that this middleware should 95 | precede any other middleware that uses `JsonApiRequest` as `$request` parameter. 96 | 97 | ```php 98 | $harmony->addMiddleware(new YinCompatibilityMiddleware()); 99 | ``` 100 | 101 | Available configuration options for the middleware (they can be passed to the constructor): 102 | 103 | - `exceptionFactory`: The [ExceptionFactoryInterface](https://github.com/woohoolabs/yin/#exceptions) instance to be used 104 | - `deserializer`: The [DeserializerInterface](https://github.com/woohoolabs/yin/#custom-deserialization) instance to be used 105 | 106 | ### JsonApiRequestValidatorMiddleware 107 | 108 | The middleware is mainly useful in a development environment, and it is able to validate a 109 | PSR-7 request against the JSON and the JSON:API schema. Just add it to your 110 | application (the example is for [Woohoo Labs. Harmony](https://github.com/woohoolabs/harmony)): 111 | 112 | ```php 113 | $harmony->addMiddleware(new JsonApiRequestValidatorMiddleware()); 114 | ``` 115 | 116 | If validation fails, an exception containing the appropriate JSON:API errors will be thrown. If you want to customize 117 | the error messages or the response, provide an Exception Factory of your own. For other customizations, feel free to extend the class. 118 | 119 | Available configuration options for the middleware (they can be passed to the constructor): 120 | 121 | - `exceptionFactory`: The [ExceptionFactoryInterface](https://github.com/woohoolabs/yin/#exceptions) instance to be used 122 | - `includeOriginalMessageInResponse`: If true, the original request body will be included in the "meta" top-level member 123 | - `negotiate`: If true, the middleware performs content-negotiation as specified by the JSON:API spec. In this case, 124 | the "Content-Type" and the "Accept" header is checked. 125 | - `validateQueryParams`: If true, query parameters are validated against the JSON:API specification 126 | - `validateJsonBody`: If true, the request body gets validated against the JSON schema 127 | 128 | ### JsonApiResponseValidatorMiddleware 129 | 130 | The middleware is mainly useful in a development environment, and it is able to validate a PSR-7 response against the 131 | JSON and the JSON:API schema. Just add it to your application (the example is for [Woohoo Labs. Harmony](https://github.com/woohoolabs/harmony)): 132 | 133 | ```php 134 | $harmony->addMiddleware(new JsonApiResponseValidatorMiddleware()); 135 | ``` 136 | 137 | If validation fails, an exception containing the appropriate JSON:API errors will be thrown. If you want to customize 138 | the error messages or the response, provide an Exception Factory of your own. For other customizations, feel free to extend the class. 139 | 140 | Available configuration options for the middleware (they can be passed to the constructor): 141 | 142 | - `exceptionFactory`: The [ExceptionFactoryInterface](https://github.com/woohoolabs/yin/#exceptions) instance to be used 143 | - `serializer`: The [SerializerInterface](https://github.com/woohoolabs/yin/#custom-serialization) instance to be used 144 | - `includeOriginalMessageInResponse`: If true, the original response will be included in the "meta" top-level member 145 | - `validateJsonBody`: If true, the response body gets validated against the JSON schema 146 | - `validateJsonApiBody`: If true, the response is validated against the JSON:API schema 147 | 148 | ### JsonApiDispatcherMiddleware 149 | 150 | This middleware is able to dispatch JSON:API-aware controllers. Just add it to your application (the example is for 151 | [Woohoo Labs. Harmony](https://github.com/woohoolabs/harmony)): 152 | 153 | ```php 154 | $harmony->addMiddleware(new JsonApiDispatcherMiddleware()); 155 | ``` 156 | 157 | This middleware works exactly as [the one in Woohoo Labs. Harmony](https://github.com/woohoolabs/harmony#using-your-favourite-di-container-with-harmony), 158 | the only difference is that it dispatches controller actions with the following signature: 159 | 160 | ```php 161 | public function myAction(JsonApi $jsonApi): ResponseInterface; 162 | ``` 163 | 164 | instead of: 165 | 166 | ```php 167 | public function myAction(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface; 168 | ``` 169 | 170 | The difference is subtle, as the `$jsonApi` object contains a PSR-7 compatible request, and PSR-7 responses can also be 171 | created with it. Learn more from the documentation of [Woohoo Labs. Yin](https://github.com/woohoolabs/yin#jsonapi-class). 172 | 173 | Available configuration options for the middleware (they can be passed to the constructor): 174 | 175 | - `container`: A [PSR-11 compliant](https://www.php-fig.org/psr/psr-11/) container instance to be used to instantiate 176 | the controller 177 | - `exceptionFactory`: The [ExceptionFactoryInterface](https://github.com/woohoolabs/yin/#exceptions) instance to be 178 | used (e.g.: when dispatching fails) 179 | - `serializer`: The [SerializerInterface](https://github.com/woohoolabs/yin/#custom-serialization) instance to be used 180 | - `handlerAttribute`: The name of the request attribute which stores a dispatchable controller (it is usually 181 | provided by a router). 182 | 183 | ### JsonApiExceptionHandlerMiddleware 184 | 185 | It catches exceptions and responds with an appropriate JSON:API error response. 186 | 187 | Available configuration options for the middleware (they can be passed to the constructor): 188 | 189 | - `errorResponsePrototype`: In case of an error, this response object will be manipulated and returned 190 | - `catching`: If false, the middleware won't catch `JsonApiException`s 191 | - `verbose`: If true, additional meta information will be provided about the exception thrown 192 | - `exceptionFactory`: The [ExceptionFactoryInterface](https://github.com/woohoolabs/yin/#exceptions) instance to be used 193 | - `serializer`: The [SerializerInterface](https://github.com/woohoolabs/yin/#custom-serialization) instance to be used 194 | 195 | ## Versioning 196 | 197 | This library follows [SemVer v2.0.0](https://semver.org/). 198 | 199 | ## Change Log 200 | 201 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 202 | 203 | ## Testing 204 | 205 | Woohoo Labs. Yin Middleware has a PHPUnit test suite. To run the tests, run the following command from the project folder 206 | after you have copied phpunit.xml.dist to phpunit.xml: 207 | 208 | ``` bash 209 | $ phpunit 210 | ``` 211 | 212 | Additionally, you may run `docker-compose up` or `make test` in order to execute the tests. 213 | 214 | ## Contributing 215 | 216 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 217 | 218 | ## Support 219 | 220 | Please see [SUPPORT](SUPPORT.md) for details. 221 | 222 | ## Credits 223 | 224 | - [Máté Kocsis][link-author] 225 | - [All Contributors][link-contributors] 226 | 227 | ## License 228 | 229 | The MIT License (MIT). Please see the [License File](LICENSE) for more information. 230 | 231 | [ico-version]: https://img.shields.io/packagist/v/woohoolabs/yin-middleware.svg 232 | [ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg 233 | [ico-build]: https://img.shields.io/github/workflow/status/woohoolabs/yin-middleware/Continuous%20Integration 234 | [ico-coverage]: https://img.shields.io/codecov/c/github/woohoolabs/yin-middleware 235 | [ico-code-quality]: https://img.shields.io/scrutinizer/g/woohoolabs/yin-middleware.svg 236 | [ico-downloads]: https://img.shields.io/packagist/dt/woohoolabs/yin-middleware.svg 237 | [ico-support]: https://badges.gitter.im/woohoolabs/yin-middleware.svg 238 | 239 | [link-version]: https://packagist.org/packages/woohoolabs/yin-middleware 240 | [link-build]: https://github.com/woohoolabs/yin-middleware/actions 241 | [link-coverage]: https://codecov.io/gh/woohoolabs/yin-middleware 242 | [link-code-quality]: https://scrutinizer-ci.com/g/woohoolabs/yin-middleware 243 | [link-downloads]: https://packagist.org/packages/woohoolabs/yin-middleware 244 | [link-author]: https://github.com/kocsismate 245 | [link-contributors]: ../../contributors 246 | [link-support]: https://gitter.im/woohoolabs/yin-middleware?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge 247 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "woohoolabs/yin-middleware", 3 | "description": "Woohoo Labs. Yin Middleware", 4 | "type": "library", 5 | "keywords": ["Woohoo Labs.", "Yin", "Middleware", "JSON API", "PSR-7", "PSR-15"], 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Máté Kocsis", 10 | "email": "kocsismate@woohoolabs.com" 11 | } 12 | ], 13 | "support": { 14 | "issues": "https://github.com/woohoolabs/yin-middleware/issues", 15 | "source": "https://github.com/woohoolabs/yin-middleware" 16 | }, 17 | "require": { 18 | "php": "^7.4.0||^8.0.0", 19 | "psr/container": "^1.0.0", 20 | "psr/http-message-implementation": "^1.0.0", 21 | "psr/http-server-middleware": "^1.0.0", 22 | "woohoolabs/yin": "^4.0.0" 23 | }, 24 | "require-dev": { 25 | "justinrainbow/json-schema": "^5.2.0", 26 | "laminas/laminas-diactoros": "^2.0.0", 27 | "phpstan/phpstan": "^0.12.0", 28 | "phpstan/phpstan-phpunit": "^0.12.0", 29 | "phpstan/phpstan-strict-rules": "^0.12.0", 30 | "phpunit/phpunit": "^9.5.0", 31 | "seld/jsonlint": "^1.7.1", 32 | "squizlabs/php_codesniffer": "^3.5.0", 33 | "woohoolabs/coding-standard": "^2.2.0", 34 | "woohoolabs/releaser": "^1.2.0" 35 | }, 36 | "autoload": { 37 | "psr-4": { 38 | "WoohooLabs\\YinMiddleware\\": "src/" 39 | } 40 | }, 41 | "autoload-dev": { 42 | "psr-4": { 43 | "WoohooLabs\\YinMiddleware\\Tests\\": "tests/" 44 | } 45 | }, 46 | "replace": { 47 | "woohoolabs/yin-middlewares": "self.version" 48 | }, 49 | "scripts": { 50 | "test": "phpunit", 51 | "phpstan": "phpstan analyse --level 8 src tests", 52 | "phpcs": "phpcs", 53 | "phpcbf": "phpcbf" 54 | }, 55 | "config": { 56 | "sort-packages": true, 57 | "platform-check": false 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Exception/JsonApiRequestException.php: -------------------------------------------------------------------------------- 1 | container = $container; 38 | $this->exceptionFactory = $exceptionFactory ?? new DefaultExceptionFactory(); 39 | $this->serializer = $serializer ?? new JsonSerializer(); 40 | $this->handlerAttributeName = $handlerAttributeName; 41 | } 42 | 43 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 44 | { 45 | $callable = $request->getAttribute($this->handlerAttributeName); 46 | $jsonApiRequest = $this->getJsonApiRequest($request); 47 | 48 | $response = $handler->handle($jsonApiRequest); 49 | 50 | if ($callable === null) { 51 | throw $this->exceptionFactory->createResourceNotFoundException($jsonApiRequest); 52 | } 53 | 54 | $jsonApi = new JsonApi($jsonApiRequest, $response, $this->exceptionFactory, $this->serializer); 55 | 56 | if (is_array($callable) && is_string($callable[0])) { 57 | $object = $this->container !== null ? $this->container->get($callable[0]) : new $callable[0](); 58 | $response = $object->{$callable[1]}($jsonApi); 59 | } else { 60 | if ($this->container !== null && !is_callable($callable)) { 61 | $callable = $this->container->get($callable); 62 | } 63 | $response = $callable($jsonApi); 64 | } 65 | 66 | return $response; 67 | } 68 | 69 | protected function getJsonApiRequest(ServerRequestInterface $request): JsonApiRequestInterface 70 | { 71 | if ($request instanceof JsonApiRequestInterface) { 72 | return $request; 73 | } 74 | 75 | throw new JsonApiRequestException(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Middleware/JsonApiExceptionHandlerMiddleware.php: -------------------------------------------------------------------------------- 1 | errorResponsePrototype = $errorResponsePrototype; 37 | $this->isCatching = $catching; 38 | $this->verbose = $verbose; 39 | $this->exceptionFactory = $exceptionFactory ?? new DefaultExceptionFactory(); 40 | $this->serializer = $serializer ?? new JsonSerializer(); 41 | } 42 | 43 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 44 | { 45 | if ($this->isCatching === false) { 46 | return $handler->handle($request); 47 | } 48 | 49 | try { 50 | return $handler->handle($request); 51 | } catch (JsonApiExceptionInterface $exception) { 52 | return $this->handleJsonApiException($exception, $request); 53 | } 54 | } 55 | 56 | protected function handleJsonApiException(JsonApiExceptionInterface $exception, ServerRequestInterface $request): ResponseInterface 57 | { 58 | $jsonApiRequest = $this->getJsonApiRequest($request); 59 | $responder = $this->createResponder($jsonApiRequest); 60 | $additionalMeta = $this->getExceptionMeta($exception); 61 | 62 | return $responder->genericError($exception->getErrorDocument(), null, $additionalMeta); 63 | } 64 | 65 | /** 66 | * @return array 67 | */ 68 | protected function getExceptionMeta(Throwable $exception): array 69 | { 70 | if ($this->verbose === false) { 71 | return []; 72 | } 73 | 74 | return [ 75 | "code" => $exception->getCode(), 76 | "message" => $exception->getMessage(), 77 | "file" => $exception->getFile(), 78 | "line" => $exception->getLine(), 79 | "trace" => $exception->getTrace(), 80 | ]; 81 | } 82 | 83 | protected function createResponder(JsonApiRequestInterface $request): Responder 84 | { 85 | return new Responder($request, $this->errorResponsePrototype, $this->exceptionFactory, $this->serializer); 86 | } 87 | 88 | protected function getJsonApiRequest(ServerRequestInterface $request): JsonApiRequestInterface 89 | { 90 | if ($request instanceof JsonApiRequestInterface) { 91 | return $request; 92 | } 93 | 94 | throw new JsonApiRequestException(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Middleware/JsonApiRequestValidatorMiddleware.php: -------------------------------------------------------------------------------- 1 | negotiate = $negotiate; 32 | $this->validateQueryParams = $validateQueryParams; 33 | $this->validator = new RequestValidator($this->exceptionFactory, $this->includeOriginalMessageInResponse); 34 | } 35 | 36 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 37 | { 38 | $jsonApiRequest = $this->getJsonApiRequest($request); 39 | 40 | if ($this->negotiate) { 41 | $this->validator->negotiate($jsonApiRequest); 42 | } 43 | 44 | if ($this->validateQueryParams) { 45 | $this->validator->validateQueryParams($jsonApiRequest); 46 | } 47 | 48 | if ($this->validateJsonBody) { 49 | $this->validator->validateJsonBody($jsonApiRequest); 50 | } 51 | 52 | return $handler->handle($request); 53 | } 54 | 55 | protected function getJsonApiRequest(ServerRequestInterface $request): JsonApiRequestInterface 56 | { 57 | if ($request instanceof JsonApiRequestInterface) { 58 | return $request; 59 | } 60 | 61 | throw new JsonApiRequestException(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Middleware/JsonApiResponseValidatorMiddleware.php: -------------------------------------------------------------------------------- 1 | validator = new ResponseValidator( 31 | $serializer, 32 | $this->exceptionFactory, 33 | $this->includeOriginalMessageInResponse 34 | ); 35 | } 36 | 37 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 38 | { 39 | $response = $handler->handle($request); 40 | 41 | if ($this->validateJsonBody) { 42 | $this->validator->validateJsonBody($response); 43 | } 44 | 45 | if ($this->validateJsonApiBody) { 46 | $this->validator->validateJsonApiBody($response); 47 | } 48 | 49 | return $response; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Middleware/YinCompatibilityMiddleware.php: -------------------------------------------------------------------------------- 1 | exceptionFactory = $exceptionFactory ?? new DefaultExceptionFactory(); 28 | $this->deserializer = $deserializer ?? new JsonDeserializer(); 29 | } 30 | 31 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 32 | { 33 | if ($request instanceof JsonApiRequestInterface === false) { 34 | $request = $this->createJsonApiRequest($request); 35 | } 36 | 37 | return $handler->handle($request); 38 | } 39 | 40 | protected function createJsonApiRequest(ServerRequestInterface $request): JsonApiRequest 41 | { 42 | return new JsonApiRequest($request, $this->exceptionFactory, $this->deserializer); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Utils/JsonApiMessageValidator.php: -------------------------------------------------------------------------------- 1 | exceptionFactory = $exceptionFactory ?? new DefaultExceptionFactory(); 24 | $this->includeOriginalMessageInResponse = $includeOriginalMessageInResponse; 25 | $this->validateJsonBody = $validateJsonBody; 26 | $this->validateJsonApiBody = $validateJsonApiBody; 27 | } 28 | } 29 | --------------------------------------------------------------------------------