<?php
/**
* An abstraction for working with ordered sets of objects
*
* @copyright 2012-2013 Rackspace Hosting, Inc.
* See COPYING for licensing information
*
* @package phpOpenCloud
* @version 1.1
* @author Glen Campbell <hide@address.com>
*/
namespace OpenCloud;
require_once(__DIR__.'/base.php');
/**
* Provides an abstraction for working with ordered sets of objects
*
* Collection objects are used whenever there are multiples; for example,
* multiple objects in a container, or multiple servers in a service.
*
* @since 1.0
* @author Glen Campbell <hide@address.com>
*/
class Collection extends Base {
private
$service,
$itemclass,
$itemlist=array(),
$pointer=0,
$sortkey,
$next_page_class,
$next_page_callback,
$next_page_url;
/**
* A Collection is an array of objects
*
* @param Service $service - the service associated with the collection
* @param string $itemclass - the Class of each item in the collection
* (assumed to be the name of the factory method)
* @param array $arr - the input array
*/
public function __construct($service, $itemclass, $arr) {
$this->service = $service;
$this->debug('Collection:service=%s, class=%s, array=%s',
get_class($service), $itemclass, print_r($arr,TRUE));
$this->next_page_class = $itemclass;
$p = strrpos($itemclass, '\\');
if ($p !== FALSE)
$this->itemclass = substr($itemclass, $p+1);
else
$this->itemclass = $itemclass;
if (!is_array($arr))
throw new \OpenCloud\CollectionError(
_('Cannot create a Collection without an array'));
// save the array of items
$this->itemlist=$arr;
}
/**
* Retrieves the service associated with the Collection
*
* @return Service
*/
public function Service() {
return $this->service;
}
/**
* Resets the pointer to the beginning, but does NOT return the first item
*
* @api
* @return void
*/
public function Reset() {
$this->pointer = 0;
}
/**
* Resets the collection pointer back to the first item in the page
* and returns it
*
* This is useful if you're only interested in the first item in the page.
*
* @api
* @return Base the first item in the set
*/
public function First() {
$this->Reset();
return $this->Next();
}
/**
* Returns the next item in the page
*
* @api
* @return Base the next item or FALSE if at the end of the page
*/
public function Next() {
if ($this->pointer >= count($this->itemlist))
return FALSE;
$class = $this->itemclass;
return $this->Service()->$class($this->itemlist[$this->pointer++]);
}
/**
* Returns the number of items in the page
*
* For most services, this is the total number of items. If the Collection
* is paginated, however, this only returns the count of items in the
* current page of data.
*
* @api
* @return integer The number of items in the set
*/
public function Size() {
return count($this->itemlist);
}
/**
* sorts the collection on a specified key
*
* Note: only top-level keys can be used as the sort key. Note that this
* only sorts the data in the current page of the Collection (for
* multi-page data).
*
* @api
* @param string $keyname the name of the field to use as the sort key
* @return void
*/
public function Sort($keyname='id') {
$this->sortkey = $keyname;
usort($this->itemlist, array($this, 'sortCompare'));
}
/**
* selects only specified items from the Collection
*
* This provides a simple form of filtering on Collections. For each item
* in the collection, it calls the callback function, passing it the item.
* If the callback returns `TRUE`, then the item is retained; if it returns
* `FALSE`, then the item is deleted from the collection.
*
* Note that this should not supersede server-side filtering; the
* `Collection::Select()` method requires that *all* of the data for the
* Collection be retrieved from the server before the filtering is
* performed; this can be very inefficient, especially for large data
* sets. This method is mostly useful on smaller-sized sets.
*
* Example:
* <code>
* $services = $connection->ServiceList();
* $services->Select(function($item){ return $item->region=='ORD';});
* // now the $services Collection only has items from the ORD region
* </code>
*
* `Select()` is *destructive*; that is, it actually removes entries from
* the collection. For example, if you use `Select()` to find items with
* the ID > 10, then use it again to find items that are <= 10, it will
* return an empty list.
*
* @api
* @param callable $testfunc a callback function that is passed each item
* in turn. Note that `Select()` performs an explicit test for
* `FALSE`, so functions like `strpos()` need to be cast into a
* boolean value (and not just return the integer).
* @returns void
* @throws DomainError if callback doesn't return a boolean value
*/
public function Select($testfunc) {
foreach($this->itemlist as $index => $item) {
$test = call_user_func($testfunc, $item);
if (!is_bool($test))
throw new DomainError(
_('Callback function for Collection::Select() '.
'did not return boolean'));
if ($test === FALSE)
unset($this->itemlist[$index]);
}
}
/**
* returns the Collection object for the next page of results, or
* FALSE if there are no more pages
*
* Generally, the structure for a multi-page collection will look like
* this:
*
* $coll = $obj->Collection();
* do {
* while($item = $coll->Next()) {
* // do something with the item
* |
* } while ($coll = $coll->NextPage());
*
* @api
* @return Collection if there are more pages of results, otherwise FALSE
*/
public function NextPage() {
if (isset($this->next_page_url)) {
return call_user_func(
$this->next_page_callback,
$this->next_page_class,
$this->next_page_url);
}
return FALSE;
}
/**
* for paginated collection, sets the callback function and URL for
* the next page
*
* The callback function should have the signature:
*
* function Whatever($class, $url, $parent)
*
* and the `$url` should be the URL of the next page of results
*
* @param callable $callback the name of the function (or array of
* object, function name)
* @param string $url the URL of the next page of results
* @return void
*/
public function SetNextPageCallback($callback, $url) {
$this->next_page_callback = $callback;
$this->next_page_url = $url;
}
/********** PRIVATE METHODS **********/
/**
* Compares two values of sort keys
*/
private function sortCompare($a, $b) {
$key = $this->sortkey;
// handle strings with strcmp()
if (is_string($a->$key))
return strcmp($a->$key, $b->$key);
// handle others with logical comparisons
if ($a->$key == $b->$key)
return 0;
if ($a->$key < $b->$key)
return -1;
else
return 1;
}
} // class Collection