1: <?php
2:
3: namespace OpenCloud\Common;
4:
5: /**
6: * Provides an abstraction for working with ordered sets of objects
7: *
8: * Collection objects are used whenever there are multiples; for example,
9: * multiple objects in a container, or multiple servers in a service.
10: *
11: * @since 1.0
12: * @author Glen Campbell <glen.campbell@rackspace.com>
13: * @author Jamie Hannaford <jamie.hannaford@rackspace.com>
14: */
15: class Collection extends Base
16: {
17:
18: private $service;
19: private $itemclass;
20: private $itemlist = array();
21: private $pointer = 0;
22: private $sortkey;
23: private $next_page_class;
24: private $next_page_callback;
25: private $next_page_url;
26:
27: /**
28: * A Collection is an array of objects
29: *
30: * Some assumptions:
31: * * The `Collection` class assumes that there exists on its service
32: * a factory method with the same name of the class. For example, if
33: * you create a Collection of class `Foobar`, it will attempt to call
34: * the method `parent::Foobar()` to create instances of that class.
35: * * It assumes that the factory method can take an array of values, and
36: * it passes that to the method.
37: *
38: * @param Service $service - the service associated with the collection
39: * @param string $itemclass - the Class of each item in the collection
40: * (assumed to be the name of the factory method)
41: * @param array $arr - the input array
42: */
43: public function __construct($service, $itemclass, $array)
44: {
45: $this->service = $service;
46:
47: $this->getLogger()->info(
48: 'Collection:service={class}, class={itemClass}, array={array}',
49: array(
50: 'class' => get_class($service),
51: 'itemClass' => $itemclass,
52: 'array' => print_r($array, true)
53: )
54: );
55:
56: $this->next_page_class = $itemclass;
57:
58: if (false !== ($classNamePos = strrpos($itemclass, '\\'))) {
59: $this->itemclass = substr($itemclass, $classNamePos + 1);
60: } else {
61: $this->itemclass = $itemclass;
62: }
63:
64: if (!is_array($array)) {
65: throw new Exceptions\CollectionError(
66: Lang::translate('Cannot create a Collection without an array')
67: );
68: }
69:
70: // save the array of items
71: $this->setItemList($array);
72: }
73:
74: /**
75: * Set the entire data array.
76: *
77: * @param array $array
78: */
79: public function setItemList(array $array)
80: {
81: $this->itemlist = $array;
82: }
83:
84: /**
85: * Retrieve the entire data array.
86: *
87: * @return array
88: */
89: public function getItemList()
90: {
91: return $this->itemlist;
92: }
93:
94: /**
95: * Returns the number of items in the collection
96: *
97: * For most services, this is the total number of items. If the Collection
98: * is paginated, however, this only returns the count of items in the
99: * current page of data.
100: *
101: * @return int
102: */
103: public function count()
104: {
105: return count($this->itemlist);
106: }
107:
108: /**
109: * Pseudonym for count()
110: *
111: * @codeCoverageIgnore
112: */
113: public function size()
114: {
115: return $this->count();
116: }
117:
118: /**
119: * Retrieves the service associated with the Collection
120: *
121: * @return Service
122: */
123: public function service()
124: {
125: return $this->service;
126: }
127:
128: /**
129: * Resets the pointer to the beginning, but does NOT return the first item
130: *
131: * @api
132: * @return void
133: */
134: public function reset()
135: {
136: $this->pointer = 0;
137: }
138:
139: /**
140: * Resets the collection pointer back to the first item in the page
141: * and returns it
142: *
143: * This is useful if you're only interested in the first item in the page.
144: *
145: * @api
146: * @return Base the first item in the set
147: */
148: public function first()
149: {
150: $this->reset();
151: return $this->next();
152: }
153:
154: /**
155: * Returns the next item in the page
156: *
157: * @api
158: * @return Base the next item or FALSE if at the end of the page
159: */
160: public function next()
161: {
162: if ($this->pointer >= $this->count()) {
163: return false;
164: }
165:
166: $service = $this->service();
167:
168: if (method_exists($service, $this->itemclass)) {
169: return $service->{$this->itemclass}($this->itemlist[$this->pointer++]);
170: } elseif (method_exists($service, 'resource')) {
171: return $service->resource($this->itemclass, $this->itemlist[$this->pointer++]);
172: }
173: // @codeCoverageIgnoreStart
174: return false;
175: // @codeCoverageIgnoreEnd
176: }
177:
178: /**
179: * sorts the collection on a specified key
180: *
181: * Note: only top-level keys can be used as the sort key. Note that this
182: * only sorts the data in the current page of the Collection (for
183: * multi-page data).
184: *
185: * @api
186: * @param string $keyname the name of the field to use as the sort key
187: * @return void
188: */
189: public function sort($keyname = 'id')
190: {
191: $this->sortkey = $keyname;
192: usort($this->itemlist, array($this, 'sortCompare'));
193: }
194:
195: /**
196: * selects only specified items from the Collection
197: *
198: * This provides a simple form of filtering on Collections. For each item
199: * in the collection, it calls the callback function, passing it the item.
200: * If the callback returns `TRUE`, then the item is retained; if it returns
201: * `FALSE`, then the item is deleted from the collection.
202: *
203: * Note that this should not supersede server-side filtering; the
204: * `Collection::Select()` method requires that *all* of the data for the
205: * Collection be retrieved from the server before the filtering is
206: * performed; this can be very inefficient, especially for large data
207: * sets. This method is mostly useful on smaller-sized sets.
208: *
209: * Example:
210: * <code>
211: * $services = $connection->ServiceList();
212: * $services->Select(function($item){ return $item->region=='ORD';});
213: * // now the $services Collection only has items from the ORD region
214: * </code>
215: *
216: * `Select()` is *destructive*; that is, it actually removes entries from
217: * the collection. For example, if you use `Select()` to find items with
218: * the ID > 10, then use it again to find items that are <= 10, it will
219: * return an empty list.
220: *
221: * @api
222: * @param callable $testfunc a callback function that is passed each item
223: * in turn. Note that `Select()` performs an explicit test for
224: * `FALSE`, so functions like `strpos()` need to be cast into a
225: * boolean value (and not just return the integer).
226: * @returns void
227: * @throws DomainError if callback doesn't return a boolean value
228: */
229: public function select($testfunc)
230: {
231: foreach ($this->getItemList() as $index => $item) {
232: $test = call_user_func($testfunc, $item);
233: if (!is_bool($test)) {
234: throw new Exceptions\DomainError(
235: Lang::translate('Callback function for Collection::Select() did not return boolean')
236: );
237: }
238: if ($test === false) {
239: unset($this->itemlist[$index]);
240: }
241: }
242: }
243:
244: /**
245: * returns the Collection object for the next page of results, or
246: * FALSE if there are no more pages
247: *
248: * Generally, the structure for a multi-page collection will look like
249: * this:
250: *
251: * $coll = $obj->Collection();
252: * do {
253: * while($item = $coll->Next()) {
254: * // do something with the item
255: * }
256: * } while ($coll = $coll->NextPage());
257: *
258: * @api
259: * @return Collection if there are more pages of results, otherwise FALSE
260: */
261: public function nextPage()
262: {
263: if (isset($this->next_page_url)) {
264: return call_user_func(
265: $this->next_page_callback,
266: $this->next_page_class,
267: $this->next_page_url
268: );
269: }
270: // @codeCoverageIgnoreStart
271: return false;
272: // @codeCoverageIgnoreEnd
273: }
274:
275: /**
276: * for paginated collection, sets the callback function and URL for
277: * the next page
278: *
279: * The callback function should have the signature:
280: *
281: * function Whatever($class, $url, $parent)
282: *
283: * and the `$url` should be the URL of the next page of results
284: *
285: * @param callable $callback the name of the function (or array of
286: * object, function name)
287: * @param string $url the URL of the next page of results
288: * @return void
289: */
290: public function setNextPageCallback($callback, $url)
291: {
292: $this->next_page_callback = $callback;
293: $this->next_page_url = $url;
294: }
295:
296: /**
297: * Compares two values of sort keys
298: */
299: private function sortCompare($a, $b)
300: {
301: $key = $this->sortkey;
302:
303: // handle strings with strcmp()
304: if (is_string($a->$key)) {
305: return strcmp($a->$key, $b->$key);
306: }
307:
308: // handle others with logical comparisons
309: if ($a->$key == $b->$key) {
310: return 0;
311: }
312:
313: if ($a->$key < $b->$key) {
314: return -1;
315: } else {
316: return 1;
317: }
318: }
319:
320: }
321: