PHP Conference Japan 2024

回调/可调用

回调可以使用 callable 类型声明来表示。

一些函数,例如 call_user_func()usort() 接受用户定义的回调函数作为参数。回调函数不仅可以是简单的函数,还可以是 object 方法,包括静态类方法。

传递

PHP 函数以其名称作为 string 传递。任何内置或用户定义的函数都可以使用,除了语言结构,例如:array()echoempty()eval()exit()isset()list()printunset()

已实例化的 object 的方法作为包含索引 0 处为 object 和索引 1 处为方法名称的 array 传递。允许在类内部访问受保护和私有方法。

静态类方法也可以在不实例化该类的 object 的情况下传递,方法是在索引 0 处传递类名而不是 object,或者传递 'ClassName::methodName'

除了常见的用户定义函数外,匿名函数箭头函数 也可以传递到回调参数。

注意:

从 PHP 8.1.0 开始,还可以使用 一等可调用语法 创建匿名函数。

通常,任何实现 __invoke() 的对象也可以传递到回调参数。

示例 #1 回调函数示例

<?php

// 回调函数示例
function my_callback_function() {
echo
'hello world!';
}

// 回调方法示例
class MyClass {
static function
myCallbackMethod() {
echo
'Hello World!';
}
}

// 类型 1:简单回调
call_user_func('my_callback_function');

// 类型 2:静态类方法调用
call_user_func(array('MyClass', 'myCallbackMethod'));

// 类型 3:对象方法调用
$obj = new MyClass();
call_user_func(array($obj, 'myCallbackMethod'));

// 类型 4:静态类方法调用
call_user_func('MyClass::myCallbackMethod');

// 类型 5:相对静态类方法调用
class A {
public static function
who() {
echo
"A\n";
}
}

class
B extends A {
public static function
who() {
echo
"B\n";
}
}

call_user_func(array('B', 'parent::who')); // A, 已弃用,自 PHP 8.2.0 起

// 类型 6:实现 __invoke 的对象可以用作可调用对象
class C {
public function
__invoke($name) {
echo
'Hello ', $name, "\n";
}
}

$c = new C();
call_user_func($c, 'PHP!');
?>

示例 #2 使用闭包的回调示例

<?php
// 我们的闭包
$double = function($a) {
return
$a * 2;
};

// 这是我们的数字范围
$numbers = range(1, 5);

// 在这里使用闭包作为回调
// 将我们范围中的每个元素的大小加倍
//
$new_numbers = array_map($double, $numbers);

print
implode(' ', $new_numbers);
?>

以上示例将输出

2 4 6 8 10

注意:

如果在之前的回调中抛出了未捕获的异常,则使用 call_user_func()call_user_func_array() 等函数注册的回调将不会被调用。

添加注释

用户贡献的注释 12 条注释

andrewbessa at gmail dot com
12 年前
您还可以使用 $this 变量来指定回调



<?php
MyClass {

public
$property = 'Hello World!';

public function
MyMethod()
{
call_user_func(array($this, 'myCallbackMethod'));
}

public function
MyCallbackMethod()
{
echo
$this->property;
}

}
?>
computrius at gmail dot com
11 年前
当使用数组表示法指定回调时(即 array($this, "myfunc")),如果从类内部调用,则方法可以是私有的,但如果从外部调用,则会收到警告。

<?php

class mc {
public function
go(array $arr) {
array_walk($arr, array($this, "walkIt"));
}

private function
walkIt($val) {
echo
$val . "<br />";
}

public function
export() {
return array(
$this, 'walkIt');
}
}

$data = array(1,2,3,4);

$m = new mc;
$m->go($data); // 有效

array_walk($data, $m->export()); // 将生成警告

?>

输出
1<br />2<br />3<br />4<br />
警告:array_walk() 期望参数 2 为有效的回调,无法访问 /in/tfh7f 第 22 行的私有方法 mc::walkIt()
steve at mrclay dot org
12 年前
性能说明:类似于 is_callable() 的可调用类型提示,如果值看起来像静态方法回调,将触发类的自动加载。
Riikka K
9 年前
关于在不使用 call_user_func() 的情况下(例如“<?php $callback = 'printf'; $callback('Hello World!') ?>”)将回调作为“变量函数”调用时的差异说明。

- 将函数名作为字符串使用至少从 4.3.0 版本开始就已支持。
- 调用匿名函数和可调用对象至少从 5.3.0 版本开始就已支持。
- 使用数组结构 [$object, 'method'] 至少从 5.4.0 版本开始就已支持。

但是,请注意,即使 call_user_func() 支持以下情况,在将回调作为变量函数调用时也不支持它们。

- 通过字符串(如“foo::doStuff”)调用静态类方法。
- 使用 [$object, 'parent::method'] 数组结构调用父方法。

但是,“可调用”类型提示会正确识别所有这些情况作为回调。因此,以下代码将产生错误“致命错误:在 /tmp/code.php 第 4 行调用未定义函数 foo::doStuff()”。

<?php
foo {
static function
callIt(callable $callback) {
$callback();
}

static function
doStuff() {
echo
"Hello World!";
}
}

foo::callIt('foo::doStuff');
?>

如果我们将“$callback()”替换为“call_user_func($callback)”或使用数组 ['foo', 'doStuff'] 作为回调,则代码将正常工作。
edanschwartz at gmail dot com
9 年前
您可以使用“self::methodName”作为可调用项,但这很危险。请考虑以下示例

<?php
Foo {
public static function
doAwesomeThings() {
FunctionCaller::callIt('self::someAwesomeMethod');
}

public static function
someAwesomeMethod() {
// 这里包含很棒的代码。
}
}

class
FunctionCaller {
public static function
callIt(callable $func) {
call_user_func($func);
}
}

Foo::doAwesomeThings();
?>

这将导致错误
警告:类“FunctionCaller”没有方法“someAwesomeMethod”。

因此,您应该始终使用完整的类名
<?php
FunctionCaller
::callIt('Foo::someAwesomeMethod');
?>

我认为这是因为 FunctionCaller 无法知道字符串“self”在某一时刻指的是`Foo`。
metamarkers at gmail dot com
11 年前
如果对象的类定义了 __invoke() 魔术方法,则可以将对象作为可调用项传递。
mariano dot REMOVE dot perez dot rodriguez at gmail dot com
9 年前
我需要一个函数来确定传递的可调用项的类型,并最终
对其进行某种程度的规范化。这是我想出的方法

<?php

/**
* 下表给出了可调用类型和规范化方式:
*
* 可调用 | 规范化 | 类型
* ---------------------------------+---------------------------------+--------------
* function (...) use (...) {...} | function (...) use (...) {...} | 'closure'
* $object | $object | 'invocable'
* "function" | "function" | 'function'
* "class::method" | ["class", "method"] | 'static'
* ["class", "parent::method"] | ["parent of class", "method"] | 'static'
* ["class", "self::method"] | ["class", "method"] | 'static'
* ["class", "method"] | ["class", "method"] | 'static'
* [$object, "parent::method"] | [$object, "parent::method"] | 'object'
* [$object, "self::method"] | [$object, "method"] | 'object'
* [$object, "method"] | [$object, "method"] | 'object'
* ---------------------------------+---------------------------------+--------------
* 其他可调用 | 同上 | 'unknown'
* ---------------------------------+---------------------------------+--------------
* 不是可调用 | null | false
*
* 如果 "strict" 参数设置为 true,则会执行其他检查,特别是:
* - 当给出 "class::method" 形式的可调用字符串或 ["class", "method"] 形式的可调用数组时,该方法必须是静态方法,
* - 当给出 [$object, "method"] 形式的可调用数组时,该方法必须是非静态方法。
*
*/
function callableType($callable, $strict = true, callable& $norm = null) {
if (!
is_callable($callable)) {
switch (
true) {
case
is_object($callable):
$norm = $callable;
return
'Closure' === get_class($callable) ? 'closure' : 'invocable';
case
is_string($callable):
$m = null;
if (
preg_match('~^(?<class>[a-z_][a-z0-9_]*)::(?<method>[a-z_][a-z0-9_]*)$~i', $callable, $m)) {
list(
$left, $right) = [$m['class'], $m['method']];
if (!
$strict || (new \ReflectionMethod($left, $right))->isStatic()) {
$norm = [$left, $right];
return
'static';
}
} else {
$norm = $callable;
return
'function';
}
break;
case
is_array($callable):
$m = null;
if (
preg_match('~^(:?(?<reference>self|parent)::)?(?<method>[a-z_][a-z0-9_]*)$~i', $callable[1], $m)) {
if (
is_string($callable[0])) {
if (
'parent' === strtolower($m['reference'])) {
list(
$left, $right) = [get_parent_class($callable[0]), $m['method']];
} else {
list(
$left, $right) = [$callable[0], $m['method']];
}
if (!
$strict || (new \ReflectionMethod($left, $right))->isStatic()) {
$norm = [$left, $right];
return
'static';
}
} else {
if (
'self' === strtolower($m['reference'])) {
list(
$left, $right) = [$callable[0], $m['method']];
} else {
list(
$left, $right) = $callable;
}
if (!
$strict || !(new \ReflectionMethod($left, $right))->isStatic()) {
$norm = [$left, $right];
return
'object';
}
}
}
break;
}
$norm = $callable;
return
'unknown';
}
$norm = null;
return
false;
}

?>

希望其他人也能发现它有用。
InvisibleSmiley
3 年前
如果您将可调用方法传递给具有可调用类型声明的函数,则错误消息具有误导性

<?php
class X {
protected function
foo(): void {}
}

function
bar(callable $c) {}

$x = new X;
$c = [$x, 'foo'];
bar($c);
?>

错误消息将类似于“参数 #1 ($c) 必须是可调用类型,但给定数组”,而这里实际的问题仅仅是方法“foo”的可见性。您只需要将其更改为 public(或使用其他方法,例如使用闭包)。
bradyn at NOSPAM dot bradynpoulsen dot com
8 年前
当尝试从命名空间中的函数名创建可调用对象时,您**必须**提供完全限定的函数名(无论当前命名空间或 use 语句如何)。

<?php

namespace MyNamespace;

function
doSomethingFancy($arg1)
{
// 执行某些操作...
}

$values = [1, 2, 3];

array_map('doSomethingFancy', $values);
// array_map() 期望参数 1 为有效的回调,未找到函数 'doSomethingFancy' 或函数名无效

array_map('MyNamespace\doSomethingFancy', $values);
// => [..., ..., ...]
gulaschsuppe2 at gmail dot com
5 年前
我尝试了许多在 3v4l 上直接通过函数名调用函数并将其分配给变量的可能方法。尚未提及的是,可以使用数组作为调用者,至少从 PHP 7.1.25 开始。以下脚本包含我获得的所有信息

<?php

// 通过函数名调用函数:
// 基本知识:
// 也可以使用函数的字符串名称来调用函数:
function callbackFunc() {
echo
'Hello World';
}

'callbackFunc'(); // Hello World

// 如果将函数名分配给变量,则也可以调用函数:
function callbackFunc() {
echo
'Hello World';
}

$funcName = 'callbackFunc';
$funcName(); // Hello World

// 静态类方法:
// 也可以使用 'ClassName::functioName' 表示法来调用公共静态类方法:
class A {
public static function
callbackMethod() {
echo
"Hello World\n";
}
}
'A::callbackMethod'(); // Hello World

$funcName = 'A::callbackMethod';
$funcName(); // Hello World

// 非静态类方法:
// 还可以通过创建一个数组来调用非静态类方法,该数组的第一个元素是应该在其上调用方法的对象,第二个元素是要调用的非静态方法。该数组可以直接用作调用者:
class A {
private
$prop = "Hello World\n";

public function
callbackMethod() {
echo
$this->prop;
}
}

$a = new A;
[
$a, 'callbackMethod']();
$funcCallArr = [$a, 'callbackMethod'];
$funcCallArr();

// 当然,这在类中也可以与 '$this' 一起使用:
class A {
private function
privCallback() {
echo
'Private';
}

public function
privCallbackCaller($funcName) {
[
$this, $funcName]();
}
}

(new
A)->privCallbackCaller('privCallback'); // Private

?>
pawel dot tadeusz dot niedzielski at gmail dot com
8 年前
@edanschwartz at gmail dot com

可以使用 ::class 属性在使用静态方法时始终指示您所在的类。

<?php
class Foo {
public static function
doAwesomeThings() {
FunctionCaller::callIt(self::class . '::someAwesomeMethod');
}

public static function
someAwesomeMethod() {
// 优秀的代码在此处。
}
}

class
FunctionCaller {
public static function
callIt(callable $func) {
call_user_func($func);
}
}

Foo::doAwesomeThings();
?>
chris dot rutledge at gmail dot com
5 年前
在阅读了上面手册中的这行内容后,

"实例化对象的某个方法以包含对象索引 0 和方法名称索引 1 的数组形式传递。允许从类内部访问受保护和私有方法。"

我决定进行一些测试,看看是否可以使用 call_user_func 方法访问私有方法。谢天谢地,不行,但为了完整起见,这里是我的测试,也涵盖了使用静态和对象上下文。

<?php
class foo {

public static
$isInstance = false;

public function
__construct() {
self::$isInstance = true;
}

public function
bar() {
var_dump(self::$isInstance);
echo
__METHOD__;
}

private function
baz() {
var_dump(self::$isInstance);
echo
__METHOD__;
}

public function
qux() {
$this->baz();
}

public function
quux() {
self::baz();
}
}

call_user_func(['foo','bar']); //false, foo:bar

call_user_func(['foo','baz']); //警告,无法访问私有方法

call_user_func(['foo','quux']); //false, foo::baz

call_user_func(['foo','qux']); //致命错误,在非对象上下文中使用 $this

$foo = new foo;

call_user_func([$foo,'bar']); //true, foo::bar
call_user_func([$foo,'baz']); //警告,无法访问私有方法
call_user_func([$foo,'qux']); //true, foo::baz

call_user_func(['foo','bar']); //true, foo::bar(静态调用,但 $isInstance 为 true)

?>
To Top