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