Last active
December 20, 2015 06:59
-
-
Save DaveRandom/6089944 to your computer and use it in GitHub Desktop.
PHP Content-Type negotiation
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
<?php | |
/** | |
* Represents a MIME content type | |
* | |
* @author Chris Wright <[email protected]> | |
*/ | |
class ContentType | |
{ | |
/** | |
* @var string | |
*/ | |
private $superType; | |
/** | |
* @var string | |
*/ | |
private $subType; | |
/** | |
* @var string[] Associative array of parameters | |
*/ | |
private $params; | |
/** | |
* @var float | |
*/ | |
private $qValue; | |
/** | |
* Constructor | |
* | |
* @param string $superType | |
* @param string $subType | |
* @param string[] $params | |
* @param float $qValue | |
*/ | |
public function __construct($superType, $subType, array $params = [], $qValue = 1) | |
{ | |
$this->superType = $superType; | |
$this->subType = $subType; | |
$this->params = $params; | |
$this->qValue = $qValue; | |
} | |
/** | |
* Get a match score against another content type | |
* | |
* Returns a value between 0 and 1, where 1 is an exact match and 0 is no match | |
* | |
* @param ContentType | |
* @return float | |
*/ | |
public function match(ContentType $contentType) | |
{ | |
$score = 0; | |
$divisor = 2; | |
if ($this->superType === '*' || $contentType->getSuperType() === $this->superType) { | |
$score++; | |
} | |
if ($this->subType === '*' || $contentType->getSubType() === $this->subType) { | |
$score++; | |
} | |
$params = $contentType->getParams(); | |
$divisor += count($params); | |
foreach ($this->params as $key => $value) { | |
if (isset($params[$key])) { | |
if ($params[$key] === $value) { | |
$score++; | |
} | |
} else { | |
$divisor++; | |
} | |
} | |
return $score / $divisor; | |
} | |
/** | |
* Get the string representation of this content type | |
* | |
* @return string | |
*/ | |
public function __toString() | |
{ | |
if ($this->params) { | |
$params = []; | |
foreach ($this->params as $key => $val) { | |
$params[] = $key . '=' . $val; | |
} | |
$params = ';' . implode(';', $params) | |
} else { | |
$params = ''; | |
} | |
return $this->getType() . $params; | |
} | |
/** | |
* Get the string representation without parameters | |
* | |
* @return string | |
*/ | |
public function getType() | |
{ | |
return $this->superType . '/' . $this->subType; | |
} | |
/** | |
* Get the super type | |
* | |
* @return string | |
*/ | |
public function getSuperType() | |
{ | |
return $this->superType; | |
} | |
/** | |
* Get the sub type | |
* | |
* @return string | |
*/ | |
public function getSubType() | |
{ | |
return $this->subType; | |
} | |
/** | |
* Get the super type of this content type | |
* | |
* @return string | |
*/ | |
public function getQValue() | |
{ | |
return $this->qValue; | |
} | |
/** | |
* Get the value of a parameter by name | |
* | |
* Returns NULL when the named parameter does not exist | |
* | |
* @return string|null | |
*/ | |
public function getParam($name) | |
{ | |
return isset($this->params[$name]) ? $this->params[$name] : null; | |
} | |
/** | |
* Get all parameters as an associative array | |
* | |
* @return string[] | |
*/ | |
public function getParams() | |
{ | |
return $this->params; | |
} | |
} |
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
<?php | |
/** | |
* Builds ContentType objects from strings | |
* | |
* @author Chris Wright <[email protected]> | |
*/ | |
class ContentTypeBuilder | |
{ | |
/** | |
* @var ContentTypeFactory | |
*/ | |
private $contentTypeFactory; | |
/** | |
* Constructor | |
* | |
* @param ContentTypeFactory $contentTypeFactory | |
*/ | |
public function __construct(ContentTypeFactory $contentTypeFactory) | |
{ | |
$this->contentTypeFactory = $contentTypeFactory; | |
} | |
/** | |
* Parse a string into a ContentType object | |
* | |
* @param string $typeDef | |
* @return ContentType | |
*/ | |
public function build($typeDef) | |
{ | |
$parts = preg_split('#\s*;\s*#', trim($typeDef), -1, PREG_SPLIT_NO_EMPTY); | |
$typeParts = preg_split('#\s*/\s*#', strtolower(array_shift($parts)), 2); | |
if (!isset($typeParts[1])) { | |
return null; | |
} | |
list($superType, $subType) = $typeParts; | |
if ($superType === '*' && $subType !== '*') { | |
return null; | |
} | |
$params = []; | |
$qValue = 1; | |
foreach ($parts as $param) { | |
$paramParts = preg_split('#\s*=\s*#', $param, 2); | |
if (isset($paramParts[1])) { | |
if ($paramParts[0] === 'q') { | |
$qValue = (float) $paramParts[1]; | |
break; // TODO: we don't account for accept-extensions | |
// It has been suggested by @rdlowrey that this should be a permanent state | |
// of affairs because extensions are a bad idea and I'm inclined to agree | |
} | |
$params[$paramParts[0]] = $paramParts[1]; | |
} | |
} | |
return $this->contentTypeFactory->create($superType, $subType, $params, $qValue); | |
} | |
} |
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
<?php | |
/** | |
* Creates ContentType objects | |
* | |
* @author Chris Wright <[email protected]> | |
*/ | |
class ContentTypeFactory | |
{ | |
/** | |
* Factory method | |
* | |
* @param string $superType | |
* @param string $subType | |
* @param string[] $params | |
* @param float $qValue | |
* @return ContentType | |
*/ | |
public function create($superType, $subType, array $params = [], $qValue = 1) | |
{ | |
return new ContentType($superType, $subType, $params, $qValue); | |
} | |
} |
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
<?php | |
/** | |
* Reconciles acceptable type lists against available type lists | |
* | |
* @author Chris Wright <[email protected]> | |
*/ | |
class ContentTypeResolver | |
{ | |
/** | |
* @var ContentTypeBuilder | |
*/ | |
private $contentTypeBuilder; | |
/** | |
* Constructor | |
* | |
* @param ContentTypeBuilder $contentTypeBuilder | |
*/ | |
public function __construct(ContentTypeBuilder $contentTypeBuilder) | |
{ | |
$this->contentTypeBuilder = $contentTypeBuilder; | |
} | |
/** | |
* Parses an acceptable type list into an array of ContentType objects | |
* | |
* @param string $acceptedTypes | |
* @return ContentType[] | |
*/ | |
private function parseAcceptedTypes($acceptedTypes) | |
{ | |
$result = []; | |
foreach (explode(',', $acceptedTypes) as $typeSpec) { | |
if ($contentType = $this->contentTypeBuilder->build($typeSpec)) { | |
$result[$contentType->getSuperType()][$contentType->getSubType()][] = $contentType; | |
} | |
} | |
return $result; | |
} | |
/** | |
* Normalise a type spec to a ContentType object | |
* | |
* @param string|ContentType $spec | |
* @return ContentType | |
*/ | |
private function getParsedType($spec) | |
{ | |
return $spec instanceof ContentType ? $spec : $this->contentTypeBuilder->build((string) $spec); | |
} | |
/** | |
* Get the best matching accepted type from the list of available types | |
* | |
* @param string $acceptedTypes Type specification as specified in an HTTP Accept: header | |
* @param array $availableTypes List of available types, as strings or ContentType objects | |
* @return ContentType | |
*/ | |
public function getResponseType($acceptedTypes, array $availableTypes) | |
{ | |
if (!$acceptedTypes = $this->parseAcceptedTypes($acceptedTypes)) { | |
return $this->getParsedType(current($availableTypes)); | |
} | |
$weightMap = []; | |
foreach ($availableTypes as $typeSpec) { | |
$availableType = $this->getParsedType($typeSpec); | |
$superType = $availableType->getSuperType(); | |
$subType = $availableType->getSubType(); | |
if (isset($acceptedTypes[$superType][$subType])) { | |
$candidates = $acceptedTypes[$superType][$subType]; | |
} else if (isset($acceptedTypes[$superType]['*'])) { | |
$candidates = $acceptedTypes[$superType]['*']; | |
} else if (isset($acceptedTypes['*']['*'])) { | |
$candidates = $acceptedTypes['*']['*']; | |
} else { | |
continue; | |
} | |
$bestMatchFactor = 0; | |
$bestMatch = null; | |
foreach ($candidates as $acceptedType) { | |
$factor = $acceptedType->match($availableType); | |
if ($factor > $bestMatchFactor) { | |
$bestMatchFactor = $factor; | |
$bestMatch = $acceptedType; | |
if ($bestMatchFactor >= 1) { | |
break; | |
} | |
} | |
} | |
$weightMap[(string) $bestMatch->getQValue()][] = $availableType; | |
} | |
if (!$weightMap) { | |
return null; | |
} | |
ksort($weightMap, SORT_NUMERIC); | |
return end($weightMap)[0]; | |
} | |
} |
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
<?php | |
require 'ContentTypeResolver.php'; | |
require 'ContentTypeBuilder.php'; | |
require 'ContentTypeFactory.php'; | |
require 'ContentType.php'; | |
$resolver = new ContentTypeResolver(new ContentTypeBuilder(new ContentTypeFactory)); | |
// Example from RFC 2616 | |
$acceptHeader = 'text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.5'; | |
$availableTypes = [ | |
'text/html', | |
'text/plain', | |
'text/html;level=1', | |
'image/jpeg', | |
'text/html;level=2', | |
'text/html;level=3', | |
]; | |
echo $resolver->getResponseType($acceptHeader, $availableTypes); |
@PeeHaa added some docblocks and generally made this presentable.
It's a long time since I wrote this but I'm pretty sure it works fairly well, the main logic is in ContentTypeResolver::getResponseType()
, all the rest is just fluff to turn content types into sexy objects :-)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
wow such gist great timing nice headers