Dev, dev and dev. Also, docs, docs and docs.

This commit is contained in:
Ivan Shalganov
2013-02-07 17:37:16 +04:00
parent 36ab6bd08a
commit 06b7fa488b
49 changed files with 2092 additions and 608 deletions

View File

@ -1,10 +1,12 @@
<?php
use Aspect\Template;
use Aspect\Template,
Aspect\ProviderInterface;
/**
* Templater
* Aspect Template Engine
*/
class Aspect {
const VERSION = 1.0;
const INLINE_COMPILER = 1;
const BLOCK_COMPILER = 2;
@ -12,48 +14,55 @@ class Aspect {
const BLOCK_FUNCTION = 4;
const MODIFIER = 5;
const DENY_METHODS = 128;
const DENY_INLINE_FUNCS = 256;
const DENY_SET_VARS = 512;
const DENY_METHODS = 0x10;
const DENY_INLINE_FUNCS = 0x20;
const FORCE_INCLUDE = 0x40;
const INCLUDE_SOURCES = 1024;
const CHECK_MTIME = 0x80;
const FORCE_COMPILE = 0xF0;
const CHECK_MTIME = 2048;
const FORCE_COMPILE = 4096;
const DEFAULT_CLOSE_COMPILER = 'Aspect\Compiler::stdClose';
const DEFAULT_FUNC_PARSER = 'Aspect\Compiler::stdFuncParser';
const DEFAULT_FUNC_OPEN = 'Aspect\Compiler::stdFuncOpen';
const DEFAULT_FUNC_CLOSE = 'Aspect\Compiler::stdFuncOpen';
const SMART_FUNC_PARSER = 'Aspect\Compiler::smartFuncParser';
/**
* @var array list of possible options, as associative array
* @var array of possible options, as associative array
* @see setOptions, addOptions, delOptions
*/
private static $_option_list = array(
"disable_methods" => 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
*

View File

@ -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 = '<?php if(!isset($tpl->blocks)) {$tpl->blocks = array();} ob_start(); ?>'.$body.'<?php ob_end_clean(); $parent->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 .= '<?php $parent->blocks = &$tpl->blocks; $parent->display((array)$tpl); unset($tpl->blocks, $parent->blocks); ?>';
//return '$tpl->blocks['.$scope["name"].'] = ob_get_clean();';
}
}
/*$body = '<?php if(!isset($tpl->blocks)) {$tpl->blocks = array();} ob_start(); ?>'.$body.'<?php ob_end_clean(); $parent->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"].";";
}
}

View File

@ -1,7 +1,7 @@
<?php
namespace Aspect;
class Misc {
/**

78
src/Aspect/Provider.php Normal file
View File

@ -0,0 +1,78 @@
<?php
namespace Aspect;
/**
* Templates provider
* @author Ivan Shalganov
*/
class Provider implements ProviderInterface {
private $_tpl_path = array();
public function setTemplateDirs($dirs) {
foreach((array)$dirs as $dir) {
$this->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;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Aspect;
interface ProviderInterface {
/**
* @param string $tpl
* @return bool
*/
public function isTemplateExists($tpl);
/**
* @param string $tpl
* @return string
*/
public function loadCode($tpl);
/**
* @param string $tpl
* @return int
*/
public function getLastModified($tpl);
public function getLastModifiedBatch($tpls);
/**
* @return array
*/
public function getAll();
}

View File

@ -20,20 +20,23 @@ class Render extends \ArrayObject {
*/
protected $_aspect;
/**
* Signature of the template
* @var mixed
* Timestamp of compilation
* @var float
*/
protected $_fingerprint;
protected $_time = 0.0;
protected $_depends = array();
/**
* @param string $name template name
* @param callable $code template body
* @param mixed $fingerprint signature
* @param mixed $props signature
*/
public function __construct($name, \Closure $code, $fingerprint = null) {
public function __construct($name, \Closure $code, $props = array()) {
$this->_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;
}
/**

View File

@ -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("<?", '<?php echo "<?" ?>', $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 "<?php \n".
"/** Aspect template '".$this->_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 "<?php\n/* {$this->_name}:{$this->_line}: {$src} */\n {$code} ?>";
} else {
return "<?php {$code} ?>";
}
} 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 {}
class SecurityException extends CompileException {}
class ImproperUseException extends \LogicException {}

View File

@ -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";
}
}
};