Commit 6445b9e9 authored by DrBenton's avatar DrBenton

Add Routes Middlewares

parent 8d30526d
...@@ -352,6 +352,63 @@ object that is returned by the routing methods:: ...@@ -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 It only makes sense to name routes if you use providers that make use
of the ``RouteCollection``. 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 Before and after filters
------------------------ ------------------------
......
...@@ -111,6 +111,18 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe ...@@ -111,6 +111,18 @@ 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) {
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.default_locale'] = 'en';
$this['request'] = function () { $this['request'] = function () {
...@@ -409,6 +421,7 @@ class Application extends \Pimple implements HttpKernelInterface, EventSubscribe ...@@ -409,6 +421,7 @@ 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);
} }
} }
......
...@@ -154,6 +154,22 @@ class Controller ...@@ -154,6 +154,22 @@ class Controller
return $this; return $this;
} }
/**
* 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. * Freezes the controller.
* *
......
...@@ -15,6 +15,9 @@ use Silex\Application; ...@@ -15,6 +15,9 @@ use Silex\Application;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/** /**
* Application test cases. * Application test cases.
...@@ -166,6 +169,120 @@ class ApplicationTest extends \PHPUnit_Framework_TestCase ...@@ -166,6 +169,120 @@ 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 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 * @expectedException RuntimeException
*/ */
......
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