Overview

Namespaces

  • OpenCloud
    • Autoscale
      • Resource
    • CloudMonitoring
      • Exception
      • Resource
    • Common
      • Collection
      • Constants
      • Exceptions
      • Http
        • Message
      • Identity
      • Log
      • Service
    • Compute
      • Constants
      • Exception
      • Resource
    • Database
      • Resource
    • DNS
      • Resource
    • LoadBalancer
      • 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:  * PHP OpenCloud library.
  4:  * 
  5:  * @copyright 2013 Rackspace Hosting, Inc. See LICENSE for information.
  6:  * @license   https://www.apache.org/licenses/LICENSE-2.0
  7:  * @author    Jamie Hannaford <jamie.hannaford@rackspace.com>
  8:  */
  9: 
 10: namespace OpenCloud\Common\Collection;
 11: 
 12: use Iterator;
 13: use Guzzle\Http\Url;
 14: use Guzzle\Http\Exception\ClientErrorResponseException;
 15: use OpenCloud\Common\Http\Message\Formatter;
 16: 
 17: /**
 18:  * Class ResourceIterator is tasked with iterating over resource collections - many of which are paginated. Based on
 19:  * a base URL, the iterator will append elements based on further requests to the API. Each time this happens,
 20:  * query parameters (marker) are updated based on the current value.
 21:  *
 22:  * @package OpenCloud\Common\Collection
 23:  * @since   1.8.0
 24:  */
 25: class PaginatedIterator extends ResourceIterator implements Iterator
 26: {
 27:     /**
 28:      * @var string Used for requests which append elements.
 29:      */
 30:     private $currentMarker;
 31: 
 32:     /**
 33:      * @var \Guzzle\Http\Url The next URL for pagination
 34:      */
 35:     private $nextUrl;
 36: 
 37:     protected $defaults = array(
 38:         // Collection limits
 39:         'limit.total' => 10000,
 40:         'limit.page'  => 100,
 41: 
 42:         // The "links" element key in response
 43:         'key.links'  => 'links',
 44: 
 45:         // JSON structure
 46:         'key.collection' => null,
 47:         'key.collectionElement' => null,
 48: 
 49:         // The property used as the marker
 50:         'key.marker' => 'name',
 51: 
 52:         // Options for "next page" request
 53:         'request.method'      => 'GET',
 54:         'request.headers'     => array(),
 55:         'request.body'        => null,
 56:         'request.curlOptions' => array()
 57:     );
 58: 
 59:     protected $required = array('resourceClass', 'baseUrl');
 60: 
 61:     /**
 62:      * Basic factory method to easily instantiate a new ResourceIterator.
 63:      *
 64:      * @param       $parent The parent object
 65:      * @param Url   $url    The base URL
 66:      * @param array $params Options for this iterator
 67:      * @return static
 68:      * @throws \OpenCloud\Common\Exceptions\InvalidArgumentError
 69:      */
 70:     public static function factory($parent, array $options = array(), array $data = null)
 71:     {
 72:         $list = new static();
 73: 
 74:         $list->setOptions($list->parseOptions($options))
 75:             ->setResourceParent($parent)
 76:             ->rewind();
 77: 
 78:         if ($data) {
 79:             $list->setElements($data);
 80:         } else {
 81:             $list->appendNewCollection();
 82:         }
 83: 
 84:         return $list;
 85:     }
 86: 
 87: 
 88:     /**
 89:      * @param Url $url
 90:      * @return $this
 91:      */
 92:     public function setBaseUrl(Url $url)
 93:     {
 94:         $this->baseUrl = $url;
 95:         return $this;
 96:     }
 97: 
 98:     public function current()
 99:     {
100:         return parent::current();
101:     }
102: 
103:     public function key()
104:     {
105:         return parent::key();
106:     }
107: 
108:     /**
109:      * {@inheritDoc}
110:      * Also update the current marker.
111:      */
112:     public function next()
113:     {
114:         if (!$this->valid()) {
115:             return false;
116:         }
117:         $this->position++;
118:         $this->updateMarkerToCurrent();
119:         return $this->current();
120:     }
121: 
122:     /**
123:      * Update the current marker based on the current element. The marker will be based on a particular property of this
124:      * current element, so you must retrieve it first.
125:      */
126:     public function updateMarkerToCurrent()
127:     {
128:         if (!isset($this->elements[$this->position])) {
129:             return;
130:         }
131: 
132:         $element = $this->elements[$this->position];
133:         $key = $this->getOption('key.marker');
134:         if (isset($element->$key)) {
135:             $this->currentMarker = $element->$key;
136:         }
137:     }
138: 
139:     /**
140:      * {@inheritDoc}
141:      * Also reset current marker.
142:      */
143:     public function rewind()
144:     {
145:         parent::rewind();
146:         $this->currentMarker = null;
147:     }
148: 
149:     public function valid()
150:     {
151:         if ($this->position >= $this->getOption('limit.total')) {
152:             return false;
153:         } elseif (isset($this->elements[$this->position])) {
154:             return true;
155:         } elseif ($this->currentMarker && $this->getOption('limit.page') % ($this->position + 1) == 0) {
156:             $before = $this->count();
157:             $this->appendNewCollection();
158:             return ($this->count() > $before) ? true : false;
159:         }
160: 
161:         return false;
162:     }
163: 
164:     /**
165:      * Append an array of standard objects to the current collection.
166:      *
167:      * @param array $elements
168:      * @return $this
169:      */
170:     public function appendElements(array $elements)
171:     {
172:         $this->elements = array_merge($this->elements, $elements);
173:         return $this;
174:     }
175: 
176:     /**
177:      * Retrieve a new page of elements from the API (based on a new request), parse its response, and append them to the
178:      * collection.
179:      *
180:      * @return $this|bool
181:      */
182:     public function appendNewCollection()
183:     {
184:         $request = $this->resourceParent
185:             ->getClient()
186:             ->createRequest(
187:                 $this->getOption('request.method'),
188:                 $this->constructNextUrl(),
189:                 $this->getOption('request.headers'),
190:                 $this->getOption('request.body'),
191:                 $this->getOption('request.curlOptions')
192:             );
193: 
194:         try {
195:             $response = $request->send();
196:         } catch (ClientErrorResponseException $e) {
197:             return false;
198:         }
199: 
200:         if (!($body = Formatter::decode($response)) || $response->getStatusCode() == 204) {
201:             return false;
202:         }
203: 
204:         if ($nextUrl = $this->extractNextLink($body)) {
205:             $this->nextUrl = $nextUrl;
206:         }
207: 
208:         return $this->appendElements($this->parseResponseBody($body));
209:     }
210: 
211:     /**
212:      * Based on the response body, extract the explicitly set "link" value if provided.
213:      *
214:      * @param $body
215:      * @return bool
216:      */
217:     public function extractNextLink($body)
218:     {
219:         $key = $this->getOption('key.links');
220: 
221:         $value = false;
222: 
223:         if (isset($body->$key)) {
224:             foreach ($body->$key as $link) {
225:                 if (isset($link->rel) && $link->rel == 'next') {
226:                     $value = $link->href;
227:                     break;
228:                 }
229:             }
230:         }
231: 
232:         return $value;
233:     }
234: 
235:     /**
236:      * Make the next page URL.
237:      *
238:      * @return Url|string
239:      */
240:     public function constructNextUrl()
241:     {
242:         if (!$url = $this->nextUrl) {
243:             $url = $this->getOption('baseUrl');
244:             if ($this->currentMarker) {
245:                 $url->setQuery(array('marker' => $this->currentMarker, 'limit' => $this->getOption('limit.page')));
246:             }
247:         }
248: 
249:         return $url;
250:     }
251: 
252:     /**
253:      * Based on the response from the API, parse it for the data we need (i.e. an meaningful array of elements).
254:      *
255:      * @param $body
256:      * @return array
257:      */
258:     public function parseResponseBody($body)
259:     {
260:         $collectionKey = $this->getOption('key.collection');
261: 
262:         $data = array();
263: 
264:         if (is_array($body)) {
265:             $data = $body;
266:         } elseif (isset($body->$collectionKey)) {
267:             if (null !== ($elementKey = $this->getOption('key.collectionElement'))) {
268:                 // The object has element levels which need to be iterated over
269:                 foreach ($body->$collectionKey as $item) {
270:                     $subValues = $item->$elementKey;
271:                     unset($item->$elementKey);
272:                     $data[] = array_merge((array) $item, (array) $subValues);
273:                 }
274:             } else {
275:                 // The object has a top-level collection name only
276:                 $data = $body->$collectionKey;
277:             }
278:         }
279: 
280:         return $data;
281:     }
282: 
283:     /**
284:      * Walk the entire collection, populating everything.
285:      */
286:     public function populateAll()
287:     {
288:         while ($this->valid()) {
289:             $this->next();
290:         }
291:     }
292: 
293: }
PHP OpenCloud API API documentation generated by ApiGen 2.8.0