May 26, 2013

Backbone.jsのModelでYouTubeのデータをfetchする

思いついたので、試してみました。

Modelは、以下の様な感じです。
var YoutubeModel = Backbone.Model.extend({
    urlRoot: '',
    defaults: {
        'title': '',
        'url': ''
    },
    url: function() {
        if (this.id) {
            return 'https://gdata.youtube.com/feeds/api/videos/' + this.id + '?v=2&alt=jsonc';
        } else {
            return '';
        }
    },
    parse: function(res) {
        return {
            title: res.data.title,
            url: 'http://www.youtube.com/watch?v=' + res.data.id
        }
    },
    save: function() {
        console.log('save() is not supported.');
    },
    destroy: function() {
        console.log('destroy() is not supported.');
    }
});
fetchしてみます。
var youtubeModel = new YoutubeModel({id: 'MDBLhdSy5t4'});
youtubeModel.fetch({success: function(model, res) {
    console.log('title: ' + model.get('title'));
    console.log('url: ' + model.get('url'));
}});
コンソールにタイトルとURLが表示されたら成功です。もちろん、YouTube以外の外部サービスでも(jsonさえ返してくれれば)使えるはずです。そして、外部サービスと通信している事実をModelの中に局所的に閉じ込められるので、後が楽そうですね。

ViewとCollectionと組み合わせればインクリメンタルサーチとかもスッキリ実装できそうだなー。

FuelPHPのバリデーションで自身を除いた一意性チェックのより冴えた(?)方法

先に、以下をご覧ください。

FuelPHPのバリデーションで自身を除いた一意性チェック
http://madroom-project.blogspot.jp/2013/05/fuelphp.html

今回は、上記の記事の内容を、より汎用的に使う方法です。

Configファイル(仮に config/app.php とします)に、以下を記述します。
<?php

return array(
    'functions' => array(
        '_validation_not_taken' => function($val, $model_name, $id, $column) {
            if (empty($id))
            {
                $exclude_val = null;

            }
            else
            {
                $exclude = $model_name::find($id);

                $exclude_val = ! empty($exclude->$column) ?
                    $exclude->$column : null;
            }

            if ($exclude_val !== null and $exclude_val === $val)
            {
                return true;
            }

            $method_name = 'find_by_'.$column;
            $model = $model_name::$method_name($val);
            return empty($model);
        }
    ),
);
以下のように使います。
$v = Validation::forge();
$v->add('uname', 'uname')
    ->add_rule(array('already_taken' => Arr::get(Config::get('app.functions'), '_validation_not_taken')),
        'Model_Xxx', $self_id, 'uname');
add_ruleに渡すパラメータは、対象のモデル名、除外したいレコードのID値、対象となるカラム名。です。

対象となるカラム名を配列で渡せるように拡張すれば、複合チェックも出来そうです。(今回はそこまではやっていませんが。)

Config::get('app.functions._validation_not_taken')のようにクロージャを直接Config::getすると、どうも
https://github.com/fuel/core/blob/888f6d914f3edca33d3ad4fab002b5ddce4bf5b1/classes/fuel.php#L341
で即コールされてしまうようで、Config::getでクロージャ一覧の配列をgetして、更にArr::getしています。

ホントは独自クラスに実装した方が良いのかもしれませんが、Configだと手元にあるファイルだけで出来て楽だなとも思いました。

Jasmineをブラウザとコマンド(PhantomJS)から実行するメモ

メモです。

Jasmine
http://pivotal.github.io/jasmine/
公式サイトです。

GitHub
https://github.com/pivotal/jasmine
GitHubです。

Standalone版
https://github.com/pivotal/jasmine/downloads
ブラウザで実行する用のスタンドアロン版です。

PhantomJSでの実行は
https://github.com/jcarver989/phantom-jasmine
から
lib/console-runner.js
lib/run_jasmine_test.coffee
をDL。
phantomjs run_jasmine_test.coffee XxxRunner.html
で出来ました。XxxRunner.htmlの書き方は
https://github.com/jcarver989/phantom-jasmine/blob/master/examples/TestRunner.html
を参考にして下さい。

実はその前に
sudo npm install phantom-jasmine -g
してphantom-jasmineコマンドから実行しようとしたんですけどエラーが出ました。
sudo npm uninstall phantom-jasmine -g
した後でも正しく実行できました。(たぶん正しかったんだと思う。)

May 25, 2013

jquery-pjax後にHTMLのタイトルを変更する

色々やり方はあると思いますが、以下の方法が簡単でした。

JS側で、ajaxCompleteを監視します。ajaxCompleteイベントが発生したら、'x-pjax-title'という名前のレスポンスヘッダでタイトルを変更します。
$('body').bind('ajaxComplete', function(event, xhr) {
    if (xhr.getResponseHeader('x-pjax-title')) {
        $('title').text(xhr.getResponseHeader('x-pjax-title'));
    }
});
'x-pjax-title'は、PHPだったら
header('x-pjax-title: xxx');
で出力します。

参考:
https://github.com/rails/pjax_rails/pull/22

FuelPHPのバリデーションで自身を除いた一意性チェック

2013/05/26 追記
より冴えた(?)方法を書きました。
http://madroom-project.blogspot.jp/2013/05/fuelphp_26.html

--

例えばユーザのアカウント名は、対応するカラムの制約がUNIQUEになりますが、これを事前にバリデーションでエラーとする方法です。

FuelPHPのValidationクラスのadd_ruleメソッドにはクロージャを登録出来ます。
http://fuelphp.jp/docs/1.6/classes/validation/errors.html#/naming_rules

クロージャに自身のレコードのidを渡せば、自身を除いた一意性チェックが出来るわけですが、以下の方法で渡せました。尚、細かく作りこんでいませんので、穴があったらごめんなさいm(_ _)m
$v = Validation::forge();
$v->add('uname', 'uname')
    ->add_rule(array('already_taken' => function($val, $self_id) {
        if (empty($self_id))
        {
            $my_uname = null;

        }
        else
        {
            $self = Model_Xxx::find($self_id);
            $my_uname = ! empty($self->uname) ? $self->uname : null;
        }

        if ($my_uname !== null and $my_uname === $val)
        {
            return true;
        }

        $ret = Model_Xxx::find_by_uname($val);
        return empty($ret);
    }), $self_id);
* $self_idが、自身のidです。
* 'already_taken'は、バリデーション用メッセージファイルの"lang/xx/validation.php"のキーに対応します。
* 別途、requiredチェックとかはして下さい。

これで、クロージャの中でInputクラスとかSessionクラスとか使わなくて済みます。

May 24, 2013

MonologでChromeのコンソールにログを出す

Chrome Loggerをインストールします。
https://chrome.google.com/webstore/detail/chrome-logger/noaneddfkdjfnfdakjjmocngnfkfehhd

Chromeの右上にアイコンが出ます。クリックでON(青)/OFF(黒)できます。

PHP側で
$log = new Monolog\Logger('Logger');
$log->pushHandler(new Monolog\Handler\ChromePHPHandler());
$log->addDebug('Foo');
とすると、ChromeのコンソールにDebugレベルで"Foo"が表示されました。

Firefoxでも同じように、FirebugとFirePHP
http://getfirebug.com/
https://addons.mozilla.org/en-US/firefox/addon/firephp/
をインストールして

PHP側で
$log = new Monolog\Logger('Logger');
$log->pushHandler(new Monolog\Handler\FirePHPHandler());
$log->addDebug('Foo');
とすれば出るかなーと思ったんですけど、何故か出ない。。。メインがChromeだから良いけど。。。

May 18, 2013

ApiGenでFuelPHPのドキュメントを生成してみる

FuelPHPはほとんど関係ありませんが、ネタとして。

ApiGenでPHPドキュメントが作成出来ます。
http://apigen.org/
https://github.com/apigen/apigen

PEARでインストールしてみます。
$ pear config-set auto_discover 1
$ pear install pear.apigen.org/apigen
FuelPHPのルートで以下を実行してみます。
$ apigen -s fuel/core/ -d apigen
ApiGen 2.8.0
------------
Scanning /xxx/fuelphp-1.6/fuel/core
[===============================================================>] 100.00%  34MB
Found 264 classes, 0 constants, 38 functions and other 19 used PHP internal classes
Documentation for 264 classes, 0 constants, 38 functions and other 19 used PHP internal classes will be generated
Using template config file /Applications/MAMP/bin/php/php5.4.4/lib/php/data/ApiGen/templates/default/config.neon
Generating to directory apigen
[===============================================================>] 100.00%  88MB
Done. Total time: 2 min 28 sec, used: 91 MB RAM
* -sでターゲットディレクトリ、-dで出力ディレクトリを指定しています。
* 結構時間がかかりました。

apigen/index.htmlに、ブラウザからアクセスしてみます。

phpDocumentor 2より楽だなー。

--

関連:
phpDocumentor 2のインストール手順(Mac/Win/Linux)
http://madroom-project.blogspot.jp/2012/12/phpdocumentor-2macwin.html

Ratchetのユニットテストを実行してみる(AutobahnTestsuite側)

以下の続きです。

Ratchetのユニットテストを実行してみる(とりあえずPHPUnit側)
http://madroom-project.blogspot.jp/2013/05/ratchetphpunit.html

AutobahnTestsuiteのテストケースをMacで実行してみました。AutobahnTestsuiteはPython製です。解決できていないエラーも結構ありますが、情報も少ないですし、とりあえずの実行結果メモということでm(_ _)m


AutobahnTestsuite
http://autobahn.ws/testsuite
https://github.com/tavendo/AutobahnTestSuite


1.
AutobahnTestsuiteをインストールします。easy_installでインストールしてみました。
$ sudo easy_install autobahntestsuite
# 略
Finished processing dependencies for autobahntestsuite
$ wstest --help
# ヘルプが表示されること
GitHubからのインストール方法は以下を参考にして下さい。
https://github.com/tavendo/AutobahnTestSuite#installation


2.
次に、Twistedというものをインストールしました。(これがないとエラー(ワーニング?)が出ました。)
$ wget http://twistedmatrix.com/Releases/Twisted/13.0/Twisted-13.0.0.tar.bz2
$ tar zxvf Twisted-13.0.0.tar.bz2
$ cd Twisted-13.0.0
$ sudo python setup.py install
Linuxもたぶんこの方法でいけるんだと思います。
Mac用のインストーラは http://twistedmatrix.com/trac/wiki/Downloads にありますが、バージョンが古いようです。Windowsのは、そのまま使えそうです。


3.
AutobahnTestsuiteのテスト(とりあえず"abtest")を実行してみます。尚、Makefileとtests/AutobahnTestSuiteの各jsonファイルに書かれているポート番号を、全て9000番台に変更しています。(いろいろぶつかるので。。。)
$ make abtest
ulimit -n 2048 && php tests/AutobahnTestSuite/bin/fuzzingserver-stream.php 9000 &
wstest -m fuzzingclient -s tests/AutobahnTestSuite/fuzzingclient-quick.json
Using Twisted reactor class 
Autobahn WebSockets 0.5.3/0.5.14 Fuzzing Client
Ok, will run 263 test cases against 1 servers
# Running test case ID x.x.x for agent Ratchet from peer 127.0.0.1:9000 のようなメッセージがずらずらと出ました。
# 結構時間がかかります。
killall php
$ /bin/sh: line 1:  4766 Terminated: 15          php tests/AutobahnTestSuite/bin/fuzzingserver-stream.php 9000
(終了したけどこれで良いのだろうか。)


4.
"abtests"を実行してみます。
$ make abtests
ulimit -n 2048 && php tests/AutobahnTestSuite/bin/fuzzingserver-libevent.php 9002 &
ulimit -n 2048 && php tests/AutobahnTestSuite/bin/fuzzingserver-stream.php 9001 &
ulimit -n 2048 && php tests/AutobahnTestSuite/bin/fuzzingserver-libev.php 9004 &
ulimit -n 2048 && php tests/AutobahnTestSuite/bin/fuzzingserver-libuv.php 9005 &
ulimit -n 2048 && php tests/AutobahnTestSuite/bin/fuzzingserver-noutf8.php 9003 &
wstest -m testeeserver -w ws://localhost:9000 &
wstest -m fuzzingclient -s tests/AutobahnTestSuite/fuzzingclient-all.json
# 以下のエラーが出ました。
# Fatal error: Class 'libev\EventLoop' not found
# Fatal error: Call to undefined function React\EventLoop\event_base_new()
# Fatal error: Class 'libev\EventLoop' not found
# でも進んでいるので、気長に様子を見てみます。(かなり時間がかかります。)
killall php wstest
macbook:Ratchet admin$ /bin/sh: line 1:  4980 Terminated: 15          php tests/AutobahnTestSuite/bin/fuzzingserver-noutf8.php 9003
/bin/sh: line 1:  4972 Terminated: 15          php tests/AutobahnTestSuite/bin/fuzzingserver-stream.php 9001
(プロセス殺し切れてる?)


5.
"profile"を実行してみます。
$ make profile
php -d 'xdebug.profiler_enable=1' tests/AutobahnTestSuite/bin/fuzzingserver-libevent.php 9000 &
wstest -m fuzzingclient -s tests/AutobahnTestSuite/fuzzingclient-profile.json
# 以下のエラーが出ました。
# Fatal error: Call to undefined function React\EventLoop\event_base_new()
Connection to ws://localhost:9000 failed (Connection was refused by other side: 61: Connection refused.)
killall php
No matching processes belonging to you were found
make: *** [profile] Error 1
(コケた。)



P.S.
各エラーは、解決次第、追記しようと思います。。。

May 17, 2013

Ratchetのユニットテストを実行してみる(とりあえずPHPUnit側)

PHP製WebSocketライブラリ(というよりはWebSocketフレームワーク?)のRatchetで、ユニットテストを実行してみました。
http://socketo.me/
https://github.com/cboden/Ratchet

とりあえずPHPUnitのテストケースを実行してみます。WebSocketのサーバサイドに対するテストなんだと思います。
$ git clone git://github.com/cboden/Ratchet.git
$ cd Ratchet/
$ curl https://getcomposer.org/installer | php
$ php composer.phar install
$ phpunit
PHPUnit 3.7.10 by Sebastian Bergmann.

Configuration read from /Users/admin/Desktop/ratchet/Ratchet/phpunit.xml.dist

...............................................................  63 / 365 ( 17%)
............................................................... 126 / 365 ( 34%)
............................................................... 189 / 365 ( 51%)
............................................................... 252 / 365 ( 69%)
............................................................... 315 / 365 ( 86%)
..................................................

Time: 1 second, Memory: 22.75Mb

OK (365 tests, 455 assertions)
あっさり。でも https://github.com/cboden/Ratchet/blob/master/Makefile を見ると、Ratchetのユニットテスト実行は、以下の方法が正しいみたいです。(まあ、同じなんですけど。)
$ make test
カバレッジレポートは
$ make cover
で生成出来ました。


Makefileに、その他で"abtests"、"abtest"、"profile"、"apidocs"が有ります。以下、根拠はありませんが、そんなに間違っていないとも思います。
1. "abtests"は、たぶん"AutobahnTestsuiteのテスト全部"の略なのだと思います。
2. "abtest"は、"AutobahnTestsuiteのテストの一部"になるのでしょうか。
3. "profile"は、"xdebug.profiler_enable=1"とあるので、Xdebugによるプロファイリングでしょう。(実は使ったこと無いので見なくちゃ。)
4. "apidocs"は、ドキュメントの生成のようです。 http://apigen.org/

上記1〜3は、AutobahnTestsuiteが関係しているようです。PHPUnitがWebSocketのサーバサイドに対するユニットテストであるなら、こっちはクライアントサイドのテストになるんだと思います。

AutobahnTestsuite
http://autobahn.ws/testsuite
https://github.com/tavendo/AutobahnTestSuite

AutobahnTestsuiteは、インストールは出来たのですが、実行するとエラーが出たので、別記事にしようと思います。

上記4の"apidocs"は、phpDocumentor2的なものと思います。これも、確認したら別記事として書こうと思います。コイツのせいで(まあ良い意味ですけど)やること増えてしまった。。。

--

追記:
AutobahnTestsuite関係について、別記事を書きました。
http://madroom-project.blogspot.jp/2013/05/ratchetautobahntestsuite.html

"apidocs"は、以下の通りApiGenをインストールして
http://madroom-project.blogspot.jp/2013/05/apigenfuelphp.html
$ make apidocs
で reports/api/ にドキュメントが生成出来ました。その中の index.html にアクセスするとドキュメントが表示されます。
http://socketo.me/api/index.html は、この方法で生成されているんだと思います。

May 13, 2013

FuelPHPの(新?)Twitterパッケージを作りました

先日、以下の記事を書きました。

FuelPHP v1.6でtmhOAuthを使ってTwitter認証と投稿をしてみる
http://madroom-project.blogspot.jp/2013/05/fuelphp-v16tmhoauth.html

認証周りをメソッド化してまとめていたら、なんとなくパッケージに出来そうだったので、作ってみました。(初代(?)Twitterパッケージも古くなってきている感じでしたし。)
https://github.com/mp-php/fuel-packages-twitter
https://packagist.org/packages/mp-php/fuel-packages-twitter

Composer / Gitのsubmodule / ZIPをDL と、好きな方法でfuel/packages/twitterとして配置します。尚、FuelPHP v1.6のcomposer.jsonを用いずに配置した場合、当パッケージディレクトリ直下でComposerによる依存パッケージ(tmhOAuth)のインストールを行う必要があります。

config.php の "always_load.packages" あるいは Package::load() で、Twitterパッケージを有効にします。

Twitterパッケージの config/twitter.php をapp側にコピーして "consumer_key" と "consumer_secret" を設定します。

認証についていは
https://github.com/mp-php/fuel-packages-twitter#authorization
を参考にして下さい。

ツイートについては
https://github.com/mp-php/fuel-packages-twitter#tweet
を参考にして下さい。

その他、TwitterのAPIは
https://dev.twitter.com/docs/api/1.1
に記載されています。

当パッケージのクラスに get / post / put / delete メソッドを用意してあるので、呼び出すAPIとパラメータを組み合わせて使っていく感じです。

なお、まだPOSTの1.1/statuses/updateしか確認してませんm(_ _)m

May 11, 2013

FuelPHP v1.6でtmhOAuthを使ってTwitter認証と投稿をしてみる

2013/05/13 追記:
tmhOAuth依存のTwitterパッケージを作りました。 http://madroom-project.blogspot.jp/2013/05/fuelphptwitter.html

--

各種FWで使えそうなTwitter用のライブラリを再考していて、Twitterのライブラリ一覧を見てみると"tmhOAuth"というのがありました。
https://dev.twitter.com/docs/twitter-libraries
https://github.com/themattharris/tmhOAuth

試しに、Composerでインストールして使ってみます。

composer.jsonに以下を追記します。
"themattharris/tmhoauth": "0.*"
インストールします。
$ php composer.phar update # またはinstall
以下を参考にした、認証コントローラのサンプルです。
https://github.com/themattharris/tmhOAuth-examples/blob/master/oauth_flow.php
<?php

class Controller_Oauth extends Controller
{

    public function action_login()
    {
        // OAuth用のSessionを削除
        Session::delete('oauth');

        // tmhOAuthのインスタンスを生成
        $twitter = new tmhOAuth(array(
            'consumer_key'    => 'YOUR_CONSUMER_KEY',
            'consumer_secret' => 'YOUR_CONSUMER_SECRET',
        ));

        // Request Tokenの取得
        $code = $twitter->request('POST', $twitter->url('oauth/request_token', ''));
        if ($code != 200)
        {
            throw new Exception('Invalid code.');
        }
        $params = $twitter->extract_params($twitter->response['response']);

        // OAuth用のSessionをセット
        Session::set('oauth.params', $params);

        // 認証画面にリダイレクト
        $url = $twitter->url('oauth/authorize', '')."?oauth_token={$params['oauth_token']}";
        Response::redirect($url);
    }

    public function action_callback()
    {
        // OAuth用のSessionを取得
        $params = Session::get('oauth.params');

        // tmhOAuthのインスタンスを生成
        $twitter = new tmhOAuth(array(
            'consumer_key'    => 'YOUR_CONSUMER_KEY',
            'consumer_secret' => 'YOUR_CONSUMER_SECRET',
        ));
        $twitter->config['user_token']  = $params['oauth_token'];
        $twitter->config['user_secret'] = $params['oauth_token_secret'];

        // Access Tokenの取得
        $code = $twitter->request(
            'POST',
            $twitter->url('oauth/access_token', ''),
            array(
                'oauth_verifier' => Input::get('oauth_verifier'),
            )
        );
        if ($code != 200)
        {
            throw new Exception('Invalid code.');
        }

        // データの確認
        $access_token = $twitter->extract_params($twitter->response['response']);
        print(' [user_id] : '.           $access_token['user_id']);
        print(' [screen_name] : '.       $access_token['screen_name']);
        print(' [oauth_token] : '.       $access_token['oauth_token']);
        print(' [oauth_token_secret] : '.$access_token['oauth_token_secret']);

        // OAuth用のSessionを削除
        Session::delete('oauth');

        exit();

        // 以下、投稿のサンプル
//        $twitter = new tmhOAuth(array(
//            'consumer_key'    => 'YOUR_CONSUMER_KEY',
//            'consumer_secret' => 'YOUR_CONSUMER_SECRET',
//            'user_token'      => $access_token['oauth_token'],
//            'user_secret'     => $access_token['oauth_token_secret'],
//        ));
//
//        $twitter->request('POST', $twitter->url('1.1/statuses/update'), array(
//            'status' => 'Test'
//        ));
//
//        print_r($twitter->response['response']);
//
//        exit();
    }

}
とりあえず、"tmhOAuth"で良いかなー。

May 6, 2013

FuelPHP x RatchetでWAMPの機能確認サンプルを公開しました

2014/01/01 追記
デモサイトは停止しましたm(_ _)m

--

以下で、FuelPHP x RatchetでWAMPの機能確認サンプルを公開しています。
http://fuelratchet.madroom.org/
今回公開したサンプルは"API Console"となります。PubSubやRPCの結果は全てconsole.log()しています。暫くは動かしておくと思いますが、もし落ちていたりしたらごめんなさいm(_ _)m

このサンプルのソースは
https://github.com/mp-php/fuel-ratchet-samples
に入っています。(デモサイトでは一部、カスタマイズしています。)


ついでに、クライアントがブラウザだけなのも寂しいなということで、Androidでもサンプルアプリを作ってみました。apkファイルは
http://madroom.org/download/WampSample.apk
に置いてあります。PubSubやRPCの結果は全てLogCatに出力しています。こちらも、接続先が落ちていたりしたらごめんなさいm(_ _)m


以下を見ると、iOS用のWAMPクライアントライブラリもあるみたいですね。
http://wamp.ws/implementations


以下、当ブログの関連記事です。

AndroidでWebSocket(WAMP)クライアントを作ってみた
http://madroom-project.blogspot.jp/2013/05/androidwamp.html

FuelPHP x RatchetのWAMPにタスクからメッセージを配信してみた
http://madroom-project.blogspot.jp/2013/05/fuelphp-x-ratchetwamp.html

FuelPHP x RatchetでWAMPのPubSubとRPCを試してみた
http://madroom-project.blogspot.jp/2013/05/fuelphp-x-ratchetwamppubsubrpc.html

WebSocketとWAMPとRatchetに関するメモ
http://madroom-project.blogspot.jp/2013/05/websocketwampratchet.html

FuelPHP x Ratchetのサンプルを公開しました
http://madroom-project.blogspot.jp/2013/04/fuelphp-x-ratchet.html


P.S.
なんか引くに引けないところまで来ましたが(w)、とりあえず残すは、ユニットテスト関係くらいかなぁ。(Androidも!?)

AndroidでWebSocket(WAMP)クライアントを作ってみた

WAMPの公式(?)サイトに、WAMPクライアント一覧とWAMPサーバ一覧が記載されています。
http://wamp.ws/implementations

これまで、サーバは(FuelPHPのパッケージとして組み込んだ)Ratchet、クライアントはAutobahnJSで確認していましたが、AutobahnAndroidが気になったので、簡単なWAMPクライアントサンプルなアプリを作ってみました。
https://github.com/mp-php/android-wamp-sample

処理はActivityにまとめてあります。
https://github.com/mp-php/android-wamp-sample/blob/master/src/net/madroom/wampsample/MainActivity.java
(実際にはActivityはUIのみで、通信部分はServiceとかにすべきなのかなと思います。)

必要なライブラリは、以下のURL先からDLできます。
設定手順等も、以下のURL先の通りです。(特殊な事はしていないので、割愛します。)
http://autobahn.ws/android/getstarted

尚、必要なライブラリは
* autobahn-x.x.x.jar
* jackson-core-asl-x.x.x.jar
* jackson-mapper-asl-x.x.x.jar
となります。

アプリを起動すると、以下のような画面が表示されます。

PubSubは
* 一番目のSpinnerでトピックを選択
* 二番目のSpinnerでpublish / subscribe / unsubscribe を選択
* publishの場合はメッセージを入力
* PubSubボタンで送信

RPCは
* Spinnerでメソッドを選択
* Callボタンで送信

としてみました。

May 4, 2013

FuelPHP v1.6リリースに伴い自作パッケージをPackagistに登録してComposerに対応させてみた

FuelPHP v1.6でComposerに標準対応となりました。

せっかくなので、これまでに作成した自作パッケージをPackagistに登録してComposer対応させてみました。


1. レポジトリにcomposer.jsonを作成
以下、composer.jsonの例です。
https://github.com/mp-php/fuel-packages-bitly/blob/master/composer.json
* "require"に"composer/installers"を指定しないと、後述の"type"や"extra"が反応しませんでした。
* "type"に"fuel-package"を指定すると、インストール先がfuel/packagesになりました。
* "extra"の"installer-name"を指定すると、その名前でインストールされました。


2. Packagistへの登録
Packagistへの登録は必須では無いと思いますが、登録しない理由も特に無いので、登録してみました。GitHubのアカウントで登録可能でした。
https://packagist.org/
初期パスワードが良くわからなかったので(GitHub経由で登録すると発行されない??)パスワードの再発行から設定しなおしました。


3. PackagistへGitHubのレポジトリを登録
"Submit Package"から、簡単に登録出来ます。以下、登録してみたパッケージ一覧です。
https://packagist.org/users/mamor/


4. FuelPHPからComposerでインストールしてみる
FuelPHP v1.6のドキュメントルートに新たに誕生したcomposer.jsonの""require"へ、以下のように追記します。
"mp-php/fuel-packages-bitly": "dev-master"
後は、お決まりなコマンドの
php composer.phar install

php composer.phar update
を実行すると、所定の場所にインストールされました。


5. 自作パッケージが、外部パッケージに依存している場合
4のように、FuelPHP標準のcomposer.jsonで自作パッケージをインストールすると、自作パッケージが依存している外部パッケージがfuel/vendorにインストールされました。
なので、自作パッケージのクラスにおけるautoload.phpの読み込みを
require_once VENDORPATH.'autoload.php';
としたいところなのですが、開発するにあたってはGitのsubmoduleで管理したいなーと。

そうすると
* FuelPHP標準のcomposer.jsonでインストールした場合、外部パッケージはfuel/vendor
* submoduleで管理する場合、外部パッケージはfuel/packages/[自作パッケージ]/vendor
となってしまったので、autoload.phpの読み込みを
if (file_exists(__DIR__.'/../vendor/autoload.php'))
{
    require_once __DIR__.'/../vendor/autoload.php';
}
else
{
    require_once VENDORPATH.'autoload.php';
}
のようにしてみました。(もっとキレイな方法はあるのだろうか。。。)

6. GitHubのService HooksでPackagistを自動更新
GitHubのService HooksでPackagistにコミットを通知できたので、設定しておきました。

FuelPHP x RatchetのWAMPにタスクからメッセージを配信してみた

前回の記事で、RatchetのWAMPの使い方を簡単に確認してみました。

FuelPHP x RatchetでWAMPのPubSubとRPCを試してみた
http://madroom-project.blogspot.jp/2013/05/fuelphp-x-ratchetwamppubsubrpc.html

前回の記事もそうですが、これまでのサンプル等は、全て配信者がクライアント(ブラウザ)です。

対して、今回はFuelPHPのタスクから配信してみます。また、未確認ですが、当然、Controller等からも可能なはずです。

前回の記事のサンプルと異なるのは
* htmlとRatchetのWampServerにpublishの機能が無い
* 代わりに、タスクからZeroMQ経由でpublishする
* RatchetのWampServerにzmqCallbackというメソッドを用意する
となります。尚、"zmqCallback"というメソッド名はRatchetパッケージのRatchetタスクのwampメソッドで登録しているだけなので、変更可能です。

ZeroMQのインストール手順については、以下を参考にして下さい。

FuelPHPでWebSocketを扱うパッケージを作りました
http://madroom-project.blogspot.jp/2013/04/fuelphpwebsocket.html

ZeroMQとはなんぞや。については、後日、別途まとめたいなと思っていますm(_ _)m
http://www.zeromq.org/

以下、html、RatchetパッケージのRatchet_Wampクラスを継承したクラス、タスクのソースです。
-- public/wamp_test2.html(実際にはviewファイル) --
* autobahn.min.jsはWampServerと通信を行うためのJSライブラリです。
* autobahn.min.jsは http://autobahn.ws/js/downloads からダウンロード出来ます。
* autobahn.min.jsのライセンスは http://autobahn.ws/js に"MIT"と記されています。
* "wsuri"の値は変更して下さい。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>FuelPHP x Ratchet WAMP Test</title>
 
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script type="text/javascript" src="assets/js/autobahn.min.js"></script>
 
</head>
<body>
 
<!--購読 (WampServerのonSubscribeメソッドが呼ばれる)-->
<div id="subscribe">
<select>
    <option value="topic_1">Topic 1</option>
    <option value="topic_2">Topic 2</option>
    <option value="topic_3">Topic 3</option>
    <option value="invalid_topic">Invalid Topic</option>
</select>
<button>Subscribe</button>
</div>
 
<hr />
 
<!--購読解除 (WampServerのonUnSubscribeメソッドが呼ばれる)-->
<div id="unsubscribe">
<select>
    <option value="topic_1">Topic 1</option>
    <option value="topic_2">Topic 2</option>
    <option value="topic_3">Topic 3</option>
    <option value="invalid_topic">Invalid Topic</option>
</select>
<button>Unsubscribe</button>
</div>
 
<hr />
 
<!--RPC (WampServerのonCallメソッドが呼ばれる)-->
<div id="rpc">
<select>
    <option value="get_subscribing_topics">Get Subscribing Topics</option>
    <option value="invalid_method">Invalid Method</option>
</select>
<button>Call</button>
</div>
 
<hr />
 
<p>Check your console.</p>
 
<script>
$(document).ready(function() {
    var sess; // WampServerとのコネクション
    var wsuri = 'ws://example.com:[ポート番号]';
 
    /**
     * 使い方等: http://autobahn.ws/js
     */
    sess = new ab.Session(wsuri,
 
        // コネクション接続時のコールバック関数
        function() {
            console.log("Connected!");
        },
 
        // コネクション切断時のコールバック関数
        function(reason) {
            switch (reason) {
                case ab.CONNECTION_CLOSED:
                    // 意図した切断の場合?
                    console.log("Connection was closed properly - done.");
                break;
                case ab.CONNECTION_UNREACHABLE:
                    // WampServerに到達できなかった場合?
                    console.log("Connection could not be established.");
                break;
                case ab.CONNECTION_UNSUPPORTED:
                    // ブラウザがWebSocketをサポートしていない場合
                    console.log("Browser does not support WebSocket.");
                break;
                case ab.CONNECTION_LOST:
                    // 意図しない切断の場合?
                    console.log("Connection lost - reconnecting ...");
 
                    // 1秒後に再接続を試みる
                    window.setTimeout(connect, 1000);
                break;
            }
        }
    );
 
    // 購読 (WampServerのonSubscribeメソッドが呼ばれる)
    $("#subscribe > button").click(function() {
        var select = $("#subscribe > select");
 
        console.log("-- Subscribe --");
        console.log("Topic: " + select.val());
 
        sess.subscribe(select.val(), function (topic, event) {
            console.log("-- Received --");
            console.log("Topic: " + topic);
            console.log("event: " + event);
        });
    });
 
    // 購読解除 (WampServerのonUnSubscribeメソッドが呼ばれる)
    $("#unsubscribe > button").click(function() {
        var select = $("#unsubscribe > select");
 
        console.log("-- Unsubscribe --");
        console.log("Topic: " + select.val());
 
        try {
            sess.unsubscribe(select.val());
        } catch(e) {
            console.warn(e);
        }
    });
 
    // RPC (WampServerのonCallメソッドが呼ばれる)
    $("#rpc > button").click(function() {
        var select = $("#rpc > select");
 
        console.log("-- RPC --");
        console.log("Method: " + select.val());
 
        sess.call(select.val()).then(function (result) {
           // do stuff with the result
           console.log(result);
        }, function(error) {
           // handle the error
           console.log(error);
        });
    });
 
});
</script>
 
</body>
</html>
-- fuel/app/classes/ratchet/wamp/test2.php --
* バリデーションとか、細かな処理は抜けています。
<?php
 
class Ratchet_Wamp_Test2 extends Ratchet_Wamp
{
    // トピック一覧
    private $topics = array();
 
    /**
     * 切断
     * 
     * @param  \Ratchet\ConnectionInterface $conn
     */
    public function onClose(\Ratchet\ConnectionInterface $conn) {
        // 全てのトピックを購読解除
        foreach ($this->topics as $topic)
        {
            $this->onUnSubscribe($conn, $topic);
        }
    }
 
    /**
     * 購読
     * 
     * @param  \Ratchet\ConnectionInterface $conn
     * @param  string|\Ratchet\Wamp\Topic $topic
     */
    public function onSubscribe(\Ratchet\ConnectionInterface $conn, $topic) {
        Log::debug('********** '.__FUNCTION__.' begin **********');
        Log::debug('$topic : '.$topic);
        Log::debug('********** '.__FUNCTION__.' end **********');
 
        // 不正なトピック
        if( ! in_array($topic, array('topic_1', 'topic_2', 'topic_3')))
        {
            return;
        }
 
        // トピック一覧にトピックを追加
        if (!array_key_exists($topic->getId(), $this->topics))
        {
            $this->topics[$topic->getId()] = $topic;
        }
    }
 
    /**
     * 購読解除
     * 
     * @param  \Ratchet\ConnectionInterface $conn
     * @param  string|\Ratchet\Wamp\Topic $topic
     */
    public function onUnSubscribe(\Ratchet\ConnectionInterface $conn, $topic) {
        Log::debug('********** '.__FUNCTION__.' begin **********');
        Log::debug('$topic : '.$topic);
        Log::debug('********** '.__FUNCTION__.' end **********');
 
        // 不正なトピック
        if( ! in_array($topic, array('topic_1', 'topic_2', 'topic_3')))
        {
            return;
        }
 
        // トピックからコネクションを削除
        $topic->remove($conn);
 
        // トピックの購読者が存在しない場合、トピック一覧からトピックを削除
        if ($topic->count() == 0)
        {
            unset($this->topics[$topic->getId()]);
        }
 
    }
 
    /**
     * RPC
     * 
     * @param  \Ratchet\ConnectionInterface $conn
     * @param  string $id
     * @param  string|\Ratchet\Wamp\Topic $fn
     * @param  array $params
     * @return \Ratchet\Wamp\WampConnection
     */
    public function onCall(\Ratchet\ConnectionInterface $conn, $id, $fn, array $params) {
        Log::debug('********** '.__FUNCTION__.' begin **********');
        Log::debug('$id : '.$id);
        Log::debug('$fn : '.$fn);
        Log::debug('$params : '.print_r($params, true));
        Log::debug('********** '.__FUNCTION__.' end **********');
 
        switch ($fn) {
            // 購読しているトピック一覧を取得
            case 'get_subscribing_topics':
                $subscribing_topics = array();
 
                Log::debug('********** Topics begin **********');
 
                foreach ($this->topics as $topic)
                {
                    Log::debug('$topic : '.$topic);
                    Log::debug('$topic->count() : '.$topic->count());
 
                    $topic->has($conn) and $subscribing_topics[] = $topic;
                }
 
                Log::debug('********** Topics end **********');
 
                return $conn->callResult($id, Security::htmlentities($subscribing_topics));
            break;
 
            // エラー処理
            default:
                $errorUri = 'errorUri';
                $desc = 'desc';
                $details = 'details';
 
                /**
                 * \Ratchet\Wamp\WampConnection
                 * 
                 * callError($id, $errorUri, $desc = '', $details = null)
                 */
                return $conn->callError($id, $errorUri, $desc, $details);
            break;
        }
    }

    /**
     * ZeroMQ経由でコールされる
     * 
     * @param  string $msg
     */
    public function zmqCallback($msg) {
        Log::debug('********** '.__FUNCTION__.' begin **********');
        Log::debug('$json_string : '.$msg);
        Log::debug('********** '.__FUNCTION__.' end **********');

        $json = json_decode($msg);

        if( ! isset($json->topic) || ! isset($json->msg))
        {
            return;
        }

        foreach ($this->topics as $topic)
        {
            if ($json->topic == $topic)
            {
                // 配信
                $topic->broadcast(Security::htmlentities($json->msg));
                break;
            }
        }
    }

}
 
/* end of file test2.php */
-- fuel/app/tasks/zmq.php --
<?php

namespace Fuel\Tasks;

class Zmq
{
    public static function run()
    {
        // TODO:
    }

    /**
     * ZeroMQを用いてRatchetのWampServerにpushする
     * 
     * Note:
     * http://php.zero.mq/
     * http://socketo.me/docs/push#editblogsubmission
     */
    public static function push($topic = null, $msg = null, $port = '5555')
    {
        if ($topic === null or $msg === null)
        {
            return;
        }

        $context = new \ZMQContext();
        $socket = $context->getSocket(\ZMQ::SOCKET_PUSH);
        $socket->connect("tcp://localhost:{$port}");

        $socket->send(json_encode(array(
            'topic' => $topic,
            'msg' => $msg,
        )));
    }

}

/* End of file tasks/zmp.php */
wamp_test2.htmlにアクセスすると、以下のような画面になります。
上から順に
* 選択したTopicを購読
* 選択したTopicを購読解除
* 選択したメソッドをRPCパターンでコール
となります。尚、各種情報は console.log() しています。

何れかのTopicを購読(仮にtopic_1を購読したとします。)した後
php oil r zmq:push topic_1 test
とすると、"topic_1"に対して"test"というメッセージをブラウザが受信します。

以下、参考として、前回記事と今回記事の、htmlとphp(WampServer)のdiffです。
$ diff test.html test2.html
13,26d12
< <!--配信 (WampServerのonPublishメソッドが呼ばれる)-->
< <div id="publish">
< <select>
<     <option value="topic_1">Topic 1</option>
<     <option value="topic_2">Topic 2</option>
<     <option value="topic_3">Topic 3</option>
<     <option value="invalid_topic">Invalid Topic</option>
< </select>
< <input type="text" />
< <button>Publish</button>
< </div>
<
< <hr />
<
107,123d92
<     // 配信 (WampServerのonPublishメソッドが呼ばれる)
<     $("#publish > button").click(function() {
<         var input = $("#publish > input");
<         var select = $("#publish > select");
<
<         console.log("-- Publish --");
<         if(input.val().length == 0) {
<             console.log("Input is empty.");
<         } else {
<             console.log("Topic: " + select.val());
<             console.log("Input: " + input.val());
<
<             sess.publish(select.val(), JSON.stringify({input: input.val()}));
<             input.val('');
<         }
<     });
<


$ diff test.php test2.php
3c3
< class Ratchet_Wamp_Test extends Ratchet_Wamp
---
> class Ratchet_Wamp_Test2 extends Ratchet_Wamp
22,53d21
<      * 配信
<      *
<      * @param  \Ratchet\ConnectionInterface $conn
<      * @param  string|\Ratchet\Wamp\Topic $topic
<      * @param  string $event
<      * @param  array $exclude
<      * @param  array $eligible
<      */
<     public function onPublish(\Ratchet\ConnectionInterface $conn, $topic, $event, array $exclude, array $eligible) {
<         Log::debug('********** '.__FUNCTION__.' begin **********');
<         Log::debug('$topic : '.$topic);
<         Log::debug('$event : '.$event);
<         Log::debug('$exclude : '.print_r($exclude, true));
<         Log::debug('$eligible : '.print_r($eligible, true));
<         Log::debug('********** '.__FUNCTION__.' end **********');
<
<         // 不正なトピック
<         if( ! in_array($topic, array('topic_1', 'topic_2', 'topic_3')))
<         {
<             return;
<         }
<
<         $json = json_decode($event);
<
<         // トピックに対する購読者が存在する場合、配信
<         if (array_key_exists($topic->getId(), $this->topics))
<         {
<             $topic->broadcast(Security::htmlentities($json->input));
<         }
<     }
<
<     /**
156a125,152
>     /**
>      * ZeroMQ経由でコールされる
>      *
>      * @param  string $msg
>      */
>     public function zmqCallback($msg) {
>         Log::debug('********** '.__FUNCTION__.' begin **********');
>         Log::debug('$json_string : '.$msg);
>         Log::debug('********** '.__FUNCTION__.' end **********');
>
>         $json = json_decode($msg);
>
>         if( ! isset($json->topic) || ! isset($json->msg))
>         {
>             return;
>         }
>
>         foreach ($this->topics as $topic)
>         {
>             if ($json->topic == $topic)
>             {
>                 // 配信
>                 $topic->broadcast(Security::htmlentities($json->msg));
>                 break;
>             }
>         }
>     }
>
159c155
< /* end of file test.php */
---
> /* end of file test2.php */

May 3, 2013

FuelPHP x RatchetでWAMPのPubSubとRPCを試してみた

2013/05/05 追記:
当記事の内容を改良して、デモサイトに追加しました。
http://fuelratchet.madroom.org/ratchet/wamp/api

--

先日、以下の記事を書きました。

WebSocketとWAMPとRatchetに関するメモ
http://madroom-project.blogspot.jp/2013/05/websocketwampratchet.html


実際にFuelPHPのRatchetパッケージで、WAMPのPubSubとRPCを試してみました。
https://github.com/mp-php/fuel-packages-ratchet

具体的には、以下の機能を作ってみました。
* 指定したトピックに配信する (PubSub)
* 指定したトピックを購読する (PubSub)
* 指定したトピックを購読解除する (PubSub)
* 購読中のトピック一覧を取得する (RPC)


以下、htmlと、RatchetパッケージのRatchet_Wampクラスを継承したクラスのソースです。

-- public/wamp_test.html(実際にはviewファイル) --
* autobahn.min.jsはWampServerと通信を行うためのJSライブラリです。
* autobahn.min.jsは http://autobahn.ws/js/downloads からダウンロード出来ます。
* autobahn.min.jsのライセンスは http://autobahn.ws/js に"MIT"と記されています。
* "wsuri"の値は変更して下さい。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>FuelPHP x Ratchet WAMP Test</title>

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
<script type="text/javascript" src="assets/js/autobahn.min.js"></script>

</head>
<body>

<!--配信 (WampServerのonPublishメソッドが呼ばれる)-->
<div id="publish">
<select>
    <option value="topic_1">Topic 1</option>
    <option value="topic_2">Topic 2</option>
    <option value="topic_3">Topic 3</option>
    <option value="invalid_topic">Invalid Topic</option>
</select>
<input type="text" />
<button>Publish</button>
</div>

<hr />

<!--購読 (WampServerのonSubscribeメソッドが呼ばれる)-->
<div id="subscribe">
<select>
    <option value="topic_1">Topic 1</option>
    <option value="topic_2">Topic 2</option>
    <option value="topic_3">Topic 3</option>
    <option value="invalid_topic">Invalid Topic</option>
</select>
<button>Subscribe</button>
</div>

<hr />

<!--購読解除 (WampServerのonUnSubscribeメソッドが呼ばれる)-->
<div id="unsubscribe">
<select>
    <option value="topic_1">Topic 1</option>
    <option value="topic_2">Topic 2</option>
    <option value="topic_3">Topic 3</option>
    <option value="invalid_topic">Invalid Topic</option>
</select>
<button>Unsubscribe</button>
</div>

<hr />

<!--RPC (WampServerのonCallメソッドが呼ばれる)-->
<div id="rpc">
<select>
    <option value="get_subscribing_topics">Get Subscribing Topics</option>
    <option value="invalid_method">Invalid Method</option>
</select>
<button>Call</button>
</div>

<hr />

<p>Check your console.</p>

<script>
$(document).ready(function() {
    var sess; // WampServerとのコネクション
    var wsuri = 'ws://example.com:[ポート番号]';

    /**
     * 使い方等: http://autobahn.ws/js
     */
    sess = new ab.Session(wsuri,

        // コネクション接続時のコールバック関数
        function() {
            console.log("Connected!");
        },

        // コネクション切断時のコールバック関数
        function(reason) {
            switch (reason) {
                case ab.CONNECTION_CLOSED:
                    // 意図した切断の場合?
                    console.log("Connection was closed properly - done.");
                break;
                case ab.CONNECTION_UNREACHABLE:
                    // WampServerに到達できなかった場合?
                    console.log("Connection could not be established.");
                break;
                case ab.CONNECTION_UNSUPPORTED:
                    // ブラウザがWebSocketをサポートしていない場合
                    console.log("Browser does not support WebSocket.");
                break;
                case ab.CONNECTION_LOST:
                    // 意図しない切断の場合?
                    console.log("Connection lost - reconnecting ...");

                    // 1秒後に再接続を試みる
                    window.setTimeout(connect, 1000);
                break;
            }
        }
    );

    // 配信 (WampServerのonPublishメソッドが呼ばれる)
    $("#publish > button").click(function() {
        var input = $("#publish > input");
        var select = $("#publish > select");

        console.log("-- Publish --");
        if(input.val().length == 0) {
            console.log("Input is empty.");
        } else {
            console.log("Topic: " + select.val());
            console.log("Input: " + input.val());

            sess.publish(select.val(), JSON.stringify({input: input.val()}));
            input.val('');
        }
    });

    // 購読 (WampServerのonSubscribeメソッドが呼ばれる)
    $("#subscribe > button").click(function() {
        var select = $("#subscribe > select");

        console.log("-- Subscribe --");
        console.log("Topic: " + select.val());

        sess.subscribe(select.val(), function (topic, event) {
            console.log("-- Received --");
            console.log("Topic: " + topic);
            console.log("event: " + event);
        });
    });

    // 購読解除 (WampServerのonUnSubscribeメソッドが呼ばれる)
    $("#unsubscribe > button").click(function() {
        var select = $("#unsubscribe > select");

        console.log("-- Unsubscribe --");
        console.log("Topic: " + select.val());

        try {
            sess.unsubscribe(select.val());
        } catch(e) {
            console.warn(e);
        }
    });

    // RPC (WampServerのonCallメソッドが呼ばれる)
    $("#rpc > button").click(function() {
        var select = $("#rpc > select");

        console.log("-- RPC --");
        console.log("Method: " + select.val());

        sess.call(select.val()).then(function (result) {
           // do stuff with the result
           console.log(result);
        }, function(error) {
           // handle the error
           console.log(error);
        });
    });

});
</script>

</body>
</html>
-- fuel/app/classes/ratchet/wamp/test.php --
* バリデーションとか、細かな処理は抜けています。
<?php

class Ratchet_Wamp_Test extends Ratchet_Wamp
{
    // トピック一覧
    private $topics = array();

    /**
     * 切断
     * 
     * @param  \Ratchet\ConnectionInterface $conn
     */
    public function onClose(\Ratchet\ConnectionInterface $conn) {
        // 全てのトピックを購読解除
        foreach ($this->topics as $topic)
        {
            $this->onUnSubscribe($conn, $topic);
        }
    }

    /**
     * 配信
     * 
     * @param  \Ratchet\ConnectionInterface $conn
     * @param  string|\Ratchet\Wamp\Topic $topic
     * @param  string $event
     * @param  array $exclude
     * @param  array $eligible
     */
    public function onPublish(\Ratchet\ConnectionInterface $conn, $topic, $event, array $exclude, array $eligible) {
        Log::debug('********** '.__FUNCTION__.' begin **********');
        Log::debug('$topic : '.$topic);
        Log::debug('$event : '.$event);
        Log::debug('$exclude : '.print_r($exclude, true));
        Log::debug('$eligible : '.print_r($eligible, true));
        Log::debug('********** '.__FUNCTION__.' end **********');

        // 不正なトピック
        if( ! in_array($topic, array('topic_1', 'topic_2', 'topic_3')))
        {
            return;
        }

        $json = json_decode($event);

        // トピックに対する購読者が存在する場合、配信
        if (array_key_exists($topic->getId(), $this->topics))
        {
            $topic->broadcast(Security::htmlentities($json->input));
        }
    }

    /**
     * 購読
     * 
     * @param  \Ratchet\ConnectionInterface $conn
     * @param  string|\Ratchet\Wamp\Topic $topic
     */
    public function onSubscribe(\Ratchet\ConnectionInterface $conn, $topic) {
        Log::debug('********** '.__FUNCTION__.' begin **********');
        Log::debug('$topic : '.$topic);
        Log::debug('********** '.__FUNCTION__.' end **********');

        // 不正なトピック
        if( ! in_array($topic, array('topic_1', 'topic_2', 'topic_3')))
        {
            return;
        }

        // トピック一覧にトピックを追加
        if (!array_key_exists($topic->getId(), $this->topics))
        {
            $this->topics[$topic->getId()] = $topic;
        }
    }

    /**
     * 購読解除
     * 
     * @param  \Ratchet\ConnectionInterface $conn
     * @param  string|\Ratchet\Wamp\Topic $topic
     */
    public function onUnSubscribe(\Ratchet\ConnectionInterface $conn, $topic) {
        Log::debug('********** '.__FUNCTION__.' begin **********');
        Log::debug('$topic : '.$topic);
        Log::debug('********** '.__FUNCTION__.' end **********');

        // 不正なトピック
        if( ! in_array($topic, array('topic_1', 'topic_2', 'topic_3')))
        {
            return;
        }

        // トピックからコネクションを削除
        $topic->remove($conn);

        // トピックの購読者が存在しない場合、トピック一覧からトピックを削除
        if ($topic->count() == 0)
        {
            unset($this->topics[$topic->getId()]);
        }

    }

    /**
     * RPC
     * 
     * @param  \Ratchet\ConnectionInterface $conn
     * @param  string $id
     * @param  string|\Ratchet\Wamp\Topic $fn
     * @param  array $params
     * @return \Ratchet\Wamp\WampConnection
     */
    public function onCall(\Ratchet\ConnectionInterface $conn, $id, $fn, array $params) {
        Log::debug('********** '.__FUNCTION__.' begin **********');
        Log::debug('$id : '.$id);
        Log::debug('$fn : '.$fn);
        Log::debug('$params : '.print_r($params, true));
        Log::debug('********** '.__FUNCTION__.' end **********');

        switch ($fn) {
            // 購読しているトピック一覧を取得
            case 'get_subscribing_topics':
                $subscribing_topics = array();

                Log::debug('********** Topics begin **********');

                foreach ($this->topics as $topic)
                {
                    Log::debug('$topic : '.$topic);
                    Log::debug('$topic->count() : '.$topic->count());

                    $topic->has($conn) and $subscribing_topics[] = $topic;
                }

                Log::debug('********** Topics end **********');

                return $conn->callResult($id, Security::htmlentities($subscribing_topics));
            break;

            // エラー処理
            default:
                $errorUri = 'errorUri';
                $desc = 'desc';
                $details = 'details';

                /**
                 * \Ratchet\Wamp\WampConnection
                 * 
                 * callError($id, $errorUri, $desc = '', $details = null)
                 */
                return $conn->callError($id, $errorUri, $desc, $details);
            break;
        }
    }

}

/* end of file test.php */
wamp_test.htmlにアクセスすると、以下のような画面になります。

上から順に
* 選択したTopicでメッセージを配信
* 選択したTopicを購読
* 選択したTopicを購読解除
* 選択したメソッドをRPCパターンでコール
となります。尚、各種情報は console.log() しています。

P.S.
手元での確認用ソースとして作りましたが、改良してデモサイトにも追加しておこうかな。
http://fuelratchet.madroom.org/
http://madroom-project.blogspot.jp/2013/04/fuelphp-x-ratchet.html
もし追加したら当記事にも追記します。

次は、ZeroMQでHTTPサーバやタスクからのPublishか。

May 1, 2013

WebSocketとWAMPとRatchetに関するメモ

FuelPHPのパッケージとしてRatchetを組み込んだ流れで、Ratchet周りを色々と探っている最中です。
http://socketo.me/
https://github.com/mp-php/fuel-packages-ratchet

で、Ratchetのドキュメントに"WAMP"とか"WampServer"という言葉が出てきます。"WAMP"は"WebSocket Application Messaging Protocol"の略らしいです。
http://wamp.ws/

上記URLには
WAMP is an open WebSocket subprotocol that provides two asynchronous messaging patterns: RPC and PubSub.
と書かれています。

RPCパターンとPubSubパターンという二つの非同期メッセージングパターンを提供する。とのことですが、PubSubパターンは何となく良いとして、RPCパターンって何だろう。となりました。そして、以下に、その説明が書かれています。
http://wamp.ws/faq
What is RPC?
Remote Procedure Call (RPC) is a messaging pattern involving peers to two roles: client and server.
A server provides methods or procedure to call under well known endpoints.
A client calls remote methods or procedures by providing the method or procedure endpoint and any arguments for the call.
The server will execute the method or procedure using the supplied arguments to the call and return the result of the call to the client.
RPCは"Remote Procedure Call"の略のようで、早い話が、クライアント(ブラウザ等)が遠隔地(サーバ)の処理を呼び出して、サーバはクライアントに結果を返す。みたいな感じでしょうか。

確かに、Ratchetにはこの機能が備わっていて、例えば公式のサンプルだと、以下にonCallというメソッドがあります。
https://github.com/cboden/Ratchet-examples/blob/master/src/Ratchet/Website/ChatRoom.php

そして、Ratchetのドキュメントにも記載があるAutobahnJSというJSライブラリにも、それを呼び出すと思われるメソッドがあります。
http://autobahn.ws/js/reference#Session_call

で、WAMPのサイトに戻って
を見てみると、"Servers"に"Ratchet"が書かれています。

なので、RatchetはPubSubパターンとRPCパターンを(正しく)提供しているWebSocketライブラリ。となるのかなーというのが、ここまでの自分なりの解釈です。

(なんかこの辺りの話は、すっ飛ばすと後でつまづく原因になる気がする。)