PageRenderTime 33ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/site/app/views/helpers/script_combiner.php

https://github.com/plastic/Cake-boilerplate
PHP | 325 lines | 151 code | 37 blank | 137 comment | 26 complexity | 61389c7d10230c019750521f4d8c1557 MD5 | raw file
  1. <?php
  2. /**
  3. * The helper definition for the Script Combiner helper.
  4. * @author Geoffrey Garbers
  5. * @version 0.1
  6. */
  7. // Include the stand-alone configuration file if it exists.
  8. if (is_file(CONFIGS . 'script_combiner.php')) {
  9. Configure::load('script_combiner');
  10. }
  11. /**
  12. * @property HtmlHelper $Html
  13. * @property JavascriptHelper $Javascript
  14. */
  15. class ScriptCombinerHelper extends Helper {
  16. /**
  17. * Array containing additional helpers to be used in this helper.
  18. * @access public
  19. * @var array
  20. */
  21. public $helpers = array('Html', 'Javascript');
  22. /**
  23. * The directory to which the combined CSS files will be cached.
  24. * @access private
  25. * @var string
  26. */
  27. private $cssCachePath;
  28. /**
  29. * The directory to which the combined Javascript files will be cached.
  30. * @access private
  31. * @var string
  32. */
  33. private $jsCachePath;
  34. /**
  35. * Indicates the time of expiry for the combined files.
  36. * @access private
  37. * @var int
  38. */
  39. private $cacheLength;
  40. /**
  41. * Indicates whether the class is active and is able to begin combining scripts.
  42. * @access private
  43. * @var bool
  44. */
  45. private $enabled = false;
  46. /**
  47. * Sets up the helper's properties, and determines whether the helper's environment
  48. * is set up and ready to be used.
  49. * @access public
  50. * @return void
  51. */
  52. public function __construct() {
  53. parent::__construct();
  54. // Check to make there are configuration options that can be found.
  55. if (!Configure::read('ScriptCombiner')) {
  56. trigger_error('Please define the ScriptCombiner configuration options.', E_USER_WARNING);
  57. return;
  58. }
  59. // Retrieve the CSS cache path, and ensure the path exists and is writable.
  60. $this->cssCachePath = Configure::read('ScriptCombiner.cssCachePath');
  61. if (!is_dir($this->cssCachePath)) {
  62. trigger_error('Cannot locate CSS combination cache directory at ' . $this->cssCachePath . ', or path is not writable.', E_USER_WARNING);
  63. return;
  64. }
  65. // Retrieve the Javascript cache path, and check to ensure that the path
  66. // is existing and writable.
  67. $this->jsCachePath = Configure::read('ScriptCombiner.jsCachePath');
  68. if (!is_dir($this->jsCachePath) || !is_writable($this->jsCachePath)) {
  69. trigger_error('Cannot locate Javascript combination cache directory at ' . $this->jsCachePath . ', or path is not writable.', E_USER_WARNING);
  70. return;
  71. }
  72. $cacheLength = Configure::read('ScriptCombiner.cacheLength');
  73. if (is_string($cacheLength)) {
  74. $this->cacheLength = strtotime($cacheLength) - time();
  75. } else {
  76. $this->cacheLength = (int)$cacheLength;
  77. }
  78. $this->enabled = true;
  79. }
  80. /**
  81. * <p>Receives numerous CSS files, and combines all the supplied CSS files into one
  82. * file, which helps in reducing the number of HTTP requests needed to load numerous
  83. * CSS files.</p>
  84. *
  85. * <p>Files to be combined should be supplied exactly as if they were being used in
  86. * the HtmlHelper::css() method, as this method is used to generate the paths
  87. * to the files.</p>
  88. * @access public
  89. * @param mixed [$url1,$url2,...] Either an array of files to combine, or multiple arguments of filenames.
  90. * @return string The HTML &lt;link /&gt; to either the combined file, or to the multiple CSS files if the combined file could not be cached.
  91. */
  92. public function css() {
  93. // Get the CSS files.
  94. $cssFiles = func_get_args();
  95. // Oh dear. There aren't any files to process. We'll have to return an empty
  96. // string.
  97. if (empty($cssFiles)) {
  98. return '';
  99. }
  100. // Whoops. No configuration options defined, or something else went wrong
  101. // in trying to set up the class. Either way, we can't process the files,
  102. // so we'll need to handle this through the parent.
  103. if (!$this->enabled) {
  104. return $this->Html->css($cssFiles);
  105. }
  106. // Let's generate the cache hash, and ensure we have all the files that
  107. // we are going to use to process.
  108. if (is_array($cssFiles[0])) {
  109. $cssFiles = $cssFiles[0];
  110. }
  111. $cacheKey = md5(serialize($cssFiles));
  112. // Let's generate the path to the cache file.
  113. $cacheFile = "{$this->cssCachePath}combined.{$cacheKey}.css";
  114. // Oh. Look. It appears we already have a cached version of the combined
  115. // file. This means we'll have to see when it last modified, and ensure
  116. // that the cached version hasn't yet expired. If it has, then we can
  117. // just return the URL to it straight away.
  118. if ($this->isCacheFileValid($cacheFile)) {
  119. return $this->Html->css($this->convertToUrl($cacheFile));
  120. }
  121. // Let's generate the HTML that would normally be returned, and strip
  122. // out the URLs.
  123. $cssData = array();
  124. $links = $this->Html->css($cssFiles, 'import');
  125. preg_match_all('#\(([^\)]+)\)#i', $links, $urlMatches);
  126. if (isset($urlMatches[1])) {
  127. $urlMatches = $urlMatches[1];
  128. } else {
  129. $urlMatches = array();
  130. }
  131. // Let's cycle through each URL, and attempt to retrieve the file contents.
  132. // If we *can* get the file data successfully, then we'll add it to the
  133. // array of data to combine.
  134. foreach ($urlMatches as $urlMatch) {
  135. $cssPath = str_replace(array('/', '\\'), DS, WWW_ROOT . ltrim(Router::normalize($urlMatch), '/'));
  136. if (is_file($cssPath)) {
  137. $cssData[] = file_get_contents($cssPath);
  138. }
  139. }
  140. // Let's combine them.
  141. $cssData = implode(Configure::read('ScriptCombiner.fileSeparator'), $cssData);
  142. // Let's check whether we need to compress the CSS. If so, we'll compress
  143. // it before saving it.
  144. if (Configure::read('ScriptCombiner.compressCss')) {
  145. $cssData = $this->compressCss($cssData);
  146. }
  147. // If we can cache the file, then we can return the URL to the file.
  148. if (file_put_contents($cacheFile, $cssData) > 0) {
  149. return $this->Html->css($this->convertToUrl($cacheFile));
  150. }
  151. // Otherwise, we'll have to trigger an error, and pass the handling of the
  152. // CSS files to the HTML Helper.
  153. trigger_error("Cannot combine CSS files to {$cacheFile}. Please ensure this directory is writable.", E_USER_WARNING);
  154. return $this->Html->css($cssFiles);
  155. }
  156. /**
  157. * Receives a number of Javascript files, and combines all of them together.
  158. * @access public
  159. * @param mixed [$url1,$url2,...] Either an array of files to combine, or multiple arguments of filenames.
  160. * @return string The HTML &lt;script /&gt; to either the combined file, or to the multiple Javascript files if the combined file could not be cached.
  161. */
  162. public function js() {
  163. // Get the javascript files.
  164. $jsFiles = func_get_args();
  165. // Whoops. No files! We'll have to return an empty string then.
  166. if (empty($jsFiles)) {
  167. return '';
  168. }
  169. // If the helper hasn't been set up correctly, then there's no point in
  170. // combining scripts. We'll pass it off to the parent to handle.
  171. if (!$this->enabled) {
  172. return $this->Javascript->link($jsFiles);
  173. }
  174. // Let's make sure we have the array of files correct. And we'll generate
  175. // a key for the cache based on the files supplied.
  176. if (is_array($jsFiles[0])) {
  177. $jsFiles = $jsFiles[0];
  178. }
  179. $cacheKey = md5(serialize($jsFiles));
  180. // And we'll generate the absolute path to the cache file.
  181. $cacheFile = "{$this->jsCachePath}combined.{$cacheKey}.js";
  182. // If we can determine that the current cache file is still valid, then
  183. // we can just return the URL to that file.
  184. if ($this->isCacheFileValid($cacheFile)) {
  185. return $this->Javascript->link($this->convertToUrl($cacheFile));
  186. }
  187. $jsData = array();
  188. $jsLinks = $this->Javascript->link($jsFiles);
  189. preg_match_all('/src="([^"]+)"/i', $jsLinks, $urlMatches);
  190. if (isset($urlMatches[1])) {
  191. $urlMatches = array_unique($urlMatches[1]);
  192. } else {
  193. $urlMatches = array();
  194. }
  195. foreach ($urlMatches as $urlMatch) {
  196. $jsPath = str_replace(array('/', '\\'), DS, WWW_ROOT . ltrim(Router::normalize($urlMatch), '/'));
  197. if (is_file($jsPath)) {
  198. $jsData[] = file_get_contents($jsPath);
  199. }
  200. }
  201. // Let's combine them.
  202. $jsData = implode(Configure::read('ScriptCombiner.fileSeparator'), $jsData);
  203. // Let's check whether we need to compress the Javascript. If so, we'll
  204. // compress it before saving it.
  205. if (Configure::read('ScriptCombiner.compressJs')) {
  206. $jsData = $this->compressJs($jsData);
  207. }
  208. // If we can cache the file, then we can return the URL to the file.
  209. if (file_put_contents($cacheFile, $jsData) > 0) {
  210. return $this->Javascript->link($this->convertToUrl($cacheFile));
  211. }
  212. // Otherwise, we'll have to trigger an error, and pass the handling of the
  213. // CSS files to the HTML Helper.
  214. #trigger_error("Cannot combine Javascript files to {$cacheFile}. Please ensure this directory is writable.", E_USER_WARNING);
  215. return $this->Javascript->link($jsFiles);
  216. }
  217. /**
  218. * Indicates whether the supplied cached file's cache life has expired or not.
  219. * Returns a boolean value indicating this.
  220. * @access private
  221. * @param string $cacheFile The path to the cached file to check.
  222. * @return bool Flag indicating whether the file has expired or not.
  223. */
  224. private function isCacheFileValid($cacheFile) {
  225. if (is_file($cacheFile) && $this->cacheLength > 0) {
  226. $lastModified = filemtime($cacheFile);
  227. $timeNow = time();
  228. if (($timeNow - $lastModified) < $this->cacheLength) {
  229. return true;
  230. }
  231. }
  232. return false;
  233. }
  234. /**
  235. * Receives the path to a given file, and strips the webroot off the file, returning
  236. * a URL path that is relative to the webroot (WWW_ROOT).
  237. * @access private
  238. * @param string $filePath The path to the file.
  239. * @return string The path to the file, relative to WWW_ROOT (webroot).
  240. */
  241. private function convertToUrl($filePath) {
  242. $___path = Set::filter(explode(DS, $filePath));
  243. $___root = Set::filter(explode(DS, WWW_ROOT));
  244. $webroot = array_diff_assoc($___root, $___path);
  245. $webrootPaths = array_diff_assoc($___path, $___root);
  246. return ('/' . implode('/', $webrootPaths));
  247. }
  248. /**
  249. * Receives the CSS data to compress, and compresses it. Doesn't apply any encoding
  250. * to it (such as GZIP), but merely strips out unnecessary whitespace.
  251. * @access private
  252. * @param string $cssData CSS data to be compressed.
  253. * @return string Compressed CSS data.
  254. */
  255. private function compressCss($cssData) {
  256. // let's remove all the comments from the css code.
  257. $cssData = preg_replace('!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $cssData);
  258. // let's remove all tabs and line breaks.
  259. $cssData = str_replace(array("\r\n", "\r", "\n", "\t"), '', $cssData);
  260. // remove trailing semicolons just before closing brace.
  261. $cssData = preg_replace('/;\s*}/i', '}', $cssData);
  262. // remove any whitespace between element selector and opening brace.
  263. $cssData = preg_replace('/[\t\s]*{[\t\s]*/i', '{', $cssData);
  264. // remove whitespace between style declarations and their values.
  265. $cssData = preg_replace('/[\t\s]*:[\t\s]*/i', ':', $cssData);
  266. // remove whitespace between sizes and their measurements.
  267. $cssData = preg_replace('/(\d)[\s\t]+(em|px|%)/i', '$1$2', $cssData);
  268. // remove any spaces between background image "url" and the opening "(".
  269. $cssData = preg_replace('/url[\s\t]+\(/i', 'url(', $cssData);
  270. return $cssData;
  271. }
  272. /**
  273. * Compresses the supplied Javascript data, removing extra whitespaces, as well
  274. * as any comments found.
  275. * @access private
  276. * @param string $jsData The Javascript data to be compressed.
  277. * @return string The compressed Javascript data.
  278. * @todo Implement reliable Javascript compression without use of a 3rd party.
  279. */
  280. private function compressJs($jsData) {
  281. return $jsData;
  282. }
  283. }
  284. ?>