1<?php 2/** 3 * Handlebars context 4 * Context for a template 5 * 6 * @category Xamin 7 * @package Handlebars 8 * @author fzerorubigd <fzerorubigd@gmail.com> 9 * @author Behrooz Shabani <everplays@gmail.com> 10 * @author Mardix <https://github.com/mardix> 11 * @copyright 2012 (c) ParsPooyesh Co 12 * @copyright 2013 (c) Behrooz Shabani 13 * @copyright 2013 (c) Mardix 14 * @license MIT 15 * @link http://voodoophp.org/docs/handlebars 16 */ 17 18namespace Handlebars; 19 20use InvalidArgumentException; 21use LogicException; 22 23class Context 24{ 25 const DATA_KEY = 'key'; 26 const DATA_INDEX = 'index'; 27 const DATA_FIRST = 'first'; 28 const DATA_LAST = 'last'; 29 30 /** 31 * @var array stack for context only top stack is available 32 */ 33 protected $stack = []; 34 35 /** 36 * @var array index stack for sections 37 */ 38 protected $index = []; 39 40 /** 41 * @var array dataStack stack for data within sections 42 */ 43 protected $dataStack = []; 44 45 /** 46 * @var array key stack for objects 47 */ 48 protected $key = []; 49 50 /** 51 * @var bool enableDataVariables true if @data variables should be used. 52 */ 53 protected $enableDataVariables = false; 54 55 /** 56 * Mustache rendering Context constructor. 57 * 58 * @param mixed $context Default rendering context (default: null) 59 * @param array $options Options for the context. It may contain the following: (default: empty array) 60 * enableDataVariables => Boolean, Enables @data variables (default: false) 61 * 62 * @throws InvalidArgumentException when calling this method when enableDataVariables is not a boolean. 63 */ 64 public function __construct($context = null, $options = []) 65 { 66 if ($context !== null) { 67 $this->stack = [$context]; 68 } 69 70 if (isset($options[Handlebars::OPTION_ENABLE_DATA_VARIABLES])) { 71 if (!is_bool($options[Handlebars::OPTION_ENABLE_DATA_VARIABLES])) { 72 throw new InvalidArgumentException( 73 'Context Constructor "' . Handlebars::OPTION_ENABLE_DATA_VARIABLES . '" option must be a boolean' 74 ); 75 } 76 $this->enableDataVariables = $options[Handlebars::OPTION_ENABLE_DATA_VARIABLES]; 77 } 78 } 79 80 /** 81 * Push a new Context frame onto the stack. 82 * 83 * @param mixed $value Object or array to use for context 84 * 85 * @return void 86 */ 87 public function push($value) 88 { 89 array_push($this->stack, $value); 90 } 91 92 /** 93 * Push an Index onto the index stack 94 * 95 * @param integer $index Index of the current section item. 96 * 97 * @return void 98 */ 99 public function pushIndex($index) 100 { 101 array_push($this->index, $index); 102 } 103 104 /** 105 * Pushes data variables onto the stack. This is used to support @data variables. 106 * @param array $data Associative array where key is the name of the @data variable and value is the value. 107 * @throws LogicException when calling this method without having enableDataVariables. 108 */ 109 public function pushData($data) 110 { 111 if (!$this->enableDataVariables) { 112 throw new LogicException('Data variables are not supported due to the enableDataVariables configuration. Remove the call to data variables or change the setting.'); 113 } 114 array_push($this->dataStack, $data); 115 } 116 117 /** 118 * Push a Key onto the key stack 119 * 120 * @param string $key Key of the current object property. 121 * 122 * @return void 123 */ 124 public function pushKey($key) 125 { 126 array_push($this->key, $key); 127 } 128 129 /** 130 * Pop the last Context frame from the stack. 131 * 132 * @return mixed Last Context frame (object or array) 133 */ 134 public function pop() 135 { 136 return array_pop($this->stack); 137 } 138 139 /** 140 * Pop the last index from the stack. 141 * 142 * @return int Last index 143 */ 144 public function popIndex() 145 { 146 return array_pop($this->index); 147 } 148 149 /** 150 * Pop the last section data from the stack. 151 * 152 * @return array Last data 153 * @throws LogicException when calling this method without having enableDataVariables. 154 */ 155 public function popData() 156 { 157 if (!$this->enableDataVariables) { 158 throw new LogicException('Data variables are not supported due to the enableDataVariables configuration. Remove the call to data variables or change the setting.'); 159 } 160 return array_pop($this->dataStack); 161 } 162 163 /** 164 * Pop the last key from the stack. 165 * 166 * @return string Last key 167 */ 168 public function popKey() 169 { 170 return array_pop($this->key); 171 } 172 173 /** 174 * Get the last Context frame. 175 * 176 * @return mixed Last Context frame (object or array) 177 */ 178 public function last() 179 { 180 return end($this->stack); 181 } 182 183 /** 184 * Get the index of current section item. 185 * 186 * @return mixed Last index 187 */ 188 public function lastIndex() 189 { 190 return end($this->index); 191 } 192 193 /** 194 * Get the key of current object property. 195 * 196 * @return mixed Last key 197 */ 198 public function lastKey() 199 { 200 return end($this->key); 201 } 202 203 /** 204 * Change the current context to one of current context members 205 * 206 * @param string $variableName name of variable or a callable on current context 207 * 208 * @return mixed actual value 209 */ 210 public function with($variableName) 211 { 212 $value = $this->get($variableName); 213 $this->push($value); 214 215 return $value; 216 } 217 218 /** 219 * Get a avariable from current context 220 * Supported types : 221 * variable , ../variable , variable.variable , . 222 * 223 * @param string $variableName variavle name to get from current context 224 * @param boolean $strict strict search? if not found then throw exception 225 * 226 * @throws InvalidArgumentException in strict mode and variable not found 227 * @return mixed 228 */ 229 public function get($variableName, $strict = false) 230 { 231 //Need to clean up 232 $variableName = trim($variableName); 233 234 //Handle data variables (@index, @first, @last, etc) 235 if ($this->enableDataVariables && substr($variableName, 0, 1) == '@') { 236 return $this->getDataVariable($variableName, $strict); 237 } 238 239 $level = 0; 240 while (substr($variableName, 0, 3) == '../') { 241 $variableName = trim(substr($variableName, 3)); 242 $level++; 243 } 244 if (count($this->stack) < $level) { 245 if ($strict) { 246 throw new InvalidArgumentException( 247 'can not find variable in context' 248 ); 249 } 250 251 return ''; 252 } 253 end($this->stack); 254 while ($level) { 255 prev($this->stack); 256 $level--; 257 } 258 $current = current($this->stack); 259 if (!$variableName) { 260 if ($strict) { 261 throw new InvalidArgumentException( 262 'can not find variable in context' 263 ); 264 } 265 return ''; 266 } elseif ($variableName == '.' || $variableName == 'this') { 267 return $current; 268 } else { 269 $chunks = explode('.', $variableName); 270 foreach ($chunks as $chunk) { 271 if (is_string($current) and $current == '') { 272 return $current; 273 } 274 $current = $this->findVariableInContext($current, $chunk, $strict); 275 } 276 } 277 return $current; 278 } 279 280 /** 281 * Given a data variable, retrieves the value associated. 282 * 283 * @param $variableName 284 * @param bool $strict 285 * @return mixed 286 * @throws LogicException when calling this method without having enableDataVariables. 287 */ 288 public function getDataVariable($variableName, $strict = false) 289 { 290 if (!$this->enableDataVariables) { 291 throw new LogicException('Data variables are not supported due to the enableDataVariables configuration. Remove the call to data variables or change the setting.'); 292 } 293 294 $variableName = trim($variableName); 295 296 // make sure we get an at-symbol prefix 297 if (substr($variableName, 0, 1) != '@') { 298 if ($strict) { 299 throw new InvalidArgumentException( 300 'Can not find variable in context' 301 ); 302 } 303 return ''; 304 } 305 306 // Remove the at-symbol prefix 307 $variableName = substr($variableName, 1); 308 309 // determine the level of relative @data variables 310 $level = 0; 311 while (substr($variableName, 0, 3) == '../') { 312 $variableName = trim(substr($variableName, 3)); 313 $level++; 314 } 315 316 // make sure the stack actually has the specified number of levels 317 if (count($this->dataStack) < $level) { 318 if ($strict) { 319 throw new InvalidArgumentException( 320 'Can not find variable in context' 321 ); 322 } 323 324 return ''; 325 } 326 327 // going from the top of the stack to the bottom, traverse the number of levels specified 328 end($this->dataStack); 329 while ($level) { 330 prev($this->dataStack); 331 $level--; 332 } 333 334 /** @var array $current */ 335 $current = current($this->dataStack); 336 337 if (!array_key_exists($variableName, $current)) { 338 if ($strict) { 339 throw new InvalidArgumentException( 340 'Can not find variable in context' 341 ); 342 } 343 344 return ''; 345 } 346 347 return $current[$variableName]; 348 } 349 350 /** 351 * Check if $variable->$inside is available 352 * 353 * @param mixed $variable variable to check 354 * @param string $inside property/method to check 355 * @param boolean $strict strict search? if not found then throw exception 356 * 357 * @throws \InvalidArgumentException in strict mode and variable not found 358 * @return boolean true if exist 359 */ 360 private function findVariableInContext($variable, $inside, $strict = false) 361 { 362 $value = ''; 363 if (($inside !== '0' && empty($inside)) || ($inside == 'this')) { 364 return $variable; 365 } elseif (is_array($variable)) { 366 if (isset($variable[$inside])) { 367 $value = $variable[$inside]; 368 } 369 } elseif (is_object($variable)) { 370 if (isset($variable->$inside)) { 371 $value = $variable->$inside; 372 } elseif (is_callable(array($variable, $inside))) { 373 $value = call_user_func(array($variable, $inside)); 374 } 375 } elseif ($inside === '.') { 376 $value = $variable; 377 } elseif ($strict) { 378 throw new InvalidArgumentException('can not find variable in context'); 379 } 380 return $value; 381 } 382} 383