読者です 読者をやめる 読者になる 読者になる

k-holyのPHPとか諸々メモ

Webで働くk-holyがPHP(スクリプト言語)とか諸々のことをメモしていきます。ソースコードはだいたいWindowsで動かしてます。

Silexアプリケーションをサブディレクトリで運用する(mod_rewriteとUrlGenerator)

Advent Calendar 用のサンプルコードを書くに当たって、Silexアプリケーションをサブディレクトリで運用したメモです。

なお、例のごとくサンプルコードではテンプレートエンジンにSmartyを使っています。

Apache + mod_rewrite で解決

Apache + mod_rewrite 環境においてSilexアプリケーションをサブディレクトリで運用する例です。

ドキュメントルートおよびサブディレクトリで運用するSilexアプリケーションのエントリスクリプトのパスは以下とします。

ドキュメントルート /home/kholy/public_html/

エントリスクリプト /home/kholy/public_html/sub/index.php

この場合 /home/kholy/public_html/sub/ に .htaccess ファイルを設置、RewriteBase ディレクティブでURLリライト時に付与するパスのサブディレクトリを指定します。

/home/kholy/public_html/sub/.htaccess

RewriteEngine On
RewriteBase /sub
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !\.(ico|gif|jpe?g|png|css|js)$ [NC]
RewriteRule ^(.*)$ index.php [QSA,NS,L]

こうしておくことで、ドメインが example.com だとすると http://example.com/sub/ 以下へのリクエストが /home/kholy/public_html/sub/index.php に振り分けられます。

ここまでは Silex の公式ドキュメント 使用方法 | Japan Symfony Group を参考にしただけです。

Windowsローカル環境のPHPビルトインサーバでSilexアプリケーションをサブディレクトリ運用する

次に、SilexアプリケーションをWindows環境で開発するにあたって、ビルトインWebサーバでサブディレクトリ運用する例です。

ビルトインサーバの場合 .htaccess は利用できませんので、それに代わるルータースクリプトを書く必要があります。

ルータースクリプト C:\Users\k-holy\Projects\test\public_html\sub\router.php

<?php
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
if ('/' === $path || !realpath(__DIR__ . $path)) {
    require __DIR__ . DIRECTORY_SEPARATOR . 'index.php';
} else {
    return false;
}

これで、存在しないパスへのリクエストは全てエントリスクリプト(index.php)に振り分けられます。

ビルトインサーバの起動には色々な方法があると思いますが、私の場合は専用のショートカットを作成し、ドキュメントルートとするパスを「作業フォルダ」に設定して起動しています。

たとえばPHPのパスは C:\php\php.exe で 8080番ポートを利用する場合、こういう設定のショートカットを使います。

リンク先 C:\php\php.exe -S 127.0.0.1:8080 sub\router.php

作業フォルダ C:\Users\k-holy\Projects\test\public_html

ビルトインサーバでは作業フォルダがドキュメントルートになりますので、上記の設定で起動した場合

ドキュメントルート C:\Users\k-holy\Projects\test\public_html

ルータースクリプト C:\Users\k-holy\Projects\test\public_html\sub\router.php

エントリスクリプト C:\Users\k-holy\Projects\test\public_html\sub\index.php

となり、 http://127.0.0.1:8080/sub/ 以下へのリクエストが前述のルータースクリプトによって、エントリスクリプトに振り分けられます。

UrlGeneratorを使ってURLを動的生成する

次に、Symfony Routingコンポーネントの UrlGenerator を使ってURLを動的生成してみます。

UrlGenerator では、あるリクエストハンドラへのパスを、リクエストハンドラに付けたルーティングの名前で記述できます。

これをHTMLテンプレートから利用してリンク先のURLを動的に生成すれば、ドキュメントルートやアプリケーション設置先のパス(サブディレクトリ)が実環境と違ったりしてパスが変わってしまう場合でも、テンプレートの記述は変更しなくて済むというわけですね。

Silex では UrlGeneratorサービスプロバイダ (Silex\Provider\UrlGeneratorServiceProvider) が提供されており、Twigサービスプロバイダ (Silex\Provider\TwigServiceProvider) と併用することで、Twig の RoutingExtension として組み込まれ、テンプレートから利用できるようです。

UrlGeneratorServiceProvider - Documentation - Silex - The PHP micro-framework based on Symfony2 Components

(なお Silex には UrlGeneratorServiceProvider のための Trait も用意されており、Silex\Application を継承したクラスにこれを組み込むこともできるようですが、今回は PHP5.3環境への対応のため断念しました)

しかし今回はSmartyなので、Smartyのテンプレート関数経由でUrlGeneratorを利用するためのクラスを別途用意しました。

UrlGeneratorPlugin C:\Users\k-holy\Projects\test\public_html\sub\classes\Holy\Smarty\UrlGeneratorPlugin.php

<?php
namespace Holy\Smarty;

class UrlGeneratorPlugin
{

    private $generator;

    public function __construct(\Symfony\Component\Routing\Generator\UrlGenerator $generator)
    {
        $this->generator = $generator;
    }

    public function path(array $params = array(), \Smarty_Internal_Template $template)
    {
        if (!isset($params['route'])) {
            throw new \SmartyException("Required parameter 'route' is not set.");
        }
        $route = $params['route'];
        unset($params['route']);
        return $this->generator->generate($route, $params, false);
    }

    public function url(array $params = array(), \Smarty_Internal_Template $template)
    {
        if (!isset($params['route'])) {
            throw new \SmartyException("Required parameter 'route' is not set.");
        }
        $route = $params['route'];
        unset($params['route']);
        return $this->generator->generate($route, $params, true);
    }

}

UrlGenerator::generate()メソッドは第1引数がルーティング名、第2引数がクエリパラメータの配列、第3引数Absolute-URLを生成するかどうかのフラグです。

Smartyのテンプレート関数プラグインでは引数は name="value" 形式の属性で渡す必要があるため、上記のような仕様にしています。(つまり route=*** というクエリパラメータは扱えません…)

Smartyインスタンス生成時に、上記クラスの2つのメソッドを、Smarty::registerPlugin('function', 関数名, callback) でテンプレート関数として登録します。

今回はサービスプロバイダを用意するまでもないということで、Application (というかPimple) の share() メソッドでインスタンスを生成しています。

エントリスクリプト C:\Users\k-holy\Projects\test\public_html\sub\index.php

<?php
namespace Acme;

/*
 * 【サブディレクトリ運用サンプル】
 *
 * @copyright 2012 k-holy <k.holy74@gmail.com>
 * @license The MIT License (MIT)
 */
$loader = include __DIR__ . '/../../vendor/autoload.php';
$loader->add('Holy', __DIR__ . '/classes');

use Silex\Application;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

$app = new Application();

$app->register(new \Silex\Provider\UrlGeneratorServiceProvider());

$app['smarty'] = $app->share(function(Application $app) {

    $smarty = new \Smarty();

    $smarty->template_dir = __DIR__ . '/templates';
    $smarty->compile_dir = sys_get_temp_dir();
    $smarty->force_compile = true;
    $smarty->escape_html = true;

    $urlPlugin = new \Holy\Smarty\UrlGeneratorPlugin($app['url_generator']);

    $smarty->registerPlugin('function', 'path', array($urlPlugin, 'path'));
    $smarty->registerPlugin('function', 'url', array($urlPlugin, 'url'));

    return $smarty;
});

// Home
$app->get('/', function(Application $app, Request $request) {

    $data = new \StdClass();
    $data->title = 'ホーム';
    $data->message = 'ホームですよ';
    $data->name = 'ゲスト';

    $app['smarty']->assign('this', $data);

    return new Response(
        $app['smarty']->fetch('index.html')
    );

})->bind('home');

// Hello
$app->get('/hello', function(Application $app, Request $request) {

    $name = $request->get('name', '名無し');

    if (!is_string($name)) {
        throw new BadRequestHttpException('Invalid Name.');
    }

    $data = new \StdClass();
    $data->title = 'Hello';
    $data->message = sprintf('こんにちは %s さん', $name);
    $data->name = 'ゲスト';

    $app['smarty']->assign('this', $data);

    return new Response(
        $app['smarty']->fetch('index.html')
    );

})->bind('hello');


$app->run();

Silex\Controller::bind('hello') でリクエストハンドラに設定したルーティング名を、テンプレート関数で {path route="hello" name="foo"} のように指定すると、対象のURL (/hello?name=foo) に展開されます。

今回はUrlGeneratorの動作がより分かりやすくなるよう、パーセントエンコードが必要な文字をテンプレート変数経由でクエリパラメータに渡してみます。

($smarty->escape_html プロパティによる自動HTMLエスケープを有効にしているため、クエリパラメータを含むURLを直接記述したケースはちょっとめんどくさいことになってます)

テンプレートファイル

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <title>{$this->title} - サブディレクトリ運用するテスト</title>
</head>
<body>

  <h1>{$this->title}</h1>

  <p>{$this->message}</p>

  <h2>URLを直接記述</h2>
  <ul>
    <li><a href="/sub/">ホーム</a></li>
    <li><a href="/sub/hello?name={$this->name|escape:url nofilter}">Hello</a></li>
  </ul>

  <h2>動的生成 (パス)</h2>
  <ul>
    <li><a href="{path route="home"}">ホーム</a></li>
    <li><a href="{path route="hello" name=$this->name}">Hello</a></li>
  </ul>

  <h2>動的生成 (Absolute-URL)</h2>
  <ul>
    <li><a href="{url route="home"}">ホーム</a></li>
    <li><a href="{url route="hello" name=$this->name}">Hello</a></li>
  </ul>

</body>
</html>

ビルトインサーバでの開発時のドキュメントルートをサブディレクトリに設定して、URL動的生成の効果を試してみます。

リンク先 C:\php\php.exe -S 127.0.0.1:8080 router.php

作業フォルダ C:\Users\k-holy\Projects\test\public_html\sub

それぞれの実行結果は、以下のようになりました。

  <h2>URLを直接記述</h2>
  <ul>
    <li><a href="/sub/">ホーム</a></li>
    <li><a href="/sub/hello?name=%E3%82%B2%E3%82%B9%E3%83%88">Hello</a></li>
  </ul>

  <h2>動的生成 (パス)</h2>
  <ul>
    <li><a href="/">ホーム</a></li>
    <li><a href="/hello?name=%E3%82%B2%E3%82%B9%E3%83%88">Hello</a></li>
  </ul>

  <h2>動的生成 (Absolute-URL)</h2>
  <ul>
    <li><a href="http://localhost:8080/">ホーム</a></li>
    <li><a href="http://localhost:8080/hello?name=%E3%82%B2%E3%82%B9%E3%83%88">Hello</a></li>
  </ul>

ドキュメントルートが変わったことで、テンプレート内にURLを直接記述しているケースではリンク先へのリクエストがステータス404になってしまいますが、UrlGeneratorで動的生成したURLは問題なく動作します。

また、クエリパラメータも自動でパーセントエンコードしてくれるようです。

さらに、hostsファイルに 127.0.0.1 sub.localhost を定義して http://sub.localhost:8080/ へリクエストしたところ、以下のような結果となりました。

  <h2>URLを直接記述</h2>
  <ul>
    <li><a href="/sub/">ホーム</a></li>
    <li><a href="/sub/hello?name=%E3%82%B2%E3%82%B9%E3%83%88">Hello</a></li>
  </ul>

  <h2>動的生成 (パス)</h2>
  <ul>
    <li><a href="/">ホーム</a></li>
    <li><a href="/hello?name=%E3%82%B2%E3%82%B9%E3%83%88">Hello</a></li>
  </ul>

  <h2>動的生成 (Absolute-URL)</h2>
  <ul>
    <li><a href="http://sub.localhost:8080/">ホーム</a></li>
    <li><a href="http://sub.localhost:8080/hello?name=%E3%82%B2%E3%82%B9%E3%83%88">Hello</a></li>
  </ul>

Absolute-URLの場合はホスト名の変更がきちんと反映されました。

もちろん サブディレクトリで RewriteBase ディレクティブを設定して運用するケースでも、問題なく動作します。

レンタルサーバの Gehirn RS2 での実行結果です。

  <h2>URLを直接記述</h2>
  <ul>
    <li><a href="/sub/">ホーム</a></li>
    <li><a href="/sub/hello?name=%E3%82%B2%E3%82%B9%E3%83%88">Hello</a></li>
  </ul>

  <h2>動的生成 (パス)</h2>
  <ul>
    <li><a href="/sub/">ホーム</a></li>
    <li><a href="/sub/hello?name=%E3%82%B2%E3%82%B9%E3%83%88">Hello</a></li>
  </ul>

  <h2>動的生成 (Absolute-URL)</h2>
  <ul>
    <li><a href="http://kholy.gehirn.ne.jp/sub/">ホーム</a></li>
    <li><a href="http://kholy.gehirn.ne.jp/sub/hello?name=%E3%82%B2%E3%82%B9%E3%83%88">Hello</a></li>
  </ul>

動作サンプル http://kholy.gehirn.ne.jp/sub/

また、ソースを追って調べたところ、Silex\Controller::assert('_scheme', 'https') と設定することによって、特定のルーティングへのURL生成時にhttpsスキームのURLを生成することもできるようです。

SSL必須とする処理に非SSLでリクエストされた場合にリダイレクトするケースもあるかと思いますが、動的生成することで余計なリダイレクトを減らせる利点もありますね)

このようなURLの動的生成は、Webアプリケーションでは当然のように実装されていますが、私自身はこれまで、いわゆる「綺麗なURL」の時代にテンプレートの可読性を犠牲にするような機能が必要なんだろうかと疑問に思っていました。

しかし今回、アプリケーションの可搬性を高める上で役立つケースもあると理解した次第です。