Overview

Namespaces

  • OpenCloud
    • Autoscale
      • Resource
    • CloudMonitoring
      • 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
      • Enum
      • Resource
    • ObjectStore
      • Constants
      • Exception
      • Resource
      • Upload
    • Orchestration
    • Queues
      • Exception
      • Resource
    • Volume
      • Resource
  • PHP

Classes

  • ArrayCollection
  • PaginatedIterator
  • ResourceIterator
  • Overview
  • Namespace
  • Class
  • Tree
  • Download
  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: 
149:         $key = $this->getOption('key.marker');
150: 
151:         if (isset($element->$key)) {
152:             $this->currentMarker = $element->$key;
153:         }
154:     }
155: 
156:     /**
157:      * {@inheritDoc}
158:      * Also reset current marker.
159:      */
160:     public function rewind()
161:     {
162:         parent::rewind();
163:         $this->currentMarker = null;
164:     }
165: 
166:     public function valid()
167:     {
168:         if ($this->getOption('limit.total') !== false && $this->position >= $this->getOption('limit.total')) {
169:             return false;
170:         } elseif (isset($this->elements[$this->position])) {
171:             return true;
172:         } elseif ($this->shouldAppend() === true) {
173:             $before = $this->count();
174:             $this->appendNewCollection();
175: 
176:             return ($this->count() > $before) ? true : false;
177:         }
178: 
179:         return false;
180:     }
181: 
182:     protected function shouldAppend()
183:     {
184:         return $this->currentMarker && $this->position % $this->getOption('limit.page') == 0;
185:     }
186: 
187:     /**
188:      * Append an array of standard objects to the current collection.
189:      *
190:      * @param array $elements
191:      * @return $this
192:      */
193:     public function appendElements(array $elements)
194:     {
195:         $this->elements = array_merge($this->elements, $elements);
196: 
197:         return $this;
198:     }
199: 
200:     /**
201:      * Retrieve a new page of elements from the API (based on a new request), parse its response, and append them to the
202:      * collection.
203:      *
204:      * @return $this|bool
205:      */
206:     public function appendNewCollection()
207:     {
208:         $request = $this->resourceParent
209:             ->getClient()
210:             ->createRequest(
211:                 $this->getOption('request.method'),
212:                 $this->constructNextUrl(),
213:                 $this->getOption('request.headers'),
214:                 $this->getOption('request.body'),
215:                 $this->getOption('request.curlOptions')
216:             );
217: 
218:         try {
219:             $response = $request->send();
220:         } catch (ClientErrorResponseException $e) {
221:             return false;
222:         }
223: 
224:         if (!($body = Formatter::decode($response)) || $response->getStatusCode() == 204) {
225:             return false;
226:         }
227: 
228:         $this->nextUrl = $this->extractNextLink($body);
229: 
230:         return $this->appendElements($this->parseResponseBody($body));
231:     }
232: 
233:     /**
234:      * Based on the response body, extract the explicitly set "link" value if provided.
235:      *
236:      * @param $body
237:      * @return bool
238:      */
239:     public function extractNextLink($body)
240:     {
241:         $key = $this->getOption('key.links');
242: 
243:         $value = null;
244: 
245:         if (isset($body->$key)) {
246:             foreach ($body->$key as $link) {
247:                 if (isset($link->rel) && $link->rel == 'next') {
248:                     $value = $link->href;
249:                     break;
250:                 }
251:             }
252:         }
253: 
254:         return $value;
255:     }
256: 
257:     /**
258:      * Make the next page URL.
259:      *
260:      * @return Url|string
261:      */
262:     public function constructNextUrl()
263:     {
264:         if (!$url = $this->nextUrl) {
265: 
266:             $url = clone $this->getOption('baseUrl');
267:             $query = $url->getQuery();
268: 
269:             if (isset($this->currentMarker)) {
270:                 $query[static::MARKER] = $this->currentMarker;
271:             }
272: 
273:             if (($limit = $this->getOption('limit.page')) && !$query->hasKey(static::LIMIT)) {
274:                 $query[static::LIMIT] = $limit;
275:             }
276: 
277:             $url->setQuery($query);
278:         }
279: 
280:         return $url;
281:     }
282: 
283:     /**
284:      * Based on the response from the API, parse it for the data we need (i.e. an meaningful array of elements).
285:      *
286:      * @param $body
287:      * @return array
288:      */
289:     public function parseResponseBody($body)
290:     {
291:         $collectionKey = $this->getOption('key.collection');
292: 
293:         $data = array();
294: 
295:         if (is_array($body)) {
296:             $data = $body;
297:         } elseif (isset($body->$collectionKey)) {
298:             if (null !== ($elementKey = $this->getOption('key.collectionElement'))) {
299:                 // The object has element levels which need to be iterated over
300:                 foreach ($body->$collectionKey as $item) {
301:                     $subValues = $item->$elementKey;
302:                     unset($item->$elementKey);
303:                     $data[] = array_merge((array) $item, (array) $subValues);
304:                 }
305:             } else {
306:                 // The object has a top-level collection name only
307:                 $data = $body->$collectionKey;
308:             }
309:         }
310: 
311:         return $data;
312:     }
313: 
314:     /**
315:      * Walk the entire collection, populating everything.
316:      */
317:     public function populateAll()
318:     {
319:         while ($this->valid()) {
320:             $this->next();
321:         }
322:     }
323: }
324: 
PHP OpenCloud API API documentation generated by ApiGen 2.8.0