symfonyとHTMLファイルを同居させた際のリンク切れ回避方法

井川です。こんにちは。

Webシステム構築をしていると、動的なシステムと静的なHTMLファイル(ドキュメントルート以下)を同時に配置することがあります。symfonyを使ってこのようなシステムやサイトを構築すると、HTMLファイルへの特定のリンクでは404エラーが発生してしまいます。今回はこれを回避する簡単な方法をご紹介します。

それらのファイルへアクセスでき、簡単に同居できます。しかしながら、リンクの書き方如何によっては、問題が発生します。例えば、/outline/index.htmlへリンクを貼りたい場合(webディレクトリ以下に配置)、次のようなリンクはsymfonyのモジュールとindex.htmlのどちらを意味するのでしょうか。


<a href="/outline">…</a>
<a href="/outline/">…</a>

リンク先でリクエストはsymfonyに渡され、symfonyではこれらのリンク先をモジュールと解釈します。そのため、仮に/outline/index.htmlが存在していても、404エラーをはいてしまいます。こういったリンクが膨大な場合や、新しいテンプレートファイルでリンク先を書き間違えた場合など、問題が発生する可能性は決して低くありません。

この問題を回避する方法の1つは、以下のようにフロントコントローラを作成することです(他の方法も多々あるとは思います)。このコントローラではsfFrontWebControllerを継承したクラスにcheckHtmlUriメソッドを加え、dispatchメソッドを上書きしています。

dispatchメソッドの中の変更点は、sfExceptionをキャッチしたブロック内部です。ここでは次のような処理を実行しています。

① 例外がsfError404Exceptionであるか確認する
② リクエストURLの最後がディレクトリである可能性を確認する
③ リクエストURL+index.htmlにファイルがあるか確認する
④ リクエストURL+index.htmlにリダイレクトする

checkHtmlUriメソッドでは②と③の処理を行い、リダイレクト先URLを設定します。dispatchメソッドでは、まず①の実行とcheckHtmlUriメソッドの呼び出しを行います。そして最終的に④のリダイレクトを行います。


<?php
/**
 * フロントウェブコントローラ
 * 
 * @package    blog
 * @subpackage controller
 * @author     Kazushi IGAWA <kazushi@asial.co.jp>
 * @date       2011.02.15
 */
class TestFrontWebController extends sfFrontWebController
{
  /**
   * インデクスファイル名
   */
  const INDEX_FILE_NAME = 'index.html';
  /**
   * リダイレクト先
   * @var string
   */
  private $redirectUrl = '';
  /**
   * リクエストを処理する
   *
   * @param  void
   * @return void
   */
  public function dispatch()
  {
    try {
      // reinitialize filters (needed for unit and functional tests)
      sfFilter::$filterCalled = array();
      
      // determine our module and action
      $request    = $this->context->getRequest();
      $moduleName = $request->getParameter('module');
      $actionName = $request->getParameter('action');
      
      if (empty($moduleName) || empty($actionName)) {
        throw new sfError404Exception(sprintf('Empty module and/or action after parsing the URL "%s" (%s/%s).', $request->getPathInfo(), $moduleName, $actionName));
      }
      
      // make the first request
      $this->forward($moduleName, $actionName);
    } catch (sfException $e) {
      if ($e instanceof sfError404Exception  & & $this->checkHtmlUri($request)) {
        $this->redirect($this->redirectUrl);
      }
      
      $e->printStackTrace();
    } catch (Exception $e) {
      sfException::createFromException($e)->printStackTrace();
    }
  }
  /**
   * HTMLファイルが存在するか確認する
   * 
   * @param  sfWebRequest $request
   * @return boolean true=静的ファイルあり, false=静的ファイルなし
   */
  protected function checkHtmlUri(sfWebRequest $request)
  {
    $uri  = $request->getUri();
    
    if ('/' !== substr($uri, -1)) {
      return false;
    }
    
    $filePath = sfConfig::get('sf_web_dir') . $request->getPathInfo() . self::INDEX_FILE_NAME;
    
    if (!file_exists($filePath)) {
      return false;
    }
    
    $this->redirectUrl = $uri . self::INDEX_FILE_NAME;
    return true;
  }
}

後は、factories.ymlでコントローラを設定し、上記コントローラを組み込めば、当初の問題であった"/outline/"のようなリンクでも"/outline/index.html"へ遷移します。もちろん、symfonyの従来の機能(404エラーなど)はそのまま使用可能です。


all:
  controller:
    class: TestFrontWebController

今回はコントローラのみを書き換える方針で作成しました。各クラスの役割を明確にするのであれば、checkHtmlUriメソッドやリダイレクト先はリクエストクラスに持たせた方がスッキリするかもしれません。