diff --git a/CHANGELOG.md b/CHANGELOG.md index 4abbb0f..d2ecab2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,19 +2,29 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release.. -## TBD - TBD +## 0.6.0 - 2014-10.17 + +Updated component to psr/http-message 0.4.0. That release contains a number of backwards-incompatible changes. ### Added -- Nothing. +- Added IncomingRequestFactory::setHeaders() for simplifying setting + (overwriting) many headers at once from an array. +- Updated MessageTrait::addHeader() to allow array values +- Modified IncomingRequest to `s/PathParams/Attributes/g` ### Deprecated -- Nothing. +- IncomingRequest now only allows arrays for either input or return values; Array-like objects are no longer accepted. +- Removed ability to pass objects to MessageTrait::addHeader()/setHeader() +- Removed setHeaders()/addHeaders() from MessageTrait +- Modified IncomingRequest to `s/PathParams/Attributes/g` ### Removed -- Nothing. +- Removed ability to pass objects to MessageTrait::addHeader()/setHeader() +- Removed setHeaders()/addHeaders() from MessageTrait +- Modified IncomingRequest to `s/PathParams/Attributes/g` ### Fixed diff --git a/composer.json b/composer.json index 297dfec..992539f 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ }, "require": { "php": ">=5.4.8", - "psr/http-message": "~0.3.0" + "psr/http-message": "~0.4.0" }, "require-dev": { "phpunit/PHPUnit": "3.7.*", diff --git a/src/IncomingRequest.php b/src/IncomingRequest.php index da271c7..44fdc19 100644 --- a/src/IncomingRequest.php +++ b/src/IncomingRequest.php @@ -1,7 +1,6 @@ setCookieParams($cookieParams); - $this->setPathParams($pathParams); + $this->setAttributes($attributes); $this->setQueryParams($queryParams); $this->setBodyParams($bodyParams); $this->setFileParams($fileParams); @@ -100,14 +100,7 @@ public function __construct( * * Retrieves cookies sent by the client to the server. * - * The assumption is these are injected during instantiation, typically - * from PHP's $_COOKIE superglobal, and should remain immutable over the - * course of the incoming request. - * - * The return value can be either an array or an object that acts like - * an array (e.g., implements ArrayAccess, or an ArrayObject). - * - * @return array|ArrayAccess + * @return array */ public function getCookieParams() { @@ -123,21 +116,10 @@ public function getCookieParams() * the original value, filter them, and re-inject into the incoming * request.. * - * The value provided should be an array or array-like object - * (e.g., implements ArrayAccess, or an ArrayObject). - * - * @param array|ArrayAccess $cookies Cookie values/structs - * - * @return void + * @param array $cookies Cookie values/structs */ - public function setCookieParams($cookies) + public function setCookieParams(array $cookies) { - if (! is_array($cookies) && ! $cookies instanceof ArrayAccess) { - throw new InvalidArgumentException( - 'Cookies must be provided as either an array or ArrayAccess' - ); - } - $this->cookieParams = $cookies; } @@ -150,10 +132,7 @@ public function setCookieParams($cookies) * from PHP's $_GET superglobal, and should remain immutable over the * course of the incoming request. * - * The return value can be either an array or an object that acts like - * an array (e.g., implements ArrayAccess, or an ArrayObject). - * - * @return array|ArrayAccess + * @return array */ public function getQueryParams() { @@ -165,16 +144,10 @@ public function getQueryParams() * * Internal method only. * - * @param array|ArrayAccess $queryParams + * @param array $queryParams */ - private function setQueryParams($queryParams) + private function setQueryParams(array $queryParams) { - if (! is_array($queryParams) && ! $queryParams instanceof ArrayAccess) { - throw new InvalidArgumentException( - 'Query string arguments must be provided as either an array or ArrayAccess' - ); - } - $this->queryParams = $queryParams; } @@ -188,10 +161,7 @@ private function setQueryParams($queryParams) * from PHP's $_FILES superglobal, and should remain immutable over the * course of the incoming request. * - * The return value can be either an array or an object that acts like - * an array (e.g., implements ArrayAccess, or an ArrayObject). - * - * @return array|ArrayAccess Upload file(s) metadata, if any. + * @return array Upload file(s) metadata, if any. */ public function getFileParams() { @@ -203,16 +173,10 @@ public function getFileParams() * * Internal method only. * - * @param array|ArrayAccess $fileParams + * @param array $fileParams */ - private function setFileParams($fileParams) + private function setFileParams(array $fileParams) { - if (! is_array($fileParams) && ! $fileParams instanceof ArrayAccess) { - throw new InvalidArgumentException( - 'Files must be provided as either an array or ArrayAccess' - ); - } - $this->fileParams = $fileParams; } @@ -220,15 +184,13 @@ private function setFileParams($fileParams) * Retrieve any parameters provided in the request body. * * If the request body can be deserialized, and if the deserialized values - * can be represented as an array or object, this method can be used to + * can be represented as an array, this method can be used to * retrieve them. * * In other cases, the parent getBody() method should be used to retrieve * the body content. * - * @return array|object The deserialized body parameters, if any. These may - * be either an array or an object, though an array or - * array-like object is recommended. + * @return array The deserialized body parameters, if any. */ public function getBodyParams() { @@ -238,61 +200,42 @@ public function getBodyParams() /** * Set the request body parameters. * - * If the body content can be deserialized, the values obtained may then + * If the body content can be deserialized as an array, the values obtained may then * be injected into the response using this method. This method will * typically be invoked by a factory marshaling request parameters. * - * @param array|object $values The deserialized body parameters, if any. - * These may be either an array or an object, - * though an array or array-like object is - * recommended. - * - * @return void + * @param array $values The deserialized body parameters, if any. */ - public function setBodyParams($values) + public function setBodyParams(array $values) { - if (! is_array($values) && ! is_object($values)) { - throw new InvalidArgumentException( - 'Body parameters must be provided as either an array or an object' - ); - } - $this->bodyParams = $values; } /** - * Retrieve parameters matched during routing. + * Retrieve attributes derived from the request * * If a router or similar is used to match against the path and/or request, * this method can be used to retrieve the results, so long as those - * results can be represented as an array or array-like object. + * results can be represented as an array. * - * @return array|ArrayAccess Path parameters matched by routing + * @return array Path parameters matched by routing */ - public function getPathParams() + public function getAttributes() { - return $this->pathParams; + return $this->attributes; } /** * Set parameters discovered by matching that path * * If a router or similar is used to match against the path and/or request, - * this method can be used to inject the request with the results, so long - * as those results can be represented as an array or array-like object. + * this method can be used to inject them, so long as those + * results can be represented as an array. * - * @param array|ArrayAccess $values Path parameters matched by routing - * - * @return void + * @param array $values Path parameters matched by routing */ - public function setPathParams(array $values) + public function setAttributes(array $values) { - if (! is_array($values) && ! $values instanceof ArrayAccess) { - throw new InvalidArgumentException( - 'Path parameters must be provided as either an array or ArrayAccess' - ); - } - - $this->pathParams = $values; + $this->attributes = $values; } } diff --git a/src/IncomingRequestFactory.php b/src/IncomingRequestFactory.php index 64855be..2d05c9d 100644 --- a/src/IncomingRequestFactory.php +++ b/src/IncomingRequestFactory.php @@ -2,6 +2,7 @@ namespace Phly\Http; use Psr\Http\Message\IncomingRequestInterface; +use Psr\Http\Message\RequestInterface; use stdClass; /** @@ -72,7 +73,7 @@ public static function fromServer(array $server, IncomingRequestInterface $reque } $request->setMethod(self::get('REQUEST_METHOD', $server, 'GET')); - $request->setHeaders(self::marshalHeaders($server)); + self::setHeaders($request, self::marshalHeaders($server)); $request->setUrl(self::marshalUri($server, $request)); return $request; } @@ -157,6 +158,21 @@ public static function marshalHeaders(array $server) return $headers; } + /** + * Set the headers for a request. + * + * Injects the given request instance with the headers provided. + * + * @param RequestInterface $request + * @param array $headers + */ + public static function setHeaders(RequestInterface $request, array $headers) + { + foreach ($headers as $header => $values) { + $request->setHeader($header, $values); + } + } + /** * Marshal the URI from the $_SERVER array and headers * diff --git a/src/MessageTrait.php b/src/MessageTrait.php index dd96e76..d444357 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -6,7 +6,7 @@ /** * Trait implementing the various methods defined in - * \Psr\Http\Message\ MessageInterface. + * \Psr\Http\Message\MessageInterface. */ trait MessageTrait { @@ -155,65 +155,23 @@ public function getHeaderAsArray($header) * or an array of strings. * * @param string $header Header name - * @param string|string[]|object|object[] $value Header values; any objects must be castable to strings - * - * @return void + * @param string|string[] $value Header value(s) */ public function setHeader($header, $value) { - if (is_object($value) && method_exists($value, '__toString')) { - $value = (string) $value; - } - - if (! is_string($value) && ! is_array($value)) { - throw new InvalidArgumentException('Invalid header value; must be a string or array of strings'); - } - - if (is_array($value)) { - $valid = true; - array_walk($value, function ($value) use (&$valid) { - if (is_object($value) && method_exists($value, '__toString')) { - $value = (string) $value; - } - - if (! is_string($value)) { - $valid = false; - } - }); - - if (! $valid) { - throw new InvalidArgumentException('Invalid header value; must be a string or array of strings'); - } - } + $header = strtolower($header); if (is_string($value)) { - $value = [$value]; + $value = [ $value ]; } - $this->headers[strtolower($header)] = $value; - } - - /** - * Sets headers, replacing any headers that have already been set on the message. - * - * The array keys MUST be a string. The array values must be either a - * string or an array of strings. - * - * @param array $headers Headers to set. - * - * @return void - */ - public function setHeaders(array $headers) - { - $this->headers = []; - - foreach ($headers as $key => $value) { - if (! is_string($key)) { - throw new InvalidArgumentException('One or more keys in the headers array is not a string'); - } - - $this->setHeader($key, $value); + if (! is_array($value) || ! $this->arrayContainsOnlyStrings($value)) { + throw new InvalidArgumentException( + 'Invalid header value; must be a string or array of strings' + ); } + + $this->headers[$header] = $value; } /** @@ -223,20 +181,20 @@ public function setHeaders(array $headers) * value will be appended to the existing list. * * @param string $header Header name to add - * @param string|object $value Value of the header; if an object, must be able to cast to a string - * - * @return void + * @param string|string[] $value Value of the header; a string or array of strings */ public function addHeader($header, $value) { $header = strtolower($header); - if (is_object($value) && method_exists($value, '__toString')) { - $value = (string) $value; + if (is_string($value)) { + $value = [ $value ]; } - if (! is_string($value)) { - throw new InvalidArgumentException('Invalid header value; must be a string'); + if (! is_array($value) || ! $this->arrayContainsOnlyStrings($value)) { + throw new InvalidArgumentException( + 'Invalid header value; must be a string or array of strings' + ); } if (! $this->hasHeader($header)) { @@ -244,27 +202,7 @@ public function addHeader($header, $value) return; } - $this->headers[$header][] = $value; - } - - /** - * Merges in an associative array of headers. - * - * Each array key MUST be a string representing the case-insensitive name - * of a header. Each value MUST be either a string or an array of strings. - * For each value, the value is appended to any existing header of the same - * name, or, if a header does not already exist by the given name, then the - * header is added. - * - * @param array $headers Associative array of headers to add to the message - * - * @return void - */ - public function addHeaders(array $headers) - { - foreach ($headers as $header => $value) { - $this->addHeader($header, $value); - } + $this->headers[$header] = array_merge($this->headers[$header], $value); } /** @@ -282,4 +220,32 @@ public function removeHeader($header) unset($this->headers[strtolower($header)]); } + + /** + * Test that an array contains only strings + * + * @param array $array + * @return bool + */ + private function arrayContainsOnlyStrings(array $array) + { + return array_reduce($array, [ __CLASS__, 'filterStringValue'], true); + } + + /** + * Test if a value is a string + * + * Used with array_reduce. + * + * @param bool $carry + * @param mixed $item + * @return bool + */ + private static function filterStringValue($carry, $item) + { + if (! is_string($item)) { + return false; + } + return $carry; + } } diff --git a/test/IncomingRequestFactoryTest.php b/test/IncomingRequestFactoryTest.php index 8042652..9991d63 100644 --- a/test/IncomingRequestFactoryTest.php +++ b/test/IncomingRequestFactoryTest.php @@ -403,6 +403,6 @@ public function testCanCreateIncomingRequestViaFromGlobalsMethod() $this->assertEquals($query, $request->getQueryParams()); $this->assertEquals($body, $request->getBodyParams()); $this->assertEquals($files, $request->getFileParams()); - $this->assertEmpty($request->getPathParams()); + $this->assertEmpty($request->getAttributes()); } } diff --git a/test/IncomingRequestTest.php b/test/IncomingRequestTest.php index 9d38e5b..549d6ea 100644 --- a/test/IncomingRequestTest.php +++ b/test/IncomingRequestTest.php @@ -31,9 +31,9 @@ public function testBodyParamsAreEmptyByDefault() $this->assertEmpty($this->request->getBodyParams()); } - public function testPathParamsAreEmptyByDefault() + public function testAttributesAreEmptyByDefault() { - $this->assertEmpty($this->request->getPathParams()); + $this->assertEmpty($this->request->getAttributes()); } /** @@ -63,35 +63,35 @@ public function testBodyParamsAreMutable() } /** - * @depends testPathParamsAreEmptyByDefault + * @depends testAttributesAreEmptyByDefault */ - public function testPathParamsAreMutable() + public function testAttributesAreMutable() { $params = [ 'foo' => 'bar', 'baz' => 'bat', ]; - $this->request->setPathParams($params); - $this->assertEquals($params, $this->request->getPathParams()); + $this->request->setAttributes($params); + $this->assertEquals($params, $this->request->getAttributes()); } public function testRequestDataMayBeSetAsDiscreteConstructorArguments() { - $cookies = $path = $query = $body = $files = [ + $cookies = $attributes = $query = $body = $files = [ 'foo' => 'bar', 'baz' => 'bat', ]; $cookies['cookies'] = true; - $path['path'] = true; + $attributes['path'] = true; $query['query'] = true; $body['body'] = true; $files['files'] = true; - $request = new IncomingRequest('php://memory', $cookies, $path, $query, $body, $files); + $request = new IncomingRequest('php://memory', $cookies, $attributes, $query, $body, $files); $this->assertEquals($cookies, $request->getCookieParams()); - $this->assertEquals($path, $request->getPathParams()); + $this->assertEquals($attributes, $request->getAttributes()); $this->assertEquals($query, $request->getQueryParams()); $this->assertEquals($body, $request->getBodyParams()); $this->assertEquals($files, $request->getFileParams()); @@ -99,13 +99,13 @@ public function testRequestDataMayBeSetAsDiscreteConstructorArguments() public function testRequestDataMayBePassedViaAnAssociativeArray() { - $cookies = $path = $query = $body = $files = [ + $cookies = $attributes = $query = $body = $files = [ 'foo' => 'bar', 'baz' => 'bat', ]; $cookies['cookies'] = true; - $path['path'] = true; + $attributes['path'] = true; $query['query'] = true; $body['body'] = true; $files['files'] = true; @@ -113,7 +113,7 @@ public function testRequestDataMayBePassedViaAnAssociativeArray() $data = [ 'stream' => 'php://memory', 'cookieParams' => $cookies, - 'pathParams' => $path, + 'attributes' => $attributes, 'queryParams' => $query, 'bodyParams' => $body, 'fileParams' => $files, @@ -122,7 +122,7 @@ public function testRequestDataMayBePassedViaAnAssociativeArray() $request = new IncomingRequest($data); $this->assertEquals($cookies, $request->getCookieParams()); - $this->assertEquals($path, $request->getPathParams()); + $this->assertEquals($attributes, $request->getAttributes()); $this->assertEquals($query, $request->getQueryParams()); $this->assertEquals($body, $request->getBodyParams()); $this->assertEquals($files, $request->getFileParams()); diff --git a/test/MessageTraitTest.php b/test/MessageTraitTest.php index e692418..2110f3c 100644 --- a/test/MessageTraitTest.php +++ b/test/MessageTraitTest.php @@ -25,23 +25,6 @@ public function testBodyIsMutable() $this->assertSame($stream, $this->message->getBody()); } - public function testCanSetHeaders() - { - $headers = array( - 'Origin' => 'http://example.com', - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer foobartoken', - ); - - $this->message->setHeaders($headers); - $expected = array_change_key_case($headers); - array_walk($expected, function (&$value) { - $value = [$value]; - }); - $this->assertEquals($expected, $this->message->getHeaders()); - } - public function testGetHeaderAsArrayReturnsHeaderValueAsArray() { $this->message->setHeader('X-Foo', ['Foo', 'Bar']); @@ -72,25 +55,6 @@ public function testAddHeaderAppendsToExistingHeader() $this->assertEquals('Foo,Bar', $this->message->getHeader('X-Foo')); } - public function testAddHeadersMergesWithExistingHeaders() - { - $headers = [ - 'Origin' => 'http://example.com', - 'Accept' => 'application/json', - 'Content-Type' => 'application/json', - 'Authorization' => 'Bearer foobartoken', - ]; - $this->message->setHeaders($headers); - - $this->message->addHeaders([ - 'Accept' => 'application/*+json', - 'X-Foo' => 'Foo', - ]); - - $this->assertEquals(['application/json', 'application/*+json'], $this->message->getHeaderAsArray('accept')); - $this->assertEquals('Foo', $this->message->getHeader('x-foo')); - } - public function testCanRemoveHeaders() { $this->message->setHeader('X-Foo', 'Foo'); @@ -107,7 +71,7 @@ public function invalidGeneralHeaderValues() 'false' => [false], 'int' => [1], 'float' => [1.1], - 'array' => [[ 'foo' => 'bar' ]], + 'array' => [[ 'foo' => [ 'bar' ] ]], 'object' => [(object) [ 'foo' => 'bar' ]], ]; } @@ -142,19 +106,10 @@ public function testSetHeaderRaisesExceptionForInvalidValueType($value) $this->message->setHeader('X-Foo', $value); } - public function testSetHeadersRaisesExceptionForNonStringKeys() - { - $this->setExpectedException('InvalidArgumentException', 'not a string'); - $this->message->setHeaders([ - 'application/json', - 'text/plain', - ]); - } - /** * @dataProvider invalidGeneralHeaderValues */ - public function testAddHeaderRaisesExceptionForNonStringValue($value) + public function testAddHeaderRaisesExceptionForNonStringNonArrayValue($value) { $this->setExpectedException('InvalidArgumentException', 'must be a string'); $this->message->addHeader('X-Foo', $value); @@ -166,28 +121,4 @@ public function testRemoveHeaderDoesNothingIfHeaderDoesNotExist() $this->message->removeHeader('X-Foo'); $this->assertFalse($this->message->hasHeader('X-Foo')); } - - public function testAllowAddingHeaderValuesUsingObjectsThatCastToString() - { - $header = new TestAsset\Header(); - $header->value = 'foo; bar; baz, bat'; - $this->message->addHeader('X-Foo', $header); - $this->assertEquals((string) $header, $this->message->getHeader('X-Foo')); - } - - public function testAllowSettingArrayOfHeaderValuesUsingObjectsThatCastToString() - { - $values = []; - foreach (range(1, 5) as $i) { - $header = new TestAsset\Header(); - $header->value = 'foo(' . $i . ')'; - $values[] = $header; - } - $this->message->setHeader('X-Foo', $values); - - $value = $this->message->getHeader('X-Foo'); - foreach (range(1, 5) as $i) { - $this->assertContains('foo(' . $i . ')', $value); - } - } } diff --git a/test/ResponseTest.php b/test/ResponseTest.php index 3fe1f82..7bb48b6 100644 --- a/test/ResponseTest.php +++ b/test/ResponseTest.php @@ -62,20 +62,4 @@ public function testConstructorRaisesExceptionForInvalidStream() $this->setExpectedException('InvalidArgumentException'); new Response([ 'TOTALLY INVALID' ]); } - - public function testSetHeadersDelegatesToParent() - { - $this->response->setHeaders([ - 'Content-Type' => 'application/json', - ]); - $this->assertTrue($this->response->hasHeader('Content-Type')); - } - - public function testAddHeadersDelegatesToParent() - { - $this->response->addHeaders([ - 'Content-Type' => 'application/json', - ]); - $this->assertTrue($this->response->hasHeader('Content-Type')); - } } diff --git a/test/TestAsset/Header.php b/test/TestAsset/Header.php deleted file mode 100644 index 4d5400d..0000000 --- a/test/TestAsset/Header.php +++ /dev/null @@ -1,12 +0,0 @@ -value; - } -}