1<?php 2 3namespace App\Template; 4 5/** 6 * Context represents a template variable scope where $this pseudo-variable can 7 * be used in the templates and context methods can be called as $this->method(). 8 */ 9class Context 10{ 11 /** 12 * Templates directory. 13 * 14 * @var string 15 */ 16 private $dir; 17 18 /** 19 * The current processed template or snippet file. 20 * 21 * @var string 22 */ 23 private $current; 24 25 /** 26 * All assigned and set variables for the template. 27 * 28 * @var array 29 */ 30 private $variables = []; 31 32 /** 33 * Pool of blocks for the template context. 34 * 35 * @var array 36 */ 37 private $blocks = []; 38 39 /** 40 * Parent templates extended by child templates. 41 * 42 * @var array 43 */ 44 public $tree = []; 45 46 /** 47 * Registered callables. 48 * 49 * @var array 50 */ 51 private $callables = []; 52 53 /** 54 * Current nesting level of the output buffering mechanism. 55 * 56 * @var int 57 */ 58 private $bufferLevel = 0; 59 60 /** 61 * Class constructor. 62 */ 63 public function __construct( 64 string $dir, 65 array $variables = [], 66 array $callables = [] 67 ) { 68 $this->dir = $dir; 69 $this->variables = $variables; 70 $this->callables = $callables; 71 } 72 73 /** 74 * Sets a parent layout for the given template. Additional variables in the 75 * parent scope can be defined via the second argument. 76 */ 77 public function extends(string $parent, array $variables = []): void 78 { 79 if (isset($this->tree[$this->current])) { 80 throw new \Exception('Extending '.$parent.' is not possible.'); 81 } 82 83 $this->tree[$this->current] = [$parent, $variables]; 84 } 85 86 /** 87 * Return a block content from the pool by name. 88 */ 89 public function block(string $name): string 90 { 91 return $this->blocks[$name] ?? ''; 92 } 93 94 /** 95 * Starts a new template block. Under the hood a simple separate output 96 * buffering is used to capture the block content. Content can be also 97 * appended to previously set same block name. 98 */ 99 public function start(string $name): void 100 { 101 $this->blocks[$name] = ''; 102 103 ++$this->bufferLevel; 104 105 ob_start(); 106 } 107 108 /** 109 * Append content to a template block. If no block with the key name exists 110 * yet it starts a new one. 111 */ 112 public function append(string $name): void 113 { 114 if (!isset($this->blocks[$name])) { 115 $this->blocks[$name] = ''; 116 } 117 118 ++$this->bufferLevel; 119 120 ob_start(); 121 } 122 123 /** 124 * Ends block output buffering and stores its content into the pool. 125 */ 126 public function end(string $name): void 127 { 128 --$this->bufferLevel; 129 130 $content = ob_get_clean(); 131 132 if (!empty($this->blocks[$name])) { 133 $this->blocks[$name] .= $content; 134 } else { 135 $this->blocks[$name] = $content; 136 } 137 } 138 139 /** 140 * Include template file into existing template. 141 * 142 * @return mixed 143 */ 144 public function include(string $template, array $variables = []) 145 { 146 if (count($variables) > extract($variables, EXTR_SKIP)) { 147 throw new \Exception( 148 'Variables with numeric names $0, $1... cannot be imported to scope '.$template 149 ); 150 } 151 152 return include $this->dir.'/'.$template; 153 } 154 155 /** 156 * Scalpel when preventing XSS vulnerabilities. This escapes given string 157 * and still preserves certain characters as HTML. 158 */ 159 public function e(string $string): string 160 { 161 return htmlspecialchars($string, ENT_QUOTES, 'UTF-8'); 162 } 163 164 /** 165 * Hammer when protecting against XSS. Sanitize strings and replace all 166 * characters to their applicable HTML entities from it. 167 */ 168 public function noHtml(string $string): string 169 { 170 return htmlentities($string, ENT_QUOTES | ENT_HTML5, 'UTF-8'); 171 } 172 173 /** 174 * A proxy to call registered callable. 175 * 176 * @return mixed 177 */ 178 public function __call(string $method, array $arguments) 179 { 180 if (isset($this->callables[$method])) { 181 return call_user_func_array($this->callables[$method], $arguments); 182 } 183 } 184} 185