Commit a1f29812 authored by Fabien Potencier's avatar Fabien Potencier

merged branch fabpot/middleware (PR #362)

Commits
-------

d53119eb added a route after middleware
794b3c28 renamed middleware() to before()

Discussion
----------

Implements route after middleware (closes #336)

This PR renames the `middleware` method to `before` and adds a new `after` route middleware.

These changes make Silex more consistent from a user point of view:

 * he can do something before the route callback on an application, a route collection, or just a route (by calling the `before` method on these objects);

 * he can do something after the route callback by calling the `after` method on an application, a route collection, or just a route.

---------------------------------------------------------------------------

by fabpot at 2012-06-13T18:20:37Z

I thought about using the `middleware` method for both before and after by passing the callback as an argument. But this has several drawback:

 * we loose the symmetry with the application before/after calls;
 * the developer must remember to manually call the callback (and symfony1 proves that many devs actually forgets about that);
 * this would act as a filter chain where the order of execution is not always clear when you have many filters.
parents 7dbc801c d53119eb
...@@ -3,6 +3,10 @@ Changelog ...@@ -3,6 +3,10 @@ Changelog
This changelog references all backward incompatibilities as we introduce them: This changelog references all backward incompatibilities as we introduce them:
* **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
* **2012-05-31**: Made the ``BrowserKit``, ``CssSelector``, ``DomCrawler``, * **2012-05-31**: Made the ``BrowserKit``, ``CssSelector``, ``DomCrawler``,
......
...@@ -418,8 +418,13 @@ Route middlewares ...@@ -418,8 +418,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::
...@@ -439,33 +444,36 @@ This can be used for a lot of use cases; for instance, here is a simple ...@@ -439,33 +444,36 @@ This can be used for a lot of use cases; for instance, here is a simple
$app->get('/user/subscribe', function () { $app->get('/user/subscribe', function () {
... ...
}) })
->middleware($mustBeAnonymous); ->before($mustBeAnonymous);
$app->get('/user/login', function () { $app->get('/user/login', function () {
... ...
}) })
->middleware($mustBeAnonymous); ->before($mustBeAnonymous);
$app->get('/user/my-profile', function () { $app->get('/user/my-profile', function () {
... ...
}) })
->middleware($mustBeLogged); ->before($mustBeLogged);
The ``middleware`` 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 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 middlewares returns a Symfony HTTP Response, it will 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 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 the route callback. You can also redirect to another page by returning 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 middleware does not return a Symfony HTTP Response or ``null``, a .. note::
``RuntimeException`` is thrown.
If a before middleware does not return a Symfony HTTP Response or
``null``, a ``RuntimeException`` is thrown.
Global Configuration Global Configuration
-------------------- --------------------
...@@ -480,7 +488,7 @@ middleware, a requirement, or a default value), you can configure it on ...@@ -480,7 +488,7 @@ middleware, a requirement, or a default value), you can configure it on
->requireHttps() ->requireHttps()
->method('get') ->method('get')
->convert('id', function () { // ... }) ->convert('id', function () { // ... })
->middleware(function () { // ... }) ->before(function () { // ... })
; ;
These settings are applied to already registered controllers and they become These settings are applied to already registered controllers and they become
...@@ -651,7 +659,7 @@ would secure all controllers for the backend collection:: ...@@ -651,7 +659,7 @@ would secure all controllers for the backend collection::
$backend = new ControllerCollection(); $backend = new ControllerCollection();
// ensure that all controllers require logged-in users // ensure that all controllers require logged-in users
$backend->middleware($mustBeLogged); $backend->before($mustBeLogged);
.. tip:: .. tip::
......
...@@ -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,21 +111,38 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe ...@@ -112,21 +111,38 @@ 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_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)) {
return; return;
} }
foreach ((array) $route->getOption('_middlewares') as $callback) { foreach ((array) $route->getOption('_before_middlewares') as $callback) {
$ret = call_user_func($callback, $request); $ret = call_user_func($callback, $request);
if ($ret instanceof Response) { if ($ret instanceof Response) {
$event->setResponse($ret); $event->setResponse($ret);
return; return;
} elseif (null !== $ret) { } elseif (null !== $ret) {
throw new \RuntimeException(sprintf('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_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);
} }
......
...@@ -157,15 +157,28 @@ class Controller ...@@ -157,15 +157,28 @@ class Controller
/** /**
* Sets a callback to handle before triggering the route callback. * Sets a callback to handle before triggering the route callback.
* (a.k.a. "Route Middleware")
* *
* @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 Controller $this The current Controller instance * @return Controller $this The current Controller instance
*/ */
public function middleware($callback) public function before($callback)
{ {
$this->route->middleware($callback); $this->route->before($callback);
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; return $this;
} }
......
...@@ -219,18 +219,35 @@ class ControllerCollection ...@@ -219,18 +219,35 @@ class ControllerCollection
/** /**
* Sets a callback to handle before triggering the route callback. * Sets a callback to handle before triggering the route callback.
* (a.k.a. "Route Middleware")
* *
* @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)
{
$this->defaultRoute->before($callback);
foreach ($this->controllers as $controller) {
$controller->before($callback);
}
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 middleware($callback) public function after($callback)
{ {
$this->defaultRoute->middleware($callback); $this->defaultRoute->after($callback);
foreach ($this->controllers as $controller) { foreach ($this->controllers as $controller) {
$controller->middleware($callback); $controller->after($callback);
} }
return $this; return $this;
......
...@@ -107,17 +107,32 @@ class Route extends BaseRoute ...@@ -107,17 +107,32 @@ class Route extends BaseRoute
/** /**
* Sets a callback to handle before triggering the route callback. * Sets a callback to handle before triggering the route callback.
* (a.k.a. "Route Middleware")
* *
* @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 Controller $this The current Controller instance * @return Controller $this The current Controller instance
*/ */
public function middleware($callback) public function before($callback)
{ {
$middlewareCallbacks = $this->getOption('_middlewares'); $callbacks = $this->getOption('_before_middlewares');
$middlewareCallbacks[] = $callback; $callbacks[] = $callback;
$this->setOption('_middlewares', $middlewareCallbacks); $this->setOption('_before_middlewares', $callbacks);
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; return $this;
} }
......
...@@ -174,15 +174,27 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase ...@@ -174,15 +174,27 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$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,28 +203,31 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase ...@@ -191,28 +203,31 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
return 'hello'; return 'hello';
}) })
->middleware($middleware1) ->before($beforeMiddleware1)
->middleware($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!');
}) })
->middleware($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());
} }
public function testRoutesMiddlewaresWithResponseObject() public function testRoutesBeforeMiddlewaresWithResponseObject()
{ {
$app = new Application(); $app = new Application();
$app->get('/foo', function () { $app->get('/foo', function () {
throw new \Exception('This route shouldn\'t run!'); throw new \Exception('This route shouldn\'t run!');
}) })
->middleware(function () { ->before(function () {
return new Response('foo'); return new Response('foo');
}); });
...@@ -222,14 +237,31 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase ...@@ -222,14 +237,31 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('foo', $result->getContent()); $this->assertEquals('foo', $result->getContent());
} }
public function testRoutesMiddlewaresWithRedirectResponseObject() 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(); $app = new Application();
$app->get('/foo', function () { $app->get('/foo', function () {
throw new \Exception('This route shouldn\'t run!'); throw new \Exception('This route shouldn\'t run!');
}) })
->middleware(function () use ($app) { ->before(function () use ($app) {
return $app->redirect('/bar'); return $app->redirect('/bar');
}); });
...@@ -240,7 +272,7 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase ...@@ -240,7 +272,7 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('/bar', $result->getTargetUrl()); $this->assertEquals('/bar', $result->getTargetUrl());
} }
public function testRoutesMiddlewaresTriggeredAfterSilexBeforeFilters() public function testRoutesBeforeMiddlewaresTriggeredAfterSilexBeforeFilters()
{ {
$app = new Application(); $app = new Application();
...@@ -252,7 +284,7 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase ...@@ -252,7 +284,7 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$app->get('/foo', function () use (&$middlewareTarget) { $app->get('/foo', function () use (&$middlewareTarget) {
$middlewareTarget[] = 'route_triggered'; $middlewareTarget[] = 'route_triggered';
}) })
->middleware($middleware); ->before($middleware);
$app->before(function () use (&$middlewareTarget) { $app->before(function () use (&$middlewareTarget) {
$middlewareTarget[] = 'before_triggered'; $middlewareTarget[] = 'before_triggered';
...@@ -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();
...@@ -293,7 +348,26 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase ...@@ -293,7 +348,26 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
/** /**
* @expectedException RuntimeException * @expectedException RuntimeException
*/ */
public function testNonResponseAndNonNullReturnFromRouteMiddlewareShouldThrowRuntimeException() public function testNonResponseAndNonNullReturnFromRouteBeforeMiddlewareShouldThrowRuntimeException()
{
$app = new Application();
$middleware = function (Request $request) {
return 'string return';
};
$app->get('/', function () {
return 'hello';
})
->before($middleware);
$app->handle(Request::create('/'), HttpKernelInterface::MASTER_REQUEST, false);
}
/**
* @expectedException RuntimeException
*/
public function testNonResponseAndNonNullReturnFromRouteAfterMiddlewareShouldThrowRuntimeException()
{ {
$app = new Application(); $app = new Application();
...@@ -304,7 +378,7 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase ...@@ -304,7 +378,7 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase
$app->get('/', function () { $app->get('/', function () {
return 'hello'; return 'hello';
}) })
->middleware($middleware); ->after($middleware);
$app->handle(Request::create('/'), HttpKernelInterface::MASTER_REQUEST, false); $app->handle(Request::create('/'), HttpKernelInterface::MASTER_REQUEST, false);
} }
......
...@@ -136,13 +136,23 @@ class ControllerCollectionTest extends \PHPUnit_Framework_TestCase ...@@ -136,13 +136,23 @@ class ControllerCollectionTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('http', $controller->getRoute()->getRequirement('_scheme')); $this->assertEquals('http', $controller->getRoute()->getRequirement('_scheme'));
} }
public function testMiddleware() public function testBefore()
{ {
$controllers = new ControllerCollection(); $controllers = new ControllerCollection();
$controllers->middleware('mid1'); $controllers->before('mid1');
$controller = $controllers->match('/{id}/{name}/{extra}', function () {})->middleware('mid2'); $controller = $controllers->match('/{id}/{name}/{extra}', function () {})->before('mid2');
$controllers->middleware('mid3'); $controllers->before('mid3');
$this->assertEquals(array('mid1', 'mid2', 'mid3'), $controller->getRoute()->getOption('_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