diff --git a/core/Requirements.php b/core/Requirements.php index 4654e472b..f93a0e600 100644 --- a/core/Requirements.php +++ b/core/Requirements.php @@ -6,14 +6,36 @@ * @subpackage view */ class Requirements { + private static $javascript = array(); + private static $css = array(); + private static $customScript = array(); + private static $customCSS = array(); + private static $customHeadTags = ""; + private static $disabled = array(); + private static $blocked = array(); + /** + * See {@link combine_files()}. + * + * @var array $files_to_combine + */ + public static $files_to_combine = array(); + + /** + * Using the JSMin library to minify any + * javascript file passed to {@link combine_files()}. + * + * @var boolean + */ + public static $combine_js_with_jsmin = true; + /** * Register the given javascript file as required. * Filenames should be relative to the base, eg, 'sapphire/javascript/loader.js' @@ -175,16 +197,26 @@ class Requirements { /** * Update the given HTML content with the appropriate include tags for the registered - * requirements. + * requirements. Needs to receive a valid HTML/XHTML template in the $content parameter, + * including a tag. The requirements will insert before the closing tag automatically. + * * @todo Calculate $prefix properly + * + * @param string $templateFilePath Absolute path for the *.ss template file + * @param string $content HTML content that has already been parsed from the $templateFilePath through {@link SSViewer}. + * @return string HTML content thats augumented with the requirements before the closing tag. */ - static function includeInHTML($templateFile, $content) { + static function includeInHTML($templateFilePath, $content) { if(isset($_GET['debug_profile'])) Profiler::mark("Requirements::includeInHTML"); if(strpos($content, ' $dummy) { if(substr($file,0,7) == 'http://' || Director::fileExists($file)) { $requirements .= "\n"; @@ -198,6 +230,9 @@ class Requirements { $requirements .= "\n//]]>\n\n"; } } + + $jsRequirements=$requirements; + foreach(array_diff_key(self::$css,self::$blocked) as $file => $params) { if(Director::fileExists($file)) { $media = (isset($params['media']) && !empty($params['media'])) ? " media=\"{$params['media']}\"" : ""; @@ -219,6 +254,139 @@ class Requirements { } } + /** + * Concatenate several css or javascript files into a single dynamically generated + * file (stored in {@link Director::baseFolder()}). This increases performance + * by fewer HTTP requests. + * + * The combined file is regenerated + * based on every file modification time. Optionally a rebuild can be triggered + * by appending ?flush=1 to the URL. + * If all files to be combined are javascript, we use the external JSMin library + * to minify the javascript. This can be controlled by {@link $combine_js_with_jsmin}. + * + * All combined files will have a comment on the start of each concatenated file + * denoting their original position. For easier debugging, we recommend to only + * minify javascript if not in development mode ({@link Director::isDev()}). + * + * CAUTION: You're responsible for ensuring that the load order for combined files + * is retained - otherwise combining javascript files can lead to functional errors + * in the javascript logic, and combining css can lead to wrong styling inheritance. + * Depending on the javascript logic, you also have to ensure that files are not included + * in more than one combine_files() call. + * + * Example for combined JavaScript: + * + * Requirements::combine_files( + * 'foobar.js', + * array( + * 'mysite/javascript/foo.js', + * 'mysite/javascript/bar.js', + * ) + * ); + * + * + * Example for combined CSS: + * + * Requirements::combine_files( + * 'foobar.css', + * array( + * 'mysite/javascript/foo.css', + * 'mysite/javascript/bar.css', + * ) + * ); + * + * + * @see http://code.google.com/p/jsmin-php/ + * + * @param string $combinedFileName Filename of the combined file (will be stored in {@link Director::baseFolder()} by default) + * @param array $files Array of filenames relative to the webroot + */ + static function combine_files($combinedFileName, $files){ + self::$files_to_combine[$combinedFileName] = $files; + } + + /** + * See {@link combine_files()}. + */ + static function process_combined_includes() { + // Make a map of files that could be potentially combined + $combinerCheck = array(); + foreach(self::$files_to_combine as $combinedFile => $sourceItems) { + foreach($sourceItems as $sourceItem) { + if(isset($combinerCheck[$sourceItem]) && $combinerCheck[$sourceItem] != $combinedFile){ + user_error("Requirements::process_combined_includes - file '$sourceItem' appears in two combined files:" . " '{$combinerCheck[$sourceItem]}' and '$combinedFile'", E_USER_WARNING); + } + $combinerCheck[$sourceItem] = $combinedFile; + + } + } + + // Figure out which ones apply to this pageview + $combinedFiles = array(); + $newJSRequirements = array(); + $newCSSRequirements = array(); + foreach(Requirements::$javascript as $file => $dummy) { + if(isset($combinerCheck[$file])) { + $newJSRequirements[$combinerCheck[$file]] = true; + $combinedFiles[$combinerCheck[$file]] = true; + } else { + $newJSRequirements[$file] = true; + } + } + + foreach(Requirements::$css as $file => $params) { + if(isset($combinerCheck[$file])) { + $newCSSRequirements[$combinerCheck[$file]] = true; + $combinedFiles[$combinerCheck[$file]] = true; + } else { + $newCSSRequirements[$file] = $params; + } + } + + Requirements::$javascript = $newJSRequirements; + Requirements::$css = $newCSSRequirements; + + // Process the combined files + if($combinedFiles) { + $base = Director::baseFolder() . '/'; + foreach($combinedFiles as $combinedFile => $dummy) { + $fileList = self::$files_to_combine[$combinedFile]; + + // Determine if we need to build the combined include + if(file_exists($base . $combinedFile) && !isset($_GET['flush'])) { + $srcLastMod = 0; + foreach($fileList as $file) { + $srcLastMod = max(filemtime($base . $file), $srcLastMod); + } + $refresh = $srcLastMod > filemtime($base . $combinedFile); + } else { + $refresh = true; + } + + // Rebuild, if necessary + if($refresh) { + $combinedData = ""; + foreach($fileList as $file) { + $fileContent = file_get_contents($base . $file); + if(stripos($file, '.js') && self::$combine_js_with_jsmin) { + $fileContent = JSMin::minify($fileContent); + } + $combinedData .= "/****** FILE: $file *****/\n" . $fileContent . "\n"; + } + if(!file_exists(dirname($base . $combinedFile))) + mkdir(dirname($base . $combinedFile), Filesystem::$folder_create_mask, true); + + $fh = fopen($base . $combinedFile, 'w'); + fwrite($fh, $combinedData); + fclose($fh); + } + } + } + + } + + static function get_custom_scripts() { $requirements = ""; diff --git a/tests/forms/RequirementsTest.php b/tests/forms/RequirementsTest.php new file mode 100644 index 000000000..f812ca157 --- /dev/null +++ b/tests/forms/RequirementsTest.php @@ -0,0 +1,53 @@ +'; + + function testCombinedJavascript() { + // require files normally (e.g. called from a FormField instance) + Requirements::javascript('sapphire/tests/forms/a.js'); + Requirements::javascript('sapphire/tests/forms/b.js'); + Requirements::javascript('sapphire/tests/forms/c.js'); + + // require two of those files as combined includes + Requirements::combine_files( + 'bc.js', + array( + 'sapphire/tests/forms/b.js', + 'sapphire/tests/forms/c.js' + ) + ); + + $combinedFilePath = Director::baseFolder() . '/' . 'bc.js'; + + $html = Requirements::includeInHTML(false, self::$html_template); + + /* COMBINED JAVASCRIPT FILE IS INCLUDED IN HTML HEADER */ + $this->assertTrue((bool)preg_match('/src=".*\/bc\.js"/', $html)); + + /* COMBINED JAVASCRIPT FILE EXISTS */ + $this->assertTrue(file_exists($combinedFilePath)); + + /* COMBINED JAVASCRIPT HAS CORRECT CONTENT */ + $this->assertTrue((strpos(file_get_contents($combinedFilePath), "alert('b')") !== false)); + $this->assertTrue((strpos(file_get_contents($combinedFilePath), "alert('c')") !== false)); + + /* COMBINED FILES ARE NOT INCLUDED TWICE */ + $this->assertFalse((bool)preg_match('/src=".*\/b\.js"/', $html)); + $this->assertFalse((bool)preg_match('/src=".*\/c\.js"/', $html)); + + /* NORMAL REQUIREMENTS ARE STILL INCLUDED */ + $this->assertTrue((bool)preg_match('/src=".*\/a\.js"/', $html)); + + unlink($combinedFilePath); + + } + +} +?> \ No newline at end of file diff --git a/tests/forms/a.css b/tests/forms/a.css new file mode 100644 index 000000000..730bd9eb3 --- /dev/null +++ b/tests/forms/a.css @@ -0,0 +1 @@ +.a {color: #f00;} \ No newline at end of file diff --git a/tests/forms/a.js b/tests/forms/a.js new file mode 100644 index 000000000..829a65243 --- /dev/null +++ b/tests/forms/a.js @@ -0,0 +1 @@ +alert('a'); \ No newline at end of file diff --git a/tests/forms/b.css b/tests/forms/b.css new file mode 100644 index 000000000..e11f7eebf --- /dev/null +++ b/tests/forms/b.css @@ -0,0 +1 @@ +.b {color: #0ff;} \ No newline at end of file diff --git a/tests/forms/b.js b/tests/forms/b.js new file mode 100644 index 000000000..3d8670ca8 --- /dev/null +++ b/tests/forms/b.js @@ -0,0 +1 @@ +alert('b'); \ No newline at end of file diff --git a/tests/forms/c.css b/tests/forms/c.css new file mode 100644 index 000000000..ae49dd202 --- /dev/null +++ b/tests/forms/c.css @@ -0,0 +1 @@ +.c {color: #0ff;} \ No newline at end of file diff --git a/tests/forms/c.js b/tests/forms/c.js new file mode 100644 index 000000000..1e0f7b3ce --- /dev/null +++ b/tests/forms/c.js @@ -0,0 +1 @@ +alert('c'); \ No newline at end of file