rfc:consistent_callables
Differences
This shows you the differences between two versions of the page.
Both sides previous revisionPrevious revisionNext revision | Previous revisionNext revisionBoth sides next revision | ||
rfc:consistent_callables [2015/10/05 19:03] – danack | rfc:consistent_callables [2019/04/28 02:51] – Made scope of changes even smaller. danack | ||
---|---|---|---|
Line 1: | Line 1: | ||
====== PHP RFC: Consistent Callables ====== | ====== PHP RFC: Consistent Callables ====== | ||
- | * Version: 0.1 | + | * Version: 0.95 |
- | * Date: 2015-09-28 | + | * Date: 2019-04-28 |
* Author: Dan Ackroyd | * Author: Dan Ackroyd | ||
- | * Status: | + | * Status: |
* First Published at: https:// | * First Published at: https:// | ||
Line 13: | Line 13: | ||
The two aims of this RFC are: | The two aims of this RFC are: | ||
- | + | ||
- | i) to make callable be a consistent type, so that it can be used safely without regard to the location where it is being used. | + | i) to make 'callable' |
ii) Make call_user_func be equivalent to calling a callable through direct invocation. i.e. for a callable that requires zero arguments, if the code `call_user_func($callable); | ii) Make call_user_func be equivalent to calling a callable through direct invocation. i.e. for a callable that requires zero arguments, if the code `call_user_func($callable); | ||
+ | |||
+ | ==== Summary of changes, aka tl:dr version ==== | ||
+ | |||
+ | 1) Modify the callable type check for parameter and return types, so that only values that are universally callable pass the type check. | ||
+ | |||
+ | 2) Add function is_callable_type(mixed $var) : bool - returns true if the parameter can be passed as a callable type, and is callable in any scope, otherwise returns false. | ||
+ | |||
+ | 3) Modify the current is_callable() function to only return true for values that will be callable in the current scope. | ||
+ | |||
+ | 4) self, parent and other non-resolved strings will no longer be usable either in string or array based callables i.e. neither ' | ||
==== Example problems ==== | ==== Example problems ==== | ||
This section lists the problems with the current implementation of callables. I believe it is complete, though it may not be due to the magic of re-binding methods. | This section lists the problems with the current implementation of callables. I believe it is complete, though it may not be due to the magic of re-binding methods. | ||
- | |||
=== Callable type is inconsistent === | === Callable type is inconsistent === | ||
Line 32: | Line 41: | ||
echo " | echo " | ||
} | } | ||
+ | |||
class Bar { | class Bar { | ||
private static function staticMethod() { | private static function staticMethod() { | ||
} | } | ||
+ | |||
public function testMethod(callable $callable) { | public function testMethod(callable $callable) { | ||
echo " | echo " | ||
Line 42: | Line 51: | ||
} | } | ||
} | } | ||
+ | |||
$callable = [' | $callable = [' | ||
+ | |||
$obj = new Bar(); | $obj = new Bar(); | ||
$obj-> | $obj-> | ||
- | + | ||
+ | |||
// output is | // output is | ||
// testInClass OK | // testInClass OK | ||
// Fatal error: Argument 1 passed to testFunction() must be callable, array given, called in | // Fatal error: Argument 1 passed to testFunction() must be callable, array given, called in | ||
// %d on line %d and defined in %s on line %d | // %d on line %d and defined in %s on line %d | ||
+ | |||
</ | </ | ||
i.e. even though the parameter was a valid callable type when passed to the instance method of the class, it became an invalid callable when passed to the function. | i.e. even though the parameter was a valid callable type when passed to the instance method of the class, it became an invalid callable when passed to the function. | ||
- | === Private / protected methods report as callable when they are not === | + | === Private / protected methods report as callable when they are not === |
<code php> | <code php> | ||
- | |||
class A | class A | ||
{ | { | ||
Line 66: | Line 74: | ||
return is_callable($param); | return is_callable($param); | ||
} | } | ||
- | | + | |
private function privateMethod() { | private function privateMethod() { | ||
echo "This is a private method"; | echo "This is a private method"; | ||
} | } | ||
+ | |||
public function test($param) { | public function test($param) { | ||
if ($this-> | if ($this-> | ||
Line 77: | Line 85: | ||
} | } | ||
} | } | ||
+ | |||
class B extends A | class B extends A | ||
{ | { | ||
Line 86: | Line 94: | ||
} | } | ||
} | } | ||
+ | |||
$a = new A(); | $a = new A(); | ||
$b = new B(); | $b = new B(); | ||
+ | |||
$callable = [$a, ' | $callable = [$a, ' | ||
+ | |||
$a-> | $a-> | ||
$b-> | $b-> | ||
+ | |||
// Output is | // Output is | ||
// This is a private method | // This is a private method | ||
Line 101: | Line 109: | ||
</ | </ | ||
- | i.e. despite checking with `is_callable` if something is callable, the program crashes because `is_callable` lied to us. | + | i.e. despite checking with `is_callable` if something is callable, the program crashes because `is_callable` lied to us. |
- | === Instance method reported as callable === | + | === Instance method reported as callable === |
- | The is_callable function reports an instance method as callable on a class. It should not callable and that behaviour is already deprecated. Instance methods should only callable on instances. | + | The is_callable function reports an instance method as callable on a class. It should not be callable and that behaviour is already deprecated. Instance methods should only callable on instances. |
<code php> | <code php> | ||
+ | |||
class Foo { | class Foo { | ||
function bar() { | function bar() { | ||
Line 113: | Line 122: | ||
} | } | ||
} | } | ||
+ | |||
$callable = [' | $callable = [' | ||
var_dump(is_callable($callable)); | var_dump(is_callable($callable)); | ||
$callable(); | $callable(); | ||
- | + | ||
+ | |||
//Output is: | //Output is: | ||
// | // | ||
Line 125: | Line 134: | ||
</ | </ | ||
- | === The method invoked varies depending where the callable is called from === | + | === The method invoked varies depending where the callable is called from === |
For callables that use `self` or `parent` as part of the definition of the callable, the actual code that will be invoked varies depending on where the callable was called from. | For callables that use `self` or `parent` as part of the definition of the callable, the actual code that will be invoked varies depending on where the callable was called from. | ||
<code php> | <code php> | ||
+ | |||
class Foo { | class Foo { | ||
public static function getCallable() { | public static function getCallable() { | ||
Line 141: | Line 151: | ||
} | } | ||
} | } | ||
+ | |||
class Bar { | class Bar { | ||
public function process(callable $callable) { | public function process(callable $callable) { | ||
Line 149: | Line 159: | ||
echo "This is bar:: | echo "This is bar:: | ||
} | } | ||
- | | + | |
public static function getCallable() { | public static function getCallable() { | ||
return ' | return ' | ||
} | } | ||
} | } | ||
+ | |||
$foo = new Foo(); | $foo = new Foo(); | ||
$bar = new Bar(); | $bar = new Bar(); | ||
$callable = $foo-> | $callable = $foo-> | ||
$bar-> | $bar-> | ||
+ | |||
$callable = $bar-> | $callable = $bar-> | ||
$foo-> | $foo-> | ||
- | + | ||
+ | |||
// Output is: | // Output is: | ||
// This is bar::hello | // This is bar::hello | ||
// Fatal error: Uncaught TypeError: Argument 1 passed to Foo:: | // Fatal error: Uncaught TypeError: Argument 1 passed to Foo:: | ||
// callable, string given, called in /in/7SCuB on line 34 and defined in / | // callable, string given, called in /in/7SCuB on line 34 and defined in / | ||
+ | i.e. calling `self:: | ||
</ | </ | ||
- | i.e. calling `self:: | + | === call_user_func different from is_callable === |
- | + | ||
- | + | ||
- | + | ||
- | === call_user_func different from is_callable === | + | |
In this example the result of calling something through call_user_func and invoking it directly is different. | In this example the result of calling something through call_user_func and invoking it directly is different. | ||
<code php> | <code php> | ||
+ | |||
class foo { | class foo { | ||
public static function getCallable() { | public static function getCallable() { | ||
Line 197: | Line 205: | ||
$callable = $foo-> | $callable = $foo-> | ||
$foo-> | $foo-> | ||
+ | |||
$bar-> | $bar-> | ||
+ | |||
// Output is: | // Output is: | ||
// This is foo::bar | // This is foo::bar | ||
// Fatal error: Uncaught Error: Class ' | // Fatal error: Uncaught Error: Class ' | ||
+ | i.e. despite something being ' | ||
</ | </ | ||
- | |||
- | i.e. despite something being ' | ||
===== Details of changes ===== | ===== Details of changes ===== | ||
- | ==== Definition of valid values for callable type ==== | + | ==== Definition of valid values for callable type ==== |
The following would be the complete list of valid values for the callable type: | The following would be the complete list of valid values for the callable type: | ||
- A string that is the name of a function. | - A string that is the name of a function. | ||
- | - An array consisting of two elements; a string at index 0 which is a valid class name, and a string at index 1 which must meet the conditions: | + | - An array consisting of two elements; a string at index 0 which is a valid fully qualified |
- | * either be the name of a public static function of the class or the class must have a magic %%__callStatic%% method. | + | * either be the name of a public static function of the class or the class must have a magic __callStatic method. |
* the name must not be that of an instance method. | * the name must not be that of an instance method. | ||
- | - An array consisting of two elements; an object at index 0, and a string at index 1 where either the string is the name of a public method of the object, or the object has a magic %%__call%% method. | + | - An array consisting of two elements; an object at index 0, and a string at index 1 where either the string is the name of a public method of the object, or the object has a magic __call method. |
- An instance of a class (an object) where the class has a public __invoke() method. | - An instance of a class (an object) where the class has a public __invoke() method. | ||
- Closures, which includes anonymous functions. | - Closures, which includes anonymous functions. | ||
- | + | Note - Does not affect calling private/ | |
- | ==== Note - removal of colon separated string ==== | + | |
- | + | ||
- | This removes the ability to define a callable as a single string composing a class-name and a method name separated by double-colons. The reasons for this are that: | + | |
- | + | ||
- | * It is duplication of ii. The duplication of code is not just in PHP core, but for all userland code and libraries that analyze callables must duplicate the handling. | + | |
- | + | ||
- | * For things like routing libraries it is useful for users to be able to specify not a valid callable, but instead a class that needs to be instantiated and the method that should be called on the instantiated object. Having ' | + | |
- | + | ||
- | * It was introduced without much discussion to address a problem in the SPL iterator. | + | |
- | https:// | + | |
- | + | ||
- | * It is easier (in the sense of fewer CPU operations) to validate [' | + | |
- | + | ||
- | + | ||
- | ==== Note - Does not affect calling private/ | + | |
While they would no longer pass the type checker for the callable type, private and protected methods could still be executed through call_user_func and direct invocation. | While they would no longer pass the type checker for the callable type, private and protected methods could still be executed through call_user_func and direct invocation. | ||
Line 245: | Line 237: | ||
class Foo { | class Foo { | ||
private function bar() { } | private function bar() { } | ||
- | | + | |
private function getCallback() { | private function getCallback() { | ||
return [$this, ' | return [$this, ' | ||
} | } | ||
- | | + | |
public execute() { | public execute() { | ||
$fn = $this-> | $fn = $this-> | ||
$fn(); // This still works | $fn(); // This still works | ||
call_user_func($fn); | call_user_func($fn); | ||
+ | echo is_callable($fn); | ||
+ | echo is_callable_type($fn); | ||
} | } | ||
} | } | ||
+ | |||
</ | </ | ||
In this example, although `$fn` is not a callable that can be passed around to arbitrary scopes, it is valid to call it in the scope that it's in. call_user_func and $fn() will continue to check whether the variable passed in is callable in the current scope. i.e. with is_callable($fn, | In this example, although `$fn` is not a callable that can be passed around to arbitrary scopes, it is valid to call it in the scope that it's in. call_user_func and $fn() will continue to check whether the variable passed in is callable in the current scope. i.e. with is_callable($fn, | ||
+ | ==== The strings ' | ||
- | ==== Add Scope Resolution Operator for self and parent ==== | + | Currently in PHP a callable can be defined using one of these words in place of a classname in a colon separated string like “self:: |
- | To allow users to resolve class-names in the compile stage of running a PHP script, | + | By replacing |
- | * self:: | + | To be clear, |
- | * parent:: | + | ==== Add a is_callable_type() function ==== |
- | Question are there any other cases where it should | + | This RFC proposes adding a separate function from is_callable that can be used to determine if a parameter can be passed as a callable type. |
- | <code php> | + | To be clear the meaning of the two functions will be: |
- | class Foo { | + | |
- | function getCallback() { | + | is_callable() - returns true if a the first parameter is callable in the current scope. |
- | return [self:: | + | |
- | } | + | |
- | public static function fooCallback() { | + | is_callable_type(mixed $var) : bool - returns true if the parameter can be passed as a callable type, and is callable in any scope, otherwise returns false. |
- | + | ||
- | } | + | |
- | } | + | |
- | class SubFoo extends | + | <code php> |
- | function | + | class Foo { |
- | | + | |
+ | | ||
+ | |||
+ | public function test($param) { | ||
+ | | ||
+ | var_dump(is_callable_type($param)); | ||
} | } | ||
} | } | ||
+ | |||
$foo = new Foo(); | $foo = new Foo(); | ||
- | $subFoo | + | $param = [$foo, ' |
- | $fn1 = $foo->getCallback(); | + | var_dump(is_callable($param)); |
- | $fn2 = $subFoo-> | + | $foo->test($param); |
+ | |||
- | //$fn1 and $fn2 will callables that have the same value | + | |
+ | output will be: | ||
+ | |||
+ | false // as the private method cannot be called from the global scope | ||
+ | true // as the private method can be called from within the class scope | ||
+ | false // as the private method cannot be passed as a parameter with callable type | ||
</ | </ | ||
- | + | ==== Instance methods | |
- | + | ||
- | ==== The strings ' | + | |
- | + | ||
- | Currently in PHP a callable can be defined using one of these keyword in place of a classname in a colon separated string like " | + | |
- | + | ||
- | By replacing the run time evaluation of these with the compile time scope resolution, the variable meaning of the values is removed and replaced with a consistent meaning. | + | |
- | + | ||
- | + | ||
- | ==== Instance methods | + | |
<code php> | <code php> | ||
Line 315: | Line 307: | ||
} | } | ||
} | } | ||
+ | |||
$callable = [' | $callable = [' | ||
var_dump(is_callable($callable)); | var_dump(is_callable($callable)); | ||
- | |||
</ | </ | ||
The output for this is currently true, it will be changed to be false. | The output for this is currently true, it will be changed to be false. | ||
- | For an instance method to be a valid callable it will need to be part of a callable that has an instance as the first element in the callable like this: | + | |
+ | For an instance method to be part of a valid callable it will need to be part of a callable that has an instance as the first element in the callable like this: | ||
<code php> | <code php> | ||
$foo = new Foo(); | $foo = new Foo(); | ||
$instanceCallable = [$foo, ' | $instanceCallable = [$foo, ' | ||
+ | |||
var_dump(is_callable($callable)); | var_dump(is_callable($callable)); | ||
</ | </ | ||
- | ==== Private and protected functions no longer report as callable for is_callable ==== | + | ==== Any additional |
- | As they are not callable from all scopes, private and protected functions | + | Any other errors in is_callable() |
- | + | ||
- | It is currently possible using the reflection methods | + | |
<code php> | <code php> | ||
- | class Foo { | + | if (is_callable($fn) === true) { |
- | + | $fn(); | |
- | //This generates a closure to the private method with ugly syntax. | + | |
- | public function getCallbackWithCurrentReflection() { | + | // given a zero argument, both of these will be guaranteed |
- | $reflection = new ReflectionMethod($this, ' | + | |
- | | + | |
- | + | ||
- | return $callback; | + | |
- | } | + | |
- | | + | |
- | //This will generate a closure | + | |
- | //RFC passes | + | |
- | public function getCallbackWithCallableRFC() { | + | |
- | $callback = callable($this, | + | |
- | + | ||
- | return $callback; | + | |
- | } | + | |
- | + | ||
- | private function somePrivateMethod() { | + | |
- | echo "This is private."; | + | |
- | } | + | |
} | } | ||
- | |||
- | $foo = new Foo(); | ||
- | $callback = $foo-> | ||
- | $callback(); | ||
</ | </ | ||
- | This will allow a class to return a private method bound to an instance as a callback, without having to have the private method be exposed to the public API of the class. | + | ==== call_user_func equivalence |
+ | The changes in the rest of the RFC should make this goal be achieved. i.e. for any callable that is invokable via `call_user_func($callable); | ||
- | ==== is_callable function change ==== | + | ===== Target versions ===== |
- | + | ||
- | Currently the function is_callable has the signature: | + | |
- | + | ||
- | <code php> | + | |
- | bool is_callable ( callable $name [, bool $syntax_only = false [, string & | + | |
- | </ | + | |
- | + | ||
- | which allows users to do this: | + | |
- | + | ||
- | <code php> | + | |
- | class Foo { | + | |
- | function bar() { | + | |
- | + | ||
- | } | + | |
- | } | + | |
- | + | ||
- | + | ||
- | $obj = new Foo(); | + | |
- | is_callable([$obj, | + | |
- | echo $name; //output is Foo::bar | + | |
- | </ | + | |
- | + | ||
- | The signature will be changed to be: | + | |
- | + | ||
- | <code php> | + | |
- | bool is_callable ( callable $name [, bool $syntax_only = false [, bool $current_scope = false]]) | + | |
- | </ | + | |
- | + | ||
- | The `$current_scope` flag will allow users to test whether a parameter is actually invokable in the current scope, even if it is not a parameter that is universally callable. e.g. for private functions. | + | |
- | + | ||
- | <code php> | + | |
- | class Foo { | + | |
- | + | ||
- | private function bar() {} | + | |
- | + | ||
- | public function test($param) { | + | |
- | var_dump(is_callable($param)); | + | |
- | $syntaxOnly = false; | + | |
- | var_dump(is_callable($param, | + | |
- | } | + | |
- | } | + | |
- | + | ||
- | $foo = new Foo(); | + | |
- | $param = [$foo, ' | + | |
- | + | ||
- | $foo-> | + | |
- | //output will be: | + | |
- | //false | + | |
- | //true | + | |
- | + | ||
- | </ | + | |
- | + | ||
- | + | ||
- | To allow detection of code that uses the 3rd parameter to indicate the callable_name should be set, in the last PHP 7.x release we can deprecate (with a warning notice) that using the 3rd parameter is going to be removed in PHP 8. | + | |
- | + | ||
- | + | ||
- | ==== call_user_func equivalence to direct invocation ==== | + | |
- | + | ||
- | The changes in the rest of the RFC should make this goal be achieved. i.e. for any callable that is invokable via `call_user_func($callable); | + | |
- | + | ||
- | + | ||
- | ===== Target versions ===== | + | |
The various things that need to be done to implement this RFC do not need to be all in the same release. There are advantages to having the changes implemented in separate versions. Below is this list of all the changes needed and the target version for them. | The various things that need to be done to implement this RFC do not need to be all in the same release. There are advantages to having the changes implemented in separate versions. Below is this list of all the changes needed and the target version for them. | ||
- | ==== Add Scope Resolution Operator for self and parent - 7.1==== | ||
- | This is a useful thing to have (in a small set of circumstances) and there is no reason not to introduce it sooner rather than late. It will allow people to start migrating any code that currently uses " | + | ==== Add function is_callable_type - 7.4 ==== |
+ | ==== Add deprecation notices for self and parent usage in string based callable types e.g. ' | ||
- | ==== Soft-deprecate colon separated string callables | + | ==== Add deprecation notices for deprecation notices for self and parent usage in array based callable types e.g. array(' |
- | Soft-deprecate colon separated string callables (i.e. things like "classname:: | + | ==== Remove support for "self:: |
- | ==== Deprecate with notices colon separated string callables | + | ==== Remove support for self and parent names in |
- | Any usage of a colon separated string callable will generate a E_DEPRECATED notice in the place that they are used, i.e. either as a callable typehint for a param, call_user_func, | + | Change behaviour |
- | ==== Deprecate using third parameter | + | Change the behaviour to reflect the new set of things that are listed as callable above. This is a non-trivial change, and although it would be nice to have it sooner than PHP 8, I can't see any acceptable way to do it without making people angry. |
- | Any call to is_callable that has the 3rd parameter | + | ==== Change behaviour of ' |
- | ==== Remove colon separated string callables | + | Change the behaviour to reflect the new set of things that are listed as callable above. This is a non-trivial change, and although it would be nice to have it sooner than PHP 8, I can't see any acceptable way to do it without making people angry. |
- | Any attempt to use " | + | ===== BC breaks ===== |
- | ==== Remove support for " | + | All of the BC breaks are targeted at the PHP 8 release. None of the other changes should have any BC impact, other than the deprecated notices, which will allow people to migrate their code easily. |
- | Although this is covered by " | ||
- | ==== Change behaviour | + | 1. Although there are semantic changes to exactly what is a callable, I don't believe these would be that impactful, as the new semantics more closely reflect how people actual use callables. e.g. having a private method report as callable outside |
- | Change | + | 2. There may be code in the wild that relies on the dynamic meaning |
- | ==== Change behaviour of 'callable' | + | <code php> |
+ | 'self:: | ||
- | Change the behaviour to reflect the new set of things that are listed as callable above. This is a non-trivial | + | // change to |
- | ==== Change function signature of is_callable - 8 ==== | + | self::class . ':: |
+ | </ | ||
- | A comment has been made that changing the signature of this function could be a hard to manage BC break. The position of this RFC is that changing the signature at a major release shouldn' | ||
- | * no PHP programmer I have spoken to is even aware this function takes 3 parameters. | + | 3. Parent resolution |
- | * I have not been able to find any code that uses the 3rd parameter. | + | <code php> |
- | * As we are deprecating all code that uses callables like "self::methodName" | + | $callable = [FooParent::class, 'parent::bar']; |
- | ===== BC breaks ===== | + | // Would need to be replaced with: |
- | All of the BC breaks are targetted at the PHP 8 release. None of the other changes should have any BC impact, other than the deprecated notices, which will allow people to migrate their code easily. | + | call_user_func(array(get_parent_class(' |
+ | </code> | ||
- | 1. Any calls to is_callable that use the `callable_name` parameter would at least need to be wrapped in a version check against the PHP_MAJOR_VERSION number. I believe the amount of code that uses this parameter is minimal. | + | ===== Implementation ===== |
- | 2. Any usage of the double-colon separated string as a callable, would need to be changed to an array. | + | TBD |
- | 3. Although there are semantic changes to exactly what is a callable, I don't believe these would be that impactful, as the new semantics more closely reflect how people actual use callables. e.g. having a private method report as callable outside of the class where it is defined is just currently not a useful thing, and so I don't think many people will be dependent on that behaviour. | ||
- | 4. There may be code in the wild that relies on the dynamic meaning of ' | ||
- | ===== Implementation ===== | ||
- | |||
- | None yet. This could be a large amount of work, so it is appropriate to get feed back on the mailing list before starting this work. The actual change of definition of callable would be a relatively small amount of work. However the SPL libraries make use of colon separated strings (including " |
rfc/consistent_callables.txt · Last modified: 2021/10/20 13:18 by danack