PHPビルトインサーバを活用し、ローカル開発環境で簡単な動作確認をするためのTips

こんにちは、パンダ大好きエンジニア、宮原です!…が…、年末年始、パンダの赤ちゃん観覧に4回応募し、4回落選しました(´・ω・`)

さて今回は、チームで利用するローカル開発環境で簡単な動作確認をするために、PHPのビルトインサーバを活用しましたよ!というちょっとマニアックな記事をお届けしたいと思います。

きっかけ

以前の記事にて、各エンジニアのローカルPC上に、仮想の動作確認環境を構築したとお伝えしました。その際、「気軽に動作確認できる環境を構築したい」という想いがありました。具体的な要件としては、「好きな場所にリポジトリをcloneさえしてくれば良い」と「ローカル開発環境とは言え、他の案件の開発にも使用しているので、なるべく環境を汚したくなかった(Webサーバをインストールして設定書き換えたりをしたくなかった)」です。特に私は、一つの開発環境を色々な用途で使用していたので、なるべくシステム全体に影響する変更をしたくありませんでした。

今回は、この動作環境の構築について紹介します。

PHPのビルトインサーバの導入

どのような環境にするか考えた際に、Dockerなどのコンテナ技術かPHPのビルトインサーバか二択になりました。

Dockerなどのコンテナを利用した場合、おそらく実際の本番環境で使っている実機サーバに一番近い環境を再現できると思いましたが、Dockerなどに慣れていない場合、手軽さに欠けてしまうかな、と思いました。また、PHPビルトインサーバに比べ、Dockerだと起動に時間がかかったり設定ファイルが複雑になったりと、慣れ以外の点でも気軽さに欠けるかなと思いました。

一方、PHPのビルトインサーバの場合、PHPさえインストールされていれば良いので手軽は手軽なのですが、機能的に十分かどうか、というのが気がかりでした。

そこで、ひとまず何か致命的な問題が起きるまでは、ローカル開発環境の動作確認はPHPのビルトインサーバで済ませることにしました。 結果として、環境を構築してから2年、今のところは大きな問題は発生していません。 (但し、あくまでも気軽な動作確認であって、実際のWebサーバの設定等のテストは、テストやステージング用の別のサーバでしっかり確認しています)

CodeIgniterでの実現例

それでは、PHPのビルトインサーバを活用する際に工夫した点などを、実例を交えて紹介したいと思います。

以前の記事と同様に、CodeIgniterというPHPフレームワークのWebアプリケーションを、PHPのビルトインサーバで動かす実例を紹介していきます。今回もファイルパスは全て相対パスで指定しているため、パスさえ書き換えれば他のフレームワークにも容易に適用できると思います。

ディレクトリ構成

まず、ファイル構成は以下のようになっています。(今回の記事に関係ない部分は省いています)

f:id:g-editor:20180508131706p:plain:h700

※「公開領域」「非公開領域」は実際には半角英数のディレクトリ名が付けられています

application/tools/localServerRunner というディレクトリに、runPHPBuiltinServer.sh と、 router.php というファイルが格納してあります。

起動用スクリプト

runPHPBuiltinServer.sh

php -S 192.168.33.10:8000  -t ../../../../公開領域 ../非公開領域/application/tools/localServerRunner/router.php

IPアドレス192.168.33.10、ポート8000番、「公開領域」ディレクトリをドキュメントルートディレクトリとし、同じ場所に格納されているrouter.phpをルータースクリプトとして読み込んで、PHPのビルトインサーバを起動します。 IPアドレス、ポート番号は使用者が必要に応じて変更します。

ルータースクリプト

router.php

<?php
/**
 * PHPのビルトインサーバで○○○コンテンツを動作させるためのルーティング設定
 * http://△△△/XXX 以下にある画像やcssへのリクエストがきたら、
 * mock ディレクトリに格納されているファイルを返す
 * それ以外は、公開領域以下へのアクセスとして処理する
 */
if (strpos($_SERVER['REQUEST_URI'], '/○○○') !== false) {

    // PHPビルトインサーバでは○○○パスが見えないため、URIを書き換え
    $path = str_replace('/○○○','',$_SERVER['REQUEST_URI']);
    $_SERVER['REQUEST_URI'] = $path;

    // PHPプログラム以外のアクセス、assetなどの場合
    if(preg_match('/\.(js|ico|gif|jpg|png|css|zip|gz|html|xml|htm|pdf|json|rss|txt)/',$path, $matches)) {
        // 実ファイルへのパスを取得
        $root = $_SERVER['DOCUMENT_ROOT'];
        chdir($root);

        // CSSファイルの場合
        if(strpos($matches[0],'css') !== false) {
            header("Content-Type: " .  "text/css");
        // CSSファイル以外の場合
        } else {
            header("Content-Type: " . mime_content_type($root . $path));
        }
        // 実ファイルを読み取り返却 
        return readfile($root . $path);
    }

    // PHPの場合はfalseを返すとそのまま実行となる
    return false;

} else {
    if (strpos($_SERVER['REQUEST_URI'], 'XXX') !== false) {
        if (strpos($_SERVER['REQUEST_URI'], '.css') !== false) {
            // cssとしてヘッダも返す
            chdir('../../mocks/assets');
            header("Content-Type: text/css");
            readfile('./'.$_SERVER['REQUEST_URI']);
        } else if (strpos($_SERVER['REQUEST_URI'], '.woff') !== false) {
            // woffファイルとして適切なヘッダも返す
            chdir('../../mocks/assets');
            header("Content-Type: application/font-woff");
            readfile('./'.parse_url($_SERVER['REQUEST_URI'])['path']);
        } else if (strpos($_SERVER['REQUEST_URI'], '.ttf') !== false) {
            // ttfファイルとして適切なヘッダも返す
            chdir('../../mocks/assets');
            header("Content-Type: application/x-font-ttf");
            readfile('./'.parse_url($_SERVER['REQUEST_URI'])['path']);
        } else {
            chdir('../../mocks/assets');
            readfile('./'.parse_url($_SERVER['REQUEST_URI'])['path']);
        }
    } else {
        return false;
    }
}

※ケースが増えるたびに継ぎ足してきてしまったので、スパゲッティになりつつあります…

このルータスクリプトでは、自コンテンツ(http://△△△/○○○配下)の公開領域へのアクセスかそれ以外かで処理を分けています。

自コンテンツの公開領域へのアクセスの場合は、まず、ビルトインサーバでは見えないパス部分を取り除く処理を行います。今回の例で言うと、公開領域へのURLが、実際のサーバでは http://△△△/○○○ となりますが、ビルトインサーバでは http://192.168.33.10:8000 として扱われるようになるので、 リクエストURLから ○○○ を除去します。

アクセス対象がPHPファイルの場合は、falseを返せば期待する動作(PHPファイルの実行)をします。それ以外の画像ファイル等の場合は、適切なヘッダを返すとともに、readfile()を使ってファイルの読み込みを行います。

未デプロイの他の公開領域へのアクセス

自コンテンツの公開領域ではなく、別の公開領域にデプロイされる予定で、さらに、開発時点ではまだデプロイされていない画像やスクリプトファイルを参照するための工夫も施してあります。簡単に言うと、非公開領域に配置した画像やスクリプトファイルのダミーファイルを参照できるようにします。

このルータースクリプトの例では、現在開発中のサービス http://△△△/○○○ から、別の領域であるhttp://△△△/XXX 以下にデプロイされる予定の画像ファイル等にアクセスしたい場合に、非公開領域に作ったmocks/assetsというディレクトリへ代わりにアクセスするようにしています。その際、適切なヘッダも合わせて返すようにしています。
(今回の例では、.woffや.ttfという拡張子を持つフォントに対応しています)

f:id:g-editor:20180417141338p:plain

なおローカル開発環境での動作時は、△△△はローカル開発環境のアドレス( 192.168.33.10:8000 )を指しています。

まとめ

PHPのビルトインWebサーバを使って、ローカル開発環境での動作確認を実現する方法を紹介しました。自コンテンツの公開領域以外へのアクセスや、標準でサポートされていないファイルに対しても適切なヘッダを返せるようにしています。Dockerなどのコンテナ技術を採用する前に、まずは気軽なローカル動作環境を作りたいという方に参考にして頂ければ幸いです。

おまけ

今回は触れませんでしたが、ローカル動作環境には一部Node.jsを利用して構築したモックサーバも活用しています。このモックサーバとPHPのビルトインサーバを同時に起動する際には、gulpを利用しています。 gulpからPHPのビルトインサーバを起動するためのgulpfile.jsの書き方も紹介しておきます。

    gulp.task('php-builtin-server', function() {
    var php = require('./node_modules/gulp-connect-php');
    php.server({
        hostname: '192.168.33.10',
        port: 8000,
        base: '../../../../公開領域',
        router: './router.php',
    })
});

gulp-connect-phpというパッケージを利用し、前述の起動用スクリプトと同じようにルートディレクトリやルータースクリプトを指定しています。 このファイルでは、gulpfile.jsもlocalServerRunnerに配置されており、gulp-connect-phpも同ディレクトリのnode_modulesディレクトリ内にインストールされている前提となっています。


お知らせ
ぐるなびでは一緒に働く仲間を募集しています。


‘宮原良介’

2010年に大学院を卒業し、新卒で某大手精密機器メーカに就職。2015年の秋、食への熱い思いを持って、ぐるなびへ転職。ぐるなびでは単に開発を行うだけではなく、様々な業務改善を推進するとともに、アジャイル(スクラム)の普及にも尽力している。
パンダ好きのフルスタック気味エンジニア。