From 847b7eeb022a97c3411334cc6e1ae5e8f7af6e62 Mon Sep 17 00:00:00 2001 From: RJ Garcia Date: Sun, 8 May 2016 02:06:14 -0700 Subject: [PATCH] Allowing callables for mount - Added the ability to pass callables into the controller collection mount method. - Added a controllersFactory parameter into the controller collection which is a callable that is used to create a new controller for the mount method. - Updated documentation to show usage of new callables in mount and recursive mounting. - Added appropriate tests Signed-off-by: RJ Garcia --- doc/organizing_controllers.rst | 13 ++++- src/Silex/Application.php | 8 +-- src/Silex/ControllerCollection.php | 20 ++++++-- src/Silex/Provider/RoutingServiceProvider.php | 7 +-- tests/Silex/Tests/ApplicationTest.php | 14 ++++- .../Silex/Tests/ControllerCollectionTest.php | 51 +++++++++++++++++++ .../Provider/RoutingServiceProviderTest.php | 12 +++++ 7 files changed, 112 insertions(+), 13 deletions(-) diff --git a/doc/organizing_controllers.rst b/doc/organizing_controllers.rst index 5b3878bbf..55c7f0465 100644 --- a/doc/organizing_controllers.rst +++ b/doc/organizing_controllers.rst @@ -25,6 +25,16 @@ group them logically:: $app->mount('/blog', $blog); $app->mount('/forum', $forum); + // define controllers for a admin + $app->mount('/admin', function ($api) { + // recursively mount + $api->mount('/blog', function ($user) { + $user->get('/', function () { + return 'Admin Blog home page'; + }); + }); + }); + .. note:: ``$app['controllers_factory']`` is a factory that returns a new instance @@ -32,7 +42,8 @@ group them logically:: ``mount()`` prefixes all routes with the given prefix and merges them into the main Application. So, ``/`` will map to the main home page, ``/blog/`` to the -blog home page, and ``/forum/`` to the forum home page. +blog home page, ``/forum/`` to the forum home page, and ``/admin/blog/`` to the +admin blog home page. .. caution:: diff --git a/src/Silex/Application.php b/src/Silex/Application.php index 6538941d7..d0418b71f 100644 --- a/src/Silex/Application.php +++ b/src/Silex/Application.php @@ -437,8 +437,8 @@ public function sendFile($file, $status = 200, array $headers = array(), $conten /** * Mounts controllers under the given route prefix. * - * @param string $prefix The route prefix - * @param ControllerCollection|ControllerProviderInterface $controllers A ControllerCollection or a ControllerProviderInterface instance + * @param string $prefix The route prefix + * @param ControllerCollection|callable|ControllerProviderInterface $controllers A ControllerCollection, a callable, or a ControllerProviderInterface instance * * @return Application * @@ -454,8 +454,8 @@ public function mount($prefix, $controllers) } $controllers = $connectedControllers; - } elseif (!$controllers instanceof ControllerCollection) { - throw new \LogicException('The "mount" method takes either a "ControllerCollection" or a "ControllerProviderInterface" instance.'); + } elseif (!$controllers instanceof ControllerCollection && !is_callable($controllers)) { + throw new \LogicException('The "mount" method takes either a "ControllerCollection" instance, "ControllerProviderInterface" instance, or a callable.'); } $this['controllers']->mount($prefix, $controllers); diff --git a/src/Silex/ControllerCollection.php b/src/Silex/ControllerCollection.php index a53a61179..403689649 100644 --- a/src/Silex/ControllerCollection.php +++ b/src/Silex/ControllerCollection.php @@ -44,11 +44,13 @@ class ControllerCollection protected $defaultController; protected $prefix; protected $routesFactory; + protected $controllersFactory; - public function __construct(Route $defaultRoute, $routesFactory = null) + public function __construct(Route $defaultRoute, RouteCollection $routesFactory = null, $controllersFactory = null) { $this->defaultRoute = $defaultRoute; $this->routesFactory = $routesFactory; + $this->controllersFactory = $controllersFactory; $this->defaultController = function (Request $request) { throw new \LogicException(sprintf('The "%s" route must have code to run when it matches.', $request->attributes->get('_route'))); }; @@ -57,11 +59,21 @@ public function __construct(Route $defaultRoute, $routesFactory = null) /** * Mounts controllers under the given route prefix. * - * @param string $prefix The route prefix - * @param ControllerCollection $controllers A ControllerCollection instance + * @param string $prefix The route prefix + * @param ControllerCollection|callable $controllers A ControllerCollection instance or a callable for defining routes + * + * @throws \LogicException */ - public function mount($prefix, ControllerCollection $controllers) + public function mount($prefix, $controllers) { + if (is_callable($controllers)) { + $collection = $this->controllersFactory ? call_user_func($this->controllersFactory) : new static(new Route(), new RouteCollection()); + call_user_func($controllers, $collection); + $controllers = $collection; + } elseif (!$controllers instanceof self) { + throw new \LogicException('The "mount" method takes either a "ControllerCollection" instance or callable.'); + } + $controllers->prefix = $prefix; $this->controllers[] = $controllers; diff --git a/src/Silex/Provider/RoutingServiceProvider.php b/src/Silex/Provider/RoutingServiceProvider.php index 57df482c7..d040ba0d6 100644 --- a/src/Silex/Provider/RoutingServiceProvider.php +++ b/src/Silex/Provider/RoutingServiceProvider.php @@ -66,9 +66,10 @@ public function register(Container $app) return $app['controllers_factory']; }; - $app['controllers_factory'] = $app->factory(function ($app) { - return new ControllerCollection($app['route_factory'], $app['routes_factory']); - }); + $controllers_factory = function () use ($app, &$controllers_factory) { + return new ControllerCollection($app['route_factory'], $app['routes_factory'], $controllers_factory); + }; + $app['controllers_factory'] = $app->factory($controllers_factory); $app['routing.listener'] = function ($app) { $urlMatcher = new LazyRequestMatcher(function () use ($app) { diff --git a/tests/Silex/Tests/ApplicationTest.php b/tests/Silex/Tests/ApplicationTest.php index d9799213b..de4289aa6 100644 --- a/tests/Silex/Tests/ApplicationTest.php +++ b/tests/Silex/Tests/ApplicationTest.php @@ -464,7 +464,7 @@ public function testMountPreservesOrder() /** * @expectedException \LogicException - * @expectedExceptionMessage The "mount" method takes either a "ControllerCollection" or a "ControllerProviderInterface" instance. + * @expectedExceptionMessage The "mount" method takes either a "ControllerCollection" instance, "ControllerProviderInterface" instance, or a callable. */ public function testMountNullException() { @@ -482,6 +482,18 @@ public function testMountWrongConnectReturnValueException() $app->mount('/exception', new IncorrectControllerCollection()); } + public function testMountCallable() + { + $app = new Application(); + $app->mount('/prefix', function (ControllerCollection $coll) { + $coll->get('/path'); + }); + + $app->flush(); + + $this->assertEquals(1, $app['routes']->count()); + } + public function testSendFile() { $app = new Application(); diff --git a/tests/Silex/Tests/ControllerCollectionTest.php b/tests/Silex/Tests/ControllerCollectionTest.php index 6e486bc28..d5c9889c6 100644 --- a/tests/Silex/Tests/ControllerCollectionTest.php +++ b/tests/Silex/Tests/ControllerCollectionTest.php @@ -127,6 +127,57 @@ public function testUniqueGeneratedRouteNamesAmongNestedMounts() $this->assertEquals(array('_root_a_tree_leaf', '_root_a_tree_leaf_1'), array_keys($routes->all())); } + public function testMountCallable() + { + $controllers = new ControllerCollection(new Route()); + $controllers->mount('/prefix', function (ControllerCollection $coll) { + $coll->mount('/path', function ($coll) { + $coll->get('/part'); + }); + }); + + $routes = $controllers->flush(); + $this->assertEquals('/prefix/path/part', current($routes->all())->getPath()); + } + + public function testMountCallableProperClone() + { + $controllers = new ControllerCollection(new Route(), new RouteCollection()); + $controllers->get('/'); + + $subControllers = null; + $controllers->mount('/prefix', function (ControllerCollection $coll) use (&$subControllers) { + $subControllers = $coll; + $coll->get('/'); + }); + + $routes = $controllers->flush(); + $subRoutes = $subControllers->flush(); + $this->assertTrue($routes->count() == 2 && $subRoutes->count() == 0); + } + + public function testMountControllersFactory() + { + $testControllers = new ControllerCollection(new Route()); + $controllers = new ControllerCollection(new Route(), null, function () use ($testControllers) { + return $testControllers; + }); + + $controllers->mount('/prefix', function ($mounted) use ($testControllers) { + $this->assertSame($mounted, $testControllers); + }); + } + + /** + * @expectedException \LogicException + * @expectedExceptionMessage The "mount" method takes either a "ControllerCollection" instance or callable. + */ + public function testMountCallableException() + { + $controllers = new ControllerCollection(new Route()); + $controllers->mount('/prefix', ''); + } + public function testAssert() { $controllers = new ControllerCollection(new Route()); diff --git a/tests/Silex/Tests/Provider/RoutingServiceProviderTest.php b/tests/Silex/Tests/Provider/RoutingServiceProviderTest.php index 5dba6ac45..da2ca78cb 100644 --- a/tests/Silex/Tests/Provider/RoutingServiceProviderTest.php +++ b/tests/Silex/Tests/Provider/RoutingServiceProviderTest.php @@ -11,7 +11,9 @@ namespace Silex\Tests\Provider; +use Pimple\Container; use Silex\Application; +use Silex\Provider\RoutingServiceProvider; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -106,4 +108,14 @@ public function testUrlGenerationWithHttps() $this->assertEquals('https://localhost/secure', $response->getContent()); } + + public function testControllersFactory() + { + $app = new Container(); + $app->register(new RoutingServiceProvider()); + $coll = $app['controllers_factory']; + $coll->mount('/blog', function ($blog) { + $this->assertInstanceOf('Silex\ControllerCollection', $blog); + }); + } }