Skip to content

Commit

Permalink
Merge branch 'feature/immutability'
Browse files Browse the repository at this point in the history
Sync with psr/http-message 0.6.0 and phly/http 0.8.1.
  • Loading branch information
weierophinney committed Jan 19, 2015
2 parents 45bfb27 + 68d341e commit 6514e24
Show file tree
Hide file tree
Showing 15 changed files with 901 additions and 400 deletions.
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
161 changes: 141 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!');
Expand Down Expand Up @@ -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);
Expand All @@ -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
) {
}
Expand All @@ -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?
}
Expand Down Expand Up @@ -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
);
}
Expand All @@ -256,29 +256,150 @@ 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

`Phly\Conduit\FinalHandler` is a default implementation of middleware to execute when the stack exhausts itself. It is provided the request and response object to the constructor, and expects zero or one arguments when invoked; one argument indicates an error condition.

`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()`.
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
8 changes: 3 additions & 5 deletions src/Dispatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
32 changes: 19 additions & 13 deletions src/FinalHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

/**
Expand All @@ -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;
}

/**
Expand Down
Loading

0 comments on commit 6514e24

Please sign in to comment.