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
This commit is contained in:
Ingo Schommer 2010-04-12 21:09:54 +00:00
parent 956df37c2c
commit 2969a7ee44
2 changed files with 185 additions and 6 deletions

View File

@ -313,9 +313,11 @@ class SSViewer {
/** /**
* The process() method handles the "meat" of the template processing. * The process() method handles the "meat" of the template processing.
*/ */
public function process($item) { public function process($item, $cache = null) {
SSViewer::$topLevel[] = $item; SSViewer::$topLevel[] = $item;
if (!$cache) $cache = Cache::factory('cacheblock');
if(isset($this->chosenTemplates['main'])) { if(isset($this->chosenTemplates['main'])) {
$template = $this->chosenTemplates['main']; $template = $this->chosenTemplates['main'];
} else { } else {
@ -356,13 +358,14 @@ class SSViewer {
if(isset($this->chosenTemplates[$subtemplate])) { if(isset($this->chosenTemplates[$subtemplate])) {
$subtemplateViewer = new SSViewer($this->chosenTemplates[$subtemplate]); $subtemplateViewer = new SSViewer($this->chosenTemplates[$subtemplate]);
$item = $item->customise(array( $item = $item->customise(array(
$subtemplate => $subtemplateViewer->process($item) $subtemplate => $subtemplateViewer->process($item, $cache)
)); ));
} }
} }
$itemStack = array(); $itemStack = array();
$val = ""; $val = "";
$valStack = array();
include($cacheFile); include($cacheFile);
@ -450,6 +453,9 @@ class SSViewer {
$content = ereg_replace('<!-- +pc +([A-Za-z0-9_(),]+) +-->', '<' . '% control \\1 %' . '>', $content); $content = ereg_replace('<!-- +pc +([A-Za-z0-9_(),]+) +-->', '<' . '% control \\1 %' . '>', $content);
$content = ereg_replace('<!-- +pc_end +-->', '<' . '% end_control %' . '>', $content); $content = ereg_replace('<!-- +pc_end +-->', '<' . '% end_control %' . '>', $content);
// < % cacheblock key, key.. % >
$content = SSViewer_PartialParser::parse($template, $content);
// < % control Foo % > // < % control Foo % >
$content = ereg_replace('<' . '% +control +([A-Za-z0-9_]+) +%' . '>', '<? array_push($itemStack, $item); if($loop = $item->obj("\\1")) foreach($loop as $key => $item) { ?>', $content); $content = ereg_replace('<' . '% +control +([A-Za-z0-9_]+) +%' . '>', '<? array_push($itemStack, $item); if($loop = $item->obj("\\1")) foreach($loop as $key => $item) { ?>', $content);
// < % control Foo.Bar % > // < % control Foo.Bar % >
@ -613,7 +619,7 @@ class SSViewer_FromString extends SSViewer {
} }
public function process($item) { public function process($item) {
$template = SSViewer::parseTemplateContent($this->content); $template = SSViewer::parseTemplateContent($this->content, "string sha1=".sha1($this->content));
$tmpFile = tempnam(TEMP_FOLDER,""); $tmpFile = tempnam(TEMP_FOLDER,"");
$fh = fopen($tmpFile, 'w'); $fh = fopen($tmpFile, 'w');
@ -632,6 +638,9 @@ class SSViewer_FromString extends SSViewer {
$itemStack = array(); $itemStack = array();
$val = ""; $val = "";
$valStack = array();
$cache = Cache::factory('cacheblock');
include($tmpFile); include($tmpFile);
unlink($tmpFile); unlink($tmpFile);
@ -641,6 +650,104 @@ class SSViewer_FromString extends SSViewer {
} }
} }
/**
* 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 '<? if ($partial = $cache->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, '<? $cache->save($val); $val = array_pop($valStack) . $val; } ?>', $content);
}
}
function supressOutput() { function supressOutput() {
return ""; return "";

View File

@ -0,0 +1,72 @@
<?php
// Not actually a data object, we just want a ViewableData object that's just for us
class SSViewerCacheBlockTest_Model extends DataObject implements TestOnly {
function Test($arg = null) {
return $this;
}
function Foo() {
return 'Bar';
}
}
class SSViewerCacheBlockTest extends SapphireTest {
protected $extraDataObjects = array('SSViewerCacheBlockTest_Model');
protected $data = null;
protected function _reset($cacheOn = true) {
$this->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');
}
}