123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434 |
- <?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
- * @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 {@link \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, elgg_get_config('default_limit'));
- // 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) {
- $result = call_user_func($callback, $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($this->getter, $options);
- // batch result sets tend to be large; we don't want to cache these.
- _elgg_services()->db->disableQueryCache();
- $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();
- }
- _elgg_services()->db->enableQueryCache();
- return true;
- } else {
- _elgg_services()->db->enableQueryCache();
- 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);
- }
- }
|