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
If you're using the skeleton app, just update aphiria.contentNegotiation.mediaTypeFormatters
in config.php, and you'll be ready to go.
If you're not using the skeleton app, 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.
If you're using the skeleton app, you don't have to worry about negotiating requests - it's done for you automatically, and you can skip this section. If you're not using it, then 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"
If you're using the skeleton app, then negotiating a response is done for you automatically, and you can skip this section. 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"}
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;
}
}
If you're using the skeleton app, set aphiria.contentNegotiation.languageMatcher
to QueryStringLanguageMatcher::class
in config.php.
If you're not using the skeleton app, pass your language matcher into ContentNegotiator
.
use Aphiria\ContentNegotiation\ContentNegotiator;
$languageMatcher = new QueryStringLanguageMatcher(new RequestParser());
$contentNegotiator = new ContentNegotiator(
// ...
languageMatcher: $languageMatcher
);
Media type formatters can read and write a particular data format to a stream. 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.
If you're using the skeleton app, these are configured under aphiria.contentNegotiation.mediaTypeFormatters
in config.php.
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.