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
|
|
|
public function __construct($jsonResponse, $canvasPest) |
55
|
|
|
{ |
56
|
|
|
$this->api = $canvasPest; |
57
|
|
|
|
58
|
|
|
$this->pagination = $this->parsePageLinks(); |
59
|
|
|
|
60
|
|
|
/* locate ourselves */ |
61
|
|
|
if (isset($this->pagination[CanvasPageLink::CURRENT])) { |
62
|
|
|
$this->page = $this->pagination[CanvasPageLink::CURRENT]->getPageNumber(); |
63
|
|
|
} else { |
64
|
|
|
$this->page = 1; // assume only one page (since no pagination) |
65
|
|
|
} |
66
|
|
|
$this->key = $this->pageNumberToKey($this->page); |
67
|
|
|
$this->paginationPerPage[$this->page] = $this->pagination; |
68
|
|
|
|
69
|
|
|
/* parse the JSON response string */ |
70
|
|
|
$key = $this->key; |
71
|
|
|
foreach (json_decode($jsonResponse, true) as $item) { |
72
|
|
|
$this->data[$key++] = new CanvasObject($item, $this->api); |
|
|
|
|
73
|
|
|
} |
74
|
|
|
} |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Parse the API response link headers into pagination information |
78
|
|
|
* |
79
|
|
|
* @param boolean|string[] $headers (Optional, defaults to `$this->api->lastHeader('link')`) |
80
|
|
|
* @return CanvasPageLink[] |
81
|
|
|
*/ |
82
|
|
|
protected function parsePageLinks($headers = false) |
83
|
|
|
{ |
84
|
|
|
$pagination = []; |
85
|
|
|
if (!$headers) { |
86
|
|
|
$headers = $this->api->lastHeader('link'); |
87
|
|
|
} |
88
|
|
|
|
89
|
|
|
/* parse Canvas page links */ |
90
|
|
|
if (preg_match_all('%<([^>]*)>\s*;\s*rel="([^"]+)"%', $headers, $links, PREG_SET_ORDER)) { |
91
|
|
|
foreach ($links as $link) { |
92
|
|
|
$pagination[$link[2]] = new CanvasPageLink($link[1], $link[2]); |
93
|
|
|
} |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
return $pagination; |
97
|
|
|
} |
98
|
|
|
/** |
99
|
|
|
* Convert a page number to an array key |
100
|
|
|
* |
101
|
|
|
* @param int $pageNumber 1-indexed page number |
102
|
|
|
* |
103
|
|
|
* @return int |
104
|
|
|
* |
105
|
|
|
* @throws CanvasArray_Exception INVALID_PAGE_NUMBER If $pageNumber < 1 |
106
|
|
|
**/ |
107
|
|
View Code Duplication |
protected function pageNumberToKey($pageNumber) |
108
|
|
|
{ |
109
|
|
|
if ($pageNumber < 1) { |
110
|
|
|
throw new CanvasArray_Exception( |
111
|
|
|
"{$pageNumber} is not a valid page number", |
112
|
|
|
CanvasArray_Exception::INVALID_PAGE_NUMBER |
113
|
|
|
); |
114
|
|
|
} |
115
|
|
|
if (isset($this->pagination[CanvasPageLink::CURRENT])) { |
116
|
|
|
return ($pageNumber - 1) * $this->pagination[CanvasPageLink::CURRENT]->getPerPage(); |
117
|
|
|
} else { |
118
|
|
|
return 0; // assume only one page (since no pagination); |
119
|
|
|
} |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Convert an array key to a page number |
124
|
|
|
* |
125
|
|
|
* @param int $key Non-negative array key |
126
|
|
|
* |
127
|
|
|
* @return int |
128
|
|
|
* |
129
|
|
|
* @throws CanvasArray_Exception INVALID_ARRAY_KEY If $key < 0 |
130
|
|
|
**/ |
131
|
|
View Code Duplication |
protected function keyToPageNumber($key) |
132
|
|
|
{ |
133
|
|
|
if ($key < 0) { |
134
|
|
|
throw new CanvasArray_Exception( |
135
|
|
|
"$key is not a valid array key", |
136
|
|
|
CanvasArray_Exception::INVALID_ARRAY_KEY |
137
|
|
|
); |
138
|
|
|
} |
139
|
|
|
|
140
|
|
|
if (isset($this->pagination[CanvasPageLink::CURRENT])) { |
141
|
|
|
return ((int) ($key / $this->pagination[CanvasPageLink::CURRENT]->getPerPage())) + 1; |
142
|
|
|
} else { |
143
|
|
|
return 1; // assume single page if no pagination |
144
|
|
|
} |
145
|
|
|
} |
146
|
|
|
|
147
|
|
|
/** |
148
|
|
|
* Request a page of responses from the API |
149
|
|
|
* |
150
|
|
|
* A page of responses will be requested if it appears that that page has |
151
|
|
|
* not yet been loaded (tested by checking if the initial element of the |
152
|
|
|
* page has been initialized in the $data array). |
153
|
|
|
* |
154
|
|
|
* @param int $pageNumber Page number to request |
155
|
|
|
* @param bool $forceRefresh (Optional) Force a refresh of backing data, |
156
|
|
|
* even if cached (defaults to `FALSE`) |
157
|
|
|
* |
158
|
|
|
* @return bool `TRUE` if the page is requested, `FALSE` if it is already |
159
|
|
|
* cached (and therefore not requested) |
160
|
|
|
**/ |
161
|
|
|
protected function requestPageNumber($pageNumber, $forceRefresh = false) |
162
|
|
|
{ |
163
|
|
|
if (!isset($this->data[$this->pageNumberToKey($pageNumber)]) || ($forceRefresh && isset($this->api))) { |
164
|
|
|
// assume one page if no pagination (and already loaded) |
165
|
|
|
if (isset($this->pagination[CanvasPageLink::CURRENT])) { |
166
|
|
|
$params = $this->pagination[CanvasPageLink::CURRENT]->getParams(); |
167
|
|
|
$params[CanvasPageLink::PARAM_PAGE_NUMBER] = $pageNumber; |
168
|
|
|
$page = $this->api->get($this->pagination[CanvasPageLink::CURRENT]->getEndpoint(), $params); |
169
|
|
|
$this->data = array_replace($this->data, $page->data); |
170
|
|
|
$pagination = $this->parsePageLinks(); |
171
|
|
|
$this->paginationPerPage[$pagination[CanvasPageLink::CURRENT]->getPageNumber()] = $pagination; |
172
|
|
|
return true; |
173
|
|
|
} |
174
|
|
|
} |
175
|
|
|
return false; |
176
|
|
|
} |
177
|
|
|
|
178
|
|
|
/** |
179
|
|
|
* Request all pages from API |
180
|
|
|
* |
181
|
|
|
* This stores the entire API response locally, in preparation for, most |
182
|
|
|
* likely, serializing this object. |
183
|
|
|
* |
184
|
|
|
* @param bool $forceRefresh (Optional) Force a refresh of backing data, |
185
|
|
|
* even if cached (defaults to `FALSE`) |
186
|
|
|
* |
187
|
|
|
* @return void |
188
|
|
|
*/ |
189
|
|
|
protected function requestAllPages($forceRefresh = false) |
190
|
|
|
{ |
191
|
|
|
$_page = $this->page; |
192
|
|
|
$_key = $this->key; |
193
|
|
|
|
194
|
|
|
/* first fall-back: just keep going from where we are */ |
195
|
|
|
$nextPageNumber = false; |
196
|
|
|
if (isset($this->pagination[CanvasPageLink::NEXT])) { |
197
|
|
|
$nextPageNumber = $this->pagination[CanvasPageLink::NEXT]->getPageNumber(); |
198
|
|
|
} |
199
|
|
|
|
200
|
|
|
/* best case: start at the beginning and request every page */ |
201
|
|
|
if (isset($this->pagination[CanvasPageLink::FIRST])) { |
202
|
|
|
$first = $this->pagination[CanvasPageLink::FIRST]->getPageNumber(); |
203
|
|
|
$this->requestPageNumber($first, $forceRefresh); |
204
|
|
View Code Duplication |
if (isset($this->paginationPerPage[$first][CanvasPageLink::NEXT])) { |
205
|
|
|
$nextPageNumber = $this->paginationPerPage[$first][CanvasPageLink::NEXT]->getPageNumber(); |
206
|
|
|
} |
207
|
|
|
} |
208
|
|
|
|
209
|
|
|
/* welp, here goes... let's hope we have a next page! */ |
210
|
|
|
while ($nextPageNumber !== false) { |
211
|
|
|
$this->requestPageNumber($nextPageNumber, true); |
212
|
|
View Code Duplication |
if (isset($this->paginationPerPage[$nextPageNumber][CanvasPageLink::NEXT])) { |
213
|
|
|
$nextPageNumber = $this->paginationPerPage[$nextPageNumber][CanvasPageLink::NEXT]->getPageNumber(); |
214
|
|
|
} else { |
215
|
|
|
$nextPageNumber = false; |
216
|
|
|
} |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
$this->page = $_page; |
220
|
|
|
$this->key = $_key; |
221
|
|
|
} |
222
|
|
|
|
223
|
|
|
/*************************************************************************** |
224
|
|
|
* ArrayObject methods |
225
|
|
|
*/ |
226
|
|
|
|
227
|
|
|
/** |
228
|
|
|
* Get the number of CanvasObjects in the Canvas response |
229
|
|
|
* |
230
|
|
|
* @return int |
231
|
|
|
* |
232
|
|
|
* @see http://php.net/manual/en/arrayobject.count.php ArrayObject::count |
233
|
|
|
**/ |
234
|
|
|
public function count() |
235
|
|
|
{ |
236
|
|
|
$this->requestAllPages(); |
237
|
|
|
return count($this->data); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
/** |
241
|
|
|
* Creates a copy of the CanvasArray |
242
|
|
|
* |
243
|
|
|
* @return CanvasObject[] |
244
|
|
|
* |
245
|
|
|
* @see http://php.net/manual/en/arrayobject.getarraycopy.php |
246
|
|
|
* ArrayObject::getArrayCopy |
247
|
|
|
**/ |
248
|
|
|
public function getArrayCopy() |
249
|
|
|
{ |
250
|
|
|
$this->requestAllPages(); |
251
|
|
|
return $this->data; |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
/*************************************************************************** |
255
|
|
|
* ArrayAccess methods |
256
|
|
|
*/ |
257
|
|
|
|
258
|
|
|
/** |
259
|
|
|
* Whether an offset exists |
260
|
|
|
* |
261
|
|
|
* @param int|string $offset |
262
|
|
|
* |
263
|
|
|
* @return bool |
264
|
|
|
* |
265
|
|
|
* @see http://php.net/manual/en/arrayaccess.offsetexists.php |
266
|
|
|
* ArrayAccess::offsetExists |
267
|
|
|
**/ |
268
|
|
|
public function offsetExists($offset) |
269
|
|
|
{ |
270
|
|
|
if (!isset($this->data[$offset])) { |
271
|
|
|
$this->requestAllPages(); |
272
|
|
|
} |
273
|
|
|
return isset($this->data[$offset]); |
274
|
|
|
} |
275
|
|
|
|
276
|
|
|
/** |
277
|
|
|
* Offset to retrieve |
278
|
|
|
* |
279
|
|
|
* @param int|string $offset |
280
|
|
|
* |
281
|
|
|
* @return CanvasObject|null |
282
|
|
|
* |
283
|
|
|
* @see http://php.net/manual/en/arrayaccess.offsetexists.php |
284
|
|
|
* ArrayAccess::offsetGet |
285
|
|
|
**/ |
286
|
|
|
public function offsetGet($offset) |
287
|
|
|
{ |
288
|
|
|
return $this->data[$offset]; |
289
|
|
|
} |
290
|
|
|
|
291
|
|
|
/** |
292
|
|
|
* Assign a value to the specified offset |
293
|
|
|
* |
294
|
|
|
* @deprecated CanvasObject and CanvasArray responses are immutable |
295
|
|
|
* |
296
|
|
|
* @param int|string $offset |
297
|
|
|
* @param CanvasObject $value |
298
|
|
|
* |
299
|
|
|
* @return void |
300
|
|
|
* |
301
|
|
|
* @throws CanvasArray_Exception IMMUTABLE All calls to this method will cause an exception |
302
|
|
|
* |
303
|
|
|
* @see http://php.net/manual/en/arrayaccess.offsetset.php |
304
|
|
|
* ArrayAccess::offsetSet |
305
|
|
|
**/ |
306
|
|
|
public function offsetSet($offset, $value) |
307
|
|
|
{ |
308
|
|
|
throw new CanvasArray_Exception( |
309
|
|
|
'Canvas responses are immutable', |
310
|
|
|
CanvasArray_Exception::IMMUTABLE |
311
|
|
|
); |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
/** |
315
|
|
|
* Unset an offset |
316
|
|
|
* |
317
|
|
|
* @deprecated CanvasObject and CanvasArray responses are immutable |
318
|
|
|
* |
319
|
|
|
* @param int|string $offset |
320
|
|
|
* |
321
|
|
|
* @return void |
322
|
|
|
* |
323
|
|
|
* @throws CanvasArray_Exception IMMUTABLE All calls to this method will |
324
|
|
|
* cause an exception |
325
|
|
|
* |
326
|
|
|
* @see http://php.net/manual/en/arrayaccess.offsetunset.php |
327
|
|
|
* ArrayAccess::offsetUnset |
328
|
|
|
**/ |
329
|
|
|
public function offsetUnset($offset) |
330
|
|
|
{ |
331
|
|
|
throw new CanvasArray_Exception( |
332
|
|
|
'Canvas responses are immutable', |
333
|
|
|
CanvasArray_Exception::IMMUTABLE |
334
|
|
|
); |
335
|
|
|
} |
336
|
|
|
|
337
|
|
|
/**************************************************************************/ |
338
|
|
|
|
339
|
|
|
/************************************************************************** |
340
|
|
|
* Iterator methods |
341
|
|
|
*/ |
342
|
|
|
|
343
|
|
|
/** |
344
|
|
|
* Return the current element |
345
|
|
|
* |
346
|
|
|
* @return CanvasObject |
347
|
|
|
* |
348
|
|
|
* @see http://php.net/manual/en/iterator.current.php Iterator::current |
349
|
|
|
**/ |
350
|
|
|
public function current() |
351
|
|
|
{ |
352
|
|
|
if (!isset($this->data[$this->key])) { |
353
|
|
|
$this->requestPageNumber($this->keyToPageNumber($this->key)); |
354
|
|
|
} |
355
|
|
|
return $this->data[$this->key]; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
/** |
359
|
|
|
* Return the key of the current element |
360
|
|
|
* |
361
|
|
|
* @return int |
362
|
|
|
* |
363
|
|
|
* @see http://php.net/manual/en/iterator.key.php Iterator::key |
364
|
|
|
**/ |
365
|
|
|
public function key() |
366
|
|
|
{ |
367
|
|
|
return $this->key; |
368
|
|
|
} |
369
|
|
|
|
370
|
|
|
/** |
371
|
|
|
* Move forward to next element |
372
|
|
|
* |
373
|
|
|
* @return void |
374
|
|
|
* |
375
|
|
|
* @see http://php.net/manual/en/iterator.next.php Iterator::next |
376
|
|
|
**/ |
377
|
|
|
public function next() |
378
|
|
|
{ |
379
|
|
|
$this->key++; |
380
|
|
|
} |
381
|
|
|
|
382
|
|
|
/** |
383
|
|
|
* Rewind the iterator to the first element |
384
|
|
|
* |
385
|
|
|
* @return void |
386
|
|
|
* |
387
|
|
|
* @see http://php.net/manual/en/iterator.rewind.php Iterator::rewind |
388
|
|
|
**/ |
389
|
|
|
public function rewind() |
390
|
|
|
{ |
391
|
|
|
$this->key = 0; |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
/** |
395
|
|
|
* Checks if current position is valid |
396
|
|
|
* |
397
|
|
|
* @return bool |
398
|
|
|
* |
399
|
|
|
* @see http://php.net/manual/en/iterator.valid.php Iterator::valid |
400
|
|
|
**/ |
401
|
|
|
public function valid() |
402
|
|
|
{ |
403
|
|
|
return ($this->offsetExists($this->key)); |
404
|
|
|
} |
405
|
|
|
|
406
|
|
|
/**************************************************************************/ |
407
|
|
|
|
408
|
|
|
/*************************************************************************** |
409
|
|
|
* Serializable methods |
410
|
|
|
*/ |
411
|
|
|
|
412
|
|
|
/** |
413
|
|
|
* String representation of CanvasArray |
414
|
|
|
* |
415
|
|
|
* @return string |
416
|
|
|
* |
417
|
|
|
* @see http://php.net/manual/en/serializable.serialize.php |
418
|
|
|
* Serializable::serialize() |
419
|
|
|
**/ |
420
|
|
|
public function serialize() |
421
|
|
|
{ |
422
|
|
|
$this->requestAllPages(); |
423
|
|
|
return serialize( |
424
|
|
|
array( |
425
|
|
|
'page' => $this->page, |
426
|
|
|
'key' => $this->key, |
427
|
|
|
'data' => $this->data |
428
|
|
|
) |
429
|
|
|
); |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
/** |
433
|
|
|
* Construct a CanvasArray from its string representation |
434
|
|
|
* |
435
|
|
|
* The data in the unserialized CanvasArray is static and cannot be |
436
|
|
|
* refreshed, as the CanvasPest API connection is _not_ serialized to |
437
|
|
|
* preserve the security of API access tokens. |
438
|
|
|
* |
439
|
|
|
* @param string $data |
440
|
|
|
* |
441
|
|
|
* @return string |
442
|
|
|
* |
443
|
|
|
* @see http://php.net/manual/en/serializable.unserialize.php |
444
|
|
|
* Serializable::unserialize() |
445
|
|
|
**/ |
446
|
|
|
public function unserialize($data) |
447
|
|
|
{ |
448
|
|
|
$_data = unserialize($data); |
449
|
|
|
$this->page = $_data['page']; |
450
|
|
|
$this->key = $_data['key']; |
451
|
|
|
$this->data = $_data['data']; |
452
|
|
|
$this->api = null; |
453
|
|
|
$this->endpoint = null; |
454
|
|
|
$this->pagination = array(); |
455
|
|
|
} |
456
|
|
|
} |
457
|
|
|
|
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.
In this case you can add the
@ignore
PhpDoc annotation to the duplicate definition and it will be ignored.