Re-organized rendering (read source / compile / cache) process to avoid circular dependencies.

This commit is contained in:
Simon Wisselink
2023-01-26 12:58:15 +01:00
parent 047b73d4a1
commit c47756b489
11 changed files with 234 additions and 259 deletions

View File

@@ -67,25 +67,6 @@ abstract class Base
*/
abstract public function retrieveCachedContent(Template $_template);
/**
* Return cached content
*
* @param Template $_template template object
*
* @return null|string
* @throws Exception
*/
public function getCachedContent(Template $_template)
{
if ($this->process($_template)) {
ob_start();
$unifunc = $_template->getCached()->unifunc;
$unifunc($_template);
return ob_get_clean();
}
return null;
}
/**
* Empty cache
*

View File

@@ -186,69 +186,4 @@ class IncludeTag extends Base {
return $_output;
}
/**
* Compile inline sub template
*
* @param \Smarty\Compiler\Template $compiler
* @param \Smarty\Template $tpl
* @param string $t_hash
*
* @return bool
* @throws \Exception
* @throws \Smarty\Exception
*/
private function compileInlineTemplate(
Template $compiler,
\Smarty\Template $tpl,
$t_hash
) {
$tplSource = $tpl->getSource();
$uid = $tplSource->type . $tplSource->uid;
if ($tplSource->exists) {
$compiler->getParentCompiler()->mergedSubTemplatesData[$uid][$t_hash]['uid'] = $tplSource->uid;
$tpl->getCompiled(true)->nocache_hash = $compiler->getParentCompiler()->getTemplate()->getCompiled()->nocache_hash;
// save unique function name
$compiler->getParentCompiler()->mergedSubTemplatesData[$uid][$t_hash]['func'] =
$tpl->getCompiled()->unifunc = 'content_' . str_replace(['.', ','], '_', uniqid('', true));
// make sure whole chain gets compiled
$tpl->mustCompile = true;
$compiler->getParentCompiler()->mergedSubTemplatesData[$uid][$t_hash]['nocache_hash'] =
$tpl->getCompiled()->nocache_hash;
if ($tplSource->type === 'file') {
$sourceInfo = $tplSource->filepath;
} else {
$basename = $tplSource->getBasename();
$sourceInfo = $tplSource->type . ':' .
($basename ? $basename : $tplSource->name);
}
// get compiled code
$compiled_code = "<?php\n\n";
$compiled_code .= $compiler->cStyleComment(" Start inline template \"{$sourceInfo}\" =============================") . "\n";
$compiled_code .= "function {$tpl->getCompiled()->unifunc} (\\Smarty\\Template \$_smarty_tpl) {\n";
$compiled_code .= "?>\n" . $tpl->getCompiler()->compileTemplateSource($tpl, $compiler->getParentCompiler());
$compiled_code .= "<?php\n";
$compiled_code .= "}\n?>\n";
$compiled_code .= $tpl->getSmarty()->runPostFilters($tpl->getCompiler()->blockOrFunctionCode, $tpl);
$compiled_code .= "<?php\n\n";
$compiled_code .= $compiler->cStyleComment(" End inline template \"{$sourceInfo}\" =============================") . "\n";
$compiled_code .= '?>';
// unset($tpl->compiler); // @TODO removed this.
if ($tpl->getCompiled()->getNocacheCode()) {
// replace nocache_hash
$compiled_code =
str_replace(
"{$tpl->getCompiled()->nocache_hash}",
$compiler->getTemplate()->getCompiled()->nocache_hash,
$compiled_code
);
$compiler->getTemplate()->getCompiled()->setNocacheCode(true);
}
$compiler->getParentCompiler()->mergedSubTemplatesCode[$tpl->getCompiled()->unifunc] = $compiled_code;
return true;
} else {
return false;
}
}
}

View File

@@ -62,7 +62,8 @@ class CodeFrame
str_replace('*/', '* /', $this->_template->getSource()->filepath)
);
$output .= "/* @var \\Smarty\\Template \$_smarty_tpl */\n";
$dec = "\$_smarty_tpl->isFresh(" . var_export($properties, true) . ',' . ($cache ? 'true' : 'false') . ')';
$dec = "\$_smarty_tpl->" . ($cache ? "getCached()" : "getCompiled()");
$dec .= "->isFresh(\$_smarty_tpl, " . var_export($properties, true) . ')';
$output .= "if ({$dec}) {\n";
$output .= "function {$properties['unifunc']} (\\Smarty\\Template \$_smarty_tpl) {\n";
if (!$cache && !empty($compiler->tpl_function)) {

View File

@@ -352,7 +352,7 @@ class Template extends BaseCompiler {
*
* @param \Smarty\Template $template template object to compile
*
* @return bool true if compiling succeeded, false if it failed
* @return string code
* @throws Exception
*/
public function compileTemplate(\Smarty\Template $template) {

View File

@@ -203,7 +203,6 @@ class Debug extends Data
$debObj = new \Smarty\Smarty();
// copy the working dirs from application
$debObj->setCompileDir($smarty->getCompileDir());
$debObj->force_compile = false;
$debObj->compile_check = \Smarty::COMPILECHECK_ON;
$debObj->security_policy = null;
$debObj->debugging = false;

View File

@@ -36,35 +36,6 @@ abstract class RecompiledPlugin extends BasePlugin {
return false;
}
/**
* compile template from source
*
* @param Template $_smarty_tpl do not change variable name, is used by compiled template
*
* @throws Exception
*/
public function recompile(Template $_smarty_tpl) {
$compiled = $_smarty_tpl->getCompiled();
$compiled->file_dependency = [];
$compiled->includes = [];
$compiled->nocache_hash = null;
$compiled->unifunc = null;
$level = ob_get_level();
ob_start();
// call compiler
try {
eval('?>' . $_smarty_tpl->getCompiler()->compileTemplate($_smarty_tpl));
} catch (\Exception $e) {
while (ob_get_level() > $level) {
ob_end_clean();
}
throw $e;
}
ob_get_clean();
$compiled->timestamp = time();
$compiled->exists = true;
}
/*
* Disable timestamp checks for recompiled resource.
*

View File

@@ -56,13 +56,6 @@ class Template extends TemplateBase {
*/
public $template_resource = null;
/**
* flag if compiled template is invalid and must be (re)compiled
*
* @var bool
*/
public $mustCompile = null;
/**
* Template ID
*
@@ -309,88 +302,16 @@ class Template extends TemplateBase {
return parent::assign($tpl_var, $value, $nocache, $scope);
}
/**
* This function is executed automatically when a compiled or cached template file is included
* - Decode saved properties from compiled template and cache files
* - Check if compiled or cache file is valid
*
* @param array $properties special template properties
* @param bool $cache flag if called from cache file
*
* @return bool flag if compiled or cache file is valid
* @throws \Smarty\Exception
*/
public function isFresh($properties, $cache = false) {
// on cache resources other than file check version stored in cache code
if (!isset($properties['version']) || \Smarty\Smarty::SMARTY_VERSION !== $properties['version']) {
if ($cache) {
$this->getSmarty()->clearAllCache();
} else {
$this->getSmarty()->clearCompiledTemplate();
}
return false;
}
$is_valid = true;
if (!empty($properties['file_dependency'])
&& ((!$cache && $this->compile_check) || $this->compile_check === \Smarty\Smarty::COMPILECHECK_ON)
) {
// check file dependencies at compiled code
foreach ($properties['file_dependency'] as $_file_to_check) {
if ($_file_to_check[2] === 'file' || $_file_to_check[2] === 'php') {
if ($this->getSource()->filepath === $_file_to_check[0]) {
// do not recheck current template
continue;
//$mtime = $this->getSource()->getTimeStamp();
} else {
// file and php types can be checked without loading the respective resource handlers
$mtime = is_file($_file_to_check[0]) ? filemtime($_file_to_check[0]) : false;
}
} else {
$handler = \Smarty\Resource\BasePlugin::load($this->getSmarty(), $_file_to_check[2]);
if ($handler->checkTimestamps()) {
$source = Source::load($this, $this->getSmarty(), $_file_to_check[0]);
$mtime = $source->getTimeStamp();
} else {
continue;
}
}
if ($mtime === false || $mtime > $_file_to_check[1]) {
$is_valid = false;
break;
}
}
}
if ($cache) {
// CACHING_LIFETIME_SAVED cache expiry has to be validated here since otherwise we'd define the unifunc
if ($this->caching === \Smarty\Smarty::CACHING_LIFETIME_SAVED && $properties['cache_lifetime'] >= 0
&& (time() > ($this->getCached()->timestamp + $properties['cache_lifetime']))
) {
$is_valid = false;
}
$this->getCached()->cache_lifetime = $properties['cache_lifetime'];
$this->getCached()->setValid($is_valid);
$generatedFile = $this->getCached();
} else {
$this->mustCompile = !$is_valid;
$generatedFile = $this->getCompiled();
$generatedFile->includes = $properties['includes'] ?? [];
}
if ($is_valid) {
$generatedFile->unifunc = $properties['unifunc'];
$generatedFile->setNocacheCode($properties['has_nocache_code']);
$generatedFile->file_dependency = $properties['file_dependency'];
}
return $is_valid && !function_exists($properties['unifunc']);
}
/**
* Compiles the template
* If the template is not evaluated the compiled template is saved on disk
*
* @TODO only used in compileAll and 1 unit test: can we move this and make compileAndWrite private?
*
* @throws \Exception
*/
public function compileTemplateSource() {
return $this->getCompiled()->compileTemplateSource($this);
return $this->getCompiled()->compileAndWrite($this);
}
/**
@@ -400,7 +321,7 @@ class Template extends TemplateBase {
* @throws Exception
*/
public function getCachedContent() {
return $this->getCached()->handler->getCachedContent($this);
return $this->getCached()->getContent($this);
}
/**
@@ -547,7 +468,7 @@ class Template extends TemplateBase {
* @return bool
* @throws \Smarty\Exception
*/
public function mustCompile() {
public function mustCompile(): bool {
if (!$this->getSource()->exists) {
if ($this->_isSubTpl()) {
$parent_resource = " in '{$this->parent->template_resource}'";
@@ -556,14 +477,13 @@ class Template extends TemplateBase {
}
throw new Exception("Unable to load template {$this->getSource()->type} '{$this->getSource()->name}'{$parent_resource}");
}
if ($this->mustCompile === null) {
$this->mustCompile = $this->smarty->force_compile
// @TODO move this logic to Compiled
return $this->smarty->force_compile
|| $this->getSource()->handler->recompiled
|| !$this->getCompiled()->exists
|| ($this->compile_check && $this->getCompiled()->getTimeStamp() < $this->getSource()->getTimeStamp());
}
return $this->mustCompile;
}
private function getCodeFrameCompiler(): Compiler\CodeFrame {
return new \Smarty\Compiler\CodeFrame($this);

View File

@@ -216,11 +216,7 @@ class Cached extends GeneratedPhpFile {
if ($this->handler->process($_template, $this, $update) === false) {
$this->valid = false;
}
if ($this->valid) {
$this->processed = true;
} else {
$this->processed = false;
}
$this->processed = $this->valid;
}
/**
@@ -287,10 +283,8 @@ class Cached extends GeneratedPhpFile {
$this->removeNoCacheHash($_template, $no_output_filter);
$compile_check = (int)$_template->compile_check;
$_template->compile_check = \Smarty\Smarty::COMPILECHECK_OFF;
if ($_template->_isSubTpl()) {
// @TODO why is this needed?
$_template->getCompiled()->unifunc = $_template->parent->getCompiled()->unifunc;
}
@@ -298,8 +292,6 @@ class Cached extends GeneratedPhpFile {
$this->process($_template, true);
}
$_template->compile_check = $compile_check;
if ($_template->getSmarty()->debugging) {
$_template->getSmarty()->getDebug()->end_cache($_template);
}
@@ -386,4 +378,60 @@ class Cached extends GeneratedPhpFile {
$this->source = $source;
}
/**
* Returns the generated content
*
* @param Template $template
*
* @return string|null
* @throws \Exception
*/
public function getContent(Template $template) {
ob_start();
$this->render($template);
return ob_get_clean();
}
/**
* This function is executed automatically when a generated file is included
* - Decode saved properties
* - Check if file is valid
*
* @param Template $_template
* @param array $properties special template properties
*
* @return bool flag if compiled or cache file is valid
* @throws Exception
*/
public function isFresh(Template $_template, array $properties): bool {
// on cache resources other than file check version stored in cache code
if (\Smarty\Smarty::SMARTY_VERSION !== $properties['version']) {
return false;
}
$is_valid = true;
if (!empty($properties['file_dependency']) && ($_template->compile_check === \Smarty\Smarty::COMPILECHECK_ON)) {
$is_valid = $this->checkFileDependencies($properties['file_dependency'], $_template);
}
// CACHING_LIFETIME_SAVED cache expiry has to be validated here since otherwise we'd define the unifunc
if ($_template->caching === \Smarty\Smarty::CACHING_LIFETIME_SAVED && $properties['cache_lifetime'] >= 0
&& (time() > ($this->timestamp + $properties['cache_lifetime']))
) {
$is_valid = false;
}
$this->cache_lifetime = $properties['cache_lifetime'];
$this->setValid($is_valid);
if ($is_valid) {
$this->unifunc = $properties['unifunc'];
$this->setNocacheCode($properties['has_nocache_code']);
$this->file_dependency = $properties['file_dependency'];
}
return $is_valid && !function_exists($properties['unifunc']);
}
}

View File

@@ -2,6 +2,7 @@
namespace Smarty\Template;
use Smarty\Exception;
use Smarty\Template;
/**
@@ -25,6 +26,10 @@ class Compiled extends GeneratedPhpFile {
* @var int[]
*/
public $includes = [];
/**
* @var bool
*/
private $isValid = false;
/**
* get a Compiled Object of this source
@@ -103,14 +108,17 @@ class Compiled extends GeneratedPhpFile {
$this->compileAndLoad($_template);
}
// @TODO Can't Cached handle this? Maybe introduce an event to decouple.
$_template->getCached()->file_dependency =
array_merge($_template->getCached()->file_dependency, $this->file_dependency);
$this->getRenderedTemplateCode($_template, $this->unifunc);
// @TODO Can't Cached handle this? Maybe introduce an event to decouple and remove the $_template->caching property.
if ($_template->caching && $this->getNocacheCode()) {
$_template->getCached()->hashes[$this->nocache_hash] = true;
}
if ($_template->getSmarty()->debugging) {
$_template->getSmarty()->getDebug()->end_render($_template);
}
@@ -124,26 +132,48 @@ class Compiled extends GeneratedPhpFile {
* @throws Exception
*/
private function compileAndLoad(Template $_smarty_tpl) {
$source = $_smarty_tpl->getSource();
$smarty = $_smarty_tpl->getSmarty();
if ($source->handler->recompiled) {
$source->handler->recompile($_smarty_tpl); // @TODO who is compiling here?
} else {
if (!$this->exists || $smarty->force_compile
|| ($_smarty_tpl->compile_check && $source->getTimeStamp() > $this->getTimeStamp())
if ($_smarty_tpl->getSource()->handler->recompiled) {
$this->recompile($_smarty_tpl);
return;
}
if ($this->exists && !$_smarty_tpl->getSmarty()->force_compile
&& !($_smarty_tpl->compile_check && $_smarty_tpl->getSource()->getTimeStamp() > $this->getTimeStamp())
) {
$this->compileTemplateSource($_smarty_tpl);
$this->loadCompiledTemplate($_smarty_tpl);
} else {
$_smarty_tpl->mustCompile = true;
@include $this->filepath;
if ($_smarty_tpl->mustCompile) {
$this->compileTemplateSource($_smarty_tpl);
$this->loadCompiledTemplate($_smarty_tpl);
}
if (!$this->isValid) {
$this->compileAndWrite($_smarty_tpl);
$this->loadCompiledTemplate($_smarty_tpl);
}
$this->processed = true;
}
/**
* compile template from source
*
* @param Template $_smarty_tpl do not change variable name, is used by compiled template
*
* @throws Exception
*/
private function recompile(Template $_smarty_tpl) {
$level = ob_get_level();
ob_start();
// call compiler
try {
eval('?>' . $this->doCompile($_smarty_tpl));
} catch (\Exception $e) {
while (ob_get_level() > $level) {
ob_end_clean();
}
throw $e;
}
ob_get_clean();
$this->timestamp = time();
$this->exists = true;
}
/**
@@ -153,11 +183,7 @@ class Compiled extends GeneratedPhpFile {
*
* @throws Exception
*/
public function compileTemplateSource(Template $_template) {
$this->file_dependency = [];
$this->includes = [];
$this->nocache_hash = null;
$this->unifunc = null;
public function compileAndWrite(Template $_template) {
// compile locking
if ($saved_timestamp = (!$_template->getSource()->handler->recompiled && is_file($this->filepath))) {
$saved_timestamp = $this->getTimeStamp();
@@ -166,7 +192,7 @@ class Compiled extends GeneratedPhpFile {
// compile locking
try {
// call compiler
$this->write($_template, $_template->getCompiler()->compileTemplate($_template));
$this->write($_template, $this->doCompile($_template));
} catch (\Exception $e) {
// restore old timestamp in case of error
if ($saved_timestamp && is_file($this->filepath)) {
@@ -176,6 +202,22 @@ class Compiled extends GeneratedPhpFile {
}
}
/**
* Do the actual compiling.
*
* @param Template $_smarty_tpl
*
* @return string
* @throws Exception
*/
private function doCompile(Template $_smarty_tpl): string {
$this->file_dependency = [];
$this->includes = [];
$this->nocache_hash = null;
$this->unifunc = null;
return $_smarty_tpl->getCompiler()->compileTemplate($_smarty_tpl);
}
/**
* Write compiled code by handler
*
@@ -203,11 +245,11 @@ class Compiled extends GeneratedPhpFile {
* Load fresh compiled template by including the PHP file
* HHVM requires a workaround because of a PHP incompatibility
*
* @param \Smarty\Template $_smarty_tpl do not change variable name, is used by compiled template
* @param Template $_smarty_tpl do not change/remove variable name, is used by compiled template
*
*/
private function loadCompiledTemplate(Template $_smarty_tpl) {
$compileCheck = $_smarty_tpl->compile_check;
$_smarty_tpl->compile_check = \Smarty\Smarty::COMPILECHECK_OFF;
if (function_exists('opcache_invalidate')
&& (!function_exists('ini_get') || strlen(ini_get("opcache.restrict_api")) < 1)
) {
@@ -220,7 +262,41 @@ class Compiled extends GeneratedPhpFile {
} else {
include $this->filepath;
}
$_smarty_tpl->compile_check = $compileCheck;
}
/**
* This function is executed automatically when a compiled or cached template file is included
* - Decode saved properties from compiled template and cache files
* - Check if compiled or cache file is valid
*
* @param Template $_template
* @param array $properties special template properties
*
* @return bool flag if compiled or cache file is valid
* @throws Exception
*/
public function isFresh(Template $_template, array $properties): bool {
// on cache resources other than file check version stored in cache code
if (\Smarty\Smarty::SMARTY_VERSION !== $properties['version']) {
return false;
}
$is_valid = true;
if (!empty($properties['file_dependency']) && $_template->compile_check) {
$is_valid = $this->checkFileDependencies($properties['file_dependency'], $_template);
}
$this->isValid = $is_valid;
$this->includes = $properties['includes'] ?? [];
if ($is_valid) {
$this->unifunc = $properties['unifunc'];
$this->setNocacheCode($properties['has_nocache_code']);
$this->file_dependency = $properties['file_dependency'];
}
return $is_valid && !function_exists($properties['unifunc']);
}
}

View File

@@ -2,8 +2,12 @@
namespace Smarty\Template;
use Smarty\Exception;
use Smarty\Template;
/**
* Base class for generated PHP files, such as compiled and cached versions of templates and config files.
*
* @author Rodney Rehm
*/
abstract class GeneratedPhpFile {
@@ -113,4 +117,37 @@ abstract class GeneratedPhpFile {
}
}
/**
* @param $file_dependency
* @param Template $_template
*
* @return bool
* @throws Exception
*/
protected function checkFileDependencies($file_dependency, Template $_template): bool {
// check file dependencies at compiled code
foreach ($file_dependency as $_file_to_check) {
if ($_file_to_check[2] === 'file') {
if ($_template->getSource()->filepath === $_file_to_check[0]) {
// do not recheck current template
continue;
}
// file and php types can be checked without loading the respective resource handlers
$mtime = is_file($_file_to_check[0]) ? filemtime($_file_to_check[0]) : false;
} else {
$handler = \Smarty\Resource\BasePlugin::load($_template->getSmarty(), $_file_to_check[2]);
if ($handler->checkTimestamps()) {
$source = Source::load($_template, $_template->getSmarty(), $_file_to_check[0]);
$mtime = $source->getTimeStamp();
} else {
continue;
}
}
if ($mtime === false || $mtime > $_file_to_check[1]) {
return false;
}
}
return true;
}
}

View File

@@ -353,25 +353,32 @@ abstract class CacheResourceTestCommon extends PHPUnit_Smarty
$this->smarty->clearAllCache();
// create and cache templates
$tpl = $this->smarty->createTemplate('helloworld.tpl');
$tpl->writeCachedContent('hello world');
$tpl->writeCachedContent('something else 1');
$tpl2 = $this->smarty->createTemplate('helloworld.tpl', null, 'bar');
$tpl2->writeCachedContent('hello world');
$tpl2->writeCachedContent('something else 2');
$tpl3 = $this->smarty->createTemplate('helloworld.tpl', 'buh|blar');
$tpl3->writeCachedContent('hello world');
$tpl3->writeCachedContent('something else 3');
// test cached content
$this->assertEquals('something else 1', $tpl->getCachedContent());
$this->assertEquals('something else 2', $tpl2->getCachedContent());
$this->assertEquals('something else 3', $tpl3->getCachedContent());
sleep(10);
$tpl4 = $this->smarty->createTemplate('helloworld2.tpl');
$tpl4->writeCachedContent('something else 4');
// test number of deleted caches
$this->doClearCacheAssertion(3,$this->smarty->clearAllCache(5));
$tpl = $this->smarty->createTemplate('helloworld.tpl');
$tpl2 = $this->smarty->createTemplate('helloworld.tpl', null, 'bar');
$tpl3 = $this->smarty->createTemplate('helloworld.tpl', 'buh|blar');
$tpl4 = $this->smarty->createTemplate('helloworld2.tpl');
// test that caches are deleted properly
$this->assertEquals('hello world', $tpl->getCachedContent());
$this->assertEquals('hello world', $tpl2->getCachedContent());
$this->assertEquals('hello world', $tpl3->getCachedContent());
sleep(10);
$tpl4 = $this->smarty->createTemplate('helloworld2.tpl');
$tpl4->writeCachedContent('hello world');
// test number of deleted caches
$this->doClearCacheAssertion(3,$this->smarty->clearAllCache(5));
// test that caches are deleted properly
$this->assertNull($tpl->getCachedContent());
$this->assertNull($tpl2->getCachedContent());
$this->assertNull($tpl3->getCachedContent());
$this->assertEquals('hello world', $tpl4->getCachedContent());
$this->assertEquals('something else 4', $tpl4->getCachedContent());
}
public function testClearCacheCacheFileSub()