Extracting asset dispatcher and cache dispatcher into separate classes to provide examples on how to use Dispatcher

Filters
This commit is contained in:
Jose Lorenzo Rodriguez 2012-04-17 00:42:18 -04:30
parent 70f3cc579c
commit 826699a670
9 changed files with 360 additions and 194 deletions

View file

@ -122,3 +122,25 @@ Cache::config('default', array('engine' => 'File'));
* CakePlugin::load('DebugKit'); //Loads a single plugin named DebugKit
*
*/
/**
* You can attach event listeners to the request lifecyle as Dispatcher Filter . By Default CakePHP bundles two filters:
*
* - AssetDispatcher filter will serve your asset files (css, images, js, etc) from your themes and plugins
* - CacheDispatcher filter will read the Cache.check configure variable and try to serve cached content generated from controllers
*
* Feel free to remove or add filters as you see fit for your application. A few examples:
*
* Configure::write('Dispatcher.filters', array(
* 'MyCacheFilter', // will use MyCacheFilter class from the Routing/Filter package in your app.
* 'MyPlugin.MyFilter', // will use MyFilter class from the Routing/Filter package in MyPlugin plugin.
* array('callbale' => $aFunction, 'on' => 'before', 'priority' => 9), // A valid PHP callback type to be called on beforeDispatch
* array('callbale' => $anotherMethod, 'on' => 'after'), // A valid PHP callback type to be called on afterDispatch
*
* ));
*/
Configure::write('Dispatcher.filters', array(
'AssetDispatcher',
'CacheDispatcher'
));

View file

@ -63,3 +63,24 @@ Cache::config('default', array('engine' => 'File'));
* CakePlugin::load('DebugKit'); //Loads a single plugin named DebugKit
*
*/
/**
* You can attach event listeners to the request lifecyle as Dispatcher Filter . By Default CakePHP bundles two filters:
*
* - AssetDispatcher filter will serve your asset files (css, images, js, etc) from your themes and plugins
* - CacheDispatcher filter will read the Cache.check configure variable and try to serve cached content generated from controllers
*
* Feel free to remove or add filters as you see fit for your application. A few examples:
*
* Configure::write('Dispatcher.filters', array(
* 'MyCacheFilter', // will use MyCacheFilter class from the Routing/Filter package in your app.
* 'MyPlugin.MyFilter', // will use MyFilter class from the Routing/Filter package in MyPlugin plugin.
* array('callbale' => $aFunction, 'on' => 'before', 'priority' => 9), // A valid PHP callback type to be called on beforeDispatch
* array('callbale' => $anotherMethod, 'on' => 'after'), // A valid PHP callback type to be called on afterDispatch
*
* ));
*/
Configure::write('Dispatcher.filters', array(
'AssetDispatcher',
'CacheDispatcher'
));

View file

@ -79,13 +79,7 @@ class Dispatcher implements CakeEventListener {
* @return array
**/
public function implementedEvents() {
return array(
'Dispatcher.beforeDispatch' => array(
array('callable' => array($this, 'asset')),
array('callable' => array($this, 'cached')),
array('callable' => array($this, 'parseParams')),
)
);
return array('Dispatcher.beforeDispatch' => 'parseParams');
}
/**
@ -283,141 +277,4 @@ class Dispatcher implements CakeEventListener {
include APP . 'Config' . DS . 'routes.php';
}
/**
* Checks whether the response was cached and set the body accordingly.
*
* @param CakeEvent $event containing the request and response object
* @return CakeResponse with cached content if found, null otherwise
*/
public function cached($event) {
$path = $event->data['request']->here();
if (Configure::read('Cache.check') === true) {
if ($path == '/') {
$path = 'home';
}
$path = strtolower(Inflector::slug($path));
$filename = CACHE . 'views' . DS . $path . '.php';
if (!file_exists($filename)) {
$filename = CACHE . 'views' . DS . $path . '_index.php';
}
if (file_exists($filename)) {
$controller = null;
$view = new View($controller);
$result = $view->renderCache($filename, microtime(true));
if ($result !== false) {
$event->data['response']->body($result);
return $event->data['response'];
}
}
}
}
/**
* Checks if a requested asset exists and sends it to the browser
*
* @param CakeEvent $event containing the request and response object
* @return CakeResponse if the client is requesting a recognized asset, null otherwise
*/
public function asset($event) {
$url = $event->data['request']->url;
$response = $event->data['response'];
if (strpos($url, '..') !== false || strpos($url, '.') === false) {
return;
}
$filters = Configure::read('Asset.filter');
$isCss = (
strpos($url, 'ccss/') === 0 ||
preg_match('#^(theme/([^/]+)/ccss/)|(([^/]+)(?<!css)/ccss)/#i', $url)
);
$isJs = (
strpos($url, 'cjs/') === 0 ||
preg_match('#^/((theme/[^/]+)/cjs/)|(([^/]+)(?<!js)/cjs)/#i', $url)
);
if (($isCss && empty($filters['css'])) || ($isJs && empty($filters['js']))) {
$response->statusCode(404);
$event->stopPropagation();
return $response;
} elseif ($isCss) {
include WWW_ROOT . DS . $filters['css'];
$event->stopPropagation();
return $response;
} elseif ($isJs) {
include WWW_ROOT . DS . $filters['js'];
$event->stopPropagation();
return $response;
}
$pathSegments = explode('.', $url);
$ext = array_pop($pathSegments);
$parts = explode('/', $url);
$assetFile = null;
if ($parts[0] === 'theme') {
$themeName = $parts[1];
unset($parts[0], $parts[1]);
$fileFragment = urldecode(implode(DS, $parts));
$path = App::themePath($themeName) . 'webroot' . DS;
if (file_exists($path . $fileFragment)) {
$assetFile = $path . $fileFragment;
}
} else {
$plugin = Inflector::camelize($parts[0]);
if (CakePlugin::loaded($plugin)) {
unset($parts[0]);
$fileFragment = urldecode(implode(DS, $parts));
$pluginWebroot = CakePlugin::path($plugin) . 'webroot' . DS;
if (file_exists($pluginWebroot . $fileFragment)) {
$assetFile = $pluginWebroot . $fileFragment;
}
}
}
if ($assetFile !== null) {
$event->stopPropagation();
$this->_deliverAsset($response, $assetFile, $ext);
return $response;
}
}
/**
* Sends an asset file to the client
*
* @param CakeResponse $response The response object to use.
* @param string $assetFile Path to the asset file in the file system
* @param string $ext The extension of the file to determine its mime type
* @return void
*/
protected function _deliverAsset(CakeResponse $response, $assetFile, $ext) {
ob_start();
$compressionEnabled = Configure::read('Asset.compress') && $response->compress();
if ($response->type($ext) == $ext) {
$contentType = 'application/octet-stream';
$agent = env('HTTP_USER_AGENT');
if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent) || preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
$contentType = 'application/octetstream';
}
$response->type($contentType);
}
if (!$compressionEnabled) {
$response->header('Content-Length', filesize($assetFile));
}
$response->cache(filemtime($assetFile));
$response->send();
ob_clean();
if ($ext === 'css' || $ext === 'js') {
include $assetFile;
} else {
readfile($assetFile);
}
if ($compressionEnabled) {
ob_end_flush();
}
}
}

View file

@ -23,7 +23,7 @@ App::uses('CakeEventListener', 'Event');
* 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
* @package Cake.Routing
*/
abstract class DispatcherFilter implements CakeEventListener {

View file

@ -0,0 +1,156 @@
<?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('DispatcherFilter', 'Routing');
/**
* Filters a request and tests whether it is a file in the webroot folder or not and
* serves the file to the client if appropriate.
*
* @package Cake.Routing.Filter
*/
class AssetDispatcher extends DispatcherFilter {
/**
* Default priority for all methods in this filter
* This filter should run before the request gets parsed by router
*
* @var int
**/
public $priority = 9;
/**
* Checks if a requested asset exists and sends it to the browser
*
* @param CakeEvent $event containing the request and response object
* @return CakeResponse if the client is requesting a recognized asset, null otherwise
*/
public function beforeDispatch($event) {
$url = $event->data['request']->url;
$response = $event->data['response'];
if (strpos($url, '..') !== false || strpos($url, '.') === false) {
return;
}
if ($result = $this->_filterAsset($event)) {
$event->stopPropagation();
return $result;
}
$pathSegments = explode('.', $url);
$ext = array_pop($pathSegments);
$parts = explode('/', $url);
$assetFile = null;
if ($parts[0] === 'theme') {
$themeName = $parts[1];
unset($parts[0], $parts[1]);
$fileFragment = urldecode(implode(DS, $parts));
$path = App::themePath($themeName) . 'webroot' . DS;
if (file_exists($path . $fileFragment)) {
$assetFile = $path . $fileFragment;
}
} else {
$plugin = Inflector::camelize($parts[0]);
if (CakePlugin::loaded($plugin)) {
unset($parts[0]);
$fileFragment = urldecode(implode(DS, $parts));
$pluginWebroot = CakePlugin::path($plugin) . 'webroot' . DS;
if (file_exists($pluginWebroot . $fileFragment)) {
$assetFile = $pluginWebroot . $fileFragment;
}
}
}
if ($assetFile !== null) {
$event->stopPropagation();
$this->_deliverAsset($response, $assetFile, $ext);
return $response;
}
}
/**
* Checks if the client is requeting a filtered asset and runs the corresponding
* filter if any is configured
*
* @param CakeEvent $event containing the request and response object
* @return CakeResponse if the client is requesting a recognized asset, null otherwise
*/
protected function _filterAsset($event) {
$url = $event->data['request']->url;
$response = $event->data['response'];
$filters = Configure::read('Asset.filter');
$isCss = (
strpos($url, 'ccss/') === 0 ||
preg_match('#^(theme/([^/]+)/ccss/)|(([^/]+)(?<!css)/ccss)/#i', $url)
);
$isJs = (
strpos($url, 'cjs/') === 0 ||
preg_match('#^/((theme/[^/]+)/cjs/)|(([^/]+)(?<!js)/cjs)/#i', $url)
);
if (($isCss && empty($filters['css'])) || ($isJs && empty($filters['js']))) {
$response->statusCode(404);
return $response;
} elseif ($isCss) {
include WWW_ROOT . DS . $filters['css'];
return $response;
} elseif ($isJs) {
include WWW_ROOT . DS . $filters['js'];
return $response;
}
}
/**
* Sends an asset file to the client
*
* @param CakeResponse $response The response object to use.
* @param string $assetFile Path to the asset file in the file system
* @param string $ext The extension of the file to determine its mime type
* @return void
*/
protected function _deliverAsset(CakeResponse $response, $assetFile, $ext) {
ob_start();
$compressionEnabled = Configure::read('Asset.compress') && $response->compress();
if ($response->type($ext) == $ext) {
$contentType = 'application/octet-stream';
$agent = env('HTTP_USER_AGENT');
if (preg_match('%Opera(/| )([0-9].[0-9]{1,2})%', $agent) || preg_match('/MSIE ([0-9].[0-9]{1,2})/', $agent)) {
$contentType = 'application/octetstream';
}
$response->type($contentType);
}
if (!$compressionEnabled) {
$response->header('Content-Length', filesize($assetFile));
}
$response->cache(filemtime($assetFile));
$response->send();
ob_clean();
if ($ext === 'css' || $ext === 'js') {
include $assetFile;
} else {
readfile($assetFile);
}
if ($compressionEnabled) {
ob_end_flush();
}
}
}

View file

@ -0,0 +1,70 @@
<?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('DispatcherFilter', 'Routing');
/**
* This filter will check wheter the response was previously cached in the file system
* and served it back to the client if appropriate.
*
* @package Cake.Routing.Filter
*/
class CacheDispatcher extends DispatcherFilter {
/**
* Default priority for all methods in this filter
* This filter should run before the request gets parsed by router
*
* @var int
**/
public $priority = 9;
/**
* Checks whether the response was cached and set the body accordingly.
*
* @param CakeEvent $event containing the request and response object
* @return CakeResponse with cached content if found, null otherwise
*/
public function beforeDispatch($event) {
if (Configure::read('Cache.check') !== true) {
return;
}
$path = $event->data['request']->here();
if ($path == '/') {
$path = 'home';
}
$path = strtolower(Inflector::slug($path));
$filename = CACHE . 'views' . DS . $path . '.php';
if (!file_exists($filename)) {
$filename = CACHE . 'views' . DS . $path . '_index.php';
}
if (file_exists($filename)) {
$controller = null;
$view = new View($controller);
$result = $view->renderCache($filename, microtime(true));
if ($result !== false) {
$event->data['response']->body($result);
return $event->data['response'];
}
}
}
}

View file

@ -38,6 +38,7 @@ class AllRoutingTest extends PHPUnit_Framework_TestSuite {
$suite->addTestDirectory($libs . 'Routing');
$suite->addTestDirectory($libs . 'Routing' . DS . 'Route');
$suite->addTestDirectory($libs . 'Routing' . DS . 'Filter');
return $suite;
}
}

View file

@ -1357,6 +1357,7 @@ class DispatcherTest extends CakeTestCase {
'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS)
));
CakePlugin::load(array('TestPlugin', 'TestPluginTwo'));
Configure::write('Dispatcher.filters', array('AssetDispatcher'));
$Dispatcher = new TestDispatcher();
$response = $this->getMock('CakeResponse', array('_sendHeader'));
@ -1377,7 +1378,7 @@ class DispatcherTest extends CakeTestCase {
}
/**
* Data provider for asset()
* Data provider for asset filter
*
* - theme assets.
* - plugin assets.
@ -1475,6 +1476,7 @@ class DispatcherTest extends CakeTestCase {
'View' => array(CAKE . 'Test' . DS . 'test_app' . DS . 'View' . DS)
));
CakePlugin::load(array('TestPlugin', 'PluginJs'));
Configure::write('Dispatcher.filters', array('AssetDispatcher'));
$Dispatcher = new TestDispatcher();
$response = $this->getMock('CakeResponse', array('_sendHeader'));
@ -1503,57 +1505,13 @@ class DispatcherTest extends CakeTestCase {
'js' => '',
'css' => null
));
Configure::write('Dispatcher.filters', array('AssetDispatcher'));
$request = new CakeRequest('ccss/cake.generic.css');
$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
$this->assertSame($response, $Dispatcher->asset($event));
$Dispatcher->dispatch($request, $response);
$this->assertEquals('404', $response->statusCode());
$this->assertTrue($event->isStopped());
}
/**
* test that asset filters work for theme and plugin assets
*
* @return void
*/
public function testAssetFilterForThemeAndPlugins() {
$Dispatcher = new TestDispatcher();
$response = $this->getMock('CakeResponse', array('_sendHeader'));
Configure::write('Asset.filter', array(
'js' => '',
'css' => ''
));
$request = new CakeRequest('theme/test_theme/ccss/cake.generic.css');
$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
$this->assertSame($response, $Dispatcher->asset($event));
$this->assertTrue($event->isStopped());
$request = new CakeRequest('theme/test_theme/cjs/debug_kit.js');
$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
$this->assertSame($response, $Dispatcher->asset($event));
$this->assertTrue($event->isStopped());
$request = new CakeRequest('test_plugin/ccss/cake.generic.css');
$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
$this->assertSame($response, $Dispatcher->asset($event));
$this->assertTrue($event->isStopped());
$request = new CakeRequest('test_plugin/cjs/debug_kit.js');
$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
$this->assertSame($response, $Dispatcher->asset($event));
$this->assertTrue($event->isStopped());
$request = new CakeRequest('css/ccss/debug_kit.css');
$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
$this->assertNull($Dispatcher->asset($event));
$this->assertFalse($event->isStopped());
$request = new CakeRequest('js/cjs/debug_kit.js');
$event = new CakeEvent('DispatcherTest', $Dispatcher, compact('request', 'response'));
$this->assertNull($Dispatcher->asset($event));
$this->assertFalse($event->isStopped());
}
/**
* Data provider for cached actions.
@ -1606,8 +1564,11 @@ class DispatcherTest extends CakeTestCase {
$dispatcher->dispatch($request, $response);
$out = $response->body();
$event = new CakeEvent('DispatcherTest', $dispatcher, array('request' => $request, 'response' => $response));
$response = $dispatcher->cached($event);
Configure::write('Dispatcher.filters', array('CacheDispatcher'));
$request = new CakeRequest($url);
$response = $this->getMock('CakeResponse', array('send'));
$dispatcher = new TestDispatcher();
$dispatcher->dispatch($request, $response);
$cached = $response->body();
$cached = preg_replace('/<!--+[^<>]+-->/', '', $cached);

View file

@ -0,0 +1,78 @@
<?php
/**
* RouterTest file
*
* PHP 5
*
* CakePHP(tm) Tests <http://book.cakephp.org/view/1196/Testing>
* Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The Open Group Test Suite License
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright 2005-2012, Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://book.cakephp.org/view/1196/Testing CakePHP(tm) Tests
* @package Cake.Test.Case.Routing.Filter
* @since CakePHP(tm) v 2.2
* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
*/
App::uses('AssetDispatcher', 'Routing/Filter');
App::uses('CakeEvent', 'Event');
App::uses('CakeResponse', 'Network');
class AssetDispatcherTest extends CakeTestCase {
/**
* tearDown method
*
* @return void
*/
public function tearDown() {
Configure::write('Dispatcher.filters', array());
}
/**
* test that asset filters work for theme and plugin assets
*
* @return void
*/
public function testAssetFilterForThemeAndPlugins() {
$filter = new AssetDispatcher();
$response = $this->getMock('CakeResponse', array('_sendHeader'));
Configure::write('Asset.filter', array(
'js' => '',
'css' => ''
));
$request = new CakeRequest('theme/test_theme/ccss/cake.generic.css');
$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
$this->assertSame($response, $filter->beforeDispatch($event));
$this->assertTrue($event->isStopped());
$request = new CakeRequest('theme/test_theme/cjs/debug_kit.js');
$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
$this->assertSame($response, $filter->beforeDispatch($event));
$this->assertTrue($event->isStopped());
$request = new CakeRequest('test_plugin/ccss/cake.generic.css');
$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
$this->assertSame($response, $filter->beforeDispatch($event));
$this->assertTrue($event->isStopped());
$request = new CakeRequest('test_plugin/cjs/debug_kit.js');
$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
$this->assertSame($response, $filter->beforeDispatch($event));
$this->assertTrue($event->isStopped());
$request = new CakeRequest('css/ccss/debug_kit.css');
$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
$this->assertNull($filter->beforeDispatch($event));
$this->assertFalse($event->isStopped());
$request = new CakeRequest('js/cjs/debug_kit.js');
$event = new CakeEvent('DispatcherTest', $this, compact('request', 'response'));
$this->assertNull($filter->beforeDispatch($event));
$this->assertFalse($event->isStopped());
}
}