WP_Hook   C
last analyzed

Complexity

Total Complexity 65

Size/Duplication

Total Lines 518
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 518
rs 5.7894
c 0
b 0
f 0
wmc 65
lcom 1
cbo 0

20 Methods

Rating   Name   Duplication   Size   Complexity  
A add_filter() 0 18 4
C resort_active_iterations() 0 57 13
A remove_filter() 0 15 4
B has_filter() 0 18 5
A has_filters() 0 8 3
B remove_all_filters() 0 15 5
C apply_filters() 0 36 7
A do_action() 0 9 2
A do_all_hook() 0 14 3
A current_priority() 0 7 2
B build_preinitialized_hooks() 0 23 6
A offsetExists() 0 3 1
A offsetGet() 0 3 2
A offsetSet() 0 7 2
A offsetUnset() 0 3 1
A current() 0 3 1
A next() 0 3 1
A key() 0 3 1
A valid() 0 3 1
A rewind() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like WP_Hook 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 WP_Hook, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Plugin API: WP_Hook class
4
 *
5
 * @package WordPress
6
 * @subpackage Plugin
7
 * @since 4.7.0
8
 */
9
10
/**
11
 * Core class used to implement action and filter hook functionality.
12
 *
13
 * @since 4.7.0
14
 *
15
 * @see Iterator
16
 * @see ArrayAccess
17
 */
18
final class WP_Hook implements Iterator, ArrayAccess {
19
20
	/**
21
	 * Hook callbacks.
22
	 *
23
	 * @since 4.7.0
24
	 * @access public
25
	 * @var array
26
	 */
27
	public $callbacks = array();
28
29
	/**
30
	 * The priority keys of actively running iterations of a hook.
31
	 *
32
	 * @since 4.7.0
33
	 * @access private
34
	 * @var array
35
	 */
36
	private $iterations = array();
37
38
	/**
39
	 * The current priority of actively running iterations of a hook.
40
	 *
41
	 * @since 4.7.0
42
	 * @access private
43
	 * @var array
44
	 */
45
	private $current_priority = array();
46
47
	/**
48
	 * Number of levels this hook can be recursively called.
49
	 *
50
	 * @since 4.7.0
51
	 * @access private
52
	 * @var int
53
	 */
54
	private $nesting_level = 0;
55
56
	/**
57
	 * Flag for if we're current doing an action, rather than a filter.
58
	 *
59
	 * @since 4.7.0
60
	 * @access private
61
	 * @var bool
62
	 */
63
	private $doing_action = false;
64
65
	/**
66
	 * Hooks a function or method to a specific filter action.
67
	 *
68
	 * @since 4.7.0
69
	 * @access public
70
	 *
71
	 * @param string   $tag             The name of the filter to hook the $function_to_add callback to.
72
	 * @param callable $function_to_add The callback to be run when the filter is applied.
73
	 * @param int      $priority        The order in which the functions associated with a
74
	 *                                  particular action are executed. Lower numbers correspond with
75
	 *                                  earlier execution, and functions with the same priority are executed
76
	 *                                  in the order in which they were added to the action.
77
	 * @param int      $accepted_args   The number of arguments the function accepts.
78
	 */
79
	public function add_filter( $tag, $function_to_add, $priority, $accepted_args ) {
80
		$idx = _wp_filter_build_unique_id( $tag, $function_to_add, $priority );
81
		$priority_existed = isset( $this->callbacks[ $priority ] );
82
83
		$this->callbacks[ $priority ][ $idx ] = array(
84
			'function' => $function_to_add,
85
			'accepted_args' => $accepted_args
86
		);
87
88
		// if we're adding a new priority to the list, put them back in sorted order
89
		if ( ! $priority_existed && count( $this->callbacks ) > 1 ) {
90
			ksort( $this->callbacks, SORT_NUMERIC );
91
		}
92
93
		if ( $this->nesting_level > 0 ) {
94
			$this->resort_active_iterations( $priority, $priority_existed );
95
		}
96
	}
97
98
	/**
99
	 * Handles reseting callback priority keys mid-iteration.
100
	 *
101
	 * @since 4.7.0
102
	 * @access private
103
	 *
104
	 * @param bool|int $new_priority     Optional. The priority of the new filter being added. Default false,
105
	 *                                   for no priority being added.
106
	 * @param bool     $priority_existed Optional. Flag for whether the priority already existed before the new
107
	 *                                   filter was added. Default false.
108
	 */
109
	private function resort_active_iterations( $new_priority = false, $priority_existed = false ) {
110
		$new_priorities = array_keys( $this->callbacks );
111
112
		// If there are no remaining hooks, clear out all running iterations.
113
		if ( ! $new_priorities ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $new_priorities of type array<integer|string> 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...
114
			foreach ( $this->iterations as $index => $iteration ) {
115
				$this->iterations[ $index ] = $new_priorities;
116
			}
117
			return;
118
		}
119
120
		$min = min( $new_priorities );
121
		foreach ( $this->iterations as $index => &$iteration ) {
122
			$current = current( $iteration );
123
			// If we're already at the end of this iteration, just leave the array pointer where it is.
124
			if ( false === $current ) {
125
				continue;
126
			}
127
128
			$iteration = $new_priorities;
129
130
			if ( $current < $min ) {
131
				array_unshift( $iteration, $current );
132
				continue;
133
			}
134
135
			while ( current( $iteration ) < $current ) {
136
				if ( false === next( $iteration ) ) {
137
					break;
138
				}
139
			}
140
141
			// If we have a new priority that didn't exist, but ::apply_filters() or ::do_action() thinks it's the current priority...
142
			if ( $new_priority === $this->current_priority[ $index ] && ! $priority_existed ) {
143
				/*
144
				 * ... and the new priority is the same as what $this->iterations thinks is the previous
145
				 * priority, we need to move back to it.
146
				 */
147
148
				if ( false === current( $iteration ) ) {
149
					// If we've already moved off the end of the array, go back to the last element.
150
					$prev = end( $iteration );
151
				} else {
152
					// Otherwise, just go back to the previous element.
153
					$prev = prev( $iteration );
154
				}
155
				if ( false === $prev ) {
156
					// Start of the array. Reset, and go about our day.
157
					reset( $iteration );
158
				} elseif ( $new_priority !== $prev ) {
159
					// Previous wasn't the same. Move forward again.
160
					next( $iteration );
161
				}
162
			}
163
		}
164
		unset( $iteration );
165
	}
166
167
	/**
168
	 * Unhooks a function or method from a specific filter action.
169
	 *
170
	 * @since 4.7.0
171
	 * @access public
172
	 *
173
	 * @param string   $tag                The filter hook to which the function to be removed is hooked. Used
174
	 *                                     for building the callback ID when SPL is not available.
175
	 * @param callable $function_to_remove The callback to be removed from running when the filter is applied.
176
	 * @param int      $priority           The exact priority used when adding the original filter callback.
177
	 * @return bool Whether the callback existed before it was removed.
178
	 */
179
	public function remove_filter( $tag, $function_to_remove, $priority ) {
180
		$function_key = _wp_filter_build_unique_id( $tag, $function_to_remove, $priority );
181
182
		$exists = isset( $this->callbacks[ $priority ][ $function_key ] );
183
		if ( $exists ) {
184
			unset( $this->callbacks[ $priority ][ $function_key ] );
185
			if ( ! $this->callbacks[ $priority ] ) {
186
				unset( $this->callbacks[ $priority ] );
187
				if ( $this->nesting_level > 0 ) {
188
					$this->resort_active_iterations();
189
				}
190
			}
191
		}
192
		return $exists;
193
	}
194
195
	/**
196
	 * Checks if a specific action has been registered for this hook.
197
	 *
198
	 * @since 4.7.0
199
	 * @access public
200
	 *
201
	 * @param callable|bool $function_to_check Optional. The callback to check for. Default false.
202
	 * @param string        $tag               Optional. The name of the filter hook. Used for building
203
	 *                                         the callback ID when SPL is not available. Default empty.
204
	 * @return bool|int The priority of that hook is returned, or false if the function is not attached.
0 ignored issues
show
Documentation introduced by
Should the return type not be boolean|integer|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
205
	 */
206
	public function has_filter( $tag = '', $function_to_check = false ) {
207
		if ( false === $function_to_check ) {
208
			return $this->has_filters();
209
		}
210
211
		$function_key = _wp_filter_build_unique_id( $tag, $function_to_check, false );
0 ignored issues
show
Bug introduced by
It seems like $function_to_check defined by parameter $function_to_check on line 206 can also be of type boolean; however, _wp_filter_build_unique_id() does only seem to accept callable, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
212
		if ( ! $function_key ) {
213
			return false;
214
		}
215
216
		foreach ( $this->callbacks as $priority => $callbacks ) {
217
			if ( isset( $callbacks[ $function_key ] ) ) {
218
				return $priority;
219
			}
220
		}
221
222
		return false;
223
	}
224
225
	/**
226
	 * Checks if any callbacks have been registered for this hook.
227
	 *
228
	 * @since 4.7.0
229
	 * @access public
230
	 *
231
	 * @return bool True if callbacks have been registered for the current hook, otherwise false.
232
	 */
233
	public function has_filters() {
234
		foreach ( $this->callbacks as $callbacks ) {
235
			if ( $callbacks ) {
236
				return true;
237
			}
238
		}
239
		return false;
240
	}
241
242
	/**
243
	 * Removes all callbacks from the current filter.
244
	 *
245
	 * @since 4.7.0
246
	 * @access public
247
	 *
248
	 * @param int|bool $priority Optional. The priority number to remove. Default false.
249
	 */
250
	public function remove_all_filters( $priority = false ) {
251
		if ( ! $this->callbacks ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->callbacks 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...
252
			return;
253
		}
254
255
		if ( false === $priority ) {
256
			$this->callbacks = array();
257
		} else if ( isset( $this->callbacks[ $priority ] ) ) {
258
			unset( $this->callbacks[ $priority ] );
259
		}
260
261
		if ( $this->nesting_level > 0 ) {
262
			$this->resort_active_iterations();
263
		}
264
	}
265
266
	/**
267
	 * Calls the callback functions added to a filter hook.
268
	 *
269
	 * @since 4.7.0
270
	 * @access public
271
	 *
272
	 * @param mixed $value The value to filter.
273
	 * @param array $args  Arguments to pass to callbacks.
274
	 * @return mixed The filtered value after all hooked functions are applied to it.
275
	 */
276
	public function apply_filters( $value, $args ) {
277
		if ( ! $this->callbacks ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->callbacks 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...
278
			return $value;
279
		}
280
281
		$nesting_level = $this->nesting_level++;
282
283
		$this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
284
		$num_args = count( $args );
285
286
		do {
287
			$this->current_priority[ $nesting_level ] = $priority = current( $this->iterations[ $nesting_level ] );
288
289
			foreach ( $this->callbacks[ $priority ] as $the_ ) {
290
				if( ! $this->doing_action ) {
291
					$args[ 0 ] = $value;
292
				}
293
294
				// Avoid the array_slice if possible.
295
				if ( $the_['accepted_args'] == 0 ) {
296
					$value = call_user_func_array( $the_['function'], array() );
297
				} elseif ( $the_['accepted_args'] >= $num_args ) {
298
					$value = call_user_func_array( $the_['function'], $args );
299
				} else {
300
					$value = call_user_func_array( $the_['function'], array_slice( $args, 0, (int)$the_['accepted_args'] ) );
301
				}
302
			}
303
		} while ( false !== next( $this->iterations[ $nesting_level ] ) );
304
305
		unset( $this->iterations[ $nesting_level ] );
306
		unset( $this->current_priority[ $nesting_level ] );
307
308
		$this->nesting_level--;
309
310
		return $value;
311
	}
312
313
	/**
314
	 * Executes the callback functions hooked on a specific action hook.
315
	 *
316
	 * @since 4.7.0
317
	 * @access public
318
	 *
319
	 * @param mixed $args Arguments to pass to the hook callbacks.
320
	 */
321
	public function do_action( $args ) {
322
		$this->doing_action = true;
323
		$this->apply_filters( '', $args );
324
325
		// If there are recursive calls to the current action, we haven't finished it until we get to the last one.
326
		if ( ! $this->nesting_level ) {
327
			$this->doing_action = false;
328
		}
329
	}
330
331
	/**
332
	 * Processes the functions hooked into the 'all' hook.
333
	 *
334
	 * @since 4.7.0
335
	 * @access public
336
	 *
337
	 * @param array $args Arguments to pass to the hook callbacks. Passed by reference.
338
	 */
339
	public function do_all_hook( &$args ) {
340
		$nesting_level = $this->nesting_level++;
341
		$this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
342
343
		do {
344
			$priority = current( $this->iterations[ $nesting_level ] );
345
			foreach ( $this->callbacks[ $priority ] as $the_ ) {
346
				call_user_func_array( $the_['function'], $args );
347
			}
348
		} while ( false !== next( $this->iterations[ $nesting_level ] ) );
349
350
		unset( $this->iterations[ $nesting_level ] );
351
		$this->nesting_level--;
352
	}
353
354
	/**
355
	 * Return the current priority level of the currently running iteration of the hook.
356
	 *
357
	 * @since 4.7.0
358
	 * @access public
359
	 *
360
	 * @return int|false If the hook is running, return the current priority level. If it isn't running, return false.
361
	 */
362
	public function current_priority() {
363
		if ( false === current( $this->iterations ) ) {
364
			return false;
365
		}
366
367
		return current( current( $this->iterations ) );
368
	}
369
370
	/**
371
	 * Normalizes filters set up before WordPress has initialized to WP_Hook objects.
372
	 *
373
	 * @since 4.7.0
374
	 * @access public
375
	 * @static
376
	 *
377
	 * @param array $filters Filters to normalize.
378
	 * @return WP_Hook[] Array of normalized filters.
0 ignored issues
show
Documentation introduced by
Should the return type not be array<*,WP_Hook>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
379
	 */
380
	public static function build_preinitialized_hooks( $filters ) {
381
		/** @var WP_Hook[] $normalized */
382
		$normalized = array();
383
384
		foreach ( $filters as $tag => $callback_groups ) {
385
			if ( is_object( $callback_groups ) && $callback_groups instanceof WP_Hook ) {
386
				$normalized[ $tag ] = $callback_groups;
387
				continue;
388
			}
389
			$hook = new WP_Hook();
390
391
			// Loop through callback groups.
392
			foreach ( $callback_groups as $priority => $callbacks ) {
393
394
				// Loop through callbacks.
395
				foreach ( $callbacks as $cb ) {
396
					$hook->add_filter( $tag, $cb['function'], $priority, $cb['accepted_args'] );
397
				}
398
			}
399
			$normalized[ $tag ] = $hook;
400
		}
401
		return $normalized;
402
	}
403
404
	/**
405
	 * Determines whether an offset value exists.
406
	 *
407
	 * @since 4.7.0
408
	 * @access public
409
	 *
410
	 * @link http://php.net/manual/en/arrayaccess.offsetexists.php
411
	 *
412
	 * @param mixed $offset An offset to check for.
413
	 * @return bool True if the offset exists, false otherwise.
414
	 */
415
	public function offsetExists( $offset ) {
416
		return isset( $this->callbacks[ $offset ] );
417
	}
418
419
	/**
420
	 * Retrieves a value at a specified offset.
421
	 *
422
	 * @since 4.7.0
423
	 * @access public
424
	 *
425
	 * @link http://php.net/manual/en/arrayaccess.offsetget.php
426
	 *
427
	 * @param mixed $offset The offset to retrieve.
428
	 * @return mixed If set, the value at the specified offset, null otherwise.
429
	 */
430
	public function offsetGet( $offset ) {
431
		return isset( $this->callbacks[ $offset ] ) ? $this->callbacks[ $offset ] : null;
432
	}
433
434
	/**
435
	 * Sets a value at a specified offset.
436
	 *
437
	 * @since 4.7.0
438
	 * @access public
439
	 *
440
	 * @link http://php.net/manual/en/arrayaccess.offsetset.php
441
	 *
442
	 * @param mixed $offset The offset to assign the value to.
443
	 * @param mixed $value The value to set.
444
	 */
445
	public function offsetSet( $offset, $value ) {
446
		if ( is_null( $offset ) ) {
447
			$this->callbacks[] = $value;
448
		} else {
449
			$this->callbacks[ $offset ] = $value;
450
		}
451
	}
452
453
	/**
454
	 * Unsets a specified offset.
455
	 *
456
	 * @since 4.7.0
457
	 * @access public
458
	 *
459
	 * @link http://php.net/manual/en/arrayaccess.offsetunset.php
460
	 *
461
	 * @param mixed $offset The offset to unset.
462
	 */
463
	public function offsetUnset( $offset ) {
464
		unset( $this->callbacks[ $offset ] );
465
	}
466
467
	/**
468
	 * Returns the current element.
469
	 *
470
	 * @since 4.7.0
471
	 * @access public
472
	 *
473
	 * @link http://php.net/manual/en/iterator.current.php
474
	 *
475
	 * @return array Of callbacks at current priority.
476
	 */
477
	public function current() {
478
		return current( $this->callbacks );
479
	}
480
481
	/**
482
	 * Moves forward to the next element.
483
	 *
484
	 * @since 4.7.0
485
	 * @access public
486
	 *
487
	 * @link http://php.net/manual/en/iterator.next.php
488
	 *
489
	 * @return array Of callbacks at next priority.
490
	 */
491
	public function next() {
492
		return next( $this->callbacks );
493
	}
494
495
	/**
496
	 * Returns the key of the current element.
497
	 *
498
	 * @since 4.7.0
499
	 * @access public
500
	 *
501
	 * @link http://php.net/manual/en/iterator.key.php
502
	 *
503
	 * @return mixed Returns current priority on success, or NULL on failure
504
	 */
505
	public function key() {
506
		return key( $this->callbacks );
507
	}
508
509
	/**
510
	 * Checks if current position is valid.
511
	 *
512
	 * @since 4.7.0
513
	 * @access public
514
	 *
515
	 * @link http://php.net/manual/en/iterator.valid.php
516
	 *
517
	 * @return boolean
518
	 */
519
	public function valid() {
520
		return key( $this->callbacks ) !== null;
521
	}
522
523
	/**
524
	 * Rewinds the Iterator to the first element.
525
	 *
526
	 * @since 4.7.0
527
	 * @access public
528
	 *
529
	 * @link http://php.net/manual/en/iterator.rewind.php
530
	 */
531
	public function rewind() {
532
		reset( $this->callbacks );
533
	}
534
535
}
536