ElggBatch   A
last analyzed

Complexity

Total Complexity 41

Size/Duplication

Total Lines 377
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
dl 0
loc 377
ccs 0
cts 136
cp 0
rs 9.1199
c 0
b 0
f 0
wmc 41
lcom 1
cbo 2

9 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 45 14
A reportIncompleteEntity() 0 3 1
C getNextResultsChunk() 0 85 12
A setIncrementOffset() 0 3 1
A rewind() 0 11 3
A current() 0 3 1
A key() 0 3 1
A next() 0 25 5
A valid() 0 7 3

How to fix   Complexity   

Complex Class

Complex classes like ElggBatch often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ElggBatch, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Efficiently run operations on batches of results for any function
4
 * that supports an options array.
5
 *
6
 * This is usually used with elgg_get_entities() and friends,
7
 * elgg_get_annotations(), and elgg_get_metadata().
8
 *
9
 * If you pass a valid PHP callback, all results will be run through that
10
 * callback. You can still foreach() through the result set after.  Valid
11
 * PHP callbacks can be a string, an array, or a closure.
12
 * {@link http://php.net/manual/en/language.pseudo-types.php}
13
 *
14
 * The callback function must accept 3 arguments: an entity, the getter
15
 * used, and the options used.
16
 *
17
 * Results from the callback are stored in callbackResult. If the callback
18
 * returns only booleans, callbackResults will be the combined result of
19
 * all calls. If no entities are processed, callbackResults will be null.
20
 *
21
 * If the callback returns anything else, callbackresult will be an indexed
22
 * array of whatever the callback returns.  If returning error handling
23
 * information, you should include enough information to determine which
24
 * result you're referring to.
25
 *
26
 * Don't combine returning bools and returning something else.
27
 *
28
 * Note that returning false will not stop the foreach.
29
 *
30
 * @warning If your callback or foreach loop deletes or disable entities
31
 * you MUST call setIncrementOffset(false) or set that when instantiating.
32
 * This forces the offset to stay what it was in the $options array.
33
 *
34
 * @example
35
 * <code>
36
 * // using foreach
37
 * $batch = new \ElggBatch('elgg_get_entities', array());
38
 * $batch->setIncrementOffset(false);
39
 *
40
 * foreach ($batch as $entity) {
41
 * 	$entity->disable();
42
 * }
43
 *
44
 * // using both a callback
45
 * $callback = function($result, $getter, $options) {
46
 * 	var_dump("Looking at annotation id: $result->id");
47
 *  return true;
48
 * }
49
 *
50
 * $batch = new \ElggBatch('elgg_get_annotations', array('guid' => 2), $callback);
51
 * </code>
52
 *
53
 * @package    Elgg.Core
54
 * @subpackage DataModel
55
 * @since      1.8
56
 */
57
class ElggBatch
58
	implements \Iterator {
59
60
	/**
61
	 * The objects to interator over.
62
	 *
63
	 * @var array
64
	 */
65
	private $results = array();
66
67
	/**
68
	 * The function used to get results.
69
	 *
70
	 * @var mixed A string, array, or closure, or lamda function
71
	 */
72
	private $getter = null;
73
74
	/**
75
	 * The number of results to grab at a time.
76
	 *
77
	 * @var int
78
	 */
79
	private $chunkSize = 25;
80
81
	/**
82
	 * A callback function to pass results through.
83
	 *
84
	 * @var mixed A string, array, or closure, or lamda function
85
	 */
86
	private $callback = null;
87
88
	/**
89
	 * Start after this many results.
90
	 *
91
	 * @var int
92
	 */
93
	private $offset = 0;
94
95
	/**
96
	 * Stop after this many results.
97
	 *
98
	 * @var int
99
	 */
100
	private $limit = 0;
101
102
	/**
103
	 * Number of processed results.
104
	 *
105
	 * @var int
106
	 */
107
	private $retrievedResults = 0;
108
109
	/**
110
	 * The index of the current result within the current chunk
111
	 *
112
	 * @var int
113
	 */
114
	private $resultIndex = 0;
115
116
	/**
117
	 * The index of the current chunk
118
	 *
119
	 * @var int
120
	 */
121
	private $chunkIndex = 0;
122
123
	/**
124
	 * The number of results iterated through
125
	 *
126
	 * @var int
127
	 */
128
	private $processedResults = 0;
129
130
	/**
131
	 * Is the getter a valid callback
132
	 *
133
	 * @var bool
134
	 */
135
	private $validGetter = null;
136
137
	/**
138
	 * The result of running all entities through the callback function.
139
	 *
140
	 * @var mixed
141
	 */
142
	public $callbackResult = null;
143
144
	/**
145
	 * If false, offset will not be incremented. This is used for callbacks/loops that delete.
146
	 *
147
	 * @var bool
148
	 */
149
	private $incrementOffset = true;
150
151
	/**
152
	 * Entities that could not be instantiated during a fetch
153
	 *
154
	 * @var \stdClass[]
155
	 */
156
	private $incompleteEntities = array();
157
158
	/**
159
	 * Total number of incomplete entities fetched
160
	 *
161
	 * @var int
162
	 */
163
	private $totalIncompletes = 0;
164
165
	/**
166
	 * Batches operations on any elgg_get_*() or compatible function that supports
167
	 * an options array.
168
	 *
169
	 * Instead of returning all objects in memory, it goes through $chunk_size
170
	 * objects, then requests more from the server.  This avoids OOM errors.
171
	 *
172
	 * @param string $getter     The function used to get objects.  Usually
173
	 *                           an elgg_get_*() function, but can be any valid PHP callback.
174
	 * @param array  $options    The options array to pass to the getter function. If limit is
175
	 *                           not set, 10 is used as the default. In most cases that is not
176
	 *                           what you want.
177
	 * @param mixed  $callback   An optional callback function that all results will be passed
178
	 *                           to upon load.  The callback needs to accept $result, $getter,
179
	 *                           $options.
180
	 * @param int    $chunk_size The number of entities to pull in before requesting more.
181
	 *                           You have to balance this between running out of memory in PHP
182
	 *                           and hitting the db server too often.
183
	 * @param bool   $inc_offset Increment the offset on each fetch. This must be false for
184
	 *                           callbacks that delete rows. You can set this after the
185
	 *                           object is created with {@link \ElggBatch::setIncrementOffset()}.
186
	 */
187
	public function __construct($getter, $options, $callback = null, $chunk_size = 25,
188
			$inc_offset = true) {
189
		
190
		$this->getter = $getter;
191
		$this->options = $options;
0 ignored issues
show
Bug introduced by
The property options does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
192
		$this->callback = $callback;
193
		$this->chunkSize = $chunk_size;
194
		$this->setIncrementOffset($inc_offset);
195
196
		if ($this->chunkSize <= 0) {
197
			$this->chunkSize = 25;
198
		}
199
200
		// store these so we can compare later
201
		$this->offset = elgg_extract('offset', $options, 0);
202
		$this->limit = elgg_extract('limit', $options, elgg_get_config('default_limit'));
203
204
		// if passed a callback, create a new \ElggBatch with the same options
205
		// and pass each to the callback.
206
		if ($callback && is_callable($callback)) {
207
			$batch = new \ElggBatch($getter, $options, null, $chunk_size, $inc_offset);
208
209
			$all_results = null;
210
211
			foreach ($batch as $result) {
212
				$result = call_user_func($callback, $result, $getter, $options);
213
214
				if (!isset($all_results)) {
215
					if ($result === true || $result === false || $result === null) {
216
						$all_results = $result;
217
					} else {
218
						$all_results = array();
219
					}
220
				}
221
222
				if (($result === true || $result === false || $result === null) && !is_array($all_results)) {
223
					$all_results = $result && $all_results;
224
				} else {
225
					$all_results[] = $result;
226
				}
227
			}
228
229
			$this->callbackResult = $all_results;
230
		}
231
	}
232
233
	/**
234
	 * Tell the process that an entity was incomplete during a fetch
235
	 *
236
	 * @param \stdClass $row
237
	 *
238
	 * @access private
239
	 */
240
	public function reportIncompleteEntity(\stdClass $row) {
241
		$this->incompleteEntities[] = $row;
242
	}
243
244
	/**
245
	 * Fetches the next chunk of results
246
	 *
247
	 * @return bool
248
	 */
249
	private function getNextResultsChunk() {
250
251
		// always reset results.
252
		$this->results = array();
253
254
		if (!isset($this->validGetter)) {
255
			$this->validGetter = is_callable($this->getter);
256
		}
257
258
		if (!$this->validGetter) {
259
			return false;
260
		}
261
262
		$limit = $this->chunkSize;
263
264
		// if someone passed limit = 0 they want everything.
265
		if ($this->limit != 0) {
266
			if ($this->retrievedResults >= $this->limit) {
267
				return false;
268
			}
269
270
			// if original limit < chunk size, set limit to original limit
271
			// else if the number of results we'll fetch if greater than the original limit
272
			if ($this->limit < $this->chunkSize) {
273
				$limit = $this->limit;
274
			} elseif ($this->retrievedResults + $this->chunkSize > $this->limit) {
275
				// set the limit to the number of results remaining in the original limit
276
				$limit = $this->limit - $this->retrievedResults;
277
			}
278
		}
279
280
		if ($this->incrementOffset) {
281
			$offset = $this->offset + $this->retrievedResults;
282
		} else {
283
			$offset = $this->offset + $this->totalIncompletes;
284
		}
285
286
		$current_options = array(
287
			'limit' => $limit,
288
			'offset' => $offset,
289
			'__ElggBatch' => $this,
290
		);
291
292
		$options = array_merge($this->options, $current_options);
293
294
		$this->incompleteEntities = array();
295
		$this->results = call_user_func($this->getter, $options);
0 ignored issues
show
Documentation Bug introduced by
It seems like call_user_func($this->getter, $options) of type * is incompatible with the declared type array of property $results.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
296
297
		// batch result sets tend to be large; we don't want to cache these.
298
		_elgg_services()->db->disableQueryCache();
299
300
		$num_results = count($this->results);
301
		$num_incomplete = count($this->incompleteEntities);
302
303
		$this->totalIncompletes += $num_incomplete;
304
305
		if ($this->incompleteEntities) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->incompleteEntities of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
306
			// pad the front of the results with nulls representing the incompletes
307
			array_splice($this->results, 0, 0, array_pad(array(), $num_incomplete, null));
308
			// ...and skip past them
309
			reset($this->results);
310
			for ($i = 0; $i < $num_incomplete; $i++) {
311
				next($this->results);
312
			}
313
		}
314
315
		if ($this->results) {
316
			$this->chunkIndex++;
317
318
			// let the system know we've jumped past the nulls
319
			$this->resultIndex = $num_incomplete;
320
321
			$this->retrievedResults += ($num_results + $num_incomplete);
322
			if ($num_results == 0) {
323
				// This fetch was *all* incompletes! We need to fetch until we can either
324
				// offer at least one row to iterate over, or give up.
325
				return $this->getNextResultsChunk();
326
			}
327
			_elgg_services()->db->enableQueryCache();
328
			return true;
329
		} else {
330
			_elgg_services()->db->enableQueryCache();
331
			return false;
332
		}
333
	}
334
335
	/**
336
	 * Increment the offset from the original options array? Setting to
337
	 * false is required for callbacks that delete rows.
338
	 *
339
	 * @param bool $increment Set to false when deleting data
340
	 * @return void
341
	 */
342
	public function setIncrementOffset($increment = true) {
343
		$this->incrementOffset = (bool) $increment;
344
	}
345
346
	/**
347
	 * Implements Iterator
348
	 */
349
350
	/**
351
	 * PHP Iterator Interface
352
	 *
353
	 * @see Iterator::rewind()
354
	 * @return void
355
	 */
356
	public function rewind() {
357
		$this->resultIndex = 0;
358
		$this->retrievedResults = 0;
359
		$this->processedResults = 0;
360
361
		// only grab results if we haven't yet or we're crossing chunks
362
		if ($this->chunkIndex == 0 || $this->limit > $this->chunkSize) {
363
			$this->chunkIndex = 0;
364
			$this->getNextResultsChunk();
365
		}
366
	}
367
368
	/**
369
	 * PHP Iterator Interface
370
	 *
371
	 * @see Iterator::current()
372
	 * @return mixed
373
	 */
374
	public function current() {
375
		return current($this->results);
376
	}
377
378
	/**
379
	 * PHP Iterator Interface
380
	 *
381
	 * @see Iterator::key()
382
	 * @return int
383
	 */
384
	public function key() {
385
		return $this->processedResults;
386
	}
387
388
	/**
389
	 * PHP Iterator Interface
390
	 *
391
	 * @see Iterator::next()
392
	 * @return mixed
393
	 */
394
	public function next() {
395
		// if we'll be at the end.
396
		if (($this->processedResults + 1) >= $this->limit && $this->limit > 0) {
397
			$this->results = array();
398
			return false;
399
		}
400
401
		// if we'll need new results.
402
		if (($this->resultIndex + 1) >= $this->chunkSize) {
403
			if (!$this->getNextResultsChunk()) {
404
				$this->results = array();
405
				return false;
406
			}
407
408
			$result = current($this->results);
409
		} else {
410
			// the function above resets the indexes, so only inc if not
411
			// getting new set
412
			$this->resultIndex++;
413
			$result = next($this->results);
414
		}
415
416
		$this->processedResults++;
417
		return $result;
418
	}
419
420
	/**
421
	 * PHP Iterator Interface
422
	 *
423
	 * @see Iterator::valid()
424
	 * @return bool
425
	 */
426
	public function valid() {
427
		if (!is_array($this->results)) {
428
			return false;
429
		}
430
		$key = key($this->results);
431
		return ($key !== null && $key !== false);
432
	}
433
}
434