Skip to content

Commit

Permalink
Add standalone ControllerFactory.
Browse files Browse the repository at this point in the history
Having this logic out of the a dispatcher filter is necessary for PSR7
migration where the controller factory acts as part of the
ActionDispatcher.
  • Loading branch information
markstory committed Apr 14, 2016
1 parent 7c68802 commit 71ac75f
Show file tree
Hide file tree
Showing 6 changed files with 404 additions and 1 deletion.
3 changes: 2 additions & 1 deletion composer.json
Expand Up @@ -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"
}
},
Expand Down
94 changes: 94 additions & 0 deletions src/Http/ControllerFactory.php
@@ -0,0 +1,94 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.3.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Http;

use Cake\Core\App;
use Cake\Network\Request;
use Cake\Network\Response;
use Cake\Routing\Exception\MissingControllerException;
use Cake\Utility\Inflector;
use ReflectionClass;

/**
* Factory method for building controllers from request/response pairs.
*/
class ControllerFactory
{
/**
* Create a controller for a given request/response
*
* @param \Cake\Network\Request $request The request to build a controller for.
* @param \Cake\Network\Response $response The response to use.
* @return \Cake\Controller\Controller
*/
public function create(Request $request, Response $response)
{
$pluginPath = $controller = null;
$namespace = 'Controller';
if (isset($request->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')
]);
}
}
249 changes: 249 additions & 0 deletions tests/TestCase/Http/ControllerFactoryTest.php
@@ -0,0 +1,249 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.3.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Test\TestCase\Http;

use Cake\Core\Configure;
use Cake\Http\ControllerFactory;
use Cake\Network\Request;
use Cake\Network\Response;
use Cake\TestSuite\TestCase;

/**
* Test case for ControllerFactory.
*/
class ControllerFactoryTest extends TestCase
{
/**
* Setup
*
* @return void
*/
public function setUp()
{
parent::setUp();
Configure::write('App.namespace', 'TestApp');
$this->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);
}
}
@@ -0,0 +1,12 @@
<?php
namespace Company\TestPluginThree\Controller;

use Cake\Controller\Controller;

class OvensController extends Controller
{
public function index()
{
$this->autoRender = false;
}
}

0 comments on commit 71ac75f

Please sign in to comment.