Skip to content

Commit

Permalink
Merge pull request #60 from willdurand/2.0.0
Browse files Browse the repository at this point in the history
[WIP] Negotiation 2.0
  • Loading branch information
willdurand committed Jul 29, 2015
2 parents 4dd4643 + 679c85b commit d26d6b0
Show file tree
Hide file tree
Showing 36 changed files with 1,199 additions and 1,526 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/vendor/
composer.lock
3 changes: 2 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ sudo: false
language: php

php:
- 5.3
- 5.4
- 5.5
- 5.6
- hhvm
- 7.0

matrix:
allow_failures:
- php: hhvm
- php: 7.0

before_script:
- composer self-update
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
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
Expand Down
137 changes: 64 additions & 73 deletions README.md
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
Expand All @@ -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
Expand All @@ -136,7 +125,7 @@ Run it using PHPUnit:
Contributing
------------

See CONTRIBUTING file.
See [CONTRIBUTING](CONTRIBUTING.md) file.


Credits
Expand All @@ -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.
6 changes: 3 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
}
105 changes: 105 additions & 0 deletions src/Negotiation/AbstractNegotiator.php
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;
}
}
Loading

0 comments on commit d26d6b0

Please sign in to comment.