Completed
Push — master ( bc2b20...9217c1 )
by Seth
01:48
created

CanvasArray::parsePageLinks()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.2
c 0
b 0
f 0
cc 4
eloc 8
nc 4
nop 1
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);
0 ignored issues
show
Unused Code introduced by
The call to CanvasObject::__construct() has too many arguments starting with $this->api.

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.

Loading history...
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