1
|
|
|
<?php |
2
|
|
|
/* |
3
|
|
|
* Copyright 2016 - 2017 Eric D. Hough (https://github.com/ehough) |
4
|
|
|
* |
5
|
|
|
* This file is part of ehough/generators (https://github.com/ehough/generators) |
6
|
|
|
* |
7
|
|
|
* This Source Code Form is subject to the terms of the Mozilla Public |
8
|
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this |
9
|
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace Hough\Generators; |
13
|
|
|
|
14
|
|
|
abstract class AbstractGenerator implements \Iterator |
15
|
|
|
{ |
16
|
|
|
/** |
17
|
|
|
* @var null|mixed |
18
|
|
|
*/ |
19
|
|
|
private $_lastValueSentIn; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @var int |
23
|
|
|
*/ |
24
|
|
|
private $_position = 0; |
25
|
|
|
|
26
|
|
|
/** |
27
|
|
|
* @var null|mixed |
28
|
|
|
*/ |
29
|
|
|
private $_lastYieldedValue; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* @var null|string|int |
33
|
|
|
*/ |
34
|
|
|
private $_lastYieldedKey; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* @var int |
38
|
|
|
*/ |
39
|
|
|
private $_lastPositionExecuted; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* @var int |
43
|
|
|
*/ |
44
|
|
|
private $_positionsExecutedCount = 0; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* @var bool |
48
|
|
|
*/ |
49
|
|
|
private $_sendInvokedAtLeastOnce = false; |
50
|
|
|
|
51
|
|
|
/** |
52
|
|
|
* @var bool |
53
|
|
|
*/ |
54
|
|
|
private $_hasMoreToExecute = true; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Get the yielded value. |
58
|
|
|
* |
59
|
|
|
* @return mixed|null the yielded value |
60
|
|
|
*/ |
61
|
4 |
|
final public function current() |
62
|
|
|
{ |
63
|
4 |
|
if (!$this->valid()) { |
64
|
|
|
|
65
|
2 |
|
return null; |
66
|
|
|
} |
67
|
|
|
|
68
|
|
|
/* |
69
|
|
|
* Multiple calls to current() should be idempotent |
70
|
|
|
*/ |
71
|
4 |
|
if ($this->_lastPositionExecuted !== $this->_position) { |
72
|
|
|
|
73
|
4 |
|
$this->runToNextYieldStatement(); |
74
|
|
|
} |
75
|
|
|
|
76
|
4 |
|
return $this->valid() ? $this->getLastYieldedValue() : null; |
77
|
|
|
} |
78
|
|
|
|
79
|
|
|
/** |
80
|
|
|
* Get the return value of a generator. |
81
|
|
|
* |
82
|
|
|
* @return mixed the generator's return value once it has finished executing |
|
|
|
|
83
|
|
|
*/ |
84
|
1 |
|
public function getReturn() |
85
|
|
|
{ |
86
|
|
|
//override point |
87
|
1 |
|
throw new \RuntimeException('Cannot get return value of a generator that hasn\'t returned'); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Get the yielded key. |
92
|
|
|
* |
93
|
|
|
* @return mixed the yielded key |
|
|
|
|
94
|
|
|
*/ |
95
|
2 |
|
final public function key() |
96
|
|
|
{ |
97
|
|
|
/* |
98
|
|
|
* Run to the first yield statement, if we haven't already. |
99
|
|
|
*/ |
100
|
2 |
|
$this->current(); |
101
|
|
|
|
102
|
2 |
|
return $this->valid() ? $this->_lastYieldedKey : null; |
103
|
|
|
} |
104
|
|
|
|
105
|
|
|
/** |
106
|
|
|
* Resume execution of the generator. |
107
|
|
|
* |
108
|
|
|
* @return void |
109
|
|
|
*/ |
110
|
3 |
|
final public function next() |
111
|
|
|
{ |
112
|
3 |
|
$this->send(null); |
113
|
3 |
|
} |
114
|
|
|
|
115
|
|
|
/** |
116
|
|
|
* Rewind the iterator. |
117
|
|
|
* |
118
|
|
|
* @return void |
119
|
|
|
*/ |
120
|
3 |
|
final public function rewind() |
121
|
|
|
{ |
122
|
3 |
|
if ($this->_sendInvokedAtLeastOnce) { |
123
|
|
|
|
124
|
2 |
|
throw new \RuntimeException('Cannot rewind a generator that was already run'); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/* |
128
|
|
|
* Run to the first yield statement, if we haven't already. |
129
|
|
|
*/ |
130
|
3 |
|
$this->current(); |
131
|
3 |
|
} |
132
|
|
|
|
133
|
|
|
/** |
134
|
|
|
* Send a value to the generator. |
135
|
|
|
* |
136
|
|
|
* @param mixed $value |
137
|
|
|
* |
138
|
|
|
* @return mixed |
139
|
|
|
*/ |
140
|
4 |
|
final public function send($value) |
141
|
|
|
{ |
142
|
4 |
|
$this->_lastValueSentIn = $value; |
143
|
4 |
|
$this->_sendInvokedAtLeastOnce = true; |
144
|
|
|
|
145
|
|
|
/* |
146
|
|
|
* If we've already ran to the first yield statement (from rewind() or key(), for instance), we need |
147
|
|
|
* to try to move to the next position; |
148
|
|
|
*/ |
149
|
4 |
|
if ($this->_positionsExecutedCount > 0) { |
150
|
|
|
|
151
|
3 |
|
$this->_position++; |
152
|
|
|
} |
153
|
|
|
|
154
|
4 |
|
return $this->current(); |
155
|
|
|
} |
156
|
|
|
|
157
|
1 |
|
final public function __call($name, $args) |
158
|
|
|
{ |
159
|
1 |
|
if ($name === 'throw') { |
160
|
|
|
|
161
|
|
|
/* |
162
|
|
|
* If the generator is already closed when this method is invoked, the exception will be thrown in the |
163
|
|
|
* caller's context instead. |
164
|
|
|
*/ |
165
|
1 |
|
if (!$this->valid()) { |
166
|
|
|
|
167
|
|
|
throw $args[0]; |
168
|
|
|
} |
169
|
|
|
|
170
|
1 |
|
return $this->onExceptionThrownIn($args[0], $this->_position); |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
throw new \RuntimeException('Cannot dynamically invoke method ' . $name . '()'); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
/** |
177
|
|
|
* Check if the iterator has been closed. |
178
|
|
|
* |
179
|
|
|
* @return bool False if the iterator has been closed. Otherwise returns true. |
180
|
|
|
*/ |
181
|
5 |
|
final public function valid() |
182
|
|
|
{ |
183
|
5 |
|
return $this->_hasMoreToExecute; |
184
|
|
|
} |
185
|
|
|
|
186
|
2 |
|
final public function __invoke() |
187
|
|
|
{ |
188
|
2 |
|
return $this; |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* @return null|mixed |
193
|
|
|
*/ |
194
|
1 |
|
final protected function getLastValueSentIn() |
195
|
|
|
{ |
196
|
1 |
|
return $this->_lastValueSentIn; |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* @return null|mixed |
201
|
|
|
*/ |
202
|
4 |
|
final protected function getLastYieldedValue() |
203
|
|
|
{ |
204
|
4 |
|
return $this->_lastYieldedValue; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* An exception was thrown into the generator from the calling context. |
209
|
|
|
* |
210
|
|
|
* @param \Exception $e the exception thrown in |
211
|
|
|
* @param int $position the current position of the generator |
212
|
|
|
* |
213
|
|
|
* @throws \Exception |
214
|
|
|
*/ |
215
|
1 |
|
protected function onExceptionThrownIn(\Exception $e, $position) |
|
|
|
|
216
|
|
|
{ |
217
|
|
|
//override point |
218
|
1 |
|
throw $e; |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Resume execution of the generator. |
223
|
|
|
* |
224
|
|
|
* @param int $position the zero-based "position" of execution |
225
|
|
|
* |
226
|
|
|
* @return null|array Return null to indicate completion. Otherwise return an array of up to two elements. If two |
227
|
|
|
* elements in the array, the first will be considered to be the yielded key and the second the |
228
|
|
|
* yielded value. If one element in the array, it will be considered to be the yielded value and |
229
|
|
|
* the yielded key will be $position. |
230
|
|
|
*/ |
231
|
|
|
abstract protected function resume($position); |
232
|
|
|
|
233
|
4 |
|
private function runToNextYieldStatement() |
234
|
|
|
{ |
235
|
4 |
|
$executionResult = $this->resume($this->_position); |
236
|
4 |
|
$this->_lastPositionExecuted = $this->_position; |
237
|
|
|
|
238
|
4 |
|
$this->_positionsExecutedCount++; |
239
|
|
|
|
240
|
|
|
/* |
241
|
|
|
* Nothing more to do. |
242
|
|
|
*/ |
243
|
4 |
|
if ($executionResult === null) { |
244
|
|
|
|
245
|
2 |
|
$this->_hasMoreToExecute = false; |
246
|
2 |
|
$this->_lastYieldedValue = null; |
247
|
2 |
|
$this->_lastYieldedKey = null; |
248
|
|
|
|
249
|
2 |
|
return; |
250
|
|
|
} |
251
|
|
|
|
252
|
4 |
|
if (!is_array($executionResult) || count($executionResult) === 0 || count($executionResult) >= 3) { |
253
|
|
|
|
254
|
|
|
throw new \LogicException('executePosition() must return an array of up to two elements. If two elements, the first is the yielded key and the second is the yielded value. If one element, it is considered to be the yielded value.'); |
255
|
|
|
} |
256
|
|
|
|
257
|
4 |
|
if (count($executionResult) === 2) { |
258
|
|
|
|
259
|
|
|
$this->_lastYieldedKey = $executionResult[0]; |
260
|
|
|
$this->_lastYieldedValue = $executionResult[1]; |
261
|
|
|
|
262
|
|
|
} else { |
263
|
|
|
|
264
|
4 |
|
$this->_lastYieldedKey = $this->_position; |
265
|
4 |
|
$this->_lastYieldedValue = $executionResult[0]; |
266
|
|
|
} |
267
|
4 |
|
} |
268
|
|
|
} |
269
|
|
|
|
This check looks for the generic type
array
as a return type and suggests a more specific type. This type is inferred from the actual code.