From 71ac75fac2b29fd0e2997f0cd5ccee864d3b821d Mon Sep 17 00:00:00 2001 From: Mark Story Date: Wed, 13 Apr 2016 22:28:38 -0400 Subject: [PATCH] Add standalone ControllerFactory. Having this logic out of the a dispatcher filter is necessary for PSR7 migration where the controller factory acts as part of the ActionDispatcher. --- composer.json | 3 +- src/Http/ControllerFactory.php | 94 +++++++ tests/TestCase/Http/ControllerFactoryTest.php | 249 ++++++++++++++++++ .../src/Controller/OvensController.php | 12 + .../TestApp/Controller/CakesController.php | 39 +++ .../Controller/InterfaceController.php | 8 + 6 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 src/Http/ControllerFactory.php create mode 100644 tests/TestCase/Http/ControllerFactoryTest.php create mode 100644 tests/test_app/Plugin/Company/TestPluginThree/src/Controller/OvensController.php create mode 100644 tests/test_app/TestApp/Controller/CakesController.php create mode 100644 tests/test_app/TestApp/Controller/InterfaceController.php diff --git a/composer.json b/composer.json index a995a775df3..52c980abb9f 100644 --- a/composer.json +++ b/composer.json @@ -47,11 +47,12 @@ "autoload-dev": { "psr-4": { "Cake\\Test\\": "tests", - "Company\\TestPluginThree\\Test\\": "tests/test_app/Plugin/Company/TestPluginThree/tests", "TestApp\\": "tests/test_app/TestApp", "TestPlugin\\": "tests/test_app/Plugin/TestPlugin/src", "TestPlugin\\Test\\": "tests/test_app/Plugin/TestPlugin/tests", "TestPluginTwo\\": "tests/test_app/Plugin/TestPluginTwo/src", + "Company\\TestPluginThree\\": "tests/test_app/Plugin/Company/TestPluginThree/src", + "Company\\TestPluginThree\\Test\\": "tests/test_app/Plugin/Company/TestPluginThree/tests", "PluginJs\\": "tests/test_app/Plugin/PluginJs/src" } }, diff --git a/src/Http/ControllerFactory.php b/src/Http/ControllerFactory.php new file mode 100644 index 00000000000..3c9898f1f3e --- /dev/null +++ b/src/Http/ControllerFactory.php @@ -0,0 +1,94 @@ +params['plugin'])) { + $pluginPath = $request->params['plugin'] . '.'; + } + if (isset($request->params['controller'])) { + $controller = $request->params['controller']; + } + if (isset($request->params['prefix'])) { + if (strpos($request->params['prefix'], '/') === false) { + $namespace .= '/' . Inflector::camelize($request->params['prefix']); + } else { + $prefixes = array_map( + 'Cake\Utility\Inflector::camelize', + explode('/', $request->params['prefix']) + ); + $namespace .= '/' . implode('/', $prefixes); + } + } + $firstChar = substr($controller, 0, 1); + if (strpos($controller, '\\') !== false || + strpos($controller, '.') !== false || + $firstChar === strtolower($firstChar) + ) { + return $this->missingController($request); + } + $className = false; + if ($pluginPath . $controller) { + $className = App::classname($pluginPath . $controller, $namespace, 'Controller'); + } + if (!$className) { + return $this->missingController($request); + } + $reflection = new ReflectionClass($className); + if ($reflection->isAbstract() || $reflection->isInterface()) { + return $this->missingController($request); + } + return $reflection->newInstance($request, $response, $controller); + } + + /** + * Throws an exception when a controller is missing. + * + * @param \Cake\Network\Request $request The request. + * @throws \Cake\Routing\Exception\MissingControllerException + * @return void + */ + protected function missingController($request) + { + throw new MissingControllerException([ + 'class' => $request->param('controller'), + 'plugin' => $request->param('plugin'), + 'prefix' => $request->param('prefix'), + '_ext' => $request->param('_ext') + ]); + } +} diff --git a/tests/TestCase/Http/ControllerFactoryTest.php b/tests/TestCase/Http/ControllerFactoryTest.php new file mode 100644 index 00000000000..72dbf457b2e --- /dev/null +++ b/tests/TestCase/Http/ControllerFactoryTest.php @@ -0,0 +1,249 @@ +factory = new ControllerFactory(); + $this->response = $this->getMock('Cake\Network\Response'); + } + + /** + * Test building an application controller + * + * @return void + */ + public function testApplicationController() + { + $request = new Request([ + 'url' => 'cakes/index', + 'params' => [ + 'controller' => 'Cakes', + 'action' => 'index', + ] + ]); + $result = $this->factory->create($request, $this->response); + $this->assertInstanceOf('TestApp\Controller\CakesController', $result); + $this->assertSame($request, $result->request); + $this->assertSame($this->response, $result->response); + } + + /** + * Test building a prefixed app controller. + * + * @return void + */ + public function testPrefixedAppController() + { + $request = new Request([ + 'url' => 'admin/posts/index', + 'params' => [ + 'prefix' => 'admin', + 'controller' => 'Posts', + 'action' => 'index', + ] + ]); + $result = $this->factory->create($request, $this->response); + $this->assertInstanceOf( + 'TestApp\Controller\Admin\PostsController', + $result + ); + $this->assertSame($request, $result->request); + $this->assertSame($this->response, $result->response); + } + + /** + * Test building a nested prefix app controller + * + * @return void + */ + public function testNestedPrefixedAppController() + { + $request = new Request([ + 'url' => 'admin/sub/posts/index', + 'params' => [ + 'prefix' => 'admin/sub', + 'controller' => 'Posts', + 'action' => 'index', + ] + ]); + $result = $this->factory->create($request, $this->response); + $this->assertInstanceOf( + 'TestApp\Controller\Admin\Sub\PostsController', + $result + ); + $this->assertSame($request, $result->request); + $this->assertSame($this->response, $result->response); + } + + /** + * Test building a plugin controller + * + * @return void + */ + public function testPluginController() + { + $request = new Request([ + 'url' => 'test_plugin/test_plugin/index', + 'params' => [ + 'plugin' => 'TestPlugin', + 'controller' => 'TestPlugin', + 'action' => 'index', + ] + ]); + $result = $this->factory->create($request, $this->response); + $this->assertInstanceOf( + 'TestPlugin\Controller\TestPluginController', + $result + ); + $this->assertSame($request, $result->request); + $this->assertSame($this->response, $result->response); + } + + /** + * Test building a vendored plugin controller. + * + * @return void + */ + public function testVendorPluginController() + { + $request = new Request([ + 'url' => 'test_plugin_three/ovens/index', + 'params' => [ + 'plugin' => 'Company/TestPluginThree', + 'controller' => 'Ovens', + 'action' => 'index', + ] + ]); + $result = $this->factory->create($request, $this->response); + $this->assertInstanceOf( + 'Company\TestPluginThree\Controller\OvensController', + $result + ); + $this->assertSame($request, $result->request); + $this->assertSame($this->response, $result->response); + } + + /** + * Test building a prefixed plugin controller + * + * @return void + */ + public function testPrefixedPluginController() + { + $request = new Request([ + 'url' => 'test_plugin/admin/comments', + 'params' => [ + 'prefix' => 'admin', + 'plugin' => 'TestPlugin', + 'controller' => 'Comments', + 'action' => 'index', + ] + ]); + $result = $this->factory->create($request, $this->response); + $this->assertInstanceOf( + 'TestPlugin\Controller\Admin\CommentsController', + $result + ); + $this->assertSame($request, $result->request); + $this->assertSame($this->response, $result->response); + } + + /** + * @expectedException \Cake\Routing\Exception\MissingControllerException + * @expectedExceptionMessage Controller class Abstract could not be found. + * @return void + */ + public function testAbstractClassFailure() + { + $request = new Request([ + 'url' => 'abstract/index', + 'params' => [ + 'controller' => 'Abstract', + 'action' => 'index', + ] + ]); + $this->factory->create($request, $this->response); + } + + /** + * @expectedException \Cake\Routing\Exception\MissingControllerException + * @expectedExceptionMessage Controller class Interface could not be found. + * @return void + */ + public function testInterfaceFailure() + { + $request = new Request([ + 'url' => 'interface/index', + 'params' => [ + 'controller' => 'Interface', + 'action' => 'index', + ] + ]); + $this->factory->create($request, $this->response); + } + + /** + * @expectedException \Cake\Routing\Exception\MissingControllerException + * @expectedExceptionMessage Controller class Invisible could not be found. + * @return void + */ + public function testMissingClassFailure() + { + $request = new Request([ + 'url' => 'interface/index', + 'params' => [ + 'controller' => 'Invisible', + 'action' => 'index', + ] + ]); + $this->factory->create($request, $this->response); + } + + /** + * @expectedException \Cake\Routing\Exception\MissingControllerException + * @expectedExceptionMessage Controller class TestApp\Controller\CakesController could not be found. + * @return void + */ + public function testAbsoluteReferenceFailure() + { + $request = new Request([ + 'url' => 'interface/index', + 'params' => [ + 'controller' => 'TestApp\Controller\CakesController', + 'action' => 'index', + ] + ]); + $this->factory->create($request, $this->response); + } +} diff --git a/tests/test_app/Plugin/Company/TestPluginThree/src/Controller/OvensController.php b/tests/test_app/Plugin/Company/TestPluginThree/src/Controller/OvensController.php new file mode 100644 index 00000000000..6869d20c316 --- /dev/null +++ b/tests/test_app/Plugin/Company/TestPluginThree/src/Controller/OvensController.php @@ -0,0 +1,12 @@ +autoRender = false; + } +} diff --git a/tests/test_app/TestApp/Controller/CakesController.php b/tests/test_app/TestApp/Controller/CakesController.php new file mode 100644 index 00000000000..fbc83bf5a7c --- /dev/null +++ b/tests/test_app/TestApp/Controller/CakesController.php @@ -0,0 +1,39 @@ +response->body('Hello Jane'); + return $this->response; + } + + /** + * invalid method + * + * @return \Cake\Network\Response + */ + public function invalid() + { + return 'Some string'; + } +} diff --git a/tests/test_app/TestApp/Controller/InterfaceController.php b/tests/test_app/TestApp/Controller/InterfaceController.php new file mode 100644 index 00000000000..9f083896e35 --- /dev/null +++ b/tests/test_app/TestApp/Controller/InterfaceController.php @@ -0,0 +1,8 @@ +