DNodeを使ってnodejsからcakephpのModelとかrequestActionを実行する

March 23, 2013,
tags: cakephp dnode nodejs php


このエントリーをはてなブックマークに追加

全国のnode.jsとcakephp好きの皆さんこんばんは。
最近cakePHPで書いたコードをnode.jsに移植したいとか若干思った僕がいます。

趣味のプロジェクトとかなら、一気にコードの書き換えとかでもありですが、現実的なプロジェクトだと徐々に移行したいとかあると思います。
というか、既存のロジックも使いつつ、nodeに徐々に移行したいって話は結構あると思います。

今回はそいういう徐々に移行するという状況を想像して、実際にどういうプランで移植するか考えてみました。

いろいろ方法はあると思いますがだいたい思いつく範囲はこんなところではないでしょうか?
・Web API的にcakephp側で対応させる
・node側でchild_processとかつかってsystem call的にcakeのshellとか実行する
・DNodeを使ってRPC的にcakeのメソッドを実行する

現実的に考えて、たぶんnodeからcakeを呼ぶとか別サーバになることが多いのでchild_processとかつかってsystem callとかは無理かと。
Web API的にcakephp側で、対応するってのもなかなかコストが掛かりそうなイメージが・・・場合によってはありですがw

で、DNodeを使う方法ですが、実際PHPとnodeでDNode連携できるのである意味これが一番現実的かと思います。
手順としては以下になります。
・cakephp側でDNode用のshellを用意する
・node側からDNodeを使ってcakeのメソッドをコールする。

つまりcakephp側がserver側になって、node側がクライアントになるってことですね。
cakeの全部のメソッド呼び出すってのはさすがに無理な話なんで、以外と必要かと思われるmodelのメソッドを実行するってのとrequestAction()を利用して出力するhtmlを取得するってところを対応したいと思います。

では実際にコードを晒ていきたいと思います。

まずはcake側のDNodeのライブラリをよういする

プロジェクト直下のディレクトリにcomposer.jsonを用意します。

{
    "require": {
        "php": ">=5.3.0",
        "evenement/evenement": "1.0.*",
        "react/socket": "0.2.*",
    "dnode":"*"
    },
    "autoload": {}
}

composerでインストールする

$ cd project # プロジェクト直下に移動
$ composer.phar install

DNodeをダウンロードしてvendorディレクトリにいれる

https://github.com/bergie/dnode-phpから落としてきて、composer.phar installで出来た「vendor」ディレクトリの中にdenodeを突っ込む。
ちなみにlsするとディレクトリはこんな感じになります。

polidog$ ls -alh
-rw-r--r--   1 polidog  staff   182B  3 23 23:21 autoload.php
drwxr-xr-x   7 polidog  staff   238B  3 23 23:14 composer
drwxr-xr-x   4 polidog  staff   136B  3 23 23:16 dnode   ← ここにdnodeディレクトリを突っ込む
drwxr-xr-x   3 polidog  staff   102B  3 23 23:14 evenement
drwxr-xr-x   5 polidog  staff   170B  3 23 23:14 react

[追記]
autoloadの調整をしなければいけないのでvendor/composer/autoload_namespaces.phpを修正します。

Usage:
  [options] command [arguments]
<?php

// autoload_namespaces.php generated by Composer

$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);

return array(
    'React\\Stream' => $vendorDir . '/react/stream/',
    'React\\Socket' => $vendorDir . '/react/socket/',
    'React\\EventLoop' => $vendorDir . '/react/event-loop/',
    'Evenement' => $vendorDir . '/evenement/evenement/src',
    'DNode'     =>$vendorDir . '/dnode/src',
);

※composerの使い方とかautoloaderの使い方とか詳しい人、もっとdnodeの良い導入方法あったら教えてくださいw

AppShellの拡張

モデルが自由にロード出来る用にAppShellを拡張します。

<?php
App::uses('Shell', 'Console');

/**
 * Application Shell
 *
 * Add your application-wide methods in the class below, your shells
 * will inherit them.
 *
 * @package       app.Console.Command
 */
class AppShell extends Shell {

  public function loadModel($modelName) {

    App::uses('ClassRegistry', 'Utility');

    $uses = array($modelName);

    $modelClassName = $uses[0];
    if (strpos($uses[0], '.') !== false) {
      list($plugin, $modelClassName) = explode('.', $uses[0]);
    }
    $this->modelClass = $modelClassName;

    foreach ($uses as $modelClass) {
      list($plugin, $modelClass) = pluginSplit($modelClass, true);
      $this->{$modelClass} = ClassRegistry::init($plugin . $modelClass);
    }

    return true;
  }
}

DnodeShellの作成

app/Console/CommandにDnodeShellを作成する
ちなみにDnodeShellの中にはDNodeShellクラスとDNodeShellWrapperクラスを作成します。
コード的には以下のような感じです。

<?php
App::import('Routing', 'Router');
App::import('Sanitize');
config('routes');

class DNodeShell extends AppShell
{
  public function main() {
    require __DIR__."/../../../vendor/autoload.php";
    $loop = new React\EventLoop\StreamSelectLoop();
    $server = new DNode\DNode($loop, new DNodeShellWrapper($this));
    $server->listen(7070);
    $loop->run();
  }

  /**
   * requestActionを実行する
   * @param string $path
   * @param array $post
   * @param array $configs
   * @return string
   */
  public function callAction($path,$post,$configs = null) {
    $url = Router::url($path);
    Configure::write('debug',0);
    $html = $this->requestAction($url,array('return','bare'=>0,'webroot'=>'/','base'=>'/'));
    $html = str_replace(dirname(__DIR__),$configs->domain,$html);
    return $html;
  }

  /**
   * modelを呼び出す
   * @param string $modelName
   * @param string $method
   * @param array $args
   * @return type
   */
  public function callModel($modelName,$method,$args) {
    if ( !isset($this->$modelName) ) {
      $this->loadModel($modelName);
    }
    return call_user_func(array($this->$modelName,$method),$args);
  }

}

class DNodeShellWrapper {
  private $shell = null;

  public function __construct($shell) {
    $this->shell = $shell;
  }

  public function callAction($path,$post,$configs,$callback) {
    $ret = $this->shell->callAction($path,$post,$configs);
    $callback($ret);
  }

  public function callModel($model,$method,$args,$callback) {
    $ret = $this->shell->callModel($model,$method,$args);
    $callback($ret);
  }
}

若干気持ち悪い実装ですが、DNodeShell::main()でphp側のDNodeを実行します。
他のメソッドでcakeの機能を提供しますが、DNodeShellWrapperクラスを経由して実行するようにしてます。

ここからはnode側の準備をします。

expressのインストールとか

polidog$ npm install -g express
polidog$ express hoge
polidog$ cd hoge

node側の処理の実装

こんな感じで対応します。

// app.js
/**
 * Module dependencies.
 */

var express = require('express')
  , cake = require('./routes/cake')
  , http = require('http')
  , path = require('path');

var app = express();

app.configure(function(){
  app.set('port', process.env.PORT || 3000);
  app.set('views', __dirname + '/views');
  app.set('view engine', 'jade');
  app.use(express.favicon());
  app.use(express.logger('dev'));
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(express.cookieParser('your secret here'));
  app.use(express.session());
  app.use(app.router);
  app.use(require('stylus').middleware(__dirname + '/public'));
  app.use(express.static(path.join(__dirname, 'public')));
});

app.configure('development', function(){
  app.use(express.errorHandler());
});

app.get('/requestAction', cake.requestAction);
app.get('/model',cake.model);

http.createServer(app).listen(app.get('port'), function(){
  console.log("Express server listening on port " + app.get('port'));
});
// routes/cake.js

var dnode = require('dnode');

exports.requestAction = function(req, res){
  dnode.connect(7070).on('remote',function(remote){
    remote.callAction('/',null,{'domain':'localhost:3000'},function(html){
      // console.log(html);
      res.set('Content-Type', 'text/html');
      res.send(html);
    });
  });
};


exports.model = function(req, res) {
  dnode.connect(7070).on('remote',function(remote){
    remote.callModel('Message','findById',[1],function(result){
      var text = "title:"+result.Message.title;
      text += "\n";
      text += "body:" + result.Message.body;
      res.set('Content-Type', 'text/plain charset=utf8');
      res.send(text);
    });
  });
}

cakephp側のdnode serverを起動

polidog$ app/Console/cake DNode main

node側の起動

node app.js

実行してみる

ブラウザに「http://localhost:3000/requestAction」と実行します。
するとスタイルシートは多分崩れるけど、cakeのhtml出力が取得できます。

同じ要領で、「http://localhost:3000/model」とやるとcake.jsの方で指定したmodelとmethodが実行されます。
今はcake.jsでMessageモデルのfindByIdをコールしているのでそれが実行されます。ちなみに第3引数にはcakeに送りたい値を配列で送れば引数が渡せます。

と行った形でやればRPC的にnodeからcakephpのメソッドが利用できるかと思います。
無駄な取り組みかもですが、nodeとphpを組み合わせが簡単に出来るってことはうまく使えば効率的な開発が出来るかもしれませんw
ぜひぜひ興味を持った方は他のフレームワークでもいいですし、試してもらえたら嬉しいです。

comments powered by Disqus