diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | htdocs/index.php | 28 | ||||
-rw-r--r-- | lib/Skin.php | 19 | ||||
-rw-r--r-- | lib/SkinContext.php | 100 | ||||
-rw-r--r-- | lib/SkinString.php | 21 | ||||
-rw-r--r-- | lib/SkinStringModificationType.php | 7 | ||||
-rw-r--r-- | skin/base.skin.php | 15 | ||||
-rw-r--r-- | skin/main.skin.php | 29 |
9 files changed, 233 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..a27e6f0 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# pure_php_templates + +This isn't a ready-to-use library but rather a concept of native php templates with: +- transparent string escaping +- ability to "include" other templates +- conditions and loops support + +PHP 8+ is required (it is not strictly necessary, but named arguments makes code much easier to write and understand). + + +## License + +GPLv3
\ No newline at end of file diff --git a/htdocs/index.php b/htdocs/index.php new file mode 100644 index 0000000..84c4ff8 --- /dev/null +++ b/htdocs/index.php @@ -0,0 +1,28 @@ +<?php + +set_include_path( + get_include_path().PATH_SEPARATOR.realpath(__DIR__.'/..')); + +error_reporting(E_ALL); +ini_set('display_errors', 1); + +spl_autoload_register(function($class) { + $path = __DIR__.'/../lib/'.$class.'.php'; + if (is_file($path)) + require_once $path; +}); + +SkinContext::setRootDirectory(realpath(__DIR__.'/../skin')); + +$skin = new Skin(); +$skin->title = 'hello world!'; + +$cities = [ + 'Moscow', + 'St. Petersburg', + '<b>New York</b>' // potential xss +]; +echo $skin->renderPage('main/index', + name: "John", + show_cities: true, + cities: $cities); diff --git a/lib/Skin.php b/lib/Skin.php new file mode 100644 index 0000000..e04aa5b --- /dev/null +++ b/lib/Skin.php @@ -0,0 +1,19 @@ +<?php + +class Skin { + + public string $title = 'no title'; + + public function renderPage($f, ...$vars): string { + $f = str_replace('/', '\\', $f); + $ctx = new SkinContext(substr($f, 0, ($pos = strrpos($f, '\\')))); + $body = call_user_func_array([$ctx, substr($f, $pos+1)], $vars); + + $layout_ctx = new SkinContext('base'); + return $layout_ctx->layout( + title: $this->title, + unsafe_body: $body, + ); + } + +} diff --git a/lib/SkinContext.php b/lib/SkinContext.php new file mode 100644 index 0000000..b4192c3 --- /dev/null +++ b/lib/SkinContext.php @@ -0,0 +1,100 @@ +<?php + +class SkinContext { + + protected string $ns; + protected array $data = []; + protected static ?string $root = null; + + public static function setRootDirectory(string $root): void { + self::$root = $root; + } + + public function __construct(string $namespace) { + $this->ns = $namespace; + require_once self::$root.'/'.str_replace('\\', '/', $namespace).'.skin.php'; + } + + public function __call($name, array $arguments) { + $plain_args = array_is_list($arguments); + + $fn = '\\skin\\'.$this->ns.'\\'.$name; + $refl = new ReflectionFunction($fn); + $fparams = $refl->getParameters(); + assert(count($fparams) == count($arguments)+1, "$fn: invalid number of arguments (".count($fparams)." != ".(count($arguments)+1).")"); + + foreach ($fparams as $n => $param) { + if ($n == 0) + continue; // skip $ctx + + $key = $plain_args ? $n-1 : $param->name; + if (!$plain_args && !array_key_exists($param->name, $arguments)) + throw new InvalidArgumentException('argument '.$param->name.' not found'); + + if (is_string($arguments[$key]) || $arguments[$key] instanceof SkinString) { + if (is_string($arguments[$key])) + $arguments[$key] = new SkinString($arguments[$key]); + + if (($pos = strpos($param->name, '_')) !== false) { + $mod_type = match(substr($param->name, 0, $pos)) { + 'unsafe' => SkinStringModificationType::RAW, + 'urlencoded' => SkinStringModificationType::URL, + default => SkinStringModificationType::HTML + }; + } else { + $mod_type = SkinStringModificationType::HTML; + } + $arguments[$key]->setModType($mod_type); + } + } + + array_unshift($arguments, $this); + return call_user_func_array($fn, $arguments); + } + + public function __get(string $name) { + $fn = '\\skin\\'.$this->ns.'\\'.$name; + if (function_exists($fn)) + return [$this, $name]; + + if (array_key_exists($name, $this->data)) + return $this->data[$name]; + } + + public function __set(string $name, $value) { + $this->data[$name] = $value; + } + + public function if_not($cond, $callback, ...$args) { + return $this->_if_condition(!$cond, $callback, ...$args); + } + + public function if_true($cond, $callback, ...$args) { + return $this->_if_condition($cond, $callback, ...$args); + } + + public function if_then_else($cond, $cb1, $cb2) { + return $cond ? $this->_return_callback($cb1) : $this->_return_callback($cb2); + } + + protected function _if_condition($condition, $callback, ...$args) { + if ($condition) + return $this->_return_callback($callback, $args); + return ''; + } + + protected function _return_callback($callback, $args = []) { + if (is_callable($callback)) + return call_user_func_array($callback, $args); + else if (is_string($callback)) + return $callback; + } + + public function for_each(array $iterable, callable $callback) { + $html = ''; + foreach ($iterable as $k => $v) + $html .= call_user_func($callback, $v, $k); + return $html; + } + +} diff --git a/lib/SkinString.php b/lib/SkinString.php new file mode 100644 index 0000000..ef43090 --- /dev/null +++ b/lib/SkinString.php @@ -0,0 +1,21 @@ +<?php + +class SkinString implements Stringable { + + protected SkinStringModificationType $modType; + + public function __construct(protected string $string) {} + + public function setModType(SkinStringModificationType $modType) { + $this->modType = $modType; + } + + public function __toString(): string { + return match ($this->modType) { + SkinStringModificationType::HTML => htmlspecialchars($this->string, ENT_QUOTES, 'UTF-8'), + SkinStringModificationType::URL => urlencode($this->string), + default => $this->string, + }; + } + +}
\ No newline at end of file diff --git a/lib/SkinStringModificationType.php b/lib/SkinStringModificationType.php new file mode 100644 index 0000000..61959ff --- /dev/null +++ b/lib/SkinStringModificationType.php @@ -0,0 +1,7 @@ +<?php + +enum SkinStringModificationType { + case RAW; + case URL; + case HTML; +}
\ No newline at end of file diff --git a/skin/base.skin.php b/skin/base.skin.php new file mode 100644 index 0000000..d5e02e2 --- /dev/null +++ b/skin/base.skin.php @@ -0,0 +1,15 @@ +<?php + +namespace skin\base; + +function layout($ctx, $title, $unsafe_body) { +return <<<HTML +<!doctype html> +<html lang="en"> + <body> + <title>{$title}</title> + </body> + <body>{$unsafe_body}</body> +</html> +HTML; +} diff --git a/skin/main.skin.php b/skin/main.skin.php new file mode 100644 index 0000000..f44f739 --- /dev/null +++ b/skin/main.skin.php @@ -0,0 +1,29 @@ +<?php + +namespace skin\main; + +function index($ctx, $name, $show_cities, $cities) { +return <<<HTML + Hello, {$name}!<br/> + + {$ctx->if_true($show_cities, 'line of truth<br/>')} + {$ctx->if_not(false, $ctx->renderIfFalse, '<b>safe<b>', '<b>unsafe<b>')} + + <ul> + {$ctx->for_each($cities, fn($city, $i) => $ctx->renderIndexCityItem($city, $i+1))} + </ul> +HTML; +} + +function renderIndexCityItem($ctx, $city, $index) { +return <<<HTML + <li>{$index} {$city}</li> +HTML; +} + +function renderIfFalse($ctx, $str, $unsafe_str) { +return <<<HTML +safe: $str<br/> +unsafe: $unsafe_str +HTML; +}
\ No newline at end of file |