レガシーPHPリファクタ 8:バリデーションなきコードへの入力チェック追加

はじめに

※本記事は『レガシーPHPリファクタ』の第8弾ですが、厳密な意味でのリファクタリング(外部の振る舞いを変えずに内部構造を改善すること)からは少し逸脱します。

実務のレガシーコードでよく遭遇する「そもそもバリデーションが存在しない(実装漏れ・仕様漏れ)」という状態に対し、既存処理を壊さずに後から入力を堅牢化していく実践アプローチとして解説します。


例1:クエリパラメータの絞り込みにおける「無防備な受け渡し」

レガシーなコードベースでは、URLのクエリパラメータを検証せず、そのままデータベースの検索条件やロジックに突っ込んでしまっていることがあります。

例えば、記事一覧や商品一覧などのカテゴリ絞り込みにおいて、次のような仕様があるとします。

  • 対象のパラメータ:$_GET['category']
  • 許容される値:'Study'、'Wok'、'Eating' の3種類のみ

修正前:安全性を捨てて「結果0件」に頼る実装

// 修正前:クエリパラメータをそのまま絞り込みに使用
$category = $_GET['category'];

// 「不正な値が渡ってきても、DBにヒットしないから結果0件になるし、まあいいか」という考え方
$sql = "SELECT * FROM posts WHERE category = :category";
// ... 実行処理

一見すると「変な値が来てもSQLインジェクションさえプリペアドステートメントで防いでいれば実害はない(結果が0件になるだけ)」ように思えますが、これは実務コードとしてはいくつかの問題を抱えています。

  1. 意図しない挙動の放置:不正な値や予期せぬ文字列が渡されたとき、サイレントに0件を返すのはデバッグや運用時のノイズになります。
  2. 仕様の曖昧さ:システム側が「どの入力を受け入れるべきか」という境界線を明確していません。

修正後:許容値を厳密にチェックし、不正な入力には 404 を返す

通常のUI導線(サイト内のリンクやフォーム)を経由している限り、定義された3種類以外の値が送られてくることはありません。そのため、想定外の値が飛んできた場合は「不正なリクエスト(あるいは存在しないページ)」とみなして 404 を返して早期終了(Fail Fast)するのが堅実です。

$selectedCategory = $_GET['category'] ?? '';

// 許可されたカテゴリのリスト
$categoryList = ['Study', 'Wok', 'Eating'];

// リストに含まれていない場合は 404 レスポンスを返して終了
if (!in_array($selectedCategory, $categoryList, true)) {
    http_response_code(404);
    echo '指定されたカテゴリは見つかりませんでした。';
    exit;
}

このように、「正常系以外の入力を早い段階で確実に弾く(ガード)」ことが、レガシーコードを堅牢にする第一歩となります。


例2:POST送信と保存処理における「フロント・サーバー双方の無防備」

続いて、データを登録・保存するPOST処理のケースです。 レガシーなフォーム画面では、HTML側に required や maxlength などのバリデーション属性が一切なく、サーバー側でも入力をそのまま受け取ってデータベースに突っ込んでいることがあります。

修正前:ノーガードな入力と保存

  • HTML:required や maxlength が一切なし
  • サーバー側:送られてきたデータをそのまま信用して処理

修正後:フロントエンドとサーバーサイドの「二重の備え」

堅牢なコードにするため、フロントエンド(HTML5)とサーバーサイド(PHP)の両方で入力制限をかけます。サーバー側では、型チェック・ホワイトリスト検証・必須チェック・文字数制限を段階的に行います。

1. 入力フォーム(HTML側でのガード)

<select name="category" id="category" required>
    <!-- 選択肢 -->
</select>

<input type="text" name="title" id="title" required maxlength="100">
<textarea name="contents" id="contents" cols="50" rows="3" required maxlength="1000"></textarea>

2. サーバーサイド(PHP側での検証)

ブラウザ側の制限はユーザー側で簡単にバイパスできるため、サーバー側で必ずバリデーションを実施します。

$category = $_POST['category'] ?? '';

// 型の安全性を確保
if (!is_string($_POST['title'] ?? '') || !is_string($_POST['contents'] ?? '')) {
    http_response_code(400);
    echo 'タイトルと本文は文字列で入力してください。';
    exit;
}

$title       = trim($_POST['title'] ?? '');
$rawContents = $_POST['contents'] ?? '';
// 改行コードを \n (LF) に統一してからトリム
$contents    = trim(str_replace("\r\n", "\n", $rawContents));

// 1. カテゴリの存在チェック(ホワイトリスト検証)
if (!in_array($category, $categoryList, true)) {
    http_response_code(400);
    echo '不正なカテゴリが選択されました。';
    exit;
}

// 2. 必須入力チェック
if ($title === '' || $contents === '') {
    http_response_code(400);
    echo 'タイトルと本文は必須項目です。';
    exit;
}

// 3. 文字数制限チェック(マルチバイト対応)
if (mb_strlen($title, 'UTF-8') > 100 || mb_strlen($contents, 'UTF-8') > 1000) {
    http_response_code(400);
    echo '入力文字数の制限を超えています。';
    exit;
}

補足:なぜ入力値やエラーメッセージをフォームに戻さないのか?

ここまで読んだ読者の中には、こう疑問に思った方もいるかもしれません。

「バリデーションエラーのときに 400 で終了するだけでなく、入力値やエラーメッセージを入力フォーム画面に戻して再表示したほうが親切ではないか?」

おっしゃる通り、近年のモダンなWebアプリケーションではそれが当たり前です。しかし、レガシーなコードベースにおいて「エラー時に値を保持してフォームに戻す」を装しようとすると、次のようなハードルがあります。

  1. レガシー環境には「値を戻す仕組み」がないことが多い
  • セッション管理やエラーメッセージを一時保持・表示する仕組みを作っていると、改修コストが大きく膨らむ。
  1. やるならレガシーで頑張るより、モダンに寄せた方が早い
  • もしユーザー体験を重視して「入力値の保持」や「インラインのエラー表示」を行いたい場合、古いPHPのテンプレート側でゴリゴリ頑張るよりも、JavaScript(Fetch APIなど)とAPIを組み合わせた構成にしたほうがはるかにシンプルで実装も楽になる。

今回のケースでは、「まずは危険な入力を確実に弾いてサーバーを守る(Fail Fast)」ことを最優先し、大掛かりなエラーハンドリング機構の導入は見送っています。アーキテクチャの古さに応じて、「どこまで手を入れるべきか」を割り切ることも、レガシーリファクタリングでは大切な判断基準になります。


まとめ

今回は無防備だったレガシーコードに「正しい入力の境界線」を引き直すプロセスをご紹介しました。

仕様や実装の漏れを後から塞ぐ作業は地味に見えますが、コードベースの健康状態を保ち、セキュリティやメンテナンス性を高めるうえで欠かせないステップです。レガシーSmartyやPHPのモダン化を進める際の参考にしてみてください!

タイトルとURLをコピーしました