Skip to content

Commit

Permalink
Introducing dispatcher filters and adding tests for them
Browse files Browse the repository at this point in the history
  • Loading branch information
lorenzo committed Apr 16, 2012
1 parent 05b88f3 commit 565a58f
Show file tree
Hide file tree
Showing 6 changed files with 347 additions and 2 deletions.
11 changes: 11 additions & 0 deletions lib/Cake/Error/exceptions.php
Expand Up @@ -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.
*
Expand Down
43 changes: 41 additions & 2 deletions lib/Cake/Routing/Dispatcher.php
Expand Up @@ -68,6 +68,7 @@ public function getEventManager() {
if (!$this->_eventManager) {
$this->_eventManager = new CakeEventManager();
$this->_eventManager->attach($this);
$this->_attachFilters($this->_eventManager);
}
return $this->_eventManager;
}
Expand All @@ -87,6 +88,41 @@ public function implementedEvents() {
);
}

/**
* 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.
Expand All @@ -102,7 +138,7 @@ public function implementedEvents() {
* @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()) {
Expand All @@ -111,7 +147,10 @@ public function dispatch(CakeRequest $request, CakeResponse $response, $addition

$request = $beforeEvent->data['request'];
if ($beforeEvent->result instanceof CakeResponse) {
$beforeEvent->result->send();
if (isset($request->params['return'])) {
return $response->body();
}
$response->send();
return;
}

Expand Down
85 changes: 85 additions & 0 deletions lib/Cake/Routing/DispatcherFilter.php
@@ -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),

This comment has been minimized.

Copy link
@markstory

markstory Apr 16, 2012

Member

Why not beforeDispatch and afterDispatch? This would match the naming conventions elsewhere in the framework.

This comment has been minimized.

Copy link
@lorenzo

lorenzo Apr 16, 2012

Author Member

Yeah, why not? :) seems like a better choice. Will change it too in the DispatcherFilter callbacks

'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) {}
}
146 changes: 146 additions & 0 deletions lib/Cake/Test/Case/Routing/DispatcherTest.php
Expand Up @@ -63,6 +63,27 @@ protected function _invoke(Controller $controller, CakeRequest $request, CakeRes
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'];
}

}

/**
Expand Down Expand Up @@ -563,6 +584,7 @@ public function tearDown() {
Configure::write('App', $this->_app);
Configure::write('Cache', $this->_cache);
Configure::write('debug', $this->_debug);
Configure::write('Dispatcher.filters', array());
}

/**
Expand Down Expand Up @@ -761,6 +783,7 @@ public function testMissingControllerAbstract() {
$Dispatcher->dispatch($url, $response, array('return' => 1));
}


/**
* testDispatch method
*
Expand Down Expand Up @@ -1167,6 +1190,129 @@ public function testTestPluginDispatch() {
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
*
Expand Down
@@ -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);
}

}

0 comments on commit 565a58f

Please sign in to comment.