Permalink
Browse files

Add Routes Middlewares

  • Loading branch information...
1 parent 8d30526 commit 6445b9e9e8be84cdb3181b0c92f62800e3159185 @DrBenton DrBenton committed Mar 10, 2012
Showing with 203 additions and 0 deletions.
  1. +57 −0 doc/usage.rst
  2. +13 −0 src/Silex/Application.php
  3. +16 −0 src/Silex/Controller.php
  4. +117 −0 tests/Silex/Tests/ApplicationTest.php
View
@@ -352,6 +352,63 @@ object that is returned by the routing methods::
It only makes sense to name routes if you use providers that make use
of the ``RouteCollection``.
+Routes middlewares
+~~~~~~~~~~~~~~~~~~
+
+You can define one or more Routes Middlewares, and link them to your routes.
+Routes Middlewares are just "PHP callables" (i.e. a Closure or a
+"ClassName::methodName" string, like others Silex callbacks), which will
+be triggered when their route is matched.
+Middlewares are fired just before the route callback,
+but after Application ``before`` filters, which have precedence
+ - see next section about these ``before`` filters.
+
+This mechanism can be used for a lot of use case - for example, a
+"anonymous/logged user" simple control::
+
+ $mustBeAnonymous = function (Request $request) use ($app) {
+ if ($app['session']->has('userId')) {
+ return $app->redirect('/user/logout');
+ }
+ };
+
+ $mustBeLogged = function (Request $request) use ($app) {
+ if (!$app['session']->has('userId')) {
+ return $app->redirect('/user/login');
+ }
+ };
+
+ $app->get('/user/subscribe', function () {
+ ...
+ })
+ ->middleware($mustBeAnonymous);
+
+ $app->get('/user/login', function () {
+ ...
+ })
+ ->middleware($mustBeAnonymous);
+
+ $app->get('/user/my-profile', function () {
+ ...
+ })
+ ->middleware($mustBeLogged);
+
+You can call the ``middleware`` function several times for a single route.
+The middlewares will be triggered in the order you added them to the route.
+
+For convenience, the routes middlewares functions are triggered with the current
+Request as their only argument.
+
+If any of the routes middlewares returns a Symfony Http Response, this response
+will short-circuit the whole rendering : the next middlewares won't run, neither
+the route callback.
+As in route callbacks, you can redirect to another page by from a route middleware
+by returning a redirect response, which you can create by calling the
+Application ``redirect`` method.
+
+A route Middleware can return a Symfony Http Response or null.
+A RuntimeException will be thrown if anything else is returned.
+
Before and after filters
------------------------
@@ -111,6 +111,18 @@ public function __construct()
return new RedirectableUrlMatcher($app['routes'], $app['request_context']);
});
+ $this['route_middlewares_trigger'] = $this->protect(function (KernelEvent $event) use ($app) {
+ foreach ($event->getRequest()->attributes->get('_middlewares', array()) as $callback) {
+ $ret = call_user_func($callback, $event->getRequest());
+ if ($ret instanceof Response) {
+ $event->setResponse($ret);
+ return;
+ } elseif (null !== $ret) {
+ throw new \RuntimeException('Middleware for route "'.$event->getRequest()->attributes->get('_route').'" returned an invalid response value. Must return null or an instance of Response.');
+ }
+ }
+ });
+
$this['request.default_locale'] = 'en';
$this['request'] = function () {
@@ -409,6 +421,7 @@ public function onKernelRequest(KernelEvent $event)
if (HttpKernelInterface::MASTER_REQUEST === $event->getRequestType()) {
$this->beforeDispatched = true;
$this['dispatcher']->dispatch(SilexEvents::BEFORE, $event);
+ $this['route_middlewares_trigger']($event);
}
}
@@ -155,6 +155,22 @@ public function requireHttps()
}
/**
+ * 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
+ * @return Controller $this The current Controller instance
+ */
+ public function middleware($callback)
+ {
+ $middlewareCallbacks = $this->route->getDefault('_middlewares');
+ $middlewareCallbacks[] = $callback;
+ $this->route->setDefault('_middlewares', $middlewareCallbacks);
+
+ return $this;
+ }
+
+ /**
* Freezes the controller.
*
* Once the controller is frozen, you can no longer change the route name
@@ -15,6 +15,9 @@
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Application test cases.
@@ -166,6 +169,120 @@ public function testHttpSpec()
$this->assertEquals('text/html; charset=ISO-8859-1', $response->headers->get('Content-Type'));
}
+ public function testRoutesMiddlewares()
+ {
+ $app = new Application();
+
+ $test = $this;
+
+ $middlewareTarget = array();
+ $middleware1 = function (Request $request) use (&$middlewareTarget, $test) {
+ $test->assertEquals('/reached', $request->getRequestUri());
+ $middlewareTarget[] = 'middleware1_triggered';
+ };
+ $middleware2 = function (Request $request) use (&$middlewareTarget, $test) {
+ $test->assertEquals('/reached', $request->getRequestUri());
+ $middlewareTarget[] = 'middleware2_triggered';
+ };
+ $middleware3 = function (Request $request) use (&$middlewareTarget, $test) {
+ throw new \Exception('This middleware shouldn\'t run!');
+ };
+
+ $app->get('/reached', function () use (&$middlewareTarget) {
+ $middlewareTarget[] = 'route_triggered';
+ return 'hello';
+ })
+ ->middleware($middleware1)
+ ->middleware($middleware2);
+
+ $app->get('/never-reached', function () use (&$middlewareTarget) {
+ throw new \Exception('This route shouldn\'t run!');
+ })
+ ->middleware($middleware3);
+
+ $result = $app->handle(Request::create('/reached'));
+
+ $this->assertSame(array('middleware1_triggered', 'middleware2_triggered', 'route_triggered'), $middlewareTarget);
+ $this->assertEquals('hello', $result->getContent());
+ }
+
+ public function testRoutesMiddlewaresWithResponseObject()
+ {
+ $app = new Application();
+
+ $app->get('/foo', function () {
+ throw new \Exception('This route shouldn\'t run!');
+ })
+ ->middleware(function () {
+ return new Response('foo');
+ });
+
+ $request = Request::create('/foo');
+ $result = $app->handle($request);
+
+ $this->assertEquals('foo', $result->getContent());
+ }
+
+ public function testRoutesMiddlewaresWithRedirectResponseObject()
+ {
+ $app = new Application();
+
+ $app->get('/foo', function () {
+ throw new \Exception('This route shouldn\'t run!');
+ })
+ ->middleware(function () use ($app) {
+ return $app->redirect('/bar');
+ });
+
+ $request = Request::create('/foo');
+ $result = $app->handle($request);
+
+ $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $result);
+ $this->assertEquals('/bar', $result->getTargetUrl());
+ }
+
+ public function testRoutesMiddlewaresTriggeredAfterSilexBeforeFilters()
+ {
+ $app = new Application();
+
+ $middlewareTarget = array();
+ $middleware = function (Request $request) use (&$middlewareTarget) {
+ $middlewareTarget[] = 'middleware_triggered';
+ };
+
+ $app->get('/foo', function () use (&$middlewareTarget) {
+ $middlewareTarget[] = 'route_triggered';
+ })
+ ->middleware($middleware);
+
+ $app->before(function () use (&$middlewareTarget) {
+ $middlewareTarget[] = 'before_triggered';
+ });
+
+ $app->handle(Request::create('/foo'));
+
+ $this->assertSame(array('before_triggered', 'middleware_triggered', 'route_triggered'), $middlewareTarget);
+ }
+
+ /**
+ * @expectedException RuntimeException
+ */
+ public function testNonResponseAndNonNullReturnFromRouteMiddlewareShouldThrowRuntimeException()
+ {
+ $app = new Application();
+
+ $middleware = function (Request $request) {
+ return 'string return';
+ };
+
+ $app->get('/', function () {
+ return 'hello';
+ })
+ ->middleware($middleware);
+
+ $app->handle(Request::create('/'), HttpKernelInterface::MASTER_REQUEST, false);
+ }
+
/**
* @expectedException RuntimeException
*/

0 comments on commit 6445b9e

Please sign in to comment.