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:

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

Setting up your content negotiator is straightforward:

use Aphiria\Net\Http\ContentNegotiation\ContentNegotiator;
use Aphiria\Net\Http\ContentNegotiation\MediaTypeFormatters\FormUrlEncodedMediaTypeFormatter;
use Aphiria\Net\Http\ContentNegotiation\MediaTypeFormatters\JsonMediaTypeFormatter;

// Register whatever media type formatters you support
$mediaTypeFormatters = [
    new JsonMediaTypeFormatter(),
    new FormUrlEncodedMediaTypeFormatter()
];
$supportedLanguages = ['en-US'];
$contentNegotiator = new ContentNegotiator($mediaTypeFormatters, $supportedLanguages);

Now you're ready to start negotiating.

Negotiating Requests

Let's build off of the previous example and negotiate a request. 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 the request content:

use App\Users\User;

// Assume the request was already instantiated
$result = $contentNegotiator->negotiateRequestContent(User::class, $request);

// The properties of a content negotiation result:
$mediaTypeFormatter = $result->getFormatter(); // An instance of JsonMediaTypeFormatter
$mediaType = $result->getMediaType(); // "application/json"
$encoding = $result->getEncoding(); // "utf-8"
$language = $result->getLanguage(); // "en-us"

Note: Any of these properties could be null if they could not be negotiated.

With these results, we know everything we need to try to deserialize the request body:

$user = $mediaTypeFormatter->readFromStream($request->getBody()->readAsStream(), User::class);
echo $user->getId(); // 123
echo $user->getEmail(); // "foo@example.com"

Negotiating Responses

We negotiate the response content 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\Net\Http\ContentNegotiation\NegotiatedResponseFactory;

$responseFactory = new NegotiatedResponseFactory($contentNegotiator);
// Assume $user is a POPO User object
$response = $responseFactory->createResponse($request, 200, null, $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"}

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->getFormatter();
$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:

Under the hood, FormUrlEncodedMediaTypeFormatter and JsonMediaTypeFormatter use Aphiria's serialization library to (de)serialize values. HtmlMediaTypeFormatter and PlainTextMediaTypeFormatter only handle strings - they do not deal with objects or arrays.