Skip to content

Instantly share code, notes, and snippets.

@westonruter
Last active December 11, 2015 09:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save westonruter/4581512 to your computer and use it in GitHub Desktop.
Save westonruter/4581512 to your computer and use it in GitHub Desktop.
Code related to WordPress Trac ticket #14671: Deprecate the "accepted args" argument in add_filter() and add_action() See: http://core.trac.wordpress.org/ticket/14671
Index: wp-includes/plugin.php
===================================================================
--- wp-includes/plugin.php (revision 23307)
+++ wp-includes/plugin.php (working copy)
@@ -59,10 +59,10 @@
* @param string $tag The name of the filter to hook the $function_to_add to.
* @param callback $function_to_add The name of the function to be called when the filter is applied.
* @param int $priority optional. Used to specify the order in which the functions associated with a particular action are executed (default: 10). Lower numbers correspond with earlier execution, and functions with the same priority are executed in the order in which they were added to the action.
- * @param int $accepted_args optional. The number of arguments the function accept (default 1).
+ * @param int $accepted_args optional. The number of arguments the function accept (default: number of required arguments introspected from $function_to_add via the PHP Reflection API).
* @return boolean true
*/
-function add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
+function add_filter($tag, $function_to_add, $priority = 10, $accepted_args = null) {
global $wp_filter, $merged_filters;
$idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
@@ -167,11 +167,13 @@
$args = func_get_args();
do {
- foreach( (array) current($wp_filter[$tag]) as $the_ )
+ foreach( (array) current($wp_filter[$tag]) as $the_ ) {
if ( !is_null($the_['function']) ){
+ $accepted_args = _wp_get_hook_handler_accepted_arg_count($the_['function'], $the_['accepted_args']);
$args[1] = $value;
- $value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
+ $value = call_user_func_array($the_['function'], array_slice($args, 1, $accepted_args));
}
+ }
} while ( next($wp_filter[$tag]) !== false );
@@ -226,8 +228,10 @@
do {
foreach( (array) current($wp_filter[$tag]) as $the_ )
- if ( !is_null($the_['function']) )
- $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
+ if ( !is_null($the_['function']) ) {
+ $accepted_args = _wp_get_hook_handler_accepted_arg_count($the_['function'], $the_['accepted_args']);
+ $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, $accepted_args));
+ }
} while ( next($wp_filter[$tag]) !== false );
@@ -254,7 +258,6 @@
* @param string $tag The filter hook to which the function to be removed is hooked.
* @param callback $function_to_remove The name of the function which should be removed.
* @param int $priority optional. The priority of the function (default: 10).
- * @param int $accepted_args optional. The number of arguments the function accepts (default: 1).
* @return boolean Whether the function existed before it was removed.
*/
function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
@@ -328,9 +331,9 @@
* @param string $tag The name of the action to which the $function_to_add is hooked.
* @param callback $function_to_add The name of the function you wish to be called.
* @param int $priority optional. Used to specify the order in which the functions associated with a particular action are executed (default: 10). Lower numbers correspond with earlier execution, and functions with the same priority are executed in the order in which they were added to the action.
- * @param int $accepted_args optional. The number of arguments the function accept (default 1).
+ * @param int $accepted_args Optional. The number of arguments the function accept (default: number of required arguments introspected from $function_to_add via the PHP Reflection API).
*/
-function add_action($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
+function add_action($tag, $function_to_add, $priority = 10, $accepted_args = null) {
return add_filter($tag, $function_to_add, $priority, $accepted_args);
}
@@ -402,8 +405,10 @@
do {
foreach ( (array) current($wp_filter[$tag]) as $the_ )
- if ( !is_null($the_['function']) )
- call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
+ if ( !is_null($the_['function']) ){
+ $accepted_args = _wp_get_hook_handler_accepted_arg_count($the_['function'], $the_['accepted_args']);
+ call_user_func_array($the_['function'], array_slice($args, 0, $accepted_args));
+ }
} while ( next($wp_filter[$tag]) !== false );
@@ -483,8 +488,10 @@
do {
foreach( (array) current($wp_filter[$tag]) as $the_ )
- if ( !is_null($the_['function']) )
- call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
+ if ( !is_null($the_['function']) ) {
+ $accepted_args = _wp_get_hook_handler_accepted_arg_count($the_['function'], $the_['accepted_args']);
+ call_user_func_array($the_['function'], array_slice($args, 0, $accepted_args));
+ }
} while ( next($wp_filter[$tag]) !== false );
@@ -787,3 +794,74 @@
return $function[0].$function[1];
}
}
+
+/**
+ * Obtain the argument count for a hook callback (function/method/closure) via Reflection
+ *
+ * It used to be that if hook (filter/action) is called with more than one
+ * argument, you would have to supply a 4th argument to
+ * add_action()/add_filter() in order for those extra arguments to be passed
+ * into the function. This is painful because it usually violates DRY
+ * principles since the number of arguments that a function takes is already
+ * defined when the function was defined and so it should already be known. It
+ * is also painful to have to pass in the number of arguments because
+ * add_action() and add_filter() take positional arguments and so you always
+ * have to define the 3rd argument ($priority) in order to be able to specify
+ * the 4th ($accepted_args). PHP now has an always-included Reflection
+ * extension that allows it to introspect to programmatically determine the
+ * number of arguments that a function or method takes. In older versions of
+ * WordPress when support for PHP 4 was required, WordPress was not able to
+ * take advantage of Reflection because it was first introduced in PHP 5. But
+ * now that PHP 5.2 is required, we can take advantage of Reflection and
+ * simplify the calls to add_action() and add_filter().
+ *
+ * This function is a helper that is re-used in do_action(), apply_filters(),
+ * and do_action_ref_array(). If the accepted_args has been supplied when
+ * add_action()/add_filter() was called (and it is not the default of null)
+ * then it will return that instead of trying to introspect for the number of
+ * args.
+ *
+ * @package WordPress
+ * @subpackage Plugin
+ * @access private
+ * @since 3.6
+ * @link http://core.trac.wordpress.org/ticket/14671
+ * @link http://php.net/manual/en/book.reflection.php
+ *
+ * @param callable $function The filter/action handler supplied when calling add_action()/add_filter()
+ * @param int $accepted_args The 4th argument passed into add_action()/add_filter()
+ * @return int The number of arguments to pass into hook handler
+ */
+function _wp_get_hook_handler_accepted_arg_count($function, $accepted_args) {
+ static $callable_arg_count_cache = array();
+
+ // Note is_callable() is better for methods than method_exists() because it will work with __call magic methods
+ if ( is_null($accepted_args) && is_callable($function) ) {
+ $arg_count_cache_key = null;
+ if (is_object($function) && ($function instanceof Closure)) {
+ $arg_count_cache_key = spl_object_hash($function);
+ }
+ else if (is_array($function)) {
+ $arg_count_cache_key = (is_object($function[0]) ? spl_object_hash($function[0]) : $function[0]);
+ $arg_count_cache_key .= '::' . $function[1];
+ }
+ else {
+ assert(is_string($function));
+ $arg_count_cache_key = $function;
+ }
+
+ if (isset($callable_arg_count_cache[$arg_count_cache_key])) {
+ $accepted_args = $callable_arg_count_cache[$arg_count_cache_key];
+ } else {
+ if ( is_array($function) ) {
+ list( $object, $method ) = $function;
+ $reflection = new ReflectionMethod( $object, $method );
+ } else {
+ $reflection = new ReflectionFunction( $function );
+ }
+ $accepted_args = $reflection->getNumberOfParameters();
+ $callable_arg_count_cache[$arg_count_cache_key] = $accepted_args;
+ }
+ }
+ return (int) $accepted_args;
+}
Index: wp-includes/plugin.php
===================================================================
--- wp-includes/plugin.php (revision 23307)
+++ wp-includes/plugin.php (working copy)
@@ -59,10 +59,10 @@
* @param string $tag The name of the filter to hook the $function_to_add to.
* @param callback $function_to_add The name of the function to be called when the filter is applied.
* @param int $priority optional. Used to specify the order in which the functions associated with a particular action are executed (default: 10). Lower numbers correspond with earlier execution, and functions with the same priority are executed in the order in which they were added to the action.
- * @param int $accepted_args optional. The number of arguments the function accept (default 1).
+ * @param int $accepted_args optional. The number of arguments the function accept (default: number of required arguments introspected from $function_to_add via the PHP Reflection API).
* @return boolean true
*/
-function add_filter($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
+function add_filter($tag, $function_to_add, $priority = 10, $accepted_args = null) {
global $wp_filter, $merged_filters;
$idx = _wp_filter_build_unique_id($tag, $function_to_add, $priority);
@@ -167,11 +167,13 @@
$args = func_get_args();
do {
- foreach( (array) current($wp_filter[$tag]) as $the_ )
+ foreach( (array) current($wp_filter[$tag]) as $the_ ) {
if ( !is_null($the_['function']) ){
+ $accepted_args = _wp_get_hook_handler_accepted_arg_count($the_['function'], $the_['accepted_args']);
$args[1] = $value;
- $value = call_user_func_array($the_['function'], array_slice($args, 1, (int) $the_['accepted_args']));
+ $value = call_user_func_array($the_['function'], array_slice($args, 1, $accepted_args));
}
+ }
} while ( next($wp_filter[$tag]) !== false );
@@ -226,8 +228,10 @@
do {
foreach( (array) current($wp_filter[$tag]) as $the_ )
- if ( !is_null($the_['function']) )
- $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
+ if ( !is_null($the_['function']) ) {
+ $accepted_args = _wp_get_hook_handler_accepted_arg_count($the_['function'], $the_['accepted_args']);
+ $args[0] = call_user_func_array($the_['function'], array_slice($args, 0, $accepted_args));
+ }
} while ( next($wp_filter[$tag]) !== false );
@@ -254,7 +258,6 @@
* @param string $tag The filter hook to which the function to be removed is hooked.
* @param callback $function_to_remove The name of the function which should be removed.
* @param int $priority optional. The priority of the function (default: 10).
- * @param int $accepted_args optional. The number of arguments the function accepts (default: 1).
* @return boolean Whether the function existed before it was removed.
*/
function remove_filter( $tag, $function_to_remove, $priority = 10 ) {
@@ -328,9 +331,9 @@
* @param string $tag The name of the action to which the $function_to_add is hooked.
* @param callback $function_to_add The name of the function you wish to be called.
* @param int $priority optional. Used to specify the order in which the functions associated with a particular action are executed (default: 10). Lower numbers correspond with earlier execution, and functions with the same priority are executed in the order in which they were added to the action.
- * @param int $accepted_args optional. The number of arguments the function accept (default 1).
+ * @param int $accepted_args Optional. The number of arguments the function accept (default: number of required arguments introspected from $function_to_add via the PHP Reflection API).
*/
-function add_action($tag, $function_to_add, $priority = 10, $accepted_args = 1) {
+function add_action($tag, $function_to_add, $priority = 10, $accepted_args = null) {
return add_filter($tag, $function_to_add, $priority, $accepted_args);
}
@@ -402,8 +405,10 @@
do {
foreach ( (array) current($wp_filter[$tag]) as $the_ )
- if ( !is_null($the_['function']) )
- call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
+ if ( !is_null($the_['function']) ){
+ $accepted_args = _wp_get_hook_handler_accepted_arg_count($the_['function'], $the_['accepted_args']);
+ call_user_func_array($the_['function'], array_slice($args, 0, $accepted_args));
+ }
} while ( next($wp_filter[$tag]) !== false );
@@ -483,8 +488,10 @@
do {
foreach( (array) current($wp_filter[$tag]) as $the_ )
- if ( !is_null($the_['function']) )
- call_user_func_array($the_['function'], array_slice($args, 0, (int) $the_['accepted_args']));
+ if ( !is_null($the_['function']) ) {
+ $accepted_args = _wp_get_hook_handler_accepted_arg_count($the_['function'], $the_['accepted_args']);
+ call_user_func_array($the_['function'], array_slice($args, 0, $accepted_args));
+ }
} while ( next($wp_filter[$tag]) !== false );
@@ -787,3 +794,54 @@
return $function[0].$function[1];
}
}
+
+/**
+ * Obtain the argument count for a hook callback (function/method/closure) via Reflection
+ *
+ * It used to be that if hook (filter/action) is called with more than one
+ * argument, you would have to supply a 4th argument to
+ * add_action()/add_filter() in order for those extra arguments to be passed
+ * into the function. This is painful because it usually violates DRY
+ * principles since the number of arguments that a function takes is already
+ * defined when the function was defined and so it should already be known. It
+ * is also painful to have to pass in the number of arguments because
+ * add_action() and add_filter() take positional arguments and so you always
+ * have to define the 3rd argument ($priority) in order to be able to specify
+ * the 4th ($accepted_args). PHP now has an always-included Reflection
+ * extension that allows it to introspect to programmatically determine the
+ * number of arguments that a function or method takes. In older versions of
+ * WordPress when support for PHP 4 was required, WordPress was not able to
+ * take advantage of Reflection because it was first introduced in PHP 5. But
+ * now that PHP 5.2 is required, we can take advantage of Reflection and
+ * simplify the calls to add_action() and add_filter().
+ *
+ * This function is a helper that is re-used in do_action(), apply_filters(),
+ * and do_action_ref_array(). If the accepted_args has been supplied when
+ * add_action()/add_filter() was called (and it is not the default of null)
+ * then it will return that instead of trying to introspect for the number of
+ * args.
+ *
+ * @package WordPress
+ * @subpackage Plugin
+ * @access private
+ * @since 3.6
+ * @link http://core.trac.wordpress.org/ticket/14671
+ * @link http://php.net/manual/en/book.reflection.php
+ *
+ * @param callable $function The filter/action handler supplied when calling add_action()/add_filter()
+ * @param int $accepted_args The 4th argument passed into add_action()/add_filter()
+ * @return int The number of arguments to pass into hook handler
+ */
+function _wp_get_hook_handler_accepted_arg_count($function, $accepted_args) {
+ // Note is_callable() is better for methods than method_exists() because it will work with __call magic methods
+ if ( is_null($accepted_args) && is_callable($function) ) {
+ if ( is_array($function) ) {
+ list( $object, $method ) = $function;
+ $reflection = new ReflectionMethod( $object, $method );
+ } else {
+ $reflection = new ReflectionFunction( $function );
+ }
+ $accepted_args = $reflection->getNumberOfParameters();
+ }
+ return (int) $accepted_args;
+}
#!/bin/bash
set -e
docroot=$(dirname $0)
target=$1
rm $docroot/wp-includes/plugin.php
ln $target $docroot/wp-includes/plugin.php

Speed comparisons between original plugins.php and the patched version:

$ ./switch.sh 
Switched to original

$ wp eval-file test.php
Running original plugin.php
Iteration count 100000
Testing function...
 2.842000 seconds
Testing method...
 2.881264 seconds
Testing closure...
 2.831295 seconds

$ ./switch.sh 
Switched to patched

$ wp eval-file test.php
Running patched plugin.php
Iteration count 100000
Testing function...
 3.783207 seconds
Testing method...
 3.864082 seconds
Testing closure...
 3.748331 seconds

So currently it is about a third slower by using reflection.

<?php
define('TEST_ACTION', 'foo');
define('TEST_LOOP_COUNT', 100000);
$is_reflecting = strpos(file_get_contents(ABSPATH . '/wp-includes/plugin.php'), 'ReflectionFunction') !== false;
if ($is_reflecting) {
print "Running patched plugin.php\n";
}
else {
print "Running original plugin.php\n";
}
// Create tests functions
$test_closure = function ($a, $b, $c, $d = 4) {
assert("$a$b$c$d" === '1234');
};
function test_function($a, $b, $c, $d = 4) {
assert("$a$b$c$d" === '1234');
}
class FooClass {
function test_method($a, $b, $c, $d = 4) {
assert("$a$b$c$d" === '1234');
}
}
$foo = new FooClass;
$tests = array(
'function' => 'test_function',
'method' => array( $foo, 'test_method' ),
'closure' => $test_closure,
);
// Run the actions with the test handlers
printf("Iteration count %d\n", TEST_LOOP_COUNT);
foreach($tests as $name => $handler) {
print "Testing $name...\n"; flush();
$start = microtime(true);
if ($is_reflecting) {
add_action(TEST_ACTION, $handler);
} else {
add_action(TEST_ACTION, $handler, 10, 4);
}
for($i = 0; $i < TEST_LOOP_COUNT; $i += 1) {
do_action(TEST_ACTION, 1, 2, 3);
}
$end = microtime(true);
remove_action(TEST_ACTION, $handler);
printf(" %f seconds\n", $end - $start);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment