Overview
  • Namespace
  • Class

Namespaces

  • OpenCloud
    • Autoscale
      • Resource
    • CloudMonitoring
      • Collection
      • Exception
      • Resource
    • Common
      • Collection
      • Constants
      • Exceptions
      • Http
        • Message
      • Log
      • Resource
      • Service
    • Compute
      • Constants
      • Exception
      • Resource
    • Database
      • Resource
    • DNS
      • Collection
      • Resource
    • Identity
      • Constants
      • Resource
    • Image
      • Enum
      • Resource
        • JsonPatch
        • Schema
    • LoadBalancer
      • Collection
      • Enum
      • Resource
    • Networking
      • Resource
    • ObjectStore
      • Constants
      • Exception
      • Resource
      • Upload
    • Orchestration
      • Resource
    • Queues
      • Collection
      • Exception
      • Resource
    • Volume
      • Resource

Classes

  • OpenCloud\Volume\Resource\Snapshot
  • OpenCloud\Volume\Resource\Volume
  • OpenCloud\Volume\Resource\VolumeType
  1 <?php
  2 /**
  3  * Copyright 2012-2014 Rackspace US, Inc.
  4  *
  5  * Licensed under the Apache License, Version 2.0 (the "License");
  6  * you may not use this file except in compliance with the License.
  7  * You may obtain a copy of the License at
  8  *
  9  * http://www.apache.org/licenses/LICENSE-2.0
 10  *
 11  * Unless required by applicable law or agreed to in writing, software
 12  * distributed under the License is distributed on an "AS IS" BASIS,
 13  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 14  * See the License for the specific language governing permissions and
 15  * limitations under the License.
 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  * Class ResourceIterator is tasked with iterating over resource collections - many of which are paginated. Based on
 27  * a base URL, the iterator will append elements based on further requests to the API. Each time this happens,
 28  * query parameters (marker) are updated based on the current value.
 29  *
 30  * @package OpenCloud\Common\Collection
 31  * @since   1.8.0
 32  */
 33 class PaginatedIterator extends ResourceIterator implements Iterator
 34 {
 35     const MARKER = 'marker';
 36     const LIMIT = 'limit';
 37 
 38     /**
 39      * @var string Used for requests which append elements.
 40      */
 41     protected $currentMarker;
 42 
 43     /**
 44      * @var \Guzzle\Http\Url The next URL for pagination
 45      */
 46     protected $nextUrl;
 47 
 48     protected $defaults = array(
 49         // Collection limits
 50         'limit.total'           => 10000,
 51         'limit.page'            => 100,
 52 
 53         // The "links" element key in response
 54         'key.links'             => 'links',
 55 
 56         // JSON structure
 57         'key.collection'        => null,
 58         'key.collectionElement' => null,
 59 
 60         // The property used as the marker
 61         'key.marker'            => 'name',
 62 
 63         // Options for "next page" request
 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      * Basic factory method to easily instantiate a new ResourceIterator.
 74      *
 75      * @param       $parent  The parent object
 76      * @param array $options Iterator options
 77      * @param array $data    Optional data to set initially
 78      * @return static
 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      * @param Url $url
100      * @return $this
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      * {@inheritDoc}
121      * Also update the current marker.
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      * Update the current marker based on the current element. The marker will be based on a particular property of this
139      * current element, so you must retrieve it first.
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      * {@inheritDoc}
162      * Also reset current marker.
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      * Append an array of standard objects to the current collection.
196      *
197      * @param array $elements
198      * @return $this
199      */
200     public function appendElements(array $elements)
201     {
202         $this->elements = array_merge($this->elements, $elements);
203 
204         return $this;
205     }
206 
207     /**
208      * Retrieve a new page of elements from the API (based on a new request), parse its response, and append them to the
209      * collection.
210      *
211      * @return $this|bool
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      * Based on the response body, extract the explicitly set "link" value if provided.
242      *
243      * @param $body
244      * @return bool
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      * Make the next page URL.
266      *
267      * @return Url|string
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      * Based on the response from the API, parse it for the data we need (i.e. an meaningful array of elements).
291      *
292      * @param $body
293      * @return array
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                 // The object has element levels which need to be iterated over
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                 // The object has a top-level collection name only
313                 $data = $body->$collectionKey;
314             }
315         }
316 
317         return $data;
318     }
319 
320     /**
321      * Walk the entire collection, populating everything.
322      */
323     public function populateAll()
324     {
325         while ($this->valid()) {
326             $this->next();
327         }
328     }
329 }
330 
API documentation generated by ApiGen