0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHPの$argvがWeb経由で消える — register_argc_argvと、事故を無音にする ?? [] の一行

0
Posted at

TL;DR

  • $argv は CLI では必ず存在するが、php-fpm / mod_php 経由では register_argc_argv が Off だと存在しない
  • 配布される php.ini-production / php.ini-development はどちらも Off。つまり既定でこうなる
  • $argv ?? [] のような「未定義を黙って吸収する書き方」を挟むと、TypeError も警告も出ずに else へ落ちる
  • 直すべきは設定ではなく既定値の向き。オプションが無いとき何もしない側に倒す
  • register_argc_argv を On にするとクエリ文字列が $argv に流入するので、対処として選ばない

起きたこと

CLI 実行を前提に書いたバッチスクリプトを、ブラウザ経由の入口から呼び出した。渡したはずの絞り込みオプションが1つも認識されず、「指定なし」と判定され、全件を処理する側の分岐が走った。

例外なし。警告なし。ログも空。失敗したという事実そのものが、どこにも出力されなかった。

再現する

最小限の再現コードを置く。

check.php
<?php
echo 'PHP_SAPI = ', PHP_SAPI, PHP_EOL;
echo 'register_argc_argv = ', var_export(ini_get('register_argc_argv'), true), PHP_EOL;
var_dump(isset($argv));

これを両方の入口から叩く。

# CLI
php check.php --foo

# Web(php-fpm など)
curl -s 'https://example.com/check.php?foo=1'

CLI 側は PHP_SAPI = cli / isset($argv) => bool(true)。Web 側は fpm-fcgi かつ bool(false) になるはずだ。

ここで注意したいのは、CLI と Web が読む php.ini は別ファイルであることが多いという点。php -i | grep register_argc_argv だけで判断すると、Web 側の値を見ていないまま結論を出すことになる。必ず両方から叩いて並べる。

なぜ無音になるのか

事故ったコードはこうだった。

$targetOnly = in_array('--target-only', $argv ?? [], true);

if ($targetOnly) {
    processTargets();          // 軽い
} else {
    processAll();              // 重い・副作用が大きい
}

$argv ?? [] は未定義変数の警告を避ける定番の書き方で、静的解析にも引っかからない。しかしこの一行のせいで、

$argv が未定義 → [] が返る → in_array() は false → 静かに else へ

という経路ができあがる。

?? [] を書いていなければどうなっていたか:

PHP バージョン in_array('x', $argv, true) で $argv が未定義のとき
PHP 8.x TypeError(第2引数が array でない)で停止
PHP 7.x Warning: in_array() expects parameter 2 to be array

どちらも気づける。握りつぶしたせいで、壊れ方が静かになった。isset($argv[1])、empty($argv) なども同じ性質を持つ。

対処

1. 既定値を「何もしない」に倒す(最優先)

$apply = in_array('--apply', $argv ?? [], true);

if (!$apply) {
    fwrite(STDERR, 'dry run: 変更は行いません' . PHP_EOL);
    exit(0);
}

引数が消えたぶんだけ動作が小さくなる形にする。被害が出たのは「指定なし=全部やる」という向きのせいで、$argv が消えること自体ではない。この形にしておけば、オプションの打ち忘れにも同じだけ効く。

2. CLI 以外の入口を閉じる(ただしログを残す)

if (PHP_SAPI !== 'cli') {
    error_log('CLI 以外から呼び出されました: ' . ($_SERVER['REQUEST_URI'] ?? '-'));
    http_response_code(403);
    exit;
}

error_log() を省略しないこと。黙って終了するガードは「実行されなかった」のか「実行されて弾かれた」のかを事後に切り分けられなくする。

加えて、スクリプト本体を公開ディレクトリの外に置くか、Web サーバー側でも拒否しておくと二重になる。

3. 引数の読み取りを一箇所に閉じ込める

function readArgs(): array {
    global $argv;                       // ← 必須
    if (!isset($argv)) {
        throw new RuntimeException('コマンドライン引数を取得できません(PHP_SAPI=' . PHP_SAPI . ')');
    }
    return $argv;
}

$argv はスーパーグローバルではない。関数内で global $argv; を書き忘れると、設定や入口に関係なく isset($argv) は常に false を返す。これも静かに間違うパターン。

空配列でフォールバックせず例外を投げるのが要点。getopt() も内部で同じコマンドライン引数を参照するため、入口が違えば同じように空振りする。「取得できない」と「1つも指定されていない」は別の事象として扱う。

register_argc_argv = On を選ばない理由

On にすれば Web 経由でも $argv は生える。ただし中身はURL のクエリ文字列だ。

GET /batch.php?--apply
→ $argv[1] === '--apply'

「引数がある/ない」の判定は通るようになるが、外部から任意の値を流し込める経路を自分で開けることになる。そして本来の問題——引数が届かなくても重い処理が走る——は解決していない。設定を変えても、既定値の向きはそのまま残る。

チェックリスト

  • CLI 前提のスクリプトの先頭に PHP_SAPI !== 'cli' のガードがあるか
  • ガードは弾いたことをログに残しているか
  • 破壊的な処理はオプション必須(=既定は dry run)になっているか
  • $argv ?? [] / isset($argv[n]) で未定義を吸収していないか
  • register_argc_argv の値を CLI と Web の両方で確認したか
  • 引数読み取りを関数に集約し、取得失敗時に例外を投げているか

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?