Passed
Push — fix-8832 ( 2eb5fa )
by Sam
07:48
created

SSViewer_Scope   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 324
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 114
dl 0
loc 324
rs 10
c 0
b 0
f 0
wmc 30
1
<?php
2
3
namespace SilverStripe\View;
4
5
use ArrayIterator;
6
use Countable;
7
use Iterator;
8
9
/**
10
 * This tracks the current scope for an SSViewer instance. It has three goals:
11
 *   - Handle entering & leaving sub-scopes in loops and withs
12
 *   - Track Up and Top
13
 *   - (As a side effect) Inject data that needs to be available globally (used to live in ViewableData)
14
 *
15
 * In order to handle up, rather than tracking it using a tree, which would involve constructing new objects
16
 * for each step, we use indexes into the itemStack (which already has to exist).
17
 *
18
 * Each item has three indexes associated with it
19
 *
20
 *   - Pop. Which item should become the scope once the current scope is popped out of
21
 *   - Up. Which item is up from this item
22
 *   - Current. Which item is the first time this object has appeared in the stack
23
 *
24
 * We also keep the index of the current starting point for lookups. A lookup is a sequence of obj calls -
25
 * when in a loop or with tag the end result becomes the new scope, but for injections, we throw away the lookup
26
 * and revert back to the original scope once we've got the value we're after
27
 */
28
class SSViewer_Scope
29
{
30
    const ITEM = 0;
31
    const ITEM_ITERATOR = 1;
32
    const ITEM_ITERATOR_TOTAL = 2;
33
    const POP_INDEX = 3;
34
    const UP_INDEX = 4;
35
    const CURRENT_INDEX = 5;
36
    const ITEM_OVERLAY = 6;
37
38
    /**
39
     * The stack of previous items ("scopes") - an indexed array of: item, item iterator, item iterator total,
40
     * pop index, up index, current index & parent overlay
41
     *
42
     * @var array
43
     */
44
    private $itemStack = [];
45
46
    /**
47
     * The current "global" item (the one any lookup starts from)
48
     *
49
     * @var object
50
     */
51
    protected $item;
52
53
    /**
54
     * If we're looping over the current "global" item, here's the iterator that tracks with item we're up to
55
     *
56
     * @var Iterator
57
     */
58
    protected $itemIterator;
59
60
    /**
61
     * Total number of items in the iterator
62
     *
63
     * @var int
64
     */
65
    protected $itemIteratorTotal;
66
67
    /**
68
     * A pointer into the item stack for the item that will become the active scope on the next pop call
69
     *
70
     * @var int
71
     */
72
    private $popIndex;
73
74
    /**
75
     * A pointer into the item stack for which item is "up" from this one
76
     *
77
     * @var int
78
     */
79
    private $upIndex;
80
81
    /**
82
     * A pointer into the item stack for which the active item (or null if not in stack yet)
83
     *
84
     * @var int
85
     */
86
    private $currentIndex;
87
88
    /**
89
     * A store of copies of the main item stack, so it's preserved during a lookup from local scope
90
     * (which may push/pop items to/from the main item stack)
91
     *
92
     * @var array
93
     */
94
    private $localStack = [];
95
96
    /**
97
     * The index of the current item in the main item stack, so we know where to restore the scope
98
     * stored in $localStack.
99
     *
100
     * @var int
101
     */
102
    private $localIndex = 0;
103
104
    /**
105
     * @var object $item
106
     * @var SSViewer_Scope $inheritedScope
107
     */
108
    public function __construct($item, SSViewer_Scope $inheritedScope = null)
109
    {
110
        $this->item = $item;
111
112
        $this->itemIterator = ($inheritedScope) ? $inheritedScope->itemIterator : null;
113
        $this->itemIteratorTotal = ($inheritedScope) ? $inheritedScope->itemIteratorTotal : 0;
114
        $this->itemStack[] = [$this->item, $this->itemIterator, $this->itemIteratorTotal, null, null, 0];
115
    }
116
117
    /**
118
     * Returns the current "active" item
119
     *
120
     * @return object
121
     */
122
    public function getItem()
123
    {
124
        return $this->itemIterator ? $this->itemIterator->current() : $this->item;
125
    }
126
127
    /**
128
     * Called at the start of every lookup chain by SSTemplateParser to indicate a new lookup from local scope
129
     *
130
     * @return self
131
     */
132
    public function locally()
133
    {
134
        list(
135
            $this->item,
136
            $this->itemIterator,
137
            $this->itemIteratorTotal,
138
            $this->popIndex,
139
            $this->upIndex,
140
            $this->currentIndex
141
        ) = $this->itemStack[$this->localIndex];
142
143
        // Remember any  un-completed (resetLocalScope hasn't been called) lookup chain. Even if there isn't an
144
        // un-completed chain we need to store an empty item, as resetLocalScope doesn't know the difference later
145
        $this->localStack[] = array_splice($this->itemStack, $this->localIndex + 1);
146
147
        return $this;
148
    }
149
150
    /**
151
     * Reset the local scope - restores saved state to the "global" item stack. Typically called after
152
     * a lookup chain has been completed
153
     */
154
    public function resetLocalScope()
155
    {
156
        // Restore previous un-completed lookup chain if set
157
        $previousLocalState = $this->localStack ? array_pop($this->localStack) : null;
158
        array_splice($this->itemStack, $this->localIndex + 1, count($this->itemStack), $previousLocalState);
159
160
        list(
161
            $this->item,
162
            $this->itemIterator,
163
            $this->itemIteratorTotal,
164
            $this->popIndex,
165
            $this->upIndex,
166
            $this->currentIndex
167
        ) = end($this->itemStack);
168
    }
169
170
    /**
171
     * @param string $name
172
     * @param array $arguments
173
     * @param bool $cache
174
     * @param string $cacheName
175
     * @return mixed
176
     */
177
    public function getObj($name, $arguments = [], $cache = false, $cacheName = null)
178
    {
179
        $on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
180
        return $on->obj($name, $arguments, $cache, $cacheName);
181
    }
182
183
    /**
184
     * @param string $name
185
     * @param array $arguments
186
     * @param bool $cache
187
     * @param string $cacheName
188
     * @return $this
189
     */
190
    public function obj($name, $arguments = [], $cache = false, $cacheName = null)
191
    {
192
        switch ($name) {
193
            case 'Up':
194
                if ($this->upIndex === null) {
195
                    throw new \LogicException('Up called when we\'re already at the top of the scope');
196
                }
197
198
                list(
199
                    $this->item,
200
                    $this->itemIterator,
201
                    $this->itemIteratorTotal,
202
                    /* dud */,
203
                    $this->upIndex,
204
                    $this->currentIndex
205
                ) = $this->itemStack[$this->upIndex];
206
                break;
207
            case 'Top':
208
                list(
209
                    $this->item,
210
                    $this->itemIterator,
211
                    $this->itemIteratorTotal,
212
                    /* dud */,
213
                    $this->upIndex,
214
                    $this->currentIndex
215
                ) = $this->itemStack[0];
216
                break;
217
            default:
218
                $this->item = $this->getObj($name, $arguments, $cache, $cacheName);
219
                $this->itemIterator = null;
220
                $this->upIndex = $this->currentIndex ? $this->currentIndex : count($this->itemStack) - 1;
221
                $this->currentIndex = count($this->itemStack);
222
                break;
223
        }
224
225
        $this->itemStack[] = [
226
            $this->item,
227
            $this->itemIterator,
228
            $this->itemIteratorTotal,
229
            null,
230
            $this->upIndex,
231
            $this->currentIndex
232
        ];
233
        return $this;
234
    }
235
236
    /**
237
     * Gets the current object and resets the scope.
238
     *
239
     * @return object
240
     */
241
    public function self()
242
    {
243
        $result = $this->itemIterator ? $this->itemIterator->current() : $this->item;
244
        $this->resetLocalScope();
245
246
        return $result;
247
    }
248
249
    /**
250
     * Jump to the last item in the stack, called when a new item is added before a loop/with
251
     *
252
     * @return self
253
     */
254
    public function pushScope()
255
    {
256
        $newLocalIndex = count($this->itemStack) - 1;
257
258
        $this->popIndex = $this->itemStack[$newLocalIndex][SSViewer_Scope::POP_INDEX] = $this->localIndex;
259
        $this->localIndex = $newLocalIndex;
260
261
        // $Up now becomes the parent scope - the parent of the current <% loop %> or <% with %>
0 ignored issues
show
Bug introduced by
A parse error occurred: Syntax error, unexpected EOF on line 261 at column 82
Loading history...
262
        $this->upIndex = $this->itemStack[$newLocalIndex][SSViewer_Scope::UP_INDEX] = $this->popIndex;
263
264
        // We normally keep any previous itemIterator around, so local $Up calls reference the right element. But
265
        // once we enter a new global scope, we need to make sure we use a new one
266
        $this->itemIterator = $this->itemStack[$newLocalIndex][SSViewer_Scope::ITEM_ITERATOR] = null;
267
268
        return $this;
269
    }
270
271
    /**
272
     * Jump back to "previous" item in the stack, called after a loop/with block
273
     *
274
     * @return self
275
     */
276
    public function popScope()
277
    {
278
        $this->localIndex = $this->popIndex;
279
        $this->resetLocalScope();
280
281
        return $this;
282
    }
283
284
    /**
285
     * Fast-forwards the current iterator to the next item
286
     *
287
     * @return mixed
288
     */
289
    public function next()
290
    {
291
        if (!$this->item) {
292
            return false;
293
        }
294
295
        if (!$this->itemIterator) {
296
            // Note: it is important that getIterator() is called before count() as implemenations may rely on
297
            // this to efficiently get both the number of records and an iterator (e.g. DataList does this)
298
299
            // Item may be an array or a regular IteratorAggregate
300
            if (is_array($this->item)) {
301
                $this->itemIterator = new ArrayIterator($this->item);
302
            } else {
303
                $this->itemIterator = $this->item->getIterator();
304
305
                // This will execute code in a generator up to the first yield. For example, this ensures that
306
                // DataList::getIterator() is called before Datalist::count()
307
                $this->itemIterator->rewind();
308
            }
309
310
            // If the item implements Countable, use that to fetch the count, otherwise we have to inspect the
311
            // iterator and then rewind it.
312
            if ($this->item instanceof Countable) {
313
                $this->itemIteratorTotal = count($this->item);
314
            } else {
315
                $this->itemIteratorTotal = iterator_count($this->itemIterator);
316
                $this->itemIterator->rewind();
317
            }
318
319
            $this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR] = $this->itemIterator;
320
            $this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR_TOTAL] = $this->itemIteratorTotal;
321
        } else {
322
            $this->itemIterator->next();
323
        }
324
325
        $this->resetLocalScope();
326
327
        if (!$this->itemIterator->valid()) {
328
            return false;
329
        }
330
331
        return $this->itemIterator->key();
332
    }
333
334
    /**
335
     * @param string $name
336
     * @param array $arguments
337
     * @return mixed
338
     */
339
    public function __call($name, $arguments)
340
    {
341
        $on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
342
        $retval = $on ? $on->$name(...$arguments) : null;
343
344
        $this->resetLocalScope();
345
        return $retval;
346
    }
347
348
    /**
349
     * @return array
350
     */
351
    protected function getItemStack()
352
    {
353
        return $this->itemStack;
354
    }
355
356
    /**
357
     * @param array
358
     */
359
    protected function setItemStack(array $stack)
360
    {
361
        $this->itemStack = $stack;
362
    }
363
364
    /**
365
     * @return int|null
366
     */
367
    protected function getUpIndex()
368
    {
369
        return $this->upIndex;
370
    }
371
}
372