Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Introducing dispatcher filters and adding tests for them

  • Loading branch information...
commit 565a58f78479f2e13990fe854280f475029ba1db 1 parent 05b88f3
José Lorenzo Rodríguez lorenzo authored
11 lib/Cake/Error/exceptions.php
View
@@ -439,6 +439,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.
*
* @package Cake.Error
43 lib/Cake/Routing/Dispatcher.php
View
@@ -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;
}
@@ -88,6 +89,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.
*
@@ -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()) {
@@ -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;
}
85 lib/Cake/Routing/DispatcherFilter.php
View
@@ -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),
Mark Story Owner

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

José Lorenzo Rodríguez Owner
lorenzo added a note

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ '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 lib/Cake/Test/Case/Routing/DispatcherTest.php
View
@@ -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'];
+ }
+
}
/**
@@ -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());
}
/**
@@ -761,6 +783,7 @@ public function testMissingControllerAbstract() {
$Dispatcher->dispatch($url, $response, array('return' => 1));
}
+
/**
* testDispatch method
*
@@ -1168,6 +1191,129 @@ public function testTestPluginDispatch() {
}
/**
+ * 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
*
* @return void
33 lib/Cake/Test/test_app/Plugin/TestPlugin/Lib/Routing/Filter/Test2DispatcherFilter.php
View
@@ -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);
+ }
+
+}
31 lib/Cake/Test/test_app/Plugin/TestPlugin/Lib/Routing/Filter/TestDispatcherFilter.php
View
@@ -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);
+ }
+
+}
Mark Story

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

José Lorenzo Rodríguez

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

Please sign in to comment.
Something went wrong with that request. Please try again.