engine/classes/ElggBatch.php
Rama 7.x · réplica de lectura del repositorio original.
<?php
use Elgg\Exceptions\RuntimeException as ElggRuntimeException;
/**
* A lazy-loading proxy for a result array from a fetching function
*
* A 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.
* {@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);
*
* // get a batch from an Elgg getter function
* $batch = elgg_get_entities([
* 'batch' => true,
* ]);
* </code>
*
* @since 1.8
*/
class ElggBatch implements \Countable, \Iterator {
/**
* The objects to iterate over.
*
* @var array
*/
private $results = [];
/**
* The function used to get results.
*
* @var callable
*/
private $getter = null;
/**
* The given $options to alter and pass to the getter.
*
* @var array
*/
private $options = [];
/**
* The number of results to grab at a time.
*
* @var int
*/
private $chunkSize = 25;
/**
* A callback function to pass results through.
*
* @var callable
*/
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;
/**
* Number of reported failures during the batch run
*
* @var int
*/
private $reportedFailures = 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 callable $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(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;
}
}
/**
* Fetches the next chunk of results
*
* @return bool
*/
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,
];
$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;
}
/**
* 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(bool $increment = true): void {
$this->incrementOffset = $increment;
}
/**
* Set chunk size
* @param int $size Size
* @return void
*/
public function setChunkSize(int $size = 25): void {
$this->chunkSize = $size;
}
/**
* Report a number of failures during the batch execution,
* this will increase the internal offset by $num in case offsett increment is disabled
*
* @param int $num number of failures to report (default: 1)
*
* @return void
*/
public function reportFailure(int $num = 1): void {
$this->reportedFailures += $num;
}
/**
* Implements Iterator
*/
/**
* {@inheritdoc}
*/
#[\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();
}
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function current() {
return current($this->results);
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function key() {
return $this->processedResults;
}
/**
* {@inheritdoc}
*/
#[\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;
}
/**
* {@inheritdoc}
*/
#[\ReturnTypeWillChange]
public function valid() {
if (!is_array($this->results)) {
return false;
}
$key = key($this->results);
return ($key !== null && $key !== false);
}
/**
* Count the total results available at this moment.
*
* As this performs a separate query, the count returned may not match the number of results you can
* fetch via iteration on a very active DB.
*
* @see Countable::count()
* @return int
* @throws \Elgg\Exceptions\RuntimeException
*/
#[\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);
}
}