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:
- Content type
- Controlled by the
Content-Type
header for requests, and theAccept
header for responses - Dictates the media type formatter to use
- Controlled by the
- Character encoding
- Controlled by the
Content-Type
header for requests, and theAccept-Charset
header for responses
- Controlled by the
- Language
- Controlled by the
Content-Language
header for requests, and theAccept-Language
header for responses
- Controlled by the
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->body, User::class);
Similarly, you can serialize a value and write it to the response body:
$mediaTypeFormatter->writeToStream($valueToWrite, $response->body);
Aphiria provides the following formatters out of the box:
HtmlMediaTypeFormatter
JsonMediaTypeFormatter
PlainTextMediaTypeFormatter
XmlMediaTypeFormatter
Note:
HtmlMediaTypeFormatter
andPlainTextMediaTypeFormatter
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.