Elgg  Version master
/root/Elgg/engine/classes/ElggBatch.php

A lazy-loading proxy for a result array from a fetching functionA batch can be counted or iterated over via foreach, where the batch will internally fetch results several rows at a time. This allows you to efficiently work on large result sets without loading all results in memory.

A batch can run operations for any function that supports an options array and supports the keys "offset", "limit", and "count". This is usually used with elgg_get_entities() and friends, elgg_get_annotations(), and elgg_get_metadata(). In fact, those functions will return results as batches by passing in "batch" as true.

Unlike a real array, direct access of results is not supported.

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. 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.

// using foreach $batch = new ('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 ('elgg_get_annotations', array('guid' => 2), $callback);

// get a batch from an Elgg getter function $batch = elgg_get_entities([ 'batch' => true, ]);

Since
1.8
<?php
class ElggBatch implements \Countable, \Iterator {
private $results = [];
private $getter = null;
private $options = [];
private $chunkSize = 25;
private $callback = null;
private $offset = 0;
private $limit = 0;
private $retrievedResults = 0;
private $resultIndex = 0;
private $chunkIndex = 0;
private $processedResults = 0;
private $validGetter = null;
public $callbackResult = null;
private $incrementOffset = true;
private $reportedFailures = 0;
public function __construct(callable $getter, array $options, $callback = null, int $chunk_size = 25, bool $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_services()->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 = [];
}
}
if (($result === true || $result === false || $result === null) && !is_array($all_results)) {
$all_results = $result && $all_results;
} else {
$all_results[] = $result;
}
}
$this->callbackResult = $all_results;
}
}
private function getNextResultsChunk(): bool {
// always reset results.
$this->results = [];
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->reportedFailures;
}
$current_options = [
'limit' => $limit,
'offset' => $offset,
'__ElggBatch' => $this,
];
$options = array_merge($this->options, $current_options);
// batch result sets tend to be large; we don't want to cache these.
_elgg_services()->queryCache->disable();
$this->results = call_user_func($this->getter, $options);
$num_results = count($this->results);
if ($this->results) {
$this->chunkIndex++;
$this->resultIndex = 0;
$this->retrievedResults += $num_results;
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()->queryCache->enable();
return true;
}
// no result
_elgg_services()->queryCache->enable();
return false;
}
public function setIncrementOffset(bool $increment = true): void {
$this->incrementOffset = $increment;
}
public function setChunkSize(int $size = 25): void {
$this->chunkSize = $size;
}
public function reportFailure(int $num = 1): void {
$this->reportedFailures += $num;
}
#[\ReturnTypeWillChange]
public function rewind() {
$this->resultIndex = 0;
$this->retrievedResults = 0;
$this->processedResults = 0;
$this->reportedFailures = 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();
}
}
#[\ReturnTypeWillChange]
public function current() {
return current($this->results);
}
#[\ReturnTypeWillChange]
public function key() {
return $this->processedResults;
}
#[\ReturnTypeWillChange]
public function next() {
// if we'll be at the end.
if (($this->processedResults + 1) >= $this->limit && $this->limit > 0) {
$this->results = [];
return;
}
// if we'll need new results.
if (($this->resultIndex + 1) >= $this->chunkSize) {
if (!$this->getNextResultsChunk()) {
$this->results = [];
return;
}
$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;
}
#[\ReturnTypeWillChange]
public function valid() {
if (!is_array($this->results)) {
return false;
}
$key = key($this->results);
return ($key !== null && $key !== false);
}
#[\ReturnTypeWillChange]
public function count() {
if (!is_callable($this->getter)) {
$inspector = new \Elgg\Debug\Inspector();
throw new ElggRuntimeException('Getter is not callable: ' . $inspector->describeCallable($this->getter));
}
$options = array_merge($this->options, ['count' => true]);
return call_user_func($this->getter, $options);
}
}