A small, dependency-light content negotiation library for PSR-7 requests.
It picks the best match between what the client asks for and what your application supports:
Accept→ which media type to respond withAccept-Language→ which locale to respond inContent-Type→ whether the request body can be parsed
Each negotiator returns a NegotiatedValue (the matched value plus its header attributes such as q or charset),
or null when nothing matches. Optional PSR-15 middlewares turn a failed negotiation into a 406 Not Acceptable
or 415 Unsupported Media Type response and expose the negotiated value as a request attribute.
- php: ^8.3
- psr/http-message: ^1.1|^2.0
- chubbyphp/chubbyphp-container: ^2.5.2 (for
NegotiationServiceFactory) - chubbyphp/chubbyphp-http-exception: ^1.3.4 (required by the middlewares)
- chubbyphp/chubbyphp-laminas-config-factory: ^1.5.3 (for the laminas-style
ServiceFactoryclasses) - pimple/pimple: ^3.6.2 (for
NegotiationServiceProvider) - psr/http-server-middleware: ^1.0.2 (required by the middlewares)
Through Composer as chubbyphp/chubbyphp-negotiation.
composer require chubbyphp/chubbyphp-negotiation "^2.3"All negotiators share the same contract: pass the supported values to the constructor, call negotiate($request)
and receive a NegotiatedValueInterface (value + header attributes) or null when nothing matches.
The middlewares wrap a negotiator, store the result as a request attribute and throw an HttpException on failure.
Each section below shows the minimal call; the linked page documents the matching rules, edge cases and error data.
Negotiates Accept-Language. Exact locale first, then the language of a regional locale (en-US → en), then *.
Full documentation
$negotiator = new AcceptLanguageNegotiator(['en', 'de']);
$value = $negotiator->negotiate($request); // 'Accept-Language: de,en-US;q=0.7,en;q=0.3'
$value->getValue(); // 'de'
$value->getAttributes(); // ['q' => '1.0']Stores the negotiated locale in the request attribute acceptLanguage, or throws 406 Not Acceptable.
Full documentation
$middleware = new AcceptLanguageMiddleware(new AcceptLanguageNegotiator(['en', 'de']));
$response = $middleware->process($request, $handler); // $request->getAttribute('acceptLanguage') inside $handlerNegotiates Accept. Exact media type first, then structured suffix (+json), then type/*, then */*.
Full documentation
$negotiator = new AcceptNegotiator(['application/json', 'application/xml', 'application/x-yaml']);
$value = $negotiator->negotiate($request); // 'Accept: text/html,application/xml;q=0.9,*/*;q=0.8'
$value->getValue(); // 'application/xml'
$value->getAttributes(); // ['q' => '0.9']Stores the negotiated media type in the request attribute accept, or throws 406 Not Acceptable.
Full documentation
$middleware = new AcceptMiddleware(new AcceptNegotiator(['application/json', 'application/xml']));
$response = $middleware->process($request, $handler); // $request->getAttribute('accept') inside $handlerNegotiates Content-Type. Exact media type first, then structured suffix (application/vnd.api+json → application/json).
Header parameters such as charset are returned as attributes.
Full documentation
$negotiator = new ContentTypeNegotiator(['application/json', 'application/xml', 'application/x-yaml']);
$value = $negotiator->negotiate($request); // 'Content-Type: application/xml; charset=UTF-8'
$value->getValue(); // 'application/xml'
$value->getAttributes(); // ['charset' => 'UTF-8']Stores the negotiated media type in the request attribute contentType, or throws 415 Unsupported Media Type.
Full documentation
$middleware = new ContentTypeMiddleware(new ContentTypeNegotiator(['application/json', 'application/xml']));
$response = $middleware->process($request, $handler); // $request->getAttribute('contentType') inside $handlerRegisters all negotiators and middlewares in a chubbyphp/chubbyphp-container under negotiator.* ids.
The supported values are read from negotiator.*.values services, which default to [].
Full documentation
$container = new Container();
$container->factories((new NegotiationServiceFactory())());
$container->factory('negotiator.acceptNegotiator.values', static fn (): array => ['application/json']);
$container->get('negotiator.acceptMiddleware')->process($request, $handler);Registers the same services in a Pimple container, using the same service ids. Full documentation
$container = new Container();
$container->register(new NegotiationServiceProvider());
$container['negotiator.acceptNegotiator.values'] = ['application/json'];
$container['negotiator.acceptMiddleware']->process($request, $handler);Invokable factories built on chubbyphp/chubbyphp-laminas-config-factory for
laminas-servicemanager style containers. Each factory can be used unnamed or with a name
([Factory::class, 'name']) to register several independent instances.
- AcceptLanguageMiddlewareFactory
- AcceptLanguageNegotiatorFactory
- AcceptMiddlewareFactory
- AcceptNegotiatorFactory
- ContentTypeMiddlewareFactory
- ContentTypeNegotiatorFactory
2026 Dominik Zogg