1<?php
2
3/*
4 * This file is part of Mustache.php.
5 *
6 * (c) 2010-2017 Justin Hileman
7 *
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
10 */
11
12/**
13 * Mustache Template rendering Context.
14 */
15class Mustache_Context
16{
17    private $stack      = array();
18    private $blockStack = array();
19
20    /**
21     * Mustache rendering Context constructor.
22     *
23     * @param mixed $context Default rendering context (default: null)
24     */
25    public function __construct($context = null)
26    {
27        if ($context !== null) {
28            $this->stack = array($context);
29        }
30    }
31
32    /**
33     * Push a new Context frame onto the stack.
34     *
35     * @param mixed $value Object or array to use for context
36     */
37    public function push($value)
38    {
39        array_push($this->stack, $value);
40    }
41
42    /**
43     * Push a new Context frame onto the block context stack.
44     *
45     * @param mixed $value Object or array to use for block context
46     */
47    public function pushBlockContext($value)
48    {
49        array_push($this->blockStack, $value);
50    }
51
52    /**
53     * Pop the last Context frame from the stack.
54     *
55     * @return mixed Last Context frame (object or array)
56     */
57    public function pop()
58    {
59        return array_pop($this->stack);
60    }
61
62    /**
63     * Pop the last block Context frame from the stack.
64     *
65     * @return mixed Last block Context frame (object or array)
66     */
67    public function popBlockContext()
68    {
69        return array_pop($this->blockStack);
70    }
71
72    /**
73     * Get the last Context frame.
74     *
75     * @return mixed Last Context frame (object or array)
76     */
77    public function last()
78    {
79        return end($this->stack);
80    }
81
82    /**
83     * Find a variable in the Context stack.
84     *
85     * Starting with the last Context frame (the context of the innermost section), and working back to the top-level
86     * rendering context, look for a variable with the given name:
87     *
88     *  * If the Context frame is an associative array which contains the key $id, returns the value of that element.
89     *  * If the Context frame is an object, this will check first for a public method, then a public property named
90     *    $id. Failing both of these, it will try `__isset` and `__get` magic methods.
91     *  * If a value named $id is not found in any Context frame, returns an empty string.
92     *
93     * @param string $id Variable name
94     *
95     * @return mixed Variable value, or '' if not found
96     */
97    public function find($id)
98    {
99        return $this->findVariableInStack($id, $this->stack);
100    }
101
102    /**
103     * Find a 'dot notation' variable in the Context stack.
104     *
105     * Note that dot notation traversal bubbles through scope differently than the regular find method. After finding
106     * the initial chunk of the dotted name, each subsequent chunk is searched for only within the value of the previous
107     * result. For example, given the following context stack:
108     *
109     *     $data = array(
110     *         'name' => 'Fred',
111     *         'child' => array(
112     *             'name' => 'Bob'
113     *         ),
114     *     );
115     *
116     * ... and the Mustache following template:
117     *
118     *     {{ child.name }}
119     *
120     * ... the `name` value is only searched for within the `child` value of the global Context, not within parent
121     * Context frames.
122     *
123     * @param string $id Dotted variable selector
124     *
125     * @return mixed Variable value, or '' if not found
126     */
127    public function findDot($id)
128    {
129        $chunks = explode('.', $id);
130        $first  = array_shift($chunks);
131        $value  = $this->findVariableInStack($first, $this->stack);
132
133        foreach ($chunks as $chunk) {
134            if ($value === '') {
135                return $value;
136            }
137
138            $value = $this->findVariableInStack($chunk, array($value));
139        }
140
141        return $value;
142    }
143
144    /**
145     * Find an 'anchored dot notation' variable in the Context stack.
146     *
147     * This is the same as findDot(), except it looks in the top of the context
148     * stack for the first value, rather than searching the whole context stack
149     * and starting from there.
150     *
151     * @see Mustache_Context::findDot
152     *
153     * @throws Mustache_Exception_InvalidArgumentException if given an invalid anchored dot $id
154     *
155     * @param string $id Dotted variable selector
156     *
157     * @return mixed Variable value, or '' if not found
158     */
159    public function findAnchoredDot($id)
160    {
161        $chunks = explode('.', $id);
162        $first  = array_shift($chunks);
163        if ($first !== '') {
164            throw new Mustache_Exception_InvalidArgumentException(sprintf('Unexpected id for findAnchoredDot: %s', $id));
165        }
166
167        $value  = $this->last();
168
169        foreach ($chunks as $chunk) {
170            if ($value === '') {
171                return $value;
172            }
173
174            $value = $this->findVariableInStack($chunk, array($value));
175        }
176
177        return $value;
178    }
179
180    /**
181     * Find an argument in the block context stack.
182     *
183     * @param string $id
184     *
185     * @return mixed Variable value, or '' if not found
186     */
187    public function findInBlock($id)
188    {
189        foreach ($this->blockStack as $context) {
190            if (array_key_exists($id, $context)) {
191                return $context[$id];
192            }
193        }
194
195        return '';
196    }
197
198    /**
199     * Helper function to find a variable in the Context stack.
200     *
201     * @see Mustache_Context::find
202     *
203     * @param string $id    Variable name
204     * @param array  $stack Context stack
205     *
206     * @return mixed Variable value, or '' if not found
207     */
208    private function findVariableInStack($id, array $stack)
209    {
210        for ($i = count($stack) - 1; $i >= 0; $i--) {
211            $frame = &$stack[$i];
212
213            switch (gettype($frame)) {
214                case 'object':
215                    if (!($frame instanceof Closure)) {
216                        // Note that is_callable() *will not work here*
217                        // See https://github.com/bobthecow/mustache.php/wiki/Magic-Methods
218                        if (method_exists($frame, $id)) {
219                            return $frame->$id();
220                        }
221
222                        if (isset($frame->$id)) {
223                            return $frame->$id;
224                        }
225
226                        if ($frame instanceof ArrayAccess && isset($frame[$id])) {
227                            return $frame[$id];
228                        }
229                    }
230                    break;
231
232                case 'array':
233                    if (array_key_exists($id, $frame)) {
234                        return $frame[$id];
235                    }
236                    break;
237            }
238        }
239
240        return '';
241    }
242}
243