1 <?php
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
17
18 namespace OpenCloud\Common\Collection;
19
20 use Guzzle\Http\Exception\ClientErrorResponseException;
21 use Guzzle\Http\Url;
22 use Iterator;
23 use OpenCloud\Common\Http\Message\Formatter;
24
25 26 27 28 29 30 31 32
33 class PaginatedIterator extends ResourceIterator implements Iterator
34 {
35 const MARKER = 'marker';
36 const LIMIT = 'limit';
37
38 39 40
41 protected $currentMarker;
42
43 44 45
46 protected $nextUrl;
47
48 protected $defaults = array(
49
50 'limit.total' => 10000,
51 'limit.page' => 100,
52
53
54 'key.links' => 'links',
55
56
57 'key.collection' => null,
58 'key.collectionElement' => null,
59
60
61 'key.marker' => 'name',
62
63
64 'request.method' => 'GET',
65 'request.headers' => array(),
66 'request.body' => null,
67 'request.curlOptions' => array()
68 );
69
70 protected $required = array('resourceClass', 'baseUrl');
71
72 73 74 75 76 77 78 79
80 public static function factory($parent, array $options = array(), array $data = null)
81 {
82 $list = new static();
83
84 $list->setOptions($list->parseOptions($options))
85 ->setResourceParent($parent)
86 ->rewind();
87
88 if ($data) {
89 $list->setElements($data);
90 } else {
91 $list->appendNewCollection();
92 }
93
94 return $list;
95 }
96
97
98 99 100 101
102 public function setBaseUrl(Url $url)
103 {
104 $this->baseUrl = $url;
105
106 return $this;
107 }
108
109 public function current()
110 {
111 return parent::current();
112 }
113
114 public function key()
115 {
116 return parent::key();
117 }
118
119 120 121 122
123 public function next()
124 {
125 if (!$this->valid()) {
126 return false;
127 }
128
129 $current = $this->current();
130
131 $this->position++;
132 $this->updateMarkerToCurrent();
133
134 return $current;
135 }
136
137 138 139 140
141 public function updateMarkerToCurrent()
142 {
143 if (!isset($this->elements[$this->position])) {
144 return;
145 }
146
147 $element = $this->elements[$this->position];
148 $this->setMarkerFromElement($element);
149 }
150
151 protected function setMarkerFromElement($element)
152 {
153 $key = $this->getOption('key.marker');
154
155 if (isset($element->$key)) {
156 $this->currentMarker = $element->$key;
157 }
158 }
159
160 161 162 163
164 public function rewind()
165 {
166 parent::rewind();
167 $this->currentMarker = null;
168 }
169
170 public function valid()
171 {
172 $totalLimit = $this->getOption('limit.total');
173 if ($totalLimit !== false && $this->position >= $totalLimit) {
174 return false;
175 } elseif (isset($this->elements[$this->position])) {
176 return true;
177 } elseif ($this->shouldAppend() === true) {
178 $before = $this->count();
179 $this->appendNewCollection();
180 return ($this->count() > $before) ? true : false;
181 }
182
183 return false;
184 }
185
186 protected function shouldAppend()
187 {
188 return $this->currentMarker && (
189 $this->nextUrl ||
190 $this->position % $this->getOption('limit.page') == 0
191 );
192 }
193
194 195 196 197 198 199
200 public function appendElements(array $elements)
201 {
202 $this->elements = array_merge($this->elements, $elements);
203
204 return $this;
205 }
206
207 208 209 210 211 212
213 public function appendNewCollection()
214 {
215 $request = $this->resourceParent
216 ->getClient()
217 ->createRequest(
218 $this->getOption('request.method'),
219 $this->constructNextUrl(),
220 $this->getOption('request.headers'),
221 $this->getOption('request.body'),
222 $this->getOption('request.curlOptions')
223 );
224
225 try {
226 $response = $request->send();
227 } catch (ClientErrorResponseException $e) {
228 return false;
229 }
230
231 if (!($body = Formatter::decode($response)) || $response->getStatusCode() == 204) {
232 return false;
233 }
234
235 $this->nextUrl = $this->extractNextLink($body);
236
237 return $this->appendElements($this->parseResponseBody($body));
238 }
239
240 241 242 243 244 245
246 public function extractNextLink($body)
247 {
248 $key = $this->getOption('key.links');
249
250 $value = null;
251
252 if (isset($body->$key)) {
253 foreach ($body->$key as $link) {
254 if (isset($link->rel) && $link->rel == 'next') {
255 $value = $link->href;
256 break;
257 }
258 }
259 }
260
261 return $value;
262 }
263
264 265 266 267 268
269 public function constructNextUrl()
270 {
271 if (!$url = $this->nextUrl) {
272 $url = clone $this->getOption('baseUrl');
273 $query = $url->getQuery();
274
275 if (isset($this->currentMarker)) {
276 $query[static::MARKER] = $this->currentMarker;
277 }
278
279 if (($limit = $this->getOption('limit.page')) && !$query->hasKey(static::LIMIT)) {
280 $query[static::LIMIT] = $limit;
281 }
282
283 $url->setQuery($query);
284 }
285
286 return $url;
287 }
288
289 290 291 292 293 294
295 public function parseResponseBody($body)
296 {
297 $collectionKey = $this->getOption('key.collection');
298
299 $data = array();
300
301 if (is_array($body)) {
302 $data = $body;
303 } elseif (isset($body->$collectionKey)) {
304 if (null !== ($elementKey = $this->getOption('key.collectionElement'))) {
305
306 foreach ($body->$collectionKey as $item) {
307 $subValues = $item->$elementKey;
308 unset($item->$elementKey);
309 $data[] = array_merge((array) $item, (array) $subValues);
310 }
311 } else {
312
313 $data = $body->$collectionKey;
314 }
315 }
316
317 return $data;
318 }
319
320 321 322
323 public function populateAll()
324 {
325 while ($this->valid()) {
326 $this->next();
327 }
328 }
329 }
330