| 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);	}}
 |