From dc0d1824f914dac337d701c5df18e241db9557a6 Mon Sep 17 00:00:00 2001 From: Moritz Kornher Date: Sun, 14 Jun 2015 21:43:03 +1200 Subject: [PATCH 1/2] Added ResponseTrait to easily create custom responses. --- src/Response.php | 136 +------------------------- src/ResponseTrait.php | 190 +++++++++++++++++++++++++++++++++++++ test/ResponseTest.php | 55 +---------- test/ResponseTraitTest.php | 80 ++++++++++++++++ 4 files changed, 272 insertions(+), 189 deletions(-) create mode 100644 src/ResponseTrait.php create mode 100644 test/ResponseTraitTest.php diff --git a/src/Response.php b/src/Response.php index ef250e6a..2cb643f7 100644 --- a/src/Response.php +++ b/src/Response.php @@ -22,87 +22,7 @@ */ class Response implements ResponseInterface { - use MessageTrait; - - /** - * Map of standard HTTP status code/reason phrases - * - * @var array - */ - private $phrases = [ - // INFORMATIONAL CODES - 100 => 'Continue', - 101 => 'Switching Protocols', - 102 => 'Processing', - // SUCCESS CODES - 200 => 'OK', - 201 => 'Created', - 202 => 'Accepted', - 203 => 'Non-Authoritative Information', - 204 => 'No Content', - 205 => 'Reset Content', - 206 => 'Partial Content', - 207 => 'Multi-status', - 208 => 'Already Reported', - // REDIRECTION CODES - 300 => 'Multiple Choices', - 301 => 'Moved Permanently', - 302 => 'Found', - 303 => 'See Other', - 304 => 'Not Modified', - 305 => 'Use Proxy', - 306 => 'Switch Proxy', // Deprecated - 307 => 'Temporary Redirect', - // CLIENT ERROR - 400 => 'Bad Request', - 401 => 'Unauthorized', - 402 => 'Payment Required', - 403 => 'Forbidden', - 404 => 'Not Found', - 405 => 'Method Not Allowed', - 406 => 'Not Acceptable', - 407 => 'Proxy Authentication Required', - 408 => 'Request Time-out', - 409 => 'Conflict', - 410 => 'Gone', - 411 => 'Length Required', - 412 => 'Precondition Failed', - 413 => 'Request Entity Too Large', - 414 => 'Request-URI Too Large', - 415 => 'Unsupported Media Type', - 416 => 'Requested range not satisfiable', - 417 => 'Expectation Failed', - 418 => 'I\'m a teapot', - 422 => 'Unprocessable Entity', - 423 => 'Locked', - 424 => 'Failed Dependency', - 425 => 'Unordered Collection', - 426 => 'Upgrade Required', - 428 => 'Precondition Required', - 429 => 'Too Many Requests', - 431 => 'Request Header Fields Too Large', - // SERVER ERROR - 500 => 'Internal Server Error', - 501 => 'Not Implemented', - 502 => 'Bad Gateway', - 503 => 'Service Unavailable', - 504 => 'Gateway Time-out', - 505 => 'HTTP Version not supported', - 506 => 'Variant Also Negotiates', - 507 => 'Insufficient Storage', - 508 => 'Loop Detected', - 511 => 'Network Authentication Required', - ]; - - /** - * @var string - */ - private $reasonPhrase = ''; - - /** - * @var int - */ - private $statusCode = 200; + use ResponseTrait; /** * @param string|resource|StreamInterface $stream Stream identifier and/or actual stream resource @@ -132,60 +52,6 @@ public function __construct($body = 'php://memory', $status = 200, array $header $this->headers = $headers; } - /** - * {@inheritdoc} - */ - public function getStatusCode() - { - return $this->statusCode; - } - - /** - * {@inheritdoc} - */ - public function getReasonPhrase() - { - if (! $this->reasonPhrase - && isset($this->phrases[$this->statusCode]) - ) { - $this->reasonPhrase = $this->phrases[$this->statusCode]; - } - - return $this->reasonPhrase; - } - - /** - * {@inheritdoc} - */ - public function withStatus($code, $reasonPhrase = '') - { - $this->validateStatus($code); - $new = clone $this; - $new->statusCode = (int) $code; - $new->reasonPhrase = $reasonPhrase; - return $new; - } - - /** - * Validate a status code. - * - * @param int|string $code - * @throws InvalidArgumentException on an invalid status code. - */ - private function validateStatus($code) - { - if (! is_numeric($code) - || is_float($code) - || $code < 100 - || $code >= 600 - ) { - throw new InvalidArgumentException(sprintf( - 'Invalid status code "%s"; must be an integer between 100 and 599, inclusive', - (is_scalar($code) ? $code : gettype($code)) - )); - } - } - /** * Ensure header names and values are valid. * diff --git a/src/ResponseTrait.php b/src/ResponseTrait.php new file mode 100644 index 00000000..1e8d1781 --- /dev/null +++ b/src/ResponseTrait.php @@ -0,0 +1,190 @@ + 'Continue', + 101 => 'Switching Protocols', + 102 => 'Processing', + // SUCCESS CODES + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-status', + 208 => 'Already Reported', + // REDIRECTION CODES + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + 306 => 'Switch Proxy', // Deprecated + 307 => 'Temporary Redirect', + // CLIENT ERROR + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Time-out', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Large', + 415 => 'Unsupported Media Type', + 416 => 'Requested range not satisfiable', + 417 => 'Expectation Failed', + 418 => 'I\'m a teapot', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + 425 => 'Unordered Collection', + 426 => 'Upgrade Required', + 428 => 'Precondition Required', + 429 => 'Too Many Requests', + 431 => 'Request Header Fields Too Large', + // SERVER ERROR + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Time-out', + 505 => 'HTTP Version not supported', + 506 => 'Variant Also Negotiates', + 507 => 'Insufficient Storage', + 508 => 'Loop Detected', + 511 => 'Network Authentication Required', + ]; + + /** + * @var string + */ + private $reasonPhrase = ''; + + /** + * @var int + */ + private $statusCode = 200; + + + /** + * Gets the response status code. + * + * The status code is a 3-digit integer result code of the server's attempt + * to understand and satisfy the request. + * + * @return int Status code. + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * Gets the response reason phrase associated with the status code. + * + * Because a reason phrase is not a required element in a response + * status line, the reason phrase value MAY be null. Implementations MAY + * choose to return the default RFC 7231 recommended reason phrase (or those + * listed in the IANA HTTP Status Code Registry) for the response's + * status code. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @return string Reason phrase; must return an empty string if none present. + */ + public function getReasonPhrase() + { + if (! $this->reasonPhrase + && isset($this->phrases[$this->statusCode]) + ) { + $this->reasonPhrase = $this->phrases[$this->statusCode]; + } + + return $this->reasonPhrase; + } + + /** + * Return an instance with the specified status code and, optionally, reason phrase. + * + * If no reason phrase is specified, implementations MAY choose to default + * to the RFC 7231 or IANA recommended reason phrase for the response's + * status code. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * updated status and reason phrase. + * + * @link http://tools.ietf.org/html/rfc7231#section-6 + * @link http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml + * @param int $code The 3-digit integer result code to set. + * @param string $reasonPhrase The reason phrase to use with the + * provided status code; if none is provided, implementations MAY + * use the defaults as suggested in the HTTP specification. + * @return self + * @throws \InvalidArgumentException For invalid status code arguments. + */ + public function withStatus($code, $reasonPhrase = '') + { + $this->validateStatus($code); + $new = clone $this; + $new->statusCode = (int) $code; + $new->reasonPhrase = $reasonPhrase; + return $new; + } + + /** + * Validate a status code. + * + * @param int|string $code + * @throws InvalidArgumentException on an invalid status code. + */ + private function validateStatus($code) + { + if (! is_numeric($code) + || is_float($code) + || $code < 100 + || $code >= 600 + ) { + throw new InvalidArgumentException(sprintf( + 'Invalid status code "%s"; must be an integer between 100 and 599, inclusive', + (is_scalar($code) ? $code : gettype($code)) + )); + } + } +} diff --git a/test/ResponseTest.php b/test/ResponseTest.php index 9bc2f046..191f9628 100644 --- a/test/ResponseTest.php +++ b/test/ResponseTest.php @@ -25,52 +25,6 @@ public function setUp() $this->response = new Response(); } - public function testStatusCodeIs200ByDefault() - { - $this->assertEquals(200, $this->response->getStatusCode()); - } - - public function testStatusCodeMutatorReturnsCloneWithChanges() - { - $response = $this->response->withStatus(400); - $this->assertNotSame($this->response, $response); - $this->assertEquals(400, $response->getStatusCode()); - } - - public function invalidStatusCodes() - { - return [ - 'too-low' => [99], - 'too-high' => [600], - 'null' => [null], - 'bool' => [true], - 'string' => ['foo'], - 'array' => [[200]], - 'object' => [(object) [200]], - ]; - } - - /** - * @dataProvider invalidStatusCodes - */ - public function testCannotSetInvalidStatusCode($code) - { - $this->setExpectedException('InvalidArgumentException'); - $response = $this->response->withStatus($code); - } - - public function testReasonPhraseDefaultsToStandards() - { - $response = $this->response->withStatus(422); - $this->assertEquals('Unprocessable Entity', $response->getReasonPhrase()); - } - - public function testCanSetCustomReasonPhrase() - { - $response = $this->response->withStatus(422, 'Foo Bar!'); - $this->assertEquals('Foo Bar!', $response->getReasonPhrase()); - } - public function testConstructorRaisesExceptionForInvalidStream() { $this->setExpectedException('InvalidArgumentException'); @@ -154,14 +108,7 @@ public function testConstructorIgonoresInvalidHeaders() $response = new Response('php://memory', null, $headers); $this->assertEquals($expected, $response->getHeaders()); } - - public function testReasonPhraseCanBeEmpty() - { - $response = $this->response->withStatus(599); - $this->assertInternalType('string', $response->getReasonPhrase()); - $this->assertEmpty($response->getReasonPhrase()); - } - + public function headersWithInjectionVectors() { return [ diff --git a/test/ResponseTraitTest.php b/test/ResponseTraitTest.php new file mode 100644 index 00000000..7d7a9138 --- /dev/null +++ b/test/ResponseTraitTest.php @@ -0,0 +1,80 @@ +response = new Response($this->getMock('Psr\Http\Message\StreamInterface')); + } + + public function testStatusCodeIs200ByDefault() + { + $this->assertEquals(200, $this->response->getStatusCode()); + } + + public function testStatusCodeMutatorReturnsCloneWithChanges() + { + $response = $this->response->withStatus(400); + $this->assertNotSame($this->response, $response); + $this->assertEquals(400, $response->getStatusCode()); + } + + public function invalidStatusCodes() + { + return [ + 'too-low' => [99], + 'too-high' => [600], + 'null' => [null], + 'bool' => [true], + 'string' => ['foo'], + 'array' => [[200]], + 'object' => [(object) [200]], + ]; + } + + /** + * @dataProvider invalidStatusCodes + */ + public function testCannotSetInvalidStatusCode($code) + { + $this->setExpectedException('InvalidArgumentException'); + $response = $this->response->withStatus($code); + } + + public function testReasonPhraseDefaultsToStandards() + { + $response = $this->response->withStatus(422); + $this->assertEquals('Unprocessable Entity', $response->getReasonPhrase()); + } + + public function testCanSetCustomReasonPhrase() + { + $response = $this->response->withStatus(422, 'Foo Bar!'); + $this->assertEquals('Foo Bar!', $response->getReasonPhrase()); + } + + public function testReasonPhraseCanBeEmpty() + { + $response = $this->response->withStatus(599); + $this->assertInternalType('string', $response->getReasonPhrase()); + $this->assertEmpty($response->getReasonPhrase()); + } +} From 79e0ad8e2c64d01008831b2b4c6d22facc20403a Mon Sep 17 00:00:00 2001 From: Moritz Kornher Date: Sun, 14 Jun 2015 21:51:00 +1200 Subject: [PATCH 2/2] Removed trailing whitespace. --- test/ResponseTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ResponseTest.php b/test/ResponseTest.php index 191f9628..8b5a6bc4 100644 --- a/test/ResponseTest.php +++ b/test/ResponseTest.php @@ -108,7 +108,7 @@ public function testConstructorIgonoresInvalidHeaders() $response = new Response('php://memory', null, $headers); $this->assertEquals($expected, $response->getHeaders()); } - + public function headersWithInjectionVectors() { return [