(1)
(0)
(0)
(0)
Total: 1 DAY8: symfonyでAJAXを使う
これまでのおさらい
ようやくそれらしいアプリケーションができてきた感があります。でも、まだ8日目なんですね。。
本日はAJAXをsymfonyでやってみるということです。ではがんばってチュートリアルを実践してみます。
symfony1.2でのJavascriptライブラリ
symfony1.0まではprototypeとscriptaculousという2つのJavaScriptライブラリを標準で用意していました。
しかし、symfony1.2では外部のライブラリをできるだけ切り離すという思想のもと、これらのライブラリはsfProtoculousPluginというプラグインとして提供されるようになりました。
といっても、symfonyのコアパッケージに含まれますので、プラグインの利用の宣言をするだけです。
また、sfJqueryPluginというjQueryをベースとしたプラグインも用意されています。
ただし、この記事を書いている時点ではsf1.1まで対応となっています。
また、JavaScriptのライブラリを公開ディレクトリ以下に用意しなければなりませんが、
-
$ ./symfony plugin:publish-assets
というタスクを叩く事でsymfonyがうまくやってくれるようになりました。
Javascriptヘルパーとsymfony
本日は、本家チュートリアルと同じようにprototypeを使った実装を行ってみたいと思います。
もし、本格的にprototypeやjQueryなどのライブラリを活用したいとするとsymfonyが提供するJavascriptヘルパーでは不足すると思います。
また、Javascriptの理解があるのであれば最初からヘルパーに頼らずに純粋にJavascriptで実装したほうが確実で早いと思います。
jQueryの使い方を覚えていればCakePHPだろうが他のアプリケーションだろうがいつでもその知識は役に立ちます。
でも、symfonyのJavascriptヘルパーを覚えたところでsymfonyを使うときにしか活用できないというデメリットもあります。
ローディング画像の用意
では、チュートリアルから話がそれつつあるので、話を戻しsymfonyでAJAXをやってみましょう。
まず、AJAXでサーバーと通信している間にローディング画像を表示させるための画像を./web/cssディレクトリに用意しておきます。

そして、レイアウトとスタイルシートに修正を加えます。
./apps/frontend/templates/layout.php
-
<body>
-
<div id="indicator" style="display: none"></div>
./web/css/main.css
-
div#indicator
-
{
-
position: absolute;
-
width: 100px;
-
height: 40px;
-
left: 10px;
-
top: 10px;
-
z-index: 900;
-
background: url(../images/indicator.gif) no-repeat 0 0;
-
}
interested?「気になる?」リンクをAJAXで実装する
質問に対して、interested?(気になる?)フラグを立てることができるAJAXを使ったリンクをUserヘルパーとして作成します。
AJAXのリンク関数はlink_to_user_interestedという名前にし、引数にユーザーインスタンス($sf_user)、Questionレコードを渡します。
Doctrineを使った実装になっている以外は本家チュートリアルと同じです。
./apps/frontend/modules/question/template/_interested_user.php
./apps/frontend/modules/question/templates/_list.php
./apps/frontend/lib/helper/UserHelper.php
-
<?php
-
use_helper('Javascript');
-
function link_to_user_interested($user, $question)
-
{
-
if ($user->isAuthenticated())
-
{
-
$interested = Interest::findById($question->getId(), $user->getSubscriberId());
-
if ($interested)
-
{
-
// already interested
-
return 'interested!';
-
}
-
else
-
{
-
// didn't declare interest yet
-
'url' => 'user/interested?id='.$question->getId(),
-
'loading' => "Element.show('indicator')",
-
'complete' => "Element.hide('indicator');".visual_effect('highlight', 'mark_'.$question->getId()),
-
));
-
}
-
}
-
else
-
{
-
return link_to('interested?', 'user/login');
-
}
-
}
ヘルパーでモデルへアクセスしている点が気になりますが。。DQLはDoctrineのクラスに閉じ込めてしまうポリシーなので静的メソッドを追加します。
./lib/model/doctrine/Interest.class.php
-
{
-
return Doctrine_Query::create()
-
->from('Interest')
-
->where('question_id = ?', $question_id)
-
->addWhere('user_id = ?', $user_id)
-
->execute();
-
}
Javascriptヘルパーを有効にする
本日の最初に書いたように、symfony1.2からは以前のJavascriptヘルパーを使うためにはsfProtoculousPluginを有効にし、plugin:publish-assetsを叩かなければなりません。
./config/ProjectConfiguration.class.php
-
public function setup()
-
{
-
$this->disablePlugins('sfPropelPlugin');
-
}
プラグインが必要とする公開ファイルへのディレクトリへシンボリックリンクを作成します。
-
$ ./symfony plugin:publish-assets
-
>> plugin Configuring core plugin - sfDoctrinePlugin
-
>> plugin Configuring core plugin - sfCompat10Plugin
-
>> plugin Configuring core plugin - sfPropelPlugin
-
>> plugin Configuring core plugin - sfProtoculousPlugin
これで、以下のようにsfPropelPluginとsfProtoculousPluginのためのシンボリックリンクが作成されました。
-
$ ls -la web/
-
合計 48
-
drwxr-xr-x 7 maedaz users 4096 7月 20 12:40 .
-
drwxr-xr-x 13 maedaz users 4096 7月 9 14:23 ..
-
-rw-r--r-- 1 maedaz users 595 6月 29 17:34 .htaccess
-
drwxr-xr-x 6 maedaz users 4096 7月 20 09:29 .svn
-
drwxr-xr-x 3 maedaz users 4096 7月 20 10:40 css
-
-rw-r--r-- 1 maedaz users 611 6月 29 17:34 frontend_dev.php
-
drwxr-xr-x 3 maedaz users 4096 7月 20 10:40 images
-
-rw-r--r-- 1 maedaz users 236 6月 29 17:34 index.php
-
drwxr-xr-x 3 maedaz users 4096 6月 29 17:34 js
-
-rw-r--r-- 1 maedaz users 26 6月 29 17:34 robots.txt
-
lrwxrwxrwx 1 maedaz users 36 6月 29 17:34 sf -> /usr/local/lib/symfony12/data/web/sf
-
lrwxrwxrwx 1 maedaz users 55 7月 20 12:40 sfPropelPlugin -> /usr/local/lib/symfony12/lib/plugins/sfPropelPlugin/web
-
lrwxrwxrwx 1 maedaz users 60 7月 20 12:40 sfProtoculousPlugin -> /usr/local/lib/symfony12/lib/plugins/sfProtoculousPlugin/web
-
drwxrwxrwx 4 maedaz users 4096 6月 29 17:34 uploads
symfony1.0までは場合によって自分自身でシンボリックリンクを作成する必要があったので便利になりました。
でも、sfPropelPluginは使わないんだけどシンボリックリンクが作成されるのはどうなんでしょう。
というわけで、本家にチケット投げました。(記事を公開するまでに無事修正されました)
さて、一部腑に落ちない部分もありつつ最後にキャッシュをクリアしておきます。
-
$ ./symfony cc
これで、
ニックネーム: francoisz
パスワード: adventcal
でログインし
http://symfony.centos5.localhost/askeet/frontend_dev.php/question/list/page/2
を見てみましょう。
prototype.js
builder.js
effects.js
が読み込まれていることがわかります。
サーバー側の実装
AJAXで呼び出されるサーバー側の実装を行います。
./apps/frontend/modules/user/actions/actions.class.php
-
public function executeInterested()
-
{
-
$this->question = Question::findById($request->getParameter('id'));
-
$this->forward404Unless($this->question);
-
$user = $this->getUser()->getSubscriber();
-
$interest = new Interest();
-
$interest->setQuestion($this->question);
-
$interest->setUser($user);
-
$interest->save();
-
}
QuestionモデルにfindByIdメソッドを用意しておきます。
./lib/model/doctrine/Question.class.php
-
{
-
return Doctrine::getTable('Question')->find($id);
-
}
最後にAJAXで書き換えられる領域のテンプレートを返すので、先ほど用意したパーシャルを返すようにします。
./apps/frontend/modules/user/templates/interestedSuccess.php
QuestionモデルにDQLを閉じ込めた以外は本家チュートリアルと同じですね。
また、Interestモデルにデータをセットしsaveする処理はPropelであってもDoctrineであっても全く同じ書き方になります。
もし、Propelで実装するとしても同じようにQuestionモデルにCriteriaを閉じ込めておくようにすればORMを変更するのもより簡単に行えるようになるというのがわかります。
AJAXでのログイン処理実装
ログインフォームをコンポーネントで用意する
以前作成したログインフォームを利用したいと思います。
sfFormで作成したログインフォームを呼び出したいので、ここでは本家チュートリアルとは異なりコンポーネントで実装するようにしてみます。
./apps/frontend/modules/user/actions/components.class.php
-
<?php
-
class userComponents extents sfComponents
-
{
-
public function executeLogin(sfWegRequest $request)
-
{
-
$this->form = new LoginUserForm();
-
}
-
}
そしてテンプレート部分も用意しておきます。
./apps/frontend/modules/user/templates/_login.php
-
<?php use_helper('Javascript') ?>
-
<form action="<?php echo url_for('user/login') ?>" method="POST" id="loginform">
-
<input type='submit' value="サインイン">
-
</form>
今回はecho $formではなくレイアウトを変えたいので、widget単位でechoするようにしています。
デザインを変更したい場合も無理なく対応が可能です。
そして、レイアウトにこのコンポーネントを呼び出すように記述します。
./apps/frontend/templates/layout.php
-
<div id="login" style="display: none">
-
<h2>Please sign-in first</h2>
-
<?php include_component('user', 'login') ?>
-
</div>
これで以下のように、未ログインの状態で「interested?」を押すと、ログインフォームがにゅにゅにゅと出現し、「cancel」をクリックすることでフォームが隠れます。
回答への投票機能
最後に同様にヘルパーとパーシャルを利用して回答に対して投票を行う事ができる機能を実装します。
仕組みは一緒なので、_vote_user.phpというパーシャルを作成し、さらにAJAXを行うlink_to_user_relevancy_upメソッド, link_to_user_relevancy_downメソッドを用意したAnswerHelper.phpヘルパーを作成します。
./apps/frontend/modules/question/templates/showSuccess.php
-
<?php use_helpers('Date') ?>
-
-
<div id="answers">
-
<?php foreach ($answer_pager->getResults() as $answer): ?>
-
<div>
-
<div class="vote_block" id="vote_<?php echo $answer->getId() ?>">
-
</div>
-
<div>
-
</div>
-
</div>
-
<?php endforeach ?>
-
</div>
./apps/frontend/modules/answer/templates/_vote_user.php
-
<?php use_helper('Answer') ?>
-
-
<span class="vote_up_mark" id="vote_up_<?php echo $answer->getId() ?>">
-
-
<span class="vote_down_mark" id="vote_down_<?php echo $answer->getId() ?>">
./apps/frontend/lib/helper/AnswerHelper.php
-
<?php
-
function link_to_user_relevancy_up($user, $answer)
-
{
-
return link_to_user_relevancy('up', $user, $answer);
-
}
-
function link_to_user_relevancy_down($user, $answer)
-
{
-
return link_to_user_relevancy('down', $user, $answer);
-
}
-
function link_to_user_relevancy($name, $user, $answer)
-
{
-
use_helper('Javascript');
-
if ($user->isAuthenticated())
-
{
-
$relevancy = Relevancy::findById($answer->getId(), $user->getSubscriberId())
-
if ($relevancy)
-
{
-
// already interested
-
return $name;
-
}
-
else
-
{
-
// didn't declare interest yet
-
'url' => 'user/vote?id='.$answer->getId().'&score='.($name == 'up' ? 1 : -1),
-
'loading' => "Element.show('indicator')",
-
'complete' => "Element.hide('indicator');".visual_effect('highlight', 'vote_'.$name.'_'.$answer->getId()),
-
));
-
}
-
}
-
else
-
{
-
}
-
}
投票できるかどうかのために、Relevancyモデルからデータを取得するメソッドを追加します。
./lib/model/doctrine/Relevancy.class.php
-
{
-
return Doctrine_Query::create()
-
->from('Interest')
-
->where('question_id = ?', $question_id)
-
->addWhere('user_id = ?', $user_id)
-
->execute();
-
}
投票処理を実行するメソッドを追加します。
./apps/frontend/modules/user/actions/actions.class.php
-
public function executeVote()
-
{
-
$this->answer = Answer::findById($this->getRequestParameter('id'));
-
$this->forward404Unless($this->answer);
-
-
$user = $this->getUser()->getSubscriber();
-
-
$relevancy = new Relevancy();
-
$relevancy->setAnswer($this->answer);
-
$relevancy->setUser($user);
-
$relevancy->setScore($this->getRequestParameter('score') == 1 ? 1 : -1);
-
$relevancy->save();
-
}
./apps/frontend/modules/user/templates/voteSuccess.php
さぁこれで完成と思いきや、投稿リンクを押してもカウントアップ/ダウンが処理されていません。
Firebugで見るとレスポンスはエラーになっていません。処理自体は正しく行われているようです。
怪しそうなのはクエリーの処理だと思われます。
symfonyではWebデバッグツールバーという開発環境時に右上に表示されるデバッグ情報があり、そこで発行されたSQLを確認できる機能があるのですがAJAXなどで実行されたクエリーについては確認できません。
そういった場合のために、AJAXでも確認できる拡張されたWebデバッグツールバーというプラグインもありますが、てっとり早い方法はDBで実行されたログが見れればよいので、私の場合はローカル開発時は以下のようにmy.cnfを設定しています。
/etc/my.cnf
-
[mysqld]
-
log=/var/log/mysqld_bin.log
これで、発行されたログは/var/log/mysqld_bin.logに保存されていきます。
あとはターミナルで
-
$ tail -f /var/log/mysqld_bin.log
としておけばいつでも発行されたクエリを確認できます。
そして、今回の場合はクエリー発行後に何故かROLLBACKが発行されていることがわかりました。
ということは、余計なROLLBACKを行っているかCOMMITを忘れている可能性が大です。
さっそくモデルを確認してみると
./lib/model/doctrine/Relevancy.class.php
-
public function preSave($event)
-
{
-
$con = Doctrine_Manager::connection();
-
try {
-
$con->beginTransaction();
-
$answer = $this->getAnswer();
-
if ($this->getScore() == 1) {
-
$answer->relevancy_up++;
-
} else {
-
$answer->relevancy_down++;
-
}
-
$answer->save();
-
} catch (Doctrine_Exception $e) {
-
$con->rollback();
-
}
-
}
$answer->save()の後にcommitを忘れています!
単体テストをきちんと行っていないとこういう間違いに気づかないので駄目ですね。。
というわけで、以下のように変更します。
-
$answer->save();
-
$con->commit();
これでようやくVoteの実装が終わりました。
また明日
複雑なAJAXは直接prototypeなどのライブラリを利用するとして、この程度の実装であればヘルパーを使う事で簡単に実装できてしまうことがわかります。
また、symfony1.0のころと比べると準備までの手間が変わりましたが、使い勝手そのものは変わっていない事がわかります。
ここまでのソースは以下のリポジトリからチェックアウトすることができます。
http://svn.1ms.jp/public/symfony/askeet12/tags/release_day_8
関連するその他の記事
Comments
Leave a Reply

