willdurand/negotiation
HTTP content negotiation library for PHP. Parses Accept* headers to match the best media type, language, charset, or encoding, with flexible matchers and prioritization. Handy for APIs and middleware to select response formats based on client preferences.
Begin by installing the package via Composer:
composer require willdurand/negotiation
The primary entry points are RequestHeaderNegotiator, AcceptHeaderNegotiator, LanguageNegotiator, CharsetNegotiator, and EncodingNegotiator. Start with AcceptHeaderNegotiator to handle Accept headers for content-type negotiation:
use Negotiation\AcceptHeaderNegotiator;
$negotiator = new AcceptHeaderNegotiator();
// Server-supported formats (in order of preference)
$preferred = $negotiator->getBest('application/json', 'text/html');
// or
$preferred = $negotiator->getBest(['application/json', 'text/html']);
The getBest() method returns a HeaderInterface instance (e.g., AcceptHeader) representing the best match, or null if none found. Use getValue() to extract the matched type:
if ($preferred) {
$contentType = $preferred->getValue(); // e.g., "application/json"
}
Check the README for usage examples and examples/ directory for quick demos.
Accept:$negotiator = new AcceptHeaderNegotiator();
$serverFormats = ['application/json', 'application/xml', 'text/html'];
$best = $negotiator->getBest($serverFormats);
if (!$best) {
return new Response('Not Acceptable', 406);
}
$response = match ($best->getValue()) {
'application/json' => $this->renderJson($data),
'application/xml' => $this->renderXml($data),
default => $this->renderHtml($data),
};
$languageNegotiator = new LanguageNegotiator();
$locales = ['en-US', 'fr-FR', 'es-ES'];
$bestLanguage = $languageNegotiator->getBest($locales);
if ($bestLanguage) {
$locale = $bestLanguage->getValue(); // e.g., "fr-FR"
// Set locale, fetch translated content, etc.
}
$app->add(function ($request, $handler) {
$negotiator = new AcceptHeaderNegotiator();
$supported = ['application/json', 'text/html'];
$best = $negotiator->getBest(...$supported);
$response = $handler->handle($request);
if ($best) {
$response = $response->withHeader('Content-Type', $best->getValue());
}
return $response;
});
$serverFormats = ['application/json', 'text/html', '*/*'];
$best = $negotiator->getBest(...$serverFormats);
Use getBestMatch() instead of getBest() when you want the full HeaderInterface, including q value for logging or debugging.
Q-value Ignoring: getBest() ignores q=0 (explicit reject). If a client sends Accept: text/html;q=0, application/json, application/json will be selected — be sure your defaults align with expectations.
Wildcard Matching is Conservative: */* matches any, but text/* won’t match application/json. Prefer explicit types for security-sensitive or format-critical endpoints.
Case Sensitivity: MIME types and languages are not normalized — Text/HTML and text/html may not match as expected. Normalize input before negotiation if required.
Null handling: getBest() returns null on no match. Always check for null before calling getValue() — return 406 Not Acceptable when no match.
Testing: The library is easily mockable — inject AcceptHeaderNegotiatorInterface in production and use MockObject in tests to assert negotiation behavior.
Version Stability: Last release in 2022, but stable. No active development — suitable for stable APIs. Check for forks (e.g.,symfony/serializer may use it internally) for newer forks or alternatives.
Framework Integration: While framework-agnostic, many frameworks (e.g., Symfony via Negotiation component or Laravel via Accept middleware packages) abstract this — evaluate if reinvention is needed.
How can I help you explore Laravel packages today?