Overview

Namespaces

  • OpenCloud
    • Autoscale
      • Resource
    • CDN
      • 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
      • Enum
      • Exception
      • Resource
      • Upload
    • Orchestration
      • Resource
    • Queues
      • Collection
      • Exception
      • Resource
    • Volume
      • Resource
  • PHP

Classes

  • ArrayCollection
  • PaginatedIterator
  • ResourceIterator
  • Overview
  • Namespace
  • Class
  • Tree
  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 2.8.0