Content Negotiation

Basics

Content negotiation is a process between the client and server to determine how to best process a request and serve content back to the client. This negotiation is typically done via headers, where the client says "Here's the type of content I'd prefer (eg JSON, XML, etc)", and the server trying to accommodate the client's preferences. For example, the process can involve negotiating the following for requests and responses per the HTTP spec:

Just update aphiria.contentNegotiation.mediaTypeFormatters in config.php, and you'll be ready to go.

Setting up your content negotiator with default settings is trivial:

use Aphiria\ContentNegotiation\ContentNegotiator;

$contentNegotiator = new ContentNegotiator();

This will create a negotiator with JSON, XML, HTML, and plain text media type formatters.

If you'd like to customize things like media type formatters and supported languages, you can override the defaults:

use Aphiria\ContentNegotiation\AcceptCharsetEncodingMatcher;
use Aphiria\ContentNegotiation\AcceptLanguageMatcher;
use Aphiria\ContentNegotiation\ContentNegotiator;
use Aphiria\ContentNegotiation\MediaTypeFormatterMatcher;
use Aphiria\ContentNegotiation\MediaTypeFormatters\JsonMediaTypeFormatter;
use Aphiria\ContentNegotiation\MediaTypeFormatters\XmlMediaTypeFormatter;

// Register whatever media type formatters you support
$mediaTypeFormatters = [
    new JsonMediaTypeFormatter(),
    new XmlMediaTypeFormatter()
];
$contentNegotiator = new ContentNegotiator(
    $mediaTypeFormatters, 
    new MediaTypeFormatterMatcher($mediaTypeFormatters),
    new AcceptCharsetEncodingMatcher(),
    new AcceptLanguageMatcher(['en'])
);

Now you're ready to start negotiating.

Note: AcceptLanguageMatcher uses language tags from RFC 5646, and follows the lookup rules in RFC 4647 Section 3.4.

Negotiating Requests

There's nothing you have to do to negotiate requests - Aphiria handles it automatically.

Let's build off the previous example and negotiate a request manually. Let's assume the raw request looked something like this:

POST https://example.com/users HTTP/1.1
Content-Type: application/json; charset=utf-8
Content-Language: en-US
Encoding: UTF-8
Accept: application/json, text/xml
Accept-Language: en-US, en
Accept-Charset: utf-8, utf-16

{"id":123,"email":"foo@example.com"}

Here's how we'd negotiate and deserialize the request body:

use Aphiria\ContentNegotiation\NegotiatedBodyDeserializer;
use App\Users\User;

$bodyDeserializer = new NegotiatedBodyDeserializer($contentNegotiator);
// Assume the request was already instantiated
$user = $bodyDeserializer->readRequestBodyAs(User::class, $request);
echo $user->id; // 123
echo $user->email; // "foo@example.com"

Negotiating Responses

Aphiria also automatically negotiates your responses for you - there's nothing for you to do.

You can manually negotiate a response by inspecting the Accept, Accept-Charset, and Accept-Language headers. If those headers are missing, we default to using the first media type formatter that can write the response body.

Constructing a response with all the appropriate headers is a little involved when doing it manually, which is why Aphiria provides NegotiatedResponseFactory to handle it for you:

use Aphiria\ContentNegotiation\NegotiatedResponseFactory;
use Aphiria\Net\Http\HttpStatusCode;

$responseFactory = new NegotiatedResponseFactory($contentNegotiator);
// Assume $user is a POPO User object
$response = $responseFactory->createResponse($request, HttpStatusCode::Ok, rawBody: $user);

Our response will look something like the following:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Language: en-US
Content-Length: 36

{"id":123,"email":"foo@example.com"}

Negotiating Language

By default, ContentNegotiator uses AcceptLanguageMatcher to find the best language to respond in from the Accept-Language header. However, if your locale is, for example, set as a query string parameter, you can use a custom language matcher and inject it into your ContentNegotiator.

use Aphiria\ContentNegotiation\ILanguageMatcher;
use Aphiria\Net\Http\Formatting\RequestParser;
use Aphiria\Net\Http\IRequest;

final class QueryStringLanguageMatcher implements ILanguageMatcher
{
    public function __construct(private RequestParser $requestParser) {}

    public function getBestLanguageMatch(IRequest $request): ?string
    {
        $queryStringVars = $this->requestParser->parseQueryString($request);
        $bestLanguage = null;
        
        if ($queryStringVars->tryGet('locale', $bestLanguage)) {
            return $bestLanguage;
        }

        return null;
    }
}

Then, set aphiria.contentNegotiation.languageMatcher to QueryStringLanguageMatcher::class in config.php.

Pass your language matcher into ContentNegotiator.

use Aphiria\ContentNegotiation\ContentNegotiator;

$languageMatcher = new QueryStringLanguageMatcher(new RequestParser());
$contentNegotiator = new ContentNegotiator(
    // ...
    languageMatcher: $languageMatcher
);

Media Type Formatters

Media type formatters can read and write a particular data format to a stream. Aphiria provides the following formatters out of the box:

Note: HtmlMediaTypeFormatter and PlainTextMediaTypeFormatter only handle strings - they do not deal with objects or arrays.

These are configured under aphiria.contentNegotiation.mediaTypeFormatters in config.php.

Customizing (De)Serialization

Under the hood, JsonMediaTypeFormatter and XmlMediaTypeFormatter use Symfony's serialization component to (de)serialize values. Aphiria provides a binder and some config settings in config.php under aphiria.serialization to help you get started. For more in-depth tutorials on how to customize Symfony's serializer, refer to its documentation.