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 diff --git a/README.md b/README.md index 8050e5e..033917b 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,102 @@ 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(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: + +#### 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 response: + +```php +function ($request, $response, $next) +{ + $response = $response->addHeader('Cache-Control', [ + 'public', + 'max-age=18600', + 's-maxage=18600', + ]); + return $next($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 + +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) +{ + $response = $response->addHeader('Cache-Control', [ + 'public', + 'max-age=18600', + 's-maxage=18600', + ]); + return $response; +} +``` + +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 +return $next(/* ... */); +``` + +#### 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 +359,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". + +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. -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. +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()`. diff --git a/composer.json b/composer.json index 88b5c6f..5efa397 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": "~0.8.0", + "psr/http-message": "~0.6.0", "zendframework/zend-escaper": "~2.3@stable" }, "require-dev": { diff --git a/src/Dispatch.php b/src/Dispatch.php index cbb3c4c..eb14625 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 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 4d87213..a3f7351 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->withStatus( $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->withStatus(404); - $url = $this->request->originalUrl ?: $this->request->getUrl(); - $escaper = new Escaper(); - $message = sprintf( + $originalRequest = $this->request->getOriginalRequest(); + $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) ); - $this->response->end($message); + $response->end($message); + return $response; } /** diff --git a/src/Http/Request.php b/src/Http/Request.php index bbd61d8..1f0daf1 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -2,121 +2,116 @@ namespace Phly\Conduit\Http; use ArrayObject; -use Psr\Http\Message\IncomingRequestInterface; +use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\StreamableInterface; +use Psr\Http\Message\UriTargetInterface; /** - * Decorator for PSR IncomingRequestInterface + * 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 */ -class Request implements IncomingRequestInterface +class Request implements ServerRequestInterface { /** - * Current URL (URL set in the proxy) - * - * @var string + * Original ServerRequestInterface instance. + * + * @var mixed */ - private $currentUrl; + private $originalRequest; /** - * @var IncomingRequestInterface + * 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; /** - * @param IncomingRequestInterface $request + * @param ServerRequestInterface $request */ - public function __construct(IncomingRequestInterface $request) - { - $this->psrRequest = $request; - $this->originalUrl = $request->getUrl(); - } + public function __construct( + ServerRequestInterface $decoratedRequest, + ServerRequestInterface $originalRequest = null + ) { + if (null === $originalRequest) { + $originalRequest = $decoratedRequest; + } - /** - * Return the original PSR request object - * - * @return IncomingRequestInterface - */ - public function getOriginalRequest() - { - return $this->psrRequest; + $this->originalRequest = $originalRequest; + $this->psrRequest = $decoratedRequest->withAttribute('originalUri', $originalRequest->getUri()); } /** - * Property overloading: get property value - * - * Returns null if property is not set. + * Return the currently decorated PSR request instance * - * @param string $name - * @return mixed + * @return ServerRequestInterface */ - public function __get($name) + public function getCurrentRequest() { - return $this->psrRequest->getAttribute($name); + return $this->psrRequest; } /** - * Property overloading: set property + * Return the original PSR request instance * - * @param string $name - * @param mixed $value + * @return ServerRequestInterface */ - public function __set($name, $value) + public function getOriginalRequest() { - if (is_array($value)) { - $value = new ArrayObject($value, ArrayObject::ARRAY_AS_PROPS); - } - - return $this->psrRequest->setAttribute($name, $value); + return $this->originalRequest; } /** - * Property overloading: is property set? + * Proxy to ServerRequestInterface::getProtocolVersion() * - * @param string $name - * @return bool + * @return string HTTP protocol version. */ - public function __isset($name) + public function getProtocolVersion() { - return (bool) $this->psrRequest->getAttribute($name, false); + return $this->psrRequest->getProtocolVersion(); } /** - * Property overloading: unset property + * Proxy to ServerRequestInterface::withProtocolVersion() * - * @param string $name + * @param string $version HTTP protocol version. + * @return self */ - public function __unset($name) + public function withProtocolVersion($version) { - $this->psrRequest->setAttribute($name, null); + $new = $this->psrRequest->withProtocolVersion($version); + return new self($new, $this->originalRequest); } /** - * Proxy to IncomingRequestInterface::getProtocolVersion() + * Proxy to ServerRequestInterface::getBody() * - * @return string HTTP protocol version. + * @return StreamableInterface Returns the body stream. */ - public function getProtocolVersion() + public function getBody() { - return $this->psrRequest->getProtocolVersion(); + return $this->psrRequest->getBody(); } /** - * Proxy to IncomingRequestInterface::getBody() + * Proxy to ServerRequestInterface::withBody() * - * @return StreamableInterface Returns the body stream. + * @param StreamableInterface $body Body. + * @return self + * @throws \InvalidArgumentException When the body is not valid. */ - public function getBody() + public function withBody(StreamableInterface $body) { - return $this->psrRequest->getBody(); + $new = $this->psrRequest->withBody($body); + return new self($new, $this->originalRequest); } /** - * Proxy to IncomingRequestInterface::getHeaders() + * Proxy to ServerRequestInterface::getHeaders() * * @return array Returns an associative array of the message's headers. */ @@ -126,7 +121,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 +134,7 @@ public function hasHeader($header) } /** - * Proxy to IncomingRequestInterface::getHeader() + * Proxy to ServerRequestInterface::getHeader() * * @param string $header Case-insensitive header name. * @return string @@ -150,18 +145,56 @@ 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::withHeader() + * + * @param string $header Header name + * @param string|string[] $value Header value(s) + * @return self + */ + public function withHeader($header, $value) + { + $new = $this->psrRequest->withHeader($header, $value); + return new self($new, $this->originalRequest); + } + + /** + * 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 + * @return self + */ + public function withAddedHeader($header, $value) { - return $this->psrRequest->getHeaderAsArray($header); + $new = $this->psrRequest->withAddedHeader($header, $value); + return new self($new, $this->originalRequest); } /** - * Proxy to IncomingRequestInterface::getMethod() + * Proxy to ServerRequestInterface::removeHeader() + * + * @param string $header HTTP header to remove + * @return self + */ + public function withoutHeader($header) + { + $new = $this->psrRequest->withoutHeader($header); + return new self($new, $this->originalRequest); + } + + /** + * Proxy to ServerRequestInterface::getMethod() * * @return string Returns the request method. */ @@ -171,43 +204,45 @@ public function getMethod() } /** - * Proxy to IncomingRequestInterface::getUrl() + * Proxy to ServerRequestInterface::withMethod() * - * @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. - * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * @param string $method The request method. + * @return self */ - public function getUrl() + public function withMethod($method) { - if ($this->currentUrl) { - return $this->currentUrl; - } - - return $this->psrRequest->getUrl(); + $new = $this->psrRequest->withMethod($method); + return new self($new, $this->originalRequest); } /** - * Allow mutating the URL - * - * Also sets originalUrl property if not previously set. + * Proxy to ServerRequestInterface::getUri() * + * @return UriTargetInterface Returns a UriTargetInterface instance * @link http://tools.ietf.org/html/rfc3986#section-4.3 - * @param string|object $url Request URL. - * @throws \InvalidArgumentException If the URL is invalid. */ - public function setUrl($url) + public function getUri() { - $this->currentUrl = $url; + return $this->psrRequest->getUri(); + } - if (! $this->originalUrl) { - $this->originalUrl = $this->psrRequest->getUrl(); - } + /** + * Allow mutating the URI + * + * @link http://tools.ietf.org/html/rfc3986#section-4.3 + * @param UriTargetInterface $uri Request URI. + * @return self + * @throws \InvalidArgumentException If the URI is invalid. + */ + public function withUri(UriTargetInterface $uri) + { + $new = $this->psrRequest->withUri($uri); + return new self($new, $this->originalRequest); } /** - * Proxy to IncomingRequestInterface::getServerParams() - * + * Proxy to ServerRequestInterface::getServerParams() + * * @return array */ public function getServerParams() @@ -216,7 +251,7 @@ public function getServerParams() } /** - * Proxy to IncomingRequestInterface::getCookieParams() + * Proxy to ServerRequestInterface::getCookieParams() * * @return array */ @@ -226,8 +261,20 @@ public function getCookieParams() } /** - * Proxy to IncomingRequestInterface::getQueryParams() - * + * Proxy to ServerRequestInterface::withCookieParams() + * + * @param array $cookies + * @return self + */ + public function withCookieParams(array $cookies) + { + $new = $this->psrRequest->withCookieParams($cookies); + return new self($new, $this->originalRequest); + } + + /** + * Proxy to ServerRequestInterface::getQueryParams() + * * @return array */ public function getQueryParams() @@ -236,8 +283,20 @@ public function getQueryParams() } /** - * Proxy to IncomingRequestInterface::getFileParams() - * + * Proxy to ServerRequestInterface::withQueryParams() + * + * @param array $query + * @return self + */ + public function withQueryParams(array $query) + { + $new = $this->psrRequest->withQueryParams($query); + return new self($new, $this->originalRequest); + } + + /** + * Proxy to ServerRequestInterface::getFileParams() + * * @return array Upload file(s) metadata, if any. */ public function getFileParams() @@ -246,9 +305,9 @@ public function getFileParams() } /** - * Proxy to IncomingRequestInterface::getBodyParams() + * Proxy to ServerRequestInterface::getBodyParams() + * * - * * @return array The deserialized body parameters, if any. */ public function getBodyParams() @@ -257,7 +316,19 @@ public function getBodyParams() } /** - * Proxy to IncomingRequestInterface::getAttributes() + * Proxy to ServerRequestInterface::withBodyParams() + * + * @param array $params The deserialized body parameters. + * @return self + */ + public function withBodyParams(array $params) + { + $new = $this->psrRequest->withBodyParams($params); + return new self($new, $this->originalRequest); + } + + /** + * Proxy to ServerRequestInterface::getAttributes() * * @return array Attributes derived from the request */ @@ -267,10 +338,10 @@ public function getAttributes() } /** - * Proxy to IncomingRequestInterface::getAttribute() - * - * @param string $attribute - * @param mixed $default + * Proxy to ServerRequestInterface::getAttribute() + * + * @param string $attribute + * @param mixed $default * @return mixed */ public function getAttribute($attribute, $default = null) @@ -279,24 +350,27 @@ public function getAttribute($attribute, $default = null) } /** - * Proxy to IncomingRequestInterface::setAttributes() + * Proxy to ServerRequestInterface::withAttribute() * - * @param array Attributes derived from the request + * @param string $attribute + * @param mixed $value + * @return self */ - public function setAttributes(array $values) + public function withAttribute($attribute, $value) { - return $this->psrRequest->setAttributes($values); + $new = $this->psrRequest->withAttribute($attribute, $value); + return new self($new, $this->originalRequest); } /** - * Proxy to IncomingRequestInterface::setAttribute() - * - * @param string $attribute - * @param mixed $value - * @return void + * Proxy to ServerRequestInterface::withoutAttribute() + * + * @param string $attribute + * @return self */ - public function setAttribute($attribute, $value) + public function withoutAttribute($attribute) { - return $this->psrRequest->setAttribute($attribute, $value); + $new = $this->psrRequest->withoutAttribute($attribute); + return new self($new, $this->originalRequest); } } diff --git a/src/Http/Response.php b/src/Http/Response.php index 33f7dbb..2b4c08d 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() { @@ -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; } /** @@ -95,7 +98,7 @@ public function isComplete() } /** - * Proxy to BaseResponseInterface::getProtocolVersion() + * Proxy to PsrResponseInterface::getProtocolVersion() * * @return string HTTP protocol version. */ @@ -105,18 +108,19 @@ public function getProtocolVersion() } /** - * Proxy to BaseResponseInterface::setProtocolVersion() - * - * @param string $version - * @return void + * Proxy to PsrResponseInterface::withProtocolVersion() + * + * @param string $version + * @return Response */ - public function setProtocolVersion($version) + public function withProtocolVersion($version) { - return $this->psrResponse->setProtocolVersion($version); + $new = $this->psrResponse->withProtocolVersion($version); + return new self($new); } /** - * Proxy to BaseResponseInterface::getBody() + * Proxy to PsrResponseInterface::getBody() * * @return StreamableInterface|null Returns the body, or null if not set. */ @@ -126,22 +130,24 @@ public function getBody() } /** - * Proxy to BaseResponseInterface::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; + return $this; } - return $this->psrResponse->setBody($body); + $new = $this->psrResponse->withBody($body); + return new self($new); } /** - * Proxy to BaseResponseInterface::getHeaders() + * Proxy to PsrResponseInterface::getHeaders() * * @return array Returns an associative array of the message's headers. */ @@ -151,7 +157,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 +170,7 @@ public function hasHeader($header) } /** - * Proxy to BaseResponseInterface::getHeader() + * Proxy to PsrResponseInterface::getHeader() * * @param string $header Case-insensitive header name. * @return string @@ -175,62 +181,68 @@ 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::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; + return $this; } - return $this->psrResponse->setHeader($header, $value); + $new = $this->psrResponse->withHeader($header, $value); + return new self($new); } /** - * Proxy to BaseResponseInterface::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; + return $this; } - return $this->psrResponse->addHeader($header, $value); + $new = $this->psrResponse->withAddedHeader($header, $value); + return new self($new); } /** - * Proxy to BaseResponseInterface::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; + return $this; } - return $this->psrResponse->removeHeader($header); + $new = $this->psrResponse->withoutHeader($header); + return new self($new); } /** - * Proxy to BaseResponseInterface::getStatusCode() + * Proxy to PsrResponseInterface::getStatusCode() * * @return integer Status code. */ @@ -240,22 +252,24 @@ public function getStatusCode() } /** - * Proxy to BaseResponseInterface::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; + return $this; } - return $this->psrResponse->setStatus($code, $reasonPhrase); + $new = $this->psrResponse->withStatus($code, $reasonPhrase); + return new self($new); } /** - * 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..240bb4c 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 @@ -61,17 +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); - $request->setUrl($this->getUrlFromRequest($request)); - $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; } /** @@ -124,21 +128,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..3a8ec83 100644 --- a/src/Next.php +++ b/src/Next.php @@ -3,6 +3,9 @@ use ArrayObject; use Phly\Http\Uri; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface; +use RuntimeException; /** * Iterate a stack of middlewares and execute them @@ -63,15 +66,37 @@ 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 + * @return ResponseInterface */ - public function __invoke($err = null) + public function __invoke($state = null, ResponseInterface $response = null) { - $request = $this->request; + $err = null; + $resetRequest = false; + + if ($state instanceof ResponseInterface) { + $this->response = $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 +104,7 @@ public function __invoke($err = null) } $layer = $this->stack[$this->index++]; - $path = parse_url($this->request->getUrl(), PHP_URL_PATH) ?: '/'; + $path = $this->request->getUri()->getPath() ?: '/'; $route = $layer->path; // Skip if layer path does not match current url @@ -98,25 +123,39 @@ public function __invoke($err = null) $this->stripRouteFromPath($route); } - $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; } /** * 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; - $new = $uri->setPath($path); - $request->setUrl((string) $new); + $uri = $request->getUri(); + $path = $uri->getPath(); + + if ($resetRequest + && strlen($path) >= strlen($this->removed) + && 0 === strpos($path, $this->removed) + ) { + $path = str_replace($this->removed, '', $path); + } + + $path = $this->removed . $path; + $new = $uri->withPath($path); $this->removed = ''; + $this->request = $request->withUri($new); } /** @@ -144,9 +183,36 @@ private function stripRouteFromPath($route) { $this->removed = $route; - $uri = new Uri($this->request->getUrl()); - $path = substr($uri->path, strlen($route)); - $new = $uri->setPath($path); - $this->request->setUrl((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/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 f71ca79..beac07f 100644 --- a/test/FinalHandlerTest.php +++ b/test/FinalHandlerTest.php @@ -5,8 +5,9 @@ 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 Phly\Http\Uri; use PHPUnit_Framework_TestCase as TestCase; use Zend\Escaper\Escaper; @@ -14,54 +15,55 @@ class FinalHandlerTest extends TestCase { public function setUp() { + $psrRequest = new PsrRequest([], [], 'http://example.com/', 'GET', 'php://memory'); $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); } 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() @@ -69,9 +71,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() @@ -86,33 +88,29 @@ 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 test404ResponseIncludesOriginalRequestUrl() + public function test404ResponseIncludesOriginalRequestUri() { $originalUrl = 'http://local.example.com/bar/foo'; - $request = new Request(new PsrRequest( - $originalUrl, - 'GET', - [], - 'php://memory' - )); - $request->setUrl('http://local.example.com/foo'); + $psrRequest = new PsrRequest([], [], $originalUrl, 'GET', 'php://memory'); + $request = new Request($psrRequest); + $request = $request->withUri(new Uri('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 b65fbe0..6b6e25a 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,56 +10,42 @@ class RequestTest extends TestCase { public function setUp() { - $this->original = new PsrRequest( - 'http://example.com/', - 'GET', - [], - 'php://memory' - ); + $psrRequest = new PsrRequest([], [], 'http://example.com/', 'GET', 'php://memory'); + $this->original = $psrRequest; $this->request = new Request($this->original); } - public function testAllowsManipulatingArbitraryNonPrivateProperties() + public function testCallingSetUriSetsUriInRequestAndOriginalRequestInClone() { - $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); + $url = 'http://example.com/foo'; + $request = $this->request->withUri(new Uri($url)); + $this->assertNotSame($this->request, $request); + $this->assertSame($this->original, $request->getOriginalRequest()); + $this->assertSame($url, (string) $request->getUri()); } - public function testFetchingUnknownPropertyYieldsNull() + public function testConstructorSetsOriginalRequestIfNoneProvided() { - $this->assertNull($this->request->somePropertyWeMadeUp); - } + $url = 'http://example.com/foo'; + $baseRequest = new PsrRequest([], [], $url, 'GET', 'php://memory'); - public function testArrayPropertyValueIsCastToArrayObject() - { - $original = ['test' => 'value']; - $this->request->anArray = $original; - $this->assertInstanceOf('ArrayObject', $this->request->anArray); - $this->assertEquals($original, $this->request->anArray->getArrayCopy()); + $request = new Request($baseRequest); + $this->assertSame($baseRequest, $request->getOriginalRequest()); } - public function testCallingSetUrlSetsOriginalUrlProperty() + public function testCallingSettersRetainsOriginalRequest() { $url = 'http://example.com/foo'; - $this->request->setUrl($url); - $this->assertSame('http://example.com/', $this->request->originalUrl); - $this->assertSame($url, $this->request->getUrl()); - } + $baseRequest = new PsrRequest([], [], $url, 'GET', 'php://memory'); - public function testConstructorSetsOriginalUrlIfDecoratedRequestHasUrl() - { - $url = 'http://example.com/foo'; - $baseRequest = new PsrRequest( - $url, - 'GET', - [], - 'php://memory' - ); $request = new Request($baseRequest); - $this->assertSame($baseRequest->getUrl(), $request->originalUrl); + $request = $request->withMethod('POST'); + $new = $request->withAddedHeader('X-Foo', 'Bar'); + + $this->assertNotSame($request, $new); + $this->assertNotSame($baseRequest, $new); + $this->assertNotSame($baseRequest, $new->getCurrentRequest()); + $this->assertSame($baseRequest, $new->getOriginalRequest()); } public function testCanAccessOriginalRequest() @@ -70,35 +56,14 @@ 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([], [], 'http://example.com', 'POST', $stream, [ + 'Accept' => 'application/xml', + '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 805bba1..dc7f9bf 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; @@ -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->withStatus(201); + $response = $response->write("First\n"); + $response = $response->end('DONE'); + + $test = $response->withStatus(200); + $test = $test->withHeader('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->withBody($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->withAddedHeader('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->withBody($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->withHeader('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->withAddedHeader('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->withoutHeader('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->withStatus(200, 'FOOBAR'); + $this->assertNotSame($this->response, $response); + $this->assertEquals('FOOBAR', $response->getReasonPhrase()); } } diff --git a/test/MiddlewareTest.php b/test/MiddlewareTest.php index 638dc05..e409baf 100644 --- a/test/MiddlewareTest.php +++ b/test/MiddlewareTest.php @@ -5,8 +5,9 @@ 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 Phly\Http\Uri; use PHPUnit_Framework_TestCase as TestCase; use ReflectionProperty; @@ -14,12 +15,7 @@ class MiddlewareTest extends TestCase { public function setUp() { - $this->request = new Request( - 'http://example.com/', - 'GET', - [], - 'php://memory' - ); + $this->request = new Request([], [], 'http://example.com/', 'GET', 'php://memory'); $this->response = new Response(); $this->middleware = new Middleware(); } @@ -64,7 +60,7 @@ public function testHandleInvokesUntilFirstHandlerThatDoesNotCallNext() $phpunit->fail('Should not hit fourth handler!'); }); - $request = new Request('http://local.example.com/foo', 'GET', [], 'php://memory'); + $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); @@ -92,7 +88,7 @@ public function testHandleInvokesFirstErrorHandlerOnErrorInChain() $phpunit->fail('Should not hit fourth handler!'); }); - $request = new Request('http://local.example.com/foo', 'GET', [], 'php://memory'); + $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); @@ -117,7 +113,7 @@ public function testHandleInvokesOutHandlerIfStackIsExhausted() $next(); }); - $request = new Request('http://local.example.com/foo', 'GET', [], 'php://memory'); + $request = new Request([], [], 'http://local.example.com/foo', 'GET', 'php://memory'); $this->middleware->__invoke($request, $this->response, $out); $this->assertTrue($triggered); } @@ -153,7 +149,7 @@ public function testPipeWillCreateErrorClosureForObjectImplementingHandle() public function testCanUseDecoratedRequestAndResponseDirectly() { - $baseRequest = new Request('http://local.example.com/foo', 'GET', [], 'php://memory'); + $baseRequest = new Request([], [], 'http://local.example.com/foo', 'GET', 'php://memory'); $request = new RequestDecorator($baseRequest); $response = new ResponseDecorator($this->response); @@ -173,4 +169,51 @@ 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([], [], 'http://local.example.com/foo', 'GET', 'php://memory'); + $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) { + $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([], [], '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), + spl_object_hash($result) => get_class($result), + ], 1)); + } } diff --git a/test/NextTest.php b/test/NextTest.php index 7245334..cf5a169 100644 --- a/test/NextTest.php +++ b/test/NextTest.php @@ -6,21 +6,18 @@ 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 Phly\Http\Uri; use PHPUnit_Framework_TestCase as TestCase; class NextTest extends TestCase { public function setUp() { + $psrRequest = new PsrRequest([], [], 'http://example.com/', 'GET', 'php://memory'); $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()); } @@ -51,7 +48,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(); @@ -72,7 +69,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(); @@ -93,9 +90,9 @@ public function testInvokesHandlerWhenMatched() $phpunit->fail('Should not hit done handler'); }; - $this->request->setUrl('http://local.example.com/foo'); + $request = $this->request->withUri(new Uri('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); } @@ -106,7 +103,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; @@ -115,9 +112,9 @@ public function testRequestUriInInvokedHandlerDoesNotContainMatchedPortionOfRout $phpunit->fail('Should not hit done handler'); }; - $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, $this->request, $this->response, $done); + $next = new Next($this->stack, $request, $this->response, $done); $next(); $this->assertEquals('/bar', $triggered); } @@ -132,6 +129,7 @@ public function testSlashAndPathGetResetBeforeExecutingNextMiddleware() }); $route3 = new Route('/foo/baz', function ($req, $res, $next) { $res->end('done'); + return $res; }); $this->stack->append($route1); @@ -143,9 +141,143 @@ 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->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()); } + + 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->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); + } + + 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, $cannedResponse, &$triggered) { + $phpunit->assertSame($cannedResponse, $res); + $triggered = true; + }); + + $this->stack->append($route1); + $this->stack->append($route2); + + $done = function ($err) use ($phpunit) { + $phpunit->fail('Should not hit final handler'); + }; + + $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); + $this->assertSame($cannedResponse, $result); + } + + public function testMiddlewareCallingNextWithRequestResetsRequest() + { + $phpunit = $this; + $request = $this->request->withUri(new Uri('http://example.com/foo/bar/baz')); + $cannedRequest = clone $request; + $cannedRequest = $cannedRequest->withMethod('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->withUri(new Uri('http://example.com/foo/bar/baz')); + $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->withUri(new Uri('http://example.com/foo/bar/baz')); + $next = new Next($this->stack, $request, $this->response, $done); + $result = $next(); + $this->assertSame($cannedResponse, $result); + } }