Skip to content

可变变量

可变变量(Variable Variables)允许使用一个变量的值作为另一个变量的名字。通过在变量名前加额外的 $ 符号,可以动态地访问和操作变量名。这是一种灵活但需要谨慎使用的 PHP 特性。

前置知识

基础概念

可变变量获取一个普通变量的值,并将该值作为另一个变量的名称。即变量的名字本身可以被动态地设置和使用。

基本语法

使用两个美元符号 $$ 来表示可变变量:

php
<?php
declare(strict_types=1);

// 普通变量
$a = "hello";

// 可变变量:用 $a 的值 "hello" 作为变量名
$$a = "world";

// 此时存在两个变量:
// $a 的值是 "hello"
// $hello 的值是 "world"

echo $a . "\n";      // 输出: hello
echo $hello . "\n";   // 输出: world
echo "$a {$$a}\n";   // 输出: hello world
echo "$a $hello\n";   // 输出: hello world(等价写法)

可变变量的使用

动态构建变量名

php
<?php
declare(strict_types=1);

// 使用花括号构建动态变量名
$prefix = "price";
$suffix = "For";
$day = "Monday";

// 构建 $priceForMonday
$fullVarName = "{$prefix}{$suffix}{$day}";
$$fullVarName = 29.99;

echo $priceForMonday . "\n"; // 29.99

// 模板式变量命名
$nameTypes = ["first", "last", "company"];
${"name_{$nameTypes[0]}"} = "John";       // $name_first
${"name_{$nameTypes[1]}"} = "Doe";        // $name_last
${"name_{$nameTypes[2]}"} = "Acme Corp";  // $name_company

echo $name_first . "\n";   // John
echo $name_last . "\n";    // Doe
echo $name_company . "\n"; // Acme Corp

多级可变变量

可变变量可以链式使用多个 $

php
<?php
declare(strict_types=1);

$bar = "a";
$foo = "bar";
$world = "foo";
$hello = "world";
$a = "Hello";

// 通过链式查找
echo $a . "\n";      // Hello
echo $$a . "\n";     // world($a 的值是 "Hello",但 $Hello 未定义)
// 修正示例
$b = "bar";
$foo = "bar";
$bar = "a";
$a = "Hello";

echo $a . "\n";      // Hello
echo $$a . "\n";     // 取决于 $Hello 是否定义

多级可变变量可读性差

超过两级的可变变量极其难以理解和调试,强烈不推荐使用

详细说明

可变变量与数组的歧义

当可变变量与数组一起使用时,解析器需要判断优先级。PHP 使用花括号语法来消除歧义:

php
<?php
declare(strict_types=1);

$a = "array";
$array = ["index0" => "value0"];
$array[1] = "value1";

// ${$a[1]} — 先计算 $a[1],再将结果作为变量名
// $a 是字符串 "array",$a[1] 是 "r"
// 所以等价于 $r(可能未定义)

// ${$a}[1] — 先计算 $a("array"),再取 $array[1]
echo ${$a}[1] . "\n"; // value1

// 使用花括号消除歧义
$b = "key";
${$b} = "dynamic value";
echo $key . "\n"; // dynamic value

可变属性

类的属性也可以通过可变属性名来访问:

php
<?php
declare(strict_types=1);

class User
{
    public string $name = "Alice";
    public string $email = "alice@example.com";
    public string $role = "admin";
    public array $preferences = ["theme" => "dark", "lang" => "zh"];

    public string $status = "active";
}

$user = new User();
$fieldName = "name";
echo $user->$fieldName . "\n"; // Alice

// 通过数组索引选择属性
$fields = ["name", "email", "role"];
foreach ($fields as $field) {
    echo "{$field}: {$user->$field}\n";
}

// 拼接属性名
$start = "pref";
$end = "erences";
echo $user->{$start . $end}["theme"] . "\n"; // dark

// 注意区分以下两种写法
$arr = "preferences";
echo $user->{$arr}["theme"] . "\n"; // dark($arr 作为属性名,再取索引)
echo $user->{$arr[1]} . "\n";        // e(先取 $arr[1] 即 "r",再取 $user->r)

可变方法调用

虽然不是严格意义上的可变变量,但可以通过变量名动态调用方法:

php
<?php
declare(strict_types=1);

class Mailer
{
    public function sendWelcome(string $email): string
    {
        return "Welcome email sent to {$email}";
    }

    public function sendReset(string $email): string
    {
        return "Reset email sent to {$email}";
    }

    public function sendNotify(string $email): string
    {
        return "Notification sent to {$email}";
    }
}

$mailer = new Mailer();
$email = "user@example.com";

// 动态方法调用
$method = "sendWelcome";
echo $mailer->$method($email) . "\n"; // Welcome email sent to user@example.com

// 通过条件选择方法
$type = "reset";
$methodMap = [
    "welcome" => "sendWelcome",
    "reset" => "sendReset",
    "notify" => "sendNotify",
];

if (isset($methodMap[$type])) {
    $methodName = $methodMap[$type];
    echo $mailer->$methodName($email) . "\n";
}

可变类实例化(PHP 5.3+)

可以通过变量来动态实例化类。注意在命名空间中需要使用完全限定名:

php
<?php
declare(strict_types=1);

class Validator
{
    public function validate(mixed $data): bool
    {
        return $data !== null;
    }
}

class Sanitizer
{
    public function sanitize(string $input): string
    {
        return htmlspecialchars($input, ENT_QUOTES, "UTF-8");
    }
}

// 动态实例化类
$className = "Validator";
$instance = new $className();
var_dump($instance->validate("test")); // bool(true)

$className = "Sanitizer";
$instance = new $className();
echo $instance->sanitize("<b>bold</b>") . "\n"; // &lt;b&gt;bold&lt;/b&gt;

命名空间注意

在命名空间中使用可变类名时,必须提供完整的完全限定类名(包含命名空间前缀),即使是同一个命名空间内的类也是如此。

实战示例

安全的白名单变量访问

php
<?php
declare(strict_types=1);

class SafeVariableAccessor
{
    private array $allowedVars = [
        "siteName",
        "siteUrl",
        "adminEmail",
        "maxUploadSize",
    ];

    private array $data = [
        "siteName" => "My Website",
        "siteUrl" => "https://example.com",
        "adminEmail" => "admin@example.com",
        "maxUploadSize" => 5242880,
    ];

    /**
     * 安全地获取变量值(白名单验证)
     */
    public function get(string $name): mixed
    {
        if (!in_array($name, $this->allowedVars, true)) {
            throw new InvalidArgumentException("Variable '{$name}' is not allowed");
        }

        return $this->data[$name] ?? null;
    }

    /**
     * 动态设置变量值(白名单验证)
     */
    public function set(string $name, mixed $value): void
    {
        if (!in_array($name, $this->allowedVars, true)) {
            throw new InvalidArgumentException("Variable '{$name}' is not allowed");
        }

        $this->data[$name] = $value;
    }
}

$accessor = new SafeVariableAccessor();
echo $accessor->get("siteName") . "\n"; // My Website

// 安全地使用可变属性
$field = "siteUrl";
echo $accessor->get($field) . "\n"; // https://example.com

动态配置映射

php
<?php
declare(strict_types=1);

class ConfigManager
{
    private array $configs = [];

    public function load(array $settings): void
    {
        foreach ($settings as $key => $value) {
            $this->configs[$key] = $value;
        }
    }

    /**
     * 使用可变属性动态访问配置
     */
    public function __get(string $name): mixed
    {
        return $this->configs[$name] ?? null;
    }
}

$config = new ConfigManager();
$config->load([
    "databaseHost" => "localhost",
    "databasePort" => 3306,
    "databaseName" => "myapp",
    "cacheDriver" => "redis",
    "cacheTtl" => 3600,
]);

// 动态访问配置
$group = "database";
echo $config->{"{$group}Host"} . "\n"; // localhost
echo $config->{"{$group}Port"} . "\n"; // 3306

$group = "cache";
echo $config->{"{$group}Driver"} . "\n"; // redis
echo $config->{"{$group}Ttl"} . "\n";    // 3600

注意事项

1. 超级全局变量不能用作可变变量

php
<?php
declare(strict_types=1);

// 以下代码是不合法的
// $superGlobal = "_POST";
// $$superGlobal; // 不能动态访问超级全局变量

2. $this 不能作为可变变量

$this 是特殊变量,不能被动态引用。

3. IDE 支持差

可变变量无法被 IDE 正确解析和自动补全,增加了调试和维护难度。重构(如重命名变量)时容易遗漏。

4. 安全风险

如果可变变量的名称来自用户输入,可能导致变量注入攻击:

php
<?php
declare(strict_types=1);

// 危险:用户可以读取任意变量
// $userInput = $_GET["var"];
// echo $$userInput; // 不安全!

// 安全:使用白名单
function safeGetVar(string $name, array $allowed, array $data): mixed
{
    if (in_array($name, $allowed, true)) {
        return $data[$name] ?? null;
    }
    return null;
}

最佳实践

  1. 避免使用可变变量:在现代 PHP 开发中,可变变量的使用场景越来越少
  2. 使用数组替代:当需要动态键值时,使用关联数组更安全、更清晰
  3. 使用白名单验证:如果必须使用可变变量,严格验证变量名
  4. 限制使用深度:最多使用一级可变变量 $$var,避免多级链式
  5. 添加注释说明:使用可变变量时,务必添加清晰的注释
  6. 考虑使用 __get() 魔术方法:类的动态属性访问用魔术方法更安全
  7. 优先使用数组和对象:动态数据结构应使用数组或类属性

下一节

学习了可变变量之后,接下来将了解 PHP 如何处理来自 HTTP 请求的外部变量,包括 GET/POST 参数、Cookie 数据和表单处理。请阅读 外部变量

参考链接