Skip to content

可变函数

概述

PHP 支持可变函数(Variable Functions)的概念:如果一个变量名后有圆括号 $var(),PHP 会寻找与变量的值同名的函数,并尝试执行它。这种机制使函数名可以动态确定,常用于回调模式、策略模式和插件系统等场景。

PHP 版本说明

可变函数是 PHP 的经典特性,自 PHP 4 起就存在。PHP 8.1 引入的 First-class 可调用语法 func(...) 提供了更现代的替代方案。PHP 8.2+ 对可变函数的安全使用有进一步改进。

基础概念

什么是可变函数

可变函数允许将函数名存储在变量中,然后通过该变量调用函数。PHP 会根据变量的值查找同名函数并执行。

php
$functionName = 'strlen';
echo $functionName('hello'); // 5

可变函数 vs 可变变量

  • 可变函数 $func():变量值为函数名,通过变量调用函数
  • 可变变量 $$var:变量值为另一个变量名,通过变量访问另一个变量

语法与代码

基本用法

php
<?php

declare(strict_types=1);

// 将函数名赋给变量
$func = 'strtoupper';
echo $func('hello world') . "\n"; // HELLO WORLD

// 动态选择函数
$operation = 'add';

function add(int $a, int $b): int
{
    return $a + $b;
}

function subtract(int $a, int $b): int
{
    return $a - $b;
}

$result = $operation(10, 5);
echo $result . "\n"; // 15

// 使用变量决定调用哪个函数
$format = 'json';

if ($format === 'json') {
    $encoder = 'json_encode';
} else {
    $encoder = 'serialize';
}

echo $encoder(['key' => 'value']) . "\n"; // {"key":"value"}

安全使用:is_callable 检查

在调用可变函数之前,应该使用 is_callable() 检查变量值是否确实可调用。

php
<?php

declare(strict_types=1);

function safeCall(string $functionName, mixed ...$args): mixed
{
    if (!is_callable($functionName)) {
        throw new BadFunctionCallException("Function '{$functionName}' is not callable");
    }

    return $functionName(...$args);
}

echo safeCall('strtoupper', 'hello') . "\n"; // HELLO

try {
    safeCall('nonexistent_function', 'test');
} catch (BadFunctionCallException $e) {
    echo $e->getMessage() . "\n";
    // Function 'nonexistent_function' is not callable
}

// is_callable 的不同用法
var_dump(is_callable('strlen'));         // true
var_dump(is_callable('nonexistent'));    // false
var_dump(is_callable(fn() => true));    // true
var_dump(is_callable(['stdClass', 'foo'])); // false

与 call_user_func 的区别

可变函数 $func()call_user_func($func) 都可以实现动态调用,但有细微差别。

php
<?php

declare(strict_types=1);

// 可变函数
$func = 'strtoupper';
echo $func('hello') . "\n"; // HELLO

// call_user_func(旧方式)
echo call_user_func('strtoupper', 'hello') . "\n"; // HELLO

// call_user_func_array(参数以数组传递)
echo call_user_func_array('substr', ['hello world', 0, 5]) . "\n"; // hello

// 现代替代方案:参数展开
$substr = substr(...);
echo $substr('hello world', 0, 5) . "\n"; // hello
特性可变函数 $func()call_user_func
语法简洁度更简洁较冗长
可读性
性能更好略差(函数调用开销)
参数传递直接传参需要分别传递
作用域直接创建新作用域
推荐程度推荐(PHP 7+)不推荐(旧式)

方法可变调用

可变函数也可以用于调用对象方法,使用数组语法 $obj->$method()[$obj, $method]()

php
<?php

declare(strict_types=1);

class Calculator
{
    public function add(int $a, int $b): int
    {
        return $a + $b;
    }

    public function multiply(int $a, int $b): int
    {
        return $a * $b;
    }

    public function power(int $base, int $exp): int|float
    {
        return $base ** $exp;
    }
}

$calc = new Calculator();
$method = 'add';
echo $calc->$method(3, 4) . "\n"; // 7

$method = 'multiply';
echo $calc->$method(3, 4) . "\n"; // 12

// 使用数组语法的可变调用
$methods = ['add', 'multiply', 'power'];

foreach ($methods as $m) {
    if (is_callable([$calc, $m])) {
        echo "{$m}(2, 3) = " . $calc->$m(2, 3) . "\n";
    }
}
// add(2, 3) = 5
// multiply(2, 3) = 6
// power(2, 3) = 8

静态方法可变调用

php
<?php

declare(strict_types=1);

class MathHelper
{
    public static function max(int $a, int $b): int
    {
        return max($a, $b);
    }

    public static function min(int $a, int $b): int
    {
        return min($a, $b);
    }

    public static function abs(int $n): int
    {
        return abs($n);
    }
}

// 使用类名和方法名的可变调用
$method = 'max';
echo MathHelper::$method(3, 7) . "\n"; // 7

// 使用字符串动态调用
$class = 'MathHelper';
$method = 'min';
echo $class::$method(3, 7) . "\n"; // 3

// 使用回调语法
$callable = [MathHelper::class, 'abs'];
echo $callable(-42) . "\n"; // 42

详细说明

安全风险

可变函数存在代码注入风险。如果函数名来自用户输入,攻击者可能调用任意函数。

php
<?php

declare(strict_types=1);

// 危险:用户可以调用任意函数
// $userInput = 'system';
// $userInput('rm -rf /'); // 灾难性后果!

// 安全方式:使用白名单
$allowedFunctions = ['strtoupper', 'strtolower', 'trim', 'ucfirst'];

function safeExecute(string $funcName, string $input): string
{
    if (!in_array($funcName, $allowedFunctions, true)) {
        throw new BadFunctionCallException("Function '{$funcName}' is not allowed");
    }

    if (!is_callable($funcName)) {
        throw new BadFunctionCallException("Function '{$funcName}' does not exist");
    }

    return $funcName($input);
}

echo safeExecute('strtoupper', 'hello') . "\n"; // HELLO

try {
    safeExecute('system', 'ls');
} catch (BadFunctionCallException $e) {
    echo $e->getMessage() . "\n";
}

可变函数与语言结构

可变函数不能用于语言结构(如 echoissetemptydieeval 等),因为它们不是函数。

php
<?php

declare(strict_types=1);

// 语言结构不能用作可变函数
// $func = 'echo';
// $func('hello'); // Fatal error

// $func = 'isset';
// $func($var);    // Fatal error

// 但可以用封装函数来间接实现
function issetSafe(mixed &$var): bool
{
    return isset($var);
}

$check = 'issetSafe';
$val = 42;
echo $check($val) ? 'set' : 'not set'; // set

实战示例

策略模式

php
<?php

declare(strict_types=1);

class PricingStrategy
{
    private const STRATEGIES = [
        'standard' => 'calculateStandard',
        'premium' => 'calculatePremium',
        'wholesale' => 'calculateWholesale',
    ];

    private function calculateStandard(float $price, int $quantity): float
    {
        return $price * $quantity;
    }

    private function calculatePremium(float $price, int $quantity): float
    {
        $subtotal = $price * $quantity;
        $discount = $subtotal * 0.1;

        return $subtotal - $discount;
    }

    private function calculateWholesale(float $price, int $quantity): float
    {
        if ($quantity >= 100) {
            return $price * $quantity * 0.7;
        }

        return $price * $quantity * 0.9;
    }

    public function calculate(string $type, float $price, int $quantity): float
    {
        $method = self::STRATEGIES[$type] ?? null;

        if ($method === null || !is_callable([$this, $method])) {
            throw new InvalidArgumentException("Unknown pricing strategy: {$type}");
        }

        return $this->$method($price, $quantity);
    }
}

$pricing = new PricingStrategy();

echo $pricing->calculate('standard', 10.0, 5) . "\n";   // 50
echo $pricing->calculate('premium', 10.0, 5) . "\n";      // 45
echo $pricing->calculate('wholesale', 10.0, 100) . "\n"; // 700

命令调度器

php
<?php

declare(strict_types=1);

class CommandDispatcher
{
    /** @var array<string, callable> */
    private array $commands = [];

    public function register(string $name, callable $handler): void
    {
        if (!is_callable($handler)) {
            throw new InvalidArgumentException("Handler for '{$name}' is not callable");
        }

        $this->commands[$name] = $handler;
    }

    public function dispatch(string $name, array $payload = []): mixed
    {
        if (!isset($this->commands[$name])) {
            throw new RuntimeException("Command '{$name}' not found");
        }

        return ($this->commands[$name])(...$payload);
    }

    public function hasCommand(string $name): bool
    {
        return isset($this->commands[$name]);
    }
}

$dispatcher = new CommandDispatcher();

// 注册命令处理器
$dispatcher->register('greet', fn(string $name): string => "Hello, {$name}!");
$dispatcher->register('add', fn(int $a, int $b): int => $a + $b);
$dispatcher->register('timestamp', fn(): string => date('Y-m-d H:i:s'));

// 调度命令
echo $dispatcher->dispatch('greet', ['Alice']) . "\n";
echo $dispatcher->dispatch('add', [10, 20]) . "\n";
echo $dispatcher->dispatch('timestamp') . "\n";

注意事项

性能考虑

可变函数的调用性能略低于直接函数调用,因为 PHP 需要先解析函数名。但在实际应用中,这种差异通常可以忽略。

php
<?php

declare(strict_types=1);

// 直接调用(最快)
echo strtoupper('hello') . "\n";

// 可变函数(略慢)
$func = 'strtoupper';
echo $func('hello') . "\n";

// call_user_func(最慢)
echo call_user_func('strtoupper', 'hello') . "\n";

调试困难

可变函数的动态特性可能导致调试困难。在 IDE 中很难追踪函数调用链。

php
<?php

declare(strict_types=1);

// 调试时很难知道 $func 指向哪个函数
$func = getRandomFunction();
$result = $func($data); // IDE 无法静态分析

// 缓解方案:添加注释和日志
/** @var callable(string): string 当前使用的是 strtoupper 策略 */
$func = 'strtoupper';
$result = $func($data);

最佳实践

1. 优先使用 First-class 可调用语法(PHP 8.1+)

php
<?php

declare(strict_types=1);

// PHP 8.1+ 推荐
$mapper = array_map(strtoupper(...), $data);

// 旧式可变函数
$func = 'strtoupper';
$mapper = array_map($func, $data);

2. 始终进行可调用检查

php
<?php

declare(strict_types=1);

function callSafely(callable $callback, mixed ...$args): mixed
{
    return $callback(...$args);
}

// 类型声明 callable 会在调用时自动检查
echo callSafely('strtoupper', 'hello') . "\n"; // HELLO

// 传入不可调用的值会在运行时抛出 TypeError
// callSafely('nonexistent', 'hello'); // TypeError

3. 使用白名单限制可调用的函数

php
<?php

declare(strict_types=1);

class SafeInvoker
{
    /** @var array<string, callable> */
    private array $allowedFunctions;

    public function __construct(array $allowedFunctions)
    {
        $this->allowedFunctions = array_filter(
            $allowedFunctions,
            fn(string $f): bool => is_callable($f)
        );
    }

    public function call(string $functionName, mixed ...$args): mixed
    {
        if (!isset($this->allowedFunctions[$functionName])) {
            throw new BadFunctionCallException("Function '{$functionName}' is not allowed");
        }

        return $this->allowedFunctions[$functionName](...$args);
    }
}

$invoker = new SafeInvoker([
    'upper' => strtoupper(...),
    'lower' => strtolower(...),
    'trim' => trim(...),
    'length' => strlen(...),
]);

echo $invoker->call('upper', 'hello') . "\n"; // HELLO
echo $invoker->call('length', 'hello') . "\n"; // 5

参考链接