lcobucci/content-negotiation-middleware

A PSR-15 middleware to handle content negotiation

3.2.0 2023-11-22 21:33 UTC

README

Total Downloads Latest Stable Version Unstable Version

Build Status Code Coverage

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 configure jms/serializer)
  • Plates (requires you to also install and configure league/plates)
  • Twig (requires you to also install and configure twig/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.