diff options
-rw-r--r-- | composer.json | 3 | ||||
-rw-r--r-- | src/Controller/ContentNegotiation.php | 108 | ||||
-rw-r--r-- | tests/Controller/ContentNegotiationTest.php | 140 | ||||
-rw-r--r-- | tests/support/TestHelper.php | 29 |
4 files changed, 273 insertions, 7 deletions
diff --git a/composer.json b/composer.json index 90e4820..bd02105 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "php": ">=5.6.0", "psr/http-message": "^1.0", "jasny/php-functions": "^2.0", - "dflydev/apache-mime-types": "^1.0" + "dflydev/apache-mime-types": "^1.0", + "willdurand/negotiation": "^2.2" }, "require-dev": { "jasny/php-code-quality": "^2.0", diff --git a/src/Controller/ContentNegotiation.php b/src/Controller/ContentNegotiation.php new file mode 100644 index 0000000..75aa4f1 --- /dev/null +++ b/src/Controller/ContentNegotiation.php @@ -0,0 +1,108 @@ +<?php + +namespace Jasny\Controller; + +/** + * Controller methods to negotiate content + */ +trait ContentNegotiation +{ + /** + * Get request, set for controller + * + * @return ServerRequestInterface + */ + abstract public function getRequest(); + + /** + * Pick best content type + * + * @param array $priorities + * @return string + */ + public function negotiateContentType(array $priorities) + { + return $this->negotiate($priorities); + } + + /** + * Pick best language + * + * @param array $priorities + * @return string + */ + public function negotiateLanguage(array $priorities) + { + return $this->negotiate($priorities, 'language'); + } + + /** + * Pick best encoding + * + * @param array $priorities + * @return string + */ + public function negotiateEncoding(array $priorities) + { + return $this->negotiate($priorities, 'encoding'); + } + + /** + * Pick best charset + * + * @param array $priorities + * @return string + */ + public function negotiateCharset(array $priorities) + { + return $this->negotiate($priorities, 'charset'); + } + + /** + * Generalize negotiation + * + * @param array $priorities + * @param string $type Negotiator type + * @return string + */ + protected function negotiate(array $priorities, $type = '') + { + $header = 'Accept'; + + if ($type) { + $header .= '-' . ucfirst($type); + } + + $header = $this->getRequest()->getHeader($header); + $header = join(', ', $header); + + $negotiator = $this->getNegotiator($type); + $chosen = $negotiator->getBest($header, $priorities); + + return $chosen ? $chosen->getType() : ''; + } + + /** + * Get negotiation library instance + * + * @param string $type Negotiator type + * @return Negotiation\AbstractNegotiator + */ + protected function getNegotiator($type = '') + { + $class = $this->getNegotiatorName($type); + + return new $class(); + } + + /** + * Get negotiator name + * + * @param string $type + * @return string + */ + protected function getNegotiatorName($type = '') + { + return 'Negotiation\\' . ucfirst($type) . 'Negotiator'; + } +} diff --git a/tests/Controller/ContentNegotiationTest.php b/tests/Controller/ContentNegotiationTest.php new file mode 100644 index 0000000..1a390a4 --- /dev/null +++ b/tests/Controller/ContentNegotiationTest.php @@ -0,0 +1,140 @@ +<?php + +namespace Jasny\Controller; + +use Jasny\Controller\ContentNegotiation; +use Psr\Http\Message\ServerRequestInterface; +use Jasny\Controller\TestHelper; +use Negotiation\Negotiator; +use Negotiation\BaseAccept; + +/** + * @covers Jasny\Controller\ContentNegotiation + */ +class ContentNegotiationTest extends \PHPUnit_Framework_TestCase +{ + use TestHelper; + + /** + * Test negotiation + * + * @dataProvider negotiateProvider + * @param string $result + * @param array $header + * @param array $priorities + */ + public function testNegotiate($method, $negotiatorClass, $type, $expected, $headerName, array $headerValue, array $priorities) + { + $request = $this->createMock(ServerRequestInterface::class); + $request->expects($this->once())->method('getHeader')->with($this->equalTo($headerName))->will($this->returnValue($headerValue)); + + $expectedObj = $this->createMock(BaseAccept::class); + $expectedObj->expects($this->once())->method('getType')->will($this->returnValue($expected)); + + $negotiator = $this->createMock($negotiatorClass); + $negotiator->expects($this->once())->method('getBest')->with($this->equalTo(join(', ', $headerValue)), $this->equalTo($priorities))->will($this->returnValue($expectedObj)); + + $trait = $this->getController(['getRequest', 'getNegotiator']); + $trait->expects($this->once())->method('getRequest')->will($this->returnValue($request)); + $trait->expects($this->once())->method('getNegotiator')->with($this->equalTo($type))->will($this->returnValue($negotiator)); + + $builtClass = $this->callProtectedMethod($trait, 'getNegotiatorName', [$type]); + $result = $trait->{$method}($priorities); + + $this->assertEquals($builtClass, $negotiatorClass, "Obtained wrong negotiator class"); + $this->assertEquals($result, $expected, "Obtained result does not match expected result"); + } + + /** + * Provide data for testing negotiation + * + * @return array + */ + public function negotiateProvider() + { + return [ + [ + 'negotiateContentType', + 'Negotiation\\Negotiator', + '', + 'text/html', + 'Accept', + ['text/html', 'application/xhtml+xml', 'application/xml;q=0.9', '*/*;q=0.8'], + ['text/html', 'application/xhtml+xml', 'application/xml'] + ], + [ + 'negotiateContentType', + 'Negotiation\\Negotiator', + '', + '', + 'Accept', + ['text/html', 'application/xhtml+xml', 'application/xml;q=0.9'], + ['text/plain', 'application/json'] + ], + [ + 'negotiateLanguage', + 'Negotiation\\LanguageNegotiator', + 'language', + 'en', + 'Accept-Language', + ['en', 'fr; q=0.4', 'fu; q=0.9', 'de; q=0.2'], + ['en', 'fr'] + ], + [ + 'negotiateLanguage', + 'Negotiation\\LanguageNegotiator', + 'language', + '', + 'Accept-Language', + ['en', 'fr; q=0.4', 'fu; q=0.9', 'de; q=0.2'], + ['ru', 'es'] + ], + [ + 'negotiateEncoding', + 'Negotiation\\EncodingNegotiator', + 'encoding', + 'gzip', + 'Accept-Encoding', + ['gzip', 'compress', 'deflate'], + ['gzip', 'compress'] + ], + [ + 'negotiateEncoding', + 'Negotiation\\EncodingNegotiator', + 'encoding', + '', + 'Accept-Encoding', + ['gzip', 'compress', 'deflate'], + ['br', 'identity'] + ], + [ + 'negotiateCharset', + 'Negotiation\\CharsetNegotiator', + 'charset', + 'utf-8', + 'Accept-Charset', + ['utf-8', 'iso-8859-1;q=0.5'], + ['utf-8', 'iso-8859-1;q=0.5'] + ], + [ + 'negotiateCharset', + 'Negotiation\\CharsetNegotiator', + 'charset', + '', + 'Accept-Charset', + ['utf-8', 'iso-8859-1;q=0.5'], + ['windows-1251'] + ] + ]; + } + + /** + * Get the controller class + * + * @return string + */ + protected function getControllerClass() + { + return ContentNegotiation::class; + } +} diff --git a/tests/support/TestHelper.php b/tests/support/TestHelper.php index a046e4e..05b40d7 100644 --- a/tests/support/TestHelper.php +++ b/tests/support/TestHelper.php @@ -20,14 +20,14 @@ trait TestHelper /** * Get the controller class - * + * * @return string */ protected function getControllerClass() { return Controller::class; } - + /** * Get mock for controller * @@ -46,14 +46,14 @@ trait TestHelper if (isset($mockClassName)) { $builder->setMockClassName($mockClassName); } - + $getMock = trait_exists($class) ? 'getMockForTrait' : 'getMockForAbstractClass'; return $builder->$getMock(); } - + /** * Set a private or protected property of the given object - * + * * @param object $object * @param string $property * @param mixed $value @@ -63,9 +63,26 @@ trait TestHelper if (!is_object($object)) { throw new \InvalidArgumentException("Excpected an object, got a " . gettype($object)); } - + $refl = new \ReflectionProperty($object, $property); $refl->setAccessible(true); $refl->setValue($object, $value); } + + /** + * Call protected method on some object + * + * @param object $object + * @param string $name Method name + * @param array $args + * @return mixed Result of method call + */ + protected function callProtectedMethod($object, $name, $args) + { + $class = new \ReflectionClass($object); + $method = $class->getMethod($name); + $method->setAccessible(true); + + return $method->invokeArgs($object, $args); + } } |