今回は、第4章(4.8章)の 「すべて同じ内容のエンドポイントPHPファイルが並んでいる問題」 の解消についての解説です。
ターゲット読者
- レガシーなSmartyアプリケーションの保守・運用を任されてしまったエンジニア
- 変更点やページ数が多い一方で、大規模なフレームワーク移行やDB化の工数・予算が取れない現場の担当者
背景:なぜ同じ内容のエンドポイントPHPが乱立するのか?
書籍の執筆当時(2007年)、チュートリアルとしてコードをシンプルに見せるため、あえて .htaccess によるURLルーティングを使用せず、「必要なページごとにPHPファイルとテンプレートファイルを愚直に用意する」という構成にしていました。 .htaccess の解説に紙面を取られるのを避けるという意図もありました。
しかし、この構成は現実のWebサイトのメンテナンスにおいて、次のような「あるあるな課題」を抱えがちです。
- よくある現場のシチュエーション
- URLルーティングの仕組みが入っていない
- FAQ、初めての方へ、サービスの使い方の解説など、「コンテンツ領域だけが違う」多数の静的・半静的ページが存在する
- 「DB化して管理画面を通したいが工数がない」「外部CMSやサービスに移行したいが予算がない」
- そのため、しかたなく該当するPHPやテンプレートファイルの文言を直接エディタで編集し、コミット・デプロイを繰り返している
今回のプルリクエスト(#721)では、この「ファイルは別々にあるが、中身は全く同じPHPファイル群」をどのようにスッキリ整理するかを取り上げています。
採用したアプローチの検討
このような重複を解消するため、3つのアプローチを検討しました。
案1:common_render.php 方式(★今回採用)
共通の描画処理を別ファイル(src/common_render.php など)に切り出し、各エンドポイントからはそれを require するだけの構成にする方式です。
- メリット:
.htaccessの設定変更が不要。サーバーの環境差異に強く、安全に導入できる。- ファイルの存在有無がそのまま「公開してよいページのホワイトリスト」になるため、セキュリティ事故が起きにくい。
- デメリット:
- 2行だけとはいえ、エンドポイントごとのPHPファイル自体は残り続けるため、ファイルの保守が必要。
案2: .htaccess + PHP側での動的検証
すべてのリクエストを index.php 1ファイルに集約し、パスに応じてテンプレートを切り替えます。 index.php 側で渡ってきたパスのホワイトリスト判定やディレクトリトラバーサル対策を行います。
<IfModule mod_rewrite.c>
RewriteEngine On
# すでに存在するファイルやディレクトリへのリクエストは、そのまま通す
RewriteCond %{REQUEST_FILENAME} -f [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^ - [L]
# それ以外のすべてのリクエストを index.php に転送し、パス情報を渡す
RewriteRule ^(.*)$ index.php?path=$1 [QSA,L]
</IfModule>
- メリット:
- エンドポイントの物理ファイルが
index.phpの1つだけで済む。 - 柔軟なパス制御や一元的なエラー処理が可能。
- エンドポイントの物理ファイルが
- デメリット:
- PHP側でディレクトリトラバーサルなどの脆弱性に配慮した厳格な実装とテストが必要。
- サーバーの引っ越しや、Nginxなどの別Webサーバーへ移行する際、URLルーティング設定の書き換えコストが生じる。
案3: .htaccess による静的ホワイトリスト
.htaccess 側に許可するページ(page1.php 〜 page4.php など)を RewriteCond で明示的に列挙し、それ以外を弾く方式です。
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} "page1.php" [OR]
RewriteCond %{REQUEST_FILENAME} "page2.php" [OR]
RewriteCond %{REQUEST_FILENAME} "page3.php" [OR]
RewriteCond %{REQUEST_FILENAME} "page4.php"
RewriteRule ^(.*)$ index.php?path=$1 [QSA,L]
</IfModule>
- メリット:
- エンドポイントのファイルが
index.phpの1つだけで済む。 - 不正なリクエストをPHPアプリケーション層に到達させる前に、Webサーバー層で完全にブロックできる。
- エンドポイントのファイルが
- デメリット:
- ページを追加・削除するたびに、
.htaccess側の修正・メンテナンスが必要になる。 - サーバーの引っ越しや、Nginxなどの別Webサーバーへ移行する際、URLルーティングの設定が壊れるリスクや学習コストが生じる。
- ページを追加・削除するたびに、
実際のコード変更
修正前後のコードを見比べてみましょう。
修正前:各ファイルにロジックが重複していた状態
index.php から page4.php まで、すべてのファイルにまったく同じブートストラップ読み込みとSmartyの初期化・アサイン・描画処理が書かれていました。
<?php
use App\Smarty\AppSmarty as Smarty;
require_once dirname(__DIR__, 2) . '/bootstrap/app.php';
// ----- 入力値受取・前処理 -----
$php = basename($_SERVER['SCRIPT_NAME']);
$tpl = preg_replace('/\.php$/', '.tpl', $php);
// ----- テンプレートエンジンの初期化とアサイン・描画 -----
$smarty = new Smarty();
$smarty->assign('currentPage', $php);
$smarty->display('pages/chapter4_8/' . $tpl);
修正後:エンドポイントを極限までシンプルに、処理を共通化
各エンドポイントPHPは、共通の処理を読み込むだけの1行にスリム化しました。
各エンドポイントファイル (index.php 〜 page4.php)
<?php require __DIR__ . '/src/common_render.php';
共通描画スクリプト (src/common_render.php)
<?php
use App\Smarty\AppSmarty as Smarty;
require_once dirname(__DIR__, 2) . '/bootstrap/app.php';
// ----- 入力値受取・前処理 -----
$php = basename($_SERVER['SCRIPT_NAME']);
$tpl = preg_replace('/\.php$/', '.tpl', $php);
// ----- テンプレートエンジンの初期化とアサイン・描画 -----
$smarty = new Smarty();
$smarty->assign('currentPage', $php);
$smarty->display('pages/chapter4_8/' . $tpl);
まとめ
今回のリファクタリングによって、次のようなメリットが得られました。
- 重複コードの排除: 共通の初期化処理や描画ロジックが一箇所にまとまり、将来的な仕様変更(例えば、共通のヘッダー変数を追加するなど)が容易になった。
- 保守性の向上: 各ページのPHPファイルが数行の
requireだけになったため、ノイズが減り、実質的なテンプレートとコンテンツの管理に集中できるようになった。 - 低リスク:
.htaccessをいじらないため、レンタルサーバー環境や既存のデプロイ手順を一切変更せずに安全に適用できた。
参考PR


