compactとextractの功罪、便利さと引き換えに変数の出所が消える
compactは変数名から連想配列を作り、extractは連想配列を変数として展開する関数です。便利な反面、変数の出所が消えてgrepや静的解析が効きにくくなります。
compactは変数名から配列を作る
compact は変数名を渡すと、その名前をキーにした連想配列を作ってくれる関数です。テンプレートにデータを渡すときなどにタイプ量が減って便利なんですが、便利さの裏で失うものもあると感じています。まず動きを確認します。
$title = '記事一覧';
$posts = $repository->all();
$page = 1;
return $this->render('index', compact('title', 'posts', 'page'));
// ['title' => $title, 'posts' => $posts, 'page' => $page] と同じ
キー名と変数名が必ず一致するので、配列リテラルで書くときの ‘title’ => $title のような繰り返しが消えるわけですね。ここまでは素直に便利です。
extractは配列から変数を撒き散らす
逆向きが extract で、連想配列のキーを変数として現在のスコープに展開します。これがなかなか危険で、どこで定義されたか分からない変数が突然使えるようになるので、コードを読む人がその出所を追えなくなります。外部入力を extract すると、既存の変数を上書きされる脆弱性にもつながりますね。
$data = ['name' => 'A', 'isAdmin' => false];
extract($data);
echo $name; // どこで定義された? が読み手に見えない
// もし $data が外部由来だと isAdmin を差し込まれる恐れも
extract($_GET); // これは特に危険、まずやらない
静的解析とgrepが効かなくなるのが痛い
個人的に一番の難点だと思うのが、これらを使うと変数の追跡が難しくなる点です。compact(‘user’) で渡された user を、受け手のテンプレートで grep しても定義が見つからない。extract で撒かれた変数も宣言箇所がないので、IDEの「定義へジャンプ」が空振りします。静的解析ツールも未定義変数と誤検知しがちで、結局は明示的に書いた方が読み手にも機械にも優しいことが多いです。
// 追いやすい書き方。キーと出所が一目で分かる
return $this->render('index', [
'title' => $title,
'posts' => $posts,
'page' => $page,
]);
まとめ
compact はタイプ量を減らす分には許容範囲だと思いますが、extract は変数の出所を消してしまうので、外部入力に対しては特に避けたいところですね。少し冗長でも配列リテラルで明示的に書くと、grepも静的解析も素直に効いて、後から読む人が楽になります。手数の短さと追跡しやすさを天秤にかけると、私は後者を選ぶことが多い気がします。
よくある質問
Q. compactは使ってもいいですか?
A. タイプ量を減らす分には許容範囲だと思います。ただしキーと変数名が一致するので、追跡しづらくなる点は意識しておきたいです。
Q. extractはなぜ危険なのですか?
A. 変数の出所が読み手に見えなくなり、外部入力を展開すると既存変数を上書きされる脆弱性にもつながるからです。
Q. 明示的に書く利点は?
A. 配列リテラルで書くとキーと出所が一目で分かり、grepも静的解析も素直に効いて、後から読む人が楽になります。