mirror of
https://github.com/silverstripe/silverstripe-framework
synced 2024-10-22 14:05:37 +02:00
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:
parent
956df37c2c
commit
2969a7ee44
@ -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,14 +358,15 @@ 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);
|
||||||
|
|
||||||
$output = $val;
|
$output = $val;
|
||||||
@ -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,7 +638,10 @@ 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);
|
||||||
|
|
||||||
@ -640,7 +649,105 @@ class SSViewer_FromString extends SSViewer {
|
|||||||
return $val;
|
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 '<? 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 "";
|
||||||
|
72
tests/SSViewerCacheBlockTest.php
Normal file
72
tests/SSViewerCacheBlockTest.php
Normal 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user