diff --git a/src/Aspect.php b/src/Aspect.php index 513b13a..16c6fa0 100644 --- a/src/Aspect.php +++ b/src/Aspect.php @@ -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); } } diff --git a/src/Aspect/Compiler.php b/src/Aspect/Compiler.php index 3f754d1..0469f4c 100644 --- a/src/Aspect/Compiler.php +++ b/src/Aspect/Compiler.php @@ -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 ""; diff --git a/src/Aspect/Render.php b/src/Aspect/Render.php index 326da5a..5e11694 100644 --- a/src/Aspect/Render.php +++ b/src/Aspect/Render.php @@ -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; } /** diff --git a/src/Aspect/Template.php b/src/Aspect/Template.php index 0af6b91..990726a 100644 --- a/src/Aspect/Template.php +++ b/src/Aspect/Template.php @@ -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("', $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("', $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("', substr($this->_src, $this->_pos)); if($this->_stack) { $_names = array(); $_line = 0; @@ -146,9 +185,11 @@ class Template extends Render { public function getTemplateCode() { return "_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}_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 ... diff --git a/tests/cases/Aspect/RenderTest.php b/tests/cases/Aspect/RenderTest.php index e214504..bcd6200 100644 --- a/tests/cases/Aspect/RenderTest.php +++ b/tests/cases/Aspect/RenderTest.php @@ -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()); } diff --git a/tests/cases/Aspect/Template/ExtendsTest.php b/tests/cases/Aspect/Template/ExtendsTest.php new file mode 100644 index 0000000..3e877f7 --- /dev/null +++ b/tests/cases/Aspect/Template/ExtendsTest.php @@ -0,0 +1,72 @@ +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; + } +} + diff --git a/tests/cases/Aspect/TemplateTest.php b/tests/cases/Aspect/TemplateTest.php index dd90561..620a6fc 100644 --- a/tests/cases/Aspect/TemplateTest.php +++ b/tests/cases/Aspect/TemplateTest.php @@ -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 "Welcome, ".$tpl["username"]." (".$tpl["email"].")"; - }, array())); + }, array( + "name" => "welcome.tpl" + ))); } diff --git a/tests/cases/AspectTest.php b/tests/cases/AspectTest.php index 0b67b77..ce26b68 100644 --- a/tests/cases/AspectTest.php +++ b/tests/cases/AspectTest.php @@ -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())); }