結論(先に解決策)
Claude Codeで開発していた時にハマったことのメモです。
php -S localhost:8000 でローカル起動すると、.htaccess の RewriteRule で動かしていたクリーンURL(/pref/27000/ など)が全部 404になります。
理由はシンプルで、PHP 組み込みサーバーは .htaccess を一切読まないから。本番 Apache のルーティングをローカルで再現するには、.htaccess の代わりになる router.php を自分で書いて、起動時に渡します。
php -S localhost:8000 -t public_html public_html/router.php
<?php
// router.php — .htaccess の RewriteRule を PHP で再現する薄いラッパー
declare(strict_types=1);
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$file = __DIR__ . $uri;
// 実在する静的ファイル(css/js/画像)はそのまま返す
if ($uri !== '/' && is_file($file)) {
return false; // ← 組み込みサーバーに「通常配信して」と伝える
}
// クリーンURL を各ハンドラへ振り分ける
if (preg_match('#^/pref/(\d{5})/?#', $uri, $m)) {
$_GET['area'] = $m[1];
require __DIR__ . '/pref/index.php';
} elseif ($uri === '/' || $uri === '') {
require __DIR__ . '/index.php';
} else {
http_response_code(404);
require __DIR__ . '/templates/_404.php';
}
症状
- 本番(Apache)では
/pref/27000/も/ranking/aging_rate/も開けるのに、ローカルのphp -SではNot Found - トップ(
/)だけは出る -
index.php?area=27000のようにクエリ直叩きなら動く=ルーティングの問題だと分かる
$ php -S localhost:8000 -t public_html
$ curl -i localhost:8000/topic/aging/
HTTP/1.1 404 Not Found # ← .htaccess が効いていない
原因
PHP の組み込みサーバー(php -S)は開発用の最小サーバーで、Apache のモジュール(mod_rewrite)も .htaccess も持ちません。そのため .htaccess に書いた
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^pref/([0-9]{5})/?$ pref/index.php?area=$1 [L,QSA]
のような書き換えルールは完全に無視されます。リクエストされた /pref/27000/ に対応する物理ファイルが無い → そのまま 404、という流れです。
-t public_html で docroot を指定してもルーティングは肩代わりしてくれません。.htaccess 相当のディスパッチャを自前で用意する必要があります。
解決
1. router.php を作って起動時に渡す
php -S は第3引数にルータースクリプトを取れます。全リクエストがまずここを通るので、ここで .htaccess の振り分けを再現します。
php -S localhost:8000 -t public_html public_html/router.php
2. 静的ファイルは return false で「素通し」する
router.php の中で return false; を返すと、組み込みサーバーがその URL を通常の静的配信として処理します。CSS/JS/画像をここで止めないための定石です。
if ($uri !== '/' && is_file(__DIR__ . $uri)) {
return false; // css・js・png 等はそのまま配信
}
3. ルート定義の「漏れ」に注意(ハマりどころ)
.htaccess 側に複数の RewriteRule がある場合、router.php に移し忘れたパスだけ 404 に落ちます。これが地味に厄介で、「他は動くのに /monthly/ だけ 404」「/topic/gender/ だけ出ない」のような部分的な不具合になります。
// よくある漏れ①: インデックス(末尾なし/あり両方)の定義忘れ
} elseif ($uri === '/monthly' || $uri === '/monthly/') {
require __DIR__ . '/monthly/index.php';
// よくある漏れ②: 正規表現の選択肢にページを追加し忘れ
} elseif (preg_match('#^/topic/(aging|shoulder|workforce|decline|gender)/?#', $uri, $m)) {
$_GET['topic'] = $m[1];
require __DIR__ . '/topic/' . $m[1] . '.php';
}
新しいページを増やしたら .htaccess と router.php の両方を更新する——この二重管理が漏れの温床なので、ルート一覧をコメントで並べておくとレビューしやすくなります。
補足
- router.php は ローカル開発専用です。本番 Apache では
.htaccessがそのまま効くので、router.php はデプロイ対象に含めても呼ばれません(含めない運用でも可)。 - 「
.htaccessと router.php の二重管理が嫌」なら、本番も含めてフロントコントローラ(index.php一本で全リクエストを受ける)に寄せる設計もありますが、既存の.htaccess資産がある場合は router.php で揃えるのが移行コスト最小です。 - 切り分けの順番は 「クエリ直叩き(
?area=...)で動くか」→ 動けばルーティング層の問題。router.php かルート定義の漏れを疑えば早いです。