lcobucci / content-negotiation-middleware
A PSR-15 middleware to handle content negotiation
Installs: 248 556
Dependents: 5
Suggesters: 1
Security: 0
Stars: 47
Watchers: 6
Forks: 2
Open Issues: 1
Requires
- php: ~8.1.0 || ~8.2.0 || ~8.3.0
- ext-json: *
- fig/http-message-util: ^1.1.5
- psr/http-factory: ^1.0.2
- psr/http-message: ^1.1 || ^2.0
- psr/http-server-middleware: ^1.0.2
Requires (Dev)
- infection/infection: ^0.27
- jms/serializer: ^3.26.0
- laminas/laminas-diactoros: ^3.0.0
- lcobucci/coding-standard: ^11.0
- league/plates: ^3.5.0
- middlewares/negotiation: ^2.1
- phpstan/extension-installer: ^1.3.1
- phpstan/phpstan: ^1.10.23
- phpstan/phpstan-deprecation-rules: ^1.1.3
- phpstan/phpstan-phpunit: ^1.3.13
- phpstan/phpstan-strict-rules: ^1.5.1
- phpunit/phpunit: ^10.4
- twig/twig: ^3.6.1
Suggests
- jms/serializer: For content formatting using a more flexible serializer
- laminas/laminas-diactoros: For concrete implementation of PSR-7
- league/plates: For content formatting using Plates as template engine
- middlewares/negotiation: For acceptable format identification
- twig/twig: For content formatting using Twig as template engine
README
Motivation
Packages like middlewares/negotiation
do a very good job to detect the correct
content type based on the Accept
header (or extension in the URI), however they
delegate to the RequestHandler
to format the content according to the detected
mime type.
That works fine for most cases but it usually creates a lot of duplication in
complex software, where every single RequestHandler
should do that formatting
(or depend on some component to do that). That logic should also be added to the
middleware that handles exceptions and converts them to the appropriated HTTP
response.
The goal of this middleware is to provide full content negotiation (detection and formatting).
Installation
This package is available on Packagist, and we recommend you to install it using Composer:
composer require lcobucci/content-negotiation-middleware middlewares/negotiation laminas/laminas-diactoros
Adventure mode
If you're ready for an adventure and don't want to use middlewares/negotiation
to handle the detection or laminas/diactoros
to create the response
body (StreamInterface
implementation), don't despair! You'll only have to use
the normal ContentTypeMiddleware::__construct()
instead of
ContentTypeMiddleware::fromRecommendedSettings()
.
We do have a small preference for the mentioned packages and didn't want to reinvent the wheel... but you know, it's a free world.
PHP Configuration
In order to make sure that other components are returning the expected objects we decided
to use assert()
, which is a very interesting feature in PHP but not often used.
The nice thing about assert()
is that we can (and should) disable it in production mode
so that we don't have useless statements.
So, for production mode, we recommend you to set zend.assertions
to -1
in your php.ini
.
For development, you should leave zend.assertions
as 1
and set assert.exception
to 1
, which
will make PHP throw an AssertionError
when things go wrong.
Check the documentation for more information: https://secure.php.net/manual/en/function.assert.php
Usage
Your very first step is to create the middleware using the correct configuration:
<?php declare(strict_types=1); use Lcobucci\ContentNegotiation\ContentTypeMiddleware; use Lcobucci\ContentNegotiation\Formatter\Json; use Lcobucci\ContentNegotiation\Formatter\StringCast; use Laminas\Diactoros\StreamFactory; $middleware = ContentTypeMiddleware::fromRecommendedSettings( // First argument is the list of formats you want to support: [ 'json', // You may also specify the full configuration of the format. // That's handy if you need to add extensions or mime-types: 'html' => [ 'extension' => ['html', 'htm', 'php'], 'mime-type' => ['text/html', 'application/xhtml+xml'], 'charset' => true, ], ], // It's very important to mention that: // // * the first format will be used as fallback (no acceptable mime type // found) // * the order of elements does matter // * the first element of `mime-type` list will be used as negotiated type // The second argument is the list of formatters that will be used for // each mime type: [ 'application/json' => new Json(), 'text/html' => new StringCast(), ], // The last argument is any implementation for the StreamFactoryInterface (PSR-17) new StreamFactory() );
Then you must add the middleware to very beginning of your pipeline, which will depend on the library/framework you're using, but it will be something similar to this:
<?php // ... $application->pipe($middleware);
Finally, you just need to use UnformattedResponse
as return of the request handlers you create to trigger to formatting when needed:
<?php declare(strict_types=1); namespace Me\MyApp; use Fig\Http\Message\StatusCodeInterface; use Lcobucci\ContentNegotiation\UnformattedResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\RequestHandlerInterface; use Laminas\Diactoros\Response; final class MyHandler implements RequestHandlerInterface { public function handle(ServerRequestInterface $request): ResponseInterface { // Does the necessary process and creates `$result` with the unformatted // content. return new UnformattedResponse( (new Response())->withStatus(StatusCodeInterface::STATUS_CREATED), $result ); } }
Formatters
We provide some basic formatters by default:
Json
StringCast
JmsSerializer
(requires you to also install and configurejms/serializer
)Plates
(requires you to also install and configureleague/plates
)Twig
(requires you to also install and configuretwig/twig
)
If you want to create a customised formatter the only thing needed is to
implement the Formatter
interface:
<?php declare(strict_types=1); namespace Me\MyApp; use Lcobucci\ContentNegotiation\Formatter; use Lcobucci\ContentNegotiation\UnformattedResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamFactoryInterface; final class MyFancyFormatter implements Formatter { public function format(UnformattedResponse $response, StreamFactoryInterface $streamFactory): ResponseInterface { $content = ''; // Do some fancy formatting of $response->getUnformattedContent() and put into $content return $response->withBody($streamFactory->createStream($content)); } }
License
MIT, see LICENSE.