PHP Conference Japan 2024

回呼/可呼叫

回呼可以用 callable 類型宣告來表示。

一些函式,例如 call_user_func()usort(),接受使用者定義的回呼函式作為參數。回呼函式不僅可以是簡單的函式,也可以是 物件 方法,包括靜態類別方法。

傳遞

PHP 函式是以其名稱作為 字串 傳遞。任何內建或使用者定義的函式都可以使用,除了語言結構,例如:array()echoempty()eval()exit()isset()list()printunset()

實例化 物件 的方法會以一個 陣列 傳遞,該陣列在索引 0 處包含一個 物件,在索引 1 處包含方法名稱。允許從類別內部存取 protected 和 private 方法。

靜態類別方法也可以在不實例化該類別的 物件 的情況下傳遞,方法是在索引 0 處傳遞類別名稱而不是 物件,或者傳遞 '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 使用 Closure 的回呼範例

<?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
class 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

類別 mc {
公開 函式
go(陣列 $arr) {
array_walk($arr, 陣列($this, "walkIt"));
}

私有 函式
walkIt($val) {
顯示
$val . "<br />";
}

公開 函式
export() {
返回 陣列(
$this, 'walkIt');
}
}

$data = 陣列(1,2,3,4);

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

array_walk($data, $m->export()); // 會產生警告

?>

輸出
1<br />2<br />3<br />4<br />
警告:array_walk() 預期參數 2 為有效回呼函式,無法存取 mc::walkIt() 私有方法,位於 /in/tfh7f,第 22 行
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 {
靜態函數
callIt(callable $callback) {
$callback();
}

靜態函數
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 {
公開靜態函數
doAwesomeThings() {
FunctionCaller::callIt('self::someAwesomeMethod');
}

公開靜態函數
someAwesomeMethod() {
// 멋진 코드가 여기에 있습니다.
}
}

類別
FunctionCaller {
公開靜態函數
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

/**
* The callable types and normalizations are given in the table below:
*
* Callable | Normalization | Type
* ---------------------------------+---------------------------------+--------------
* 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'
* ---------------------------------+---------------------------------+--------------
* other callable | idem | 'unknown'
* ---------------------------------+---------------------------------+--------------
* not a callable | null | false
*
* If the "strict" parameter is set to true, additional checks are
* performed, in particular:
* - when a callable string of the form "class::method" or a callable array
* of the form ["class", "method"] is given, the method must be a static one,
* - when a callable array of the form [$object, "method"] is given, the
* method must be a non-static one.
*
*/
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
類別 X {
保護函數
foo(): void {}
}

函數
bar(callable $c) {}

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

錯誤訊息會類似「Argument #1 ($c) must be of type callable, array given」,但實際問題只是方法「foo」的可見度。您只需要將其改為公開 (或使用其他方法,例如 Closure)。
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
類別 Foo {
公開 靜態 函式
doAwesomeThings() {
FunctionCaller::callIt(self::class . '::someAwesomeMethod');
}

公開 靜態 函式
someAwesomeMethod() {
// 精彩的程式碼寫在這裡。
}
}

類別
FunctionCaller {
公開 靜態 函式
callIt(callable $func) {
call_user_func($func);
}
}

Foo::doAwesomeThings();
?>
chris dot rutledge at gmail dot com
5 年前
閱讀完手冊中的這行說明後:

「實例化物件的方法會以陣列形式傳遞,索引 0 為物件,索引 1 為方法名稱。允許從類別內部存取受保護和私有方法。」

我決定進行一些測試,看看是否可以使用 call_user_func 方法存取私有方法。幸好不行,但為了完整性,這裡是我的測試,也涵蓋了使用靜態和物件上下文的情況。

<?php
類別 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