Commit d53119eb authored by Fabien Potencier's avatar Fabien Potencier

added a route after middleware

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