1<?php 2 3namespace App\Template; 4 5/** 6 * A simple template engine that assigns global variables to the templates and 7 * renders given template. 8 */ 9class Engine 10{ 11 /** 12 * Templates directory contains all application templates. 13 * 14 * @var string 15 */ 16 private $dir; 17 18 /** 19 * Registered callables. 20 * 21 * @var array 22 */ 23 private $callables = []; 24 25 /** 26 * Assigned variables after template initialization and before calling the 27 * render method. 28 * 29 * @var array 30 */ 31 private $variables = []; 32 33 /** 34 * Template context. 35 * 36 * @var Context 37 */ 38 private $context; 39 40 /** 41 * Class constructor. 42 */ 43 public function __construct(string $dir) 44 { 45 if (!is_dir($dir)) { 46 throw new \Exception($dir.' is missing or not a valid directory.'); 47 } 48 49 $this->dir = $dir; 50 } 51 52 /** 53 * This enables assigning new variables to the template scope right after 54 * initializing a template engine. Some variables in templates are like 55 * parameters or globals and should be added only on one place instead of 56 * repeating them at each ...->render() call. 57 */ 58 public function assign(array $variables = []): void 59 { 60 $this->variables = array_replace($this->variables, $variables); 61 } 62 63 /** 64 * Get assigned variables of the template. 65 */ 66 public function getVariables(): array 67 { 68 return $this->variables; 69 } 70 71 /** 72 * Add new template helper function as a callable defined in the (front) 73 * controller to the template scope. 74 */ 75 public function register(string $name, callable $callable): void 76 { 77 if (method_exists(Context::class, $name)) { 78 throw new \Exception( 79 $name.' is already registered by the template engine. Use a different name.' 80 ); 81 } 82 83 $this->callables[$name] = $callable; 84 } 85 86 /** 87 * Renders given template file and populates its scope with variables 88 * provided as array elements. Each array key is a variable name in template 89 * scope and array item value is set as a variable value. 90 */ 91 public function render(string $template, array $variables = []): string 92 { 93 $variables = array_replace($this->variables, $variables); 94 95 $this->context = new Context( 96 $this->dir, 97 $variables, 98 $this->callables 99 ); 100 101 $buffer = $this->bufferize($template, $variables); 102 103 while (!empty($current = array_shift($this->context->tree))) { 104 $buffer = trim($buffer); 105 $buffer .= $this->bufferize($current[0], $current[1]); 106 } 107 108 return $buffer; 109 } 110 111 /** 112 * Processes given template file, merges variables into template scope using 113 * output buffering and returns the rendered content string. Note that $this 114 * pseudo-variable in the closure refers to the scope of the Context class. 115 */ 116 private function bufferize(string $template, array $variables = []): string 117 { 118 if (!is_file($this->dir.'/'.$template)) { 119 throw new \Exception($template.' is missing or not a valid template.'); 120 } 121 122 $closure = \Closure::bind( 123 function ($template, $variables) { 124 $this->current = $template; 125 $this->variables = array_replace($this->variables, $variables); 126 unset($variables, $template); 127 128 if (count($this->variables) > extract($this->variables, EXTR_SKIP)) { 129 throw new \Exception( 130 'Variables with numeric names $0, $1... cannot be imported to scope '.$this->current 131 ); 132 } 133 134 ++$this->bufferLevel; 135 136 ob_start(); 137 138 try { 139 include $this->dir.'/'.$this->current; 140 } catch (\Exception $e) { 141 // Close all opened buffers 142 while ($this->bufferLevel > 0) { 143 --$this->bufferLevel; 144 145 ob_end_clean(); 146 } 147 148 throw $e; 149 } 150 151 --$this->bufferLevel; 152 153 return ob_get_clean(); 154 }, 155 $this->context, 156 Context::class 157 ); 158 159 return $closure($template, $variables); 160 } 161} 162