[wp-trac] [WordPress Trac] #47678: Modernize/simplify current_user_can()
WordPress Trac
noreply at wordpress.org
Sat Jul 20 19:02:55 UTC 2019
#47678: Modernize/simplify current_user_can()
-------------------------------------+-------------------------------------
Reporter: jrf | Owner: pento
Type: enhancement | Status: assigned
Priority: normal | Milestone: 5.3
Component: Role/Capability | Version: trunk
Severity: normal | Resolution:
Keywords: has-patch has-unit- | Focuses: performance, coding-
tests commit needs-dev-note | standards
-------------------------------------+-------------------------------------
Comment (by jrf):
Ok, so I've now also reviewed all uses of `call_user_func_array()` within
WP Core and have uploaded the relevant patches.
These are - as far as I'm concerned - the last patches for this ticket.
Passing build which includes all non-committed patches: https://travis-
ci.org/jrfnl/wordpress-develop/builds/561468904
To explain what I've done and the choices I've made - and more
importantly, why a lot of function calls to `call_user_func_array()` have
**''not''** been changed, there are a couple of things one needs to know.
== 1. Spread operator versus associative arrays
The spread operator when used for unpacking an array to individual
arguments in a function call ''only works with numerically indexed
arrays''.
To explain this in code:
{{{#!php
<?php
$return = call_user_func_array( $callable, $args );
}}}
If `$args` is ''always'' a numerically indexed array, this can be replaced
by:
{{{#!php
<?php
$return = $callable( ...$args );
}}}
If `$args` is ''could potentially be an associative array'', this would
need to be replaced by:
{{{#!php
<?php
$return = $callable( ...array_values( $args ) );
}}}
Now while `$callable( ...array_values( $args ) )` would still potentially
be faster than `call_user_func_array( $callable, $args )`, the benefits
are so small that it is rarely worth replacing these.
**Conclusion: If there is any doubt over whether or not `$args` ''could''
be an associative array, the code should be left as is.**
There is one exception to this: if the `$args` array was being created
using an `array_merge()`, then using `array_values()` in combination with
the spread operator is most definitely faster, so in a few instances those
''have'' been replaced.
== 2. PHP cross-version compatibility for dynamic calls to callables.
A callable can be defined in a number of different ways:
{{{#!php
<?php
// Type 1: Simple callback
$callback = 'my_callback_function';
// Type 2: Static class method call
$callback = array('MyClass', 'myCallbackMethod');
// Type 3: Object method call
$callback = array($obj, 'myCallbackMethod');
// Type 4: Static class method call (As of PHP 5.2.3)
$callback = 'MyClass::myCallbackMethod';
// Type 5: Relative static class method call (As of PHP 5.3.0)
$callback = array('B', 'parent::who');
// Type 6: Objects implementing __invoke can be used as callables (since
PHP 5.3)
$callback = $c, $param);
// Type 7: Closures
$callback = function($a) { return $a * 2; };
}}}
Source: https://www.php.net/manual/en/language.types.callable.php
`is_callable()` will return `true` for all of the above.
So, in that case, you'd think it'd be safe to call each of these
dynamically, wouldn't you ?
{{{#!php
<?php
// Replace call to `call_user_func( $callback, $param );`
$callback( $param );
// Replace call to `call_user_func_array( $callback, $params );`
$callback( ...$params );
}}}
Unfortunately, that is not the case. It will work in 5 out of the 7 cases,
but:
* callbacks to static methods declared as per Type 4 will **fail** with a
fatal error `Call to undefined function MyClass::myCallbackMethod()` in
PHP 5.6, though they will work fine in PHP 7 and above.
* callbacks to static methods declared as per Type 5 will **fail** with a
(catchable) fatal error `Call to undefined method
MyClass::parent::myCallbackMethod`.
See: https://3v4l.org/C28tc
**Conclusion: While the callback declarations of type 4 and type 5 are
quite rare, for any call to `call_user_func_array()` where the `$callback`
is user/plugin/theme defined, the function call to
`call_user_func_array()` should not be changed to a dynamic function
call.**
== 3. `call_user_func_array()` vs `call_user_func()` vs dynamic function
calls
As a rule of thumb for function call performance:
* A direct function call - `function_call()` - would be 1.
* A dynamic function call - `$function()` - takes ~1.5x as long.
* A call via `call_user_func()` takes ~2x as long.
* And a call via `call_user_func_array()` takes ~3x as long.
While point 2 explains why not all calls to `call_user_func_array()` can
be replaced by dynamic functions, here I will try to explain why they also
can't all be replaced by calls to `call_user_func()` in combination with
the spread operator.
The short of it, is that `call_user_func()` will always pass by value,
while a dynamic function call and a call via `call_user_func_array()`
passes the variables and keeps references intact.
In code:
{{{#!php
<?php
// This function expects to always be passed a variable by reference, not
a plain value.
function my_callback( &$param ) {}
$callback = 'my_callback';
$a = true;
$b = array( $a );
$c = array( &$a );
//my_callback( true ); // Fatal error: passing a value when reference is
expected.
my_callback( $a ); // OK
my_callback( ...$b ); // OK
my_callback( ...$c ); // OK
//$callback( true ); // Fatal error: passing a value when reference is
expected.
$callback( $a ); // OK
$callback( ...$b ); // OK
$callback( ...$c ); // OK
call_user_func( $callback, true ); // PHP 7.1+: Warning: Parameter 1 to
my_callback() expected to be a reference, value given
call_user_func( $callback, $a ); // PHP 7.1+: Warning: Parameter 1 to
my_callback() expected to be a reference, value given
call_user_func( $callback, ...$b ); // PHP 5.6+: Warning: Parameter 1 to
my_callback() expected to be a reference, value given
call_user_func( $callback, ...$c ); // PHP 7.0+: Warning: Parameter 1 to
my_callback() expected to be a reference, value given
call_user_func_array( $callback, array( true ) ); // PHP 7.0+: Warning:
Parameter 1 to my_callback() expected to be a reference, value given
call_user_func_array( $callback, array( $a ) ); // PHP 7.0+: Warning:
Parameter 1 to my_callback() expected to be a reference, value given
call_user_func_array( $callback, $b ); // PHP 5.6: Warning: Parameter 1 to
my_callback() expected to be a reference, value given
call_user_func_array( $callback, array( &$a ) ); // OK
call_user_func_array( $callback, $c ); // OK
}}}
Here's the code on 3v4l to have a play with: https://3v4l.org/AfX8n
This means that, even though `call_user_func()` in combination with the
spread operator performs better than `call_user_func_array()`, it cannot
always be used as a replacement if the function being called expects a
variable passed by reference instead of a plain value.
While to be fair, using reference is a ''not a good idea™'' and should be
avoided in most cases, we cannot ignore existing uses and while it would
be better - over time - for those to be changed, until that time, we need
to account for them.
This, in effect, means that, for instance `WP_Hooks::apply_filters()` can
not be simplified further as - even within WP Core -, there are filter
functions which expect to receive the `$value` to be filtered by
reference.
**Conclusion: When the `$callback` is user/plugin/theme defined and it is
not known whether or not it expects parameters by reference, calls to
`call_user_func_array()` can only safely be changed to dynamic function
calls, not to a call to `call_user_func()` in combination with the spread
operator.**
----
Now, in case anyone is thinking "hang on, but I seem to remember that
call-time pass by reference was deprecated/removed" ... ?
Yes, it is. Call-time pass by reference was deprecated in PHP 5.3 and
removed in PHP 5.4, but is something different than what I'm talking about
here.
In code:
{{{#!php
<?php
function my_callback( &$param ) {} // <-- This is still fine and is what
I'm talking about above.
$a = 1;
my_callback( &$a ); // <- This has been deprecated/removed in PHP 5.3/5.4.
}}}
So, the change to `do_action()` as mentioned in
https://core.trac.wordpress.org/ticket/47678#comment:30 is still perfectly
valid.
----
== The patches
Having said the above, I've erred on the side of caution and **only** made
those changes which in my estimation were safe to make.
This includes not having made significant changes to functions using an
arbitrary `$callback` ''even when those functions are never called with a
type 4 or 5 callback from within WP or (confirmed by searches) from any
plugin or theme''.
**''All of the patches for this ticket which have already been committed,
are "safe" from the above described issues.''**
Of the existing, non-committed patches, there were two which could have
run into point 2 of the above explanation. I have replaced these with new
versions of those patches.
All the new patches, of course, comply with the above conclusions.
== Additional notes
=== Some patches don't add a spread operator
A number of patches don't add the spread operator, but do remove the call
to `call_user_func_array()`. In none of these cases, the call to
`call_user_func_array()` was necessary in the first place, so I've just
gotten rid of them.
----
P.S.: sometimes I hate PHP.
--
Ticket URL: <https://core.trac.wordpress.org/ticket/47678#comment:31>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform
More information about the wp-trac
mailing list