Test Failed
Pull Request — master (#456)
by Kiran
16:45
created

WP_Background_Process::handle()   D

Complexity

Conditions 10
Paths 20

Size

Total Lines 40
Code Lines 23

Duplication

Lines 19
Ratio 47.5 %

Importance

Changes 0
Metric Value
cc 10
eloc 23
nc 20
nop 0
dl 19
loc 40
rs 4.8196
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php // @codingStandardsIgnoreLine.
2
/**
3
 * Abstract WP_Background_Process class.
4
 *
5
 * @package WP-Background-Processing
6
 * @extends WP_Async_Request
7
 */
8
9
defined( 'ABSPATH' ) || exit;
10
11
/**
12
 * Abstract WP_Background_Process class.
13
 */
14
abstract class WP_Background_Process extends WP_Async_Request {
15
16
	/**
17
	 * Action
18
	 *
19
	 * (default value: 'background_process')
20
	 *
21
	 * @var string
22
	 * @access protected
23
	 */
24
	protected $action = 'background_process';
25
26
	/**
27
	 * Start time of current process.
28
	 *
29
	 * (default value: 0)
30
	 *
31
	 * @var int
32
	 * @access protected
33
	 */
34
	protected $start_time = 0;
35
36
	/**
37
	 * Cron_hook_identifier
38
	 *
39
	 * @var mixed
40
	 * @access protected
41
	 */
42
	protected $cron_hook_identifier;
43
44
	/**
45
	 * Cron_interval_identifier
46
	 *
47
	 * @var mixed
48
	 * @access protected
49
	 */
50
	protected $cron_interval_identifier;
51
52
	/**
53
	 * Initiate new background process
54
	 */
55
	public function __construct() {
56
		parent::__construct();
57
58
		$this->cron_hook_identifier     = $this->identifier . '_cron';
59
		$this->cron_interval_identifier = $this->identifier . '_cron_interval';
60
61
		add_action( $this->cron_hook_identifier, array( $this, 'handle_cron_healthcheck' ) );
62
		add_filter( 'cron_schedules', array( $this, 'schedule_cron_healthcheck' ) );
63
	}
64
65
	/**
66
	 * Dispatch
67
	 *
68
	 * @access public
69
	 * @return void
70
	 */
71
	public function dispatch() {
72
		// Schedule the cron healthcheck.
73
		$this->schedule_event();
74
75
		// Perform remote post.
76
		return parent::dispatch();
77
	}
78
79
	/**
80
	 * Push to queue
81
	 *
82
	 * @param mixed $data Data.
83
	 *
84
	 * @return $this
85
	 */
86
	public function push_to_queue( $data ) {
87
		$this->data[] = $data;
88
89
		return $this;
90
	}
91
92
	/**
93
	 * Save queue
94
	 *
95
	 * @return $this
96
	 */
97
	public function save() {
98
		$key = $this->generate_key();
99
100
		if ( ! empty( $this->data ) ) {
101
			update_site_option( $key, $this->data );
102
		}
103
104
		return $this;
105
	}
106
107
	/**
108
	 * Update queue
109
	 *
110
	 * @param string $key Key.
111
	 * @param array  $data Data.
112
	 *
113
	 * @return $this
114
	 */
115
	public function update( $key, $data ) {
116
		if ( ! empty( $data ) ) {
117
			update_site_option( $key, $data );
118
		}
119
120
		return $this;
121
	}
122
123
	/**
124
	 * Delete queue
125
	 *
126
	 * @param string $key Key.
127
	 *
128
	 * @return $this
129
	 */
130
	public function delete( $key ) {
131
		delete_site_option( $key );
132
133
		return $this;
134
	}
135
136
	/**
137
	 * Generate key
138
	 *
139
	 * Generates a unique key based on microtime. Queue items are
140
	 * given a unique key so that they can be merged upon save.
141
	 *
142
	 * @param int $length Length.
143
	 *
144
	 * @return string
145
	 */
146
	protected function generate_key( $length = 64 ) {
147
		$unique  = md5( microtime() . rand() );
148
		$prepend = $this->identifier . '_batch_';
149
150
		return substr( $prepend . $unique, 0, $length );
151
	}
152
153
	/**
154
	 * Maybe process queue
155
	 *
156
	 * Checks whether data exists within the queue and that
157
	 * the process is not already running.
158
	 */
159
	public function maybe_handle() {
160
		// Don't lock up other requests while processing
161
		session_write_close();
162
163
		if ( $this->is_process_running() ) {
164
			// Background process already running.
165
			wp_die();
166
		}
167
168
		if ( $this->is_queue_empty() ) {
169
			// No data to process.
170
			wp_die();
171
		}
172
173
		check_ajax_referer( $this->identifier, 'nonce' );
174
175
		$this->handle();
176
177
		wp_die();
178
	}
179
180
	/**
181
	 * Is queue empty
182
	 *
183
	 * @return bool
184
	 */
185 View Code Duplication
	protected function is_queue_empty() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
186
		global $wpdb;
187
188
		$table  = $wpdb->options;
189
		$column = 'option_name';
190
191
		if ( is_multisite() ) {
192
			$table  = $wpdb->sitemeta;
193
			$column = 'meta_key';
194
		}
195
196
		$key = $this->identifier . '_batch_%';
197
198
		$count = $wpdb->get_var( $wpdb->prepare( "
199
			SELECT COUNT(*)
200
			FROM {$table}
201
			WHERE {$column} LIKE %s
202
		", $key ) );
203
204
		return ! ( $count > 0 );
205
	}
206
207
	/**
208
	 * Is process running
209
	 *
210
	 * Check whether the current process is already running
211
	 * in a background process.
212
	 */
213
	protected function is_process_running() {
214
		if ( get_site_transient( $this->identifier . '_process_lock' ) ) {
215
			// Process already running.
216
			return true;
217
		}
218
219
		return false;
220
	}
221
222
	/**
223
	 * Lock process
224
	 *
225
	 * Lock the process so that multiple instances can't run simultaneously.
226
	 * Override if applicable, but the duration should be greater than that
227
	 * defined in the time_exceeded() method.
228
	 */
229
	protected function lock_process() {
230
		$this->start_time = time(); // Set start time of current process.
231
232
		$lock_duration = ( property_exists( $this, 'queue_lock_time' ) ) ? $this->queue_lock_time : 60; // 1 minute
0 ignored issues
show
Bug introduced by
The property queue_lock_time 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...
233
		$lock_duration = apply_filters( $this->identifier . '_queue_lock_time', $lock_duration );
234
235
		set_site_transient( $this->identifier . '_process_lock', microtime(), $lock_duration );
236
	}
237
238
	/**
239
	 * Unlock process
240
	 *
241
	 * Unlock the process so that other instances can spawn.
242
	 *
243
	 * @return $this
244
	 */
245
	protected function unlock_process() {
246
		delete_site_transient( $this->identifier . '_process_lock' );
247
248
		return $this;
249
	}
250
251
	/**
252
	 * Get batch
253
	 *
254
	 * @return stdClass Return the first batch from the queue
255
	 */
256 View Code Duplication
	protected function get_batch() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
257
		global $wpdb;
258
259
		$table        = $wpdb->options;
260
		$column       = 'option_name';
261
		$key_column   = 'option_id';
262
		$value_column = 'option_value';
263
264
		if ( is_multisite() ) {
265
			$table        = $wpdb->sitemeta;
266
			$column       = 'meta_key';
267
			$key_column   = 'meta_id';
268
			$value_column = 'meta_value';
269
		}
270
271
		$key = $this->identifier . '_batch_%';
272
273
		$query = $wpdb->get_row( $wpdb->prepare( "
274
			SELECT *
275
			FROM {$table}
276
			WHERE {$column} LIKE %s
277
			ORDER BY {$key_column} ASC
278
			LIMIT 1
279
		", $key ) );
280
281
		$batch       = new stdClass();
282
		$batch->key  = $query->$column;
283
		$batch->data = maybe_unserialize( $query->$value_column );
284
285
		return $batch;
286
	}
287
288
	/**
289
	 * Handle
290
	 *
291
	 * Pass each queue item to the task handler, while remaining
292
	 * within server memory and time limit constraints.
293
	 */
294
	protected function handle() {
295
		$this->lock_process();
296
297
		do {
298
			$batch = $this->get_batch();
299
300 View Code Duplication
			foreach ( $batch->data as $key => $value ) {
301
				$task = $this->task( $value );
302
303
				if ( false !== $task ) {
304
					$batch->data[ $key ] = $task;
305
				} else {
306
					unset( $batch->data[ $key ] );
307
				}
308
309
				if ( $this->time_exceeded() || $this->memory_exceeded() ) {
310
					// Batch limits reached.
311
					break;
312
				}
313
			}
314
315
			// Update or delete current batch.
316 View Code Duplication
			if ( ! empty( $batch->data ) ) {
317
				$this->update( $batch->key, $batch->data );
318
			} else {
319
				$this->delete( $batch->key );
320
			}
321
		} while ( ! $this->time_exceeded() && ! $this->memory_exceeded() && ! $this->is_queue_empty() );
322
323
		$this->unlock_process();
324
325
		// Start next batch or complete process.
326
		if ( ! $this->is_queue_empty() ) {
327
			$this->dispatch();
328
		} else {
329
			$this->complete();
330
		}
331
332
		wp_die();
333
	}
334
335
	/**
336
	 * Memory exceeded
337
	 *
338
	 * Ensures the batch process never exceeds 90%
339
	 * of the maximum WordPress memory.
340
	 *
341
	 * @return bool
342
	 */
343
	protected function memory_exceeded() {
344
		$memory_limit   = $this->get_memory_limit() * 0.9; // 90% of max memory
345
		$current_memory = memory_get_usage( true );
346
		$return         = false;
347
348
		if ( $current_memory >= $memory_limit ) {
349
			$return = true;
350
		}
351
352
		return apply_filters( $this->identifier . '_memory_exceeded', $return );
353
	}
354
355
	/**
356
	 * Get memory limit
357
	 *
358
	 * @return int
359
	 */
360 View Code Duplication
	protected function get_memory_limit() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
361
		if ( function_exists( 'ini_get' ) ) {
362
			$memory_limit = ini_get( 'memory_limit' );
363
		} else {
364
			// Sensible default.
365
			$memory_limit = '128M';
366
		}
367
368
		if ( ! $memory_limit || -1 === $memory_limit ) {
0 ignored issues
show
Unused Code Bug introduced by
The strict comparison === seems to always evaluate to false as the types of -1 (integer) and $memory_limit (string) can never be identical. Maybe you want to use a loose comparison == instead?
Loading history...
369
			// Unlimited, set to 32GB.
370
			$memory_limit = '32000M';
371
		}
372
373
		return intval( $memory_limit ) * 1024 * 1024;
374
	}
375
376
	/**
377
	 * Time exceeded.
378
	 *
379
	 * Ensures the batch never exceeds a sensible time limit.
380
	 * A timeout limit of 30s is common on shared hosting.
381
	 *
382
	 * @return bool
383
	 */
384
	protected function time_exceeded() {
385
		$finish = $this->start_time + apply_filters( $this->identifier . '_default_time_limit', 20 ); // 20 seconds
386
		$return = false;
387
388
		if ( time() >= $finish ) {
389
			$return = true;
390
		}
391
392
		return apply_filters( $this->identifier . '_time_exceeded', $return );
393
	}
394
395
	/**
396
	 * Complete.
397
	 *
398
	 * Override if applicable, but ensure that the below actions are
399
	 * performed, or, call parent::complete().
400
	 */
401
	protected function complete() {
402
		// Unschedule the cron healthcheck.
403
		$this->clear_scheduled_event();
404
	}
405
406
	/**
407
	 * Schedule cron healthcheck
408
	 *
409
	 * @access public
410
	 * @param mixed $schedules Schedules.
411
	 * @return mixed
412
	 */
413 View Code Duplication
	public function schedule_cron_healthcheck( $schedules ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
414
		$interval = apply_filters( $this->identifier . '_cron_interval', 5 );
415
416
		if ( property_exists( $this, 'cron_interval' ) ) {
417
			$interval = apply_filters( $this->identifier . '_cron_interval', $this->cron_interval_identifier );
418
		}
419
420
		// Adds every 5 minutes to the existing schedules.
421
		$schedules[ $this->identifier . '_cron_interval' ] = array(
422
			'interval' => MINUTE_IN_SECONDS * $interval,
423
			'display'  => sprintf( __( 'Every %d Minutes' ), $interval ),
424
		);
425
426
		return $schedules;
427
	}
428
429
	/**
430
	 * Handle cron healthcheck
431
	 *
432
	 * Restart the background process if not already running
433
	 * and data exists in the queue.
434
	 */
435
	public function handle_cron_healthcheck() {
436
		if ( $this->is_process_running() ) {
437
			// Background process already running.
438
			exit;
439
		}
440
441
		if ( $this->is_queue_empty() ) {
442
			// No data to process.
443
			$this->clear_scheduled_event();
444
			exit;
445
		}
446
447
		$this->handle();
448
449
		exit;
450
	}
451
452
	/**
453
	 * Schedule event
454
	 */
455
	protected function schedule_event() {
456
		if ( ! wp_next_scheduled( $this->cron_hook_identifier ) ) {
457
			wp_schedule_event( time(), $this->cron_interval_identifier, $this->cron_hook_identifier );
458
		}
459
	}
460
461
	/**
462
	 * Clear scheduled event
463
	 */
464
	protected function clear_scheduled_event() {
465
		$timestamp = wp_next_scheduled( $this->cron_hook_identifier );
466
467
		if ( $timestamp ) {
468
			wp_unschedule_event( $timestamp, $this->cron_hook_identifier );
469
		}
470
	}
471
472
	/**
473
	 * Cancel Process
474
	 *
475
	 * Stop processing queue items, clear cronjob and delete batch.
476
	 *
477
	 */
478
	public function cancel_process() {
479
		if ( ! $this->is_queue_empty() ) {
480
			$batch = $this->get_batch();
481
482
			$this->delete( $batch->key );
483
484
			wp_clear_scheduled_hook( $this->cron_hook_identifier );
485
		}
486
487
	}
488
489
	/**
490
	 * Task
491
	 *
492
	 * Override this method to perform any actions required on each
493
	 * queue item. Return the modified item for further processing
494
	 * in the next pass through. Or, return false to remove the
495
	 * item from the queue.
496
	 *
497
	 * @param mixed $item Queue item to iterate over.
498
	 *
499
	 * @return mixed
500
	 */
501
	abstract protected function task( $item );
502
503
}
504