Skip to content

Commit

Permalink
Merge branch 'feature/route-result-observer'
Browse files Browse the repository at this point in the history
  • Loading branch information
weierophinney committed Nov 30, 2015
2 parents e45379f + 2ee9ba1 commit 226a185
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 1 deletion.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ Third release candidate.
array **must** be callables, service names resolving to callable middleware,
or fully qualified class names that can be instantiated without arguments, and
which result in invokable middleware.
- [#200](https://github.com/zendframework/zend-expressive/pull/200) adds a new
interface, `Zend\Expressive\Router\RouteResultObserverInterface`.
`Zend\Expressive\Application` now also defines two methods,
`attachRouteResultObserver()` and `detachRouteResultObserver()`, which accept
instances of the interface. During `routeMiddleware()`, all observers are
updated immediately following the call to `RouterInterface::match()` with the
`RouteResult` instance. This feature enables the ability to notify objects of
the calculated `RouteResult` without needing to inject middleware into the
system.

### Deprecated

Expand Down
1 change: 1 addition & 0 deletions doc/book/router/bookdown.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
{"Introduction": "intro.md"},
{"Routing Interface": "interface.md"},
{"URI Generation": "uri-generation.md"},
{"Route Result Observers": "result-observers.md"},
{"Routing vs Piping": "piping.md"},
{"Using Aura": "aura.md"},
{"Using FastRoute": "fast-route.md"},
Expand Down
216 changes: 216 additions & 0 deletions doc/book/router/result-observers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# Route Result Observers

Occasionally, you may have need of the `RouteResult` within other application
code. As a primary example, a URI generator may want this information to allow
creating "self" URIs, or to allow presenting a subset of parameters to generate
a URI.

Consider this URI:

```php
'/api/v{version:\d+}/post/{post_id:\d+}/comment/{comment_id:\d+}'
```

If you wanted to generate URIs to a list of related comments, you may not want
to pass the `$version` and `$post_id` parameters each and every time, but
instead just the `$comment_id`. As such, *route result observers* exist to allow
you to notify such utilities of the results of matching.

## RouteResultObserverInterface

Route result observers must implement the `RouteResultObserverInterface`:

```php
namespace Zend\Expressive\Router;

interface RouteResultObserverInterface
{
/**
* Observe a route result.
*
* @param RouteResult $result
*/
public function update(RouteResult $result);
}
```

These can then be attached to the `Application` instance:

```php
$app->attachRouteResultObserver($observer);
```

As noted, the observer receives the `RouteResult` from attempting to match a
route.

You can detach an existing observer as well, by passing its instance to the
`detachRouteResultObserver()` method:

```php
$app->detachRouteResultObserver($observer);
```

## Example

For this example, we'll build a simple URI generator. It will compose a
`RouterInterface` implementation, implement `RouteResultObserverInterface`, and,
when invoked, generate a URI.

```php
use Zend\Expressive\Router\RouterInterface;
use Zend\Expressive\Router\RouteResult;
use Zend\Expressive\Router\RouteResultObserverInterface;

class UriGenerator implements RouteResultObserverInterface
{
private $defaults = [];

private $routeName;

private $router;

public function __construct(RouterInterface $router)
{
$this->router = $router;
}

public function update(RouteResult $result)
{
if ($result->isFailure()) {
return;
}

$this->routeName = $result->getMatchedRouteName();
$this->defaults = $result->getMatchedParams();
}

public function __invoke($route = null, array $params = [])
{
if (! $route && ! $this->routeName) {
throw new InvalidArgumentException('Missing route, and no route was matched to use as a default!');
}

$route = $route ?: $this->routeName;

if ($route === $this->routeName) {
$params = array_merge($this->defaults, $params);
}

return $this->router->generateUri($route, $params);
}
}
```

Now that we've defined the `UriGenerator`, we need:

- a factory for creating it
- a way to attach it to the application

First, the factory, which is essentially a one-liner wrapped in a class:

```php
use Container\Interop\ContainerInterface;
use Zend\Expressive\Router\RouterInterface;

class UriGeneratorFactory
{
public function __invoke(ContainerInterface $container)
{
return new UriGenerator($container->get(RouterInterface::class));
}
}
```

Attaching the observer to the application can happen in one of two ways:

- Via modification of the bootstrap script.
- Via container-specific "extension" or "delegation" features.

### Modifying the bootstrap script

If you choose this method, you will modify your `public/index.php` script (or
whatever script you've defined as the application gateway.) The following
assumes you're using the `public/index.php` generated for you when using the
Expressive skeleton.

In this case, you would attach any observers between the line where you fetch
the application from the container, and the line when you run it.

```php
$app = $container->get('Zend\Expressive\Application');

// Attach observers
$app->attachRouteResultObserver($container->get(UriGenerator::class));

$app->run();
```

### Container-specific Delegation

Pimple offers a feature called "extension" to allow modification of a service
after creation, and zend-servicemanager provides a [delegator factories](http://framework.zend.com/manual/current/en/modules/zend.service-manager.delegator-factories.html)
feature for a similar purpose.

Both examples below assume you are using the Expressive skeleton to generate
your initial project; if not, read the examples, and adapt them to your own
configuration and container initialization strategy.

To make use of this in Pimple, you would modify the `config/container.php` file
to add the following just prior to returning the container instance:

```php
$container->extend('Zend\Expressive\Application', function ($app, $container) {
$app->attachRouteResultObserver($container->get(UriGenerator::class));
return $app;
});
```

For zend-servicemanager, you will do two things:

- Create a delegator factory
- Add the delegator factory to your configuration

The delegator factory will look like this:

```php
use Zend\ServiceManager\DelegatorFactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;

class UriGeneratorDelegatorFactory
{
public function createDelegatorWithName(
ServiceLocatorInterface $container,
$name,
$requestedName,
$callback
) {
$app = $callback();
$app->attachRouteResultObserver($container->get(UriGenerator::class));
return $app;
}
}
```

From here, you can register the delegator factory in any configuration file
where you're specifying application dependencies; we recommend a
`config/autoload/dependencies.global.php` file for this.

```php
use Zend\Expressive\Application;

return [
'dependencies' => [
'factories' => [
UriGenerator::class => UriGeneratorFactory::class,
],
'delegator_factories' => [
Application::class => [
UriGeneratorDelegatorFactory::class,
],
],
],
];
```

Note: You may see code like the above already, for either example, depending on
the selections you made when creating your project!
2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pages:
- { 'Quick Start: Standalone': quick-start.md }
- { Applications: application.md }
- { Containers: [{ Introduction: container/intro.md }, { 'Container Factories': container/factories.md }, { 'Using zend-servicemanager': container/zend-servicemanager.md }, { 'Using Pimple': container/pimple.md }, { 'Using Aura.Di': container/aura-di.md }] }
- { 'Routing Adapters': [{ Introduction: router/intro.md }, { 'Routing Interface': router/interface.md }, { 'URI Generation': router/uri-generation.md }, { 'Routing vs Piping': router/piping.md }, { 'Using Aura': router/aura.md }, { 'Using FastRoute': router/fast-route.md }, { 'Using the ZF2 Router': router/zf2.md }] }
- { 'Routing Adapters': [{ Introduction: router/intro.md }, { 'Routing Interface': router/interface.md }, { 'URI Generation': router/uri-generation.md }, { 'Route Result Observers': router/result-observers.md }, { 'Routing vs Piping': router/piping.md }, { 'Using Aura': router/aura.md }, { 'Using FastRoute': router/fast-route.md }, { 'Using the ZF2 Router': router/zf2.md }] }
- { Templating: [{ Introduction: template/intro.md }, { 'Template Renderer Interface': template/interface.md }, { 'Templated Middleware': template/middleware.md }, { 'Using Plates': template/plates.md }, { 'Using Twig': template/twig.md }, { 'Using zend-view': template/zend-view.md }] }
- { 'Error Handling': error-handling.md }
- { Emitters: emitters.md }
Expand Down
43 changes: 43 additions & 0 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ class Application extends MiddlewarePipe
*/
private $router;

/**
* Observers to trigger once we have a route result.
*
* @var Router\RouteResultObserverInterface[]
*/
private $routeResultObservers = [];

/**
* List of all routes registered directly with the application.
*
Expand Down Expand Up @@ -166,6 +173,29 @@ public function any($path, $middleware, $name = null)
return $this->route($path, $middleware, null, $name);
}

/**
* Attach a route result observer.
*
* @param Router\RouteResultObserverInterface $observer
*/
public function attachRouteResultObserver(Router\RouteResultObserverInterface $observer)
{
$this->routeResultObservers[] = $observer;
}

/**
* Detach a route result observer.
*
* @param Router\RouteResultObserverInterface $observer
*/
public function detachRouteResultObserver(Router\RouteResultObserverInterface $observer)
{
if (false === ($index = array_search($observer, $this->routeResultObservers, true))) {
return;
}
unset($this->routeResultObservers[$index]);
}

/**
* Overload pipe() operation.
*
Expand Down Expand Up @@ -315,6 +345,7 @@ public function pipeRoutingMiddleware()
public function routeMiddleware(ServerRequestInterface $request, ResponseInterface $response, callable $next)
{
$result = $this->router->match($request);
$this->notifyRouteResultObservers($result);

if ($result->isFailure()) {
if ($result->isMethodFailure()) {
Expand Down Expand Up @@ -661,4 +692,16 @@ private function marshalLazyErrorMiddlewareService($middleware, ContainerInterfa
return $invokable($error, $request, $response, $next);
};
}

/**
* Notify all route result observers with the given route result.
*
* @param Router\RouteResult
*/
private function notifyRouteResultObservers(Router\RouteResult $result)
{
foreach ($this->routeResultObservers as $observer) {
$observer->update($result);
}
}
}
20 changes: 20 additions & 0 deletions src/Router/RouteResultObserverInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
/**
* Zend Framework (http://framework.zend.com/)
*
* @see https://github.com/zendframework/zend-expressive for the canonical source repository
* @copyright Copyright (c) 2015 Zend Technologies USA Inc. (http://www.zend.com)
* @license https://github.com/zendframework/zend-expressive/blob/master/LICENSE.md New BSD License
*/

namespace Zend\Expressive\Router;

interface RouteResultObserverInterface
{
/**
* Observe a route result.
*
* @param RouteResult $result
*/
public function update(RouteResult $result);
}
Loading

0 comments on commit 226a185

Please sign in to comment.