0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

php -S(PHP組み込みサーバー)で .htaccess のクリーンURLが全部404になる原因と router.php での解決

0
Posted at

結論(先に解決策)

Claude Codeで開発していた時にハマったことのメモです。

php -S localhost:8000 でローカル起動すると、.htaccessRewriteRule で動かしていたクリーン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';
}

新しいページを増やしたら .htaccessrouter.php の両方を更新する——この二重管理が漏れの温床なので、ルート一覧をコメントで並べておくとレビューしやすくなります。


補足

  • router.php は ローカル開発専用です。本番 Apache では .htaccess がそのまま効くので、router.php はデプロイ対象に含めても呼ばれません(含めない運用でも可)。
  • .htaccess と router.php の二重管理が嫌」なら、本番も含めてフロントコントローラ(index.php 一本で全リクエストを受ける)に寄せる設計もありますが、既存の .htaccess 資産がある場合は router.php で揃えるのが移行コスト最小です。
  • 切り分けの順番は 「クエリ直叩き(?area=...)で動くか」→ 動けばルーティング層の問題。router.php かルート定義の漏れを疑えば早いです。
0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?