Skip to content

Commit

Permalink
Extracting asset dispatcher and cache dispatcher into separate classe…
Browse files Browse the repository at this point in the history
…s to provide examples on how to use Dispatcher

Filters
  • Loading branch information
lorenzo committed Apr 17, 2012
1 parent 70f3cc5 commit 826699a
Show file tree
Hide file tree
Showing 9 changed files with 360 additions and 194 deletions.
22 changes: 22 additions & 0 deletions app/Config/bootstrap.php
Expand Up @@ -122,3 +122,25 @@
* 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

This comment has been minimized.

Copy link
@jrbasso

jrbasso Apr 20, 2012

Member

Typo on callbale. Also, seems the spacing is different.

*
* ));
*/
Configure::write('Dispatcher.filters', array(
'AssetDispatcher',
'CacheDispatcher'
));
21 changes: 21 additions & 0 deletions lib/Cake/Console/Templates/skel/Config/bootstrap.php
Expand Up @@ -63,3 +63,24 @@
* 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'
));
145 changes: 1 addition & 144 deletions lib/Cake/Routing/Dispatcher.php
Expand Up @@ -79,13 +79,7 @@ public function getEventManager() {
* @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');
}

/**
Expand Down Expand Up @@ -283,141 +277,4 @@ protected function _loadRoutes() {
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();
}
}

}
2 changes: 1 addition & 1 deletion lib/Cake/Routing/DispatcherFilter.php
Expand Up @@ -23,7 +23,7 @@
* 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 {

Expand Down
156 changes: 156 additions & 0 deletions lib/Cake/Routing/Filter/AssetDispatcher.php
@@ -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();
}
}

}

0 comments on commit 826699a

Please sign in to comment.