ru-se.com

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

class-wp-hook.php (15006B)


      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 	 * @var array
     25 	 */
     26 	public $callbacks = array();
     27 
     28 	/**
     29 	 * The priority keys of actively running iterations of a hook.
     30 	 *
     31 	 * @since 4.7.0
     32 	 * @var array
     33 	 */
     34 	private $iterations = array();
     35 
     36 	/**
     37 	 * The current priority of actively running iterations of a hook.
     38 	 *
     39 	 * @since 4.7.0
     40 	 * @var array
     41 	 */
     42 	private $current_priority = array();
     43 
     44 	/**
     45 	 * Number of levels this hook can be recursively called.
     46 	 *
     47 	 * @since 4.7.0
     48 	 * @var int
     49 	 */
     50 	private $nesting_level = 0;
     51 
     52 	/**
     53 	 * Flag for if we're currently doing an action, rather than a filter.
     54 	 *
     55 	 * @since 4.7.0
     56 	 * @var bool
     57 	 */
     58 	private $doing_action = false;
     59 
     60 	/**
     61 	 * Adds a callback function to a filter hook.
     62 	 *
     63 	 * @since 4.7.0
     64 	 *
     65 	 * @param string   $hook_name     The name of the filter to add the callback to.
     66 	 * @param callable $callback      The callback to be run when the filter is applied.
     67 	 * @param int      $priority      The order in which the functions associated with a particular filter
     68 	 *                                are executed. Lower numbers correspond with earlier execution,
     69 	 *                                and functions with the same priority are executed in the order
     70 	 *                                in which they were added to the filter.
     71 	 * @param int      $accepted_args The number of arguments the function accepts.
     72 	 */
     73 	public function add_filter( $hook_name, $callback, $priority, $accepted_args ) {
     74 		$idx = _wp_filter_build_unique_id( $hook_name, $callback, $priority );
     75 
     76 		$priority_existed = isset( $this->callbacks[ $priority ] );
     77 
     78 		$this->callbacks[ $priority ][ $idx ] = array(
     79 			'function'      => $callback,
     80 			'accepted_args' => $accepted_args,
     81 		);
     82 
     83 		// If we're adding a new priority to the list, put them back in sorted order.
     84 		if ( ! $priority_existed && count( $this->callbacks ) > 1 ) {
     85 			ksort( $this->callbacks, SORT_NUMERIC );
     86 		}
     87 
     88 		if ( $this->nesting_level > 0 ) {
     89 			$this->resort_active_iterations( $priority, $priority_existed );
     90 		}
     91 	}
     92 
     93 	/**
     94 	 * Handles resetting callback priority keys mid-iteration.
     95 	 *
     96 	 * @since 4.7.0
     97 	 *
     98 	 * @param false|int $new_priority     Optional. The priority of the new filter being added. Default false,
     99 	 *                                    for no priority being added.
    100 	 * @param bool      $priority_existed Optional. Flag for whether the priority already existed before the new
    101 	 *                                    filter was added. Default false.
    102 	 */
    103 	private function resort_active_iterations( $new_priority = false, $priority_existed = false ) {
    104 		$new_priorities = array_keys( $this->callbacks );
    105 
    106 		// If there are no remaining hooks, clear out all running iterations.
    107 		if ( ! $new_priorities ) {
    108 			foreach ( $this->iterations as $index => $iteration ) {
    109 				$this->iterations[ $index ] = $new_priorities;
    110 			}
    111 
    112 			return;
    113 		}
    114 
    115 		$min = min( $new_priorities );
    116 
    117 		foreach ( $this->iterations as $index => &$iteration ) {
    118 			$current = current( $iteration );
    119 
    120 			// If we're already at the end of this iteration, just leave the array pointer where it is.
    121 			if ( false === $current ) {
    122 				continue;
    123 			}
    124 
    125 			$iteration = $new_priorities;
    126 
    127 			if ( $current < $min ) {
    128 				array_unshift( $iteration, $current );
    129 				continue;
    130 			}
    131 
    132 			while ( current( $iteration ) < $current ) {
    133 				if ( false === next( $iteration ) ) {
    134 					break;
    135 				}
    136 			}
    137 
    138 			// If we have a new priority that didn't exist, but ::apply_filters() or ::do_action() thinks it's the current priority...
    139 			if ( $new_priority === $this->current_priority[ $index ] && ! $priority_existed ) {
    140 				/*
    141 				 * ...and the new priority is the same as what $this->iterations thinks is the previous
    142 				 * priority, we need to move back to it.
    143 				 */
    144 
    145 				if ( false === current( $iteration ) ) {
    146 					// If we've already moved off the end of the array, go back to the last element.
    147 					$prev = end( $iteration );
    148 				} else {
    149 					// Otherwise, just go back to the previous element.
    150 					$prev = prev( $iteration );
    151 				}
    152 
    153 				if ( false === $prev ) {
    154 					// Start of the array. Reset, and go about our day.
    155 					reset( $iteration );
    156 				} elseif ( $new_priority !== $prev ) {
    157 					// Previous wasn't the same. Move forward again.
    158 					next( $iteration );
    159 				}
    160 			}
    161 		}
    162 
    163 		unset( $iteration );
    164 	}
    165 
    166 	/**
    167 	 * Removes a callback function from a filter hook.
    168 	 *
    169 	 * @since 4.7.0
    170 	 *
    171 	 * @param string   $hook_name The filter hook to which the function to be removed is hooked.
    172 	 * @param callable $callback  The callback to be removed from running when the filter is applied.
    173 	 * @param int      $priority  The exact priority used when adding the original filter callback.
    174 	 * @return bool Whether the callback existed before it was removed.
    175 	 */
    176 	public function remove_filter( $hook_name, $callback, $priority ) {
    177 		$function_key = _wp_filter_build_unique_id( $hook_name, $callback, $priority );
    178 
    179 		$exists = isset( $this->callbacks[ $priority ][ $function_key ] );
    180 
    181 		if ( $exists ) {
    182 			unset( $this->callbacks[ $priority ][ $function_key ] );
    183 
    184 			if ( ! $this->callbacks[ $priority ] ) {
    185 				unset( $this->callbacks[ $priority ] );
    186 
    187 				if ( $this->nesting_level > 0 ) {
    188 					$this->resort_active_iterations();
    189 				}
    190 			}
    191 		}
    192 
    193 		return $exists;
    194 	}
    195 
    196 	/**
    197 	 * Checks if a specific callback has been registered for this hook.
    198 	 *
    199 	 * When using the `$callback` argument, this function may return a non-boolean value
    200 	 * that evaluates to false (e.g. 0), so use the `===` operator for testing the return value.
    201 	 *
    202 	 * @since 4.7.0
    203 	 *
    204 	 * @param string         $hook_name Optional. The name of the filter hook. Default empty.
    205 	 * @param callable|false $callback  Optional. The callback to check for. Default false.
    206 	 * @return bool|int If `$callback` is omitted, returns boolean for whether the hook has
    207 	 *                  anything registered. When checking a specific function, the priority
    208 	 *                  of that hook is returned, or false if the function is not attached.
    209 	 */
    210 	public function has_filter( $hook_name = '', $callback = false ) {
    211 		if ( false === $callback ) {
    212 			return $this->has_filters();
    213 		}
    214 
    215 		$function_key = _wp_filter_build_unique_id( $hook_name, $callback, false );
    216 
    217 		if ( ! $function_key ) {
    218 			return false;
    219 		}
    220 
    221 		foreach ( $this->callbacks as $priority => $callbacks ) {
    222 			if ( isset( $callbacks[ $function_key ] ) ) {
    223 				return $priority;
    224 			}
    225 		}
    226 
    227 		return false;
    228 	}
    229 
    230 	/**
    231 	 * Checks if any callbacks have been registered for this hook.
    232 	 *
    233 	 * @since 4.7.0
    234 	 *
    235 	 * @return bool True if callbacks have been registered for the current hook, otherwise false.
    236 	 */
    237 	public function has_filters() {
    238 		foreach ( $this->callbacks as $callbacks ) {
    239 			if ( $callbacks ) {
    240 				return true;
    241 			}
    242 		}
    243 
    244 		return false;
    245 	}
    246 
    247 	/**
    248 	 * Removes all callbacks from the current filter.
    249 	 *
    250 	 * @since 4.7.0
    251 	 *
    252 	 * @param int|false $priority Optional. The priority number to remove. Default false.
    253 	 */
    254 	public function remove_all_filters( $priority = false ) {
    255 		if ( ! $this->callbacks ) {
    256 			return;
    257 		}
    258 
    259 		if ( false === $priority ) {
    260 			$this->callbacks = array();
    261 		} elseif ( isset( $this->callbacks[ $priority ] ) ) {
    262 			unset( $this->callbacks[ $priority ] );
    263 		}
    264 
    265 		if ( $this->nesting_level > 0 ) {
    266 			$this->resort_active_iterations();
    267 		}
    268 	}
    269 
    270 	/**
    271 	 * Calls the callback functions that have been added to a filter hook.
    272 	 *
    273 	 * @since 4.7.0
    274 	 *
    275 	 * @param mixed $value The value to filter.
    276 	 * @param array $args  Additional parameters to pass to the callback functions.
    277 	 *                     This array is expected to include $value at index 0.
    278 	 * @return mixed The filtered value after all hooked functions are applied to it.
    279 	 */
    280 	public function apply_filters( $value, $args ) {
    281 		if ( ! $this->callbacks ) {
    282 			return $value;
    283 		}
    284 
    285 		$nesting_level = $this->nesting_level++;
    286 
    287 		$this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
    288 		$num_args                           = count( $args );
    289 
    290 		do {
    291 			$this->current_priority[ $nesting_level ] = current( $this->iterations[ $nesting_level ] );
    292 			$priority                                 = $this->current_priority[ $nesting_level ];
    293 
    294 			foreach ( $this->callbacks[ $priority ] as $the_ ) {
    295 				if ( ! $this->doing_action ) {
    296 					$args[0] = $value;
    297 				}
    298 
    299 				// Avoid the array_slice() if possible.
    300 				if ( 0 == $the_['accepted_args'] ) {
    301 					$value = call_user_func( $the_['function'] );
    302 				} elseif ( $the_['accepted_args'] >= $num_args ) {
    303 					$value = call_user_func_array( $the_['function'], $args );
    304 				} else {
    305 					$value = call_user_func_array( $the_['function'], array_slice( $args, 0, (int) $the_['accepted_args'] ) );
    306 				}
    307 			}
    308 		} while ( false !== next( $this->iterations[ $nesting_level ] ) );
    309 
    310 		unset( $this->iterations[ $nesting_level ] );
    311 		unset( $this->current_priority[ $nesting_level ] );
    312 
    313 		$this->nesting_level--;
    314 
    315 		return $value;
    316 	}
    317 
    318 	/**
    319 	 * Calls the callback functions that have been added to an action hook.
    320 	 *
    321 	 * @since 4.7.0
    322 	 *
    323 	 * @param array $args Parameters to pass to the callback functions.
    324 	 */
    325 	public function do_action( $args ) {
    326 		$this->doing_action = true;
    327 		$this->apply_filters( '', $args );
    328 
    329 		// If there are recursive calls to the current action, we haven't finished it until we get to the last one.
    330 		if ( ! $this->nesting_level ) {
    331 			$this->doing_action = false;
    332 		}
    333 	}
    334 
    335 	/**
    336 	 * Processes the functions hooked into the 'all' hook.
    337 	 *
    338 	 * @since 4.7.0
    339 	 *
    340 	 * @param array $args Arguments to pass to the hook callbacks. Passed by reference.
    341 	 */
    342 	public function do_all_hook( &$args ) {
    343 		$nesting_level                      = $this->nesting_level++;
    344 		$this->iterations[ $nesting_level ] = array_keys( $this->callbacks );
    345 
    346 		do {
    347 			$priority = current( $this->iterations[ $nesting_level ] );
    348 
    349 			foreach ( $this->callbacks[ $priority ] as $the_ ) {
    350 				call_user_func_array( $the_['function'], $args );
    351 			}
    352 		} while ( false !== next( $this->iterations[ $nesting_level ] ) );
    353 
    354 		unset( $this->iterations[ $nesting_level ] );
    355 		$this->nesting_level--;
    356 	}
    357 
    358 	/**
    359 	 * Return the current priority level of the currently running iteration of the hook.
    360 	 *
    361 	 * @since 4.7.0
    362 	 *
    363 	 * @return int|false If the hook is running, return the current priority level.
    364 	 *                   If it isn't running, return false.
    365 	 */
    366 	public function current_priority() {
    367 		if ( false === current( $this->iterations ) ) {
    368 			return false;
    369 		}
    370 
    371 		return current( current( $this->iterations ) );
    372 	}
    373 
    374 	/**
    375 	 * Normalizes filters set up before WordPress has initialized to WP_Hook objects.
    376 	 *
    377 	 * The `$filters` parameter should be an array keyed by hook name, with values
    378 	 * containing either:
    379 	 *
    380 	 *  - A `WP_Hook` instance
    381 	 *  - An array of callbacks keyed by their priorities
    382 	 *
    383 	 * Examples:
    384 	 *
    385 	 *     $filters = array(
    386 	 *         'wp_fatal_error_handler_enabled' => array(
    387 	 *             10 => array(
    388 	 *                 array(
    389 	 *                     'accepted_args' => 0,
    390 	 *                     'function'      => function() {
    391 	 *                         return false;
    392 	 *                     },
    393 	 *                 ),
    394 	 *             ),
    395 	 *         ),
    396 	 *     );
    397 	 *
    398 	 * @since 4.7.0
    399 	 *
    400 	 * @param array $filters Filters to normalize. See documentation above for details.
    401 	 * @return WP_Hook[] Array of normalized filters.
    402 	 */
    403 	public static function build_preinitialized_hooks( $filters ) {
    404 		/** @var WP_Hook[] $normalized */
    405 		$normalized = array();
    406 
    407 		foreach ( $filters as $hook_name => $callback_groups ) {
    408 			if ( is_object( $callback_groups ) && $callback_groups instanceof WP_Hook ) {
    409 				$normalized[ $hook_name ] = $callback_groups;
    410 				continue;
    411 			}
    412 
    413 			$hook = new WP_Hook();
    414 
    415 			// Loop through callback groups.
    416 			foreach ( $callback_groups as $priority => $callbacks ) {
    417 
    418 				// Loop through callbacks.
    419 				foreach ( $callbacks as $cb ) {
    420 					$hook->add_filter( $hook_name, $cb['function'], $priority, $cb['accepted_args'] );
    421 				}
    422 			}
    423 
    424 			$normalized[ $hook_name ] = $hook;
    425 		}
    426 
    427 		return $normalized;
    428 	}
    429 
    430 	/**
    431 	 * Determines whether an offset value exists.
    432 	 *
    433 	 * @since 4.7.0
    434 	 *
    435 	 * @link https://www.php.net/manual/en/arrayaccess.offsetexists.php
    436 	 *
    437 	 * @param mixed $offset An offset to check for.
    438 	 * @return bool True if the offset exists, false otherwise.
    439 	 */
    440 	public function offsetExists( $offset ) {
    441 		return isset( $this->callbacks[ $offset ] );
    442 	}
    443 
    444 	/**
    445 	 * Retrieves a value at a specified offset.
    446 	 *
    447 	 * @since 4.7.0
    448 	 *
    449 	 * @link https://www.php.net/manual/en/arrayaccess.offsetget.php
    450 	 *
    451 	 * @param mixed $offset The offset to retrieve.
    452 	 * @return mixed If set, the value at the specified offset, null otherwise.
    453 	 */
    454 	public function offsetGet( $offset ) {
    455 		return isset( $this->callbacks[ $offset ] ) ? $this->callbacks[ $offset ] : null;
    456 	}
    457 
    458 	/**
    459 	 * Sets a value at a specified offset.
    460 	 *
    461 	 * @since 4.7.0
    462 	 *
    463 	 * @link https://www.php.net/manual/en/arrayaccess.offsetset.php
    464 	 *
    465 	 * @param mixed $offset The offset to assign the value to.
    466 	 * @param mixed $value The value to set.
    467 	 */
    468 	public function offsetSet( $offset, $value ) {
    469 		if ( is_null( $offset ) ) {
    470 			$this->callbacks[] = $value;
    471 		} else {
    472 			$this->callbacks[ $offset ] = $value;
    473 		}
    474 	}
    475 
    476 	/**
    477 	 * Unsets a specified offset.
    478 	 *
    479 	 * @since 4.7.0
    480 	 *
    481 	 * @link https://www.php.net/manual/en/arrayaccess.offsetunset.php
    482 	 *
    483 	 * @param mixed $offset The offset to unset.
    484 	 */
    485 	public function offsetUnset( $offset ) {
    486 		unset( $this->callbacks[ $offset ] );
    487 	}
    488 
    489 	/**
    490 	 * Returns the current element.
    491 	 *
    492 	 * @since 4.7.0
    493 	 *
    494 	 * @link https://www.php.net/manual/en/iterator.current.php
    495 	 *
    496 	 * @return array Of callbacks at current priority.
    497 	 */
    498 	public function current() {
    499 		return current( $this->callbacks );
    500 	}
    501 
    502 	/**
    503 	 * Moves forward to the next element.
    504 	 *
    505 	 * @since 4.7.0
    506 	 *
    507 	 * @link https://www.php.net/manual/en/iterator.next.php
    508 	 *
    509 	 * @return array Of callbacks at next priority.
    510 	 */
    511 	public function next() {
    512 		return next( $this->callbacks );
    513 	}
    514 
    515 	/**
    516 	 * Returns the key of the current element.
    517 	 *
    518 	 * @since 4.7.0
    519 	 *
    520 	 * @link https://www.php.net/manual/en/iterator.key.php
    521 	 *
    522 	 * @return mixed Returns current priority on success, or NULL on failure
    523 	 */
    524 	public function key() {
    525 		return key( $this->callbacks );
    526 	}
    527 
    528 	/**
    529 	 * Checks if current position is valid.
    530 	 *
    531 	 * @since 4.7.0
    532 	 *
    533 	 * @link https://www.php.net/manual/en/iterator.valid.php
    534 	 *
    535 	 * @return bool Whether the current position is valid.
    536 	 */
    537 	public function valid() {
    538 		return key( $this->callbacks ) !== null;
    539 	}
    540 
    541 	/**
    542 	 * Rewinds the Iterator to the first element.
    543 	 *
    544 	 * @since 4.7.0
    545 	 *
    546 	 * @link https://www.php.net/manual/en/iterator.rewind.php
    547 	 */
    548 	public function rewind() {
    549 		reset( $this->callbacks );
    550 	}
    551 
    552 }