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:

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

If you're using the skeleton app, you don't have to worry about negotiating requests - it's done for you automatically. If you're not using it, then let's build off of 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

If you're using the skeleton app, then negotiating a response is done for you automatically. If you're not, though, 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, 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. You can get the media type formatter from ContentNegotiationResult, and use it to deserialize a request body to a particular type (User in this example):

$mediaTypeFormatter = $result->formatter;
$mediaTypeFormatter->readFromStream($request->getBody(), User::class);

Similarly, you can serialize a value and write it to the response body:

$mediaTypeFormatter->writeToStream($valueToWrite, $response->getBody());

Aphiria provides the following formatters out of the box:

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

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.