Introducing dispatcher filters and adding tests for them

This commit is contained in:
Jose Lorenzo Rodriguez 2012-04-16 00:36:08 -04:30
parent 05b88f3f0e
commit 565a58f784
6 changed files with 347 additions and 2 deletions

View file

@ -438,6 +438,17 @@ class MissingPluginException extends CakeException {
}
/**
* Exception raised when a Dispatcher filter could not be found
*
* @package Cake.Error
*/
class MissingDispatcherFilterException extends CakeException {
protected $_messageTemplate = 'Dispatcher filter %s could not be found.';
}
/**
* Exception class for AclComponent and Interface implementations.
*

View file

@ -68,6 +68,7 @@ class Dispatcher implements CakeEventListener {
if (!$this->_eventManager) {
$this->_eventManager = new CakeEventManager();
$this->_eventManager->attach($this);
$this->_attachFilters($this->_eventManager);
}
return $this->_eventManager;
}
@ -87,6 +88,41 @@ class Dispatcher implements CakeEventListener {
);
}
/**
* Attaches all event listeners for this dispatcher instance. Loads the
* dispatcher filters from the configured locations.
*
* @param CakeEventManager $manager
* @return void
**/
protected function _attachFilters($manager) {
$filters = Configure::read('Dispatcher.filters');
if (empty($filters)) {
return;
}
foreach ($filters as $filter) {
if (is_string($filter)) {
$filter = array('callable' => $filter);
}
if (is_string($filter['callable'])) {
list($plugin, $callable) = pluginSplit($filter['callable'], true);
App::uses($callable, $plugin . 'Routing/Filter');
if (!class_exists($callable)) {
throw new MissingDispatcherFilterException($callable);
}
$manager->attach(new $callable);
} else {
$on = strtolower($filter['on']);
$options = array();
if (isset($filter['priority'])) {
$options['priority'] = $filter['priority'];
}
$manager->attach($filter['callable'], 'Dispatcher.' . $on, $options);
}
}
}
/**
* Dispatches and invokes given Request, handing over control to the involved controller. If the controller is set
* to autoRender, via Controller::$autoRender, then Dispatcher will render the view.
@ -102,7 +138,7 @@ class Dispatcher implements CakeEventListener {
* @param CakeRequest $request Request object to dispatch.
* @param CakeResponse $response Response object to put the results of the dispatch into.
* @param array $additionalParams Settings array ("bare", "return") which is melded with the GET and POST params
* @return boolean Success
* @return string|void if `$request['return']` is set then it returns response body, null otherwise
* @throws MissingControllerException When the controller is missing.
*/
public function dispatch(CakeRequest $request, CakeResponse $response, $additionalParams = array()) {
@ -111,7 +147,10 @@ class Dispatcher implements CakeEventListener {
$request = $beforeEvent->data['request'];
if ($beforeEvent->result instanceof CakeResponse) {
$beforeEvent->result->send();
if (isset($request->params['return'])) {
return $response->body();
}
$response->send();
return;
}

View file

@ -0,0 +1,85 @@
<?php
/**
*
* PHP 5
*
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @package Cake.Routing
* @since CakePHP(tm) v 2.2
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
App::uses('CakeEventListener', 'Event');
/**
* This abstract class represents a filter to be applied to a dispatcher cycle. It acts as as
* event listener with the ability to alter the request or response as needed before it is handled
* by a controller or after the response body has already been built.
*
* @package Cake.Event
*/
abstract class DispatcherFilter implements CakeEventListener {
/**
* Default priority for all methods in this filter
*
* @var int
**/
public $priority = 10;
/**
* Returns the list of events this filter listens to.
* Dispatcher notifies 2 different events `Dispatcher.before` and `Dispatcher.after`.
* By default this class will attach `preDispatch` and `postDispatch` method respectively.
*
* Override this method at will to only listen to the events you are interested in.
*
* @return array
**/
public function implementedEvents() {
return array(
'Dispatcher.before' => array('callable' => 'preDispatch', 'priority' => $this->priority),
'Dispatcher.after' => array('callable' => 'postDispatch', 'priority' => $this->priority),
);
}
/**
* Method called before the controller is instantiated and called to ser a request.
* If used with default priority, it will be called after the Router has parsed the
* url and set the routing params into the request object.
*
* If a CakeResponse object instance is returned, it will be served at the end of the
* event cycle, not calling any controller as a result. This will also have the effect of
* not calling the after event in the dispatcher.
*
* If false is returned, the event will be stopped and no more listeners will be notified.
* Alternatively you can call `$event->stopPropagation()` to acheive the same result.
*
* @param CakeEvent $event container object having the `request`, `response` and `additionalParams`
* keys in the data property.
* @return CakeResponse|boolean
**/
public function preDispatch($event) {
}
/**
* Method called after the controller served a request and generated a response.
* It is posible to alter the response object at this point as it is not sent to the
* client yet.
*
* If false is returned, the event will be stopped and no more listeners will be notified.
* Alternatively you can call `$event->stopPropagation()` to acheive the same result.
*
* @param CakeEvent $event container object having the `request` and `response`
* keys in the data property.
* @return mixed boolean to stop the event dispatching or null to continue
**/
public function postDispatch($event) {}
}

View file

@ -63,6 +63,27 @@ class TestDispatcher extends Dispatcher {
return parent::_invoke($controller, $request, $response);
}
/**
* Helper function to test single method attaching for dispatcher filters
*
* @param CakeEvent
* @return void
**/
public function filterTest($event) {
$event->data['request']->params['eventName'] = $event->name();
}
/**
* Helper function to test single method attaching for dispatcher filters
*
* @param CakeEvent
* @return void
**/
public function filterTest2($event) {
$event->stopPropagation();
return $event->data['response'];
}
}
/**
@ -563,6 +584,7 @@ class DispatcherTest extends CakeTestCase {
Configure::write('App', $this->_app);
Configure::write('Cache', $this->_cache);
Configure::write('debug', $this->_debug);
Configure::write('Dispatcher.filters', array());
}
/**
@ -761,6 +783,7 @@ class DispatcherTest extends CakeTestCase {
$Dispatcher->dispatch($url, $response, array('return' => 1));
}
/**
* testDispatch method
*
@ -1167,6 +1190,129 @@ class DispatcherTest extends CakeTestCase {
App::build();
}
/**
* Tests that it is possible to attach filter classes to the dispatch cycle
*
* @return void
**/
public function testDispatcherFilterSuscriber() {
App::build(array(
'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS),
'Plugin' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS)
), App::RESET);
CakePlugin::load('TestPlugin');
Configure::write('Dispatcher.filters', array(
array('callable' => 'TestPlugin.TestDispatcherFilter')
));
$dispatcher = new TestDispatcher();
$request = new CakeRequest('/');
$request->params['altered'] = false;
$response = $this->getMock('CakeResponse', array('send'));
$dispatcher->dispatch($request, $response);
$this->assertTrue($request->params['altered']);
$this->assertEquals(304, $response->statusCode());
Configure::write('Dispatcher.filters', array(
'TestPlugin.Test2DispatcherFilter',
'TestPlugin.TestDispatcherFilter'
));
$dispatcher = new TestDispatcher();
$request = new CakeRequest('/');
$request->params['altered'] = false;
$response = $this->getMock('CakeResponse', array('send'));
$dispatcher->dispatch($request, $response);
$this->assertFalse($request->params['altered']);
$this->assertEquals(500, $response->statusCode());
$this->assertNull($dispatcher->controller);
}
/**
* Tests that attaching an inexistent class as filter will throw an exception
*
* @expectedException MissingDispatcherFilterException
* @return void
**/
public function testDispatcherFilterSuscriberMissing() {
App::build(array(
'Plugin' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'Plugin' . DS)
), App::RESET);
CakePlugin::load('TestPlugin');
Configure::write('Dispatcher.filters', array(
array('callable' => 'TestPlugin.NotAFilter')
));
$dispatcher = new TestDispatcher();
$request = new CakeRequest('/');
$response = $this->getMock('CakeResponse', array('send'));
$dispatcher->dispatch($request, $response);
}
/**
* Tests it is possible to attach single callables as filters
*
* @return void
**/
public function testDispatcherFilterCallable() {
App::build(array(
'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS)
), App::RESET);
$dispatcher = new TestDispatcher();
Configure::write('Dispatcher.filters', array(
array('callable' => array($dispatcher, 'filterTest'), 'on' => 'before')
));
$request = new CakeRequest('/');
$response = $this->getMock('CakeResponse', array('send'));
$dispatcher->dispatch($request, $response);
$this->assertEquals('Dispatcher.before', $request->params['eventName']);
$dispatcher = new TestDispatcher();
Configure::write('Dispatcher.filters', array(
array('callable' => array($dispatcher, 'filterTest'), 'on' => 'after')
));
$request = new CakeRequest('/');
$response = $this->getMock('CakeResponse', array('send'));
$dispatcher->dispatch($request, $response);
$this->assertEquals('Dispatcher.after', $request->params['eventName']);
// Test that it is possible to skip the route connection process
$dispatcher = new TestDispatcher();
Configure::write('Dispatcher.filters', array(
array('callable' => array($dispatcher, 'filterTest2'), 'on' => 'before', 'priority' => 1)
));
$request = new CakeRequest('/');
$response = $this->getMock('CakeResponse', array('send'));
$dispatcher->dispatch($request, $response);
$this->assertEmpty($dispatcher->controller);
$this->assertEquals(array('controller' => null, 'action' => null, 'plugin' => null), $request->params);
$dispatcher = new TestDispatcher();
Configure::write('Dispatcher.filters', array(
array('callable' => array($dispatcher, 'filterTest2'), 'on' => 'before', 'priority' => 1)
));
$request = new CakeRequest('/');
$request->params['return'] = true;
$response = $this->getMock('CakeResponse', array('send'));
$response->body('this is a body');
$result = $dispatcher->dispatch($request, $response);
$this->assertEquals('this is a body', $result);
$request = new CakeRequest('/');
$response = $this->getMock('CakeResponse', array('send'));
$response->expects($this->once())->method('send');
$response->body('this is a body');
$result = $dispatcher->dispatch($request, $response);
$this->assertNull($result);
}
/**
* testChangingParamsFromBeforeFilter method
*

View file

@ -0,0 +1,33 @@
<?php
/**
*
* PHP 5
*
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @package Cake.Test.test_app.Routing.Filter
* @since CakePHP(tm) v 2.2
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
App::uses('DispatcherFilter', 'Routing');
class Test2DispatcherFilter extends DispatcherFilter {
public function preDispatch($event) {
$event->data['response']->statusCode(500);
$event->stopPropagation();
return $event->data['response'];
}
public function postDispatch($event) {
$event->data['response']->statusCode(200);
}
}

View file

@ -0,0 +1,31 @@
<?php
/**
*
* PHP 5
*
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @package Cake.Test.test_app.Routing.Filter
* @since CakePHP(tm) v 2.2
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
App::uses('DispatcherFilter', 'Routing');
class TestDispatcherFilter extends DispatcherFilter {
public function preDispatch($event) {
$event->data['request']->params['altered'] = true;
}
public function postDispatch($event) {
$event->data['response']->statusCode(304);
}
}