面向對象開發中,判斷方法是否可調用是動態調用、插件系統、路由分發的基礎。method_exists() 和 is_callable() 看似相似,但在訪問控制、靜態方法、閉包支持上有本質區別。
// method_exists: 僅檢查類中是否聲明了方法
bool method_exists(object|string $object_or_class, string $method)
// is_callable: 檢查是否可在當前上下文調用
bool is_callable(callable $callback, bool $syntax_only = false)
/* $callback 可為:
- 'function_name'
- ['ClassName', 'staticMethod']
- [$object, 'method']
- [$object, 'method'] (私有/受保護需在類內部)
- 閉包函數
*/
| 場景 | method_exists() | is_callable() |
|---|---|---|
| 公共實例方法 | true | true |
| 受保護方法 | true | 類外 false,類內 true |
| 私有方法 | true | 類外 false,類內 true |
| 靜態方法 | true | true (['Class', 'method']) |
| 魔術方法 (__call) | false | true (若實現) |
| 不存在方法 | false | false |
| 閉包/可調用對象 | false | true |
| 語法檢查模式 | 不支持 | 支持 (第2參數 true) |
<?php
class Demo {
public function publicMethod() {}
protected function protectedMethod() {}
private function privateMethod() {}
public static function staticMethod() {}
public function __call($name, $args) { return "magic: $name"; }
}
$obj = new Demo();
// method_exists: 只看聲明
var_dump(method_exists($obj, 'publicMethod')); // true
var_dump(method_exists($obj, 'protectedMethod')); // true
var_dump(method_exists($obj, 'privateMethod')); // true
var_dump(method_exists($obj, 'staticMethod')); // true
var_dump(method_exists($obj, 'magicMethod')); // false (無聲明)
// is_callable: 看可訪問性
var_dump(is_callable([$obj, 'publicMethod'])); // true
var_dump(is_callable([$obj, 'protectedMethod'])); // false (類外)
var_dump(is_callable([$obj, 'privateMethod'])); // false (類外)
var_dump(is_callable([$obj, 'staticMethod'])); // true
var_dump(is_callable([$obj, 'magicMethod'])); // true (__call 生效)
// 類內部調用 (受保護/私有可調用)
class InnerTest extends Demo {
public function test() {
var_dump(is_callable([$this, 'protectedMethod'])); // true
var_dump(is_callable([$this, 'privateMethod'])); // true (同類)
}
}
// 靜態方法調用
var_dump(is_callable(['Demo', 'staticMethod'])); // true
var_dump(method_exists('Demo', 'staticMethod')); // true
// 閉包支持
$closure = fn() => 'hello';
var_dump(is_callable($closure)); // true
var_dump(method_exists($closure, '__invoke')); // true (Closure 類有 __invoke)
<?php
// 安全動態調用模式
function safe_call($object, string $method, array $args = []) {
// 1. 語法檢查
if (!is_callable([$object, $method], true)) {
throw new InvalidArgumentException("Invalid method name: $method");
}
// 2. 實際可調用性檢查
if (!is_callable([$object, $method])) {
throw new BadMethodCallException("Method not accessible: $method");
}
// 3. 調用
return $object->$method(...$args);
}
// 路由分發示例
class Router {
public function dispatch(string $controller, string $action) {
$ctrl = new $controller();
// 僅允許公共方法
if (!is_callable([$ctrl, $action])) {
throw new Exception("Action '$action' not accessible");
}
return $ctrl->$action();
}
}
<?php
// First-class callable syntax (PHP 8.1+)
$callable = $obj->publicMethod(...); // 自動綁定 $this
var_dump(is_callable($callable)); // true
// 類型聲明
function execute(callable $callback): mixed {
return $callback();
}
// Closure::fromCallable (PHP 7.1+)
$privateCaller = Closure::fromCallable([$obj, 'privateMethod']);
// $privateCaller() 仍會報錯 (綁定作用域為創建處)
method_exists()is_callable()ReflectionMethod::exists() + isPublic()is_callable(['Class', 'method']) 更語義化記住:
method_exists問「有沒有」,is_callable問「能不能用」。動態調用永遠用後者。