From 06b7fa488bda1748e1317c7f664930f4a384a2be Mon Sep 17 00:00:00 2001 From: Ivan Shalganov Date: Thu, 7 Feb 2013 17:37:16 +0400 Subject: [PATCH] Dev, dev and dev. Also, docs, docs and docs. --- .gitignore | 6 +- .travis.yml | 7 + README.md | 21 +- benchmark/aspect/benchmark/compile.aspect.php | 22 + .../aspect/benchmark/compile.smarty3.php | 23 + benchmark/aspect/benchmark/data.php | 87 ++++ benchmark/aspect/benchmark/execute.aspect.php | 25 ++ benchmark/aspect/benchmark/execute.native.php | 23 + .../benchmark/execute.simple.aspect.php | 25 ++ .../benchmark/execute.simple.smarty3.php | 29 ++ .../aspect/benchmark/execute.smarty3.php | 29 ++ benchmark/aspect/benchmark/foreach.last.php | 47 ++ benchmark/aspect/templates/fullsyntax.tpl | 94 ++++ benchmark/aspect/templates/simple.tpl | 17 + benchmark/aspect/templates/subdir/subtpl.php | 18 + benchmark/aspect/templates/subdir/subtpl.tpl | 9 + benchmark/aspect/templates/syntax.php | 184 ++++++++ benchmark/aspect/templates/syntax.tpl | 179 ++++++++ benchmark/aspect/test.php | 103 +++++ benchmark/run.php | 19 +- benchmark/scripts/bootstrap.php | 68 +++ benchmark/scripts/run.php | 15 + benchmark/templates/echo.php | 19 +- benchmark/templates/foreach.php | 2 +- benchmark/templates/inheritance.php | 2 +- composer.json | 4 +- docs/about.md | 3 + docs/ext/mods.md | 11 + docs/ext/tags.md | 20 + docs/main.md | 43 ++ docs/{ => mods}/modifiers.md | 0 docs/settings.md | 48 +++ docs/{intro.md => syntax.md} | 78 ---- src/Aspect.php | 400 +++++++++--------- src/Aspect/Compiler.php | 255 ++++++++--- src/Aspect/Misc.php | 2 +- src/Aspect/Provider.php | 78 ++++ src/Aspect/ProviderInterface.php | 28 ++ src/Aspect/Render.php | 40 +- src/Aspect/Template.php | 121 ++++-- src/Aspect/Tokenizer.php | 280 ++++++------ tests/cases/Aspect/RenderTest.php | 4 +- tests/cases/Aspect/TemplateTest.php | 172 ++++++-- tests/cases/Aspect/TokenizerTest.php | 2 +- tests/cases/AspectTest.php | 17 +- tests/resources/template/child1.tpl | 4 + tests/resources/template/child2.tpl | 4 + tests/resources/template/child3.tpl | 9 + tests/resources/template/parent.tpl | 4 + 49 files changed, 2092 insertions(+), 608 deletions(-) create mode 100644 .travis.yml create mode 100644 benchmark/aspect/benchmark/compile.aspect.php create mode 100644 benchmark/aspect/benchmark/compile.smarty3.php create mode 100644 benchmark/aspect/benchmark/data.php create mode 100644 benchmark/aspect/benchmark/execute.aspect.php create mode 100644 benchmark/aspect/benchmark/execute.native.php create mode 100644 benchmark/aspect/benchmark/execute.simple.aspect.php create mode 100644 benchmark/aspect/benchmark/execute.simple.smarty3.php create mode 100644 benchmark/aspect/benchmark/execute.smarty3.php create mode 100644 benchmark/aspect/benchmark/foreach.last.php create mode 100644 benchmark/aspect/templates/fullsyntax.tpl create mode 100644 benchmark/aspect/templates/simple.tpl create mode 100644 benchmark/aspect/templates/subdir/subtpl.php create mode 100644 benchmark/aspect/templates/subdir/subtpl.tpl create mode 100644 benchmark/aspect/templates/syntax.php create mode 100644 benchmark/aspect/templates/syntax.tpl create mode 100644 benchmark/aspect/test.php create mode 100644 benchmark/scripts/bootstrap.php create mode 100644 benchmark/scripts/run.php create mode 100644 docs/about.md create mode 100644 docs/ext/mods.md create mode 100644 docs/ext/tags.md create mode 100644 docs/main.md rename docs/{ => mods}/modifiers.md (100%) create mode 100644 docs/settings.md rename docs/{intro.md => syntax.md} (68%) create mode 100644 src/Aspect/Provider.php create mode 100644 src/Aspect/ProviderInterface.php create mode 100644 tests/resources/template/child1.tpl create mode 100644 tests/resources/template/child2.tpl create mode 100644 tests/resources/template/child3.tpl create mode 100644 tests/resources/template/parent.tpl diff --git a/.gitignore b/.gitignore index 0cfbc65..012b995 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ .idea vendor composer.lock -tests/resources/compile/* \ No newline at end of file +composer.phar +tests/resources/compile/* +benchmark/compile/* +benchmark/templates/inheritance/smarty +benchmark/templates/inheritance/twig \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..7dc5540 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: php + +php: + - 5.3 + - 5.4 + +script: phpunit \ No newline at end of file diff --git a/README.md b/README.md index 0739937..c6dad0f 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ -Aspect Template Engine -================ +Aspect PHP Template Engine +========================== -Templates looks like Smarty: +## [About](./docs/about.md) | [Documentation](./docs/main.md) | [Benchmark](./docs/benchmark.md) | [Bugs](https://github.com/bzick/aspect/issues) | [Articles](./docs/articles.md) + +Smarty-like syntax: ```smarty @@ -9,11 +11,13 @@ Templates looks like Smarty: Aspect - {if $user?} {* or {if !empty($user)} *} -
User name: {$user.name}
+ {if $templaters.aspect?} + {var $tpl = $templaters.aspect} +
Name: {$tpl.name}
+
Description: {$tpl.name|truncate:80}
{/if} @@ -29,7 +33,7 @@ $aspect = Aspect::factory('./templates', './compiled', Aspect::CHECK_MTIME); $aspect->display("pages/about.tpl", $data); ``` -Fetch template's result^ +Get content ```php compileCode('Hello {$user.name}! {if $user.email?} Your email: {$user.email} {/if}'); $tempate->display($data); +// or $content = $tempate->fetch($data); ``` diff --git a/benchmark/aspect/benchmark/compile.aspect.php b/benchmark/aspect/benchmark/compile.aspect.php new file mode 100644 index 0000000..43d3213 --- /dev/null +++ b/benchmark/aspect/benchmark/compile.aspect.php @@ -0,0 +1,22 @@ + \ No newline at end of file diff --git a/benchmark/aspect/benchmark/compile.smarty3.php b/benchmark/aspect/benchmark/compile.smarty3.php new file mode 100644 index 0000000..538a5a1 --- /dev/null +++ b/benchmark/aspect/benchmark/compile.smarty3.php @@ -0,0 +1,23 @@ +addTemplateDir(__DIR__.'/../templates'); +$smarty->setCompileDir(__DIR__.'/../compiled'); + +$t = microtime(1); +$tpl = $smarty->createTemplate('syntax.tpl'); +/* @var Smarty_Internal_Template $tpl */ +$tpl->compileTemplateSource(); +$t = microtime(1) - $t; +var_dump("First compile: ".$t); + +$t = microtime(1); +$tpl = $smarty->createTemplate('syntax.tpl'); +/* @var Smarty_Internal_Template $tpl */ +$tpl->compileTemplateSource(); +$t = microtime(1) - $t; +var_dump("Second compile: ".$t); + +var_dump("Pick memory: ".memory_get_peak_usage()); \ No newline at end of file diff --git a/benchmark/aspect/benchmark/data.php b/benchmark/aspect/benchmark/data.php new file mode 100644 index 0000000..78cf3a8 --- /dev/null +++ b/benchmark/aspect/benchmark/data.php @@ -0,0 +1,87 @@ + "syntax test page", + + "user" => array( + "email" => 'bzick@megagroup.ru', + "name" => 'Ivan' + ), + + "data" => array( + 1 => array( + "foo" => "data.1.foo value" + ), + 2 => 4, + 4 => "data.four value", + 5 => array( + "bar" => "data.5.baz value", + "baz" => array( + "foo" => "data.5.baz.foo value" + ) + ), + 6 => array( + "foo" => "data.6.foo value" + ), + "bar" => "data.bar value", + "barz" => "data.barz value", + "baz_key" => "baz", + "foo" => array( + "bar" => "data.foo.baz value", + "baz" => array( + "foo" => "data.foo.baz.foo value", + "ls" => array( + 4 => "data.foo.baz.ls.4 value", + 5 => 555 + ) + ) + ), + "obj" => new obj(), + "tpl_name" => 'subdir/subtpl' + ), + + "foo_key" => "foo", + "bar_key" => "bar", + "barz_key" => "barz", + "baz_key" => "baz", + + "item" => "some item", + "x" => 1, + "y" => 2, + "tpl_name" => "subtpl", + + "contacts" => array( + array( + "foo" => "bar", + "foo1" => "bar1", + "foo2" => "bar2", + ), + array( + "baz" => "buh", + "baz1" => "buh1", + "baz2" => "buh2", + ) + ), + "logged_in" => false +); \ No newline at end of file diff --git a/benchmark/aspect/benchmark/execute.aspect.php b/benchmark/aspect/benchmark/execute.aspect.php new file mode 100644 index 0000000..9a8ad52 --- /dev/null +++ b/benchmark/aspect/benchmark/execute.aspect.php @@ -0,0 +1,25 @@ + \ No newline at end of file diff --git a/benchmark/aspect/benchmark/execute.native.php b/benchmark/aspect/benchmark/execute.native.php new file mode 100644 index 0000000..11d4fe7 --- /dev/null +++ b/benchmark/aspect/benchmark/execute.native.php @@ -0,0 +1,23 @@ + \ No newline at end of file diff --git a/benchmark/aspect/benchmark/execute.simple.aspect.php b/benchmark/aspect/benchmark/execute.simple.aspect.php new file mode 100644 index 0000000..4135b04 --- /dev/null +++ b/benchmark/aspect/benchmark/execute.simple.aspect.php @@ -0,0 +1,25 @@ + "Ivan", + "email" => "bzick@megagroup.ru" +); + +$data = Aspect::fetch('simple.tpl', $_data, 0); + +$dt = microtime(1); +$data = Aspect::fetch('simple.tpl', $_data, 0); +$dt = microtime(1) - $dt; + + +echo "\n=====================\nCompile: $ct\nDisplay: $dt\n"; +?> \ No newline at end of file diff --git a/benchmark/aspect/benchmark/execute.simple.smarty3.php b/benchmark/aspect/benchmark/execute.simple.smarty3.php new file mode 100644 index 0000000..8e1803e --- /dev/null +++ b/benchmark/aspect/benchmark/execute.simple.smarty3.php @@ -0,0 +1,29 @@ +addTemplateDir(__DIR__.'/../templates'); +$smarty->setCompileDir(__DIR__.'/../compiled'); + +$ct = microtime(1); +$tpl = $smarty->createTemplate('simple.tpl'); +/* @var Smarty_Internal_Template $tpl */ +$tpl->compileTemplateSource(); +$ct = microtime(1) - $ct; + +$_data = array( + "name" => "Ivan", + "email" => "bzick@megagroup.ru" +); + +$smarty->assign($_data); +$data = $smarty->fetch('simple.tpl'); + +$t = microtime(1); +$smarty->assign($_data); +$data = $smarty->fetch('simple.tpl'); +$dt = microtime(1) - $t; + + +echo "\n=====================\nCompile: $ct\nDisplay: $dt\n"; \ No newline at end of file diff --git a/benchmark/aspect/benchmark/execute.smarty3.php b/benchmark/aspect/benchmark/execute.smarty3.php new file mode 100644 index 0000000..0adcc35 --- /dev/null +++ b/benchmark/aspect/benchmark/execute.smarty3.php @@ -0,0 +1,29 @@ +addTemplateDir(__DIR__.'/../templates'); +$smarty->setCompileDir(__DIR__.'/../compiled'); + +/*$ct = microtime(1); +$tpl = $smarty->createTemplate('syntax.tpl'); +/* @var Smarty_Internal_Template $tpl */ +/*$tpl->compileTemplateSource(); +$ct = microtime(1) - $ct; +echo "\n=====================\nCompile: $ct\n"; +*/ + +$_data = require_once __DIR__.'/data.php'; + +$smarty->assign($_data); +$data = $smarty->fetch('syntax.tpl'); + +$t = microtime(1); +$smarty->assign($_data); +$data = $smarty->fetch('syntax.tpl'); +$dt = microtime(1) - $t; + +$data = MF\Misc\Str::strip($data, true); +echo "$data\n====\n".md5($data)."\n=====================\nDisplay: $dt\n"; +var_dump("Pick memory: ".memory_get_peak_usage()); \ No newline at end of file diff --git a/benchmark/aspect/benchmark/foreach.last.php b/benchmark/aspect/benchmark/foreach.last.php new file mode 100644 index 0000000..f487c98 --- /dev/null +++ b/benchmark/aspect/benchmark/foreach.last.php @@ -0,0 +1,47 @@ + $v) { + if(!--$c) var_dump("last"); + //var_dump($v); +} + +print_r("\n\nforeach + count: ".(microtime(1) - $t)."\n\n"); + +$t = microtime(1); +reset($a); +while(list($k, $v) = each($a)) { + if(key($a) === null) { + var_dump("last"); + } + /*next($a); + if(key($a) === null) { + var_dump("last"); + }*/ + //var_dump($v); +} +print_r("\neach: ".(microtime(1) - $t)."\n\n"); + +$t = microtime(1); +foreach($a as $k => $v) { + //var_dump($v); +} + +print_r("\n\nforeach: ".(microtime(1) - $t)."\n\n"); +?> diff --git a/benchmark/aspect/templates/fullsyntax.tpl b/benchmark/aspect/templates/fullsyntax.tpl new file mode 100644 index 0000000..6366e79 --- /dev/null +++ b/benchmark/aspect/templates/fullsyntax.tpl @@ -0,0 +1,94 @@ +{* + data.value = "TXT value"; + data.arr.dot.4.retval = "Four"; + data.arr.dot.5 = "FiVe"; + data.arr.i = 4; + data.arr.retval = "Retval key"; + data.set->get = "Get props"; + data.iam->getName(...) == "return Name"; + data.iam->getFormat(...)== "return Format"; + data.num = 777; + data.k = 0; + data.ls = array("a" => "lit A", "c" => "lit C", "d" => "lit D"); + +*} +Hello world! +My Name is {$data.value}... +Yeah + +Hello world! +My Name is {$data.arr.dot|json_encode|lower}... +Yeah + +Hello world, {$data.value|upper}! +My Name is {$data.arr[dot].5|upper}... +My Name is {$data.arr.dot[ $data.arr.i|round ]."retval"|upper}... +My Name is {$data.arr."retval"|upper}... +Yeah + +Hello world! +My Name is {$data.set->get|upper}... +Yeah + +Hello world! +My Name is {$data.iam->getName()|upper}... +Your Name is {$data.you->getFormat(1, 0.4, "%dth", 'grade', $data.arr[dot].5|lower)|upper}? +Yeah + + +Hello world! +{if isset($data.iam) && !empty($data.set->get)} +My Name is {$data.set->get}... +{/if} +Yeah + +Hello world! +{if $data.num >= 5 && $data.k++ || foo($data.value) && !bar($data.num) + 3 || $data.k?} +My Name is {$data->user|upper}... +{/if} +Yeah + +Hello world! +{foreach from=$data.ls key=k item=e} +My Name is {$e|upper} ({$k})... +{/foreach} +Yeah + +Hello world! +{switch $data.num} + {case "dotted"} +dotted lines
+ {break} + {case 777} +numeric lines
+ {break} + {case $data[arr]["dot"].4.retval|upper} +lister
+ {break} +{/switch} +Yeah + +Hello world! +{* if !empty($data.num) *} +{if $data.num?} + dotted lines
+{elseif $data[arr]["dot"].4.retval|lower} + lister
+{/if} +Yeah + +Check system variable
+Current timestamp {$aspect.now}...
+$_GET {$aspect.get.item}...
+$_POST {$aspect.post.myval|upper}...
+$_COOKIES {$aspect.cookies["uid"]}...
+$_REQUEST {$aspect.request?}...
+Consts {$data.number|round:$aspect.const.PHP_INT_MAX}...
+Ok + + +Hello world! +{for from=1 to=$data.ls|count} +
Go
+{/for} +Yeah \ No newline at end of file diff --git a/benchmark/aspect/templates/simple.tpl b/benchmark/aspect/templates/simple.tpl new file mode 100644 index 0000000..08e1283 --- /dev/null +++ b/benchmark/aspect/templates/simple.tpl @@ -0,0 +1,17 @@ + + + Simple template about {$name|upper} + + + +{if $name} + My name is {$name} +{else} + I haven't name :( +{/if} + +Ok. + +My email {$email}. It is great! + + \ No newline at end of file diff --git a/benchmark/aspect/templates/subdir/subtpl.php b/benchmark/aspect/templates/subdir/subtpl.php new file mode 100644 index 0000000..cc10f25 --- /dev/null +++ b/benchmark/aspect/templates/subdir/subtpl.php @@ -0,0 +1,18 @@ + + +Ok. + +My email + + \ No newline at end of file diff --git a/benchmark/aspect/templates/subdir/subtpl.tpl b/benchmark/aspect/templates/subdir/subtpl.tpl new file mode 100644 index 0000000..f152c35 --- /dev/null +++ b/benchmark/aspect/templates/subdir/subtpl.tpl @@ -0,0 +1,9 @@ +{if $user.name} + My name is {$user.name} +{else} + I haven't name :( +{/if} + +Ok. + +My email {$user.name}. It is great! \ No newline at end of file diff --git a/benchmark/aspect/templates/syntax.php b/benchmark/aspect/templates/syntax.php new file mode 100644 index 0000000..35a3f5e --- /dev/null +++ b/benchmark/aspect/templates/syntax.php @@ -0,0 +1,184 @@ + + + <?php echo $tpl["title"]; ?> + + + + +Simple manipulations + + + + + + + + +value; ?> +method(); ?> + +Many other combinations are allowed + + + + + + + + +methodArgs($tpl["baz_key"], 2, 2.3, "some string", $tpl["bar_key"]); ?> <-- passing parameters + + +Math and embedding tags: + + // will output the sum of x and y. + + // tags within tags + + // tags within double quoted strings + + +Short variable assignment: + + + // function in assignment + // as function parameter + // assign to specific array element + + +Smarty "dot" syntax (note: embedded {} are used to address ambiguities): + + + + + + +Object chaining: + +getSelf($tpl["x"])->method($tpl["y"]); ?> + +Direct PHP function access: + + + + +'.$tpl["name"].''; + } else { + echo "hi, ".$tpl["user"]["name"]; + } +?> + +Embedding Vars in Double Quotes + + $tpl["user"]["email"],"text" => "test ".$tpl["item"]." test"), $tpl). + MF\Aspect\Func::mailto(array("address" => $tpl["user"]["email"],"text" => "test ".$tpl["foo_key"]." test"), $tpl). + MF\Aspect\Func::mailto(array("address" => $tpl["user"]["email"],"text" => "test ".($tpl["data"][4])." test"), $tpl). + MF\Aspect\Func::mailto(array("address" => $tpl["user"]["email"],"text" => "test ".$tpl["item"].".bar test"), $tpl). + MF\Aspect\Func::mailto(array("address" => $tpl["user"]["email"],"text" => 'test {$data.barz} test'), $tpl). + MF\Aspect\Func::mailto(array("address" => $tpl["user"]["email"],"text" => "test ".($tpl["data"]["barz"])." test"), $tpl). + MF\Aspect\Func::mailto(array("address" => $tpl["user"]["email"],"text" => strtoupper("test ".($tpl["data"]["barz"])." test")), $tpl). + MF\Aspect\Func::mailto(array("address" => $tpl["user"]["email"],"text" => "test ".(strtoupper($tpl["data"]["barz"]))." test"), $tpl); ?> + + +will replace $tpl_name with value + + +does NOT replace $tpl_name + $tpl["user"]["email"],"text" => "one,two"), $tpl); ?> + + + +Math + +some more complicated examples + +num * !$tpl["data"]["foo"]["baz"]["ls"][4] - 3 * 7 % $tpl["data"][2]; ?> + +num * $tpl["data"]["foo"]["baz"]["ls"][4] - 3 * 7 % $tpl["data"][2]) { + echo MF\Misc\Str::truncate($tpl["data"]["barz"], "".($tpl["data"][2] / 2 - 1)."")."\n". + MF\Misc\Str::truncate($tpl["data"]["barz"], ($tpl["data"][2] / 2 - 1)); + } +?> + +Escaping Smarty Parsing + + + + +name:
+email:
+ +Modifier examples + +apply modifier to a variable + + +modifier with parameters + + +apply modifier to a function parameter + $tpl["user"]["email"],"text" => strtoupper($tpl["user"]["name"])), $tpl); ?> + +with parameters + $tpl["user"]["email"],"text" => MF\Misc\Str::truncate($tpl["user"]["name"], 40, "...")), $tpl); ?> + +apply modifier to literal string + +using date_format to format the current date + + +apply modifier to a custom function + +Foreach + $tpl["value"]) { + echo $tpl["key"].": ".$tpl["value"]." "; + } + } else { + echo "no items"; + } + } + } +?> + +If condition + + + + 0) { + echo "do a foreach loop"; + } +?> + + + + + \ No newline at end of file diff --git a/benchmark/aspect/templates/syntax.tpl b/benchmark/aspect/templates/syntax.tpl new file mode 100644 index 0000000..da23378 --- /dev/null +++ b/benchmark/aspect/templates/syntax.tpl @@ -0,0 +1,179 @@ + + + {$title} + + + +{* this is a comment *} + +Simple manipulations + +{$item} +{$data[4]} +{$data.bar} +{*$data[bar]*} +{$data["bar"]} +{$data['bar']} +{$data.$bar_key} +{$data.obj->value} +{$data.obj->method()} + +Many other combinations are allowed + +{$data.foo.bar} +{$data.foo.$baz_key.$foo_key} +{$data.foo.$bar_key} +{$data.$foo_key.bar} +{$data[5].bar} +{$data[5].$bar_key} +{$data.foo.$baz_key.ls[4]} +{$data.obj->methodArgs($baz_key, 2, 2.3, "some string", $bar_key)} <-- passing parameters +{*"foo"*} + + +Math and embedding tags: + +{$x+$y} // will output the sum of x and y. +{$data[$x+3]} +{$item=4+3} // tags within tags +{$data[4]=4+3} +{$item="this is message"} // tags within double quoted strings + + +Short variable assignment: + +{$item=$y+2} +{$item = strlen($bar_key)} // function in assignment +{$item = intval( ($x+$y)*3 )} // as function parameter +{$data.bar=1} // assign to specific array element +{$data.foo.bar="data.foo.bar: tpl value"} + +Smarty "dot" syntax (note: embedded {} are used to address ambiguities): + +{$data.foo.baz.foo} +{$data.foo.$baz_key.foo} +{$data[$y+4].foo} +{$data.foo[$data.baz_key].foo} + +Object chaining: + +{$data.obj->getSelf($x)->method($y)} + +Direct PHP function access: + +{strlen("hi!")} + + +{if $logged_in} +Welcome, {$name}! +{else} +hi, {$user.name} +{/if} + +Embedding Vars in Double Quotes + +{mailto address=$user.email text="test $item test"} +{mailto address=$user.email text="test $foo_key test"} +{mailto address=$user.email text="test {$data[4]} test"} +{*mailto address=$user.email text="test {$data[barz]} test"*} +{mailto address=$user.email text="test $item.bar test"} +{mailto address=$user.email text='test {$data.barz} test'} +{mailto address=$user.email text="test {$data.barz} test"} +{mailto address=$user.email text="test {$data.barz} test"|upper} +{mailto address=$user.email text="test {$data.barz|upper} test"} + + +will replace $tpl_name with value +{include file="subdir/$tpl_name.tpl"} + +does NOT replace $tpl_name +{mailto address=$user.email text="one,two"} + +{include file="{$data.tpl_name}.tpl"} + +Math + +some more complicated examples + +{$data[2] - $data.obj->num * !$data.foo.baz.ls[4] - 3 * 7 % $data[2]} + +{if $data[2] - $data.obj->num * $data.foo.baz.ls[4] - 3 * 7 % $data[2]} + {$data.barz|truncate:"{$data[2]/2-1}"} + {$data.barz|truncate:($data[2]/2-1)} +{/if} + +Escaping Smarty Parsing + + + + +name: {$user.name}
+email: {$user.email}
+ +Modifier examples + +apply modifier to a variable +{$user.name|upper} + +modifier with parameters +{$user.name|truncate:40:"..."} + +apply modifier to a function parameter +{mailto address=$user.email text=$user.name|upper} + +with parameters +{mailto address=$user.email text=$user.name|truncate:40:"..."} + +apply modifier to literal string +{*"foobar"|upper*} + +using date_format to format the current date +{$smarty.now|date_format:"%Y/%m/%d"} + +apply modifier to a custom function +{*mailto|upper address="smarty@example.com"*} + +Foreach +{foreach $contacts as $contact} + {foreach $contact as $key => $value} + {$key}: {$value} + {foreachelse} + no items + {/foreach} +{/foreach} + +If condition +{if isset($user.name) && $user.name == 'yandex'} +do something +{elseif $user.name == $data.foo.bar} +do something2 +{/if} + +{*switch $item} + {case 1} + item 1 + {break} + {case 2} + item 2 + {break} + {default} + on item +{/switch*} + +{if is_array($data.foo) && count($data.foo) > 0} +do a foreach loop +{/if} + + + + \ No newline at end of file diff --git a/benchmark/aspect/test.php b/benchmark/aspect/test.php new file mode 100644 index 0000000..bfe2fe1 --- /dev/null +++ b/benchmark/aspect/test.php @@ -0,0 +1,103 @@ +world! +My Name is {$data.value}... +Yeah'; + +$tpl[1] = 'Hello world! +My Name is {$data.value[label].dot|json_decode|lower|truncate:80:"...":true:null:0.22:$a.keyz|upper}... +Yeah'; + +$tpl[2] = 'Hello world, {$user.name|upper}! +My Name is {$data.user1.5.$i."return"[ $hello.world|lower ]|upper}... +Yeah'; + +$tpl[3] = 'Hello world! +My Name is {$data->set->get|upper}... +Yeah'; + +$tpl[4] = 'Hello world! +My Name is {$iam->getName()|upper}... +Your Name is {$you->getFromat(1, 0.4, "%dth", \'grade\', $global->name.k|lower)|upper}? +Yeah'; + +$tpl[5] = 'Hello world! +{if isset($data.user) && !empty($data->value)} +My Name is {$data->user|upper}... +{/if} +Yeah'; + +$tpl[6] = 'Hello world! +{if $data.user >= 5 && $k++ || foo($data->value) && !bar($d) + 3 || $show.go?} +My Name is {$data->user|upper}... +{/if} +Yeah'; + +$tpl[7] = 'Hello world! +{foreach from=$users."list" key=k item=e} +My Name is {$e|upper} ({$k})... +{/foreach} +Yeah'; + +$tpl[8] = 'Hello world! +{switch $data->enum} + {case "dotted"} + dotted lines
+ {break} + {case 7} + numeric lines
+ {break} + {case $list|truncate} + lister
+ {break} +{/switch} +Yeah'; + +$tpl[9] = 'Hello world! +{if $data->enum?} + dotted lines
+{elseif $list|truncate} + lister
+{/if} +Yeah'; + +$tpl[10] = 'Check system variable
+Current timestamp {$aspect.now}...
+$_GET {$aspect.get.item}...
+$_POST {$aspect.post.myval|upper}...
+$_COOKIES {$aspect.cookies["uid"]}...
+$_REQUEST {$aspect.request?}...
+Consts {$number|round:$aspect.const.PHP_INT_MAX|}...
+Ok'; + +$tpl[11] = 'Hello world! +{for from=1 to=$e|count} +
Go
+{/for} +Yeah'; + +$tpl_ = ' +Hello world! +My Name is {$data|upper}... + {* comment *} +I have + +'; + +$template = new MF\Aspect\Template($tpl[7], 'my.tpl'); +//var_dump(Tokenizer::decode('case 6:')); +//exit; +echo "\n".$template->getBody()."\n"; +//var_dump($template->fetch(array("data" => array("value" => "Yohoho")))); +?> \ No newline at end of file diff --git a/benchmark/run.php b/benchmark/run.php index dd6dc3c..b364a76 100644 --- a/benchmark/run.php +++ b/benchmark/run.php @@ -1,4 +1,6 @@ compile_check = false; + + $smarty->setTemplateDir(__DIR__.'/../templates'); + $smarty->setCompileDir(__DIR__."/../compile/"); + + if($double) { + $smarty->assign($data); + $smarty->fetch($tpl); + } + + $start = microtime(true); + $smarty->assign($data); + $smarty->fetch($tpl); + + printf(self::$t, __FUNCTION__, $message, round(microtime(true)-$start, 4), round(memory_get_peak_usage()/1024/1024, 2)); + } + + public static function twig($tpl, $data, $double, $message) { + + Twig_Autoloader::register(); + $loader = new Twig_Loader_Filesystem(__DIR__.'/../templates'); + $twig = new Twig_Environment($loader, array( + 'cache' => __DIR__."/../compile/", + 'autoescape' => false, + 'auto_reload' => false, + )); + + if($double) { + $twig->loadTemplate($tpl)->render($data); + } + + $start = microtime(true); + $twig->loadTemplate($tpl)->render($data); + printf(self::$t, __FUNCTION__, $message, round(microtime(true)-$start, 4), round(memory_get_peak_usage()/1024/1024, 2)); + } + + public static function aspect($tpl, $data, $double, $message) { + + $aspect = Aspect::factory(__DIR__.'/../templates', __DIR__."/../compile/"); + + if($double) { + $aspect->fetch($tpl, $data); + } + $start = microtime(true); + $aspect->fetch($tpl, $data); + printf(self::$t, __FUNCTION__, $message, round(microtime(true)-$start, 4), round(memory_get_peak_usage()/1024/1024, 2)); + } + + public static function run($engine, $template, $data, $double, $message) { + passthru(sprintf("php %s/run.php --engine '%s' --template '%s' --data '%s' --message '%s' %s", __DIR__, $engine, $template, $data, $message, $double ? '--double' : '')); + } + + public static function runs($engine, $template, $data) { + self::run($engine, $template, $data, false, '!compiled and !loaded'); + self::run($engine, $template, $data, false, 'compiled and !loaded'); + self::run($engine, $template, $data, true, 'compiled and loaded'); + echo "\n"; + } +} \ No newline at end of file diff --git a/benchmark/scripts/run.php b/benchmark/scripts/run.php new file mode 100644 index 0000000..eb064a2 --- /dev/null +++ b/benchmark/scripts/run.php @@ -0,0 +1,15 @@ +compile_check = false; diff --git a/benchmark/templates/foreach.php b/benchmark/templates/foreach.php index a0a5306..0439771 100644 --- a/benchmark/templates/foreach.php +++ b/benchmark/templates/foreach.php @@ -5,7 +5,7 @@ exec("rm -rf ".__DIR__."/../compile/*"); require(__DIR__.'/../../vendor/autoload.php'); $smarty = new Smarty(); -$smarty->compile_check = false; +$smarty->compile_check = true; $smarty->setTemplateDir(__DIR__); $smarty->setCompileDir(__DIR__."/../compile/"); diff --git a/benchmark/templates/inheritance.php b/benchmark/templates/inheritance.php index 2445f7a..dc0b04b 100644 --- a/benchmark/templates/inheritance.php +++ b/benchmark/templates/inheritance.php @@ -16,7 +16,7 @@ exec("rm -rf ".__DIR__."/../compile/*"); require(__DIR__.'/../../vendor/autoload.php'); $smarty = new Smarty(); -$smarty->compile_check = false; +$smarty->compile_check = true; $smarty->setTemplateDir(__DIR__); $smarty->setCompileDir(__DIR__."/../compile/"); diff --git a/composer.json b/composer.json index 5a3d070..fd588c8 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,8 @@ { "name": "megagroup/aspect", "type": "library", - "description": "Aspect - faster templater for PHP", - "keywords": ["aspect", "templater", "templating", "yohoho"], + "description": "Aspect - faster template engine", + "keywords": ["aspect", "templater", "templating", "template", "yohoho"], "license": "MIT", "authors": [ { diff --git a/docs/about.md b/docs/about.md new file mode 100644 index 0000000..4310767 --- /dev/null +++ b/docs/about.md @@ -0,0 +1,3 @@ +About Aspect +============ + diff --git a/docs/ext/mods.md b/docs/ext/mods.md new file mode 100644 index 0000000..a859741 --- /dev/null +++ b/docs/ext/mods.md @@ -0,0 +1,11 @@ +Модификаторы +============ + +Добавить модификатор: + +```php +$aspect->addModifier($modifier, $callback); +``` + +* `$modifier` - имя модификатора +* `$callback` - строка с именем функции \ No newline at end of file diff --git a/docs/ext/tags.md b/docs/ext/tags.md new file mode 100644 index 0000000..f857bda --- /dev/null +++ b/docs/ext/tags.md @@ -0,0 +1,20 @@ +Теги +==== + +Теги делятся на компилеры и функции. +Компилеры формируют синтаксис языка шаблона, добавляя такой функционал как foreach, if, while и т.д. В то время как функции - обычный вызов некоторой именованной функции + +Добавить компилер: + +```php +$aspect->addCompiler($compiler, $parser); +``` + +* `$compiler` - имя модификатора +* `$parser` - функция разбора тега в формате function (MF\Tokenizer $tokens, MF\Aspect\Template $tpl) {} + +Добавить блочный компилер: + +```php +$aspect->addBlockCompiler($compiler, $parsers, $tags); +``` \ No newline at end of file diff --git a/docs/main.md b/docs/main.md new file mode 100644 index 0000000..24e2b35 --- /dev/null +++ b/docs/main.md @@ -0,0 +1,43 @@ +Documentation +============= + +**Aspect** + +* [About](./about.md) +* [Requirements and installation](./install.md) +* [Syntax](./syntax.md) +* [Settings](./settings.md) +* [Callbacks and filters](./callbacks.md) + +**Modifiers** + +* [upper](./mods/upper.md) +* [lower](./mods/lower.md) +* [date_format](./mods/date_format.md) +* [date](./mods/date.md) +* [truncate](./mods/truncate.md) +* [escape](./mods/escape.md) +* [unescape](./mods/unescape.md) +* [strip](./mods/strip.md) +Also see [allowed functions](./mods/allowed_functions.md). + +**Internal tags** + +* [var](./tags/var.md) +* [if](./tags/if.md), `elseif` and `else` +* [foreach](./tags/foreach.md), `foreaelse`, `break` and `continue` +* [for](./tags/for.md), `break` and `continue` +* [while](./tags/while.md), `break` and `continue` +* [switch](./tags/switch.md), `case`, `default` and `break` +* [include](./tags/include.md) +* [extends](./tags/extends.md), `use` and `block` +* [capture](./tags/capture.md) +* [filter](./tags/filter.md) +* [ignore](./tags/ignore.md) + +**Extend Aspect** + +* [Add tags](./ext/tags.md) +* [Add modificators](./ext/mods.md) +* [Add template provider](./ext/provider.md) +* [Parsing](./ext/parsing.md) \ No newline at end of file diff --git a/docs/modifiers.md b/docs/mods/modifiers.md similarity index 100% rename from docs/modifiers.md rename to docs/mods/modifiers.md diff --git a/docs/settings.md b/docs/settings.md new file mode 100644 index 0000000..cb5c9f1 --- /dev/null +++ b/docs/settings.md @@ -0,0 +1,48 @@ +Настройка +========= + +### Параметры + +#### Исходные шаблоны + +Добавить папку с шаблонами: + +```php +$aspect->addTemplateDir($dir); +``` + +Шаблонизатор последовательно будет перебирать папки и искать указанный шаблон. + +#### Сборки шаблонов + +Задаёт папку в которую будут сохранятся преобразованные в PHP шаблоны + +```php +$aspect->setCompileDir($dir); +``` + +#### Опции + +```php +$aspect->setOptions($options); +``` + +Массив `'option_name' => boolean` (если ключ не указан автоматически задаётся false) + +* **disable_methods**, `boolean`, запретить вызов методов у объектов +* **disable_native_funcs**, `boolean`, запретить использование PHP функций, кроме разрешенных +* **disable_set_vars**, `boolean`, запретить изменять или задавать переменные +* **include_sources**, `boolean`, вставлять исходный код шаблона в его сборку +* **compile_check**, `boolean`, сравнивать mtime у исходного шаблона и его сборки. При изменении исходного шаблона будет производится его пересборка (замедляет работу шаблонизатора). +* **force_compile**, `boolean`, пересобирать шаблон при каждом вызове (сильно замедляет работу шаблонизатора). +* **force_include**, `boolean`. + +или битовая маска из флагов: + +* `Aspect::DENY_METHODS` то же что и **disable_methods** +* `Aspect::DENY_INLINE_FUNCS` то же что и **disable_native_funcs** +* `Aspect::DENY_SET_VARS` то же что и **disable_set_vars** +* `Aspect::INCLUDE_SOURCES` то же что и **include_sources** +* `Aspect::CHECK_MTIME` то же что и **compile_check** +* `Aspect::FORCE_COMPILE` то же что и **force_compile** +* `Aspect::FORCE_INCLUDE` то же что и **force_include** diff --git a/docs/intro.md b/docs/syntax.md similarity index 68% rename from docs/intro.md rename to docs/syntax.md index ce15700..d51d6c2 100644 --- a/docs/intro.md +++ b/docs/syntax.md @@ -194,82 +194,4 @@ document.body.appendChild(e); })('test'); -``` - -Настройка -========= - -### Параметры - -#### Исходные шаблоны - -Добавить папку с шаблонами: - -```php -$aspect->addTemplateDir($dir); -``` - -Шаблонизатор последовательно будет перебирать папки и искать указанный шаблон. - -#### Сборки шаблонов - -Задаёт папку в которую будут сохранятся преобразованные в PHP шаблоны - -```php -$aspect->setCompileDir($dir); -``` - -#### Опции - -```php -$aspect->setOptions($options); -``` - -Массив `'option_name' => boolean` (если ключ не указан автоматически задаётся false) - -* **disable_methods**, `boolean`, запретить вызов методов у объектов -* **disable_native_funcs**, `boolean`, запретить использование PHP функций, кроме разрешенных -* **disable_set_vars**, `boolean`, запретить изменять или задавать переменные -* **include_sources**, `boolean`, вставлять исходный код шаблона в его сборку -* **compile_check**, `boolean`, сравнивать mtime у исходного шаблона и его сборки. При изменении исходного шаблона будет производится его пересборка (замедляет работу шаблонизатора). -* **force_compile**, `boolean`, пересобирать шаблон при каждом вызове (сильно замедляет работу шаблонизатора). - -или битовая маска из флагов: - -* `Aspect::DENY_METHODS` то же что и **disable_methods** -* `Aspect::DENY_INLINE_FUNCS` то же что и **disable_native_funcs** -* `Aspect::DENY_SET_VARS` то же что и **disable_set_vars** -* `Aspect::INCLUDE_SOURCES` то же что и **include_sources** -* `Aspect::CHECK_MTIME` то же что и **compile_check** -* `Aspect::FORCE_COMPILE` то же что и **force_compile** - -### Модификаторы - -Добавить модификатор: - -```php -$aspect->addModifier($modifier, $callback); -``` - -* `$modifier` - имя модификатора -* `$callback` - строка с именем функции - -### Теги - -Теги делятся на компилеры и функции. -Компилеры формируют синтаксис языка шаблона, добавляя такой функционал как foreach, if, while и т.д. В то время как функции - обычный вызов некоторой именованной функции - -Добавить компилер: - -```php -$aspect->addCompiler($compiler, $parser); -``` - -* `$compiler` - имя модификатора -* `$parser` - функция разбора тега в формате function (MF\Tokenizer $tokens, MF\Aspect\Template $tpl) {} - -Добавить блочный компилер: - -```php -$aspect->addBlockCompiler($compiler, $parsers, $tags); ``` \ No newline at end of file diff --git a/src/Aspect.php b/src/Aspect.php index a7a6769..bedcd35 100644 --- a/src/Aspect.php +++ b/src/Aspect.php @@ -1,10 +1,12 @@ self::DENY_METHODS, "disable_native_funcs" => self::DENY_INLINE_FUNCS, - "disable_set_vars" => self::DENY_SET_VARS, - "include_sources" => self::INCLUDE_SOURCES, "force_compile" => self::FORCE_COMPILE, "compile_check" => self::CHECK_MTIME, + "force_include" => self::FORCE_INCLUDE, ); + /** + * Default options for functions + * @var array + */ private static $_actions_defaults = array( self::BLOCK_FUNCTION => array( 'type' => self::BLOCK_FUNCTION, - 'open' => 'MF\Aspect\Compiler::stdFuncOpen', - 'close' => 'MF\Aspect\Compiler::stdFuncClose', + 'open' => self::DEFAULT_FUNC_OPEN, + 'close' => self::DEFAULT_FUNC_CLOSE, 'function' => null, ), self::INLINE_FUNCTION => array( 'type' => self::INLINE_FUNCTION, - 'parser' => 'MF\Aspect\Compiler::stdFuncParser', + 'parser' => self::DEFAULT_FUNC_PARSER, 'function' => null, ), - self::INLINE_FUNCTION => array( + self::INLINE_COMPILER => array( 'type' => self::INLINE_COMPILER, 'open' => null, - 'close' => 'MF\Aspect\Compiler::stdClose', + 'close' => self::DEFAULT_CLOSE_COMPILER, 'tags' => array(), 'float_tags' => array() ), - self::BLOCK_FUNCTION => array( + self::BLOCK_COMPILER => array( 'type' => self::BLOCK_COMPILER, 'open' => null, 'close' => null, @@ -62,7 +71,6 @@ class Aspect { ) ); - public $blocks = array(); /** * @var array Templates storage */ @@ -81,49 +89,50 @@ class Aspect { */ protected $_options = 0; - /** - * Modifiers loader - * @var callable - */ - protected $_loader_mod; - /** - * Functions loader - * @var callable - */ - protected $_loader_func; + protected $_on_pre_cmp = array(); + protected $_on_cmp = array(); + protected $_on_post_cmp = array(); + + /** + * @var Aspect\Provider + */ + private $_provider; + /** + * @var array of Aspect\ProviderInterface + */ + protected $_providers = array(); /** - * @var array list of modifiers + * @var array of modifiers [modifier_name => callable] */ protected $_modifiers = array( "upper" => 'strtoupper', "lower" => 'strtolower', - "nl2br" => 'nl2br', "date_format" => 'Aspect\Modifier::dateFormat', "date" => 'Aspect\Modifier::date', "truncate" => 'Aspect\Modifier::truncate', "escape" => 'Aspect\Modifier::escape', "e" => 'Aspect\Modifier::escape', // alias of escape + "url" => 'urlencode', // alias of escape:"url" "unescape" => 'Aspect\Modifier::unescape', - "strip_tags" => 'strip_tags', "strip" => 'Aspect\Modifier::strip', - "default" => 'Aspect\Modifier::defaultValue', - "isset" => 'isset', - "empty" => 'empty' + "default" => 'Aspect\Modifier::defaultValue' ); /** - * @var array list of allowed PHP functions + * @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, "is_object" => 1 + "empty" => 1, "isset" => 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 ); /** - * @var array list of compilers and functions + * @var array of compilers and functions */ protected $_actions = array( - 'foreach' => array( + 'foreach' => array( // {foreach ...} {break} {continue} {foreachelse} {/foreach} 'type' => self::BLOCK_COMPILER, 'open' => 'Aspect\Compiler::foreachOpen', 'close' => 'Aspect\Compiler::foreachClose', @@ -134,7 +143,7 @@ class Aspect { ), 'float_tags' => array('break' => 1, 'continue' => 1) ), - 'if' => array( + 'if' => array( // {if ...} {elseif ...} {else} {/if} 'type' => self::BLOCK_COMPILER, 'open' => 'Aspect\Compiler::ifOpen', 'close' => 'Aspect\Compiler::stdClose', @@ -143,7 +152,7 @@ class Aspect { 'else' => 'Aspect\Compiler::tagElse', ) ), - 'switch' => array( + 'switch' => array( // {switch ...} {case ...} {break} {default} {/switch} 'type' => self::BLOCK_COMPILER, 'open' => 'Aspect\Compiler::switchOpen', 'close' => 'Aspect\Compiler::stdClose', @@ -154,7 +163,7 @@ class Aspect { ), 'float_tags' => array('break' => 1) ), - 'for' => array( + 'for' => array( // {for ...} {break} {continue} {/for} 'type' => self::BLOCK_COMPILER, 'open' => 'Aspect\Compiler::forOpen', 'close' => 'Aspect\Compiler::forClose', @@ -165,7 +174,7 @@ class Aspect { ), 'float_tags' => array('break' => 1, 'continue' => 1) ), - 'while' => array( + 'while' => array( // {while ...} {break} {continue} {/while} 'type' => self::BLOCK_COMPILER, 'open' => 'Aspect\Compiler::whileOpen', 'close' => 'Aspect\Compiler::stdClose', @@ -175,24 +184,24 @@ class Aspect { ), 'float_tags' => array('break' => 1, 'continue' => 1) ), - 'include' => array( + 'include' => array( // {include ...} 'type' => self::INLINE_COMPILER, 'parser' => 'Aspect\Compiler::tagInclude' ), - 'var' => array( + 'var' => array( // {var ...} 'type' => self::INLINE_COMPILER, 'parser' => 'Aspect\Compiler::assign' ), - 'block' => array( + 'block' => array( // {block ...} {/block} 'type' => self::BLOCK_COMPILER, 'open' => 'Aspect\Compiler::tagBlockOpen', 'close' => 'Aspect\Compiler::tagBlockClose', ), - 'extends' => array( + 'extends' => array( // {extends ...} 'type' => self::INLINE_COMPILER, 'parser' => 'Aspect\Compiler::tagExtends' ), - 'capture' => array( + 'capture' => array( // {capture ...} {/capture} 'type' => self::BLOCK_FUNCTION, 'open' => 'Aspect\Compiler::stdFuncOpen', 'close' => 'Aspect\Compiler::stdFuncClose', @@ -206,8 +215,16 @@ class Aspect { ); - public static function factory($template_dir, $compile_dir, $options = 0) { - $aspect = new static(); + /** + * 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) { + $aspect = new static($provider); $aspect->setCompileDir($compile_dir); $aspect->setTemplateDirs($template_dir); if($options) { @@ -216,34 +233,76 @@ class Aspect { return $aspect; } + /** + * @param Aspect\Provider $provider + */ + public function __construct(Aspect\Provider $provider = null) { + $this->_provider = $provider ?: new Aspect\Provider(); + } + + /** + * Set checks template for modifications + * @param $state + * @return Aspect + */ public function setCompileCheck($state) { $state && ($this->_options |= self::CHECK_MTIME); return $this; } + /** + * Set force template compiling + * @param $state + * @return Aspect + */ public function setForceCompile($state) { $state && ($this->_options |= self::FORCE_COMPILE); $this->_storage = $state ? new Aspect\BlackHole() : array(); return $this; } + /** + * Set compile directory + * @param string $dir directory to store compiled templates in + * @return Aspect + */ public function setCompileDir($dir) { $this->_compile_dir = $dir; return $this; } + /** + * Set template directory + * @param string|array $dirs directory(s) of template sources + * @return Aspect + */ public function setTemplateDirs($dirs) { - $this->_tpl_path = (array)$dirs; + $this->_provider->setTemplateDirs($dirs); return $this; } - /*public function addPostCompileFilter($cb) { - $this->_post_cmp[] = $cb; + /** + * + * @param callable $cb + */ + public function addPreCompileFilter($cb) { + $this->_on_pre_cmp[] = $cb; } + /** + * + * @param callable $cb + */ + public function addPostCompileFilter($cb) { + $this->_on_post_cmp[] = $cb; + } + + /** + * @param callable $cb + */ public function addCompileFilter($cb) { - $this->_cmp[] = $cb; - }*/ + $this->_on_cmp[] = $cb; + } /** * Add modifier @@ -252,17 +311,17 @@ class Aspect { * @param string $callback * @return Aspect */ - public function setModifier($modifier, $callback) { + public function addModifier($modifier, $callback) { $this->_modifiers[$modifier] = $callback; return $this; } /** - * @param $compiler - * @param $parser + * @param string $compiler + * @param string $parser * @return Aspect */ - public function setCompiler($compiler, $parser) { + public function addCompiler($compiler, $parser) { $this->_actions[$compiler] = array( 'type' => self::INLINE_COMPILER, 'parser' => $parser @@ -271,31 +330,46 @@ class Aspect { } /** - * @param $compiler - * @param array $parsers + * @param string $compiler + * @param string $open_parser + * @param string $close_parser * @param array $tags * @return Aspect */ - public function setBlockCompiler($compiler, array $parsers, array $tags = array()) { + public function addBlockCompiler($compiler, $open_parser, $close_parser = self::DEFAULT_CLOSE_COMPILER, array $tags = array()) { $this->_actions[$compiler] = array( 'type' => self::BLOCK_COMPILER, - 'open' => $parsers["open"], - 'close' => isset($parsers["close"]) ? $parsers["close"] : 'Aspect\Compiler::stdClose', + 'open' => $open_parser, + 'close' => $close_parser ?: self::DEFAULT_CLOSE_COMPILER, 'tags' => $tags, ); return $this; } /** - * @param $function - * @param $callback - * @param null $parser + * @param string $function + * @param callable $callback + * @param string $parser * @return Aspect */ - public function setFunction($function, $callback, $parser = null) { + public function addFunction($function, $callback, $parser = self::DEFAULT_FUNC_PARSER) { $this->_actions[$function] = array( 'type' => self::INLINE_FUNCTION, - 'parser' => $parser ?: 'Aspect\Compiler::stdFuncParser', + 'parser' => $parser ?: self::DEFAULT_FUNC_PARSER, + 'function' => $callback, + ); + return $this; + } + + /** + * @param string $function + * @param callable $callback + * @return Aspect + */ + public function addFunctionSmart($function, $callback) { + $this->_actions[$function] = array( + 'type' => self::INLINE_FUNCTION, + 'parser' => self::SMART_FUNC_PARSER, 'function' => $callback, ); return $this; @@ -308,7 +382,7 @@ class Aspect { * @param null $parser_close * @return Aspect */ - public function setBlockFunction($function, $callback, $parser_open = null, $parser_close = null) { + public function addBlockFunction($function, $callback, $parser_open = null, $parser_close = null) { $this->_actions[$function] = array( 'type' => self::BLOCK_FUNCTION, 'open' => $parser_open ?: 'Aspect\Compiler::stdFuncOpen', @@ -322,29 +396,11 @@ class Aspect { * @param array $funcs * @return Aspect */ - public function setAllowedFunctions(array $funcs) { + public function addAllowedFunctions(array $funcs) { $this->_allowed_funcs = $this->_allowed_funcs + array_flip($funcs); return $this; } - /** - * @param callable $callback - * @return Aspect - */ - public function setFunctionsLoader($callback) { - $this->_loader_func = $callback; - return $this; - } - - /** - * @param callable $callback - * @return Aspect - */ - public function setModifiersLoader($callback) { - $this->_loader_mod = $callback; - return $this; - } - /** * @param $modifier * @return mixed @@ -355,8 +411,6 @@ class Aspect { return $this->_modifiers[$modifier]; } elseif($this->isAllowedFunction($modifier)) { return $modifier; - } elseif($this->_loader_mod && $this->_loadModifier($modifier)) { - return $this->_modifiers[$modifier]; } else { throw new \Exception("Modifier $modifier not found"); } @@ -369,29 +423,6 @@ class Aspect { public function getFunction($function) { if(isset($this->_actions[$function])) { return $this->_actions[$function]; - } elseif($this->_loader_func && $this->_loadFunction($function)) { - return $this->_actions[$function]; - } else { - return false; - } - } - - private function _loadModifier($modifier) { - $mod = call_user_func($this->_loader_mod, $modifier); - if($mod) { - $this->_modifiers[$modifier] = $mod; - return true; - } else { - return false; - } - } - - private function _loadFunction($function) { - $func = call_user_func($this->_loader_func, $function); - if($func && isset(self::$_actions_defaults[ $func["type"] ])) { - - $this->_actions[$function] = $func + self::$_actions_defaults[ $func["type"] ]; - return true; } else { return false; } @@ -419,23 +450,23 @@ class Aspect { * Add template directory * @static * @param string $dir + * @return \Aspect * @throws \InvalidArgumentException */ public function addTemplateDir($dir) { - $_dir = realpath($dir); - if(!$_dir) { - throw new \InvalidArgumentException("Invalid template dir: $dir"); - } - $this->_tpl_path[] = $_dir; + $this->_provider->addTemplateDir($dir); + return $this; + } + + public function addProvider($scm, \Aspect\Provider $provider) { + $this->_providers[$scm] = $provider; } /** * Set options. May be bitwise mask of constants DENY_METHODS, DENY_INLINE_FUNCS, DENY_SET_VARS, INCLUDE_SOURCES, * FORCE_COMPILE, CHECK_MTIME, or associative array with boolean values: - * disable_methods - disable all call method in template + * disable_methods - disable all calls method in template * disable_native_funcs - disable all native PHP functions in template - * disable_set_vars - forbidden rewrite variables - * include_sources - insert comments with source code into compiled template * force_compile - recompile template every time (very slow!) * compile_check - check template modifications (slow!) * @param int|array $options @@ -456,6 +487,23 @@ class Aspect { return $this->_options; } + /** + * @param bool|string $scm + * @return Aspect\Provider + * @throws InvalidArgumentException + */ + public function getProvider($scm = false) { + if($scm) { + if(isset($this->_provider[$scm])) { + return $this->_provider[$scm]; + } else { + throw new InvalidArgumentException("Provider for '$scm' not found"); + } + } else { + return $this->_provider; + } + } + /** * Execute template and write result into stdout * @@ -486,8 +534,11 @@ class Aspect { * @return Aspect\Template */ public function getTemplate($template) { + if(isset($this->_storage[ $template ])) { - if(($this->_options & self::CHECK_MTIME) && !$this->_check($template)) { + /** @var Aspect\Template $tpl */ + $tpl = $this->_storage[ $template ]; + if(($this->_options & self::CHECK_MTIME) && !$tpl->isValid()) { return $this->_storage[ $template ] = $this->compile($template); } else { return $this->_storage[ $template ]; @@ -503,7 +554,7 @@ class Aspect { * Add custom template into storage * @param Aspect\Render $template */ - public function storeTemplate(Aspect\Render $template) { + public function addTemplate(Aspect\Render $template) { $this->_storage[ $template->getName() ] = $template; $template->setStorage($this); } @@ -517,7 +568,7 @@ class Aspect { */ protected function _load($tpl) { $file_name = $this->_getHash($tpl); - if(!is_file($this->_compile_dir."/".$file_name) || ($this->_options & self::CHECK_MTIME) && !$this->_check($tpl)) { + if(!is_file($this->_compile_dir."/".$file_name)) { return $this->compile($tpl); } else { /** @var Aspect\Render $tpl */ @@ -527,25 +578,6 @@ class Aspect { } } - /** - * @param string $template - * @return bool - */ - private function _check($template) { - return $this->_isActual($template, filemtime($this->_compile_dir."/".$this->_getHash($template))); - } - - /** - * Check, if template is actual - * @param $template - * @param $compiled_time - * @return bool - */ - protected function _isActual($template, $compiled_time) { - clearstatcache(false, $template = $this->_getTemplatePath($template)); - return filemtime($template) < $compiled_time; - } - /** * Generate unique name of compiled template * @@ -557,45 +589,48 @@ class Aspect { return basename($tpl).".".crc32($hash).".".strlen($hash).".php"; } - /** - * Compile and save template - * - * - * @param string $tpl - * @throws \RuntimeException - * @return \Aspect\Template - */ - public function compile($tpl) { - $file_name = $this->_compile_dir."/".$this->_getHash($tpl); - $template = new Template($this, $this->_loadCode($tpl), $tpl); - $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?"); - } - fwrite($tpl_fp, $template->getTemplateCode()); - fclose($tpl_fp); - if(!rename($tpl_tmp, $file_name)) { - throw new \RuntimeException("Can not to move $tpl_tmp to $tpl"); - } + /** + * Compile and save template + * + * @param string $tpl + * @param bool $store + * @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); + 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?"); + } + 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"); + } + } return $template; } /** - * Remove all compiled templates. Warning! Do cleanup the compiled directory. + * Remove all compiled templates. + * + * @param string $scm * @return int - * @api */ - public function compileAll() { + public function compileAll($scm = null) { //return FS::rm($this->_compile_dir.'/*'); } /** * @param string $tpl * @return bool - * @api */ - public function clearCompileTemplate($tpl) { + public function clearCompiledTemplate($tpl) { $file_name = $this->_compile_dir."/".$this->_getHash($tpl); if(file_exists($file_name)) { return unlink($file_name); @@ -606,38 +641,11 @@ class Aspect { /** * @return int - * @api */ public function clearAllCompiles() { } - /** - * Get template path - * @param $tpl - * @return string - * @throws \RuntimeException - */ - private function _getTemplatePath($tpl) { - foreach($this->_tpl_path as $tpl_path) { - if(($path = stream_resolve_include_path($tpl_path."/".$tpl)) && strpos($path, $tpl_path) === 0) { - return $path; - } - } - throw new \RuntimeException("Template $tpl not found"); - } - - /** - * Code loader - * - * @param string $tpl - * @return string - * @throws \RuntimeException - */ - protected function _loadCode(&$tpl) { - return file_get_contents($tpl = $this->_getTemplatePath($tpl)); - } - /** * Compile code to template * diff --git a/src/Aspect/Compiler.php b/src/Aspect/Compiler.php index c8f02b3..3f754d1 100644 --- a/src/Aspect/Compiler.php +++ b/src/Aspect/Compiler.php @@ -4,6 +4,9 @@ use Aspect\Tokenizer; use Aspect\Template; use Aspect\Scope; +/** + * Compilers collection + */ class Compiler { /** * Tag {include ...} @@ -11,7 +14,7 @@ class Compiler { * @static * @param Tokenizer $tokens * @param Template $tpl - * @throws \Exception + * @throws ImproperUseException * @return string */ public static function tagInclude(Tokenizer $tokens, Template $tpl) { @@ -22,7 +25,7 @@ class Compiler { } elseif (isset($p["file"])) { $file_name = $p["file"]; } else { - throw new \Exception("{include} require 'file' parameter"); + throw new ImproperUseException("The tag {include} requires 'file' parameter"); } unset($p["file"], $p[0]); if($p) { @@ -46,20 +49,18 @@ class Compiler { return 'if('.$scope->tpl->parseExp($tokens, true).') {'; } - /** - * Tag {elseif ...} - * - * @static - * @param Tokenizer $tokens - * @param Tokenizer $tokens - * @param Scope $scope - * @throws \Exception - * @internal param \Exception $ - * @return string - */ + /** + * Tag {elseif ...} + * + * @static + * @param Tokenizer $tokens + * @param Scope $scope + * @throws ImproperUseException + * @return string + */ public static function tagElseIf(Tokenizer $tokens, Scope $scope) { if($scope["else"]) { - throw new \Exception('Incorrect use of the tag {else if}'); + throw new ImproperUseException('Incorrect use of the tag {elseif}'); } return '} elseif('.$scope->tpl->parseExp($tokens, true).') {'; } @@ -85,8 +86,7 @@ class Compiler { * @param Tokenizer $tokens * @param Tokenizer $tokens * @param Scope $scope - * @throws \Exception - * @internal param \Exception $ + * @throws ImproperUseException * @return string */ public static function foreachOpen(Tokenizer $tokens, Scope $scope) { @@ -102,12 +102,7 @@ class Compiler { $prepend = $uid.' = '.$from.';'; $from = $uid; } else { - - if($tokens->valid()) { - throw new \Exception("Unexpected token '".$tokens->current()."' in 'foreach'"); - } else { - throw new \Exception("Unexpected end of 'foreach'"); - } + throw new UnexpectedException($tokens, null, "tag {foreach}"); } $tokens->get(T_AS); $tokens->next(); @@ -124,7 +119,7 @@ class Compiler { while($token = $tokens->key()) { $param = $tokens->get(T_STRING); if(!isset($p[ $param ])) { - throw new \Exception("Unknown parameter '$param'"); + throw new ImproperUseException("Unknown parameter '$param' in {foreach}"); } $tokens->getNext("="); $tokens->next(); @@ -166,7 +161,7 @@ class Compiler { * @param Scope $scope * @return string */ - public static function foreachElse(Tokenizer $tokens, Scope $scope) { + public static function foreachElse($tokens, Scope $scope) { $scope["no-break"] = $scope["no-continue"] = $scope["else"] = true; return " {$scope['after']} } } else {"; } @@ -179,7 +174,7 @@ class Compiler { * @param Scope $scope * @return string */ - public static function foreachClose(Tokenizer $tokens, Scope $scope) { + public static function foreachClose($tokens, Scope $scope) { if($scope["else"]) { return '}'; } else { @@ -192,7 +187,7 @@ class Compiler { * @param Tokenizer $tokens * @param Scope $scope * @return string - * @throws \Exception + * @throws ImproperUseException */ public static function forOpen(Tokenizer $tokens, Scope $scope) { $p = array("index" => false, "first" => false, "last" => false, "step" => 1, "to" => false, "max" => false, "min" => false); @@ -213,7 +208,7 @@ class Compiler { $condition = "$var >= {$p['to']}"; if($p["last"]) $c = "($var + {$p['step']}) < {$p['to']}"; } else { - throw new \Exception("Invalid step value"); + throw new ImproperUseException("Invalid step value if {for}"); } } else { $condition = "({$p['step']} > 0 && $var <= {$p['to']} || {$p['step']} < 0 && $var >= {$p['to']})"; @@ -261,7 +256,7 @@ class Compiler { * @param Scope $scope * @return string */ - public static function forClose(Tokenizer $tokens, Scope $scope) { + public static function forClose($tokens, Scope $scope) { if($scope["else"]) { return '}'; } else { @@ -318,14 +313,14 @@ class Compiler { * @static * @param Tokenizer $tokens * @param Scope $scope - * @throws \Exception + * @throws ImproperUseException * @return string */ - public static function tagContinue(Tokenizer $tokens, Scope $scope) { + public static function tagContinue($tokens, Scope $scope) { if(empty($scope["no-continue"])) { return 'continue;'; } else { - throw new \Exception("Incorrect use of the tag {continue}"); + throw new ImproperUseException("Improper usage of the tag {continue}"); } } @@ -335,7 +330,7 @@ class Compiler { * @static * @return string */ - public static function tagDefault(Tokenizer $tokens, Scope $scope) { + public static function tagDefault($tokens, Scope $scope) { $code = 'default: '; if($scope["switch"]) { unset($scope["no-break"], $scope["no-continue"]); @@ -350,21 +345,38 @@ class Compiler { * * @static * @param Tokenizer $tokens - * @param Scope $scope - * @throws \Exception + * @param Scope $scope + * @throws ImproperUseException * @return string */ - public static function tagBreak(Tokenizer $tokens, Scope $scope) { + public static function tagBreak($tokens, Scope $scope) { if(empty($scope["no-break"])) { return 'break;'; } else { - throw new \Exception("Incorrect use of the tag {break}"); + throw new ImproperUseException("Improper usage of the tag {break}"); } } - public static function tagExtends(Tokenizer $tokens, Template $tpl) { + /** + * 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 + * @param Template $tpl + * @throws ImproperUseException + * @return string + */ + public static function tagExtends(Tokenizer $tokens, Template $tpl) { if(!empty($tpl->_extends)) { - throw new \Exception("Only one {extends} allowed"); + throw new ImproperUseException("Only one {extends} allowed"); } $p = $tpl->parseParams($tokens); if(isset($p[0])) { @@ -372,17 +384,63 @@ class Compiler { } elseif (isset($p["file"])) { $tpl_name = $p["file"]; } else { - throw new \Exception("{extends} require 'file' parameter"); + throw new ImproperUseException("{extends} require 'file' parameter"); + } + $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 + return "/* Static extends */"; + } else { // dynamic extends + $tpl->_extends = $tpl_name; + return '/* Dynamic extends */'."\n".'$parent = $tpl->getStorage()->getTemplate('.$tpl_name.');'; } - $tpl->addPostCompile(__CLASS__."::extendBody"); - $tpl->_extends = $tpl_name; - return '$parent = $tpl->getStorage()->getTemplate('.$tpl_name.');'; } - public static function extendBody(&$body, Template $tpl) { - $body = 'blocks)) {$tpl->blocks = array();} ob_start(); ?>'.$body.'blocks = &$tpl->blocks; $parent->display((array)$tpl); unset($tpl->blocks, $parent->blocks); ?>'; + /** + * Post compile method for {extends ...} tag + * @param $body + * @param Template $tpl + */ + public static function extendBody(&$body, $tpl) { + if(isset($tpl->_extends)) { // is child + if(is_object($tpl->_extends)) { // static extends + $t = $tpl; + while(isset($t->_extends)) { + $t->compile(); + $t->_blocks += (array)$t->_extends->_blocks; + $t = $t->_extends; + + } + + if(empty($t->_blocks)) { + $body = $t->getBody(); + } else { + $b = $t->getBody(); + foreach($t->_blocks as $name => $pos) { + + } + } + } else { // dynamic extends + $body .= 'blocks = &$tpl->blocks; $parent->display((array)$tpl); unset($tpl->blocks, $parent->blocks); ?>'; + //return '$tpl->blocks['.$scope["name"].'] = ob_get_clean();'; + } + } + /*$body = 'blocks)) {$tpl->blocks = array();} ob_start(); ?>'.$body.'blocks = &$tpl->blocks; $parent->display((array)$tpl); unset($tpl->blocks, $parent->blocks); ?>';*/ } + + public static function tagUse(Tokenizer $tokens, Template $tpl) { + + } + + /** + * Tag {block ...} + * @param Tokenizer $tokens + * @param Scope $scope + * @return string + * @throws ImproperUseException + */ public static function tagBlockOpen(Tokenizer $tokens, Scope $scope) { $p = $scope->tpl->parseParams($tokens); if(isset($p["name"])) { @@ -390,23 +448,63 @@ class Compiler { } elseif (isset($p[0])) { $scope["name"] = $p[0]; } else { - throw new \Exception("{block} require name parameter"); - } - - if($scope->closed) { - return 'isset($tpl->blocks['.$scope["name"].']) ? $tpl->blocks[] : "" ;'; - } else { - return 'ob_start();'; + throw new ImproperUseException("{block} must be named"); } + if(isset($scope->tpl->_extends)) { // is child + if(is_object($scope->tpl->_extends)) { // static extends + $code = ""; + } else { // dynamic extends + $code = 'if(empty($tpl->blocks['.$scope["name"].'])) { ob_start();'; + } + } else { // is parent + if(isset($scope->tpl->_blocks[ $scope["name"] ])) { // skip own block and insert child's block after + $scope["body"] = $scope->tpl->_body; + $scope->tpl->_body = ""; + return ''; + } else { + $code = 'if(isset($tpl->blocks['.$scope["name"].'])) { echo $tpl->blocks['.$scope["name"].']; } else {'; + } + } + $scope["offset"] = strlen($scope->tpl->getBody()) + strlen($code); + return $code; } - public static function tagBlockClose(Tokenizer $tokens, Scope $scope) { - if(isset($scope->tpl->_extends)) { + /** + * Close tag {/block} + * @param Tokenizer $tokens + * @param Scope $scope + * @return string + */ + public static function tagBlockClose($tokens, Scope $scope) { + $scope->tpl->_blocks[ self::isScalar($scope["name"]) ] = substr($scope->tpl->getBody(), $scope["offset"]); + if(isset($scope->tpl->_extends)) { // is child + if(is_object($scope->tpl->_extends)) { // static extends + return ""; + } else { // dynamic extends + return '$tpl->blocks['.$scope["name"].'] = ob_get_clean(); }'; + } + } else { // is parent + if(isset($scope["body"])) { + $scope->tpl->_body = $scope["body"].$scope->tpl->_blocks[ $scope["name"] ]; + return ""; + } else { + return '}'; + } + } + /* $scope->tpl->_blocks[ $scope["name"] ] = substr($scope->tpl->getBody(), $scope["offset"]); + return '}';*/ + /*if(isset($scope->tpl->_extends) && is_object($scope->tpl->_extends)) { + + //var_dump("fetched block ".$scope->tpl->_blocks[ $scope["name"] ]); + } else { + return '}'; + }*/ + /*if(isset($scope->tpl->_extends)) { $var = '$i'.$scope->tpl->i++; return $var.' = ob_get_clean(); if('.$var.') $tpl->blocks['.$scope["name"].'] = '.$var.';'; } else { return 'if(empty($tpl->blocks['.$scope["name"].'])) { ob_end_flush(); } else { print($tpl->blocks['.$scope["name"].']); ob_end_clean(); }'; - } + }*/ } /** @@ -420,7 +518,7 @@ class Compiler { } /** - * Standard function tag parser + * Standard function parser * * @static * @param $function @@ -432,6 +530,35 @@ class Compiler { return "echo $function(".self::_toArray($tpl->parseParams($tokens)).', $tpl);'; } + /** + * Smart function parser + * + * @static + * @param $function + * @param Tokenizer $tokens + * @param Template $tpl + * @return string + */ + public static function smartFuncParser($function, Tokenizer $tokens, Template $tpl) { + if(strpos($function, "::")) { + $ref = new \ReflectionMethod($function); + } else { + $ref = new \ReflectionFunction($function); + } + $args = array(); + $params = $tpl->parseParams($tokens); + foreach($ref->getParameters() as $param) { + if(isset($params[ $param->getName() ])) { + $args[] = $params[ $param->getName() ]; + } elseif(isset($params[ $param->getPosition() ])) { + $args[] = $params[ $param->getPosition() ]; + } elseif($param->isOptional()) { + $args[] = $param->getDefaultValue(); + } + } + return "echo $function(".implode(", ", $args).');'; + } + /** * Standard function open tag parser * @@ -453,7 +580,7 @@ class Compiler { * @param Scope $scope * @return string */ - public static function stdFuncClose(Tokenizer $tokens, Scope $scope) { + public static function stdFuncClose($tokens, Scope $scope) { return "echo ".$scope["function"].'('.$scope["params"].', ob_get_clean(), $tpl);'; } @@ -478,6 +605,13 @@ class Compiler { return self::setVar($tokens, $tpl).';'; } + /** + * Set variable expression parser + * @param Tokenizer $tokens + * @param Template $tpl + * @param bool $allow_array + * @return string + */ public static function setVar(Tokenizer $tokens, Template $tpl, $allow_array = true) { $var = $tpl->parseVar($tokens, $tpl::DENY_MODS); @@ -490,4 +624,13 @@ class Compiler { } } + public static function tagModifyOpen(Tokenizer $tokens, Scope $scope) { + $scope["modifiers"] = $scope->tpl->parseModifier($tokens, "ob_get_clean()"); + return "ob_start();"; + } + + public static function tagModifyClose($tokens, Scope $scope) { + return "echo ".$scope["modifiers"].";"; + } + } diff --git a/src/Aspect/Misc.php b/src/Aspect/Misc.php index adad9b4..686daa0 100644 --- a/src/Aspect/Misc.php +++ b/src/Aspect/Misc.php @@ -1,7 +1,7 @@ addTemplateDir($dir); + } + return $this; + } + + public function addTemplateDir($dir) { + if($_dir = realpath($dir)) { + $this->_tpl_path[] = $_dir; + } else { + throw new \LogicException("Template directory {$dir} doesn't exists"); + } + } + + /** + * @param string $tpl + * @return string + */ + public function loadCode($tpl) { + return file_get_contents($tpl = $this->_getTemplatePath($tpl)); + } + + public function getLastModified($tpl) { + clearstatcache(null, $tpl = $this->_getTemplatePath($tpl)); + return filemtime($tpl); + } + + public function getAll() { + + } + + /** + * Get template path + * @param $tpl + * @return string + * @throws \RuntimeException + */ + private function _getTemplatePath($tpl) { + foreach($this->_tpl_path as $tpl_path) { + if(($path = realpath($tpl_path."/".$tpl)) && strpos($path, $tpl_path) === 0) { + return $path; + } + } + throw new \RuntimeException("Template $tpl not found"); + } + + /** + * @param string $tpl + * @return bool + */ + public function isTemplateExists($tpl) { + foreach($this->_tpl_path as $tpl_path) { + if(($path = realpath($tpl_path."/".$tpl)) && strpos($path, $tpl_path) === 0) { + return true; + } + } + + return false; + } + + public function getLastModifiedBatch($tpls) { + $tpls = array_flip($tpls); + foreach($tpls as $tpl => &$time) { + $time = $this->getLastModified($tpl); + } + return $tpls; + } +} diff --git a/src/Aspect/ProviderInterface.php b/src/Aspect/ProviderInterface.php new file mode 100644 index 0000000..c1a69f1 --- /dev/null +++ b/src/Aspect/ProviderInterface.php @@ -0,0 +1,28 @@ +_name = $name; $this->_code = $code; - $this->_fingerprint = $fingerprint; + $this->_time = isset($props["time"]) ? $props["time"] : microtime(true); + $this->_depends = isset($props["depends"]) ? $props["depends"] : array(); } /** @@ -67,17 +70,26 @@ class Render extends \ArrayObject { return $this->_name; } + public function getCompileTime() { + return $this->_time; + } + + /** - * Validate template version - * @param mixed $fingerprint of the template + * Validate template * @return bool */ - public function isValid($fingerprint) { - if($this->_fingerprint) { - return $fingerprint === $this->_fingerprint; - } else { - return true; - } + public function isValid() { + $provider = $this->_aspect->getProvider(strstr($this->_name, ":"), true); + if($provider->getLastModified($this->_name) >= $this->_time) { + return false; + } + foreach($this->_depends as $tpl => $time) { + if($this->_aspect->getTemplate($tpl)->getCompileTime() !== $time) { + return false; + } + } + return true; } /** diff --git a/src/Aspect/Template.php b/src/Aspect/Template.php index 989eed6..0af6b91 100644 --- a/src/Aspect/Template.php +++ b/src/Aspect/Template.php @@ -19,7 +19,7 @@ class Template extends Render { * Template PHP code * @var string */ - private $_body; + public $_body; /** * Call stack * @var Scope[] @@ -41,7 +41,7 @@ class Template extends Render { /** * @var bool */ - private $_literal = false; + private $_ignore = false; /** * Options * @var int @@ -57,45 +57,57 @@ class Template extends Render { * @param Aspect $aspect Template storage * @param string $code template source * @param string $name template name - * @throws CompileException + * @param bool $auto_compile */ - public function __construct(Aspect $aspect, $code, $name = "runtime template") { + 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 function compile() { + if(!isset($this->_src)) { + return; + } + $this->_time = microtime(true); $pos = 0; - while(($start = strpos($code, '{', $pos)) !== false) { // search open-char of tags - switch($code[$start + 1]) { // check next char + while(($start = strpos($this->_src, '{', $pos)) !== false) { // search open-char of tags + switch($this->_src[$start + 1]) { // check next char case "\n": case "\r": case "\t": case " ": case "}": // ignore the tag - $pos = $start + 1; // trying finding tags after the current char + $pos = $start + 1; // try find tags after the current char continue 2; case "*": // if comment block - $end = strpos($code, '*}', $start); // finding end of the comment block - $frag = substr($code, $this->_pos, $start - $end); // read the comment block for precessing + $end = strpos($this->_src, '*}', $start); // finding end of the comment block + $frag = substr($this->_src, $this->_pos, $start - $end); // read the comment block for precessing $this->_line += substr_count($frag, "\n"); // count skipped lines $pos = $end + 1; // trying finding tags after the comment block continue 2; } - $end = strpos($code, '}', $start); // search close-char of the tag + $end = strpos($this->_src, '}', $start); // search close-char of the tag if(!$end) { // if unexpected end of template throw new CompileException("Unclosed tag in line {$this->_line}", 0, 1, $this->_name, $this->_line); } - $frag = substr($code, $this->_pos, $start - $this->_pos); // variable $frag contains chars after last '}' and new '{' - $tag = substr($code, $start, $end - $start + 1); // variable $tag contains aspect tag '{...}' - $this->_line += substr_count($code, "\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 + $frag = substr($this->_src, $this->_pos, $start - $this->_pos); // variable $frag contains chars after last '}' and next '{' + $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); } - $tag = $this->_tag($tag, $this->_trim); + + $tag = $this->_tag($tag, $this->_trim); // dispatching tags + if($this->_trim) { // if current tag has trim flag $frag = rtrim($frag); } - $this->_body .= $frag.$tag; + $this->_body .= str_replace("', $frag).$tag; } - $this->_body .= substr($code, $this->_pos); + $this->_body .= substr($this->_src, $this->_pos); if($this->_stack) { $_names = array(); $_line = 0; @@ -105,7 +117,7 @@ class Template extends Render { } $_names[] = $scope->name.' defined on line '.$scope->line; } - throw new CompileException("Unclosed tags: ".implode(", ", $_names), 0, 1, $this->_name, $_line); + throw new CompileException("Unclosed block tags: ".implode(", ", $_names), 0, 1, $this->_name, $_line); } unset($this->_src); if($this->_post) { @@ -128,13 +140,18 @@ class Template extends Render { } /** - * Return PHP code of PHP file of template + * Return PHP code for saving to file * @return string */ public function getTemplateCode() { return "_name."' compiled at ".date('Y-m-d H:i:s')." */\n". - "return new Aspect\\Render('{$this->_name}', ".$this->_getClosureCode().", ".$this->_options.");\n"; + "return new Aspect\\Render('{$this->_name}', ".$this->_getClosureCode().", ".var_export(array( + "options" => $this->_options, + //"provider" => + "time" => $this->_time, + "depends" => $this->_depends + ), true).");\n"; } /** @@ -164,6 +181,14 @@ class Template extends Render { } + /** + * Add depends from template + * @param Render $tpl + */ + public function addDepend(Render $tpl) { + $this->_depends[$tpl->getName()] = $tpl->getCompileTime(); + } + /** * Execute template and return result as string * @param array $values for template @@ -198,9 +223,9 @@ class Template extends Render { $trim = false; } $token = trim($token); - if($this->_literal) { - if($token === '/literal') { - $this->_literal = false; + if($this->_ignore) { + if($token === '/ignore') { + $this->_ignore = false; return ''; } else { return $src; @@ -229,15 +254,17 @@ class Template extends Render { if($tokens->key()) { // if tokenizer still have tokens throw new UnexpectedException($tokens); } - if($this->_options & Aspect::INCLUDE_SOURCES) { + if(!$code) { + return ""; + } else { return "_name}:{$this->_line}: {$src} */\n {$code} ?>"; - } else { - return ""; } + } catch (ImproperUseException $e) { + throw new CompileException($e->getMessage()." in {$this} line {$this->_line}", 0, E_ERROR, $this->_name, $this->_line, $e); } catch (\LogicException $e) { - throw new SecurityException($e->getMessage()." in {$this} line {$this->_line}, near '{".$tokens->getSnippetAsString(0,0)."' <- there", 0, 1, $this->_name, $this->_line, $e); + throw new SecurityException($e->getMessage()." in {$this} line {$this->_line}, near '{".$tokens->getSnippetAsString(0,0)."' <- there", 0, E_ERROR, $this->_name, $this->_line, $e); } catch (\Exception $e) { - throw new CompileException($e->getMessage()." in {$this} line {$this->_line}, near '{".$tokens->getSnippetAsString(0,0)."' <- there", 0, 1, $this->_name, $this->_line, $e); + throw new CompileException($e->getMessage()." in {$this} line {$this->_line}, near '{".$tokens->getSnippetAsString(0,0)."' <- there", 0, E_ERROR, $this->_name, $this->_line, $e); } } @@ -277,8 +304,8 @@ class Template extends Render { return 'echo '.$this->parseExp($tokens).';'; } - if($action === "literal") { - $this->_literal = true; + if($action === "ignore") { + $this->_ignore = true; $tokens->next(); return ''; } @@ -409,9 +436,6 @@ class Template extends Render { $_exp .= $tokens->getAndNext(); } elseif($term && !$cond && !$tokens->isLast()) { if($tokens->is(Tokenizer::MACRO_EQUALS) && $term === 2) { - if($this->_options & Aspect::DENY_SET_VARS) { - throw new \LogicException("Forbidden to set a variable"); - } $_exp .= ' '.$tokens->getAndNext().' '; $term = 0; } else { @@ -507,14 +531,34 @@ class Template extends Render { } } elseif($t === T_DNUMBER) { $_var .= '['.substr($tokens->getAndNext(), 1).']'; - } elseif($t === "?") { + } elseif($t === "?" || $t === "!") { $pure_var = false; + $empty = ($t === "?"); $tokens->next(); if($tokens->is(":")) { $tokens->next(); - return '(empty('.$_var.') ? ('.$this->parseExp($tokens, true).') : '.$_var.')'; + if($empty) { + return '(empty('.$_var.') ? ('.$this->parseExp($tokens, true).') : '.$_var.')'; + } else { + return '(isset('.$_var.') ? '.$_var.' : ('.$this->parseExp($tokens, true).'))'; + } + } elseif($tokens->is(Tokenizer::MACRO_BINARY, Tokenizer::MACRO_BOOLEAN, Tokenizer::MACRO_MATH) || !$tokens->valid()) { + if($empty) { + return '!empty('.$_var.')'; + } else { + return 'isset('.$_var.')'; + } } else { - return '!empty('.$_var.')'; + $expr1 = $this->parseExp($tokens, true); + if(!$tokens->is(":")) { + throw new UnexpectedException($tokens, null, "ternary operator"); + } + $expr2 = $this->parseExp($tokens, true); + if($empty) { + return '(empty('.$_var.') ? '.$expr2.' : '.$expr1; + } else { + return '(isset('.$_var.') ? '.$expr1.' : '.$expr2; + } } } elseif($t === "!") { $pure_var = false; @@ -802,7 +846,7 @@ class Template extends Render { $params[ $key ] = $this->parseExp($tokens); } else { $params[ $key ] = true; - $params[] = "'".$key."'"; + $params[] = '"'.$key.'"'; } } elseif($tokens->is(Tokenizer::MACRO_SCALAR, '"', '`', T_VARIABLE, "[", '(')) { $params[] = $this->parseExp($tokens); @@ -821,4 +865,5 @@ class Template extends Render { class CompileException extends \ErrorException {} -class SecurityException extends CompileException {} \ No newline at end of file +class SecurityException extends CompileException {} +class ImproperUseException extends \LogicException {} \ No newline at end of file diff --git a/src/Aspect/Tokenizer.php b/src/Aspect/Tokenizer.php index 0c1f93d..da0f335 100644 --- a/src/Aspect/Tokenizer.php +++ b/src/Aspect/Tokenizer.php @@ -6,7 +6,6 @@ defined('T_TRAIT') || define('T_TRAIT', 355); defined('T_TRAIT_C') || define('T_TRAIT_C', 365); /** - * This iterator cannot be rewinded. * Each token have structure * - Token (constant T_* or text) * - Token name (textual representation of the token) @@ -28,14 +27,14 @@ class Tokenizer { * Some text value: foo, bar, new, class ... */ const MACRO_STRING = 1000; - /** - * Unary operation: ~, !, ^ - */ - const MACRO_UNARY = 1001; - /** - * Binary operation (operation between two values): +, -, *, /, &&, or , ||, >=, !=, ... - */ - const MACRO_BINARY = 1002; + /** + * Unary operation: ~, !, ^ + */ + const MACRO_UNARY = 1001; + /** + * Binary operation (operation between two values): +, -, *, /, &&, or , ||, >=, !=, ... + */ + const MACRO_BINARY = 1002; /** * Equal operation */ @@ -44,14 +43,14 @@ class Tokenizer { * Scalar values (such as int, float, escaped strings): 2, 0.5, "foo", 'bar\'s' */ const MACRO_SCALAR = 1004; - /** - * Increment or decrement: ++ -- - */ - const MACRO_INCDEC = 1005; - /** - * Boolean operations: &&, ||, or, xor - */ - const MACRO_BOOLEAN = 1006; + /** + * Increment or decrement: ++ -- + */ + const MACRO_INCDEC = 1005; + /** + * Boolean operations: &&, ||, or, xor + */ + const MACRO_BOOLEAN = 1006; /** * Math operation */ @@ -61,8 +60,8 @@ class Tokenizer { */ const MACRO_COND = 1008; - public $tokens; - public $p = 0; + public $tokens; + public $p = 0; private $_max = 0; private $_last_no = 0; @@ -73,45 +72,45 @@ class Tokenizer { private static $_macros = array( self::MACRO_STRING => array( \T_ABSTRACT => 1, \T_ARRAY => 1, \T_AS => 1, \T_BREAK => 1, \T_BREAK => 1, \T_CASE => 1, - \T_CATCH => 1, \T_CLASS => 1, \T_CLASS_C => 1, \T_CLONE => 1, \T_CONST => 1, \T_CONTINUE => 1, + \T_CATCH => 1, \T_CLASS => 1, \T_CLASS_C => 1, \T_CLONE => 1, \T_CONST => 1, \T_CONTINUE => 1, \T_DECLARE => 1, \T_DEFAULT => 1, \T_DIR => 1, \T_DO => 1, \T_ECHO => 1, \T_ELSE => 1, - \T_ELSEIF => 1, \T_EMPTY => 1, \T_ENDDECLARE => 1, \T_ENDFOR => 1, \T_ENDFOREACH => 1, \T_ENDIF => 1, - \T_ENDSWITCH => 1, \T_ENDWHILE => 1, \T_EVAL => 1, \T_EXIT => 1, \T_EXTENDS => 1, \T_FILE => 1, - \T_FINAL => 1, \T_FOR => 1, \T_FOREACH => 1, \T_FUNCTION => 1, \T_FUNC_C => 1, \T_GLOBAL => 1, - \T_GOTO => 1, \T_HALT_COMPILER => 1, \T_IF => 1, \T_IMPLEMENTS => 1, \T_INCLUDE => 1, \T_INCLUDE_ONCE => 1, - \T_INSTANCEOF => 1, \T_INSTEADOF => 1, \T_INTERFACE => 1, \T_ISSET => 1, \T_LINE => 1, \T_LIST => 1, - \T_LOGICAL_AND => 1, \T_LOGICAL_OR => 1, \T_LOGICAL_XOR => 1, \T_METHOD_C => 1, \T_NAMESPACE => 1, \T_NS_C => 1, - \T_NEW => 1, \T_PRINT => 1, \T_PRIVATE => 1, \T_PUBLIC => 1, \T_PROTECTED => 1, \T_REQUIRE => 1, - \T_REQUIRE_ONCE => 1,\T_RETURN => 1, \T_RETURN => 1, \T_STRING => 1, \T_SWITCH => 1, \T_THROW => 1, - \T_TRAIT => 1, \T_TRAIT_C => 1, \T_TRY => 1, \T_UNSET => 1, \T_UNSET => 1, \T_VAR => 1, - \T_WHILE => 1 + \T_ELSEIF => 1, \T_EMPTY => 1, \T_ENDDECLARE => 1, \T_ENDFOR => 1, \T_ENDFOREACH => 1, \T_ENDIF => 1, + \T_ENDSWITCH => 1, \T_ENDWHILE => 1, \T_EVAL => 1, \T_EXIT => 1, \T_EXTENDS => 1, \T_FILE => 1, + \T_FINAL => 1, \T_FOR => 1, \T_FOREACH => 1, \T_FUNCTION => 1, \T_FUNC_C => 1, \T_GLOBAL => 1, + \T_GOTO => 1, \T_HALT_COMPILER => 1, \T_IF => 1, \T_IMPLEMENTS => 1, \T_INCLUDE => 1, \T_INCLUDE_ONCE => 1, + \T_INSTANCEOF => 1, \T_INSTEADOF => 1, \T_INTERFACE => 1, \T_ISSET => 1, \T_LINE => 1, \T_LIST => 1, + \T_LOGICAL_AND => 1, \T_LOGICAL_OR => 1, \T_LOGICAL_XOR => 1, \T_METHOD_C => 1, \T_NAMESPACE => 1, \T_NS_C => 1, + \T_NEW => 1, \T_PRINT => 1, \T_PRIVATE => 1, \T_PUBLIC => 1, \T_PROTECTED => 1, \T_REQUIRE => 1, + \T_REQUIRE_ONCE => 1,\T_RETURN => 1, \T_RETURN => 1, \T_STRING => 1, \T_SWITCH => 1, \T_THROW => 1, + \T_TRAIT => 1, \T_TRAIT_C => 1, \T_TRY => 1, \T_UNSET => 1, \T_UNSET => 1, \T_VAR => 1, + \T_WHILE => 1 + ), + self::MACRO_INCDEC => array( + \T_INC => 1, \T_DEC => 1 ), - self::MACRO_INCDEC => array( - \T_INC => 1, \T_DEC => 1 - ), self::MACRO_UNARY => array( "!" => 1, "~" => 1, "-" => 1 ), self::MACRO_BINARY => array( \T_BOOLEAN_AND => 1, \T_BOOLEAN_OR => 1, \T_IS_GREATER_OR_EQUAL => 1, \T_IS_EQUAL => 1, \T_IS_IDENTICAL => 1, - \T_IS_NOT_EQUAL => 1,\T_IS_NOT_IDENTICAL => 1, \T_IS_SMALLER_OR_EQUAL => 1, \T_LOGICAL_AND => 1, - \T_LOGICAL_OR => 1, \T_LOGICAL_XOR => 1, \T_SL => 1, \T_SR => 1, - "+" => 1, "-" => 1, "*" => 1, "/" => 1, ">" => 1, "<" => 1, "^" => 1, "%" => 1, "&" => 1 + \T_IS_NOT_EQUAL => 1,\T_IS_NOT_IDENTICAL => 1, \T_IS_SMALLER_OR_EQUAL => 1, \T_LOGICAL_AND => 1, + \T_LOGICAL_OR => 1, \T_LOGICAL_XOR => 1, \T_SL => 1, \T_SR => 1, + "+" => 1, "-" => 1, "*" => 1, "/" => 1, ">" => 1, "<" => 1, "^" => 1, "%" => 1, "&" => 1 + ), + self::MACRO_BOOLEAN => array( + \T_LOGICAL_OR => 1, \T_LOGICAL_XOR => 1, \T_BOOLEAN_AND => 1, \T_BOOLEAN_OR => 1 + ), + self::MACRO_MATH => array( + "+" => 1, "-" => 1, "*" => 1, "/" => 1, "^" => 1, "%" => 1, "&" => 1, "|" => 1 + ), + self::MACRO_COND => array( + \T_IS_EQUAL => 1, \T_IS_IDENTICAL => 1, ">" => 1, "<" => 1, \T_SL => 1, \T_SR => 1, + \T_IS_NOT_EQUAL => 1,\T_IS_NOT_IDENTICAL => 1, \T_IS_SMALLER_OR_EQUAL => 1, ), - self::MACRO_BOOLEAN => array( - \T_LOGICAL_OR => 1, \T_LOGICAL_XOR => 1, \T_BOOLEAN_AND => 1, \T_BOOLEAN_OR => 1 - ), - self::MACRO_MATH => array( - "+" => 1, "-" => 1, "*" => 1, "/" => 1, "^" => 1, "%" => 1, "&" => 1, "|" => 1 - ), - self::MACRO_COND => array( - \T_IS_EQUAL => 1, \T_IS_IDENTICAL => 1, ">" => 1, "<" => 1, \T_SL => 1, \T_SR => 1, - \T_IS_NOT_EQUAL => 1,\T_IS_NOT_IDENTICAL => 1, \T_IS_SMALLER_OR_EQUAL => 1, - ), self::MACRO_EQUALS => array( \T_AND_EQUAL => 1, \T_CONCAT_EQUAL => 1,\T_DIV_EQUAL => 1, \T_MINUS_EQUAL => 1, \T_MOD_EQUAL => 1, - \T_MUL_EQUAL => 1, \T_OR_EQUAL => 1, \T_PLUS_EQUAL => 1, \T_SL_EQUAL => 1, \T_SR_EQUAL => 1, - \T_XOR_EQUAL => 1, '=' => 1 + \T_MUL_EQUAL => 1, \T_OR_EQUAL => 1, \T_PLUS_EQUAL => 1, \T_SL_EQUAL => 1, \T_SR_EQUAL => 1, + \T_XOR_EQUAL => 1, '=' => 1 ), self::MACRO_SCALAR => array( \T_LNUMBER => 1, \T_DNUMBER => 1, \T_CONSTANT_ENCAPSED_STRING => 1 @@ -165,12 +164,12 @@ class Tokenizer { return $tokens; } - public function __construct($query, $decode = 0) { - $this->tokens = self::decode($query, $decode); + public function __construct($query, $decode = 0) { + $this->tokens = self::decode($query, $decode); unset($this->tokens[-1]); $this->_max = count($this->tokens) - 1; $this->_last_no = $this->tokens[$this->_max][3]; - } + } /** * Set the filter callback. Token may be changed by reference or skipped if callback return false. @@ -188,15 +187,15 @@ class Tokenizer { $this->_max = count($this->tokens) - 1; } - /** - * Return the current element - * - * @link http://php.net/manual/en/iterator.current.php - * @return mixed Can return any type. - */ - public function current() { - return $this->curr[1]; - } + /** + * Return the current element + * + * @link http://php.net/manual/en/iterator.current.php + * @return mixed Can return any type. + */ + public function current() { + return $this->curr[1]; + } /** * Move forward to next element @@ -204,14 +203,14 @@ class Tokenizer { * @link http://php.net/manual/en/iterator.next.php * @return Tokenizer */ - public function next() { + public function next() { if($this->p > $this->_max) { return $this; } - $this->p++; + $this->p++; unset($this->prev, $this->curr, $this->next); return $this; - } + } /** * Check token type. If token type is one of expected types return true. Otherwise return false @@ -243,7 +242,7 @@ class Tokenizer { * @return mixed */ public function _next($tokens) { - $this->next(); + $this->next(); if(!$this->curr) { throw new TokenizeException("Unexpected end of expression"); } @@ -260,16 +259,16 @@ class Tokenizer { $expect = ""; } throw new TokenizeException("Unexpected token '".$this->current()."'$expect"); - } + } /** * Fetch next specified token or throw an exception * @return mixed */ public function getNext(/*int|string $token1, int|string $token2, ... */) { - $this->_next(func_get_args()); - return $this->current(); - } + $this->_next(func_get_args()); + return $this->current(); + } /** * Concatenate tokens from the current one to one of the specified and returns the string. @@ -356,12 +355,12 @@ class Tokenizer { * @return mixed */ public function get($token1 /*, $token2 ...*/) { - if($this->curr && $this->_valid(func_get_args(), $this->curr[0])) { - return $this->curr[1]; - } else { + if($this->curr && $this->_valid(func_get_args(), $this->curr[0])) { + return $this->curr[1]; + } else { throw new UnexpectedException($this, func_get_args()); } - } + } /** * Step back @@ -374,7 +373,7 @@ class Tokenizer { $this->p--; unset($this->prev, $this->curr, $this->next); return $this; - } + } /** * Lazy load properties @@ -395,31 +394,31 @@ class Tokenizer { } } - /** - * Return the key of the current element - * @link http://php.net/manual/en/iterator.key.php - * @return mixed scalar on success, or null on failure. - */ - public function key() { - return $this->curr ? $this->curr[0] : null; - } + /** + * Return the key of the current element + * @link http://php.net/manual/en/iterator.key.php + * @return mixed scalar on success, or null on failure. + */ + public function key() { + return $this->curr ? $this->curr[0] : null; + } - /** - * Checks if current position is valid - * @link http://php.net/manual/en/iterator.valid.php - * @return boolean The return value will be casted to boolean and then evaluated. - * Returns true on success or false on failure. - */ - public function valid() { - return (bool)$this->curr; - } + /** + * Checks if current position is valid + * @link http://php.net/manual/en/iterator.valid.php + * @return boolean The return value will be casted to boolean and then evaluated. + * Returns true on success or false on failure. + */ + public function valid() { + return (bool)$this->curr; + } - /** - * Rewind the Iterator to the first element. Disabled. - * @link http://php.net/manual/en/iterator.rewind.php - * @return void Any returned value is ignored. - */ - public function rewind() {} + /** + * Rewind the Iterator to the first element. Disabled. + * @link http://php.net/manual/en/iterator.rewind.php + * @return void Any returned value is ignored. + */ + public function rewind() {} /** * Get token name @@ -428,16 +427,16 @@ class Tokenizer { * @return string */ public static function getName($token) { - if(is_string($token)) { - return $token; - } elseif(is_integer($token)) { - return token_name($token); - } elseif(is_array($token)) { + if(is_string($token)) { + return $token; + } elseif(is_integer($token)) { + return token_name($token); + } elseif(is_array($token)) { return token_name($token[0]); } else { return null; } - } + } /** * Return whitespace of current token @@ -499,15 +498,15 @@ class Tokenizer { } } - /** - * Count elements of an object - * @link http://php.net/manual/en/countable.count.php - * @return int The custom count as an integer. - * The return value is cast to an integer. - */ - public function count() { - return $this->_max; - } + /** + * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + * The return value is cast to an integer. + */ + public function count() { + return $this->_max; + } /** * Get tokens near current token @@ -516,20 +515,20 @@ class Tokenizer { * @return array */ public function getSnippet($before = 0, $after = 0) { - $from = 0; - $to = $this->p; - if($before > 0) { - if($before > $this->p) { - $from = $this->p; - } else { - $from = $before; - } - } elseif($before < 0) { - $from = $this->p + $before; - if($from < 0) { - $from = 0; - } - } + $from = 0; + $to = $this->p; + if($before > 0) { + if($before > $this->p) { + $from = $this->p; + } else { + $from = $before; + } + } elseif($before < 0) { + $from = $this->p + $before; + if($from < 0) { + $from = 0; + } + } if($after > 0) { $to = $this->p + $after; if($to > $this->_max) { @@ -543,13 +542,13 @@ class Tokenizer { } elseif($this->p > $this->_max) { $to = $this->_max; } - $code = array(); - for($i=$from; $i<=$to; $i++) { - $code[] = $this->tokens[ $i ]; - } + $code = array(); + for($i=$from; $i<=$to; $i++) { + $code[] = $this->tokens[ $i ]; + } - return $code; - } + return $code; + } /** * Return snippet as string @@ -596,19 +595,6 @@ class Tokenizer { return $this->curr ? $this->curr[3] : $this->_last_no; } - /** - * Dump (append) token into variable - * - * @param mixed $var - * @param bool $whitespace include whitespace - */ - /*public function appendTo(&$var, $whitespace = false) { - $var .= $this->curr[1]; - if($whitespace && $this->curr[2]) { - $var .= $this->curr[2]; - } - }*/ - /** * Parse code and append tokens. This method move pointer to offset. * @param string $code @@ -642,20 +628,20 @@ class TokenizeException extends \RuntimeException {} * Unexpected token */ class UnexpectedException extends TokenizeException { - public function __construct(Tokenizer $tokens, $expect = null) { + public function __construct(Tokenizer $tokens, $expect = null, $where = null) { if($expect && count($expect) == 1 && is_string($expect[0])) { $expect = ", expect '".$expect[0]."'"; } else { $expect = ""; } if(!$tokens->curr) { - $this->message = "Unexpected end of expression$expect"; + $this->message = "Unexpected end of ".($where?:"expression")."$expect"; } elseif($tokens->curr[1] === "\n") { $this->message = "Unexpected new line$expect"; } elseif($tokens->curr[0] === T_WHITESPACE) { $this->message = "Unexpected whitespace$expect"; } else { - $this->message = "Unexpected token '".$tokens->current()."'$expect"; + $this->message = "Unexpected token '".$tokens->current()."' in ".($where?:"expression")."$expect"; } } }; diff --git a/tests/cases/Aspect/RenderTest.php b/tests/cases/Aspect/RenderTest.php index 4074605..e214504 100644 --- a/tests/cases/Aspect/RenderTest.php +++ b/tests/cases/Aspect/RenderTest.php @@ -13,13 +13,13 @@ class RenderTest extends \PHPUnit_Framework_TestCase { public static function setUpBeforeClass() { self::$render = new Render("render.tpl", function ($tpl) { echo "It is render function ".$tpl["render"]; - }); + }, array()); } public function testCreate() { $r = new Render("test.render.tpl", function () { echo "Test render"; - }); + }, array()); $this->assertSame("Test render", $r->fetch(array())); } diff --git a/tests/cases/Aspect/TemplateTest.php b/tests/cases/Aspect/TemplateTest.php index 4919e57..ce6255f 100644 --- a/tests/cases/Aspect/TemplateTest.php +++ b/tests/cases/Aspect/TemplateTest.php @@ -16,15 +16,7 @@ class TemplateTest extends \PHPUnit_Framework_TestCase { echo "Welcome, {block name='username'}{/block} ({block name='usermail'}{/block})"; }));*/ try { - drop(self::$aspect->compileCode(<< -literal: function () { return 1; } end -asdasd -EOT -)->fetch([])); + } catch(\Exception $e) { echo $e->getTraceAsString(); drop($e->getMessage(), $e->getFile().":".$e->getLine()); @@ -32,10 +24,16 @@ EOT } public function setUp() { - self::$aspect = new Aspect(); - self::$aspect->storeTemplate(new Render("welcome.tpl", function ($tpl) { + 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) { echo "Welcome, ".$tpl["username"]." (".$tpl["email"].")"; - })); + }, array())); + /*$parent = self::$aspect->compileCode('Parent template block1: {block name="bk1"}{/block} + block2: {block "bk2"} default block 2{/block} + {var $bk="bk3"} + block3: {block "$bk"} default block 3{/block} ', "parent.tpl"); + self::$aspect->addTemplate($parent);*/ } @@ -102,7 +100,6 @@ EOT array('hello, {$b[3]$c}!', 'Aspect\CompileException', "Unexpected token '\$c'"), array('hello, {$b[3]c}!', 'Aspect\CompileException', "Unexpected token 'c'"), array('hello, {$b.obj->valid()}!', 'Aspect\SecurityException', "Forbidden to call methods", Aspect::DENY_METHODS), - array('hello, {$b = 5}!', 'Aspect\SecurityException', "Forbidden to set a variable", Aspect::DENY_SET_VARS), ); } @@ -157,8 +154,8 @@ EOT public static function providerModifiersInvalid() { return array( - array('Mod: {$lorem|}!', 'Aspect\CompileException', "Unexpected end of expression"), - array('Mod: {$lorem|json_encode}!', 'Aspect\CompileException', "Modifier json_encode not found", Aspect::DENY_INLINE_FUNCS), + array('Mod: {$lorem|}!', 'Aspect\CompileException', "Unexpected end of expression"), + array('Mod: {$lorem|str_rot13}!', 'Aspect\CompileException', "Modifier str_rot13 not found", Aspect::DENY_INLINE_FUNCS), array('Mod: {$lorem|my_encode}!', 'Aspect\CompileException', "Modifier my_encode not found"), array('Mod: {$lorem|truncate:}!', 'Aspect\CompileException', "Unexpected end of expression"), array('Mod: {$lorem|truncate:abs}!', 'Aspect\CompileException', "Unexpected token 'abs'"), @@ -189,16 +186,16 @@ EOT array('Exp: {!$x} result', $b, 'Exp: result'), array('Exp: {!5} result', $b, 'Exp: result'), array('Exp: {-1} result', $b, 'Exp: -1 result'), - array('Exp: {$z = 5} {$z} result', $b, 'Exp: 5 5 result'), - array('Exp: {$k.i = "str"} {$k.i} result', $b, 'Exp: str str result'), + array('Exp: {$z = 5} {$z} result', $b, 'Exp: 5 5 result'), + array('Exp: {$k.i = "str"} {$k.i} result', $b, 'Exp: str str result'), array('Exp: {($y*$x - (($x+$y) + $y/$x) ^ $y)/4} result', - $b, 'Exp: 53.75 result'), - array('Exp: {$x+max($x, $y)} result', $b, 'Exp: 36 result'), + $b, 'Exp: 53.75 result'), + array('Exp: {$x+max($x, $y)} result', $b, 'Exp: 36 result'), array('Exp: {max(1,2)} result', $b, 'Exp: 2 result'), - array('Exp: {round(sin(pi()), 8)} result', $b, 'Exp: 0 result'), + array('Exp: {round(sin(pi()), 8)} result', $b, 'Exp: 0 result'), array('Exp: {max($x, $y) + round(sin(pi()), 8) - min($x, $y) +3} result', - $b, 'Exp: 21 result'), + $b, 'Exp: 21 result'), ); } @@ -213,7 +210,6 @@ EOT array('If: {$a != 5 => 4} end', 'Aspect\CompileException', "Unexpected token '=>'"), array('If: {$a + (*6)} end', 'Aspect\CompileException', "Unexpected token '*'"), array('If: {$a + ( 6} end', 'Aspect\CompileException', "Brackets don't match"), - array('If: {$a = 4} end', 'Aspect\SecurityException', "Forbidden to set a variable", Aspect::DENY_SET_VARS), ); } @@ -250,7 +246,7 @@ EOT public static function providerIncludeInvalid() { return array( - array('Include {include} template', 'Aspect\CompileException', "{include} require 'file' parameter"), + array('Include {include} template', 'Aspect\CompileException', "The tag {include} requires 'file' parameter"), ); } @@ -292,7 +288,7 @@ EOT return array( array('If: {if} block1 {/if} end', 'Aspect\CompileException', "Unexpected end of expression"), array('If: {if 1} block1 {elseif} block2 {/if} end', 'Aspect\CompileException', "Unexpected end of expression"), - array('If: {if 1} block1 {else} block2 {elseif 0} block3 {/if} end', 'Aspect\CompileException', "Incorrect use of the tag {else if}"), + array('If: {if 1} block1 {else} block2 {elseif 0} block3 {/if} end', 'Aspect\CompileException', "Incorrect use of the tag {elseif}"), array('If: {if 1} block1 {else} block2 {/if} block3 {elseif 0} end', 'Aspect\CompileException', "Unexpected tag 'elseif' (this tag can be used with 'if')"), ); } @@ -349,6 +345,67 @@ EOT ); } + public static function providerTernary() { + $a = array( + "a" => 1, + "em" => "empty", + "empty" => array( + "array" => array(), + "int" => 0, + "string" => "", + "double" => 0.0, + "bool" => false, + ), + "nonempty" => array( + "array" => array(1,2), + "int" => 2, + "string" => "abc", + "double" => 0.2, + "bool" => true, + ) + ); + return array( + // ? + array('{if $a?} right {/if}', $a), + array('{if $unexists?} no way {else} right {/if}', $a), + array('{if $empty.array?} no way {else} right {/if}', $a), + array('{if $empty.int?} no way {else} right {/if}', $a), + array('{if $empty.string?} no way {else} right {/if}', $a), + array('{if $empty.double?} no way {else} right {/if}', $a), + array('{if $empty.bool?} no way {else} right {/if}', $a), + array('{if $empty.unexist?} no way {else} right {/if}', $a), + array('{if $nonempty.array?} right {/if}', $a), + array('{if $nonempty.int?} right {/if}', $a), + array('{if $nonempty.string?} right {/if}', $a), + array('{if $nonempty.double?} right {/if}', $a), + array('{if $nonempty.bool?} right {/if}', $a), + // ?: ... + array('{$a?:"empty"}', $a, "1"), + array('{$unexists?:"empty"}', $a, "empty"), + array('{$empty.array?:"empty"}', $a, "empty"), + array('{$empty.int?:"empty"}', $a, "empty"), + array('{$empty.string?:"empty"}', $a, "empty"), + array('{$empty.double?:"empty"}', $a, "empty"), + array('{$empty.bool?:"empty"}', $a, "empty"), + array('{$empty.unexist?:"empty"}', $a, "empty"), + // ? ... : .... + // ! + array('{if $a!} right {/if}', $a), + array('{if $unexists!} no way {else} right {/if}', $a), + array('{if $empty.array!} right {/if}', $a), + array('{if $empty.int!} right {/if}', $a), + array('{if $empty.string!} right {/if}', $a), + array('{if $empty.double!} right {/if}', $a), + array('{if $empty.bool!} right {/if}', $a), + array('{if $empty.unexist!} no way {else} right {/if}', $a), + array('{if $nonempty.array!} right {/if}', $a), + array('{if $nonempty.int!} right {/if}', $a), + array('{if $nonempty.string!} right {/if}', $a), + array('{if $nonempty.double!} right {/if}', $a), + array('{if $nonempty.bool!} right {/if}', $a), + ); + } + public static function providerForeach() { $a = array( "list" => array(1 => "one", 2 => "two", 3 => "three"), @@ -376,7 +433,7 @@ EOT public static function providerForeachInvalid() { return array( - array('Foreach: {foreach} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected end of 'foreach'"), + array('Foreach: {foreach} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected end of tag {foreach}"), array('Foreach: {foreach $list} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected end of expression"), array('Foreach: {foreach $list+1 as $e} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected token '+'"), array('Foreach: {foreach array_random() as $e} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected token 'array_random'"), @@ -387,7 +444,7 @@ EOT array('Foreach: {foreach $list => $e} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected token '=>'"), array('Foreach: {foreach $list $k => $e} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected token '\$k'"), array('Foreach: {foreach $list as $k =>} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected end of expression"), - array('Foreach: {foreach last=$l $list as $e } {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected token 'last' in 'foreach'"), + array('Foreach: {foreach last=$l $list as $e } {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected token 'last' in tag {foreach}"), array('Foreach: {foreach $list as $e unknown=1} {$e}, {/foreach} end', 'Aspect\CompileException', "Unknown parameter 'unknown'"), array('Foreach: {foreach $list as $e index=$i+1} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected token '+'"), array('Foreach: {foreach $list as $e first=$f+1} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected token '+'"), @@ -395,16 +452,16 @@ EOT array('Foreach: {foreach $list as $e index=max($i,1)} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected token 'max'"), array('Foreach: {foreach $list as $e first=max($i,1)} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected token 'max'"), array('Foreach: {foreach $list as $e last=max($i,1)} {$e}, {/foreach} end', 'Aspect\CompileException', "Unexpected token 'max'"), - array('Foreach: {foreach $list as $e} {$e}, {foreachelse} {break} {/foreach} end', 'Aspect\CompileException', "Incorrect use of the tag {break}"), - array('Foreach: {foreach $list as $e} {$e}, {foreachelse} {continue} {/foreach} end', 'Aspect\CompileException', "Incorrect use of the tag {continue}"), + array('Foreach: {foreach $list as $e} {$e}, {foreachelse} {break} {/foreach} end', 'Aspect\CompileException', "Improper usage of the tag {break}"), + array('Foreach: {foreach $list as $e} {$e}, {foreachelse} {continue} {/foreach} end', 'Aspect\CompileException', "Improper usage of the tag {continue}"), ); } - public static function providerLiteral() { + public static function providerIgnores() { $a = array("a" => "lit. A"); return array( array('{if 0}none{/if} literal: {$a} end', $a, 'literal: lit. A end'), - array('{if 0}none{/if} literal:{literal} {$a} {/literal} end', $a, 'literal: {$a} end'), + array('{if 0}none{/if} literal:{ignore} {$a} {/ignore} end', $a, 'literal: {$a} end'), array('{if 0}none{/if} literal: { $a} end', $a, 'literal: { $a} end'), array('{if 0}none{/if} literal: { $a} end', $a, 'literal: { $a} end'), @@ -443,7 +500,7 @@ EOT return array( array('Switch: {switch}{case 1} one {break}{/switch} end', 'Aspect\CompileException', "Unexpected end of expression"), array('Switch: {switch 1}{case} one {break}{/switch} end', 'Aspect\CompileException', "Unexpected end of expression"), - array('Switch: {switch 1}{break}{case} one {/switch} end', 'Aspect\CompileException', "Incorrect use of the tag {break}"), + array('Switch: {switch 1}{break}{case} one {/switch} end', 'Aspect\CompileException', "Improper usage of the tag {break}"), ); } @@ -496,8 +553,8 @@ EOT array('For: {for first=$i $a=3 to=6} block1 {/for} end', 'Aspect\CompileException', "Unexpected token 'first'"), array('For: {for last=$i $a=3 to=6} block1 {/for} end', 'Aspect\CompileException', "Unexpected token 'last'"), array('For: {for $a=4 to=6 unk=4} block1 {/for} end', 'Aspect\CompileException', "Unknown parameter 'unk'"), - array('For: {for $a=4 to=6} $a: {$a}, {forelse} {break} {/for} end', 'Aspect\CompileException', "Incorrect use of the tag {break}"), - array('For: {for $a=4 to=6} $a: {$a}, {forelse} {continue} {/for} end', 'Aspect\CompileException', "Incorrect use of the tag {continue}"), + array('For: {for $a=4 to=6} $a: {$a}, {forelse} {break} {/for} end', 'Aspect\CompileException', "Improper usage of the tag {break}"), + array('For: {for $a=4 to=6} $a: {$a}, {forelse} {continue} {/for} end', 'Aspect\CompileException', "Improper usage of the tag {continue}"), ); } @@ -509,19 +566,20 @@ EOT array('Layers: {for $a=4 to=6} block1 {if 1} {/for} {/if} end', 'Aspect\CompileException', "Unexpected closing of the tag 'for'"), array('Layers: {switch 1} {if 1} {case 1} {/if} {/switch} end', 'Aspect\CompileException', "Unexpected tag 'case' (this tag can be used with 'switch')"), array('Layers: {/switch} end', 'Aspect\CompileException', "Unexpected closing of the tag 'switch'"), - array('Layers: {if 1} end', 'Aspect\CompileException', "Unclosed tags: if"), + array('Layers: {if 1} end', 'Aspect\CompileException', "Unclosed block tags: if"), ); } - /*public static function providerExtends() { + public static function providerExtends() { return array( - array('{extends file="proto.tpl"}{block name="bk1"} block1 {/block}', "Template extended by block1"), - array('{extends file="proto.tpl"}{block name=bk1} block1 {/block}', "Template extended by block1"), - array('{extends "proto.tpl"}{block "bk1"} block1 {/block}', "Template extended by block1"), - array('{extends "proto.tpl"}{block "bk1"} block1 {/block}{block "bk2"} block2 {/block} garbage', "Template extended by block1"), - array('{extends "proto2.tpl"}{block "bk1"} block1 {/block}{block "bk2"} block2 {/block} garbage', "Template multi-extended by block1"), + array('{extends file="parent.tpl"}{block name="bk1"} block1 {/block}', "Template extended by block1"), + 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 file="parent.tpl"}{block "bk1"} block1 {/block}{block "bk2"} block2 {/block} garbage', "Template multi-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"); @@ -629,6 +687,14 @@ EOT $this->execError($code, $exception, $message, $options); } + /** + * @group ternary + * @dataProvider providerTernary + */ + public function testTernary($code, $vars, $result = 'right') { + $this->exec(__FUNCTION__.": $code end", $vars, __FUNCTION__.": $result end"); + } + /** * @dataProvider providerForeach */ @@ -658,9 +724,9 @@ EOT } /** - * @dataProvider providerLiteral + * @dataProvider providerIgnores */ - public function testLiterals($code, $vars, $result) { + public function testIgnores($code, $vars, $result) { $this->exec($code, $vars, $result); } @@ -700,10 +766,24 @@ EOT } /** - * @dataProvider providerExtends + * @group extends */ - public function _testExtends($code, $exception, $message, $options = 0) { - $this->execError($code, $exception, $message, $options); + public function _testExtends() { + echo(self::$aspect->getTemplate("parent.tpl")->getBody()); exit; } + + /** + * @group extends + */ + public function testExtends() { + 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/TokenizerTest.php b/tests/cases/Aspect/TokenizerTest.php index 85d230c..9f2e3e8 100644 --- a/tests/cases/Aspect/TokenizerTest.php +++ b/tests/cases/Aspect/TokenizerTest.php @@ -55,7 +55,7 @@ class TokenizerTest extends \PHPUnit_Framework_TestCase { $tokens->skip(T_STRING)->skip('(')->skip(':'); } catch(\Exception $e) { $this->assertInstanceOf('Aspect\UnexpectedException', $e); - $this->assertStringStartsWith("Unexpected token '3', expect ':'", $e->getMessage()); + $this->assertStringStartsWith("Unexpected token '3' in expression, expect ':'", $e->getMessage()); } $this->assertTrue($tokens->valid()); $this->assertSame("3", $tokens->current()); diff --git a/tests/cases/AspectTest.php b/tests/cases/AspectTest.php index f178fe1..e57135b 100644 --- a/tests/cases/AspectTest.php +++ b/tests/cases/AspectTest.php @@ -33,11 +33,11 @@ class AspectTest extends \PHPUnit_Framework_TestCase { public function testAddRender() { $test = $this; - $this->aspect->storeTemplate(new Render('render.tpl', function($tpl) use ($test) { + $this->aspect->addTemplate(new Render('render.tpl', function($tpl) use ($test) { /** @var \PHPUnit_Framework_TestCase $test */ $test->assertInstanceOf('Aspect\Render', $tpl); echo "Inline render"; - })); + }, array())); $this->assertSame("Inline render", $this->aspect->fetch('render.tpl', array())); } @@ -85,15 +85,15 @@ class AspectTest extends \PHPUnit_Framework_TestCase { } public function testSetModifier() { - $this->aspect->setModifier("mymod", "myMod"); + $this->aspect->addModifier("mymod", "myMod"); $this->tpl('Custom modifier {$a|mymod}'); $this->assertSame("Custom modifier (myMod)Custom(/myMod)", $this->aspect->fetch('custom.tpl', array("a" => "Custom"))); } public function testSetFunctions() { $this->aspect->setForceCompile(true); - $this->aspect->setFunction("myfunc", "myFunc"); - $this->aspect->setBlockFunction("myblockfunc", "myBlockFunc"); + $this->aspect->addFunction("myfunc", "myFunc"); + $this->aspect->addBlockFunction("myblockfunc", "myBlockFunc"); $this->tpl('Custom function {myfunc name="foo"}'); $this->assertSame("Custom function MyFunc:foo", $this->aspect->fetch('custom.tpl', array())); $this->tpl('Custom function {myblockfunc name="foo"} this block1 {/myblockfunc}'); @@ -102,11 +102,8 @@ class AspectTest extends \PHPUnit_Framework_TestCase { public function testSetCompilers() { $this->aspect->setForceCompile(true); - $this->aspect->setCompiler("mycompiler", 'myCompiler'); - $this->aspect->setBlockCompiler("myblockcompiler", array( - 'open' => 'myBlockCompilerOpen', - 'close' => 'myBlockCompilerClose' - ), array( + $this->aspect->addCompiler("mycompiler", 'myCompiler'); + $this->aspect->addBlockCompiler("myblockcompiler", 'myBlockCompilerOpen', 'myBlockCompilerClose', array( 'tag' => 'myBlockCompilerTag' )); $this->tpl('Custom compiler {mycompiler name="bar"}'); diff --git a/tests/resources/template/child1.tpl b/tests/resources/template/child1.tpl new file mode 100644 index 0000000..f8b27b4 --- /dev/null +++ b/tests/resources/template/child1.tpl @@ -0,0 +1,4 @@ +{extends "parent.tpl"} +{block blk1} +blk1.{$a} +{/block} \ No newline at end of file diff --git a/tests/resources/template/child2.tpl b/tests/resources/template/child2.tpl new file mode 100644 index 0000000..0095c8f --- /dev/null +++ b/tests/resources/template/child2.tpl @@ -0,0 +1,4 @@ +{extends "parent.tpl"} +{block blk2} +blk2.{$a} +{/block} \ No newline at end of file diff --git a/tests/resources/template/child3.tpl b/tests/resources/template/child3.tpl new file mode 100644 index 0000000..fb75924 --- /dev/null +++ b/tests/resources/template/child3.tpl @@ -0,0 +1,9 @@ +{extends "parent.tpl"} + +{block blk1} +blk1.{$a} (rewrited) +{/block} + +{block blk3} +blk3.{$a} +{/block} \ No newline at end of file diff --git a/tests/resources/template/parent.tpl b/tests/resources/template/parent.tpl new file mode 100644 index 0000000..93c210b --- /dev/null +++ b/tests/resources/template/parent.tpl @@ -0,0 +1,4 @@ +Parent template +Block1: {block blk1}{/block} +Block2: {block blk2}{/block} +Block3: {block blk3}default{/block} \ No newline at end of file