タイトル: 可変変数
SEOタイトル: PHP 可変変数 (Variable Variables) 完全ガイド($$var の落とし穴と代替策)
| この記事の要点 |
|
可変変数とは
PHP の可変変数 (Variable Variables) は $$var という二重ドル記号で、「変数 $var の値を新しい変数名として使う」機能です。1990 年代から PHP に存在する古い文法。
<?php
$name = 'foo';
$$name = 'bar'; // 実質 $foo = 'bar';
echo $foo; // bar
echo $$name; // bar
echo ${$name}; // bar(明示的に書いた形)
// 「$$name」は「${$name}」の糖衣構文
動作の仕組み
<?php
$prefix = 'user';
// ${$prefix . '_id'} = 100;
// ↑ これは「$prefix の値 = 'user'」+ '_id' = 'user_id' を変数名として使う
// = $user_id = 100;
${$prefix . '_id'} = 100;
${$prefix . '_name'} = 'taro';
echo $user_id; // 100
echo $user_name; // taro
配列での利用(PHP 7 の挙動変更)
PHP 7 で式の評価順序が左から右に統一され、可変変数と配列の組み合わせは明示的な波括弧が必要になりました。
<?php
$arr = ['x' => 'hello'];
$name = 'arr';
// PHP 5: $$name['x'] → ${$name['x']} → $arr の x キー = 'hello' を変数名扱い
// PHP 7+: 左から評価 → $$name → $arr、その後 ['x']
echo ${$name}['x']; // 'hello' (明示的)
echo ${$name['x']}; // PHP 5/7 で挙動差。明示化が必須
// 解釈を明確にするため、PHP 7+ では波括弧を強く推奨
可変関数(Variable Functions)
可変変数の親戚として、変数に格納した関数名 / メソッド名で呼び出す機能があります。こちらは現代でもよく使われる正当な使い方です。
<?php
// 関数の動的呼び出し
$func = 'strtoupper';
echo $func('hello'); // HELLO
// オブジェクトメソッドの動的呼び出し
class Service {
public function run() { echo "running"; }
public function stop() { echo "stopped"; }
}
$obj = new Service();
$method = 'run';
$obj->$method(); // run() が呼ばれる
$obj->{$method}(); // 同じ(明示形)
// クラスメソッドの動的呼び出し
$class = 'Service';
$instance = new $class();
なぜ可変変数は非推奨か
| 問題 | 影響 |
|---|---|
| 可読性 | $$x のような変数がどこで定義されたか追えない |
| 静的解析 (PHPStan / Psalm) | 変数の型 / 存在を追跡不能 → 警告 or 解析停止 |
| IDE 補完 | 動的生成のため補完できない |
| セキュリティ | ユーザ入力を変数名にすると任意変数上書きに直結(過去の register_globals 問題) |
| パフォーマンス | OPcache 等の最適化が効きにくい |
| リファクタ | 変数名検索 / リネームが効かない |
代替策1: 連想配列を使う
動的命名がしたいなら連想配列で十分です。
<?php
// ❌ 古い可変変数パターン
foreach ($_POST as $key => $value) {
$$key = $value; // ⚠️ register_globals 相当。脆弱性の温床
}
echo $user_id; // どこから来た値か分からない
// ✅ 明示的な配列アクセス
$input = [];
foreach ($_POST as $key => $value) {
$input[$key] = $value;
}
echo $input['user_id']; // ★ 出所が明確
// ✅ もっとシンプル: $_POST をそのまま使う
echo $_POST['user_id'] ?? '';
代替策2: Reflection でプロパティ動的アクセス
<?php
class User {
public string $name = 'taro';
public int $age = 30;
}
$user = new User();
// ❌ 可変変数で「無理やり」
// $field = 'name';
// echo $user->$field; // 一応動くがダメ
// ✅ Reflection で意図を明示
$ref = new ReflectionObject($user);
$prop = $ref->getProperty('name');
echo $prop->getValue($user); // 'taro'
// ✅ プロパティ存在チェック
if (property_exists($user, 'name')) {
echo $user->{'name'}; // 同等だが意図明確
}
セキュリティ事故例
<?php
// ❌ 危険: 認証情報を上書きされる
$is_admin = false;
foreach ($_POST as $key => $value) {
$$key = $value; // ★ 攻撃者が is_admin=1 を POST で送ると...
}
if ($is_admin) {
grantAdminAccess(); // ★ 認証バイパス成功
}
// ✅ 安全: 明示的に必要なキーだけ取り出す
$name = $_POST['name'] ?? '';
$email = $_POST['email'] ?? '';
// $is_admin はリクエストから上書きできない
これは古い register_globals 問題と同根で、PHP 5.4 で register_globals が削除された理由でもあります。
許容できる用途(限定的)
- extract() の代替で、信頼できる連想配列を変数展開する場合(テンプレートエンジン内部など)
- テスト用のクイックハック(本番コードに残さない)
- レガシーコードを読み解く必要がある場合(書き換えはしない)
PHP 8 での挙動
可変変数は PHP 8 でも機能として残っていますが、エンジンレベルでの最適化対象外です。コード品質ツール(PHPStan level 5+ / Psalm)は警告を出します。
<?php
// PHP 8 でも動く
$x = 'hello';
$$x = 'world';
echo $hello; // world
// PHPStan で実行
// $ vendor/bin/phpstan analyse --level 5
// Variable $hello might not be defined.
extract() / compact() という関連機能
<?php
// extract: 配列キーを変数として展開(可変変数の親戚)
$data = ['name' => 'taro', 'age' => 30];
extract($data);
echo $name; // taro
echo $age; // 30
// ⚠️ extract($_POST) は厳禁。register_globals と同じ脆弱性
// ✅ EXTR_SKIP / EXTR_PREFIX_ALL で対策可
extract($_POST, EXTR_PREFIX_ALL, 'in');
echo $in_name; // in_ プレフィックス付きになる
// compact: 変数を配列にまとめる(逆操作)
$name = 'taro';
$age = 30;
$data = compact('name', 'age');
// ['name' => 'taro', 'age' => 30]
FAQ
Q: 可変変数を使っているレガシーコードを引き継いだ
A: まずは触らず動作を理解 → ユニットテストを書く → 連想配列に置き換え、というステップを推奨。一気に書き換えると挙動差で壊しやすい。
Q: $$x と ${$x} の違い?
A: 同じ意味です。${$x} が明示形でパース順が確定するため、複雑な式(配列・プロパティ絡み)では波括弧形を使うべき。
Q: 可変関数も非推奨?
A: 可変関数は OK です。コールバック / ストラテジーパターン / ルーティングで標準的に使われます。call_user_func よりも $callable() の方が速い。