Commit d53119eb authored by Fabien Potencier's avatar Fabien Potencier

added a route after middleware

parent 794b3c28
......@@ -3,7 +3,9 @@ Changelog
This changelog references all backward incompatibilities as we introduce them:
* **2012-06-13**: Renamed ``middleware`` to ``before``
* **2012-06-13**: Added a route ``before`` middleware
* **2012-06-13**: Renamed the route ``middleware`` to ``before``
* **2012-06-13**: Added an extension for the Symfony Security component
......
......@@ -419,8 +419,13 @@ Route middlewares
-----------------
Route middlewares are PHP callables which are triggered when their associated
route is matched. They are fired just before the route callback, but after the
application ``before`` filters.
route is matched:
* ``before`` middlewares are fired just before the route callback, but after
the application ``before`` filters;
* ``after`` middlewares are fired just after the route callback, but before
the application ``after`` filters.
This can be used for a lot of use cases; for instance, here is a simple
"anonymous/logged user" check::
......@@ -452,21 +457,24 @@ This can be used for a lot of use cases; for instance, here is a simple
})
->before($mustBeLogged);
The ``before`` function can be called several times for a given route, in
which case they are triggered in the same order as you added them to the
route.
The ``before`` and ``after`` methods can be called several times for a given
route, in which case they are triggered in the same order as you added them to
the route.
For convenience, the route before middlewares functions are triggered with the
current ``Request`` instance as their only argument.
For convenience, the ``before`` middlewares are called with the current
``Request`` instance as an argument and the ``after`` middlewares are called
with the current ``Request`` and ``Response`` instance as arguments.
If any of the route before middlewares returns a Symfony HTTP Response, it
will short-circuit the whole rendering: the next middlewares won't be run,
neither the route callback. You can also redirect to another page by returning
a redirect response, which you can create by calling the Application
If any of the before middlewares returns a Symfony HTTP Response, it will
short-circuit the whole rendering: the next middlewares won't be run, neither
the route callback. You can also redirect to another page by returning a
redirect response, which you can create by calling the Application
``redirect`` method.
If a route before middleware does not return a Symfony HTTP Response or
``null``, a ``RuntimeException`` is thrown.
.. note::
If a before middleware does not return a Symfony HTTP Response or
``null``, a ``RuntimeException`` is thrown.
Global Configuration
--------------------
......
......@@ -14,7 +14,6 @@ namespace Silex;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\TerminableInterface;
use Symfony\Component\HttpKernel\Event\KernelEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
......@@ -112,7 +111,7 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe
return new RedirectableUrlMatcher($app['routes'], $app['request_context']);
});
$this['route_before_middlewares_trigger'] = $this->protect(function (KernelEvent $event) use ($app) {
$this['route_before_middlewares_trigger'] = $this->protect(function (GetResponseEvent $event) use ($app) {
$request = $event->getRequest();
$routeName = $request->attributes->get('_route');
if (!$route = $app['routes']->get($routeName)) {
......@@ -126,7 +125,24 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe
return;
} elseif (null !== $ret) {
throw new \RuntimeException(sprintf('The before middleware for route "%s" returned an invalid response value. Must return null or an instance of Response.', $routeName));
throw new \RuntimeException(sprintf('A before middleware for route "%s" returned an invalid response value. Must return null or an instance of Response.', $routeName));
}
}
});
$this['route_after_middlewares_trigger'] = $this->protect(function (FilterResponseEvent $event) use ($app) {
$request = $event->getRequest();
$routeName = $request->attributes->get('_route');
if (!$route = $app['routes']->get($routeName)) {
return;
}
foreach ((array) $route->getOption('_after_middlewares') as $callback) {
$response = call_user_func($callback, $request, $event->getResponse());
if ($response instanceof Response) {
$event->setResponse($response);
} elseif (null !== $response) {
throw new \RuntimeException(sprintf('An after middleware for route "%s" returned an invalid response value. Must return null or an instance of Response.', $routeName));
}
}
});
......@@ -516,8 +532,9 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe
if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) {
$this->beforeDispatched = true;
$this['dispatcher']->dispatch(SilexEvents::BEFORE, $event);
$this['route_before_middlewares_trigger']($event);
}
$this['route_before_middlewares_trigger']($event);
}
/**
......@@ -557,6 +574,8 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe
*/
public function onKernelResponse(FilterResponseEvent $event)
{
$this['route_after_middlewares_trigger']($event);
if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) {
$this['dispatcher']->dispatch(SilexEvents::AFTER, $event);
}
......
......@@ -169,6 +169,20 @@ class Controller
return $this;
}
/**
* Sets a callback to handle after the route callback.
*
* @param mixed $callback A PHP callback to be triggered after the route callback
*
* @return Controller $this The current Controller instance
*/
public function after($callback)
{
$this->route->after($callback);
return $this;
}
/**
* Freezes the controller.
*
......
......@@ -222,7 +222,7 @@ class ControllerCollection
*
* @param mixed $callback A PHP callback to be triggered when the Route is matched, just before the route callback
*
* @return ControllerCollection $this The current Controller instance
* @return ControllerCollection $this The current ControllerCollection instance
*/
public function before($callback)
{
......@@ -235,6 +235,24 @@ class ControllerCollection
return $this;
}
/**
* Sets a callback to handle after the route callback.
*
* @param mixed $callback A PHP callback to be triggered after the route callback
*
* @return ControllerCollection $this The current ControllerCollection instance
*/
public function after($callback)
{
$this->defaultRoute->after($callback);
foreach ($this->controllers as $controller) {
$controller->after($callback);
}
return $this;
}
/**
* Persists and freezes staged controllers.
*
......
......@@ -120,4 +120,20 @@ class Route extends BaseRoute
return $this;
}
/**
* Sets a callback to handle after the route callback.
*
* @param mixed $callback A PHP callback to be triggered after the route callback
*
* @return Controller $this The current Controller instance
*/
public function after($callback)
{
$callbacks = $this->getOption('_after_middlewares');
$callbacks[] = $callback;
$this->setOption('_after_middlewares', $callbacks);
return $this;
}
}
......@@ -167,22 +167,34 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('text/html; charset=ISO-8859-1', $response->headers->get('Content-Type'));
}
public function testRoutesBeforeMiddlewares()
public function testRoutesMiddlewares()
{
$app = new Application();
$test = $this;
$middlewareTarget = array();
$middleware1 = function (Request $request) use (&$middlewareTarget, $test) {
$beforeMiddleware1 = function (Request $request) use (&$middlewareTarget, $test) {
$test->assertEquals('/reached', $request->getRequestUri());
$middlewareTarget[] = 'middleware1_triggered';
$middlewareTarget[] = 'before_middleware1_triggered';
};
$middleware2 = function (Request $request) use (&$middlewareTarget, $test) {
$beforeMiddleware2 = function (Request $request) use (&$middlewareTarget, $test) {
$test->assertEquals('/reached', $request->getRequestUri());
$middlewareTarget[] = 'middleware2_triggered';
$middlewareTarget[] = 'before_middleware2_triggered';
};
$middleware3 = function (Request $request) use (&$middlewareTarget, $test) {
$beforeMiddleware3 = function (Request $request) use (&$middlewareTarget, $test) {
throw new \Exception('This middleware shouldn\'t run!');
};
$afterMiddleware1 = function (Request $request, Response $response) use (&$middlewareTarget, $test) {
$test->assertEquals('/reached', $request->getRequestUri());
$middlewareTarget[] = 'after_middleware1_triggered';
};
$afterMiddleware2 = function (Request $request, Response $response) use (&$middlewareTarget, $test) {
$test->assertEquals('/reached', $request->getRequestUri());
$middlewareTarget[] = 'after_middleware2_triggered';
};
$afterMiddleware3 = function (Request $request, Response $response) use (&$middlewareTarget, $test) {
throw new \Exception('This middleware shouldn\'t run!');
};
......@@ -191,17 +203,20 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
return 'hello';
})
->before($middleware1)
->before($middleware2);
->before($beforeMiddleware1)
->before($beforeMiddleware2)
->after($afterMiddleware1)
->after($afterMiddleware2);
$app->get('/never-reached', function () use (&$middlewareTarget) {
throw new \Exception('This route shouldn\'t run!');
})
->before($middleware3);
->before($beforeMiddleware3)
->after($afterMiddleware3);
$result = $app->handle(Request::create('/reached'));
$this->assertSame(array('middleware1_triggered', 'middleware2_triggered', 'route_triggered'), $middlewareTarget);
$this->assertSame(array('before_middleware1_triggered', 'before_middleware2_triggered', 'route_triggered', 'after_middleware1_triggered', 'after_middleware2_triggered'), $middlewareTarget);
$this->assertEquals('hello', $result->getContent());
}
......@@ -222,6 +237,23 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('foo', $result->getContent());
}
public function testRoutesAfterMiddlewaresWithResponseObject()
{
$app = new Application();
$app->get('/foo', function () {
return new Response('foo');
})
->after(function () {
return new Response('bar');
});
$request = Request::create('/foo');
$result = $app->handle($request);
$this->assertEquals('bar', $result->getContent());
}
public function testRoutesBeforeMiddlewaresWithRedirectResponseObject()
{
$app = new Application();
......@@ -263,6 +295,29 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$this->assertSame(array('before_triggered', 'middleware_triggered', 'route_triggered'), $middlewareTarget);
}
public function testRoutesAfterMiddlewaresTriggeredBeforeSilexAfterFilters()
{
$app = new Application();
$middlewareTarget = array();
$middleware = function (Request $request) use (&$middlewareTarget) {
$middlewareTarget[] = 'middleware_triggered';
};
$app->get('/foo', function () use (&$middlewareTarget) {
$middlewareTarget[] = 'route_triggered';
})
->after($middleware);
$app->after(function () use (&$middlewareTarget) {
$middlewareTarget[] = 'after_triggered';
});
$app->handle(Request::create('/foo'));
$this->assertSame(array('route_triggered', 'middleware_triggered', 'after_triggered'), $middlewareTarget);
}
public function testFinishFilter()
{
$containerTarget = array();
......@@ -309,6 +364,25 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$app->handle(Request::create('/'), HttpKernelInterface::MASTER_REQUEST, false);
}
/**
* @expectedException RuntimeException
*/
public function testNonResponseAndNonNullReturnFromRouteAfterMiddlewareShouldThrowRuntimeException()
{
$app = new Application();
$middleware = function (Request $request) {
return 'string return';
};
$app->get('/', function () {
return 'hello';
})
->after($middleware);
$app->handle(Request::create('/'), HttpKernelInterface::MASTER_REQUEST, false);
}
/**
* @expectedException RuntimeException
*/
......
......@@ -145,4 +145,14 @@ class ControllerCollectionTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(array('mid1', 'mid2', 'mid3'), $controller->getRoute()->getOption('_before_middlewares'));
}
public function testAfter()
{
$controllers = new ControllerCollection();
$controllers->after('mid1');
$controller = $controllers->match('/{id}/{name}/{extra}', function () {})->after('mid2');
$controllers->after('mid3');
$this->assertEquals(array('mid1', 'mid2', 'mid3'), $controller->getRoute()->getOption('_after_middlewares'));
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment