CakePHP on Java Servlet - quercus 使用

前回 id:fits:20101111 に続き、今度は CakePHP を Java Servlet Engine(jetty)上で実行してみました。

使用した環境は以下の通り。CakePHP 以外は id:fits:20101111 と同じです。

サンプルのソースコードCakePHP の変更ファイルは http://github.com/fits/try_samples/tree/master/blog/20101112/

Maven プロジェクトの作成・設定や quercus のファイルの配置に関しては id:fits:20101111 を参照。

CakePHP を Java Servlet で実行するための作業

Java Servlet(quercus)で CakePHP を実行するには、以下を実施します。

  • CakePHPのシングルトンの実装を変更
  • CakePHP用の Servlet Filter を用意
  • キャッシュを無効化

なお、事前準備として src/main/webapp に CakePHP のソースファイルを配置して、core.php に必要最小限の設定を行っておきます。

CakePHPのシングルトンの実装を変更

まず、シングルトンの実装を変更します。
これをやらないとシングルトンが有効に機能せず、ページの表示時に Fatal Error が発生し、CakePHP が正常に動作しません。

cake/libs 内のソースファイルを対象に getInstance の実装を以下のように変更します。

変更後(src/main/webapp/cake/libs/configure.php
function &getInstance() {
    if (is_null(self::$instance)) {
        self::$instance =& new App();
        self::$instance->__map = (array)Cache::read('file_map', '_cake_core_');
    }
    return self::$instance;
}
//$instance を外出し
static $instance = null;

ちなみに、元の実装は以下のようになっています。

変更前(src/main/webapp/cake/libs/configure.php
function &getInstance() {
    static $instance = array();
    if (!$instance) {
        $instance[0] =& new App();
        $instance[0]->__map = (array)Cache::read('file_map', '_cake_core_');
    }
    return $instance[0];
}

なお、変更対象ファイルは以下のようなものです。

シングルトンの実装変更対象ファイル

また、変数未定義の Notice が発生する箇所も必要に応じて修正します。(例えば、debugger.php の compact に渡す helpID の変数定義など)

CakePHP用の Servlet Filter を用意

.htaccess の代わりにアクセス先を変更する Servlet Filter を用意します。

必要最低限の処理はファイルの有無に応じて app/webroot もしくは app/webroot/index.php?url=[URL] にフォワードする事です。今回も自作してみました。

src/main/java/fits/sample/CakeRoutingFilter.java
package fits.sample;
・・・
public class CakeRoutingFilter implements Filter {
    ・・・
    public void init(FilterConfig config) {
        this.ctx = config.getServletContext();
    }
    //フィルター処理
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
        HttpServletRequest req = (HttpServletRequest)request;
        String contextPath = req.getContextPath();
        String requestPath = req.getRequestURI();
        String path = requestPath.replace(contextPath + "/", "").trim();

        if (!path.startsWith("app/webroot")) {
            String vPath = "app/webroot/" + path;
            String rPath = this.ctx.getRealPath(vPath);

            if (new File(rPath).exists()) {
                this.dispatchForward("/" + vPath, request, response);
                return;
            }
            else {
                this.dispatchForward("/app/webroot/index.php?url=" + path, request, response);
                return;
            }
        }
        chain.doFilter(request, response);
    }
    ・・・
    private void dispatchForward(String path, ServletRequest request, ServletResponse response) throws ServletException, IOException {
        request.getRequestDispatcher(path).forward(request, response);
    }
}

キャッシュを無効化

キャッシュが有効な状態だと 2度目以降のアクセスで例外が発生するようになるため、app/config/core.php で Cache.disable を true に設定しキャッシュを無効化します。

src/main/webapp/app/config/core.php
    Configure::write('Cache.disable', true);

動作確認

DB に MySQL を使って、簡単なモデル・ビュー・コントローラーを実行してみます。

DB・テーブルを作成(sql/todo_db.sql 参照)し、app/config/database.php を作成します。
なお、src/main/webapp/WEB-INF/lib に MySQLJDBC ドライバー(mysql-connector-java-5.1.13-bin.jar)を配置しておきます。

src/main/webapp/app/config/database.php
<?php
class DATABASE_CONFIG {
    var $default = array(
        'driver' => 'mysql',
        'persistent' => false,
        'host' => 'localhost',
        'login' => 'root',
        'password' => '',
        'database' => 'todo',
        'prefix' => '',
        'encoding' => 'utf8'
    );
}

モデル・コントローラーを作成します。

src/main/webapp/app/models/datasources/tasks.php
<?php
class Task extends AppModel {
    var $name = "Task";
}
src/main/webapp/app/controllers/tasks_controller.php
<?php
class TasksController extends AppController {
    var $name = "Tasks";

    function index() {
        $tasks = $this->Task->find('all');
        $this->set('tasks', $tasks);
    }
    //追加
    function add() {
        $this->Task->save($this->data);
        $this->redirect('.');
    }
}

ビューを作成します。(レイアウトは src/main/webapp/app/views/layouts/default.ctp 参照)
Java Servlet として実行する場合、find の結果がテーブル名 tasks の連想配列の配列になるようなので注意。(通常はモデル名 Task の連想配列の配列になる)

src/main/webapp/app/views/tasks/index.ctp
<div>
<?php
  echo $form->create('Task', array('type' => 'POST', 'action' => 'add'));
  echo $form->label(null, 'タイトル');
  echo $form->text('Task.title');
  echo $form->submit('追加');
  echo $form->end();
?>
</div>
<div>
  <table>
    <tr>
      <th>ID</th>
      <th>タイトル</th>
      <th>登録日</th>
      <th>更新日</th>
    </tr>
    <?php
      foreach ($tasks as $task) {
        //Java で実行する場合はテーブル名 tasks の連想配列になっている
        //XAMPP で普通に実行する場合はモデル名 Task の連想配列になる
        $t = $task['tasks'];
    ?>
    <tr>
      <td><?php echo $t['task_id']; ?></td>
      <td><?php echo $t['title']; ?></td>
      <td><?php echo $t['created']; ?></td>
      <td><?php echo $t['modified']; ?></td>
    </tr>
    <?php
      }
    ?>
  </table>
</div>

jetty を起動して http://localhost:8080/cakephp-sample/tasks/ にアクセスすれば、一応動作している事が確認できると思います。

jetty 起動
> mvn jetty:run

ただし、原因は未調査ですが。
タイトルに日本語を入力して「追加」ボタンを押すと文字化けするようなので注意。(同一コードを XAMPP で実行しても文字化けは発生しない)