Completed
Push — generators ( d5c396...b04334 )
by Sam
09:17
created

SSViewer_Scope   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 244
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 244
rs 10
c 0
b 0
f 0
wmc 30
lcom 1
cbo 0

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 2
A getItem() 0 4 2
A locally() 0 11 1
A resetLocalScope() 0 9 2
A getObj() 0 5 2
B obj() 0 35 5
A self() 0 7 2
A pushScope() 0 13 1
A popScope() 0 7 1
B next() 0 43 6
A __call() 0 8 3
A getItemStack() 0 4 1
A setItemStack() 0 4 1
A getUpIndex() 0 4 1
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
31
    const ITEM = 0;
32
    const ITEM_ITERATOR = 1;
33
    const ITEM_ITERATOR_TOTAL = 2;
34
    const POP_INDEX = 3;
35
    const UP_INDEX = 4;
36
    const CURRENT_INDEX = 5;
37
    const ITEM_OVERLAY = 6;
38
39
    // The stack of previous "global" items
40
    // An indexed array of item, item iterator, item iterator total, pop index, up index, current index & parent overlay
41
    private $itemStack = array();
42
43
    /**
44
     * The current "global" item (the one any lookup starts from)
45
     */
46
    protected $item;
47
48
    /**
49
     * If we're looping over the current "global" item, here's the iterator that tracks with item we're up to
50
     *
51
     * @var Iterator
52
     */
53
    protected $itemIterator;
54
55
    //Total number of items in the iterator
56
    protected $itemIteratorTotal;
57
58
    // A pointer into the item stack for which item should be scope on the next pop call
59
    private $popIndex;
60
61
    // A pointer into the item stack for which item is "up" from this one
62
    private $upIndex = null;
63
64
    // A pointer into the item stack for which item is this one (or null if not in stack yet)
65
    private $currentIndex = null;
66
67
    private $localIndex;
68
69
    public function __construct($item, $inheritedScope = null)
70
    {
71
        $this->item = $item;
72
        $this->localIndex = 0;
73
        $this->localStack = array();
0 ignored issues
show
Bug introduced by
The property localStack does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
74
        if ($inheritedScope instanceof SSViewer_Scope) {
75
            $this->itemIterator = $inheritedScope->itemIterator;
76
            $this->itemIteratorTotal = $inheritedScope->itemIteratorTotal;
77
            $this->itemStack[] = array($this->item, $this->itemIterator, $this->itemIteratorTotal, null, null, 0);
78
        } else {
79
            $this->itemStack[] = array($this->item, null, 0, null, null, 0);
80
        }
81
    }
82
83
    public function getItem()
84
    {
85
        return $this->itemIterator ? $this->itemIterator->current() : $this->item;
86
    }
87
88
    /** Called at the start of every lookup chain by SSTemplateParser to indicate a new lookup from local scope */
89
    public function locally()
90
    {
91
        list($this->item, $this->itemIterator, $this->itemIteratorTotal, $this->popIndex, $this->upIndex,
92
            $this->currentIndex) = $this->itemStack[$this->localIndex];
93
94
        // Remember any  un-completed (resetLocalScope hasn't been called) lookup chain. Even if there isn't an
95
        // un-completed chain we need to store an empty item, as resetLocalScope doesn't know the difference later
96
        $this->localStack[] = array_splice($this->itemStack, $this->localIndex + 1);
97
98
        return $this;
99
    }
100
101
    public function resetLocalScope()
102
    {
103
        $previousLocalState = $this->localStack ? array_pop($this->localStack) : null;
104
105
        array_splice($this->itemStack, $this->localIndex + 1, count($this->itemStack), $previousLocalState);
106
107
        list($this->item, $this->itemIterator, $this->itemIteratorTotal, $this->popIndex, $this->upIndex,
108
            $this->currentIndex) = end($this->itemStack);
109
    }
110
111
    public function getObj($name, $arguments = [], $cache = false, $cacheName = null)
112
    {
113
        $on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
114
        return $on->obj($name, $arguments, $cache, $cacheName);
115
    }
116
117
    /**
118
     * @param string $name
119
     * @param array $arguments
120
     * @param bool $cache
121
     * @param string $cacheName
122
     * @return $this
123
     */
124
    public function obj($name, $arguments = [], $cache = false, $cacheName = null)
125
    {
126
        switch ($name) {
127
            case 'Up':
128
                if ($this->upIndex === null) {
129
                    user_error('Up called when we\'re already at the top of the scope', E_USER_ERROR);
130
                }
131
132
                list($this->item, $this->itemIterator, $this->itemIteratorTotal, $unused2, $this->upIndex,
0 ignored issues
show
Unused Code introduced by
The assignment to $unused2 is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
133
                    $this->currentIndex) = $this->itemStack[$this->upIndex];
134
                break;
135
136
            case 'Top':
137
                list($this->item, $this->itemIterator, $this->itemIteratorTotal, $unused2, $this->upIndex,
0 ignored issues
show
Unused Code introduced by
The assignment to $unused2 is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
138
                    $this->currentIndex) = $this->itemStack[0];
139
                break;
140
141
            default:
142
                $this->item = $this->getObj($name, $arguments, $cache, $cacheName);
143
                $this->itemIterator = null;
144
                $this->upIndex = $this->currentIndex ? $this->currentIndex : count($this->itemStack) - 1;
145
                $this->currentIndex = count($this->itemStack);
146
                break;
147
        }
148
149
        $this->itemStack[] = array(
150
            $this->item,
151
            $this->itemIterator,
152
            $this->itemIteratorTotal,
153
            null,
154
            $this->upIndex,
155
            $this->currentIndex
156
        );
157
        return $this;
158
    }
159
160
    /**
161
     * Gets the current object and resets the scope.
162
     *
163
     * @return object
164
     */
165
    public function self()
166
    {
167
        $result = $this->itemIterator ? $this->itemIterator->current() : $this->item;
168
        $this->resetLocalScope();
169
170
        return $result;
171
    }
172
173
    public function pushScope()
174
    {
175
        $newLocalIndex = count($this->itemStack) - 1;
176
177
        $this->popIndex = $this->itemStack[$newLocalIndex][SSViewer_Scope::POP_INDEX] = $this->localIndex;
178
        $this->localIndex = $newLocalIndex;
179
180
        // We normally keep any previous itemIterator around, so local $Up calls reference the right element. But
181
        // once we enter a new global scope, we need to make sure we use a new one
182
        $this->itemIterator = $this->itemStack[$newLocalIndex][SSViewer_Scope::ITEM_ITERATOR] = null;
183
184
        return $this;
185
    }
186
187
    public function popScope()
188
    {
189
        $this->localIndex = $this->popIndex;
190
        $this->resetLocalScope();
191
192
        return $this;
193
    }
194
195
    public function next()
196
    {
197
        if (!$this->item) {
198
            return false;
199
        }
200
201
        if (!$this->itemIterator) {
202
            // Note: it is important that getIterator() is called before count() as implemenations may rely on
203
            // this to efficiency get both the number of records and an iterator (e.g. DataList does this)
204
205
            // Item may be an array or a regular IteratorAggregate
206
            if (is_array($this->item)) {
207
                $this->itemIterator = new ArrayIterator($this->item);
208
            } else {
209
                $this->itemIterator = $this->item->getIterator();
210
211
                // This will execute code in a generator up to the first yield. For example, this ensures that
212
                // DataList::getIterator() is called before Datalist::count()
213
                $this->itemIterator->rewind();
214
            }
215
216
            // If the item implements Countable, use that to fetch the count, otherwise we have to inspect the
217
            // iterator and then rewind it.
218
            if ($this->item instanceof Countable) {
219
                $this->itemIteratorTotal = count($this->item);
220
            } else {
221
                $this->itemIteratorTotal = iterator_count($this->itemIterator);
222
                $this->itemIterator->rewind();
223
            }
224
225
            $this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR] = $this->itemIterator;
226
            $this->itemStack[$this->localIndex][SSViewer_Scope::ITEM_ITERATOR_TOTAL] = $this->itemIteratorTotal;
227
        } else {
228
            $this->itemIterator->next();
229
        }
230
231
        $this->resetLocalScope();
232
233
        if (!$this->itemIterator->valid()) {
234
            return false;
235
        }
236
        return $this->itemIterator->key();
237
    }
238
239
    public function __call($name, $arguments)
240
    {
241
        $on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
242
        $retval = $on ? call_user_func_array(array($on, $name), $arguments) : null;
243
244
        $this->resetLocalScope();
245
        return $retval;
246
    }
247
248
    /**
249
     * @return array
250
     */
251
    protected function getItemStack()
252
    {
253
        return $this->itemStack;
254
    }
255
256
    /**
257
     * @param array
258
     */
259
    protected function setItemStack(array $stack)
260
    {
261
        $this->itemStack = $stack;
262
    }
263
264
    /**
265
     * @return int|null
266
     */
267
    protected function getUpIndex()
268
    {
269
        return $this->upIndex;
270
    }
271
}
272