⬆️ ⬇️

Templating in PHP using lambda functions and closures

Starting with php 5.3, we have a great opportunity to use closures and anonymous functions. They, along with the alternative syntax, are very convenient for use in templating (of course, except when the coder does not need to be given access to php), and templates based on them are fast, easily converted to accelerator bytecode, can support block inheritance, do not require compilation and caching, support skins and are very convenient in development.

It is assumed that the reader has experience with template engines, such as twig. Details under the cut.



To begin with, we will define semantics and syntax.

1) The lambda functions in the template will be called instructions, and we will write them in capital letters;

2) System and service variables in the template begin with an underscore character;

3) All other variables in the template are its direct arguments and are written with a lowercase letter.



The template engine is a service object that has a render method:

public function exec($_template,array $_data=array(),$_skin=null,$_type='php',&$_buffer=null) { } 


In the namespace of this method will be all the most interesting.

To begin with, we define variables and declare several instructions with closures in it, at the end we add processing of the result:

 public function exec($_template,array $_data=array(),$_skin=null,$_type='php',&$_buffer=null) { if (!isset($_skin)) $_skin = $this->api->cfg['default_skin']; if (!$_filename = $this->getFile($_template,$_skin,$_type)) return ''; $_parent = null; $_api = $this->api; //           $R = function($name) use ($_template, $_skin) { echo "/res/t/{$_skin}/{$_template}/$name"; }; //     $BEGIN = function($blockname) { ob_start(); }; //     $END = function($blockname) use (&$_buffer,$_parent) { if (isset($_buffer[$blockname])) { ob_end_clean(); echo $_buffer[$blockname]; } else { $_buffer[$blockname] = isset($_parent)?ob_get_clean():ob_get_flush(); } }; //     $EXTEND = function($template,$type=null) use (&$_parent) { if ($template) $_parent =array($template,$type); }; //      $INCLUDE = function($template,$type=null) use ($_data,$_api,$_skin) { if ($template) echo $_api->templater->exec($template,$_data,$_skin,$type); }; //  css-   dom- $CLASS = function() use ($_template,$_skin) { echo "t-{$_template} s-{$_skin}"; }; if (!isset($this->instructions)) $this->instructions = $this->getInstructions(); //      $V = function(&$var,$default='',$raw=false) use ($api) { if (isset($var)) { if (is_scalar($var)) echo $raw?$var:htmlspecialchars($var); else $api->templater->dump($var); } else { echo $raw?$default:htmlspecialchars($default); } }; //      $GV = function(&$var,$default='') { if (isset($var)) return $var; else return $default; }, extract($_data); //   -   -  ( ,   ) if (!isset($_language)) $_language = $this->api->cfg['default_language']; $L = function(&$stringhash,$default=array('?')) use ($_language,$_api) { if (!isset($stringhash)) $stringhash = $default; if (is_string($stringhash)) { echo htmlspecialchars($stringhash); return; } if (isset($stringhash[$_language[0]])) echo htmlspecialchars($stringhash[$_language[0]]); elseif (isset($stringhash[$_api->cfg['default_language'][0]])) echo htmlspecialchars($stringhash[$_api->cfg['default_language'][0]]); else echo htmlspecialchars(reset($stringhash)); }; extract($this->instructions); ob_start(); include $_filename; $content = ob_get_clean(); if ($_parent) $content = $this->exec($_parent[0],$_data,$_skin,$_parent[1],$_buffer); return $content; } 




Inheritance and redefinition of blocks works as follows:

1) If the $ EXTEND () instruction is specified at the beginning of the template, then the extensible template is specified by the “ancestor” of the current template.

2) At the beginning of the inherited block ("$ BEGIN ()"), the write buffer opens. The name in the parameter is needed only for semantics;

3) When the inherited block is completed, the write buffer is thrown into the output, or the existing stored buffer is thrown out if it was filled (here is a nuance: both the ancestor and the descendant are executed, but one is output; in practice this is not essential);

')

Thus, the child is first rendered, then the ancestor. If there are blocks in the ancestor defined in the descendant, then they are not output, but are replaced by the descendant blocks. At the same time, the ancestor's skin is taken from the descendant skin (set when the render is called). There can be more than two levels of ancestor-child relationship, as a result of the render function there will be only the last block with the specified name.



The $ V, $ L, and $ GV instructions accept only variables (using the change permission "&", which is necessary in order to exclude warnings to a non-existent variable; the variable inside the lambda function will be null)



The $ R instruction is used to display resources bound to a template. For example:

 <?foreach($GV($pictures,array()) as $picture):?> <img src="<?$R($picture)?>"/> <?endforeach;?> 


Additional instructions (for example, work with constants, etc.) can be transferred to the method via extract ($ this-> instructions), this array of lambda functions is formed dynamically.



Example of a template (the class is used in the html tag to indicate height: 100% in one of the skins, for example):

 <!DOCTYPE html> <html class="<?$CLASS()?>" id="<?$V($_id)?>"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <title><?$L($title,array('en'=>'No title','ru'=>' '))?></title> <link rel="stylesheet" href="/res/var/t.css?<?=$_api->build?>" type="text/css" media="all" /> <script type="text/javascript" src="/res/jquery.js?<?=$_api->build?>"></script> <script type="text/javascript" src="/res/var/t.js?<?=$_api->build?>"></script> <script type="text/javascript" src="/res/var/frontend.js?<?=$_api->build?>"></script> </head> <body> <header> <?$BEGIN('header')?> <h1><?$L($title,array('en'=>'No title','ru'=>' '))?></h1> <?$SLOT($langswitch)?> <?$END('header')?> </header> <section> <?$BEGIN('content')?> <?$L($content,array('en'=>'No content','ru'=>' '))?> <?$END('content')?> </section> <footer> <?$BEGIN('footer')?> <?$SLOT($menu_footer)?> <p class="copy"> <?=date('Y')?> MyProject </p> <p class="info"> <?$s=array('en'=>'Generated','ru'=>'');$L($s)?>: <?=date('r')?> </p> <?$END('footer')?> </footer> </body> </html> 




An example of using a template engine when building css:

 . {background: url("<?$R('bg.png')?>")} . > .left {width: <?$C('left-margin')?>; border: 1px <?$C('border-color')?> solid;} 


Here $ C is a manual for working with constants. Thanks to templating, you can make cycles in style files, embed coordinate calculation functions, expand css files, enable on the server side, and much more. Styles start with a dot, because the css collector replaces the dot at the beginning of the line with the template block selector, so block layout is supported.



In the same way, you can pass javascript files through the template engine when assembling project components. For example, this is convenient for substituting paths to ajax requests and other links using the $ PATH ('route_name', $ args) instruction.



All template resources must be collected by the collector in folders, and all css and js templates are prefixed by prefix selectors (see "$ CLASS ()") and glued into one (well, two) files in the add. I will tell about these mechanisms separately in other articles.



I note that this approach exists in the prototype within the framework of the working system, which I, for obvious reasons, will not describe in the article, and is experimental. The main goal was to reduce development time losses due to the long compilation (twig) of a large number of templates, while maintaining acceptable performance. Therefore, I am happy to read about the nuances and pitfalls of this approach from experienced people in the comments.

Source: https://habr.com/ru/post/136516/



All Articles