-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #60 from willdurand/2.0.0
[WIP] Negotiation 2.0
- Loading branch information
Showing
36 changed files
with
1,199 additions
and
1,526 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
/vendor/ | ||
composer.lock |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
Copyright (c) 2013 William Durand <[email protected]> | ||
Copyright (c) William Durand <[email protected]> | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy | ||
of this software and associated documentation files (the "Software"), to deal | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,23 @@ | ||
Negotiation | ||
=========== | ||
|
||
[![Build Status](https://travis-ci.org/willdurand/Negotiation.png?branch=master)](http://travis-ci.org/willdurand/Negotiation) | ||
[![Total Downloads](https://poser.pugx.org/willdurand/Negotiation/downloads.png)](https://packagist.org/packages/willdurand/Negotiation) | ||
[![Latest Stable Version](https://poser.pugx.org/willdurand/Negotiation/v/stable.png)](https://packagist.org/packages/willdurand/Negotiation) | ||
[![Build | ||
Status](https://travis-ci.org/willdurand/Negotiation.png?branch=master)](http://travis-ci.org/willdurand/Negotiation) | ||
[![Total | ||
Downloads](https://poser.pugx.org/willdurand/Negotiation/downloads.png)](https://packagist.org/packages/willdurand/Negotiation) | ||
[![Latest Stable | ||
Version](https://poser.pugx.org/willdurand/Negotiation/v/stable.png)](https://packagist.org/packages/willdurand/Negotiation) | ||
|
||
**Negotiation** is a standalone library without any dependencies that allows you | ||
to implement [content | ||
negotiation](http://www.w3.org/Protocols/rfc2616/rfc2616-sec12.html) in your | ||
application, whatever framework you use. | ||
This library is based on [RFC | ||
2616](http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html). Negotiation is | ||
easy to use, and extensively unit tested. | ||
negotiation](https://tools.ietf.org/html/rfc7231#section-5.3) in your | ||
application, whatever framework you use. This library is based on [RFC | ||
7231](https://tools.ietf.org/html/rfc7231). Negotiation is easy to use, and | ||
extensively unit tested! | ||
|
||
> **Important:** You are browsing the documentation of Negotiation **2.x**. | ||
Documentation for version **1.x** is available here: [Negotiation 1.x | ||
documentation](https://github.com/willdurand/Negotiation/blob/1.x/README.md#usage). | ||
|
||
|
||
Installation | ||
|
@@ -24,101 +30,84 @@ The recommended way to install Negotiation is through | |
$ composer require willdurand/negotiation | ||
``` | ||
|
||
**Protip:** you can also choose the correct version via | ||
[`willdurand/negotiation`](https://packagist.org/packages/willdurand/negotiation). | ||
|
||
|
||
Usage | ||
----- | ||
Usage Examples | ||
-------------- | ||
|
||
In a nutshell: | ||
### Media Type Negotiation | ||
|
||
``` php | ||
<?php | ||
|
||
$negotiator = new \Negotiation\Negotiator(); | ||
$bestHeader = $negotiator->getBest('en; q=0.1, fr; q=0.4, fu; q=0.9, de; q=0.2'); | ||
// $bestHeader = 'fu'; | ||
``` | ||
|
||
The `getBest()` method, part of the `NegotiatorInterface`, returns either `null` | ||
or `AcceptHeader` instances. An `AcceptHeader` object owns a `value` and a | ||
`quality`. | ||
$acceptHeader = 'text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8'; | ||
$priorities = array('text/html; charset=UTF-8', 'application/json'); | ||
|
||
$mediaType = $negotiator->getBest($acceptHeader, $priorities); | ||
|
||
$value = $mediaType->getValue(); | ||
// $value == 'text/html; charset=UTF-8' | ||
``` | ||
|
||
### Format Negotiation | ||
The `Negotiator` returns an instance of `Accept`, or `null` if negotiating the | ||
best media type has failed. | ||
|
||
The **Format Negotiation** is handled by the `FormatNegotiator` class. | ||
Basically, pass an `Accept` header and optionally a set of preferred media types | ||
to the `getBest()` method in order to retrieve the best **media type**: | ||
### Language Negotiation | ||
|
||
``` php | ||
<?php | ||
|
||
$negotiator = new \Negotiation\FormatNegotiator(); | ||
$negotiator = new \Negotiation\LanguageNegotiator(); | ||
|
||
$acceptLangageHeader = 'en; q=0.1, fr; q=0.4, fu; q=0.9, de; q=0.2'; | ||
$priorities = array('de', 'fu', 'en'); | ||
|
||
$bestLanguage = $negotiator->getBest($acceptLangageHeader, $priorities); | ||
|
||
$acceptHeader = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'; | ||
$priorities = array('text/html', 'application/json', '*/*'); | ||
$type = $bestLanguage->getType(); | ||
// $type == 'fu'; | ||
|
||
$format = $negotiator->getBest($acceptHeader, $priorities); | ||
// $format->getValue() = text/html | ||
$quality = $bestLanguage->getQuality(); | ||
// $quality == 0.9 | ||
``` | ||
|
||
The `FormatNegotiator` class also provides a `getBestFormat()` method that | ||
returns the best format given an `Accept` header string and a set of | ||
preferred/allowed formats or mime types: | ||
The `LanguageNegotiator` returns an instance of `AcceptLanguage`. | ||
|
||
### Encoding Negotiation | ||
|
||
``` php | ||
<?php | ||
|
||
$negotiator = new \Negotiation\FormatNegotiator(); | ||
|
||
$acceptHeader = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'; | ||
$priorities = array('html', 'application/json', '*/*'); | ||
|
||
$format = $negotiator->getBestFormat($acceptHeader, $priorities); | ||
// $format = html | ||
$negotiator = new \Negotiation\EncodingNegotiator(); | ||
$encoding = $negotiator->getBest($acceptHeader, $priorities); | ||
``` | ||
|
||
#### Other Methods | ||
|
||
* `registerFormat($format, array $mimeTypes, $override = false)`: registers a new | ||
format with its mime types; | ||
* `getFormat($mimeType)`: returns the format for a given mime type, or null if | ||
not found; | ||
* `normalizePriorities($priorities)`: ensures that any formats are converted to | ||
mime types. | ||
The `EncodingNegotiator` returns an instance of `AcceptEncoding`. | ||
|
||
### Language Negotiation | ||
|
||
Language negotiation is handled by the `LanguageNegotiator` class: | ||
### Charset Negotiation | ||
|
||
``` php | ||
<?php | ||
|
||
$negotiator = new \Negotiation\LanguageNegotiator(); | ||
$language = $negotiator->getBest('da, en-gb;q=0.8, en;q=0.7'); | ||
// $language = da | ||
$negotiator = new \Negotiation\CharsetNegotiator(); | ||
$charset = $negotiator->getBest($acceptHeader, $priorities); | ||
``` | ||
|
||
The `CharsetNegotiator` returns an instance of `AcceptCharset`. | ||
|
||
### Charset/Encoding Negotiation | ||
### `Accept*` Classes | ||
|
||
Charset/Encoding negotiation works out of the box using the `Negotiator` class: | ||
`Accept` and `Accept*` classes share common methods such as: | ||
|
||
``` php | ||
<?php | ||
|
||
$negotiator = new \Negotiation\Negotiator(); | ||
$priorities = array( | ||
'utf-8', | ||
'big5', | ||
'shift-jis', | ||
); | ||
|
||
$bestHeader = $negotiator->getBest('ISO-8859-1, Big5;q=0.6,utf-8;q=0.7, *;q=0.5', $priorities); | ||
// $bestHeader = 'utf-8' | ||
``` | ||
* `getValue()` returns the accept value (e.g. `text/html; z=y; a=b; c=d`) | ||
* `getNormalizedValue()` returns the value with parameters sorted (e.g. | ||
`text/html; a=b; c=d; z=y`) | ||
* `getQuality()` returns the quality if available (`q` parameter) | ||
* `getType()` returns the accept type (e.g. `text/html`) | ||
* `getParameters()` returns the set of parameters (excluding the `q` parameter | ||
if provided) | ||
* `getParameter()` allows to retrieve a given parameter by its name. Fallback to | ||
a `$default` (nullable) value otherwise. | ||
* `hasParameter()` indicates whether a parameter exists. | ||
|
||
|
||
Unit Tests | ||
|
@@ -136,7 +125,7 @@ Run it using PHPUnit: | |
Contributing | ||
------------ | ||
|
||
See CONTRIBUTING file. | ||
See [CONTRIBUTING](CONTRIBUTING.md) file. | ||
|
||
|
||
Credits | ||
|
@@ -148,10 +137,12 @@ Credits | |
* [FOSRest](http://github.com/FriendsOfSymfony/FOSRest); | ||
* [PEAR HTTP2](https://github.com/pear/HTTP2). | ||
|
||
* William Durand <[email protected]> | ||
* William Durand <[email protected]> | ||
* [@neural-wetware](https://github.com/neural-wetware) | ||
|
||
|
||
License | ||
------- | ||
|
||
Negotiation is released under the MIT License. See the bundled LICENSE file for details. | ||
Negotiation is released under the MIT License. See the bundled LICENSE file for | ||
details. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,18 +7,18 @@ | |
"authors": [ | ||
{ | ||
"name": "William Durand", | ||
"email": "[email protected]" | ||
"email": "[email protected]" | ||
} | ||
], | ||
"require": { | ||
"php": ">=5.3.0" | ||
"php": ">=5.4.0" | ||
}, | ||
"autoload": { | ||
"psr-4": { "Negotiation\\": "src/Negotiation" } | ||
}, | ||
"extra": { | ||
"branch-alias": { | ||
"dev-master": "1.4-dev" | ||
"dev-master": "2.0-dev" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
<?php | ||
|
||
namespace Negotiation; | ||
|
||
use Negotiation\Exception\InvalidArgument; | ||
use Negotiation\Exception\InvalidHeader; | ||
|
||
abstract class AbstractNegotiator | ||
{ | ||
/** | ||
* @param string $header A string containing an `Accept|Accept-*` header. | ||
* @param array $priorities A set of server priorities. | ||
* | ||
* @return AcceptHeader best matching type | ||
*/ | ||
public function getBest($header, array $priorities) | ||
{ | ||
if (empty($priorities)) { | ||
throw new InvalidArgument('A set of server priorities should be given.'); | ||
} | ||
|
||
if (!$header) { | ||
throw new InvalidArgument('The header string should not be empty.'); | ||
} | ||
|
||
$headers = $this->parseHeader($header); | ||
$headers = array_map(array($this, 'acceptFactory'), $headers); | ||
$priorities = array_map(array($this, 'acceptFactory'), $priorities); | ||
|
||
$matches = $this->findMatches($headers, $priorities); | ||
$specificMatches = array_reduce($matches, 'Negotiation\Match::reduce', []); | ||
|
||
usort($specificMatches, 'Negotiation\Match::compare'); | ||
|
||
$match = array_shift($specificMatches); | ||
|
||
return null === $match ? null : $priorities[$match->index]; | ||
} | ||
|
||
/** | ||
* @param string $header accept header part or server priority | ||
* | ||
* @return AcceptHeader Parsed header object | ||
*/ | ||
abstract protected function acceptFactory($header); | ||
|
||
/** | ||
* @param AcceptHeader $header | ||
* @param AcceptHeader $priority | ||
* @param integer $index | ||
* | ||
* @return Match|null Headers matched | ||
*/ | ||
protected function match(AcceptHeader $header, AcceptHeader $priority, $index) | ||
{ | ||
$ac = $header->getType(); | ||
$pc = $priority->getType(); | ||
|
||
$equal = !strcasecmp($ac, $pc); | ||
|
||
if ($equal || $ac === '*') { | ||
$score = 1 * $equal; | ||
|
||
return new Match($header->getQuality(), $score, $index); | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* @param string $header A string that contains an `Accept*` header. | ||
* | ||
* @return AcceptHeader[] | ||
*/ | ||
private function parseHeader($header) | ||
{ | ||
$res = preg_match_all('/(?:[^,"]*+(?:"[^"]*+")?)+[^,"]*+/', $header, $matches); | ||
|
||
if (!$res) { | ||
throw new InvalidHeader(sprintf('Failed to parse accept header: "%s"', $header)); | ||
} | ||
|
||
return array_values(array_filter(array_map('trim', $matches[0]))); | ||
} | ||
|
||
/** | ||
* @param AcceptHeader[] $headerParts | ||
* @param Priority[] $priorities Configured priorities | ||
* | ||
* @return Match[] Headers matched | ||
*/ | ||
private function findMatches(array $headerParts, array $priorities) | ||
{ | ||
$matches = []; | ||
foreach ($priorities as $index => $p) { | ||
foreach ($headerParts as $h) { | ||
if (null !== $match = $this->match($h, $p, $index)) { | ||
$matches[] = $match; | ||
} | ||
} | ||
} | ||
|
||
return $matches; | ||
} | ||
} |
Oops, something went wrong.