diff options
Diffstat (limited to 'engine/classes/ElggBatch.php')
-rw-r--r-- | engine/classes/ElggBatch.php | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/engine/classes/ElggBatch.php b/engine/classes/ElggBatch.php new file mode 100644 index 000000000..d810ea066 --- /dev/null +++ b/engine/classes/ElggBatch.php @@ -0,0 +1,433 @@ +<?php +/** + * Efficiently run operations on batches of results for any function + * that supports an options array. + * + * This is usually used with elgg_get_entities() and friends, + * elgg_get_annotations(), and elgg_get_metadata(). + * + * If you pass a valid PHP callback, all results will be run through that + * callback. You can still foreach() through the result set after. Valid + * PHP callbacks can be a string, an array, or a closure. + * {@link http://php.net/manual/en/language.pseudo-types.php} + * + * The callback function must accept 3 arguments: an entity, the getter + * used, and the options used. + * + * Results from the callback are stored in callbackResult. If the callback + * returns only booleans, callbackResults will be the combined result of + * all calls. If no entities are processed, callbackResults will be null. + * + * If the callback returns anything else, callbackresult will be an indexed + * array of whatever the callback returns. If returning error handling + * information, you should include enough information to determine which + * result you're referring to. + * + * Don't combine returning bools and returning something else. + * + * Note that returning false will not stop the foreach. + * + * @warning If your callback or foreach loop deletes or disable entities + * you MUST call setIncrementOffset(false) or set that when instantiating. + * This forces the offset to stay what it was in the $options array. + * + * @example + * <code> + * // using foreach + * $batch = new ElggBatch('elgg_get_entities', array()); + * $batch->setIncrementOffset(false); + * + * foreach ($batch as $entity) { + * $entity->disable(); + * } + * + * // using both a callback + * $callback = function($result, $getter, $options) { + * var_dump("Looking at annotation id: $result->id"); + * return true; + * } + * + * $batch = new ElggBatch('elgg_get_annotations', array('guid' => 2), $callback); + * </code> + * + * @package Elgg.Core + * @subpackage DataModel + * @link http://docs.elgg.org/DataModel/ElggBatch + * @since 1.8 + */ +class ElggBatch + implements Iterator { + + /** + * The objects to interator over. + * + * @var array + */ + private $results = array(); + + /** + * The function used to get results. + * + * @var mixed A string, array, or closure, or lamda function + */ + private $getter = null; + + /** + * The number of results to grab at a time. + * + * @var int + */ + private $chunkSize = 25; + + /** + * A callback function to pass results through. + * + * @var mixed A string, array, or closure, or lamda function + */ + private $callback = null; + + /** + * Start after this many results. + * + * @var int + */ + private $offset = 0; + + /** + * Stop after this many results. + * + * @var int + */ + private $limit = 0; + + /** + * Number of processed results. + * + * @var int + */ + private $retrievedResults = 0; + + /** + * The index of the current result within the current chunk + * + * @var int + */ + private $resultIndex = 0; + + /** + * The index of the current chunk + * + * @var int + */ + private $chunkIndex = 0; + + /** + * The number of results iterated through + * + * @var int + */ + private $processedResults = 0; + + /** + * Is the getter a valid callback + * + * @var bool + */ + private $validGetter = null; + + /** + * The result of running all entities through the callback function. + * + * @var mixed + */ + public $callbackResult = null; + + /** + * If false, offset will not be incremented. This is used for callbacks/loops that delete. + * + * @var bool + */ + private $incrementOffset = true; + + /** + * Entities that could not be instantiated during a fetch + * + * @var stdClass[] + */ + private $incompleteEntities = array(); + + /** + * Total number of incomplete entities fetched + * + * @var int + */ + private $totalIncompletes = 0; + + /** + * Batches operations on any elgg_get_*() or compatible function that supports + * an options array. + * + * Instead of returning all objects in memory, it goes through $chunk_size + * objects, then requests more from the server. This avoids OOM errors. + * + * @param string $getter The function used to get objects. Usually + * an elgg_get_*() function, but can be any valid PHP callback. + * @param array $options The options array to pass to the getter function. If limit is + * not set, 10 is used as the default. In most cases that is not + * what you want. + * @param mixed $callback An optional callback function that all results will be passed + * to upon load. The callback needs to accept $result, $getter, + * $options. + * @param int $chunk_size The number of entities to pull in before requesting more. + * You have to balance this between running out of memory in PHP + * and hitting the db server too often. + * @param bool $inc_offset Increment the offset on each fetch. This must be false for + * callbacks that delete rows. You can set this after the + * object is created with {@see ElggBatch::setIncrementOffset()}. + */ + public function __construct($getter, $options, $callback = null, $chunk_size = 25, + $inc_offset = true) { + + $this->getter = $getter; + $this->options = $options; + $this->callback = $callback; + $this->chunkSize = $chunk_size; + $this->setIncrementOffset($inc_offset); + + if ($this->chunkSize <= 0) { + $this->chunkSize = 25; + } + + // store these so we can compare later + $this->offset = elgg_extract('offset', $options, 0); + $this->limit = elgg_extract('limit', $options, 10); + + // if passed a callback, create a new ElggBatch with the same options + // and pass each to the callback. + if ($callback && is_callable($callback)) { + $batch = new ElggBatch($getter, $options, null, $chunk_size, $inc_offset); + + $all_results = null; + + foreach ($batch as $result) { + if (is_string($callback)) { + $result = $callback($result, $getter, $options); + } else { + $result = call_user_func_array($callback, array($result, $getter, $options)); + } + + if (!isset($all_results)) { + if ($result === true || $result === false || $result === null) { + $all_results = $result; + } else { + $all_results = array(); + } + } + + if (($result === true || $result === false || $result === null) && !is_array($all_results)) { + $all_results = $result && $all_results; + } else { + $all_results[] = $result; + } + } + + $this->callbackResult = $all_results; + } + } + + /** + * Tell the process that an entity was incomplete during a fetch + * + * @param stdClass $row + * + * @access private + */ + public function reportIncompleteEntity(stdClass $row) { + $this->incompleteEntities[] = $row; + } + + /** + * Fetches the next chunk of results + * + * @return bool + */ + private function getNextResultsChunk() { + + // always reset results. + $this->results = array(); + + if (!isset($this->validGetter)) { + $this->validGetter = is_callable($this->getter); + } + + if (!$this->validGetter) { + return false; + } + + $limit = $this->chunkSize; + + // if someone passed limit = 0 they want everything. + if ($this->limit != 0) { + if ($this->retrievedResults >= $this->limit) { + return false; + } + + // if original limit < chunk size, set limit to original limit + // else if the number of results we'll fetch if greater than the original limit + if ($this->limit < $this->chunkSize) { + $limit = $this->limit; + } elseif ($this->retrievedResults + $this->chunkSize > $this->limit) { + // set the limit to the number of results remaining in the original limit + $limit = $this->limit - $this->retrievedResults; + } + } + + if ($this->incrementOffset) { + $offset = $this->offset + $this->retrievedResults; + } else { + $offset = $this->offset + $this->totalIncompletes; + } + + $current_options = array( + 'limit' => $limit, + 'offset' => $offset, + '__ElggBatch' => $this, + ); + + $options = array_merge($this->options, $current_options); + + $this->incompleteEntities = array(); + $this->results = call_user_func_array($this->getter, array($options)); + + $num_results = count($this->results); + $num_incomplete = count($this->incompleteEntities); + + $this->totalIncompletes += $num_incomplete; + + if ($this->incompleteEntities) { + // pad the front of the results with nulls representing the incompletes + array_splice($this->results, 0, 0, array_pad(array(), $num_incomplete, null)); + // ...and skip past them + reset($this->results); + for ($i = 0; $i < $num_incomplete; $i++) { + next($this->results); + } + } + + if ($this->results) { + $this->chunkIndex++; + + // let the system know we've jumped past the nulls + $this->resultIndex = $num_incomplete; + + $this->retrievedResults += ($num_results + $num_incomplete); + if ($num_results == 0) { + // This fetch was *all* incompletes! We need to fetch until we can either + // offer at least one row to iterate over, or give up. + return $this->getNextResultsChunk(); + } + return true; + } else { + return false; + } + } + + /** + * Increment the offset from the original options array? Setting to + * false is required for callbacks that delete rows. + * + * @param bool $increment Set to false when deleting data + * @return void + */ + public function setIncrementOffset($increment = true) { + $this->incrementOffset = (bool) $increment; + } + + /** + * Implements Iterator + */ + + /** + * PHP Iterator Interface + * + * @see Iterator::rewind() + * @return void + */ + public function rewind() { + $this->resultIndex = 0; + $this->retrievedResults = 0; + $this->processedResults = 0; + + // only grab results if we haven't yet or we're crossing chunks + if ($this->chunkIndex == 0 || $this->limit > $this->chunkSize) { + $this->chunkIndex = 0; + $this->getNextResultsChunk(); + } + } + + /** + * PHP Iterator Interface + * + * @see Iterator::current() + * @return mixed + */ + public function current() { + return current($this->results); + } + + /** + * PHP Iterator Interface + * + * @see Iterator::key() + * @return int + */ + public function key() { + return $this->processedResults; + } + + /** + * PHP Iterator Interface + * + * @see Iterator::next() + * @return mixed + */ + public function next() { + // if we'll be at the end. + if (($this->processedResults + 1) >= $this->limit && $this->limit > 0) { + $this->results = array(); + return false; + } + + // if we'll need new results. + if (($this->resultIndex + 1) >= $this->chunkSize) { + if (!$this->getNextResultsChunk()) { + $this->results = array(); + return false; + } + + $result = current($this->results); + } else { + // the function above resets the indexes, so only inc if not + // getting new set + $this->resultIndex++; + $result = next($this->results); + } + + $this->processedResults++; + return $result; + } + + /** + * PHP Iterator Interface + * + * @see Iterator::valid() + * @return bool + */ + public function valid() { + if (!is_array($this->results)) { + return false; + } + $key = key($this->results); + return ($key !== NULL && $key !== FALSE); + } +} |