From 2969a7ee44273700b85cf850a45adf6be2fb5512 Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Mon, 12 Apr 2010 21:09:54 +0000 Subject: [PATCH] FEATURE: Add partial caching support to SSViewer. (from r97391) git-svn-id: svn://svn.silverstripe.com/silverstripe/open/modules/sapphire/trunk@102488 467b73ca-7a2a-4603-9d3b-597d59a354a9 --- core/SSViewer.php | 119 +++++++++++++++++++++++++++++-- tests/SSViewerCacheBlockTest.php | 72 +++++++++++++++++++ 2 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 tests/SSViewerCacheBlockTest.php diff --git a/core/SSViewer.php b/core/SSViewer.php index 51ecdc789..6572d1f25 100755 --- a/core/SSViewer.php +++ b/core/SSViewer.php @@ -313,9 +313,11 @@ class SSViewer { /** * The process() method handles the "meat" of the template processing. */ - public function process($item) { + public function process($item, $cache = null) { SSViewer::$topLevel[] = $item; - + + if (!$cache) $cache = Cache::factory('cacheblock'); + if(isset($this->chosenTemplates['main'])) { $template = $this->chosenTemplates['main']; } else { @@ -356,14 +358,15 @@ class SSViewer { if(isset($this->chosenTemplates[$subtemplate])) { $subtemplateViewer = new SSViewer($this->chosenTemplates[$subtemplate]); $item = $item->customise(array( - $subtemplate => $subtemplateViewer->process($item) + $subtemplate => $subtemplateViewer->process($item, $cache) )); } } $itemStack = array(); $val = ""; - + $valStack = array(); + include($cacheFile); $output = $val; @@ -450,6 +453,9 @@ class SSViewer { $content = ereg_replace('', '<' . '% control \\1 %' . '>', $content); $content = ereg_replace('', '<' . '% end_control %' . '>', $content); + // < % cacheblock key, key.. % > + $content = SSViewer_PartialParser::parse($template, $content); + // < % control Foo % > $content = ereg_replace('<' . '% +control +([A-Za-z0-9_]+) +%' . '>', 'obj("\\1")) foreach($loop as $key => $item) { ?>', $content); // < % control Foo.Bar % > @@ -613,7 +619,7 @@ class SSViewer_FromString extends SSViewer { } public function process($item) { - $template = SSViewer::parseTemplateContent($this->content); + $template = SSViewer::parseTemplateContent($this->content, "string sha1=".sha1($this->content)); $tmpFile = tempnam(TEMP_FOLDER,""); $fh = fopen($tmpFile, 'w'); @@ -632,7 +638,10 @@ class SSViewer_FromString extends SSViewer { $itemStack = array(); $val = ""; - + $valStack = array(); + + $cache = Cache::factory('cacheblock'); + include($tmpFile); unlink($tmpFile); @@ -640,7 +649,105 @@ class SSViewer_FromString extends SSViewer { return $val; } } + +/** + * Handle the parsing for cacheblock tags. + * + * Needs to be handled differently from the other tags, because cacheblock can take any number of arguments + * + * This shouldn't be used as an example of how to add functionality to SSViewer - the eventual plan is to re-write + * SSViewer using a proper parser (probably http://github.com/hafriedlander/php-peg), so that extra functionality + * can be added without relying on ad-hoc parsers like this. + */ +class SSViewer_PartialParser { + + static $opening_tag = '/< % [ \t]+ cacheblock [ \t]+ ([^%]+ [ \t]+)? % >/xS'; + static $argument_splitter = '/^\s* + ( (\w+) \s* ( \( ([^\)]*) \) )? ) | # A property lookup or a function call + ( \' [^\']+ \' ) | # A string surrounded by \' + ( " [^"]+ " ) # A string surrounded by " + \s*/xS'; + + static $closing_tag = '/< % [ \t]+ end_cacheblock [ \t]+ % >/xS'; + + static function parse($template, $content) { + $parser = new SSViewer_PartialParser($template); + + $content = $parser->replaceOpeningTags($content); + $content = $parser->replaceClosingTags($content); + return $content; + } + + function __construct($template) { + $this->template = $template; + $this->cacheblocks = 0; + } + + function replaceOpeningTags($content) { + return preg_replace_callback(self::$opening_tag, array($this, 'replaceOpeningTagsCallback'), $content); + } + + function replaceOpeningTagsCallback($matches) { + $this->cacheblocks += 1; + $key = $this->key($matches); + + return 'load('.$key.')) { $val .= $partial; } else { $valStack[] = $val; $val = ""; ?>'; + } + + function key($matches) { + $parts = array(); + $parts[] = "'".preg_replace('/[^\w+]/', '_', $this->template)."'"; + + // If there weren't any arguments, that'll do + if (!@$matches[1]) return $parts[0]; + + $current = 'preg_replace(\'/[^\w+]/\', \'_\', $item->'; + $keyspec = $matches[1]; + + while (strlen($keyspec) && preg_match(self::$argument_splitter, $keyspec, $submatch)) { + $joiner = substr($keyspec, strlen($submatch[0]), 1); + $keyspec = substr($keyspec, strlen($submatch[0]) + 1); + + // If it's a property lookup or a function call + if ($submatch[1]) { + // Get the property + $what = $submatch[2]; + $args = array(); + + // Extract any arguments passed to the function call + if (@$submatch[3]) { + foreach (explode(',', $submatch[4]) as $arg) { + $args[] = is_numeric($arg) ? (string)$arg : '"'.$arg.'"'; + } + } + + $args = empty($args) ? 'null' : 'array('.implode(',',$args).')'; + + // If this fragment ended with '.', then there's another lookup coming, so return an obj for that lookup + if ($joiner == '.') { + $current .= "obj(\"$what\", $args, true)->"; + } + // Otherwise this is the end of the lookup chain, so add the resultant value to the key array and reset the key-get php fragement + else { + $parts[] = $current . "XML_val(\"$what\", $args, true))"; $current = 'preg_replace(\'/[^\w+]/\', \'_\', $item->'; + } + } + + // Else it's a quoted string of some kind + else if ($submatch[5] || $submatch[6]) { + $parts[] = $submatch[5] ? $submatch[5] : $submatch[6]; + } + + } + + return implode(".'_'.", $parts); + } + + function replaceClosingTags($content) { + return preg_replace(self::$closing_tag, 'save($val); $val = array_pop($valStack) . $val; } ?>', $content); + } +} function supressOutput() { return ""; diff --git a/tests/SSViewerCacheBlockTest.php b/tests/SSViewerCacheBlockTest.php new file mode 100644 index 000000000..f332cd7cc --- /dev/null +++ b/tests/SSViewerCacheBlockTest.php @@ -0,0 +1,72 @@ +data = new SSViewerCacheBlockTest_Model(); + + Cache::factory('cacheblock')->clean(); + Cache::set_cache_lifetime('cacheblock', $cacheOn ? 600 : -1); + } + + protected function _runtemplate($template, $data = null) { + if ($data === null) $data = $this->data; + if (is_array($data)) $data = new ArrayData($data); + + $viewer = SSViewer::fromString($template); + return $viewer->process($data); + } + + function testParsing() { + // Make sure an empty cacheblock parses + $this->_reset(); + $this->assertEquals($this->_runtemplate('<% cacheblock %><% end_cacheblock %>'), ''); + + // Make sure a simple cacheblock parses + $this->_reset(); + $this->assertEquals($this->_runtemplate('<% cacheblock %>Yay<% end_cacheblock %>'), 'Yay'); + + // Make sure a moderately complicated cacheblock parses + $this->_reset(); + $this->assertEquals($this->_runtemplate('<% cacheblock \'block\', Foo, "jumping" %>Yay<% end_cacheblock %>'), 'Yay'); + + // Make sure a complicated cacheblock parses + $this->_reset(); + $this->assertEquals($this->_runtemplate('<% cacheblock \'block\', Foo, Test.Test(4).Test(jumping).Foo %>Yay<% end_cacheblock %>'), 'Yay'); + } + + /** + * Test that cacheblocks actually cache + */ + function testBlocksCache() { + // First, run twice without caching, to prove $Increment actually increments + $this->_reset(false); + + $this->assertEquals($this->_runtemplate('<% cacheblock %>$Foo<% end_cacheblock %>', array('Foo' => 1)), '1'); + $this->assertEquals($this->_runtemplate('<% cacheblock %>$Foo<% end_cacheblock %>', array('Foo' => 2)), '2'); + + // Then twice with caching, should get same result each time + $this->_reset(true); + + $this->assertEquals($this->_runtemplate('<% cacheblock %>$Foo<% end_cacheblock %>', array('Foo' => 1)), '1'); + $this->assertEquals($this->_runtemplate('<% cacheblock %>$Foo<% end_cacheblock %>', array('Foo' => 2)), '1'); + } + +} \ No newline at end of file