レガシーPHPリファクタ 1:「ロジックとHTMLの分離配置」でコードの見通しを激変させる

レガシーPHPリファクタ 1:「ロジックとHTMLの分離配置」でコードの見通しを激変させる
Smarty動的Webサイト構築入門サンプルソース. Contribute to ninton/smartybook development by creating an account on GitHub.

レガシーなPHPプロジェクトによくある「HTMLの合間にPHPコードやファイル操作・DB処理が入り乱れているコード」。どこで何が起きているか把握しづらく、改修やテストのハードルを上げる大きな原因です。

フレームワーク(LaravelやSymfony等)への移行やMVC化が理想ですが、巨大なレガシーコードをいきなり一新するのはリスクが伴います。

そこで今回は、1ファイル内で完結する安全かつ最も効果的なリファクタリング手法「ロジックとHTMLの分離配置」について、実際のPR(プルリクエスト)例を交えて解説します。


1. 「分離配置」とは?(セクションヘッダーによる構造化)

「分離配置」とは、ファイルの冒頭でデータ処理やビジネスロジックをすべて終わらせ、ファイル後半でHTMLの描画のみを行う構造に整える手法です。

役割の境界を明確にするため、以下のようなセクションヘッダーコメントをファイル内に設けて整理します。

セクションヘッダーの構造パターン

<?php
// ----- インポート・設定読み込み (use, ini.php, autoload.php) -----
// ----- インラインクラス定義 -----
// ----- インライン関数定義 -----
// ----- 入力値受取・前処理 -----
// ----- メイン処理・データ操作 -----
// ----- HTML表示 -----
?>
<!-- ここから下はHTML描画のみ -->

このルールを徹底するだけで、サイドエフェクト(処理の実行)とレスポンス出力が混ざらなくなり、コードの可読性が格段に向上します。


2. 【例1】基本編:変数の準備と表示の分離

まずはシンプルな例から見てみましょう。HTMLの途中に変数の定義と echo が混在しているコードです。

リファクタ前

HTMLの <body> 内で変数定義と出力が同時に行われており、どのようなデータが使われているのか上から順に読まないとわかりません。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>文字や数字を表示</title>
</head>
<body>
<?php
// ウェブサイト名
$siteName = 'Smarty for Designers';

// バージョン番号
$revision = 1;

// CSVファイル名
$csv = 'data.csv';

// 画像ディレクトリ
$imageDir = './images/';

//トップページ
$home = '02_02.php';

//管理ページ
$admin = '02_04.php';

echo $siteName;
echo $revision;
echo $csv;
echo $imageDir;
echo $home;
echo $admin;
?>
</body>
</html>
Blaming smartybook/smartybook/chapter2/02_01.php at 5528d20833e051f6d5e359f3552f770fb7f28240 · ninton/smartybook
Smarty動的Webサイト構築入門サンプルソース. Contribute to ninton/smartybook development by creating an account on GitHub.

リファクタ後

セクションヘッダーを追加し、ファイル冒頭で変数を定義して、後半のHTMLでそれらを出力する形に分離しました。

<?php
// ----- データ準備 -----
// ウェブサイト名
$siteName = 'Smarty for Designers';

// バージョン番号
$revision = 1;

// CSVファイル名
$csv = 'data.csv';

// 画像ディレクトリ
$imageDir = './images/';

// トップページ
$home = '02_02.php';

// 管理ページ
$admin = '02_04.php';

// ----- HTML表示 -----
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<title>文字や数字を表示</title>
</head>
<body>
<?php
echo $siteName;
echo $revision;
echo $csv;
echo $imageDir;
echo $home;
echo $admin;
?>
</body>
</html>
Blaming smartybook/smartybook/chapter2/02_01.php at 194eb06b758adc1b796005e13f49f78400d3db56 · ninton/smartybook
Smarty動的Webサイト構築入門サンプルソース. Contribute to ninton/smartybook development by creating an account on GitHub.

ポイント

  • // ----- データ準備 ----- と // ----- HTML表示 ----- の境界を作ったことで、ファイルを開いた瞬間に「どんな変数が使われるか」がすぐ把握できるようになりました。

3. 【例2】実践編:ビジネスロジックとビューの完全分離

次に、POST値の受け取り、ファイル操作(CSV書き込み)、実行結果の制御が含まれる、より実践的な例です。

リファクタ前

HTMLを出力している途中で関数を定義し、CSVの読み書き・ロック制御・エラーハンドリング(print による直接出力)まで行っています。

<?php
// 設定ファイルの読み込み
require_once __DIR__ . '/02_01_b.php';
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>登録完了 - <?php print $siteName;?></title>
</head>
<body>
<?php
// HTML描画スタート後に関数定義している
function lastIdCheck($file): int { /* ... */ }
function convertNl(string $str): string { /* ... */ }

// HTML描画スタート後にCSVファイル処理している
$fp = fopen(__DIR__ . '/data.csv', 'a+') or die('file_open_error');
flock($fp, LOCK_EX);
// ...
$check = fwrite($fp, $string);

// 処理の途中でHTMLを直接echo/printしている
if ($check === false) {
    print '登録に失敗しました。<br />';
} else {
    print '正常に登録できました。<br />';
}
flock($fp, LOCK_UN);
fclose($fp);
print '<a href="' . $admin . '">登録フォームに戻る</a>';
?>
</body>
</html>
smartybook/smartybook/chapter2/02_06.php at 847fd06eb1e322e440619bf6b1934ae26bee620e · ninton/smartybook
Smarty動的Webサイト構築入門サンプルソース. Contribute to ninton/smartybook development by creating an account on GitHub.

リファクタ後

処理の流れを「関数定義」→「ビジネスロジック(データ処理)」→「HTML表示」に切り分けました。

  1. POST受取とエラー制御の分離: ロジック部で書き込み成否をフラグ変数($isSuccess)に保持。
  2. ビューのシンプル化: HTML側では $isSuccess に基づく条件分岐(if ($isSuccess):)による結果描画のみを担当。
  3. セキュリティとモダン化: 併せて htmlspecialchars によるXSS対策や、DOCTYPE のモダン化を実施。
<?php
// 設定ファイルの読み込み
require_once __DIR__ . '/02_01_b.php';

// ----- インライン関数定義 -----
/**
 * 最新記事ID(CSVファイルの最終行のID)を取得する関数
 */
function lastIdCheck($file): int
{
    $lastId = 0;
    while ($arr = fgetcsv($file, 5000, escape: '')) {
        if (isset($arr[0])) {
            $lastId = (int)$arr[0];
        }
    }
    return $lastId;
}

/**
 * 改行文字,カンマ,クォートを処理する関数
 */
function convertNl(string $str): string
{
    $str = stripslashes($str);
    $str = str_replace('"', '""', $str);
    return '"' . $str . '"';
}

// ----- ビジネスロジック(CSV保存処理) -----
// POSTデータの取得(Null合体演算子で安全に受け取り)
$category    = $_POST['category'] ?? '';
$rawTitle    = $_POST['title'] ?? '';
$rawContents = $_POST['contents'] ?? '';
$date        = $_POST['date'] ?? '';
$image       = $_POST['image'] ?? '';

// CSVファイルの読み込み・ロック処理
$fp = fopen(__DIR__ . '/data.csv', 'a+');
if ($fp === false) {
    die('file_open_error');
}

flock($fp, LOCK_EX);

$lastId = lastIdCheck($fp);
$id     = sprintf('%04d', $lastId + 1);

$title    = convertNl($rawTitle);
$contents = convertNl($rawContents);
$string   = "$id,$category,$title,$contents,$date,$image\n";

// 書き込み結果をブーリアン値で保持(直接printしない)
$isSuccess = (fwrite($fp, $string) !== false);

flock($fp, LOCK_UN);
fclose($fp);

// ----- HTML表示部 -----
?>
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <title>登録完了 - <?php echo htmlspecialchars($siteName, ENT_QUOTES, 'UTF-8'); ?></title>
</head>
<body>

<?php if ($isSuccess): ?>
    <p>正常に登録できました。</p>
<?php else: ?>
    <p>登録に失敗しました。</p>
<?php endif; ?>

<p><a href="<?php echo htmlspecialchars($admin, ENT_QUOTES, 'UTF-8'); ?>">登録フォームに戻る</a></p>

</body>
</html>
Blaming smartybook/smartybook/chapter2/02_06.php at a19575e7a3913d94c4771e5be779146d3085017d · ninton/smartybook
Smarty動的Webサイト構築入門サンプルソース. Contribute to ninton/smartybook development by creating an account on GitHub.

4. 💡 補足:HTMLタグを echo / print で出力しない

リファクタリング時に合わせて見直したい重要なポイントが、「PHP側でHTMLタグを文字列として出力しない」 ということです。

// ❌ 避けたほうが良い例(PHP文字列としてHTMLを出力)
echo '<a href="' . $admin . '">登録フォームに戻る</a>';
// ⭕ ベターな例(HTMLの中に最小限のPHPを埋め込む)
<a href="<?php echo htmlspecialchars($admin, ENT_QUOTES, 'UTF-8'); ?>">登録フォームに戻る</a><

なぜ後者のほうがベターなのか?

  1. エディタの補完やハイライトが効く 文字列('...')の中にHTMLを書くと、エディタのHTML補完やシンタックスハイライト、Emmetなどの支援機能が効かなくなります。HTMLとして記述することでコーディングミスを防げます。
  2. 「出力時のエスケープ(Escape on Output)」が徹底できる
    • ❌ echo と同時に htmlspecialchars すると、ごちゃごちゃして読みにくくなるので、事前に $admin = htmlspecialchars($admin); とエスケープするスタイルがあります。ところが、これは、二重エスケープの原因になったり、「パッと見でエスケープ済みかどうかわからない」ため脆弱性を生みやすくなります。
    • ❌ かつてのフレームワーク等で見られた1文字のグローバルヘルパー関数 h() は、グローバル汚染や静的解析との兼ね合いから、現代では実質的なレガシー/アンチパターンとして扱われる傾向にあります。
    • ⭕ 原則として「変数には生の値を保持し、echo と同時に htmlspecialchars を呼ぶ」 スタイルに統一することで、出力箇所を見ただけで安全性がひと目で確認できるようになります。
  3. HTML構造の可読性が上がる PHPの文字列結合(.)が減り、HTMLのタグ構造がそのまま見えるため、マークアップの修正やCSSクラスの追加が圧倒的にやりやすくなります。

5. このリファクタリングがもたらすメリット

  1. ヘッダー送信エラー(Headers already sent)の防止 リダイレクト(header('Location: ...'))やセッション操作(session_start())が必要になった場合でも、HTML出力前にロジックを完結させておくことでエラーを防げます。
  2. テンプレートエンジンや別ファイルへの切り出しが容易になる ロジックとHTMLが分離されていれば、将来的にHTML部分を Smarty や Twig などのテンプレートファイル(.tpl / .twig)へ引っ越しさせたり、別ファイルへ require 抽出するのが格段に楽になります。
  3. コードレビューの負担軽減 セクションヘッダーによって「どこに何を書くべきか」チーム内のコンベンション(約束事)が揃うため、レビューしやすくなります。

まとめ

レガシーコードの改善は、一気にすべてを書き換えるのではなく「安全に、段階的に」進めることが何より重要です。

今回紹介した「分離配置」は、実質的にはコードの行の順序を入れ替えるだけのリファクタリングです。そのため既存機能を壊してしまうリスクが非常に低く、安全に着手できます。

たったこれだけの「おそうじ(Tidy リファクタ)」をするだけでも、以前は読む気が起きなかったコードが見違えるほど読みやすくなります。

また、最近ではAIを活用してコード修正や機能追加を行う場面が増えていますが、元のコードがあまりにスパゲッティ状態だと、AIが提案してきたコードが本当に正しいのかどうか人間が判断することすら難しくなってしまいます。

本格的な改修や新機能の追加に入る前に、まずは安全な第一歩として「1ファイル内でロジックを上部へ、HTMLを下部へ集約する」整理から始めてみてはいかがでしょうか。

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