6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめに

「リファクタリングしたい」

エンジニアなら誰もが一度は思ったことがあるはず。

でも、今回の話は 「リファクタリングしなかった」 話です。

PHP5.6で動いていた10年以上前のレガシーシステムをPHP8.2に移行する案件を担当しました。

バージョン管理の痕跡すらない、コピペの嵐、SQLインジェクションだらけ・・・

そんなコードを前にして、私は リファクタリングしない 決断をしました。

※記事内のコードは事実を元に再構成したものです。

なぜリファクタリングしなかったのか

✅ 動いているコードは正義

10年以上動いてきた実績がある。

バグがあるかもしれないけど、それも含めて「仕様」として運用されてきた。

下手に触ると、その「仕様化されたバグ」に依存した運用が壊れる。

🏢 内部システムだから

外部からのアクセスがない社内システム。

SQLインジェクションがあっても、攻撃者がいない。

セキュリティリスクは認識しつつも、優先度は下がる。

まあ、外部システムだったらこんなやべえコードにはなってなかったんだろうけど・・・

🧪 テストがない

ユニットテストもE2Eテストもない。

リファクタリングしても、正しく動いているか確認する手段がない。

「動いてるからOK」を証明できない。

📄 ドキュメントがない

仕様書がない。コメントもない。

なぜそう書いたのか、誰にもわからない。

意図を推測しながらリファクタリングするのは危険すぎる。

💰 予算と納期

リファクタリングには時間がかかる。

クライアントは「PHP8で動くようにしてほしい」としか言っていない。

美しいコードを求めているわけじゃない。

⚖️ 責任の所在

リファクタリングして壊したら、私の責任。

そのまま移行して壊れたら、元のコードの責任。

この違いは大きい。

👥 次に触る人のため

リファクタリングすると、元の構造が見えなくなる。

レガシーコードのまま残しておけば、次の人も「これがレガシーだ」とわかる。

中途半端にリファクタリングすると、どこまでが元でどこからが新しいのかわからなくなる。

⏰ 全箇所修正は工数的に不可能

同じ問題が何十箇所、何百箇所とある。

全部直すのは現実的じゃない。

「動くようにする」が目的であって「綺麗にする」が目的じゃない。

遭遇したやべえコード

やべえレベルが高い順に上位8つを紹介。

1位: SQLインジェクション

// ユーザー入力をそのままクエリに結合
$sql = "SELECT * FROM users WHERE id = " . $_GET['id'];
$result = mysql_query($sql);

?id=1 OR 1=1 でDBの中身が丸見え。

?id=1; DROP TABLE users;-- で全データ消失。

教科書に載ってるレベルの脆弱性がそのまま本番環境に。

2位: GETパラメータで認証バイパス

// 各画面でGETパラメータからユーザーIDを取得
$Staff_No = $_GET['Staff_No'];

// このIDでデータを取得・表示
$query = "SELECT * FROM user_data WHERE Staff_No = '".$Staff_No."'";

URLの?Staff_No=1?Staff_No=2に変えるだけで他人のデータにアクセス可能。

認証の意味がない。パスワードなんて飾り。

3位: パスワード平文保存

// データベースにそのまま保存
$sql = "INSERT INTO users (password) VALUES ('" . $_POST['password'] . "')";

ハッシュ化なし。ソルトなし。

DBが漏洩したら全ユーザーのパスワードがそのまま流出。

4位: DB認証情報がソースコードに直書き

// 変数すら使わず直接ハードコード
$conn = mysql_connect("192.168.1.100", "admin_user", "SecretPass123!");
mysql_select_db("production_db", $conn);

// 別ファイルでも同じ認証情報がコピペ
$conn = mysql_connect("192.168.1.100", "admin_user", "SecretPass123!");

本番IPアドレスもパスワードも丸見え。

ソースコードが漏洩したらDBに直接アクセス可能。

5位: 謎のフラグ管理

if ($flag == 1) {
    // 処理A
} elseif ($flag == 2) {
    // 処理B
} elseif ($flag == 3) {
    // 処理A + 一部処理B
} elseif ($flag == 4) {
    // 処理Bの亜種
} elseif ($flag == 5) {
    // 処理A + 処理B + 特殊処理
}
// ... flag == 20 まで続く

フラグの意味がどこにも書いてない。

「3」が何を表すのか、神のみぞ知る。

6位: コピペの嵐

// file_a.php
function GetDetailVal($office_no, $name) {
    // 実装A(50行)
}

// file_b.php
function GetDetailVal($office_no, $name) {
    // 実装B(微妙に違う50行)
}

// file_c.php
function GetDtailVal($office_no, $name) {  // typoバージョン
    // 実装C
}

同じ名前の関数が複数ファイルに存在。しかもtypoバージョンまである。

どれが正しいか不明。修正しても全ファイルに反映されない。

7位: グローバル変数の乱用

$glbLoginID;  // グローバルだがundefined

function doSomething() {
    global $LogStaffNo;  // 関数内でglobal宣言
    global $TSPassword;
    // ...
}

// セッションとグローバル変数が混在
$_SESSION["Login_ID"] = $result[0]['Login_ID'];  // セッション
$LogStaffNo = $result[0]['Staff_No'];  // グローバル変数

変数のスコープが追えない。どこで書き換わるか不明。

8位: HTMLとPHPのスパゲッティ

<td class="<?php echo $bgcolor1;?>">
    <input type="text"
        value="<?php echo ($_POST['field_'.$j.'']!=""
            ?$_POST['field_'.$j.'']
            :(($data[$j]!=""
                ?$data[$j]
                :$error!="")
                    ?$defaultVal[$j][$l]
                    :$fallback))?>" />
</td>

三項演算子のネストが深すぎて解読不能。

これを読める人、天才か変態。

まだまだやべえコードたくさんあったけど、長くなっちゃうのでこの辺で・・・

PHP8移行で遭遇したエラー

「なんでこれ今まで許されてた?画面にエラー出なかったの?」

と思ったら、php.iniに・・・

error_reporting = E_ERROR | E_PARSE | E_CORE_ERROR | E_COMPILE_ERROR | E_RECOVERABLE_ERROR

Warning/Noticeは非表示かよ!

PHPって結構許容範囲が広くて、変数未定義くらいじゃErrorじゃなくてWarning扱い。

そんなPHPでWarningを非表示にしちゃう危険性・・・

表面上は動いてるけど、裏では大量のWarningが出てたってこと。

以下、PHP8で動かなくなった代表的なやつ。

1. count(null) が Fatal Error に

// PHP7以前: Warning出してたけど動いてた
// PHP8: Fatal error: Uncaught TypeError
$count = count($maybeNull);

対処法:

$count = is_countable($maybeNull) ? count($maybeNull) : 0;

2. NULL との算術演算が TypeError に

// PHP7以前: NULLを0として計算してた
// PHP8: Uncaught TypeError
$total = $price + $tax; // $tax が NULL の場合

対処法:

$total = $price + ($tax ?? 0);

3. mysql_* 関数が完全削除

// PHP7で非推奨、PHP8で削除
mysql_connect($host, $user, $pass);
mysql_query($sql);

対処法:

PDOで共通クラスを作成し、削除されたmysql_queryと同名の関数を定義。

既存コードはそのまま動く。これが一番賢かった。

バージョン管理もない

その痕跡がこちら。

バックアップファイルの存在

index.php
index.php.bak
index.php.bak2
index.php.old
index.php.20190401
index_new.php
index_test.php

どれが本番で使われてるのか、ファイル名だけじゃわからない。

コメントによる履歴管理

// 2019/04/01 田中 修正
// 2019/05/15 鈴木 上記を戻した
// 2019/06/01 田中 やっぱり修正
// 2020/01/10 佐藤 暫定対応
// ↑いつか消す

消されることなく5年以上経過。

「暫定」という名の永続

// TODO: 暫定対応。後で直す
// ↓このコード、7年前から「暫定」

暫定は永遠に暫定のまま。

移行作業でやったこと

1. 動作確認環境の構築

まずPHP5.6とPHP8.2の両方で動く環境を用意。

Dockerで切り替えられるようにした。

2. エラーログの収集

PHP8で動かして、出てくるエラーを片っ端から記録。

エラーの種類と発生箇所をリスト化。

3. 最小限の修正

リファクタリングはしない。

  • 削除されたmysql_*と同名の関数をPDOベースで定義
  • count(null)is_countable() でガード
  • NULL算術?? 0 でデフォルト値設定

元のロジックには触らない。

4. 動作確認

修正前と修正後で同じ挙動になることを確認。

「美しくなったか」ではなく「同じように動くか」が基準。

学んだこと

📜 レガシーコードは歴史の産物

「なんでこんな書き方?」と思うコードにも理由がある。

当時のPHPバージョン、当時のベストプラクティス、当時の制約。

それを理解せずに「ダメなコード」と断じるのは傲慢。

🛡️ 動いているものを壊すな

「きれいにしたい」は開発者のエゴ。

クライアントは「動くこと」を求めている。

美しさより安定性。

💸 リファクタリングは別予算で

移行とリファクタリングは別の作業。

一緒にやろうとすると、どっちも中途半端になる。

リファクタリングしたいなら、別途提案して予算を取る。

🤝 次の人への配慮

中途半端に手を入れると、次の人が混乱する。

「全部レガシー」か「全部リファクタリング済み」のどちらかが理想。

まとめ

リファクタリングしたい気持ちはわかる。

でも、時にはその衝動を抑える勇気も必要。

  • 動いているコードは尊重する
  • リファクタリングと移行は分ける
  • 最小限の修正で目的を達成する
  • 次の人のことを考える

レガシーコードと向き合うのは大変だけど、それも立派なエンジニアリング。

美しいコードを書くことだけがエンジニアの仕事じゃない。

動くものを、動くままに、次の世代へ繋ぐ。

それもまた、大切な仕事だと思います。

・・・でも、最初からちゃんと綺麗なコード書け!!!

6
2
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
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?