Refactory template ctor

Dev extends
This commit is contained in:
bzick 2013-02-13 20:51:27 +04:00
parent 95daa95cd2
commit 8c618c2b34
8 changed files with 260 additions and 136 deletions

View File

@ -94,7 +94,7 @@ class Aspect {
protected $_on_post_cmp = array();
/**
* @var Aspect\Provider
* @var ProviderInterface
*/
private $_provider;
/**
@ -107,7 +107,9 @@ class Aspect {
*/
protected $_modifiers = array(
"upper" => 'strtoupper',
"up" => 'strtoupper',
"lower" => 'strtolower',
"low" => 'strtolower',
"date_format" => 'Aspect\Modifier::dateFormat',
"date" => 'Aspect\Modifier::date',
"truncate" => 'Aspect\Modifier::truncate',
@ -123,9 +125,9 @@ class Aspect {
* @var array of allowed PHP functions
*/
protected $_allowed_funcs = array(
"empty" => 1, "isset" => 1, "count" => 1, "is_string" => 1, "is_array" => 1, "is_numeric" => 1, "is_int" => 1,
"count" => 1, "is_string" => 1, "is_array" => 1, "is_numeric" => 1, "is_int" => 1,
"is_object" => 1, "strtotime" => 1, "gettype" => 1, "is_double" => 1, "json_encode" => 1, "json_decode" => 1,
"ip2long" => 1, "long2ip" => 1, "strip_tags" => 1, "nl2br" => 1
"ip2long" => 1, "long2ip" => 1, "strip_tags" => 1, "nl2br" => 1, "explode" => 1, "implode" => 1
);
/**
@ -215,18 +217,25 @@ class Aspect {
);
/**
* Factory
* @param string $template_dir path to templates
* @param string $compile_dir path to compiled files
* @param int $options
* @param \Aspect\Provider $provider
* @return Aspect
*/
public static function factory($template_dir, $compile_dir, $options = 0, Aspect\Provider $provider = null) {
/**
* Just factory
*
* @param string|Aspect\ProviderInterface $source path to templates or custom provider
* @param string $compile_dir path to compiled files
* @param int $options
* @throws InvalidArgumentException
* @return Aspect
*/
public static function factory($source, $compile_dir = '/tmp', $options = 0) {
if(is_string($source)) {
$provider = new \Aspect\Provider\FS($source);
} elseif($source instanceof Aspect\ProviderInterface) {
$provider = $source;
} else {
throw new InvalidArgumentException("Source must be a valid path or provider object");
}
$aspect = new static($provider);
$aspect->setCompileDir($compile_dir);
$aspect->setTemplateDirs($template_dir);
if($options) {
$aspect->setOptions($options);
}
@ -234,10 +243,10 @@ class Aspect {
}
/**
* @param Aspect\Provider $provider
* @param Aspect\ProviderInterface $provider
*/
public function __construct(Aspect\Provider $provider = null) {
$this->_provider = $provider ?: new Aspect\Provider();
public function __construct(Aspect\ProviderInterface $provider) {
$this->_provider = $provider;
}
/**
@ -271,16 +280,6 @@ class Aspect {
return $this;
}
/**
* Set template directory
* @param string|array $dirs directory(s) of template sources
* @return Aspect
*/
public function setTemplateDirs($dirs) {
$this->_provider->setTemplateDirs($dirs);
return $this;
}
/**
*
* @param callable $cb
@ -446,19 +445,8 @@ class Aspect {
return $tags;
}
/**
* Add template directory
* @static
* @param string $dir
* @return \Aspect
* @throws \InvalidArgumentException
*/
public function addTemplateDir($dir) {
$this->_provider->addTemplateDir($dir);
return $this;
}
public function addProvider($scm, \Aspect\Provider $provider) {
public function addProvider($scm, \Aspect\ProviderInterface $provider) {
$this->_providers[$scm] = $provider;
}
@ -489,7 +477,7 @@ class Aspect {
/**
* @param bool|string $scm
* @return Aspect\Provider
* @return Aspect\ProviderInterface
* @throws InvalidArgumentException
*/
public function getProvider($scm = false) {
@ -504,6 +492,10 @@ class Aspect {
}
}
public function getRawTemplate() {
return new \Aspect\Template($this);
}
/**
* Execute template and write result into stdout
*
@ -556,7 +548,6 @@ class Aspect {
*/
public function addTemplate(Aspect\Render $template) {
$this->_storage[ $template->getName() ] = $template;
$template->setStorage($this);
}
/**
@ -572,9 +563,7 @@ class Aspect {
return $this->compile($tpl);
} else {
/** @var Aspect\Render $tpl */
$tpl = include($this->_compile_dir."/".$file_name);
$tpl->setStorage($this);
return $tpl;
return include($this->_compile_dir."/".$file_name);
}
}
@ -593,24 +582,23 @@ class Aspect {
* Compile and save template
*
* @param string $tpl
* @param bool $store
* @param bool $store store template on disk
* @throws RuntimeException
* @return \Aspect\Template
*/
public function compile($tpl, $store = true) {
$provider = $this->getProvider(strstr($tpl, ":", true));
$template = new Template($this, $provider->loadCode($tpl), $tpl);
$template = Template::factory($this)->load($tpl);
if($store) {
$tpl_tmp = tempnam($this->_compile_dir, basename($tpl));
$tpl_fp = fopen($tpl_tmp, "w");
if(!$tpl_fp) {
throw new \RuntimeException("Can not open temporary file $tpl_tmp. Directory ".$this->_compile_dir." is writable?");
throw new \RuntimeException("Can't to open temporary file $tpl_tmp. Directory ".$this->_compile_dir." is writable?");
}
fwrite($tpl_fp, $template->getTemplateCode());
fclose($tpl_fp);
$file_name = $this->_compile_dir."/".$this->_getHash($tpl);
if(!rename($tpl_tmp, $file_name)) {
throw new \RuntimeException("Can not to move $tpl_tmp to $tpl");
throw new \RuntimeException("Can't to move $tpl_tmp to $tpl");
}
}
return $template;
@ -654,7 +642,7 @@ class Aspect {
* @return Aspect\Template
*/
public function compileCode($code, $name = 'Runtime compile') {
return new Template($this, $code, $name);
return Template::factory($this)->source($name, $code);
}
}

View File

@ -357,16 +357,6 @@ class Compiler {
}
}
/**
* check if value is scalar, like "string", 2, 2.2, true, false, null
* @param string $value
* @return bool
* @todo add 'string' support
*/
public static function isScalar($value) {
return json_decode($value);
}
/**
* Dispatch {extends} tag
* @param Tokenizer $tokens
@ -378,18 +368,11 @@ class Compiler {
if(!empty($tpl->_extends)) {
throw new ImproperUseException("Only one {extends} allowed");
}
$p = $tpl->parseParams($tokens);
if(isset($p[0])) {
$tpl_name = $p[0];
} elseif (isset($p["file"])) {
$tpl_name = $p["file"];
} else {
throw new ImproperUseException("{extends} require 'file' parameter");
}
$tpl_name = $tpl->parseFirstArg($tokens, $name);
$tpl->addPostCompile(__CLASS__."::extendBody");
if($name = self::isScalar($tpl_name)) { // static extends
$tpl->_extends = $tpl->getStorage()->compile($name, false);
$tpl->addDepend($tpl->getStorage()->getTemplate($name)); // for valid compile-time need take template from storage
if($name) { // static extends
$tpl->_extends = $tpl->getStorage()->getRawTemplate()->load($name, false);
$tpl->addDepend($tpl->_extends); // for valid compile-time need take template from storage
return "/* Static extends */";
} else { // dynamic extends
$tpl->_extends = $tpl_name;
@ -443,9 +426,7 @@ class Compiler {
*/
public static function tagBlockOpen(Tokenizer $tokens, Scope $scope) {
$p = $scope->tpl->parseParams($tokens);
if(isset($p["name"])) {
$scope["name"] = $p["name"];
} elseif (isset($p[0])) {
if (isset($p[0])) {
$scope["name"] = $p[0];
} else {
throw new ImproperUseException("{block} must be named");
@ -476,7 +457,7 @@ class Compiler {
* @return string
*/
public static function tagBlockClose($tokens, Scope $scope) {
$scope->tpl->_blocks[ self::isScalar($scope["name"]) ] = substr($scope->tpl->getBody(), $scope["offset"]);
$scope->tpl->_blocks[ $scope["name"] ] = substr($scope->tpl->getBody(), $scope["offset"]);
if(isset($scope->tpl->_extends)) { // is child
if(is_object($scope->tpl->_extends)) { // static extends
return "";

View File

@ -6,6 +6,13 @@ use Aspect;
* Primitive template
*/
class Render extends \ArrayObject {
private static $_props = array(
"name" => "runtime",
"base_name" => "",
"scm" => false,
"time" => 0,
"depends" => array()
);
/**
* @var \Closure
*/
@ -14,7 +21,9 @@ class Render extends \ArrayObject {
* Template name
* @var string
*/
protected $_name = 'runtime template';
protected $_name = 'runtime';
protected $_scm = false;
protected $_base_name = 'runtime';
/**
* @var Aspect
*/
@ -28,24 +37,26 @@ class Render extends \ArrayObject {
protected $_depends = array();
/**
* @param string $name template name
* @param callable $code template body
* @param mixed $props signature
* Template provider
* @var ProviderInterface
*/
public function __construct($name, \Closure $code, $props = array()) {
$this->_name = $name;
$this->_code = $code;
$this->_time = isset($props["time"]) ? $props["time"] : microtime(true);
$this->_depends = isset($props["depends"]) ? $props["depends"] : array();
}
protected $_provider;
/**
* Set template storage
* @param Aspect $aspect
* @param callable $code template body
* @param array $props
*/
public function setStorage(Aspect $aspect) {
public function __construct(Aspect $aspect, \Closure $code, $props = array()) {
$this->_aspect = $aspect;
}
$props += self::$_props;
$this->_name = $props["name"];
$this->_provider = $this->_aspect->getProvider($props["scm"]);
$this->_scm = $props["scm"];
$this->_time = $props["time"];
$this->_depends = $props["depends"];
$this->_code = $code;
}
/**
* Get template storage
@ -55,11 +66,23 @@ class Render extends \ArrayObject {
return $this->_aspect;
}
public function getScm() {
return $this->_scm;
}
public function getProvider() {
return $this->_provider;
}
public function getBaseName() {
return $this->_base_name;
}
/**
* @return string
*/
public function __toString() {
return "Template({$this->_name})";
return $this->_name;
}
/**

View File

@ -53,27 +53,65 @@ class Template extends Render {
*/
public static $sysvar = array('$aspect' => 1, '$smarty' => 1);
/**
* @param Aspect $aspect Template storage
* @param string $code template source
* @param string $name template name
* @param bool $auto_compile
*/
public function __construct(Aspect $aspect, $code, $name = "runtime template", $auto_compile = true) {
$this->_src = $code;
$this->_name = $name;
$this->_aspect = $aspect;
$this->_options = $aspect->getOptions();
if($auto_compile) {
$this->compile();
}
public static function factory(Aspect $aspect) {
return new static($aspect);
}
/**
* @param Aspect $aspect Template storage
*/
public function __construct(Aspect $aspect) {
$this->_aspect = $aspect;
$this->_options = $this->_aspect->getOptions();
}
/**
* Load source from provider
* @param string $name
* @param bool $compile
* @return \Aspect\Template
*/
public function load($name, $compile = true) {
$this->_name = $name;
if($provider = strstr($name, ":", true)) {
$this->_scm = $provider;
$this->_base_name = substr($name, strlen($provider));
} else {
$this->_base_name = $name;
}
$this->_provider = $this->_aspect->getProvider($provider);
$this->_src = $this->_provider->getSource($name, $this->_time);
if($compile) {
$this->compile();
}
return $this;
}
/**
* Load custom source
* @param string $name template name
* @param string $src template source
* @param bool $compile
* @return \Aspect\Template
*/
public function source($name, $src, $compile = true) {
$this->_name = $name;
$this->_src = $src;
if($compile) {
$this->compile();
}
return $this;
}
/**
* Convert template to PHP code
*
* @throws CompileException
*/
public function compile() {
if(!isset($this->_src)) {
return;
}
$this->_time = microtime(true);
$pos = 0;
while(($start = strpos($this->_src, '{', $pos)) !== false) { // search open-char of tags
switch($this->_src[$start + 1]) { // check next char
@ -95,19 +133,20 @@ class Template extends Render {
$tag = substr($this->_src, $start, $end - $start + 1); // variable $tag contains aspect tag '{...}'
$this->_line += substr_count($this->_src, "\n", $this->_pos, $end - $start + 1); // count lines in $frag and $tag (using original text $code)
$pos = $this->_pos = $end + 1; // move search-pointer to end of the tag
if($this->_trim) { // if previous tag has trim flag
$frag = ltrim($frag);
}
//if($this->_trim) { // if previous tag has trim flag
// $frag = ltrim($frag);
//}
$this->_body .= str_replace("<?", '<?php echo "<?" ?>', $frag);
$tag = $this->_tag($tag, $this->_trim); // dispatching tags
if($this->_trim) { // if current tag has trim flag
$frag = rtrim($frag);
}
$this->_body .= str_replace("<?", '<?php echo "<?" ?>', $frag).$tag;
//if($this->_trim) { // if current tag has trim flag
// $frag = rtrim($frag);
//}
$this->_body .= $tag;
}
$this->_body .= substr($this->_src, $this->_pos);
$this->_body .= str_replace("<?", '<?php echo "<?" ?>', substr($this->_src, $this->_pos));
if($this->_stack) {
$_names = array();
$_line = 0;
@ -146,9 +185,11 @@ class Template extends Render {
public function getTemplateCode() {
return "<?php \n".
"/** Aspect template '".$this->_name."' compiled at ".date('Y-m-d H:i:s')." */\n".
"return new Aspect\\Render('{$this->_name}', ".$this->_getClosureCode().", ".var_export(array(
"options" => $this->_options,
//"provider" =>
"return new Aspect\\Render(\$this, ".$this->_getClosureSource().", ".var_export(array(
//"options" => $this->_options,
"provider" => $this->_scm,
"name" => $this->_name,
"base_name" => $this->_base_name,
"time" => $this->_time,
"depends" => $this->_depends
), true).");\n";
@ -158,7 +199,7 @@ class Template extends Render {
* Return closure code
* @return string
*/
private function _getClosureCode() {
private function _getClosureSource() {
return "function (\$tpl) {\n?>{$this->_body}<?php\n}";
}
@ -172,7 +213,7 @@ class Template extends Render {
public function display(array $values) {
if(!$this->_code) {
// evaluate template's code
eval("\$this->_code = ".$this->_getClosureCode().";");
eval("\$this->_code = ".$this->_getClosureSource().";");
if(!$this->_code) {
throw new CompileException("Fatal error while creating the template");
}
@ -186,6 +227,7 @@ class Template extends Render {
* @param Render $tpl
*/
public function addDepend(Render $tpl) {
$this->_depends[$tpl->getName()] = $tpl->getCompileTime();
}
@ -197,7 +239,7 @@ class Template extends Render {
*/
public function fetch(array $values) {
if(!$this->_code) {
eval("\$this->_code = ".$this->_getClosureCode().";");
eval("\$this->_code = ".$this->_getClosureSource().";");
if(!$this->_code) {
throw new CompileException("Fatal error while creating the template");
}
@ -245,9 +287,6 @@ class Template extends Render {
break;
default:
$code = $this->_parseAct($tokens);
if($code === null) {
}
break;
}
@ -823,6 +862,18 @@ class Template extends Render {
throw new TokenizeException("Unexpected token '".$tokens->current()."' in argument list");
}
public function parseFirstArg(Tokenizer $tokens, &$static) {
if($tokens->is(T_CONSTANT_ENCAPSED_STRING)) {
$str = $tokens->getAndNext();
$static = stripslashes(substr($str, 1, -1));
return $str;
} elseif($tokens->is(Tokenizer::MACRO_STRING)) {
return $static = $tokens->getAndNext();
} else {
return $this->parseExp($tokens, true);
}
}
/**
* Parse parameters as $key=$value
* param1=$var param2=3 ...

View File

@ -11,15 +11,19 @@ class RenderTest extends \PHPUnit_Framework_TestCase {
public static $render;
public static function setUpBeforeClass() {
self::$render = new Render("render.tpl", function ($tpl) {
echo "It is render function ".$tpl["render"];
}, array());
self::$render = new Render(Aspect::factory("."), function ($tpl) {
echo "It is render's function ".$tpl["render"];
}, array(
"name" => "render.tpl"
));
}
public function testCreate() {
$r = new Render("test.render.tpl", function () {
$r = new Render(Aspect::factory("."), function () {
echo "Test render";
}, array());
}, array(
"name" => "test.render.tpl"
));
$this->assertSame("Test render", $r->fetch(array()));
}
@ -27,22 +31,24 @@ class RenderTest extends \PHPUnit_Framework_TestCase {
ob_start();
self::$render->display(array("render" => "display"));
$out = ob_get_clean();
$this->assertSame("It is render function display", $out);
$this->assertSame("It is render's function display", $out);
}
public function testFetch() {
$this->assertSame("It is render function fetch", self::$render->fetch(array("render" => "fetch")));
$this->assertSame("It is render's function fetch", self::$render->fetch(array("render" => "fetch")));
}
/**
* @expectedException RuntimeException
* @expectedException \RuntimeException
* @expectedExceptionMessage template error
*/
public function testFetchException() {
$render = new Render("render.tpl", function ($tpl) {
$render = new Render(Aspect::factory("."), function () {
echo "error";
throw new \RuntimeException("template error");
});
}, array(
"name" => "render.tpl"
));
$render->fetch(array());
}

View File

@ -0,0 +1,72 @@
<?php
namespace Aspect\Template;
use Aspect, Aspect\Modifier;
class ExtendsTest extends \PHPUnit_Framework_TestCase {
/**
* @var Aspect
*/
public static $aspect;
public function setUp() {
if(!file_exists(ASPECT_RESOURCES.'/compile')) {
mkdir(ASPECT_RESOURCES.'/compile', 0777, true);
} else {
exec("rm -f ".ASPECT_RESOURCES.'/compile/*');
}
self::$aspect = Aspect::factory(ASPECT_RESOURCES.'/template', ASPECT_RESOURCES.'/compile');
}
public static function providerExtends() {
return array(
array('{extends "parent.tpl"}{block "bk1"} block1 {/block}', "Template extended by block1"),
array('{extends "parent.tpl"}{block "bk1"} block1 {/block}{block "bk2"} block2 {/block} garbage', "Template extended by block1"),
array('{extends "parent.tpl"}{block "bk1"} block1 {/block}{block "bk2"} block2 {/block} {block "bk3"} block3 {/block} garbage', "Template multi-extended by block1"),
array('{extends "parent.tpl"}{var $bk = "bk3"}{block "bk1"} block1 {/block}{block "bk2"} block2 {/block} {block "$bk"} block3 {/block} garbage', "Template multi-extended by block1"),
);
}
public function exec($code, $vars, $result, $dump = false) {
$tpl = self::$aspect->compileCode($code, "inline.tpl");
if($dump) {
echo "\n===========================\n".$code.": ".$tpl->getBody();
}
$this->assertSame(Modifier::strip($result), Modifier::strip($tpl->fetch($vars), true), "Test $code");
}
public function execError($code, $exception, $message, $options) {
self::$aspect->setOptions($options);
try {
self::$aspect->compileCode($code, "inline.tpl");
} catch(\Exception $e) {
$this->assertSame($exception, get_class($e), "Exception $code");
$this->assertStringStartsWith($message, $e->getMessage());
self::$aspect->setOptions(0);
return;
}
self::$aspect->setOptions(0);
$this->fail("Code $code must be invalid");
}
/**
* @group extends
*/
public function testParent() {
//echo(self::$aspect->getTemplate("parent.tpl")->getBody()); exit;
}
/**
* @group extends
*/
public function ___testChildLevel1() {
echo(self::$aspect->getTemplate("child1.tpl")->getBody()); exit;
}
/**
* @group extends
*/
public function __testExtends() {
echo(self::$aspect->fetch("child1.tpl", array("a" => "value", "z" => ""))."\n"); exit;
}
}

View File

@ -17,9 +17,11 @@ class TemplateTest extends \PHPUnit_Framework_TestCase {
exec("rm -f ".ASPECT_RESOURCES.'/compile/*');
}
self::$aspect = Aspect::factory(ASPECT_RESOURCES.'/template', ASPECT_RESOURCES.'/compile');
self::$aspect->addTemplate(new Render("welcome.tpl", function ($tpl) {
self::$aspect->addTemplate(new Render(self::$aspect, function ($tpl) {
echo "<b>Welcome, ".$tpl["username"]." (".$tpl["email"].")</b>";
}, array()));
}, array(
"name" => "welcome.tpl"
)));
}

View File

@ -21,7 +21,6 @@ class AspectTest extends \PHPUnit_Framework_TestCase {
self::tearDownAfterClass();
$this->aspect = $aspect = Aspect::factory(ASPECT_RESOURCES.'/template', ASPECT_RESOURCES.'/compile');
$aspect->setCompileDir(ASPECT_RESOURCES.'/compile');
$aspect->addTemplateDir(ASPECT_RESOURCES.'/template');
$aspect->setForceCompile(false);
$aspect->setCompileCheck(false);
}
@ -36,11 +35,13 @@ class AspectTest extends \PHPUnit_Framework_TestCase {
public function testAddRender() {
$test = $this;
$this->aspect->addTemplate(new Render('render.tpl', function($tpl) use ($test) {
$this->aspect->addTemplate(new Render($this->aspect, function($tpl) use ($test) {
/** @var \PHPUnit_Framework_TestCase $test */
$test->assertInstanceOf('Aspect\Render', $tpl);
echo "Inline render";
}, array()));
}, array(
"name" => 'render.tpl'
)));
$this->assertSame("Inline render", $this->aspect->fetch('render.tpl', array()));
}