1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** smtech\CanvasPest\CanvasArray */ |
4
|
|
|
|
5
|
|
|
namespace smtech\CanvasPest; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* An object to represent a list of Canvas Objects returned as a response from |
9
|
|
|
* the Canvas API. |
10
|
|
|
* |
11
|
|
|
* @author Seth Battis <[email protected]> |
12
|
|
|
**/ |
13
|
|
|
class CanvasArray implements \Iterator, \ArrayAccess, \Serializable |
14
|
|
|
{ |
15
|
|
|
/** The maximum supported number of responses per page */ |
16
|
|
|
const MAXIMUM_PER_PAGE = 100; |
17
|
|
|
|
18
|
|
|
/** @var CanvasPest $api Canvas API (for paging through the array) */ |
19
|
|
|
protected $api; |
20
|
|
|
|
21
|
|
|
/** |
22
|
|
|
* @var string $endpoint API endpoint whose response is represented by this |
23
|
|
|
* object |
24
|
|
|
**/ |
25
|
|
|
private $endpoint = null; |
26
|
|
|
|
27
|
|
|
/** |
28
|
|
|
* @var CanvasPageLink[] $pagination The canonical (first, last, next, |
29
|
|
|
* prev, current) pages relative to the current page of responses |
30
|
|
|
**/ |
31
|
|
|
private $pagination = []; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @var array Cached pagination per each page response |
35
|
|
|
*/ |
36
|
|
|
private $paginationPerPage = []; |
37
|
|
|
|
38
|
|
|
/** @var CanvasObject[] $data Backing store */ |
39
|
|
|
private $data = []; |
40
|
|
|
|
41
|
|
|
/** @var int $page Page number corresponding to current $key */ |
42
|
|
|
private $page = null; |
43
|
|
|
|
44
|
|
|
/** @var int $key Current key-value of iterator */ |
45
|
|
|
private $key = null; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* Construct a CanvasArray |
49
|
|
|
* |
50
|
|
|
* @param string $jsonResponse A JSON-encoded response array from the |
51
|
|
|
* Canvas API |
52
|
|
|
* @param CanvasPest $canvasPest An API object for making pagination calls |
53
|
|
|
**/ |
54
|
7 |
|
public function __construct($jsonResponse, $canvasPest) |
55
|
|
|
{ |
56
|
7 |
|
$this->api = $canvasPest; |
57
|
|
|
|
58
|
7 |
|
$this->pagination = $this->parsePageLinks(); |
59
|
|
|
|
60
|
|
|
/* locate ourselves */ |
61
|
7 |
|
if (isset($this->pagination[CanvasPageLink::CURRENT])) { |
62
|
7 |
|
$this->page = $this->pagination[CanvasPageLink::CURRENT]->getPageNumber(); |
63
|
7 |
|
$this->key = $this->pageNumberToKey($this->page); |
|
|
|
|
64
|
7 |
|
$this->paginationPerPage[$this->page] = $this->pagination; |
65
|
7 |
|
} |
66
|
|
|
|
67
|
|
|
/* parse the JSON response string */ |
68
|
7 |
|
$key = $this->key; |
69
|
7 |
|
foreach (json_decode($jsonResponse, true) as $item) { |
70
|
7 |
|
$this->data[$key++] = new CanvasObject($item); |
71
|
7 |
|
} |
72
|
7 |
|
} |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Parse the API response link headers into pagination information |
76
|
|
|
* |
77
|
|
|
* @param boolean|string[] $headers (Optional, defaults to `$this->api->lastHeader('link')`) |
78
|
|
|
* @return CanvasPageLink[] |
79
|
|
|
*/ |
80
|
7 |
|
protected function parsePageLinks($headers = false) |
81
|
|
|
{ |
82
|
7 |
|
$pagination = []; |
83
|
7 |
|
if (!$headers) { |
84
|
7 |
|
$headers = $this->api->lastHeader('link'); |
85
|
7 |
|
} |
86
|
|
|
|
87
|
|
|
/* parse Canvas page links */ |
88
|
7 |
|
if (preg_match_all('%<([^>]*)>\s*;\s*rel="([^"]+)"%', $headers, $links, PREG_SET_ORDER)) { |
89
|
7 |
|
foreach ($links as $link) { |
90
|
7 |
|
$pagination[$link[2]] = new CanvasPageLink($link[1], $link[2]); |
91
|
7 |
|
} |
92
|
7 |
|
} |
93
|
|
|
|
94
|
7 |
|
return $pagination; |
95
|
|
|
} |
96
|
|
|
/** |
97
|
|
|
* Convert a page number to an array key |
98
|
|
|
* |
99
|
|
|
* @param int $pageNumber 1-indexed page number |
100
|
|
|
* |
101
|
|
|
* @return int|false |
102
|
|
|
**/ |
103
|
10 |
|
protected function pageNumberToKey($pageNumber) |
104
|
|
|
{ |
105
|
10 |
|
if (isset($this->pagination[CanvasPageLink::CURRENT])) { |
106
|
10 |
|
return ($pageNumber - 1) * $this->pagination[CanvasPageLink::CURRENT]->getPerPage(); |
107
|
|
|
} |
108
|
|
|
return false; |
109
|
|
|
} |
110
|
|
|
|
111
|
|
|
/** |
112
|
|
|
* Request a page of responses from the API |
113
|
|
|
* |
114
|
|
|
* A page of responses will be requested if it appears that that page has |
115
|
|
|
* not yet been loaded (tested by checking if the initial element of the |
116
|
|
|
* page has been initialized in the $data array). |
117
|
|
|
* |
118
|
|
|
* @param int $pageNumber Page number to request |
119
|
|
|
* @param bool $forceRefresh (Optional) Force a refresh of backing data, |
120
|
|
|
* even if cached (defaults to `FALSE`) |
121
|
|
|
* |
122
|
|
|
* @return bool `TRUE` if the page is requested, `FALSE` if it is already |
123
|
|
|
* cached (and therefore not requested) |
124
|
|
|
**/ |
125
|
7 |
|
protected function requestPageNumber($pageNumber, $forceRefresh = false) |
126
|
|
|
{ |
127
|
7 |
|
if (!isset($this->data[$this->pageNumberToKey($pageNumber)]) || ($forceRefresh && isset($this->api))) { |
128
|
|
|
// assume one page if no pagination (and already loaded) |
129
|
4 |
|
if (isset($this->pagination[CanvasPageLink::CURRENT])) { |
130
|
4 |
|
$params = $this->pagination[CanvasPageLink::CURRENT]->getParams(); |
131
|
4 |
|
$params[CanvasPageLink::PARAM_PAGE_NUMBER] = $pageNumber; |
132
|
4 |
|
$page = $this->api->get($this->pagination[CanvasPageLink::CURRENT]->getEndpoint(), $params); |
133
|
4 |
|
$this->data = array_replace($this->data, $page->data); |
134
|
4 |
|
$pagination = $this->parsePageLinks(); |
135
|
4 |
|
$this->paginationPerPage[$pagination[CanvasPageLink::CURRENT]->getPageNumber()] = $pagination; |
136
|
4 |
|
return true; |
137
|
|
|
} |
138
|
|
|
} |
139
|
4 |
|
return false; |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Request all pages from API |
144
|
|
|
* |
145
|
|
|
* This stores the entire API response locally, in preparation for, most |
146
|
|
|
* likely, serializing this object. |
147
|
|
|
* |
148
|
|
|
* @param bool $forceRefresh (Optional) Force a refresh of backing data, |
149
|
|
|
* even if cached (defaults to `FALSE`) |
150
|
|
|
* |
151
|
|
|
* @return void |
152
|
|
|
*/ |
153
|
7 |
|
protected function requestAllPages($forceRefresh = false) |
154
|
|
|
{ |
155
|
7 |
|
$_page = $this->page; |
156
|
7 |
|
$_key = $this->key; |
157
|
|
|
|
158
|
7 |
|
$nextPageNumber = false; |
159
|
7 |
|
if (isset($this->pagination[CanvasPageLink::NEXT])) { |
160
|
7 |
|
$nextPageNumber = $this->pagination[CanvasPageLink::NEXT]->getPageNumber(); |
161
|
7 |
|
} |
162
|
|
|
|
163
|
|
|
/* welp, here goes... let's hope we have a next page! */ |
164
|
7 |
|
while ($nextPageNumber !== false) { |
165
|
7 |
|
$this->requestPageNumber($nextPageNumber, $forceRefresh); |
166
|
7 |
|
if (isset($this->paginationPerPage[$nextPageNumber][CanvasPageLink::NEXT])) { |
167
|
7 |
|
$nextPageNumber = $this->paginationPerPage[$nextPageNumber][CanvasPageLink::NEXT]->getPageNumber(); |
168
|
7 |
|
} else { |
169
|
7 |
|
$nextPageNumber = false; |
170
|
|
|
} |
171
|
7 |
|
} |
172
|
|
|
|
173
|
7 |
|
$this->page = $_page; |
174
|
7 |
|
$this->key = $_key; |
175
|
7 |
|
} |
176
|
|
|
|
177
|
|
|
/*************************************************************************** |
178
|
|
|
* ArrayObject methods |
179
|
|
|
*/ |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Get the number of CanvasObjects in the Canvas response |
183
|
|
|
* |
184
|
|
|
* @return int |
185
|
|
|
* |
186
|
|
|
* @see http://php.net/manual/en/arrayobject.count.php ArrayObject::count |
187
|
|
|
**/ |
188
|
1 |
|
public function count() |
189
|
|
|
{ |
190
|
1 |
|
$this->requestAllPages(); |
191
|
1 |
|
return count($this->data); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Creates a copy of the CanvasArray |
196
|
|
|
* |
197
|
|
|
* @return CanvasObject[] |
198
|
|
|
* |
199
|
|
|
* @see http://php.net/manual/en/arrayobject.getarraycopy.php |
200
|
|
|
* ArrayObject::getArrayCopy |
201
|
|
|
**/ |
202
|
1 |
|
public function getArrayCopy() |
203
|
|
|
{ |
204
|
1 |
|
$this->requestAllPages(); |
205
|
1 |
|
return $this->data; |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
/*************************************************************************** |
209
|
|
|
* ArrayAccess methods |
210
|
|
|
*/ |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Whether an offset exists |
214
|
|
|
* |
215
|
|
|
* @param int|string $offset |
216
|
|
|
* |
217
|
|
|
* @return bool |
218
|
|
|
* |
219
|
|
|
* @see http://php.net/manual/en/arrayaccess.offsetexists.php |
220
|
|
|
* ArrayAccess::offsetExists |
221
|
|
|
**/ |
222
|
4 |
|
public function offsetExists($offset) |
223
|
|
|
{ |
224
|
4 |
|
if (!isset($this->data[$offset])) { |
225
|
4 |
|
$this->requestAllPages(); |
226
|
4 |
|
} |
227
|
4 |
|
return isset($this->data[$offset]); |
228
|
|
|
} |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Offset to retrieve |
232
|
|
|
* |
233
|
|
|
* @param int|string $offset |
234
|
|
|
* |
235
|
|
|
* @return CanvasObject|null |
236
|
|
|
* |
237
|
|
|
* @see http://php.net/manual/en/arrayaccess.offsetexists.php |
238
|
|
|
* ArrayAccess::offsetGet |
239
|
|
|
**/ |
240
|
2 |
|
public function offsetGet($offset) |
241
|
|
|
{ |
242
|
2 |
|
if (!isset($this->data[$offset])) { |
243
|
1 |
|
$this->requestAllPages(); |
244
|
1 |
|
} |
245
|
2 |
|
return $this->data[$offset]; |
246
|
|
|
} |
247
|
|
|
|
248
|
|
|
/** |
249
|
|
|
* Assign a value to the specified offset |
250
|
|
|
* |
251
|
|
|
* @deprecated CanvasObject and CanvasArray responses are immutable |
252
|
|
|
* |
253
|
|
|
* @param int|string $offset |
254
|
|
|
* @param CanvasObject $value |
255
|
|
|
* |
256
|
|
|
* @return void |
257
|
|
|
* |
258
|
|
|
* @throws CanvasArray_Exception IMMUTABLE All calls to this method will cause an exception |
259
|
|
|
* |
260
|
|
|
* @see http://php.net/manual/en/arrayaccess.offsetset.php |
261
|
|
|
* ArrayAccess::offsetSet |
262
|
|
|
**/ |
263
|
1 |
|
public function offsetSet($offset, $value) |
264
|
|
|
{ |
265
|
1 |
|
throw new CanvasArray_Exception( |
266
|
1 |
|
'Canvas responses are immutable', |
267
|
|
|
CanvasArray_Exception::IMMUTABLE |
268
|
1 |
|
); |
269
|
|
|
} |
270
|
|
|
|
271
|
|
|
/** |
272
|
|
|
* Unset an offset |
273
|
|
|
* |
274
|
|
|
* @deprecated CanvasObject and CanvasArray responses are immutable |
275
|
|
|
* |
276
|
|
|
* @param int|string $offset |
277
|
|
|
* |
278
|
|
|
* @return void |
279
|
|
|
* |
280
|
|
|
* @throws CanvasArray_Exception IMMUTABLE All calls to this method will |
281
|
|
|
* cause an exception |
282
|
|
|
* |
283
|
|
|
* @see http://php.net/manual/en/arrayaccess.offsetunset.php |
284
|
|
|
* ArrayAccess::offsetUnset |
285
|
|
|
**/ |
286
|
1 |
|
public function offsetUnset($offset) |
287
|
|
|
{ |
288
|
1 |
|
throw new CanvasArray_Exception( |
289
|
1 |
|
'Canvas responses are immutable', |
290
|
|
|
CanvasArray_Exception::IMMUTABLE |
291
|
1 |
|
); |
292
|
|
|
} |
293
|
|
|
|
294
|
|
|
/**************************************************************************/ |
295
|
|
|
|
296
|
|
|
/************************************************************************** |
297
|
|
|
* Iterator methods |
298
|
|
|
*/ |
299
|
|
|
|
300
|
|
|
/** |
301
|
|
|
* Return the current element |
302
|
|
|
* |
303
|
|
|
* @return CanvasObject |
304
|
|
|
* |
305
|
|
|
* @see http://php.net/manual/en/iterator.current.php Iterator::current |
306
|
|
|
**/ |
307
|
2 |
|
public function current() |
308
|
|
|
{ |
309
|
2 |
|
return $this->data[$this->key]; |
310
|
|
|
} |
311
|
|
|
|
312
|
|
|
/** |
313
|
|
|
* Return the key of the current element |
314
|
|
|
* |
315
|
|
|
* @return int |
316
|
|
|
* |
317
|
|
|
* @see http://php.net/manual/en/iterator.key.php Iterator::key |
318
|
|
|
**/ |
319
|
1 |
|
public function key() |
320
|
|
|
{ |
321
|
1 |
|
return $this->key; |
322
|
|
|
} |
323
|
|
|
|
324
|
|
|
/** |
325
|
|
|
* Move forward to next element |
326
|
|
|
* |
327
|
|
|
* @return void |
328
|
|
|
* |
329
|
|
|
* @see http://php.net/manual/en/iterator.next.php Iterator::next |
330
|
|
|
**/ |
331
|
2 |
|
public function next() |
332
|
|
|
{ |
333
|
2 |
|
$this->key++; |
334
|
2 |
|
} |
335
|
|
|
|
336
|
|
|
/** |
337
|
|
|
* Rewind the iterator to the first element |
338
|
|
|
* |
339
|
|
|
* @return void |
340
|
|
|
* |
341
|
|
|
* @see http://php.net/manual/en/iterator.rewind.php Iterator::rewind |
342
|
|
|
**/ |
343
|
2 |
|
public function rewind() |
344
|
|
|
{ |
345
|
2 |
|
$this->key = 0; |
346
|
2 |
|
} |
347
|
|
|
|
348
|
|
|
/** |
349
|
|
|
* Checks if current position is valid |
350
|
|
|
* |
351
|
|
|
* @return bool |
352
|
|
|
* |
353
|
|
|
* @see http://php.net/manual/en/iterator.valid.php Iterator::valid |
354
|
|
|
**/ |
355
|
2 |
|
public function valid() |
356
|
|
|
{ |
357
|
2 |
|
return ($this->offsetExists($this->key)); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/**************************************************************************/ |
361
|
|
|
|
362
|
|
|
/*************************************************************************** |
363
|
|
|
* Serializable methods |
364
|
|
|
*/ |
365
|
|
|
|
366
|
|
|
/** |
367
|
|
|
* String representation of CanvasArray |
368
|
|
|
* |
369
|
|
|
* @return string |
370
|
|
|
* |
371
|
|
|
* @see http://php.net/manual/en/serializable.serialize.php |
372
|
|
|
* Serializable::serialize() |
373
|
|
|
**/ |
374
|
1 |
|
public function serialize() |
375
|
|
|
{ |
376
|
1 |
|
$this->requestAllPages(); |
377
|
1 |
|
return serialize( |
378
|
|
|
array( |
379
|
1 |
|
'page' => $this->page, |
380
|
1 |
|
'key' => $this->key, |
381
|
1 |
|
'data' => $this->data |
382
|
1 |
|
) |
383
|
1 |
|
); |
384
|
|
|
} |
385
|
|
|
|
386
|
|
|
/** |
387
|
|
|
* Construct a CanvasArray from its string representation |
388
|
|
|
* |
389
|
|
|
* The data in the unserialized CanvasArray is static and cannot be |
390
|
|
|
* refreshed, as the CanvasPest API connection is _not_ serialized to |
391
|
|
|
* preserve the security of API access tokens. |
392
|
|
|
* |
393
|
|
|
* @param string $data |
394
|
|
|
* |
395
|
|
|
* @return string |
396
|
|
|
* |
397
|
|
|
* @see http://php.net/manual/en/serializable.unserialize.php |
398
|
|
|
* Serializable::unserialize() |
399
|
|
|
**/ |
400
|
1 |
|
public function unserialize($data) |
401
|
|
|
{ |
402
|
1 |
|
$_data = unserialize($data); |
403
|
1 |
|
$this->page = $_data['page']; |
404
|
1 |
|
$this->key = $_data['key']; |
405
|
1 |
|
$this->data = $_data['data']; |
406
|
1 |
|
$this->api = null; |
407
|
1 |
|
$this->endpoint = null; |
408
|
1 |
|
$this->pagination = array(); |
409
|
1 |
|
} |
410
|
|
|
} |
411
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.