From eb7639f8101fc3b4c316b63cb9da80dd4a8b1676 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 8 Jan 2015 12:38:33 -0600 Subject: [PATCH 01/13] Refactored to use php-fig/fig-standards#400 Refactors to use interfaces as defined in linked php-fig pull request, and as implemented for PR phly/http#18. Since many signatures changed, particularly request signatures, a number of tests needed to be updated. --- composer.json | 16 +++- src/Http/Request.php | 186 +++++++++++++++++++++++++++++++------ src/Http/Response.php | 44 ++++----- src/Middleware.php | 4 +- test/FinalHandlerTest.php | 20 ++-- test/Http/RequestTest.php | 47 +++++----- test/Http/ResponseTest.php | 2 +- test/MiddlewareTest.php | 30 +++--- test/NextTest.php | 15 ++- 9 files changed, 257 insertions(+), 107 deletions(-) diff --git a/composer.json b/composer.json index 88b5c6f..cc937bd 100644 --- a/composer.json +++ b/composer.json @@ -27,8 +27,8 @@ }, "require": { "php": ">=5.4.8", - "phly/http": "~0.7.0@dev", - "psr/http-message": "~0.5.1@dev", + "phly/http": "dev-feature/psr7-full-mutability@dev", + "psr/http-message": "dev-feature/full-mutability@dev", "zendframework/zend-escaper": "~2.3@stable" }, "require-dev": { @@ -44,5 +44,15 @@ "psr-4": { "PhlyTest\\Conduit\\": "test/" } - } + }, + "repositories": [ + { + "url": "git://github.com/weierophinney/http-message.git", + "type": "vcs" + }, + { + "url": "git://github.com/weierophinney/http.git", + "type": "vcs" + } + ] } diff --git a/src/Http/Request.php b/src/Http/Request.php index bbd61d8..5850ae6 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -2,19 +2,26 @@ namespace Phly\Conduit\Http; use ArrayObject; -use Psr\Http\Message\IncomingRequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamableInterface; /** - * Decorator for PSR IncomingRequestInterface + * Decorator for PSR ServerRequestInterface * * Decorates the PSR incoming request interface to add the ability to * manipulate arbitrary instance members. * * @property \Phly\Http\Uri $originalUrl Original URL of this instance */ -class Request implements IncomingRequestInterface +class Request implements ServerRequestInterface { + /** + * Current absolute URI (URI set in the proxy) + * + * @var string + */ + private $currentAbsoluteUri; + /** * Current URL (URL set in the proxy) * @@ -23,14 +30,14 @@ class Request implements IncomingRequestInterface private $currentUrl; /** - * @var IncomingRequestInterface + * @var ServerRequestInterface */ private $psrRequest; /** - * @param IncomingRequestInterface $request + * @param ServerRequestInterface $request */ - public function __construct(IncomingRequestInterface $request) + public function __construct(ServerRequestInterface $request) { $this->psrRequest = $request; $this->originalUrl = $request->getUrl(); @@ -39,7 +46,7 @@ public function __construct(IncomingRequestInterface $request) /** * Return the original PSR request object * - * @return IncomingRequestInterface + * @return ServerRequestInterface */ public function getOriginalRequest() { @@ -96,7 +103,7 @@ public function __unset($name) } /** - * Proxy to IncomingRequestInterface::getProtocolVersion() + * Proxy to ServerRequestInterface::getProtocolVersion() * * @return string HTTP protocol version. */ @@ -106,7 +113,17 @@ public function getProtocolVersion() } /** - * Proxy to IncomingRequestInterface::getBody() + * Proxy to ServerRequestInterface::setProtocolVersion() + * + * @param string $version HTTP protocol version. + */ + public function setProtocolVersion($version) + { + return $this->psrRequest->setProtocolVersion($version); + } + + /** + * Proxy to ServerRequestInterface::getBody() * * @return StreamableInterface Returns the body stream. */ @@ -116,7 +133,18 @@ public function getBody() } /** - * Proxy to IncomingRequestInterface::getHeaders() + * Proxy to ServerRequestInterface::setBody() + * + * @param StreamableInterface $body Body. + * @throws \InvalidArgumentException When the body is not valid. + */ + public function setBody(StreamableInterface $body) + { + return $this->psrRequest->setBody($body); + } + + /** + * Proxy to ServerRequestInterface::getHeaders() * * @return array Returns an associative array of the message's headers. */ @@ -126,7 +154,7 @@ public function getHeaders() } /** - * Proxy to IncomingRequestInterface::hasHeader() + * Proxy to ServerRequestInterface::hasHeader() * * @param string $header Case-insensitive header name. * @return bool Returns true if any header names match the given header @@ -139,7 +167,7 @@ public function hasHeader($header) } /** - * Proxy to IncomingRequestInterface::getHeader() + * Proxy to ServerRequestInterface::getHeader() * * @param string $header Case-insensitive header name. * @return string @@ -150,18 +178,50 @@ public function getHeader($header) } /** - * Proxy to IncomingRequestInterface::getHeaderAsArray() + * Proxy to ServerRequestInterface::getHeaderLines() * * @param string $header Case-insensitive header name. * @return string[] */ - public function getHeaderAsArray($header) + public function getHeaderLines($header) + { + return $this->psrRequest->getHeaderLines($header); + } + + /** + * Proxy to ServerRequestInterface::setHeader() + * + * @param string $header Header name + * @param string|string[] $value Header value(s) + */ + public function setHeader($header, $value) + { + return $this->psrRequest->setHeader($header, $value); + } + + /** + * Proxy to ServerRequestInterface::addHeader() + * + * @param string $header Header name to add or append + * @param string|string[] $value Value(s) to add or merge into the header + */ + public function addHeader($header, $value) + { + return $this->psrRequest->addHeader($header, $value); + } + + /** + * Proxy to ServerRequestInterface::removeHeader() + * + * @param string $header HTTP header to remove + */ + public function removeHeader($header) { - return $this->psrRequest->getHeaderAsArray($header); + return $this->psrRequest->removeHeader($header); } /** - * Proxy to IncomingRequestInterface::getMethod() + * Proxy to ServerRequestInterface::getMethod() * * @return string Returns the request method. */ @@ -171,7 +231,50 @@ public function getMethod() } /** - * Proxy to IncomingRequestInterface::getUrl() + * Proxy to ServerRequestInterface::setMethod() + * + * @param string $method The request method. + */ + public function setMethod($method) + { + $this->psrRequest->setMethod($method); + } + + /** + * Proxy to ServerRequestInterface::getAbsoluteUri() + * + * @return string Returns the absolute URI as a string + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + */ + public function getAbsoluteUri() + { + if ($this->currentAbsoluteUri) { + return $this->currentAbsoluteUri; + } + + return $this->psrRequest->getAbsoluteUri(); + } + + /** + * Allow mutating the absolute URI + * + * Also sets originalAbsoluteUri property if not previously set. + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * @param string|object $uri Absolute request URI. + * @throws \InvalidArgumentException If the URL is invalid. + */ + public function setAbsoluteUri($uri) + { + $this->currentAbsoluteUri = $uri; + + if (! $this->originalAbsoluteUri) { + $this->originalAbsoluteUri = $this->psrRequest->getAbsoluteUri(); + } + } + + /** + * Proxy to ServerRequestInterface::getUrl() * * @return string|object Returns the URL as a string, or an object that * implements the `__toString()` method. The URL must be an absolute URI @@ -206,7 +309,7 @@ public function setUrl($url) } /** - * Proxy to IncomingRequestInterface::getServerParams() + * Proxy to ServerRequestInterface::getServerParams() * * @return array */ @@ -216,7 +319,7 @@ public function getServerParams() } /** - * Proxy to IncomingRequestInterface::getCookieParams() + * Proxy to ServerRequestInterface::getCookieParams() * * @return array */ @@ -226,7 +329,17 @@ public function getCookieParams() } /** - * Proxy to IncomingRequestInterface::getQueryParams() + * Proxy to ServerRequestInterface::setCookieParams() + * + * @param array $cookies + */ + public function setCookieParams(array $cookies) + { + $this->psrRequest->setCookieParams($cookies); + } + + /** + * Proxy to ServerRequestInterface::getQueryParams() * * @return array */ @@ -236,7 +349,17 @@ public function getQueryParams() } /** - * Proxy to IncomingRequestInterface::getFileParams() + * Proxy to ServerRequestInterface::setQueryParams() + * + * @param array $query + */ + public function setQueryParams(array $query) + { + $this->psrRequest->setQueryParams($query); + } + + /** + * Proxy to ServerRequestInterface::getFileParams() * * @return array Upload file(s) metadata, if any. */ @@ -246,7 +369,7 @@ public function getFileParams() } /** - * Proxy to IncomingRequestInterface::getBodyParams() + * Proxy to ServerRequestInterface::getBodyParams() * * * @return array The deserialized body parameters, if any. @@ -257,7 +380,18 @@ public function getBodyParams() } /** - * Proxy to IncomingRequestInterface::getAttributes() + * Proxy to ServerRequestInterface::setBodyParams() + * + * + * @param array $params The deserialized body parameters. + */ + public function setBodyParams(array $params) + { + return $this->psrRequest->setBodyParams($params); + } + + /** + * Proxy to ServerRequestInterface::getAttributes() * * @return array Attributes derived from the request */ @@ -267,7 +401,7 @@ public function getAttributes() } /** - * Proxy to IncomingRequestInterface::getAttribute() + * Proxy to ServerRequestInterface::getAttribute() * * @param string $attribute * @param mixed $default @@ -279,7 +413,7 @@ public function getAttribute($attribute, $default = null) } /** - * Proxy to IncomingRequestInterface::setAttributes() + * Proxy to ServerRequestInterface::setAttributes() * * @param array Attributes derived from the request */ @@ -289,7 +423,7 @@ public function setAttributes(array $values) } /** - * Proxy to IncomingRequestInterface::setAttribute() + * Proxy to ServerRequestInterface::setAttribute() * * @param string $attribute * @param mixed $value diff --git a/src/Http/Response.php b/src/Http/Response.php index 33f7dbb..5228f9a 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -1,7 +1,7 @@ psrResponse = $response; } @@ -35,7 +35,7 @@ public function __construct(OutgoingResponseInterface $response) /** * Return the original PSR response object * - * @return BaseResponseInterface + * @return PsrResponseInterface */ public function getOriginalResponse() { @@ -95,7 +95,7 @@ public function isComplete() } /** - * Proxy to BaseResponseInterface::getProtocolVersion() + * Proxy to PsrResponseInterface::getProtocolVersion() * * @return string HTTP protocol version. */ @@ -105,7 +105,7 @@ public function getProtocolVersion() } /** - * Proxy to BaseResponseInterface::setProtocolVersion() + * Proxy to PsrResponseInterface::setProtocolVersion() * * @param string $version * @return void @@ -116,7 +116,7 @@ public function setProtocolVersion($version) } /** - * Proxy to BaseResponseInterface::getBody() + * Proxy to PsrResponseInterface::getBody() * * @return StreamableInterface|null Returns the body, or null if not set. */ @@ -126,7 +126,7 @@ public function getBody() } /** - * Proxy to BaseResponseInterface::setBody() + * Proxy to PsrResponseInterface::setBody() * * @param StreamableInterface $body Body. * @throws \InvalidArgumentException When the body is not valid. @@ -141,7 +141,7 @@ public function setBody(StreamableInterface $body) } /** - * Proxy to BaseResponseInterface::getHeaders() + * Proxy to PsrResponseInterface::getHeaders() * * @return array Returns an associative array of the message's headers. */ @@ -151,7 +151,7 @@ public function getHeaders() } /** - * Proxy to BaseResponseInterface::hasHeader() + * Proxy to PsrResponseInterface::hasHeader() * * @param string $header Case-insensitive header name. * @return bool Returns true if any header names match the given header @@ -164,7 +164,7 @@ public function hasHeader($header) } /** - * Proxy to BaseResponseInterface::getHeader() + * Proxy to PsrResponseInterface::getHeader() * * @param string $header Case-insensitive header name. * @return string @@ -175,18 +175,18 @@ public function getHeader($header) } /** - * Proxy to BaseResponseInterface::getHeaderAsArray() + * Proxy to PsrResponseInterface::getHeaderAsArray() * * @param string $header Case-insensitive header name. * @return string[] */ - public function getHeaderAsArray($header) + public function getHeaderLines($header) { - return $this->psrResponse->getHeaderAsArray($header); + return $this->psrResponse->getHeaderLines($header); } /** - * Proxy to BaseResponseInterface::setHeader() + * Proxy to PsrResponseInterface::setHeader() * * @param string $header Header name * @param string|string[] $value Header value(s) @@ -201,7 +201,7 @@ public function setHeader($header, $value) } /** - * Proxy to BaseResponseInterface::addHeader() + * Proxy to PsrResponseInterface::addHeader() * * @param string $header Header name to add or append * @param string|string[] $value Value(s) to add or merge into the header @@ -216,7 +216,7 @@ public function addHeader($header, $value) } /** - * Proxy to BaseResponseInterface::removeHeader() + * Proxy to PsrResponseInterface::removeHeader() * * @param string $header HTTP header to remove */ @@ -230,7 +230,7 @@ public function removeHeader($header) } /** - * Proxy to BaseResponseInterface::getStatusCode() + * Proxy to PsrResponseInterface::getStatusCode() * * @return integer Status code. */ @@ -240,7 +240,7 @@ public function getStatusCode() } /** - * Proxy to BaseResponseInterface::setStatus() + * Proxy to PsrResponseInterface::setStatus() * * @param integer $code The 3-digit integer result code to set. * @param null|string $reasonPhrase The reason phrase to use with the status, if any. @@ -255,7 +255,7 @@ public function setStatus($code, $reasonPhrase = null) } /** - * Proxy to BaseResponseInterface::getReasonPhrase() + * Proxy to PsrResponseInterface::getReasonPhrase() * * @return string|null Reason phrase, or null if unknown. */ diff --git a/src/Middleware.php b/src/Middleware.php index afc56b7..8458635 100644 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -4,8 +4,8 @@ use ArrayObject; use InvalidArgumentException; use Phly\Http\Uri; -use Psr\Http\Message\IncomingRequestInterface as Request; -use Psr\Http\Message\OutgoingResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; /** * Middleware diff --git a/test/FinalHandlerTest.php b/test/FinalHandlerTest.php index f71ca79..87743c8 100644 --- a/test/FinalHandlerTest.php +++ b/test/FinalHandlerTest.php @@ -5,8 +5,8 @@ use Phly\Conduit\FinalHandler; use Phly\Conduit\Http\Request; use Phly\Conduit\Http\Response; -use Phly\Http\IncomingRequest as PsrRequest; -use Phly\Http\OutgoingResponse as PsrResponse; +use Phly\Http\ServerRequest as PsrRequest; +use Phly\Http\Response as PsrResponse; use PHPUnit_Framework_TestCase as TestCase; use Zend\Escaper\Escaper; @@ -14,8 +14,12 @@ class FinalHandlerTest extends TestCase { public function setUp() { + $psrRequest = new PsrRequest('php://memory'); + $psrRequest->setMethod('GET'); + $psrRequest->setAbsoluteUri('http://example.com/'); + $this->escaper = new Escaper(); - $this->request = new Request(new PsrRequest('http://example.com/', 'GET', [], 'php://memory')); + $this->request = new Request($psrRequest); $this->response = new Response(new PsrResponse()); $this->final = new FinalHandler($this->request, $this->response); } @@ -103,12 +107,10 @@ public function testCreates404ResponseWhenNoErrorIsPresent() public function test404ResponseIncludesOriginalRequestUrl() { $originalUrl = 'http://local.example.com/bar/foo'; - $request = new Request(new PsrRequest( - $originalUrl, - 'GET', - [], - 'php://memory' - )); + $psrRequest = new PsrRequest('php://memory'); + $psrRequest->setMethod('GET'); + $psrRequest->setAbsoluteUri($originalUrl); + $request = new Request($psrRequest); $request->setUrl('http://local.example.com/foo'); $final = new FinalHandler($request, $this->response); diff --git a/test/Http/RequestTest.php b/test/Http/RequestTest.php index b65fbe0..f2d10d4 100644 --- a/test/Http/RequestTest.php +++ b/test/Http/RequestTest.php @@ -2,7 +2,7 @@ namespace PhlyTest\Conduit\Http; use Phly\Conduit\Http\Request; -use Phly\Http\IncomingRequest as PsrRequest; +use Phly\Http\ServerRequest as PsrRequest; use Phly\Http\Uri; use PHPUnit_Framework_TestCase as TestCase; @@ -10,12 +10,9 @@ class RequestTest extends TestCase { public function setUp() { - $this->original = new PsrRequest( - 'http://example.com/', - 'GET', - [], - 'php://memory' - ); + $this->original = new PsrRequest('php://memory'); + $this->original->setMethod('GET'); + $this->original->setAbsoluteUri('http://example.com/'); $this->request = new Request($this->original); } @@ -41,23 +38,29 @@ public function testArrayPropertyValueIsCastToArrayObject() $this->assertEquals($original, $this->request->anArray->getArrayCopy()); } - public function testCallingSetUrlSetsOriginalUrlProperty() + public function testCallingSetAbsoluteUriSetsOriginalAbsoluteUriProperty() { $url = 'http://example.com/foo'; + $this->request->setAbsoluteUri($url); + $this->assertSame('http://example.com/', $this->request->originalAbsoluteUri); + $this->assertSame($url, $this->request->getAbsoluteUri()); + } + + public function testCallingSetUrlSetsOriginalUrlProperty() + { + $url = '/foo'; $this->request->setUrl($url); - $this->assertSame('http://example.com/', $this->request->originalUrl); + $this->assertSame('/', $this->request->originalUrl); $this->assertSame($url, $this->request->getUrl()); } public function testConstructorSetsOriginalUrlIfDecoratedRequestHasUrl() { $url = 'http://example.com/foo'; - $baseRequest = new PsrRequest( - $url, - 'GET', - [], - 'php://memory' - ); + $baseRequest = new PsrRequest('php://memory'); + $baseRequest->setMethod('GET'); + $baseRequest->setAbsoluteUri($url); + $request = new Request($baseRequest); $this->assertSame($baseRequest->getUrl(), $request->originalUrl); } @@ -70,15 +73,11 @@ public function testCanAccessOriginalRequest() public function testDecoratorProxiesToAllMethods() { $stream = $this->getMock('Psr\Http\Message\StreamableInterface'); - $psrRequest = new PsrRequest( - 'http://example.com/', - 'POST', - [ - 'Accept' => 'application/xml', - 'X-URL' => 'http://example.com/foo', - ], - $stream - ); + $psrRequest = new PsrRequest($stream); + $psrRequest->setMethod('POST'); + $psrRequest->setAbsoluteUri('http://example.com/'); + $psrRequest->setHeader('Accept', 'application/xml'); + $psrRequest->setHeader('X-URL', 'http://example.com/foo'); $request = new Request($psrRequest); $this->assertEquals('1.1', $request->getProtocolVersion()); diff --git a/test/Http/ResponseTest.php b/test/Http/ResponseTest.php index 805bba1..d656744 100644 --- a/test/Http/ResponseTest.php +++ b/test/Http/ResponseTest.php @@ -2,7 +2,7 @@ namespace PhlyTest\Conduit\Http; use Phly\Conduit\Http\Response; -use Phly\Http\OutgoingResponse as PsrResponse; +use Phly\Http\Response as PsrResponse; use Phly\Http\Stream; use PHPUnit_Framework_TestCase as TestCase; diff --git a/test/MiddlewareTest.php b/test/MiddlewareTest.php index 638dc05..6cc903c 100644 --- a/test/MiddlewareTest.php +++ b/test/MiddlewareTest.php @@ -5,8 +5,8 @@ use Phly\Conduit\Http\Response as ResponseDecorator; use Phly\Conduit\Middleware; use Phly\Conduit\Utils; -use Phly\Http\IncomingRequest as Request; -use Phly\Http\OutgoingResponse as Response; +use Phly\Http\ServerRequest as Request; +use Phly\Http\Response; use PHPUnit_Framework_TestCase as TestCase; use ReflectionProperty; @@ -14,12 +14,10 @@ class MiddlewareTest extends TestCase { public function setUp() { - $this->request = new Request( - 'http://example.com/', - 'GET', - [], - 'php://memory' - ); + $this->request = new Request('php://memory'); + $this->request->setMethod('GET'); + $this->request->setAbsoluteUri('http://example.com/'); + $this->response = new Response(); $this->middleware = new Middleware(); } @@ -64,7 +62,9 @@ public function testHandleInvokesUntilFirstHandlerThatDoesNotCallNext() $phpunit->fail('Should not hit fourth handler!'); }); - $request = new Request('http://local.example.com/foo', 'GET', [], 'php://memory'); + $request = new Request('php://memory'); + $request->setMethod('GET'); + $request->setAbsoluteUri('http://local.example.com/foo'); $this->middleware->__invoke($request, $this->response); $body = (string) $this->response->getBody(); $this->assertContains('First', $body); @@ -92,7 +92,9 @@ public function testHandleInvokesFirstErrorHandlerOnErrorInChain() $phpunit->fail('Should not hit fourth handler!'); }); - $request = new Request('http://local.example.com/foo', 'GET', [], 'php://memory'); + $request = new Request('php://memory'); + $request->setMethod('GET'); + $request->setAbsoluteUri('http://local.example.com/foo'); $this->middleware->__invoke($request, $this->response); $body = (string) $this->response->getBody(); $this->assertContains('First', $body); @@ -117,7 +119,9 @@ public function testHandleInvokesOutHandlerIfStackIsExhausted() $next(); }); - $request = new Request('http://local.example.com/foo', 'GET', [], 'php://memory'); + $request = new Request('php://memory'); + $request->setMethod('GET'); + $request->setAbsoluteUri('http://local.example.com/foo'); $this->middleware->__invoke($request, $this->response, $out); $this->assertTrue($triggered); } @@ -153,7 +157,9 @@ public function testPipeWillCreateErrorClosureForObjectImplementingHandle() public function testCanUseDecoratedRequestAndResponseDirectly() { - $baseRequest = new Request('http://local.example.com/foo', 'GET', [], 'php://memory'); + $baseRequest = new Request('php://memory'); + $baseRequest->setMethod('GET'); + $baseRequest->setAbsoluteUri('http://local.example.com/foo'); $request = new RequestDecorator($baseRequest); $response = new ResponseDecorator($this->response); diff --git a/test/NextTest.php b/test/NextTest.php index 7245334..87fc290 100644 --- a/test/NextTest.php +++ b/test/NextTest.php @@ -6,21 +6,20 @@ use Phly\Conduit\Http\Response; use Phly\Conduit\Next; use Phly\Conduit\Route; -use Phly\Http\IncomingRequest as PsrRequest; -use Phly\Http\OutgoingResponse as PsrResponse; +use Phly\Http\ServerRequest as PsrRequest; +use Phly\Http\Response as PsrResponse; use PHPUnit_Framework_TestCase as TestCase; class NextTest extends TestCase { public function setUp() { + $psrRequest = new PsrRequest('php://memory'); + $psrRequest->setMethod('GET'); + $psrRequest->setAbsoluteUri('http://example.com/'); + $this->stack = new ArrayObject(); - $this->request = new Request(new PsrRequest( - 'http://example.com/', - 'GET', - [], - 'php://memory' - )); + $this->request = new Request($psrRequest); $this->response = new Response(new PsrResponse()); } From 139a921e3713de960e8f661d18f6e0fc10853fae Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 8 Jan 2015 13:25:24 -0600 Subject: [PATCH 02/13] Fix FinalHandler::create404 and related test - Test was not calling setAbsoluteUri() when giving the absolute URI the second time. - Changes to FinalHandler::create404 which queried the absolute URI vs the URL had not been committed. --- src/FinalHandler.php | 2 +- test/FinalHandlerTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/FinalHandler.php b/src/FinalHandler.php index 4d87213..3d8281e 100644 --- a/src/FinalHandler.php +++ b/src/FinalHandler.php @@ -88,7 +88,7 @@ private function create404() { $this->response->setStatus(404); - $url = $this->request->originalUrl ?: $this->request->getUrl(); + $url = $this->request->originalAbsoluteUri ?: $this->request->getAbsoluteUri(); $escaper = new Escaper(); $message = sprintf( "Cannot %s %s\n", diff --git a/test/FinalHandlerTest.php b/test/FinalHandlerTest.php index 87743c8..502b379 100644 --- a/test/FinalHandlerTest.php +++ b/test/FinalHandlerTest.php @@ -104,14 +104,14 @@ public function testCreates404ResponseWhenNoErrorIsPresent() $this->assertEquals(404, $this->response->getStatusCode()); } - public function test404ResponseIncludesOriginalRequestUrl() + public function test404ResponseIncludesOriginalRequestAbsoluteUri() { $originalUrl = 'http://local.example.com/bar/foo'; $psrRequest = new PsrRequest('php://memory'); $psrRequest->setMethod('GET'); $psrRequest->setAbsoluteUri($originalUrl); $request = new Request($psrRequest); - $request->setUrl('http://local.example.com/foo'); + $request->setAbsoluteUri('http://local.example.com/foo'); $final = new FinalHandler($request, $this->response); call_user_func($final); From b76f80124da005faddf2498178a8a6ff61534726 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Mon, 12 Jan 2015 13:37:36 -0600 Subject: [PATCH 03/13] Use immutable messages feature branch of phly/http --- composer.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index cc937bd..6bede14 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ }, "require": { "php": ">=5.4.8", - "phly/http": "dev-feature/psr7-full-mutability@dev", + "phly/http": "dev-feature/immutability@dev", "psr/http-message": "dev-feature/full-mutability@dev", "zendframework/zend-escaper": "~2.3@stable" }, @@ -47,11 +47,11 @@ }, "repositories": [ { - "url": "git://github.com/weierophinney/http-message.git", + "url": "https://github.com/weierophinney/http-message.git", "type": "vcs" }, { - "url": "git://github.com/weierophinney/http.git", + "url": "https://github.com/weierophinney/http.git", "type": "vcs" } ] From 459726007a567fa09354adf79aa91b9c819a4a44 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 13 Jan 2015 08:35:35 -0600 Subject: [PATCH 04/13] Refactor to pick up immutable messages Refactored to use immutable messages. Required a few changes: - Next() now allows the following: - If the first argument is a Response, it is immediately returned. - If the first argument is a Request, it replaces the currently stored request. - If the second argument is a Response, it replaces the currently stored response. - Returning a Response from any middleware should now end iteration. - FinalHandler() was modified to return a response. --- src/Dispatch.php | 8 +- src/FinalHandler.php | 30 +++--- src/Http/Request.php | 182 +++++++++++++++---------------------- src/Http/Response.php | 48 ++++++---- src/Middleware.php | 16 ---- src/Next.php | 59 +++++++++--- test/DispatchTest.php | 53 +++++++++++ test/FinalHandlerTest.php | 72 +++++++-------- test/Http/RequestTest.php | 90 ++++++++---------- test/Http/ResponseTest.php | 76 +++++++++------- test/MiddlewareTest.php | 23 ++--- test/NextTest.php | 128 ++++++++++++++++++++++++-- 12 files changed, 471 insertions(+), 314 deletions(-) diff --git a/src/Dispatch.php b/src/Dispatch.php index cbb3c4c..7d08526 100644 --- a/src/Dispatch.php +++ b/src/Dispatch.php @@ -53,18 +53,16 @@ public function __invoke( try { if ($hasError && $arity === 4) { - call_user_func($handler, $err, $request, $response, $next); - return; + return $response = call_user_func($handler, $err, $request, $response, $next); } if (! $hasError && $arity < 4) { - call_user_func($handler, $request, $response, $next); - return; + return call_user_func($handler, $request, $response, $next); } } catch (Exception $e) { $err = $e; } - $next($err); + return $next($err); } } diff --git a/src/FinalHandler.php b/src/FinalHandler.php index 3d8281e..7cca058 100644 --- a/src/FinalHandler.php +++ b/src/FinalHandler.php @@ -45,15 +45,15 @@ public function __construct(Http\Request $request, Http\Response $response, arra * Otherwise, a 404 status is created. * * @param null|mixed $err + * @return Http\Response */ public function __invoke($err = null) { if ($err) { - $this->handleError($err); - return; + return $this->handleError($err); } - $this->create404(); + return $this->create404(); } /** @@ -62,40 +62,46 @@ public function __invoke($err = null) * Use the $error to create details for the response. * * @param mixed $error + * @return Http\Response */ private function handleError($error) { - $this->response->setStatus( + $response = $this->response->setStatus( $this->getStatusCode($error, $this->response) ); - $message = $this->response->getReasonPhrase() ?: 'Unknown Error'; + $message = $response->getReasonPhrase() ?: 'Unknown Error'; if (! isset($this->options['env']) || $this->options['env'] !== 'production' ) { $message = $this->createDevelopmentErrorMessage($error); } - $this->triggerError($error, $this->request, $this->response); + $this->triggerError($error, $this->request, $response); - $this->response->end($message); + $response->end($message); + return $response; } /** * Create a 404 status in the response + * + * @return Http\Response */ private function create404() { - $this->response->setStatus(404); + $response = $this->response->setStatus(404); - $url = $this->request->originalAbsoluteUri ?: $this->request->getAbsoluteUri(); - $escaper = new Escaper(); - $message = sprintf( + $originalRequest = $this->request->getOriginalRequest(); + $url = $originalRequest->getAbsoluteUri(); + $escaper = new Escaper(); + $message = sprintf( "Cannot %s %s\n", $escaper->escapeHtml($this->request->getMethod()), $escaper->escapeHtml((string) $url) ); - $this->response->end($message); + $response->end($message); + return $response; } /** diff --git a/src/Http/Request.php b/src/Http/Request.php index 5850ae6..e51da5b 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -8,7 +8,7 @@ /** * Decorator for PSR ServerRequestInterface * - * Decorates the PSR incoming request interface to add the ability to + * Decorates the PSR incoming request interface to add the ability to * manipulate arbitrary instance members. * * @property \Phly\Http\Uri $originalUrl Original URL of this instance @@ -16,20 +16,17 @@ class Request implements ServerRequestInterface { /** - * Current absolute URI (URI set in the proxy) - * - * @var string - */ - private $currentAbsoluteUri; - - /** - * Current URL (URL set in the proxy) - * - * @var string + * Original ServerRequestInterface instance. + * + * @var mixed */ - private $currentUrl; + private $originalRequest; /** + * The currently decorated ServerRequestInterface instance; it may or may + * not be the same as the originalRequest, depending on how many changes + * have been pushed to the original. + * * @var ServerRequestInterface */ private $psrRequest; @@ -37,69 +34,36 @@ class Request implements ServerRequestInterface /** * @param ServerRequestInterface $request */ - public function __construct(ServerRequestInterface $request) - { - $this->psrRequest = $request; - $this->originalUrl = $request->getUrl(); - } - - /** - * Return the original PSR request object - * - * @return ServerRequestInterface - */ - public function getOriginalRequest() - { - return $this->psrRequest; - } - - /** - * Property overloading: get property value - * - * Returns null if property is not set. - * - * @param string $name - * @return mixed - */ - public function __get($name) - { - return $this->psrRequest->getAttribute($name); - } - - /** - * Property overloading: set property - * - * @param string $name - * @param mixed $value - */ - public function __set($name, $value) - { - if (is_array($value)) { - $value = new ArrayObject($value, ArrayObject::ARRAY_AS_PROPS); + public function __construct( + ServerRequestInterface $decoratedRequest, + ServerRequestInterface $originalRequest = null + ) { + if (null === $originalRequest) { + $originalRequest = $decoratedRequest; } - return $this->psrRequest->setAttribute($name, $value); + $this->originalRequest = $originalRequest; + $this->psrRequest = $decoratedRequest->setAttribute('originalUrl', $originalRequest->getUrl()); } /** - * Property overloading: is property set? + * Return the currently decorated PSR request instance * - * @param string $name - * @return bool + * @return ServerRequestInterface */ - public function __isset($name) + public function getCurrentRequest() { - return (bool) $this->psrRequest->getAttribute($name, false); + return $this->psrRequest; } /** - * Property overloading: unset property + * Return the original PSR request instance * - * @param string $name + * @return ServerRequestInterface */ - public function __unset($name) + public function getOriginalRequest() { - $this->psrRequest->setAttribute($name, null); + return $this->originalRequest; } /** @@ -116,10 +80,12 @@ public function getProtocolVersion() * Proxy to ServerRequestInterface::setProtocolVersion() * * @param string $version HTTP protocol version. + * @return Request */ public function setProtocolVersion($version) { - return $this->psrRequest->setProtocolVersion($version); + $new = $this->psrRequest->setProtocolVersion($version); + return new self($new, $this->originalRequest); } /** @@ -136,11 +102,13 @@ public function getBody() * Proxy to ServerRequestInterface::setBody() * * @param StreamableInterface $body Body. + * @return Request * @throws \InvalidArgumentException When the body is not valid. */ public function setBody(StreamableInterface $body) { - return $this->psrRequest->setBody($body); + $new = $this->psrRequest->setBody($body); + return new self($new, $this->originalRequest); } /** @@ -193,10 +161,12 @@ public function getHeaderLines($header) * * @param string $header Header name * @param string|string[] $value Header value(s) + * @return Request */ public function setHeader($header, $value) { - return $this->psrRequest->setHeader($header, $value); + $new = $this->psrRequest->setHeader($header, $value); + return new self($new, $this->originalRequest); } /** @@ -204,20 +174,24 @@ public function setHeader($header, $value) * * @param string $header Header name to add or append * @param string|string[] $value Value(s) to add or merge into the header + * @return Request */ public function addHeader($header, $value) { - return $this->psrRequest->addHeader($header, $value); + $new = $this->psrRequest->addHeader($header, $value); + return new self($new, $this->originalRequest); } /** * Proxy to ServerRequestInterface::removeHeader() * * @param string $header HTTP header to remove + * @return Request */ public function removeHeader($header) { - return $this->psrRequest->removeHeader($header); + $new = $this->psrRequest->removeHeader($header); + return new self($new, $this->originalRequest); } /** @@ -234,10 +208,12 @@ public function getMethod() * Proxy to ServerRequestInterface::setMethod() * * @param string $method The request method. + * @return Request */ public function setMethod($method) { - $this->psrRequest->setMethod($method); + $new = $this->psrRequest->setMethod($method); + return new self($new, $this->originalRequest); } /** @@ -248,29 +224,21 @@ public function setMethod($method) */ public function getAbsoluteUri() { - if ($this->currentAbsoluteUri) { - return $this->currentAbsoluteUri; - } - return $this->psrRequest->getAbsoluteUri(); } /** * Allow mutating the absolute URI * - * Also sets originalAbsoluteUri property if not previously set. - * * @link http://tools.ietf.org/html/rfc3986#section-4.3 * @param string|object $uri Absolute request URI. + * @return Request * @throws \InvalidArgumentException If the URL is invalid. */ public function setAbsoluteUri($uri) { - $this->currentAbsoluteUri = $uri; - - if (! $this->originalAbsoluteUri) { - $this->originalAbsoluteUri = $this->psrRequest->getAbsoluteUri(); - } + $new = $this->psrRequest->setAbsoluteUri($uri); + return new self($new, $this->originalRequest); } /** @@ -283,34 +251,26 @@ public function setAbsoluteUri($uri) */ public function getUrl() { - if ($this->currentUrl) { - return $this->currentUrl; - } - return $this->psrRequest->getUrl(); } /** * Allow mutating the URL * - * Also sets originalUrl property if not previously set. - * * @link http://tools.ietf.org/html/rfc3986#section-4.3 * @param string|object $url Request URL. + * @return Request * @throws \InvalidArgumentException If the URL is invalid. */ public function setUrl($url) { - $this->currentUrl = $url; - - if (! $this->originalUrl) { - $this->originalUrl = $this->psrRequest->getUrl(); - } + $new = $this->psrRequest->setUrl($url); + return new self($new, $this->originalRequest); } /** * Proxy to ServerRequestInterface::getServerParams() - * + * * @return array */ public function getServerParams() @@ -332,15 +292,17 @@ public function getCookieParams() * Proxy to ServerRequestInterface::setCookieParams() * * @param array $cookies + * @return Request */ public function setCookieParams(array $cookies) { - $this->psrRequest->setCookieParams($cookies); + $new = $this->psrRequest->setCookieParams($cookies); + return new self($new, $this->originalRequest); } /** * Proxy to ServerRequestInterface::getQueryParams() - * + * * @return array */ public function getQueryParams() @@ -350,17 +312,19 @@ public function getQueryParams() /** * Proxy to ServerRequestInterface::setQueryParams() - * + * * @param array $query + * @return Request */ public function setQueryParams(array $query) { - $this->psrRequest->setQueryParams($query); + $new = $this->psrRequest->setQueryParams($query); + return new self($new, $this->originalRequest); } /** * Proxy to ServerRequestInterface::getFileParams() - * + * * @return array Upload file(s) metadata, if any. */ public function getFileParams() @@ -371,7 +335,7 @@ public function getFileParams() /** * Proxy to ServerRequestInterface::getBodyParams() * - * + * * @return array The deserialized body parameters, if any. */ public function getBodyParams() @@ -382,12 +346,13 @@ public function getBodyParams() /** * Proxy to ServerRequestInterface::setBodyParams() * - * * @param array $params The deserialized body parameters. + * @return Request */ public function setBodyParams(array $params) { - return $this->psrRequest->setBodyParams($params); + $new = $this->psrRequest->setBodyParams($params); + return new self($new, $this->originalRequest); } /** @@ -402,9 +367,9 @@ public function getAttributes() /** * Proxy to ServerRequestInterface::getAttribute() - * - * @param string $attribute - * @param mixed $default + * + * @param string $attribute + * @param mixed $default * @return mixed */ public function getAttribute($attribute, $default = null) @@ -416,21 +381,24 @@ public function getAttribute($attribute, $default = null) * Proxy to ServerRequestInterface::setAttributes() * * @param array Attributes derived from the request + * @return Request */ public function setAttributes(array $values) { - return $this->psrRequest->setAttributes($values); + $new = $this->psrRequest->setAttributes($values); + return new self($new, $this->originalRequest); } /** * Proxy to ServerRequestInterface::setAttribute() - * - * @param string $attribute - * @param mixed $value - * @return void + * + * @param string $attribute + * @param mixed $value + * @return Request */ public function setAttribute($attribute, $value) { - return $this->psrRequest->setAttribute($attribute, $value); + $new = $this->psrRequest->setAttribute($attribute, $value); + return new self($new, $this->originalRequest); } } diff --git a/src/Http/Response.php b/src/Http/Response.php index 5228f9a..ff74c98 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -52,10 +52,11 @@ public function getOriginalResponse() public function write($data) { if ($this->complete) { - return; + return $this; } $this->getBody()->write($data); + return $this; } /** @@ -72,14 +73,16 @@ public function write($data) public function end($data = null) { if ($this->complete) { - return; + return $this; } if ($data) { $this->write($data); } - $this->complete = true; + $new = clone $this; + $new->complete = true; + return $new; } /** @@ -106,13 +109,14 @@ public function getProtocolVersion() /** * Proxy to PsrResponseInterface::setProtocolVersion() - * - * @param string $version - * @return void + * + * @param string $version + * @return Response */ public function setProtocolVersion($version) { - return $this->psrResponse->setProtocolVersion($version); + $new = $this->psrResponse->setProtocolVersion($version); + return new self($new); } /** @@ -129,15 +133,17 @@ public function getBody() * Proxy to PsrResponseInterface::setBody() * * @param StreamableInterface $body Body. + * @return Response * @throws \InvalidArgumentException When the body is not valid. */ public function setBody(StreamableInterface $body) { if ($this->complete) { - return; + return $this; } - return $this->psrResponse->setBody($body); + $new = $this->psrResponse->setBody($body); + return new self($new); } /** @@ -190,14 +196,16 @@ public function getHeaderLines($header) * * @param string $header Header name * @param string|string[] $value Header value(s) + * @return Response */ public function setHeader($header, $value) { if ($this->complete) { - return; + return $this; } - return $this->psrResponse->setHeader($header, $value); + $new = $this->psrResponse->setHeader($header, $value); + return new self($new); } /** @@ -205,28 +213,32 @@ public function setHeader($header, $value) * * @param string $header Header name to add or append * @param string|string[] $value Value(s) to add or merge into the header + * @return Response */ public function addHeader($header, $value) { if ($this->complete) { - return; + return $this; } - return $this->psrResponse->addHeader($header, $value); + $new = $this->psrResponse->addHeader($header, $value); + return new self($new); } /** * Proxy to PsrResponseInterface::removeHeader() * * @param string $header HTTP header to remove + * @return Response */ public function removeHeader($header) { if ($this->complete) { - return; + return $this; } - return $this->psrResponse->removeHeader($header); + $new = $this->psrResponse->removeHeader($header); + return new self($new); } /** @@ -244,14 +256,16 @@ public function getStatusCode() * * @param integer $code The 3-digit integer result code to set. * @param null|string $reasonPhrase The reason phrase to use with the status, if any. + * @return Response */ public function setStatus($code, $reasonPhrase = null) { if ($this->complete) { - return; + return $this; } - return $this->psrResponse->setStatus($code, $reasonPhrase); + $new = $this->psrResponse->setStatus($code, $reasonPhrase); + return new self($new); } /** diff --git a/src/Middleware.php b/src/Middleware.php index 8458635..6ecb7d5 100644 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -68,7 +68,6 @@ public function __invoke(Request $request, Response $response, callable $out = n $request = $this->decorateRequest($request); $response = $this->decorateResponse($response); - $request->setUrl($this->getUrlFromRequest($request)); $done = is_callable($out) ? $out : new FinalHandler($request, $response); $next = new Next($this->stack, $request, $response, $done); $next(); @@ -124,21 +123,6 @@ public function pipe($path, $handler = null) return $this; } - /** - * Ensure the request URI is an Uri instance - * - * @param Request $request - * @return Uri - */ - protected function getUrlFromRequest(Request $request) - { - $url = $request->getUrl(); - if (is_string($url)) { - $url = new Uri($url); - } - return $url; - } - /** * Normalize a path used when defining a pipe * diff --git a/src/Next.php b/src/Next.php index d9c968f..18119d0 100644 --- a/src/Next.php +++ b/src/Next.php @@ -3,6 +3,8 @@ use ArrayObject; use Phly\Http\Uri; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; /** * Iterate a stack of middlewares and execute them @@ -63,15 +65,36 @@ public function __construct(ArrayObject $stack, Http\Request $request, Http\Resp /** * Call the next Route in the stack * - * @param null|mixed $err + * @param null|ServerRequestInterface|ResponseInterface|mixed $state + * @param null|ResponseInterface $response */ - public function __invoke($err = null) + public function __invoke($state = null, ResponseInterface $response = null) { - $request = $this->request; + $err = null; + $resetRequest = false; + + if ($state instanceof ResponseInterface) { + return $state; + } + + if ($state instanceof ServerRequestInterface) { + $this->request = $state; + $resetRequest = true; + } + + if ($response instanceof ResponseInterface) { + $this->response = $response; + } + + if (! $state instanceof ServerRequestInterface + && ! $state instanceof ResponseInterface + ) { + $err = $state; + } + $dispatch = $this->dispatch; $done = $this->done; - - $this->resetPath($request); + $this->resetPath($this->request, $resetRequest); // No middleware remains; done if (! isset($this->stack[$this->index])) { @@ -79,7 +102,7 @@ public function __invoke($err = null) } $layer = $this->stack[$this->index++]; - $path = parse_url($this->request->getUrl(), PHP_URL_PATH) ?: '/'; + $path = parse_url($this->request->getAbsoluteUri(), PHP_URL_PATH) ?: '/'; $route = $layer->path; // Skip if layer path does not match current url @@ -98,25 +121,35 @@ public function __invoke($err = null) $this->stripRouteFromPath($route); } - $dispatch($layer, $err, $this->request, $this->response, $this); + return $dispatch($layer, $err, $this->request, $this->response, $this); } /** * Reset the path, if a segment was previously stripped * * @param Http\Request $request + * @param bool $resetRequest Whether or not the request was reset in this iteration */ - private function resetPath(Http\Request $request) + private function resetPath(Http\Request $request, $resetRequest = false) { if (! $this->removed) { return; } - $uri = new Uri($this->request->getUrl()); - $path = $this->removed . $uri->path; + $uri = new Uri($request->getAbsoluteUri()); + $path = $uri->path; + + if ($resetRequest + && strlen($path) >= strlen($this->removed) + && 0 === strpos($path, $this->removed) + ) { + $path = str_replace($this->removed, '', $path); + } + + $path = $this->removed . $path; $new = $uri->setPath($path); - $request->setUrl((string) $new); $this->removed = ''; + $this->request = $request->setAbsoluteUri((string) $new); } /** @@ -144,9 +177,9 @@ private function stripRouteFromPath($route) { $this->removed = $route; - $uri = new Uri($this->request->getUrl()); + $uri = new Uri($this->request->getAbsoluteUri()); $path = substr($uri->path, strlen($route)); $new = $uri->setPath($path); - $this->request->setUrl((string) $new); + $this->request = $this->request->setAbsoluteUri((string) $new); } } diff --git a/test/DispatchTest.php b/test/DispatchTest.php index cdfdb30..b758b40 100644 --- a/test/DispatchTest.php +++ b/test/DispatchTest.php @@ -133,4 +133,57 @@ public function testThrowingExceptionInNonErrorHandlerTriggersNextWithException( $dispatch($route, $err, $this->request, $this->response, $next); $this->assertSame($exception, $triggered); } + + public function testReturnsValueFromNonErrorHandler() + { + $phpunit = $this; + $handler = function ($req, $res, $next) { + return $res; + }; + $next = function ($err) use ($phpunit) { + $phpunit->fail('Next was called; it should not have been'); + }; + + $route = new Route('/foo', $handler); + $dispatch = new Dispatch(); + $err = null; + $result = $dispatch($route, $err, $this->request, $this->response, $next); + $this->assertSame($this->response, $result); + } + + public function testReturnsValueFromErrorHandler() + { + $phpunit = $this; + $handler = function ($err, $req, $res, $next) { + return $res; + }; + $next = function ($err) use ($phpunit) { + $phpunit->fail('Next was called; it should not have been'); + }; + + $route = new Route('/foo', $handler); + $dispatch = new Dispatch(); + $err = (object) ['error' => true]; + $result = $dispatch($route, $err, $this->request, $this->response, $next); + $this->assertSame($this->response, $result); + } + + public function testReturnsValueFromTriggeringNextAfterThrowingExceptionInNonErrorHandler() + { + $phpunit = $this; + $exception = new RuntimeException; + + $handler = function ($req, $res, $next) use ($exception) { + throw $exception; + }; + $next = function ($err) { + return $err; + }; + + $route = new Route('/foo', $handler); + $dispatch = new Dispatch(); + $err = null; + $result = $dispatch($route, $err, $this->request, $this->response, $next); + $this->assertSame($exception, $result); + } } diff --git a/test/FinalHandlerTest.php b/test/FinalHandlerTest.php index 502b379..e31c2ca 100644 --- a/test/FinalHandlerTest.php +++ b/test/FinalHandlerTest.php @@ -14,9 +14,9 @@ class FinalHandlerTest extends TestCase { public function setUp() { - $psrRequest = new PsrRequest('php://memory'); - $psrRequest->setMethod('GET'); - $psrRequest->setAbsoluteUri('http://example.com/'); + $psrRequest = new PsrRequest('php://memory'); + $psrRequest = $psrRequest->setMethod('GET'); + $psrRequest = $psrRequest->setAbsoluteUri('http://example.com/'); $this->escaper = new Escaper(); $this->request = new Request($psrRequest); @@ -26,46 +26,46 @@ public function setUp() public function testInvokingWithErrorAndNoStatusCodeSetsStatusTo500() { - $error = 'error'; - call_user_func($this->final, $error); - $this->assertEquals(500, $this->response->getStatusCode()); + $error = 'error'; + $response = call_user_func($this->final, $error); + $this->assertEquals(500, $response->getStatusCode()); } public function testInvokingWithExceptionWithValidCodeSetsStatusToExceptionCode() { - $error = new Exception('foo', 400); - call_user_func($this->final, $error); - $this->assertEquals(400, $this->response->getStatusCode()); + $error = new Exception('foo', 400); + $response = call_user_func($this->final, $error); + $this->assertEquals(400, $response->getStatusCode()); } public function testInvokingWithExceptionWithInvalidCodeSetsStatusTo500() { - $error = new Exception('foo', 32001); - call_user_func($this->final, $error); - $this->assertEquals(500, $this->response->getStatusCode()); + $error = new Exception('foo', 32001); + $response = call_user_func($this->final, $error); + $this->assertEquals(500, $response->getStatusCode()); } public function testInvokingWithErrorInNonProductionModeSetsResponseBodyToError() { - $error = 'error'; - call_user_func($this->final, $error); - $this->assertEquals($error, (string) $this->response->getBody()); + $error = 'error'; + $response = call_user_func($this->final, $error); + $this->assertEquals($error, (string) $response->getBody()); } public function testInvokingWithExceptionInNonProductionModeIncludesExceptionMessageInResponseBody() { - $error = new Exception('foo', 400); - call_user_func($this->final, $error); + $error = new Exception('foo', 400); + $response = call_user_func($this->final, $error); $expected = $this->escaper->escapeHtml($error->getMessage()); - $this->assertContains($expected, (string) $this->response->getBody()); + $this->assertContains($expected, (string) $response->getBody()); } public function testInvokingWithExceptionInNonProductionModeIncludesTraceInResponseBody() { - $error = new Exception('foo', 400); - call_user_func($this->final, $error); + $error = new Exception('foo', 400); + $response = call_user_func($this->final, $error); $expected = $this->escaper->escapeHtml($error->getTraceAsString()); - $this->assertContains($expected, (string) $this->response->getBody()); + $this->assertContains($expected, (string) $response->getBody()); } public function testInvokingWithErrorInProductionSetsResponseToReasonPhrase() @@ -73,9 +73,9 @@ public function testInvokingWithErrorInProductionSetsResponseToReasonPhrase() $final = new FinalHandler($this->request, $this->response, [ 'env' => 'production', ]); - $error = new Exception('foo', 400); - $final($error); - $this->assertEquals($this->response->getReasonPhrase(), (string) $this->response->getBody()); + $error = new Exception('foo', 400); + $response = $final($error); + $this->assertEquals($response->getReasonPhrase(), (string) $response->getBody()); } public function testTriggersOnErrorCallableWithErrorWhenPresent() @@ -90,31 +90,31 @@ public function testTriggersOnErrorCallableWithErrorWhenPresent() 'env' => 'production', 'onerror' => $callback, ]); - $final($error); + $response = $final($error); $this->assertInternalType('array', $triggered); $this->assertEquals(3, count($triggered)); $this->assertSame($error, array_shift($triggered)); $this->assertSame($this->request, array_shift($triggered)); - $this->assertSame($this->response, array_shift($triggered)); + $this->assertSame($response, array_shift($triggered)); } public function testCreates404ResponseWhenNoErrorIsPresent() { - call_user_func($this->final); - $this->assertEquals(404, $this->response->getStatusCode()); + $response = call_user_func($this->final); + $this->assertEquals(404, $response->getStatusCode()); } public function test404ResponseIncludesOriginalRequestAbsoluteUri() { $originalUrl = 'http://local.example.com/bar/foo'; - $psrRequest = new PsrRequest('php://memory'); - $psrRequest->setMethod('GET'); - $psrRequest->setAbsoluteUri($originalUrl); - $request = new Request($psrRequest); - $request->setAbsoluteUri('http://local.example.com/foo'); + $psrRequest = new PsrRequest('php://memory'); + $psrRequest = $psrRequest->setMethod('GET'); + $psrRequest = $psrRequest->setAbsoluteUri($originalUrl); + $request = new Request($psrRequest); + $request = $request->setAbsoluteUri('http://local.example.com/foo'); - $final = new FinalHandler($request, $this->response); - call_user_func($final); - $this->assertContains($originalUrl, (string) $this->response->getBody()); + $final = new FinalHandler($request, $this->response); + $response = call_user_func($final); + $this->assertContains($originalUrl, (string) $response->getBody()); } } diff --git a/test/Http/RequestTest.php b/test/Http/RequestTest.php index f2d10d4..a2388d2 100644 --- a/test/Http/RequestTest.php +++ b/test/Http/RequestTest.php @@ -10,59 +10,57 @@ class RequestTest extends TestCase { public function setUp() { - $this->original = new PsrRequest('php://memory'); - $this->original->setMethod('GET'); - $this->original->setAbsoluteUri('http://example.com/'); + $psrRequest = new PsrRequest('php://memory'); + $psrRequest = $psrRequest->setMethod('GET'); + $psrRequest = $psrRequest->setAbsoluteUri('http://example.com/'); + $this->original = $psrRequest; $this->request = new Request($this->original); } - public function testAllowsManipulatingArbitraryNonPrivateProperties() + public function testCallingSetAbsoluteUriSetsUriInRequestAndOriginalRequestInClone() { - $this->request->originalUrl = 'http://foo.example.com/foo'; - $this->assertTrue(isset($this->request->originalUrl)); - $this->assertEquals('http://foo.example.com/foo', $this->request->originalUrl); - unset($this->request->originalUrl); - $this->assertNull($this->request->originalUrl); - } - - public function testFetchingUnknownPropertyYieldsNull() - { - $this->assertNull($this->request->somePropertyWeMadeUp); + $url = 'http://example.com/foo'; + $request = $this->request->setAbsoluteUri($url); + $this->assertNotSame($this->request, $request); + $this->assertSame($this->original, $request->getOriginalRequest()); + $this->assertSame($url, $request->getAbsoluteUri()); } - public function testArrayPropertyValueIsCastToArrayObject() + public function testCallingSetUrlSetsOriginalUrlPropertyInClone() { - $original = ['test' => 'value']; - $this->request->anArray = $original; - $this->assertInstanceOf('ArrayObject', $this->request->anArray); - $this->assertEquals($original, $this->request->anArray->getArrayCopy()); + $url = '/foo'; + $request = $this->request->setUrl($url); + $this->assertNotSame($this->request, $request); + $this->assertSame('/', $request->getAttribute('originalUrl')); + $this->assertSame($url, $request->getUrl()); } - public function testCallingSetAbsoluteUriSetsOriginalAbsoluteUriProperty() + public function testConstructorSetsOriginalRequestIfNoneProvided() { $url = 'http://example.com/foo'; - $this->request->setAbsoluteUri($url); - $this->assertSame('http://example.com/', $this->request->originalAbsoluteUri); - $this->assertSame($url, $this->request->getAbsoluteUri()); - } + $baseRequest = new PsrRequest('php://memory'); + $baseRequest = $baseRequest->setMethod('GET'); + $baseRequest = $baseRequest->setAbsoluteUri($url); - public function testCallingSetUrlSetsOriginalUrlProperty() - { - $url = '/foo'; - $this->request->setUrl($url); - $this->assertSame('/', $this->request->originalUrl); - $this->assertSame($url, $this->request->getUrl()); + $request = new Request($baseRequest); + $this->assertSame($baseRequest, $request->getOriginalRequest()); } - public function testConstructorSetsOriginalUrlIfDecoratedRequestHasUrl() + public function testCallingSettersRetainsOriginalRequest() { $url = 'http://example.com/foo'; $baseRequest = new PsrRequest('php://memory'); - $baseRequest->setMethod('GET'); - $baseRequest->setAbsoluteUri($url); + $baseRequest = $baseRequest->setMethod('GET'); + $baseRequest = $baseRequest->setAbsoluteUri($url); $request = new Request($baseRequest); - $this->assertSame($baseRequest->getUrl(), $request->originalUrl); + $request = $request->setMethod('POST'); + $new = $request->addHeader('X-Foo', 'Bar'); + + $this->assertNotSame($request, $new); + $this->assertNotSame($baseRequest, $new); + $this->assertNotSame($baseRequest, $new->getCurrentRequest()); + $this->assertSame($baseRequest, $new->getOriginalRequest()); } public function testCanAccessOriginalRequest() @@ -74,30 +72,14 @@ public function testDecoratorProxiesToAllMethods() { $stream = $this->getMock('Psr\Http\Message\StreamableInterface'); $psrRequest = new PsrRequest($stream); - $psrRequest->setMethod('POST'); - $psrRequest->setAbsoluteUri('http://example.com/'); - $psrRequest->setHeader('Accept', 'application/xml'); - $psrRequest->setHeader('X-URL', 'http://example.com/foo'); + $psrRequest = $psrRequest->setMethod('POST'); + $psrRequest = $psrRequest->setAbsoluteUri('http://example.com/'); + $psrRequest = $psrRequest->setHeader('Accept', 'application/xml'); + $psrRequest = $psrRequest->setHeader('X-URL', 'http://example.com/foo'); $request = new Request($psrRequest); $this->assertEquals('1.1', $request->getProtocolVersion()); $this->assertSame($stream, $request->getBody()); $this->assertSame($psrRequest->getHeaders(), $request->getHeaders()); } - - public function testPropertyAccessProxiesToRequestAttributes() - { - $this->original->setAttributes([ - 'foo' => 'bar', - 'bar' => 'baz', - ]); - - $this->assertTrue(isset($this->request->foo)); - $this->assertTrue(isset($this->request->bar)); - $this->assertFalse(isset($this->request->baz)); - - $this->request->baz = 'quz'; - $this->assertTrue(isset($this->request->baz)); - $this->assertEquals('quz', $this->original->getAttribute('baz', false)); - } } diff --git a/test/Http/ResponseTest.php b/test/Http/ResponseTest.php index d656744..851f07d 100644 --- a/test/Http/ResponseTest.php +++ b/test/Http/ResponseTest.php @@ -21,8 +21,8 @@ public function testIsNotCompleteByDefault() public function testCallingEndMarksAsComplete() { - $this->response->end(); - $this->assertTrue($this->response->isComplete()); + $response = $this->response->end(); + $this->assertTrue($response->isComplete()); } public function testWriteAppendsBody() @@ -36,43 +36,44 @@ public function testWriteAppendsBody() public function testCannotMutateResponseAfterCallingEnd() { - $this->response->setStatus(201); - $this->response->write("First\n"); - $this->response->end('DONE'); - - $this->response->setStatus(200); - $this->response->setHeader('X-Foo', 'Foo'); - $this->response->write('MOAR!'); - - $this->assertEquals(201, $this->response->getStatusCode()); - $this->assertFalse($this->response->hasHeader('X-Foo')); - $this->assertNotContains('MOAR!', (string) $this->response->getBody()); - $this->assertContains('First', (string) $this->response->getBody()); - $this->assertContains('DONE', (string) $this->response->getBody()); + $response = $this->response->setStatus(201); + $response = $response->write("First\n"); + $response = $response->end('DONE'); + + $test = $response->setStatus(200); + $test = $test->setHeader('X-Foo', 'Foo'); + $test = $test->write('MOAR!'); + + $this->assertSame($response, $test); + $this->assertEquals(201, $test->getStatusCode()); + $this->assertFalse($test->hasHeader('X-Foo')); + $this->assertNotContains('MOAR!', (string) $test->getBody()); + $this->assertContains('First', (string) $test->getBody()); + $this->assertContains('DONE', (string) $test->getBody()); } public function testSetBodyReturnsEarlyIfComplete() { - $this->response->end('foo'); + $response = $this->response->end('foo'); $body = new Stream('php://memory', 'r+'); - $this->response->setBody($body); + $response = $response->setBody($body); - $this->assertEquals('foo', (string) $this->response->getBody()); + $this->assertEquals('foo', (string) $response->getBody()); } public function testAddHeaderDoesNothingIfComplete() { - $this->response->end('foo'); - $this->response->addHeader('Content-Type', 'application/json'); - $this->assertFalse($this->response->hasHeader('Content-Type')); + $response = $this->response->end('foo'); + $response = $response->addHeader('Content-Type', 'application/json'); + $this->assertFalse($response->hasHeader('Content-Type')); } public function testCallingEndMultipleTimesDoesNothingAfterFirstCall() { - $this->response->end('foo'); - $this->response->end('bar'); - $this->assertEquals('foo', (string) $this->response->getBody()); + $response = $this->response->end('foo'); + $response = $response->end('bar'); + $this->assertEquals('foo', (string) $response->getBody()); } public function testCanAccessOriginalResponse() @@ -85,22 +86,27 @@ public function testDecoratorProxiesToAllMethods() $this->assertEquals('1.1', $this->response->getProtocolVersion()); $stream = $this->getMock('Psr\Http\Message\StreamableInterface'); - $this->response->setBody($stream); - $this->assertSame($stream, $this->response->getBody()); + $response = $this->response->setBody($stream); + $this->assertNotSame($this->response, $response); + $this->assertSame($stream, $response->getBody()); $this->assertSame($this->original->getHeaders(), $this->response->getHeaders()); - $this->response->setHeader('Accept', 'application/xml'); - $this->assertTrue($this->response->hasHeader('Accept')); - $this->assertEquals('application/xml', $this->response->getHeader('Accept')); + $response = $this->response->setHeader('Accept', 'application/xml'); + $this->assertNotSame($this->response, $response); + $this->assertTrue($response->hasHeader('Accept')); + $this->assertEquals('application/xml', $response->getHeader('Accept')); - $this->response->addHeader('X-URL', 'http://example.com/foo'); - $this->assertTrue($this->response->hasHeader('X-URL')); + $response = $this->response->addHeader('X-URL', 'http://example.com/foo'); + $this->assertNotSame($this->response, $response); + $this->assertTrue($response->hasHeader('X-URL')); - $this->response->removeHeader('X-URL'); - $this->assertFalse($this->response->hasHeader('X-URL')); + $response = $this->response->removeHeader('X-URL'); + $this->assertNotSame($this->response, $response); + $this->assertFalse($response->hasHeader('X-URL')); - $this->response->setStatus(200, 'FOOBAR'); - $this->assertEquals('FOOBAR', $this->response->getReasonPhrase()); + $response = $this->response->setStatus(200, 'FOOBAR'); + $this->assertNotSame($this->response, $response); + $this->assertEquals('FOOBAR', $response->getReasonPhrase()); } } diff --git a/test/MiddlewareTest.php b/test/MiddlewareTest.php index 6cc903c..f83304b 100644 --- a/test/MiddlewareTest.php +++ b/test/MiddlewareTest.php @@ -14,9 +14,10 @@ class MiddlewareTest extends TestCase { public function setUp() { - $this->request = new Request('php://memory'); - $this->request->setMethod('GET'); - $this->request->setAbsoluteUri('http://example.com/'); + $request = new Request('php://memory'); + $request = $request->setMethod('GET'); + $request = $request->setAbsoluteUri('http://example.com/'); + $this->request = $request; $this->response = new Response(); $this->middleware = new Middleware(); @@ -63,8 +64,8 @@ public function testHandleInvokesUntilFirstHandlerThatDoesNotCallNext() }); $request = new Request('php://memory'); - $request->setMethod('GET'); - $request->setAbsoluteUri('http://local.example.com/foo'); + $request = $request->setMethod('GET'); + $request = $request->setAbsoluteUri('http://local.example.com/foo'); $this->middleware->__invoke($request, $this->response); $body = (string) $this->response->getBody(); $this->assertContains('First', $body); @@ -93,8 +94,8 @@ public function testHandleInvokesFirstErrorHandlerOnErrorInChain() }); $request = new Request('php://memory'); - $request->setMethod('GET'); - $request->setAbsoluteUri('http://local.example.com/foo'); + $request = $request->setMethod('GET'); + $request = $request->setAbsoluteUri('http://local.example.com/foo'); $this->middleware->__invoke($request, $this->response); $body = (string) $this->response->getBody(); $this->assertContains('First', $body); @@ -120,8 +121,8 @@ public function testHandleInvokesOutHandlerIfStackIsExhausted() }); $request = new Request('php://memory'); - $request->setMethod('GET'); - $request->setAbsoluteUri('http://local.example.com/foo'); + $request = $request->setMethod('GET'); + $request = $request->setAbsoluteUri('http://local.example.com/foo'); $this->middleware->__invoke($request, $this->response, $out); $this->assertTrue($triggered); } @@ -158,8 +159,8 @@ public function testPipeWillCreateErrorClosureForObjectImplementingHandle() public function testCanUseDecoratedRequestAndResponseDirectly() { $baseRequest = new Request('php://memory'); - $baseRequest->setMethod('GET'); - $baseRequest->setAbsoluteUri('http://local.example.com/foo'); + $baseRequest = $baseRequest->setMethod('GET'); + $baseRequest = $baseRequest->setAbsoluteUri('http://local.example.com/foo'); $request = new RequestDecorator($baseRequest); $response = new ResponseDecorator($this->response); diff --git a/test/NextTest.php b/test/NextTest.php index 87fc290..a09edaf 100644 --- a/test/NextTest.php +++ b/test/NextTest.php @@ -15,8 +15,8 @@ class NextTest extends TestCase public function setUp() { $psrRequest = new PsrRequest('php://memory'); - $psrRequest->setMethod('GET'); - $psrRequest->setAbsoluteUri('http://example.com/'); + $psrRequest = $psrRequest->setMethod('GET'); + $psrRequest = $psrRequest->setAbsoluteUri('http://example.com/'); $this->stack = new ArrayObject(); $this->request = new Request($psrRequest); @@ -92,9 +92,9 @@ public function testInvokesHandlerWhenMatched() $phpunit->fail('Should not hit done handler'); }; - $this->request->setUrl('http://local.example.com/foo'); + $request = $this->request->setUrl('http://local.example.com/foo'); - $next = new Next($this->stack, $this->request, $this->response, $done); + $next = new Next($this->stack, $request, $this->response, $done); $next(); $this->assertTrue($triggered); } @@ -114,9 +114,9 @@ public function testRequestUriInInvokedHandlerDoesNotContainMatchedPortionOfRout $phpunit->fail('Should not hit done handler'); }; - $this->request->setUrl('http://local.example.com/foo/bar'); + $request = $this->request->setUrl('http://local.example.com/foo/bar'); - $next = new Next($this->stack, $this->request, $this->response, $done); + $next = new Next($this->stack, $request, $this->response, $done); $next(); $this->assertEquals('/bar', $triggered); } @@ -131,6 +131,7 @@ public function testSlashAndPathGetResetBeforeExecutingNextMiddleware() }); $route3 = new Route('/foo/baz', function ($req, $res, $next) { $res->end('done'); + return $res; }); $this->stack->append($route1); @@ -142,9 +143,120 @@ public function testSlashAndPathGetResetBeforeExecutingNextMiddleware() $phpunit->fail('Should not hit final handler'); }; - $this->request->setUrl('http://example.com/foo/baz/bat'); - $next = new Next($this->stack, $this->request, $this->response, $done); + $request = $this->request->setUrl('http://example.com/foo/baz/bat'); + $next = new Next($this->stack, $request, $this->response, $done); $next(); $this->assertEquals('done', (string) $this->response->getBody()); } + + public function testMiddlewareReturningResponseShortcircuits() + { + $phpunit = $this; + $route1 = new Route('/foo', function ($req, $res, $next) { + return $res; + }); + $route2 = new Route('/foo/bar', function ($req, $res, $next) use ($phpunit) { + $next(); + $phpunit->fail('Should not hit route2 handler'); + }); + $route3 = new Route('/foo/baz', function ($req, $res, $next) use ($phpunit) { + $next(); + $phpunit->fail('Should not hit route3 handler'); + }); + + $this->stack->append($route1); + $this->stack->append($route2); + $this->stack->append($route3); + + $done = function ($err) use ($phpunit) { + $phpunit->fail('Should not hit final handler'); + }; + + $request = $this->request->setUrl('http://example.com/foo/bar/baz'); + $next = new Next($this->stack, $request, $this->response, $done); + $result = $next(); + $this->assertSame($this->response, $result); + } + + public function testMiddlewareCallingNextWithResponseShortCircuits() + { + $phpunit = $this; + $cannedResponse = new Response(new PsrResponse()); + + $route1 = new Route('/foo', function ($req, $res, $next) use ($cannedResponse) { + return $next($cannedResponse); + }); + $route2 = new Route('/foo/bar', function ($req, $res, $next) use ($phpunit) { + $next(); + $phpunit->fail('Should not hit route2 handler'); + }); + $route3 = new Route('/foo/baz', function ($req, $res, $next) use ($phpunit) { + $next(); + $phpunit->fail('Should not hit route3 handler'); + }); + + $this->stack->append($route1); + $this->stack->append($route2); + $this->stack->append($route3); + + $done = function ($err) use ($phpunit) { + $phpunit->fail('Should not hit final handler'); + }; + + $request = $this->request->setUrl('http://example.com/foo/bar/baz'); + $next = new Next($this->stack, $request, $this->response, $done); + $result = $next(); + $this->assertSame($cannedResponse, $result); + } + + public function testMiddlewareCallingNextWithRequestResetsRequest() + { + $phpunit = $this; + $request = $this->request->setUrl('http://example.com/foo/bar/baz'); + $cannedRequest = clone $request; + $cannedRequest = $cannedRequest->setMethod('POST'); + + $route1 = new Route('/foo/bar', function ($req, $res, $next) use ($cannedRequest) { + return $next($cannedRequest); + }); + $route2 = new Route('/foo/bar/baz', function ($req, $res, $next) use ($phpunit, $cannedRequest) { + $phpunit->assertEquals($cannedRequest->getMethod(), $req->getMethod()); + return $res; + }); + + $this->stack->append($route1); + $this->stack->append($route2); + + $done = function ($err) use ($phpunit) { + $phpunit->fail('Should not hit final handler'); + }; + + $next = new Next($this->stack, $request, $this->response, $done); + $next(); + } + + public function testMiddlewareCallingNextWithResponseResetsResponse() + { + $phpunit = $this; + $cannedResponse = new Response(new PsrResponse()); + + $route1 = new Route('/foo', function ($req, $res, $next) use ($cannedResponse) { + return $next(null, $cannedResponse); + }); + $route2 = new Route('/foo/bar', function ($req, $res, $next) use ($phpunit, $cannedResponse) { + $phpunit->assertSame($cannedResponse, $res); + return $res; + }); + + $this->stack->append($route1); + $this->stack->append($route2); + + $done = function ($err) use ($phpunit) { + $phpunit->fail('Should not hit final handler'); + }; + + $request = $this->request->setUrl('http://example.com/foo/bar/baz'); + $next = new Next($this->stack, $request, $this->response, $done); + $next(); + } } From cc40476d6a4d0070c864ecd220c317689ff250a4 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 13 Jan 2015 09:13:59 -0600 Subject: [PATCH 05/13] Middleware should return a response --- src/Dispatch.php | 2 +- src/Middleware.php | 13 +++++++---- test/MiddlewareTest.php | 51 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/Dispatch.php b/src/Dispatch.php index 7d08526..eb14625 100644 --- a/src/Dispatch.php +++ b/src/Dispatch.php @@ -53,7 +53,7 @@ public function __invoke( try { if ($hasError && $arity === 4) { - return $response = call_user_func($handler, $err, $request, $response, $next); + return call_user_func($handler, $err, $request, $response, $next); } if (! $hasError && $arity < 4) { diff --git a/src/Middleware.php b/src/Middleware.php index 6ecb7d5..240bb4c 100644 --- a/src/Middleware.php +++ b/src/Middleware.php @@ -61,16 +61,21 @@ public function __construct() * @param Request $request * @param Response $response * @param callable $out - * @return void + * @return Response */ public function __invoke(Request $request, Response $response, callable $out = null) { $request = $this->decorateRequest($request); $response = $this->decorateResponse($response); - $done = is_callable($out) ? $out : new FinalHandler($request, $response); - $next = new Next($this->stack, $request, $response, $done); - $next(); + $done = is_callable($out) ? $out : new FinalHandler($request, $response); + $next = new Next($this->stack, $request, $response, $done); + $result = $next(); + + if ($result instanceof Response) { + return $result; + } + return $response; } /** diff --git a/test/MiddlewareTest.php b/test/MiddlewareTest.php index f83304b..74d10ab 100644 --- a/test/MiddlewareTest.php +++ b/test/MiddlewareTest.php @@ -180,4 +180,55 @@ public function testCanUseDecoratedRequestAndResponseDirectly() $this->assertTrue($executed); } + + public function testReturnsOrigionalResponseIfStackDoesNotReturnAResponse() + { + $this->middleware->pipe(function ($req, $res, $next) { + $next(); + }); + $this->middleware->pipe(function ($req, $res, $next) { + $next(); + }); + $this->middleware->pipe(function ($req, $res, $next) { + return; + }); + $phpunit = $this; + $this->middleware->pipe(function ($req, $res, $next) use ($phpunit) { + $phpunit->fail('Should not hit fourth handler!'); + }); + + $request = new Request('php://memory'); + $request = $request->setMethod('GET'); + $request = $request->setAbsoluteUri('http://local.example.com/foo'); + $result = $this->middleware->__invoke($request, $this->response); + $this->assertSame($this->response, $result->getOriginalResponse()); + } + + public function testReturnsResponseReturnedByStack() + { + $return = new Response(); + + $this->middleware->pipe(function ($req, $res, $next) { + return $next(); + }); + $this->middleware->pipe(function ($req, $res, $next) { + return $next(); + }); + $this->middleware->pipe(function ($req, $res, $next) use ($return) { + return $return; + }); + $phpunit = $this; + $this->middleware->pipe(function ($req, $res, $next) use ($phpunit) { + $phpunit->fail('Should not hit fourth handler!'); + }); + + $request = new Request('php://memory'); + $request = $request->setMethod('GET'); + $request = $request->setAbsoluteUri('http://local.example.com/foo'); + $result = $this->middleware->__invoke($request, $this->response); + $this->assertSame($return, $result, var_export([ + spl_object_hash($return) => get_class($return), + spl_object_hash($result) => get_class($result), + ], 1)); + } } From dcb8ad41bb0bb690f8f1c92fad9c5bf18baec3b3 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 13 Jan 2015 09:18:34 -0600 Subject: [PATCH 06/13] Always return a response from Next() - Ensures that the Middleware and hence the Server always return a response, and the most current one. --- src/Next.php | 7 ++++++- test/MiddlewareTest.php | 2 +- test/NextTest.php | 26 ++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/Next.php b/src/Next.php index 18119d0..e9b28cb 100644 --- a/src/Next.php +++ b/src/Next.php @@ -67,6 +67,7 @@ public function __construct(ArrayObject $stack, Http\Request $request, Http\Resp * * @param null|ServerRequestInterface|ResponseInterface|mixed $state * @param null|ResponseInterface $response + * @return ResponseInterface */ public function __invoke($state = null, ResponseInterface $response = null) { @@ -121,7 +122,11 @@ public function __invoke($state = null, ResponseInterface $response = null) $this->stripRouteFromPath($route); } - return $dispatch($layer, $err, $this->request, $this->response, $this); + $result = $dispatch($layer, $err, $this->request, $this->response, $this); + if ($result instanceof ResponseInterface) { + $this->response = $result; + } + return $this->response; } /** diff --git a/test/MiddlewareTest.php b/test/MiddlewareTest.php index 74d10ab..2963b70 100644 --- a/test/MiddlewareTest.php +++ b/test/MiddlewareTest.php @@ -209,7 +209,7 @@ public function testReturnsResponseReturnedByStack() $return = new Response(); $this->middleware->pipe(function ($req, $res, $next) { - return $next(); + $next(); }); $this->middleware->pipe(function ($req, $res, $next) { return $next(); diff --git a/test/NextTest.php b/test/NextTest.php index a09edaf..d58b24b 100644 --- a/test/NextTest.php +++ b/test/NextTest.php @@ -259,4 +259,30 @@ public function testMiddlewareCallingNextWithResponseResetsResponse() $next = new Next($this->stack, $request, $this->response, $done); $next(); } + + public function testNextShouldReturnCurrentResponseAlways() + { + $phpunit = $this; + $cannedResponse = new Response(new PsrResponse()); + + $route1 = new Route('/foo', function ($req, $res, $next) use ($cannedResponse) { + $next(null, $cannedResponse); + }); + $route2 = new Route('/foo/bar', function ($req, $res, $next) use ($phpunit, $cannedResponse) { + $phpunit->assertSame($cannedResponse, $res); + return $res; + }); + + $this->stack->append($route1); + $this->stack->append($route2); + + $done = function ($err) use ($phpunit) { + $phpunit->fail('Should not hit final handler'); + }; + + $request = $this->request->setUrl('http://example.com/foo/bar/baz'); + $next = new Next($this->stack, $request, $this->response, $done); + $result = $next(); + $this->assertSame($cannedResponse, $result); + } } From d7cb21a16e6583c7fb0be82e7281aea7882d5c87 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 13 Jan 2015 11:16:09 -0600 Subject: [PATCH 07/13] Ensure users know how to use the new Next() API Additionally, covers how to get the original request/response from the decorated instances. --- README.md | 175 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 155 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 8050e5e..be84d00 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ $server = Server::createServer($app, $_SERVER, $_GET, $_POST, $_COOKIE, $_FILES) // Landing page $app->pipe('/', function ($req, $res, $next) { - if ($req->getUrl()->path !== '/') { + if (parse_url($req->getUrl(), PHP_URL_PATH) !== '/') { return $next(); } $res->end('Hello world!'); @@ -137,13 +137,13 @@ $app = new Middleware(); $app->pipe('/api', $api); ``` -Another way to create middleware is to write a callable capable of receiving minimally a request and a response object, and optionally a callback to call the next in the chain. In this callback, you can handle as much or as little of the request as you want -- including delegating to other handlers. If your middleware also accepts a `$next` argument, if it is unable to complete the request, or allows further processing, it can call it to return handling to the parent middleware. +Another way to create middleware is to write a callable capable of receiving minimally a request and a response object, and optionally a callback to call the next in the chain. In your middleware callable, you can handle as much or as little of the request as you want -- including delegating to other handlers. If your middleware also accepts a `$next` argument, if it is unable to complete the request, or allows further processing, it can call it to return handling to the parent middleware. As an example, consider the following middleware which will use an external router to map the incoming request path to a handler; if unable to map the request, it returns processing to the next middleware. ```php $app->pipe(function ($req, $res, $next) use ($router) { - $path = $req->getUrl()->path; + $path = parse_url($req->getUrl(), PHP_URL_PATH); // Route the path $route = $router->route($path); @@ -169,8 +169,8 @@ In all cases, if you wish to implement typehinting, the signature is: ```php function ( - Psr\Http\Message\IncomingRequestInterface $request, - Psr\Http\Message\OutgoingResponseInterface $response, + Psr\Http\Message\ServerRequestInterface $request, + Psr\Http\Message\ResponseInterface $response, callable $next = null ) { } @@ -181,28 +181,28 @@ Error handler middleware has the following signature: ```php function ( $error, // Can be any type - Psr\Http\Message\IncomingRequestInterface $request, - Psr\Http\Message\OutgoingResponseInterface $response, + Psr\Http\Message\ServerRequestInterface $request, + Psr\Http\Message\ResponseInterface $response, callable $next ) { } ``` -Another approach is to extend the `Phly\Conduit\Middleware` class itself -- particularly if you want to allow attaching other middleware to your own middleware. In such a case, you will generally override the `handle()` method to perform any additional logic you have, and then call on the parent in order to iterate through your stack of middleware: +Another approach is to extend the `Phly\Conduit\Middleware` class itself -- particularly if you want to allow attaching other middleware to your own middleware. In such a case, you will generally override the `__invoke()` method to perform any additional logic you have, and then call on the parent in order to iterate through your stack of middleware: ```php use Phly\Conduit\Middleware; -use Psr\Http\Message\IncomingRequestInterface as Request; -use Psr\Http\Message\OutgoingResponseInterface as Response; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; class CustomMiddleware extends Middleware { - public function handle(Request $request, Response $response, callable $next = null) + public function __invoke(Request $request, Response $response, callable $next = null) { // perform some work... // delegate to parent - parent::handle($request, $response, $next); + parent::__invoke($request, $response, $next); // maybe do more work? } @@ -245,8 +245,8 @@ class Middleware { public function pipe($path, $handler = null); public function handle( - Psr\Http\Message\IncomingRequestInterface $request = null, - Psr\Http\Message\OutgoingResponseInterface $response = null, + Psr\Http\Message\ServerRequestInterface $request = null, + Psr\Http\Message\ResponseInterface $response = null, callable $out = null ); } @@ -256,7 +256,116 @@ class Middleware Handlers are executed in the order in which they are piped to the `Middleware` instance. -`handle()` is itself a middleware handler. If `$out` is not provided, an instance of `Phly\Conduit\FinalHandler` will be created, and used in the event that the pipe stack is exhausted. +`__invoke()` is itself a middleware handler. If `$out` is not provided, an instance of `Phly\Conduit\FinalHandler` will be created, and used in the event that the pipe stack is exhausted. + +### Next + +`Phly\Conduit\Next` is primarily an implementation detail of middleware, and +exists to allow delegating to middleware registered later in the stack. + +Because `Psr\Http\Message`'s interfaces are immutable, if you make changes to +your Request and/or Response instances, you will have new instances, and will +need to make these known to the next middleware in the chain. `Next` allows this +by allowing the following argument combinations: + +- `Next()` will re-use the currently registered Request and Response instances. +- `Next(ResponseInterface $response)` indicates that handling is done, and the + provided `$response` will be returned. +- `Next(RequestInterface $request)` will register the provided `$request` with + itself, and that instance will be used for subsequent invocations. +- `Next(RequestInterface|null $request, ResponseInterface $response)` will + register the provided `$response` with itself, and that instance will be used + for subsequent invocations; if a request is also provided, it will be + registered as well. +- If any other argument is provided for the first argument, it is considered the + error to report and pass to registered error middleware. + +As examples: + +#### Providing an altered request: + +```php +function ($request, $response, $next) use ($bodyParser) +{ + $bodyParams = $bodyParser($request); + $request = $request->setBodyParams($bodyParams); + return $next($request); // Next will now register this altered request + // instance +} +``` + +#### Providing an altered request: + +```php +function ($request, $response, $next) +{ + $response = $response->addHeader('Cache-Control', [ + 'public', + 'max-age=18600', + 's-maxage=18600', + ]); + return $next(null, $response); // Next will now register this altered + // response instance +} +``` + +#### Providing both an altered request and response: + +```php +function ($request, $response, $next) use ($bodyParser) +{ + $request = $request->setBodyParams($bodyParser($request)); + $response = $response->addHeader('Cache-Control', [ + 'public', + 'max-age=18600', + 's-maxage=18600', + ]); + return $next($request, $response); +} +``` + +#### Returning a response to complete the request + +```php +function ($request, $response, $next) +{ + $response = $response->addHeader('Cache-Control', [ + 'public', + 'max-age=18600', + 's-maxage=18600', + ]); + return $next($response); // This response will be used, and no more + // middleware will be executed. +} +``` + +A shorthand for the above is to omit the call to `$next()`, and simply return +the response instance: + +```php +function ($request, $response, $next) +{ + $response = $response->addHeader('Cache-Control', [ + 'public', + 'max-age=18600', + 's-maxage=18600', + ]); + return $response; +} +``` + +#### Raising an error condition + +```php +function ($request, $response, $next) +{ + try { + // try some operation... + } catch (Exception $e) { + return $next($e); // Next registered error middleware will be invoked + } +} +``` ### FinalHandler @@ -264,21 +373,47 @@ Handlers are executed in the order in which they are piped to the `Middleware` i `FinalHandler` allows an optional third argument during instantiation, `$options`, an array of options with which to configure itself. These options currently include: -- env, the application environment. If set to "production", no stack traces will be provided. -- onerror, a callable to execute if an error is passed when `FinalHandler` is invoked. The callable is invoked with the error, the request, and the response. +- `env`, the application environment. If set to "production", no stack traces will be provided. +- `onerror`, a callable to execute if an error is passed when `FinalHandler` is invoked. The callable is invoked with the error, the request, and the response. ### HTTP Messages #### Phly\Conduit\Http\Request -`Phly\Conduit\Http\Request` acts as a decorator for a `Psr\Http\Message\IncomingRequestInterface` instance, and implements property overloading, allowing the developer to set and retrieve arbitrary properties other than those exposed via getters. This allows the ability to pass values between handlers. +`Phly\Conduit\Http\Request` acts as a decorator for a `Psr\Http\Message\ServerRequestInterface` instance. The primary reason is to allow composing middleware to get a request instance that has a "root path". -Property overloading writes to the _attributes_ property of the incoming request, ensuring that the two are synchronized; in essence, it offers a convenience API to the various `(get|set)Attributes?()` methods. +As an example, consider the following: + +```php +$app1 = new Middleware(); +$app1->pipe('/foo', $fooCallback); + +$app2 = new Middleware(); +$app2->pipe('/root', $app1); + +$server = Server::createServer($app2 /* ... */); +``` + +In the above, if the URI of the original incoming request is `/root/foo`, what `$fooCallback` will receive is a URI with a past consisting of only `/foo`. This practice ensures that middleware can be nested safely and resolve regardless of the nesting level. + +If you want access to the full URI -- for instance, to construct a fully qualified URI to your current middleware -- `Phly\Conduit\Http\Request` contains a method, `getOriginalRequest()`, which will always return the original request provided: + +```php +function ($request, $response, $next) +{ + $location = $request->getOriginalRequest()->getAbsoluteUri() . '/[:id]'; + $response = $response->setHeader('Location', $location); + $response = $response->setStatus(302); + return $response; +} +``` #### Phly\Conduit\Http\Response -`Phly\Conduit\Http\Response` acts as a decorator for a `Psr\Http\Message\OutgoingResponseInterface` instance, and also implements `Phly\Conduit\Http\ResponseInterface`, which provides the following convenience methods: +`Phly\Conduit\Http\Response` acts as a decorator for a `Psr\Http\Message\ResponseInterface` instance, and also implements `Phly\Conduit\Http\ResponseInterface`, which provides the following convenience methods: - `write()`, which proxies to the `write()` method of the composed response stream. - `end()`, which marks the response as complete; it can take an optional argument, which, when provided, will be passed to the `write()` method. Once `end()` has been called, the response is immutable. - `isComplete()` indicates whether or not `end()` has been called. + +Additionally, it provides access to the original response created by the server via the method `getOriginalResponse()`. From 39ae564e07f9303d3140fa27c5639e56af48348b Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Tue, 13 Jan 2015 11:43:44 -0600 Subject: [PATCH 08/13] Next(Response) should only reset the response not end the invocation. Return a response if you want to end; calling Next() should always iterate. --- README.md | 48 +++++++++++++++++------------------------------ src/Next.php | 2 +- test/NextTest.php | 15 ++++++--------- 3 files changed, 24 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index be84d00..033917b 100644 --- a/README.md +++ b/README.md @@ -260,25 +260,17 @@ Handlers are executed in the order in which they are piped to the `Middleware` i ### Next -`Phly\Conduit\Next` is primarily an implementation detail of middleware, and -exists to allow delegating to middleware registered later in the stack. +`Phly\Conduit\Next` is primarily an implementation detail of middleware, and exists to allow delegating to middleware registered later in the stack. -Because `Psr\Http\Message`'s interfaces are immutable, if you make changes to -your Request and/or Response instances, you will have new instances, and will -need to make these known to the next middleware in the chain. `Next` allows this -by allowing the following argument combinations: +Because `Psr\Http\Message`'s interfaces are immutable, if you make changes to your Request and/or Response instances, you will have new instances, and will need to make these known to the next middleware in the chain. `Next` allows this by allowing the following argument combinations: - `Next()` will re-use the currently registered Request and Response instances. -- `Next(ResponseInterface $response)` indicates that handling is done, and the - provided `$response` will be returned. -- `Next(RequestInterface $request)` will register the provided `$request` with - itself, and that instance will be used for subsequent invocations. -- `Next(RequestInterface|null $request, ResponseInterface $response)` will - register the provided `$response` with itself, and that instance will be used - for subsequent invocations; if a request is also provided, it will be - registered as well. -- If any other argument is provided for the first argument, it is considered the - error to report and pass to registered error middleware. +- `Next(RequestInterface $request)` will register the provided `$request` with itself, and that instance will be used for subsequent invocations. +- `Next(ResponseInterface $response)` will register the provided `$response` with itself, and that instance will be used for subsequent invocations. provided `$response` will be returned. +- `Next(RequestInterface $request, ResponseInterface $response)` will register each of the provided `$request` and `$response` with itself, and those instances will be used for subsequent invocations. +- If any other argument is provided for the first argument, it is considered the error to report and pass to registered error middleware. + +Note: you **can** pass an error as the first argument and a response as the second, and `Next` will reset the response in that condition as well. As examples: @@ -294,7 +286,7 @@ function ($request, $response, $next) use ($bodyParser) } ``` -#### Providing an altered request: +#### Providing an altered response: ```php function ($request, $response, $next) @@ -304,7 +296,7 @@ function ($request, $response, $next) 'max-age=18600', 's-maxage=18600', ]); - return $next(null, $response); // Next will now register this altered + return $next($response); // Next will now register this altered // response instance } ``` @@ -326,6 +318,8 @@ function ($request, $response, $next) use ($bodyParser) #### Returning a response to complete the request +If you want to complete the request, don't call `$next()`. However, if you have modified, populated, or created a response that you want returned, you can return it from your middleware, and that value will be returned on the completion of the current iteration of `$next()`. + ```php function ($request, $response, $next) { @@ -334,24 +328,16 @@ function ($request, $response, $next) 'max-age=18600', 's-maxage=18600', ]); - return $next($response); // This response will be used, and no more - // middleware will be executed. + return $response; } ``` -A shorthand for the above is to omit the call to `$next()`, and simply return -the response instance: +One caveat: if you are in a nested middleware or not the first in the stack, all parent and/or previous middleware must also call `return $next(/* ... */)` for this to work correctly. + +As such, _I recommend always returning `$next()` when invoking it in your middleware_: ```php -function ($request, $response, $next) -{ - $response = $response->addHeader('Cache-Control', [ - 'public', - 'max-age=18600', - 's-maxage=18600', - ]); - return $response; -} +return $next(/* ... */); ``` #### Raising an error condition diff --git a/src/Next.php b/src/Next.php index e9b28cb..dc2471d 100644 --- a/src/Next.php +++ b/src/Next.php @@ -75,7 +75,7 @@ public function __invoke($state = null, ResponseInterface $response = null) $resetRequest = false; if ($state instanceof ResponseInterface) { - return $state; + $this->response = $state; } if ($state instanceof ServerRequestInterface) { diff --git a/test/NextTest.php b/test/NextTest.php index d58b24b..7ab3cdc 100644 --- a/test/NextTest.php +++ b/test/NextTest.php @@ -178,26 +178,22 @@ public function testMiddlewareReturningResponseShortcircuits() $this->assertSame($this->response, $result); } - public function testMiddlewareCallingNextWithResponseShortCircuits() + public function testMiddlewareCallingNextWithResponseAsFirstArgumentResetsResponse() { $phpunit = $this; $cannedResponse = new Response(new PsrResponse()); + $triggered = false; $route1 = new Route('/foo', function ($req, $res, $next) use ($cannedResponse) { return $next($cannedResponse); }); - $route2 = new Route('/foo/bar', function ($req, $res, $next) use ($phpunit) { - $next(); - $phpunit->fail('Should not hit route2 handler'); - }); - $route3 = new Route('/foo/baz', function ($req, $res, $next) use ($phpunit) { - $next(); - $phpunit->fail('Should not hit route3 handler'); + $route2 = new Route('/foo/bar', function ($req, $res, $next) use ($phpunit, $cannedResponse, &$triggered) { + $phpunit->assertSame($cannedResponse, $res); + $triggered = true; }); $this->stack->append($route1); $this->stack->append($route2); - $this->stack->append($route3); $done = function ($err) use ($phpunit) { $phpunit->fail('Should not hit final handler'); @@ -206,6 +202,7 @@ public function testMiddlewareCallingNextWithResponseShortCircuits() $request = $this->request->setUrl('http://example.com/foo/bar/baz'); $next = new Next($this->stack, $request, $this->response, $done); $result = $next(); + $this->assertTrue($triggered); $this->assertSame($cannedResponse, $result); } From 0159f457fb9eab5beecdac305e403cc33e068bc1 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 15 Jan 2015 13:26:14 -0600 Subject: [PATCH 09/13] Update to psr/http-message@feature/immutability --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6bede14..54b364c 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ "require": { "php": ">=5.4.8", "phly/http": "dev-feature/immutability@dev", - "psr/http-message": "dev-feature/full-mutability@dev", + "psr/http-message": "dev-feature/immutability@dev", "zendframework/zend-escaper": "~2.3@stable" }, "require-dev": { From 1ebb5c014a8956366a96f5f02b4fe0b3a3cee5fb Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Thu, 15 Jan 2015 14:23:39 -0600 Subject: [PATCH 10/13] Adapt Conduit to use new phly/http mutator verbiage Instead of `setters`, `with*()` methods. Additionally, requests now only compose a Uri instance, which means we do not need to use `parse_url()` and/or casting to a URI to get at the URI segments. This also exposed a flaw in the logic of Next for truncating paths, which is now fixed. --- src/FinalHandler.php | 8 +-- src/Http/Request.php | 112 ++++++++++++++----------------------- src/Http/Response.php | 36 ++++++------ src/Next.php | 46 ++++++++++++--- test/FinalHandlerTest.php | 13 +++-- test/Http/RequestTest.php | 39 +++++-------- test/Http/ResponseTest.php | 20 +++---- test/MiddlewareTest.php | 29 +++++----- test/NextTest.php | 29 +++++----- 9 files changed, 163 insertions(+), 169 deletions(-) diff --git a/src/FinalHandler.php b/src/FinalHandler.php index 7cca058..a3f7351 100644 --- a/src/FinalHandler.php +++ b/src/FinalHandler.php @@ -66,7 +66,7 @@ public function __invoke($err = null) */ private function handleError($error) { - $response = $this->response->setStatus( + $response = $this->response->withStatus( $this->getStatusCode($error, $this->response) ); @@ -90,15 +90,15 @@ private function handleError($error) */ private function create404() { - $response = $this->response->setStatus(404); + $response = $this->response->withStatus(404); $originalRequest = $this->request->getOriginalRequest(); - $url = $originalRequest->getAbsoluteUri(); + $uri = $originalRequest->getUri(); $escaper = new Escaper(); $message = sprintf( "Cannot %s %s\n", $escaper->escapeHtml($this->request->getMethod()), - $escaper->escapeHtml((string) $url) + $escaper->escapeHtml((string) $uri) ); $response->end($message); return $response; diff --git a/src/Http/Request.php b/src/Http/Request.php index e51da5b..e14b328 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -4,14 +4,13 @@ use ArrayObject; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamableInterface; +use Psr\Http\Message\UriInterface; /** * Decorator for PSR ServerRequestInterface * * Decorates the PSR incoming request interface to add the ability to * manipulate arbitrary instance members. - * - * @property \Phly\Http\Uri $originalUrl Original URL of this instance */ class Request implements ServerRequestInterface { @@ -43,7 +42,7 @@ public function __construct( } $this->originalRequest = $originalRequest; - $this->psrRequest = $decoratedRequest->setAttribute('originalUrl', $originalRequest->getUrl()); + $this->psrRequest = $decoratedRequest->withAttribute('originalUri', $originalRequest->getUri()); } /** @@ -77,14 +76,14 @@ public function getProtocolVersion() } /** - * Proxy to ServerRequestInterface::setProtocolVersion() + * Proxy to ServerRequestInterface::withProtocolVersion() * * @param string $version HTTP protocol version. * @return Request */ - public function setProtocolVersion($version) + public function withProtocolVersion($version) { - $new = $this->psrRequest->setProtocolVersion($version); + $new = $this->psrRequest->withProtocolVersion($version); return new self($new, $this->originalRequest); } @@ -99,15 +98,15 @@ public function getBody() } /** - * Proxy to ServerRequestInterface::setBody() + * Proxy to ServerRequestInterface::withBody() * * @param StreamableInterface $body Body. * @return Request * @throws \InvalidArgumentException When the body is not valid. */ - public function setBody(StreamableInterface $body) + public function withBody(StreamableInterface $body) { - $new = $this->psrRequest->setBody($body); + $new = $this->psrRequest->withBody($body); return new self($new, $this->originalRequest); } @@ -157,15 +156,15 @@ public function getHeaderLines($header) } /** - * Proxy to ServerRequestInterface::setHeader() + * Proxy to ServerRequestInterface::withHeader() * * @param string $header Header name * @param string|string[] $value Header value(s) * @return Request */ - public function setHeader($header, $value) + public function withHeader($header, $value) { - $new = $this->psrRequest->setHeader($header, $value); + $new = $this->psrRequest->withHeader($header, $value); return new self($new, $this->originalRequest); } @@ -176,9 +175,9 @@ public function setHeader($header, $value) * @param string|string[] $value Value(s) to add or merge into the header * @return Request */ - public function addHeader($header, $value) + public function withAddedHeader($header, $value) { - $new = $this->psrRequest->addHeader($header, $value); + $new = $this->psrRequest->withAddedHeader($header, $value); return new self($new, $this->originalRequest); } @@ -188,9 +187,9 @@ public function addHeader($header, $value) * @param string $header HTTP header to remove * @return Request */ - public function removeHeader($header) + public function withoutHeader($header) { - $new = $this->psrRequest->removeHeader($header); + $new = $this->psrRequest->withoutHeader($header); return new self($new, $this->originalRequest); } @@ -205,66 +204,39 @@ public function getMethod() } /** - * Proxy to ServerRequestInterface::setMethod() + * Proxy to ServerRequestInterface::withMethod() * * @param string $method The request method. * @return Request */ - public function setMethod($method) - { - $new = $this->psrRequest->setMethod($method); - return new self($new, $this->originalRequest); - } - - /** - * Proxy to ServerRequestInterface::getAbsoluteUri() - * - * @return string Returns the absolute URI as a string - * @link http://tools.ietf.org/html/rfc3986#section-4.3 - */ - public function getAbsoluteUri() - { - return $this->psrRequest->getAbsoluteUri(); - } - - /** - * Allow mutating the absolute URI - * - * @link http://tools.ietf.org/html/rfc3986#section-4.3 - * @param string|object $uri Absolute request URI. - * @return Request - * @throws \InvalidArgumentException If the URL is invalid. - */ - public function setAbsoluteUri($uri) + public function withMethod($method) { - $new = $this->psrRequest->setAbsoluteUri($uri); + $new = $this->psrRequest->withMethod($method); return new self($new, $this->originalRequest); } /** - * Proxy to ServerRequestInterface::getUrl() + * Proxy to ServerRequestInterface::getUri() * - * @return string|object Returns the URL as a string, or an object that - * implements the `__toString()` method. The URL must be an absolute URI - * as specified in RFC 3986. + * @return UriInterface Returns a UriInterface instance * @link http://tools.ietf.org/html/rfc3986#section-4.3 */ - public function getUrl() + public function getUri() { - return $this->psrRequest->getUrl(); + return $this->psrRequest->getUri(); } /** - * Allow mutating the URL + * Allow mutating the URI * * @link http://tools.ietf.org/html/rfc3986#section-4.3 - * @param string|object $url Request URL. + * @param UriInterface $uri Request URI. * @return Request - * @throws \InvalidArgumentException If the URL is invalid. + * @throws \InvalidArgumentException If the URI is invalid. */ - public function setUrl($url) + public function withUri(UriInterface $uri) { - $new = $this->psrRequest->setUrl($url); + $new = $this->psrRequest->withUri($uri); return new self($new, $this->originalRequest); } @@ -289,14 +261,14 @@ public function getCookieParams() } /** - * Proxy to ServerRequestInterface::setCookieParams() + * Proxy to ServerRequestInterface::withCookieParams() * * @param array $cookies * @return Request */ - public function setCookieParams(array $cookies) + public function withCookieParams(array $cookies) { - $new = $this->psrRequest->setCookieParams($cookies); + $new = $this->psrRequest->withCookieParams($cookies); return new self($new, $this->originalRequest); } @@ -311,14 +283,14 @@ public function getQueryParams() } /** - * Proxy to ServerRequestInterface::setQueryParams() + * Proxy to ServerRequestInterface::withQueryParams() * * @param array $query * @return Request */ - public function setQueryParams(array $query) + public function withQueryParams(array $query) { - $new = $this->psrRequest->setQueryParams($query); + $new = $this->psrRequest->withQueryParams($query); return new self($new, $this->originalRequest); } @@ -344,14 +316,14 @@ public function getBodyParams() } /** - * Proxy to ServerRequestInterface::setBodyParams() + * Proxy to ServerRequestInterface::withBodyParams() * * @param array $params The deserialized body parameters. * @return Request */ - public function setBodyParams(array $params) + public function withBodyParams(array $params) { - $new = $this->psrRequest->setBodyParams($params); + $new = $this->psrRequest->withBodyParams($params); return new self($new, $this->originalRequest); } @@ -378,27 +350,27 @@ public function getAttribute($attribute, $default = null) } /** - * Proxy to ServerRequestInterface::setAttributes() + * Proxy to ServerRequestInterface::withAttributes() * * @param array Attributes derived from the request * @return Request */ - public function setAttributes(array $values) + public function withAttributes(array $values) { - $new = $this->psrRequest->setAttributes($values); + $new = $this->psrRequest->withAttributes($values); return new self($new, $this->originalRequest); } /** - * Proxy to ServerRequestInterface::setAttribute() + * Proxy to ServerRequestInterface::withAttribute() * * @param string $attribute * @param mixed $value * @return Request */ - public function setAttribute($attribute, $value) + public function withAttribute($attribute, $value) { - $new = $this->psrRequest->setAttribute($attribute, $value); + $new = $this->psrRequest->withAttribute($attribute, $value); return new self($new, $this->originalRequest); } } diff --git a/src/Http/Response.php b/src/Http/Response.php index ff74c98..2b4c08d 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -108,14 +108,14 @@ public function getProtocolVersion() } /** - * Proxy to PsrResponseInterface::setProtocolVersion() + * Proxy to PsrResponseInterface::withProtocolVersion() * * @param string $version * @return Response */ - public function setProtocolVersion($version) + public function withProtocolVersion($version) { - $new = $this->psrResponse->setProtocolVersion($version); + $new = $this->psrResponse->withProtocolVersion($version); return new self($new); } @@ -130,19 +130,19 @@ public function getBody() } /** - * Proxy to PsrResponseInterface::setBody() + * Proxy to PsrResponseInterface::withBody() * * @param StreamableInterface $body Body. * @return Response * @throws \InvalidArgumentException When the body is not valid. */ - public function setBody(StreamableInterface $body) + public function withBody(StreamableInterface $body) { if ($this->complete) { return $this; } - $new = $this->psrResponse->setBody($body); + $new = $this->psrResponse->withBody($body); return new self($new); } @@ -192,52 +192,52 @@ public function getHeaderLines($header) } /** - * Proxy to PsrResponseInterface::setHeader() + * Proxy to PsrResponseInterface::withHeader() * * @param string $header Header name * @param string|string[] $value Header value(s) * @return Response */ - public function setHeader($header, $value) + public function withHeader($header, $value) { if ($this->complete) { return $this; } - $new = $this->psrResponse->setHeader($header, $value); + $new = $this->psrResponse->withHeader($header, $value); return new self($new); } /** - * Proxy to PsrResponseInterface::addHeader() + * Proxy to PsrResponseInterface::withAddedHeader() * * @param string $header Header name to add or append * @param string|string[] $value Value(s) to add or merge into the header * @return Response */ - public function addHeader($header, $value) + public function withAddedHeader($header, $value) { if ($this->complete) { return $this; } - $new = $this->psrResponse->addHeader($header, $value); + $new = $this->psrResponse->withAddedHeader($header, $value); return new self($new); } /** - * Proxy to PsrResponseInterface::removeHeader() + * Proxy to PsrResponseInterface::withoutHeader() * * @param string $header HTTP header to remove * @return Response */ - public function removeHeader($header) + public function withoutHeader($header) { if ($this->complete) { return $this; } - $new = $this->psrResponse->removeHeader($header); + $new = $this->psrResponse->withoutHeader($header); return new self($new); } @@ -252,19 +252,19 @@ public function getStatusCode() } /** - * Proxy to PsrResponseInterface::setStatus() + * Proxy to PsrResponseInterface::withStatus() * * @param integer $code The 3-digit integer result code to set. * @param null|string $reasonPhrase The reason phrase to use with the status, if any. * @return Response */ - public function setStatus($code, $reasonPhrase = null) + public function withStatus($code, $reasonPhrase = null) { if ($this->complete) { return $this; } - $new = $this->psrResponse->setStatus($code, $reasonPhrase); + $new = $this->psrResponse->withStatus($code, $reasonPhrase); return new self($new); } diff --git a/src/Next.php b/src/Next.php index dc2471d..3a8ec83 100644 --- a/src/Next.php +++ b/src/Next.php @@ -5,6 +5,7 @@ use Phly\Http\Uri; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; +use RuntimeException; /** * Iterate a stack of middlewares and execute them @@ -103,7 +104,7 @@ public function __invoke($state = null, ResponseInterface $response = null) } $layer = $this->stack[$this->index++]; - $path = parse_url($this->request->getAbsoluteUri(), PHP_URL_PATH) ?: '/'; + $path = $this->request->getUri()->getPath() ?: '/'; $route = $layer->path; // Skip if layer path does not match current url @@ -141,8 +142,8 @@ private function resetPath(Http\Request $request, $resetRequest = false) return; } - $uri = new Uri($request->getAbsoluteUri()); - $path = $uri->path; + $uri = $request->getUri(); + $path = $uri->getPath(); if ($resetRequest && strlen($path) >= strlen($this->removed) @@ -152,9 +153,9 @@ private function resetPath(Http\Request $request, $resetRequest = false) } $path = $this->removed . $path; - $new = $uri->setPath($path); + $new = $uri->withPath($path); $this->removed = ''; - $this->request = $request->setAbsoluteUri((string) $new); + $this->request = $request->withUri($new); } /** @@ -182,9 +183,36 @@ private function stripRouteFromPath($route) { $this->removed = $route; - $uri = new Uri($this->request->getAbsoluteUri()); - $path = substr($uri->path, strlen($route)); - $new = $uri->setPath($path); - $this->request = $this->request->setAbsoluteUri((string) $new); + $uri = $this->request->getUri(); + $path = $this->getTruncatedPath($route, $uri->getPath()); + $new = $uri->withPath($path); + + $this->request = $this->request->withUri($new); + } + + /** + * Strip the segment from the start of the given path. + * + * @param string $segment + * @param string $path + * @return string Truncated path + * @throws RuntimeException if the segment does not begin the path. + */ + private function getTruncatedPath($segment, $path) + { + if ($path === $segment) { + // Segment and path are same; return empty string + return ''; + } + + if (strlen($path) > $segment) { + // Strip segment from start of path + return substr($path, strlen($segment)); + } + + // Segment is longer than path. There's an issue + throw new RuntimeException( + 'Layer and request path have gone out of sync' + ); } } diff --git a/test/FinalHandlerTest.php b/test/FinalHandlerTest.php index e31c2ca..feb2dbf 100644 --- a/test/FinalHandlerTest.php +++ b/test/FinalHandlerTest.php @@ -7,6 +7,7 @@ use Phly\Conduit\Http\Response; use Phly\Http\ServerRequest as PsrRequest; use Phly\Http\Response as PsrResponse; +use Phly\Http\Uri; use PHPUnit_Framework_TestCase as TestCase; use Zend\Escaper\Escaper; @@ -15,8 +16,8 @@ class FinalHandlerTest extends TestCase public function setUp() { $psrRequest = new PsrRequest('php://memory'); - $psrRequest = $psrRequest->setMethod('GET'); - $psrRequest = $psrRequest->setAbsoluteUri('http://example.com/'); + $psrRequest = $psrRequest->withMethod('GET'); + $psrRequest = $psrRequest->withUri(new Uri('http://example.com/')); $this->escaper = new Escaper(); $this->request = new Request($psrRequest); @@ -104,14 +105,14 @@ public function testCreates404ResponseWhenNoErrorIsPresent() $this->assertEquals(404, $response->getStatusCode()); } - public function test404ResponseIncludesOriginalRequestAbsoluteUri() + public function test404ResponseIncludesOriginalRequestUri() { $originalUrl = 'http://local.example.com/bar/foo'; $psrRequest = new PsrRequest('php://memory'); - $psrRequest = $psrRequest->setMethod('GET'); - $psrRequest = $psrRequest->setAbsoluteUri($originalUrl); + $psrRequest = $psrRequest->withMethod('GET'); + $psrRequest = $psrRequest->withUri(new Uri($originalUrl)); $request = new Request($psrRequest); - $request = $request->setAbsoluteUri('http://local.example.com/foo'); + $request = $request->withUri(new Uri('http://local.example.com/foo')); $final = new FinalHandler($request, $this->response); $response = call_user_func($final); diff --git a/test/Http/RequestTest.php b/test/Http/RequestTest.php index a2388d2..e19a478 100644 --- a/test/Http/RequestTest.php +++ b/test/Http/RequestTest.php @@ -11,36 +11,27 @@ class RequestTest extends TestCase public function setUp() { $psrRequest = new PsrRequest('php://memory'); - $psrRequest = $psrRequest->setMethod('GET'); - $psrRequest = $psrRequest->setAbsoluteUri('http://example.com/'); + $psrRequest = $psrRequest->withMethod('GET'); + $psrRequest = $psrRequest->withUri(new Uri('http://example.com/')); $this->original = $psrRequest; $this->request = new Request($this->original); } - public function testCallingSetAbsoluteUriSetsUriInRequestAndOriginalRequestInClone() + public function testCallingSetUriSetsUriInRequestAndOriginalRequestInClone() { $url = 'http://example.com/foo'; - $request = $this->request->setAbsoluteUri($url); + $request = $this->request->withUri(new Uri($url)); $this->assertNotSame($this->request, $request); $this->assertSame($this->original, $request->getOriginalRequest()); - $this->assertSame($url, $request->getAbsoluteUri()); - } - - public function testCallingSetUrlSetsOriginalUrlPropertyInClone() - { - $url = '/foo'; - $request = $this->request->setUrl($url); - $this->assertNotSame($this->request, $request); - $this->assertSame('/', $request->getAttribute('originalUrl')); - $this->assertSame($url, $request->getUrl()); + $this->assertSame($url, (string) $request->getUri()); } public function testConstructorSetsOriginalRequestIfNoneProvided() { $url = 'http://example.com/foo'; $baseRequest = new PsrRequest('php://memory'); - $baseRequest = $baseRequest->setMethod('GET'); - $baseRequest = $baseRequest->setAbsoluteUri($url); + $baseRequest = $baseRequest->withMethod('GET'); + $baseRequest = $baseRequest->withUri(new Uri($url)); $request = new Request($baseRequest); $this->assertSame($baseRequest, $request->getOriginalRequest()); @@ -50,12 +41,12 @@ public function testCallingSettersRetainsOriginalRequest() { $url = 'http://example.com/foo'; $baseRequest = new PsrRequest('php://memory'); - $baseRequest = $baseRequest->setMethod('GET'); - $baseRequest = $baseRequest->setAbsoluteUri($url); + $baseRequest = $baseRequest->withMethod('GET'); + $baseRequest = $baseRequest->withUri(new Uri($url)); $request = new Request($baseRequest); - $request = $request->setMethod('POST'); - $new = $request->addHeader('X-Foo', 'Bar'); + $request = $request->withMethod('POST'); + $new = $request->withAddedHeader('X-Foo', 'Bar'); $this->assertNotSame($request, $new); $this->assertNotSame($baseRequest, $new); @@ -72,10 +63,10 @@ public function testDecoratorProxiesToAllMethods() { $stream = $this->getMock('Psr\Http\Message\StreamableInterface'); $psrRequest = new PsrRequest($stream); - $psrRequest = $psrRequest->setMethod('POST'); - $psrRequest = $psrRequest->setAbsoluteUri('http://example.com/'); - $psrRequest = $psrRequest->setHeader('Accept', 'application/xml'); - $psrRequest = $psrRequest->setHeader('X-URL', 'http://example.com/foo'); + $psrRequest = $psrRequest->withMethod('POST'); + $psrRequest = $psrRequest->withUri(new Uri('http://example.com/')); + $psrRequest = $psrRequest->withHeader('Accept', 'application/xml'); + $psrRequest = $psrRequest->withHeader('X-URL', 'http://example.com/foo'); $request = new Request($psrRequest); $this->assertEquals('1.1', $request->getProtocolVersion()); diff --git a/test/Http/ResponseTest.php b/test/Http/ResponseTest.php index 851f07d..dc7f9bf 100644 --- a/test/Http/ResponseTest.php +++ b/test/Http/ResponseTest.php @@ -36,12 +36,12 @@ public function testWriteAppendsBody() public function testCannotMutateResponseAfterCallingEnd() { - $response = $this->response->setStatus(201); + $response = $this->response->withStatus(201); $response = $response->write("First\n"); $response = $response->end('DONE'); - $test = $response->setStatus(200); - $test = $test->setHeader('X-Foo', 'Foo'); + $test = $response->withStatus(200); + $test = $test->withHeader('X-Foo', 'Foo'); $test = $test->write('MOAR!'); $this->assertSame($response, $test); @@ -57,7 +57,7 @@ public function testSetBodyReturnsEarlyIfComplete() $response = $this->response->end('foo'); $body = new Stream('php://memory', 'r+'); - $response = $response->setBody($body); + $response = $response->withBody($body); $this->assertEquals('foo', (string) $response->getBody()); } @@ -65,7 +65,7 @@ public function testSetBodyReturnsEarlyIfComplete() public function testAddHeaderDoesNothingIfComplete() { $response = $this->response->end('foo'); - $response = $response->addHeader('Content-Type', 'application/json'); + $response = $response->withAddedHeader('Content-Type', 'application/json'); $this->assertFalse($response->hasHeader('Content-Type')); } @@ -86,26 +86,26 @@ public function testDecoratorProxiesToAllMethods() $this->assertEquals('1.1', $this->response->getProtocolVersion()); $stream = $this->getMock('Psr\Http\Message\StreamableInterface'); - $response = $this->response->setBody($stream); + $response = $this->response->withBody($stream); $this->assertNotSame($this->response, $response); $this->assertSame($stream, $response->getBody()); $this->assertSame($this->original->getHeaders(), $this->response->getHeaders()); - $response = $this->response->setHeader('Accept', 'application/xml'); + $response = $this->response->withHeader('Accept', 'application/xml'); $this->assertNotSame($this->response, $response); $this->assertTrue($response->hasHeader('Accept')); $this->assertEquals('application/xml', $response->getHeader('Accept')); - $response = $this->response->addHeader('X-URL', 'http://example.com/foo'); + $response = $this->response->withAddedHeader('X-URL', 'http://example.com/foo'); $this->assertNotSame($this->response, $response); $this->assertTrue($response->hasHeader('X-URL')); - $response = $this->response->removeHeader('X-URL'); + $response = $this->response->withoutHeader('X-URL'); $this->assertNotSame($this->response, $response); $this->assertFalse($response->hasHeader('X-URL')); - $response = $this->response->setStatus(200, 'FOOBAR'); + $response = $this->response->withStatus(200, 'FOOBAR'); $this->assertNotSame($this->response, $response); $this->assertEquals('FOOBAR', $response->getReasonPhrase()); } diff --git a/test/MiddlewareTest.php b/test/MiddlewareTest.php index 2963b70..9aee5b8 100644 --- a/test/MiddlewareTest.php +++ b/test/MiddlewareTest.php @@ -7,6 +7,7 @@ use Phly\Conduit\Utils; use Phly\Http\ServerRequest as Request; use Phly\Http\Response; +use Phly\Http\Uri; use PHPUnit_Framework_TestCase as TestCase; use ReflectionProperty; @@ -15,8 +16,8 @@ class MiddlewareTest extends TestCase public function setUp() { $request = new Request('php://memory'); - $request = $request->setMethod('GET'); - $request = $request->setAbsoluteUri('http://example.com/'); + $request = $request->withMethod('GET'); + $request = $request->withUri(new Uri('http://example.com/')); $this->request = $request; $this->response = new Response(); @@ -64,8 +65,8 @@ public function testHandleInvokesUntilFirstHandlerThatDoesNotCallNext() }); $request = new Request('php://memory'); - $request = $request->setMethod('GET'); - $request = $request->setAbsoluteUri('http://local.example.com/foo'); + $request = $request->withMethod('GET'); + $request = $request->withUri(new Uri('http://local.example.com/foo')); $this->middleware->__invoke($request, $this->response); $body = (string) $this->response->getBody(); $this->assertContains('First', $body); @@ -94,8 +95,8 @@ public function testHandleInvokesFirstErrorHandlerOnErrorInChain() }); $request = new Request('php://memory'); - $request = $request->setMethod('GET'); - $request = $request->setAbsoluteUri('http://local.example.com/foo'); + $request = $request->withMethod('GET'); + $request = $request->withUri(new Uri('http://local.example.com/foo')); $this->middleware->__invoke($request, $this->response); $body = (string) $this->response->getBody(); $this->assertContains('First', $body); @@ -121,8 +122,8 @@ public function testHandleInvokesOutHandlerIfStackIsExhausted() }); $request = new Request('php://memory'); - $request = $request->setMethod('GET'); - $request = $request->setAbsoluteUri('http://local.example.com/foo'); + $request = $request->withMethod('GET'); + $request = $request->withUri(new Uri('http://local.example.com/foo')); $this->middleware->__invoke($request, $this->response, $out); $this->assertTrue($triggered); } @@ -159,8 +160,8 @@ public function testPipeWillCreateErrorClosureForObjectImplementingHandle() public function testCanUseDecoratedRequestAndResponseDirectly() { $baseRequest = new Request('php://memory'); - $baseRequest = $baseRequest->setMethod('GET'); - $baseRequest = $baseRequest->setAbsoluteUri('http://local.example.com/foo'); + $baseRequest = $baseRequest->withMethod('GET'); + $baseRequest = $baseRequest->withUri(new Uri('http://local.example.com/foo')); $request = new RequestDecorator($baseRequest); $response = new ResponseDecorator($this->response); @@ -198,8 +199,8 @@ public function testReturnsOrigionalResponseIfStackDoesNotReturnAResponse() }); $request = new Request('php://memory'); - $request = $request->setMethod('GET'); - $request = $request->setAbsoluteUri('http://local.example.com/foo'); + $request = $request->withMethod('GET'); + $request = $request->withUri(new Uri('http://local.example.com/foo')); $result = $this->middleware->__invoke($request, $this->response); $this->assertSame($this->response, $result->getOriginalResponse()); } @@ -223,8 +224,8 @@ public function testReturnsResponseReturnedByStack() }); $request = new Request('php://memory'); - $request = $request->setMethod('GET'); - $request = $request->setAbsoluteUri('http://local.example.com/foo'); + $request = $request->withMethod('GET'); + $request = $request->withUri(new Uri('http://local.example.com/foo')); $result = $this->middleware->__invoke($request, $this->response); $this->assertSame($return, $result, var_export([ spl_object_hash($return) => get_class($return), diff --git a/test/NextTest.php b/test/NextTest.php index 7ab3cdc..9cee010 100644 --- a/test/NextTest.php +++ b/test/NextTest.php @@ -8,6 +8,7 @@ use Phly\Conduit\Route; use Phly\Http\ServerRequest as PsrRequest; use Phly\Http\Response as PsrResponse; +use Phly\Http\Uri; use PHPUnit_Framework_TestCase as TestCase; class NextTest extends TestCase @@ -15,8 +16,8 @@ class NextTest extends TestCase public function setUp() { $psrRequest = new PsrRequest('php://memory'); - $psrRequest = $psrRequest->setMethod('GET'); - $psrRequest = $psrRequest->setAbsoluteUri('http://example.com/'); + $psrRequest = $psrRequest->withMethod('GET'); + $psrRequest = $psrRequest->withUri(new Uri('http://example.com/')); $this->stack = new ArrayObject(); $this->request = new Request($psrRequest); @@ -50,7 +51,7 @@ public function testInvokesItselfWhenRouteDoesNotMatchCurrentUrl() $triggered = true; }; - $this->request->setUrl('http://local.example.com/bar'); + $this->request->withUri(new Uri('http://local.example.com/bar')); $next = new Next($this->stack, $this->request, $this->response, $done); $next(); @@ -71,7 +72,7 @@ public function testInvokesItselfIfRouteDoesNotMatchAtABoundary() $triggered = true; }; - $this->request->setUrl('http://local.example.com/foobar'); + $this->request->withUri(new Uri('http://local.example.com/foobar')); $next = new Next($this->stack, $this->request, $this->response, $done); $next(); @@ -92,7 +93,7 @@ public function testInvokesHandlerWhenMatched() $phpunit->fail('Should not hit done handler'); }; - $request = $this->request->setUrl('http://local.example.com/foo'); + $request = $this->request->withUri(new Uri('http://local.example.com/foo')); $next = new Next($this->stack, $request, $this->response, $done); $next(); @@ -105,7 +106,7 @@ public function testRequestUriInInvokedHandlerDoesNotContainMatchedPortionOfRout // then the URI path in the handler is "/bar" $triggered = null; $route = new Route('/foo', function ($req, $res, $next) use (&$triggered) { - $triggered = parse_url($req->getUrl(), PHP_URL_PATH); + $triggered = $req->getUri()->getPath(); }); $this->stack[] = $route; @@ -114,7 +115,7 @@ public function testRequestUriInInvokedHandlerDoesNotContainMatchedPortionOfRout $phpunit->fail('Should not hit done handler'); }; - $request = $this->request->setUrl('http://local.example.com/foo/bar'); + $request = $this->request->withUri(new Uri('http://local.example.com/foo/bar')); $next = new Next($this->stack, $request, $this->response, $done); $next(); @@ -143,7 +144,7 @@ public function testSlashAndPathGetResetBeforeExecutingNextMiddleware() $phpunit->fail('Should not hit final handler'); }; - $request = $this->request->setUrl('http://example.com/foo/baz/bat'); + $request = $this->request->withUri(new Uri('http://example.com/foo/baz/bat')); $next = new Next($this->stack, $request, $this->response, $done); $next(); $this->assertEquals('done', (string) $this->response->getBody()); @@ -172,7 +173,7 @@ public function testMiddlewareReturningResponseShortcircuits() $phpunit->fail('Should not hit final handler'); }; - $request = $this->request->setUrl('http://example.com/foo/bar/baz'); + $request = $this->request->withUri(new Uri('http://example.com/foo/bar/baz')); $next = new Next($this->stack, $request, $this->response, $done); $result = $next(); $this->assertSame($this->response, $result); @@ -199,7 +200,7 @@ public function testMiddlewareCallingNextWithResponseAsFirstArgumentResetsRespon $phpunit->fail('Should not hit final handler'); }; - $request = $this->request->setUrl('http://example.com/foo/bar/baz'); + $request = $this->request->withUri(new Uri('http://example.com/foo/bar/baz')); $next = new Next($this->stack, $request, $this->response, $done); $result = $next(); $this->assertTrue($triggered); @@ -209,9 +210,9 @@ public function testMiddlewareCallingNextWithResponseAsFirstArgumentResetsRespon public function testMiddlewareCallingNextWithRequestResetsRequest() { $phpunit = $this; - $request = $this->request->setUrl('http://example.com/foo/bar/baz'); + $request = $this->request->withUri(new Uri('http://example.com/foo/bar/baz')); $cannedRequest = clone $request; - $cannedRequest = $cannedRequest->setMethod('POST'); + $cannedRequest = $cannedRequest->withMethod('POST'); $route1 = new Route('/foo/bar', function ($req, $res, $next) use ($cannedRequest) { return $next($cannedRequest); @@ -252,7 +253,7 @@ public function testMiddlewareCallingNextWithResponseResetsResponse() $phpunit->fail('Should not hit final handler'); }; - $request = $this->request->setUrl('http://example.com/foo/bar/baz'); + $request = $this->request->withUri(new Uri('http://example.com/foo/bar/baz')); $next = new Next($this->stack, $request, $this->response, $done); $next(); } @@ -277,7 +278,7 @@ public function testNextShouldReturnCurrentResponseAlways() $phpunit->fail('Should not hit final handler'); }; - $request = $this->request->setUrl('http://example.com/foo/bar/baz'); + $request = $this->request->withUri(new Uri('http://example.com/foo/bar/baz')); $next = new Next($this->stack, $request, $this->response, $done); $result = $next(); $this->assertSame($cannedResponse, $result); From 7c00ebe12b5a02d3e16b0eab1add9e36dcf881f3 Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 18 Jan 2015 11:29:09 -0600 Subject: [PATCH 11/13] Updated to latest psr/http-message - Request instance needed to remove getAttributes and add withoutAttribute(). - Updated tests to use new message constructors. --- src/Http/Request.php | 48 +++++++++++++++++++-------------------- test/FinalHandlerTest.php | 9 ++------ test/Http/RequestTest.php | 21 ++++++----------- test/MiddlewareTest.php | 30 ++++++------------------ test/NextTest.php | 5 +--- 5 files changed, 41 insertions(+), 72 deletions(-) diff --git a/src/Http/Request.php b/src/Http/Request.php index e14b328..1f0daf1 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -4,7 +4,7 @@ use ArrayObject; use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamableInterface; -use Psr\Http\Message\UriInterface; +use Psr\Http\Message\UriTargetInterface; /** * Decorator for PSR ServerRequestInterface @@ -79,7 +79,7 @@ public function getProtocolVersion() * Proxy to ServerRequestInterface::withProtocolVersion() * * @param string $version HTTP protocol version. - * @return Request + * @return self */ public function withProtocolVersion($version) { @@ -101,7 +101,7 @@ public function getBody() * Proxy to ServerRequestInterface::withBody() * * @param StreamableInterface $body Body. - * @return Request + * @return self * @throws \InvalidArgumentException When the body is not valid. */ public function withBody(StreamableInterface $body) @@ -160,7 +160,7 @@ public function getHeaderLines($header) * * @param string $header Header name * @param string|string[] $value Header value(s) - * @return Request + * @return self */ public function withHeader($header, $value) { @@ -173,7 +173,7 @@ public function withHeader($header, $value) * * @param string $header Header name to add or append * @param string|string[] $value Value(s) to add or merge into the header - * @return Request + * @return self */ public function withAddedHeader($header, $value) { @@ -185,7 +185,7 @@ public function withAddedHeader($header, $value) * Proxy to ServerRequestInterface::removeHeader() * * @param string $header HTTP header to remove - * @return Request + * @return self */ public function withoutHeader($header) { @@ -207,7 +207,7 @@ public function getMethod() * Proxy to ServerRequestInterface::withMethod() * * @param string $method The request method. - * @return Request + * @return self */ public function withMethod($method) { @@ -218,7 +218,7 @@ public function withMethod($method) /** * Proxy to ServerRequestInterface::getUri() * - * @return UriInterface Returns a UriInterface instance + * @return UriTargetInterface Returns a UriTargetInterface instance * @link http://tools.ietf.org/html/rfc3986#section-4.3 */ public function getUri() @@ -230,11 +230,11 @@ public function getUri() * Allow mutating the URI * * @link http://tools.ietf.org/html/rfc3986#section-4.3 - * @param UriInterface $uri Request URI. - * @return Request + * @param UriTargetInterface $uri Request URI. + * @return self * @throws \InvalidArgumentException If the URI is invalid. */ - public function withUri(UriInterface $uri) + public function withUri(UriTargetInterface $uri) { $new = $this->psrRequest->withUri($uri); return new self($new, $this->originalRequest); @@ -264,7 +264,7 @@ public function getCookieParams() * Proxy to ServerRequestInterface::withCookieParams() * * @param array $cookies - * @return Request + * @return self */ public function withCookieParams(array $cookies) { @@ -286,7 +286,7 @@ public function getQueryParams() * Proxy to ServerRequestInterface::withQueryParams() * * @param array $query - * @return Request + * @return self */ public function withQueryParams(array $query) { @@ -319,7 +319,7 @@ public function getBodyParams() * Proxy to ServerRequestInterface::withBodyParams() * * @param array $params The deserialized body parameters. - * @return Request + * @return self */ public function withBodyParams(array $params) { @@ -350,27 +350,27 @@ public function getAttribute($attribute, $default = null) } /** - * Proxy to ServerRequestInterface::withAttributes() + * Proxy to ServerRequestInterface::withAttribute() * - * @param array Attributes derived from the request - * @return Request + * @param string $attribute + * @param mixed $value + * @return self */ - public function withAttributes(array $values) + public function withAttribute($attribute, $value) { - $new = $this->psrRequest->withAttributes($values); + $new = $this->psrRequest->withAttribute($attribute, $value); return new self($new, $this->originalRequest); } /** - * Proxy to ServerRequestInterface::withAttribute() + * Proxy to ServerRequestInterface::withoutAttribute() * * @param string $attribute - * @param mixed $value - * @return Request + * @return self */ - public function withAttribute($attribute, $value) + public function withoutAttribute($attribute) { - $new = $this->psrRequest->withAttribute($attribute, $value); + $new = $this->psrRequest->withoutAttribute($attribute); return new self($new, $this->originalRequest); } } diff --git a/test/FinalHandlerTest.php b/test/FinalHandlerTest.php index feb2dbf..beac07f 100644 --- a/test/FinalHandlerTest.php +++ b/test/FinalHandlerTest.php @@ -15,10 +15,7 @@ class FinalHandlerTest extends TestCase { public function setUp() { - $psrRequest = new PsrRequest('php://memory'); - $psrRequest = $psrRequest->withMethod('GET'); - $psrRequest = $psrRequest->withUri(new Uri('http://example.com/')); - + $psrRequest = new PsrRequest([], [], 'http://example.com/', 'GET', 'php://memory'); $this->escaper = new Escaper(); $this->request = new Request($psrRequest); $this->response = new Response(new PsrResponse()); @@ -108,9 +105,7 @@ public function testCreates404ResponseWhenNoErrorIsPresent() public function test404ResponseIncludesOriginalRequestUri() { $originalUrl = 'http://local.example.com/bar/foo'; - $psrRequest = new PsrRequest('php://memory'); - $psrRequest = $psrRequest->withMethod('GET'); - $psrRequest = $psrRequest->withUri(new Uri($originalUrl)); + $psrRequest = new PsrRequest([], [], $originalUrl, 'GET', 'php://memory'); $request = new Request($psrRequest); $request = $request->withUri(new Uri('http://local.example.com/foo')); diff --git a/test/Http/RequestTest.php b/test/Http/RequestTest.php index e19a478..6b6e25a 100644 --- a/test/Http/RequestTest.php +++ b/test/Http/RequestTest.php @@ -10,9 +10,7 @@ class RequestTest extends TestCase { public function setUp() { - $psrRequest = new PsrRequest('php://memory'); - $psrRequest = $psrRequest->withMethod('GET'); - $psrRequest = $psrRequest->withUri(new Uri('http://example.com/')); + $psrRequest = new PsrRequest([], [], 'http://example.com/', 'GET', 'php://memory'); $this->original = $psrRequest; $this->request = new Request($this->original); } @@ -29,9 +27,7 @@ public function testCallingSetUriSetsUriInRequestAndOriginalRequestInClone() public function testConstructorSetsOriginalRequestIfNoneProvided() { $url = 'http://example.com/foo'; - $baseRequest = new PsrRequest('php://memory'); - $baseRequest = $baseRequest->withMethod('GET'); - $baseRequest = $baseRequest->withUri(new Uri($url)); + $baseRequest = new PsrRequest([], [], $url, 'GET', 'php://memory'); $request = new Request($baseRequest); $this->assertSame($baseRequest, $request->getOriginalRequest()); @@ -40,9 +36,7 @@ public function testConstructorSetsOriginalRequestIfNoneProvided() public function testCallingSettersRetainsOriginalRequest() { $url = 'http://example.com/foo'; - $baseRequest = new PsrRequest('php://memory'); - $baseRequest = $baseRequest->withMethod('GET'); - $baseRequest = $baseRequest->withUri(new Uri($url)); + $baseRequest = new PsrRequest([], [], $url, 'GET', 'php://memory'); $request = new Request($baseRequest); $request = $request->withMethod('POST'); @@ -62,11 +56,10 @@ public function testCanAccessOriginalRequest() public function testDecoratorProxiesToAllMethods() { $stream = $this->getMock('Psr\Http\Message\StreamableInterface'); - $psrRequest = new PsrRequest($stream); - $psrRequest = $psrRequest->withMethod('POST'); - $psrRequest = $psrRequest->withUri(new Uri('http://example.com/')); - $psrRequest = $psrRequest->withHeader('Accept', 'application/xml'); - $psrRequest = $psrRequest->withHeader('X-URL', 'http://example.com/foo'); + $psrRequest = new PsrRequest([], [], 'http://example.com', 'POST', $stream, [ + 'Accept' => 'application/xml', + 'X-URL' => 'http://example.com/foo', + ]); $request = new Request($psrRequest); $this->assertEquals('1.1', $request->getProtocolVersion()); diff --git a/test/MiddlewareTest.php b/test/MiddlewareTest.php index 9aee5b8..e409baf 100644 --- a/test/MiddlewareTest.php +++ b/test/MiddlewareTest.php @@ -15,11 +15,7 @@ class MiddlewareTest extends TestCase { public function setUp() { - $request = new Request('php://memory'); - $request = $request->withMethod('GET'); - $request = $request->withUri(new Uri('http://example.com/')); - $this->request = $request; - + $this->request = new Request([], [], 'http://example.com/', 'GET', 'php://memory'); $this->response = new Response(); $this->middleware = new Middleware(); } @@ -64,9 +60,7 @@ public function testHandleInvokesUntilFirstHandlerThatDoesNotCallNext() $phpunit->fail('Should not hit fourth handler!'); }); - $request = new Request('php://memory'); - $request = $request->withMethod('GET'); - $request = $request->withUri(new Uri('http://local.example.com/foo')); + $request = new Request([], [], 'http://local.example.com/foo', 'GET', 'php://memory'); $this->middleware->__invoke($request, $this->response); $body = (string) $this->response->getBody(); $this->assertContains('First', $body); @@ -94,9 +88,7 @@ public function testHandleInvokesFirstErrorHandlerOnErrorInChain() $phpunit->fail('Should not hit fourth handler!'); }); - $request = new Request('php://memory'); - $request = $request->withMethod('GET'); - $request = $request->withUri(new Uri('http://local.example.com/foo')); + $request = new Request([], [], 'http://local.example.com/foo', 'GET', 'php://memory'); $this->middleware->__invoke($request, $this->response); $body = (string) $this->response->getBody(); $this->assertContains('First', $body); @@ -121,9 +113,7 @@ public function testHandleInvokesOutHandlerIfStackIsExhausted() $next(); }); - $request = new Request('php://memory'); - $request = $request->withMethod('GET'); - $request = $request->withUri(new Uri('http://local.example.com/foo')); + $request = new Request([], [], 'http://local.example.com/foo', 'GET', 'php://memory'); $this->middleware->__invoke($request, $this->response, $out); $this->assertTrue($triggered); } @@ -159,9 +149,7 @@ public function testPipeWillCreateErrorClosureForObjectImplementingHandle() public function testCanUseDecoratedRequestAndResponseDirectly() { - $baseRequest = new Request('php://memory'); - $baseRequest = $baseRequest->withMethod('GET'); - $baseRequest = $baseRequest->withUri(new Uri('http://local.example.com/foo')); + $baseRequest = new Request([], [], 'http://local.example.com/foo', 'GET', 'php://memory'); $request = new RequestDecorator($baseRequest); $response = new ResponseDecorator($this->response); @@ -198,9 +186,7 @@ public function testReturnsOrigionalResponseIfStackDoesNotReturnAResponse() $phpunit->fail('Should not hit fourth handler!'); }); - $request = new Request('php://memory'); - $request = $request->withMethod('GET'); - $request = $request->withUri(new Uri('http://local.example.com/foo')); + $request = new Request([], [], 'http://local.example.com/foo', 'GET', 'php://memory'); $result = $this->middleware->__invoke($request, $this->response); $this->assertSame($this->response, $result->getOriginalResponse()); } @@ -223,9 +209,7 @@ public function testReturnsResponseReturnedByStack() $phpunit->fail('Should not hit fourth handler!'); }); - $request = new Request('php://memory'); - $request = $request->withMethod('GET'); - $request = $request->withUri(new Uri('http://local.example.com/foo')); + $request = new Request([], [], 'http://local.example.com/foo', 'GET', 'php://memory'); $result = $this->middleware->__invoke($request, $this->response); $this->assertSame($return, $result, var_export([ spl_object_hash($return) => get_class($return), diff --git a/test/NextTest.php b/test/NextTest.php index 9cee010..cf5a169 100644 --- a/test/NextTest.php +++ b/test/NextTest.php @@ -15,10 +15,7 @@ class NextTest extends TestCase { public function setUp() { - $psrRequest = new PsrRequest('php://memory'); - $psrRequest = $psrRequest->withMethod('GET'); - $psrRequest = $psrRequest->withUri(new Uri('http://example.com/')); - + $psrRequest = new PsrRequest([], [], 'http://example.com/', 'GET', 'php://memory'); $this->stack = new ArrayObject(); $this->request = new Request($psrRequest); $this->response = new Response(new PsrResponse()); From 9cdaf0045e0bffc97a40d36166fca9e67ac7471b Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 18 Jan 2015 19:22:44 -0600 Subject: [PATCH 12/13] Updated to psr/http-message 0.6.0 and phly/http 0.8.1 --- composer.json | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/composer.json b/composer.json index 54b364c..5efa397 100644 --- a/composer.json +++ b/composer.json @@ -27,8 +27,8 @@ }, "require": { "php": ">=5.4.8", - "phly/http": "dev-feature/immutability@dev", - "psr/http-message": "dev-feature/immutability@dev", + "phly/http": "~0.8.0", + "psr/http-message": "~0.6.0", "zendframework/zend-escaper": "~2.3@stable" }, "require-dev": { @@ -44,15 +44,5 @@ "psr-4": { "PhlyTest\\Conduit\\": "test/" } - }, - "repositories": [ - { - "url": "https://github.com/weierophinney/http-message.git", - "type": "vcs" - }, - { - "url": "https://github.com/weierophinney/http.git", - "type": "vcs" - } - ] + } } From 68d341edf6073ab3bae52106506bff6cb02a129d Mon Sep 17 00:00:00 2001 From: Matthew Weier O'Phinney Date: Sun, 18 Jan 2015 19:32:49 -0600 Subject: [PATCH 13/13] Prepared 0.9.0 CHANGELOG --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 53f4d9f..b7f3074 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,42 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release.. +## 0.9.0 - 2015-01-18 + +This version syncs Conduit with psr/http-message 0.6.0 and phly/http 0.8.1. The +primary changes are: + +- `Phly\Conduit\Http\Request` now implements + `Psr\Http\Message\ServerRequestInterface`, and extends + `Phly\Http\ServerRequest`, which means it is also now immutable. It no longer + provides property access to attributes, and also now stores the original + request, not the original URI, as a property, providing an accessor to it. +- `Phly\Conduit\Http\Response` now implements + `Psr\Http\Message\ResponseInterface`, which means it is now immutable. +- The logic in `Phly\Conduit\Next`'s `__invoke()` was largely rewritten due to + the fact that the request/response pair are now immutable, and the fact that + the URI is now an object (simplifying many operations). +- The logic in `Phly\Conduit\Middleware`, `Phly\Conduit\Dispatch`, and + `Phly\Conduit\FinalHandler` also needed slight updates to work with the + request/response changes. + +### Added + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + + ## 0.8.2 - 2014-11-05 ### Added