メイン

2010年9月 1日

アジャイルな開発をチームでやってみた(2010年版) - その2
このエントリーをブックマークに追加 このエントリーをlivedoorクリップに追加

テストコード書いてますか? HIROKIです。

murahashiに続いて、テストファーストを導入してみての振り返りをします。 まず、どうやってチームにテストファーストのスタイルを持ち込んだのか。

1.テストが重要だという共通認識を持つ。

前のプロジェクトではテストコードは、ほとんどありませんでした。 その中で、開発になれていない人や新たに人が投入され、 極少数ですが、デグレーションが起きました。

その経験を元にテストが重要だという共通認識を持つことができました。

2.プロジェクト開始時からテストファーストに踏み切る気持ちが必要

テストコードを書かなければコミットさせない。ぐらいの気持ちが必要です。 実際に何度も、本格的な実装が始まる前から口にしていました。 「うちのチームはテストを書かなければコミットを許さない。」と。

3.でも、テストコード書いたことないよ?

テストコードを書いたことのないメンバーもいるかと思います。 そんな人は下記の参考書を写経することから始めましょう。

symfonyを使うことが前提だったので、上記の本をlimeで置き換えて写経しました。

また、下記のような動画もとても参考になります。

4.そんなこと言っても、自主的に学習する人ばかりでないよ?

初日にペアプロしちゃいましょう。

そのチームでTDDに一番詳しい人と、TDD初心者な人をペアにして、 実装を始める初日かその前(できるだけはやく)にペアプロをしましょう。

ペアプロは1モジュール書き上げるくらいが良いかと思います。 そのチームで利用する開発環境の基本的なテストコードの書き方として後に参考にします。

私のチームではsymfony+memcached+lime+hudsonという環境でしたので、 どのタイミングでfixtureを流してmemcachedをクリアして、 どういったテストの書き方をしていくのか参考になるものを1つ作り上げました。 (細部に関してはチームみんなの意見をあつめて、検討してつくりました。)

これができれば他のモジュールにも適応して開発していくだけです。

5.じゃあ、やってみた経験から為になること教えてよ?

symfonyでテストファーストを実施してみた経験から具体例をあげてみます。

fixtureを理想的なものを最初につくっておく

テーブルの定義だけではなくて、 テーブルの中身のサンプルを全部のテーブル用意しておくこと。 データ自体は簡単なデータでよい。

モジュールをつくる度にあらたなfixtureをつくることを避けられる。 fixtureをつくるのが結構めんどくさい。 テストを書きたいのにデータがないというフラストレーションがたまる。 結局、つくらないといけないけど、他のモジュールで使っているfixtureと整合性がとれているのか?

規模が大きくなればなるほど、面倒なことになります。

limeのOKメソッドは使うな

bool値をチェックするメソッドとしてokメソッドがLimeには用意されていますが、 これを使用することを非推奨としました。

たとえば、テストが失敗したケースを考えると。

$test->ok(false);

実行画面

not ok 21
#     Failed test (./hogeTest.php at line 34)

という感じで返ってきます。ですが、isメソッドで書けば

$test->is(false,true,trueが返ってくるはず);
not ok 22 - trueが返ってくるはず
#     Failed test (./hogeTest.phpat line 35)
#            got: false
#       expected: true

という感じで出力が返ってくる。コメントもあるので、わかりやすい。

okメソッドだけだと情報が少なくて苦労するケースがあるので、面倒でもisメソッドで書くことにしました。

コメントではなくて、コードに含めろ

非推奨

// trueが返ってくるはず
$test->is($foo->bar(),true);
not ok 22
#     Failed test (./hogeTest.phpat line 35)
#            got: false
#       expected: true

推奨

$test->is($foo->bar(),true,trueが返ってくるはず);
not ok 22 - trueが返ってくるはず
#     Failed test (./hogeTest.phpat line 35)
#            got: false
#       expected: true

コメントを元にテストの場所や書いたテストの意図がわかるので、メソッドに含めましょう。

まとめ

build_status.png

このグラフがhudsonが1回目のビルドから529回目のビルドまでのテストにかかった時間と成功・失敗の結果です。

チーム全体で常にすべてのテストをパスしている状態を保つという意識とそれに伴う行動の結果です。

Files=59, Tests=3152

現時点で59ファイル、3152のテストをパスしています。

赤色の部分も目立ちますが、テストが成長していることが目で見てわかります。 こうやって、テストに支えられていることも再認識することも大切なことなのかな。とも思います。

今回は、チームでテストファーストをやるためのステップと、少し具体的な例を紹介してみました。

2010年8月26日

アジャイルな開発をチームでやってみた(2010年版)
このエントリーをブックマークに追加 このエントリーをlivedoorクリップに追加

こんにちは murahashi です。
アジャイルな開発をチームでやってみている(2010年)のですが、いざやってみると結構ハマリどころがありました。やってみたことを共有しておこうと思います。

かたちから入ろう

acts_as_agile_armor_600_457.jpg
朝会
アジャイルな開発と言えば朝会なので、朝会から始めました。
開始時刻をメンバーで決めて、それぞれが昨日やったこと、今日やること、おしらせ、困っていること、を共有しました。
さらに、朝会前に社内wikiにメモ書き程度の項目を書いておきます。これにあらかじめ目を通すことで、一番の課題に時間を集中することができました。

acts_as_agile_anti_pattern_50_67.jpg
アンチパターン・決めた時刻を守らない
11時から朝会始めようと決めたのに、11時過ぎに汗だくで飛び込んできて「遅れてすみません」「wiki書いてません」「wiki読んでません」というのは、チームの空気を悪くするだけでなく、単純に全員の時間を無駄にしてしまいます。
時刻を守るか、守れるルールに変えましょう。ごめんなさい。

できることからやろう
組み込めそうなものからとりいれてみました。
具体的には、『達人プログラマー』で言う三本柱(テスト・自動化・早期デプロイ)、朝会、継続的な統合(CI)、テスト駆動開発(TDD)、テスト用データベースの独立、分散VCSを使う、などです。
CI, git, TDDは一気に導入しました。テスト用データベースの独立は必要になってから取り入れました。

acts_as_agile_anti_pattern_50_67.jpg
アンチパターン・あれもこれも
一個一個見ればそれはあったほうがいいんだろうなと思いますが、きりがありません。多すぎて身構えてしまうし、開発が乗ってくるまでにどれだけかかるんだよと。
かといって完全マスターしないと増やさないポリシーも意味がありません。要はバランスです。
今思えばgitの導入がかなり説明不足でした。その時はsubversionをフル活用した社内ライブラリとの接合をとるのでいっぱいいっぱいでした。

チームでやろう
今までもタスクが個人レベルまで分解されたあとは、アジャイルというかXPというか"ぼくのかんがえたTDD"でやってみてました。これが結構できるようになったなーと思ってチームでやるようになったのですが、ひとりxpとチームxpのあいだの一歩は思ったより大きかったです。
逆説的ですが、ひとりxpが完璧にできるようになるのを待つ必要はないと思います。最終的にチームでやらない意味がわからないので、できることからさっさとチームで始めるのが良いと思います。

acts_as_agile_anti_pattern_50_67.jpg
アンチパターン・チームで決めた方針は変えない
合わなかったら、そして合理的な理由があればさっさと方針を変えましょう。
はじめのルールのままテストがどんどん遅くなっていっているのでどうにかしたいです。

テスト駆動で開発しよう
テストコードを先に書く事で、テスト可能な設計になります。むしろテスト可能な設計でしか書けません。テストコードが存在することで、テストコードの範囲内で正しく動くっぽいことが確認できます。テストコードがなければ、正しく動くっぽいことの確認すらできません。
また、テストを壊してしまったら、CIサーバのhudsonが容赦なく "Build faild in Hudson" なるメールをガッシュガッシュ投げつけてくるので、想定の範囲内でデグレードを防ぐことができます。

acts_as_agile_anti_pattern_50_67.jpg
アンチパターン・テストを書かなければ失敗しない
テストを書かなければ失敗しないのですが、それでは意味がありません。また、通らなくなったテストをコメントアウトしたり、消したりしてテストを通してはいけません。
さらに「いけません」とチームに言ってるのに「他の作業するところで影響でるから」と実装だけ修正したりすると、「なんであいつだけ」となるので以後気をつけます。
ただし"たまに落ちるテスト"はどうしていいかいまだによくわかりません。

イディオムに従う
symfonyのイディオムはjobeetなので、jobeetに従っておくと共通認識をつくりやすいです。php, symfony, lime, jobeetのイディオムから外れるときは明確に意図を持って外れるようにしましょう。
「正しいunit testでは」みたいな話ばかりをしていても仕方がないので。しましたけど。

そんなこんなやってみた構成

  • php5.3, symfony1.4, mysql5.1, memcache, flashlite1.1
  • テスティングフレームワーク lime
  • BTS trac
  • VCS git
  • CI hudson
  • デプロイ capistrano
だいたいそんなかんじ

やってみてのハマりどころ

アジャイルな開発のスイートスポットは新規開発?
不慣れなチームだったので別にスイートスポットではありませんでした。
ケータイ向けアプリのflash
flashが重要な役割を占めるけど、limeでテスト駆動開発...そこは割り切って開発することにしました。
デプロイするcapistranoのレシピは早々に書いて動かしてたのですが、flashとphpが出来上がってきたところでがっちゃんこになりました。そして、がっちゃんこしたら結局わりきった接合面でなんでか動きません。TDDでmodelを厚く作ったのに...
だいたい原因は、symfony にも不慣れな人ばかりだったので、そんなに厚くないcontroller部分の値の受け渡し部分なことが多かったです。
ケータイ向けopensocialに特有の部分
テスト用擬似ブラウザsfBrowserのclickとかredirectとか(NDAかもしれないので省略されました)

acts_as_agile_stone_wall_600_569.JPG
fixtureの硬直化がDBのschemaの硬直化を呼ぶ
fixtureをyamlで書いてそれを毎回DBにロードしてるので、スキーマ変更するたびにyamlの編集が必要になりました。テスト書き換えるだけでもしんどいとおもうひとばかりなのに、yamlをえんえん編集してるのはもっとひどくてかなりダメージを受けてました。赤→緑→リファクタリングのサイクルで脳汁が出る人ばかりではありません。自然と、 DBのschemaが硬直してしまいました。
factory_girlはやくきてくれー。
全体的に
やりたいやりたい言ってる私がコーチになるどころかそんなに役に立ててないのが割と原因の一つです。でも私もわかんないよ。手探り手探りです。

あわせてよみたい

これから

問題にぶつかったなかで、今一番多い解決策は"とりあえずわりきって前に進む"であることがおおいので、もうちょっとましな解決tipsのエントリを挙げられるといいなあと思います。
以上デース

2010年7月29日

symfonyのsfBrowserを使ってコンソールツールを作成してみました
このエントリーをブックマークに追加 このエントリーをlivedoorクリップに追加

はじめまして、4月に入社しました、はなだと申します。
以後よろしくお願いいたします。


私自身は、これまでにJava/C#/C++/Perlなどを使った開発を行って来ましたが、現在は(はじめての!)PHPを使ったソーシャルアプリの開発を行っています。
これまでは、新しい言語を学習する際に、サンプルやチュートリアルを終えたあとで、ちょっとした作業を簡略化するためのツールをチョコチョコとつくっていました。今回は、はじめてのPHPということで、symfonysfBrowserを使った、コンソールツールを作ってみました。

このツール開発を通じてやりたいことは次のような内容です。

  • 開発しているsymfonyのURLを指定して、コンソールベースのツールでリンクを遷移させたい
  • できればUser-Agentとして携帯電話に対応させたい

さらに 

  • symfonyの内部構造を理解したい
  • PHPの言語仕様やライブラリに慣れたい
というものがありました。

ところで、携帯電話対応のWebアプリに対する動作確認を行う場合には、FireMobileSimulatorを使うのが一般的だと思いますが、2台以上の携帯電話で相互に影響を及ぼす機能を確認する際には、いちいち端末を切り替えるよりも、コンソールからお手軽にリンクをクリックさせたいと思ったのが作成動機です。

コードは以下のようになります。

<?php
// ツールの初期化関数
function initialize()
{
    // symfonyプロジェクトの設定情報を読み込む
    require_once dirname(__FILE__).'/config/ProjectConfiguration.class.php';
    // アプリ名と環境を指定して設定情報を取得
    $configuration = ProjectConfiguration::getApplicationConfiguration('アプリ名', 'test', true);
    // コンテキストを作成
    sfContext::createInstance($configuration);
    // remove all cache
    sfToolkit::clearDirectory(sfConfig::get('sf_app_cache_dir'));

    // sfBrowserインスタンスを作成
    $browser = new sfBrowser();
    // 必要であれば携帯電話のUser-Agentと固有IDを設定
    $browser
        ->setHttpHeader('User-Agent', 'DoCoMo/2.0 N02A(c100;TB;W24H16)')
        ->setHttpHeader('X-DCMGUID', '携帯電話の固有ID');
    return $browser;
}

// sfBrowserインスタンスとURLを指定して、anchorのリストを表示
function process($browser, $url)
{
    echo "Url: [$url] \n";
    // セレクタ取得
    $selector = $browser->get($url)->getResponseDomCssSelector();
    echo 'Title: ' . $browser->getResponse()->getTitle() . "\n";
    // aタグノードの一覧を取得
    $anchors = $selector->matchAll('a')->nodes;
    foreach($anchors as $key=>$anchor) {
        echo "$key: " . $anchor->nodeValue . "\n";
    }
    echo "q:quit, 0-:click link\n> ";
    return $anchors;
}

function main($url)
{
    $browser = initialize();
    // 標準入力をopen
    $fp = fopen('php://stdin', 'r');
    if (!$fp) {
        exit("Failed to open STDIN\n");
    }

    // ループを回す
    while(!feof($fp)) {
        $anchors = process($browser, $url);
        $command = fgets($fp, 256);
        $command = trim($command);
        if ($command === 'q') {
            // コマンドが'q'の場合には終了
            break;
        } else if (is_numeric($command)) {
            // コマンドが数字の場合にはanchorの配列から取得
            $index = intval($command);
            if ($index < count($anchors)) {
                // href属性からURLを取得
                $url = $anchors[$index]->getAttribute('href');
            } else {
                echo "Out of range. [$index]\n";
            }
        } else {
            echo "Unknown command. [$command]\n";
        }
    }
    fclose($fp);
    echo("\nQuit.\n");
}

if ($argc < 2) {
    exit("Usage:>php $argv[0] [url]\n");
}
main($argv[1]);
?>

例えばこれをsymfonyのプロジェクトルートディレクトリ直下にConsoleBrowser.phpという名前で保存します。
ルートパス(/)をオープンするには、次のようなコマンドを実行します。

$ cd myprj
$ php ConsoleBrowser.php /

"q"コマンドでツールを終了できます。読み込んだコンテンツにaタグ(anchor)がある場合には、数字付きリストで表示されるので、その番号を入力すれば、リンク先に遷移します。

基本的な実装しかしておりませんので、formタグに対応させる、内容を表示させる、postで送信する、パラメータの入力を行う、などの拡張は皆様の手で是非とも追加してみてください。

2010年7月15日

symfonyを使ってみた所感
このエントリーをブックマークに追加 このエントリーをlivedoorクリップに追加

こんにちは、オマタです。

4月に入社したばかりでここに書ける小ネタがないのですが、
symfonyを使い始めてみて便利だなと思ったことを書いてみたいと思います。


jobeetが用意されているところがすごい

symfony初心者は、jobeetと言われるチュートリアルを参照しつつ、
説明の通りに自分の環境で動かしながら使い方に慣れていくのが一般的です。
このjobeetがとてもよく出来ていて、これを一通りこなすだけでインストールから
エンジニア求人サイトの正しい作り方までを勉強することができます。
途中、日本語訳の説明文に間違いや分かり辛いところがあったりしますが、
そのときは個人blogに書かれているjobeet正誤情報が助けてくれます。
ちなみにウノウではjobeetの14日目までが習得推奨となっています。
jobeetへのリンク


symfonyコマンドがすごい

symfonyコマンドを使ってプロジェクトの作成、モジュールの作成、
modelやformの生成の他にDBのスキーマ構築、fixtures.ymlの生成、
バージョンやプラグインの確認など様々なことが行えます。
CakePHPのbakeコマンドもお茶目で便利で素敵と思ったものでしたが、
symfonyコマンドの便利さをしみじみ感じている今日この頃です。


デバッグツールバーがすごい

symfonyのデバッグツールバーはとっても便利です。
パラメータの中身やactionからviewへ渡すオブジェクトの中身が閲覧できる他、
実行速度やSQLクエリのログなども見れます。
そのツールバーが画面の右上に常駐していて、見たい時に開いて見ることができます。
エラー時にはログやstack traceを見れば何処でエラーが起こったかを特定できます。


ルーティングが便利

routing.ymlで各ページのURLの命名規則やパラメータの有無、
リクエストの種類(GET/POST)など設定することができ、
条件に合わないURLがリクエストされた場合にはエラーを表示させることができます。
さらにはsfPropelRouteクラスを使うことで、指定クラスのオブジェクトを
受け取ることができます。
テンプレート内でURLを記述する場合には、

<?php echo url_for('ルート名') ?>
を使います。
後からURLの規則を変えたいときでも、ルート名で呼び出しておけば
routing.ymlの設定を変えるだけでいいので便利です。


フィルターが便利

アプリケーションディレクトリ内のconfig/security.ymlの設定を

is_secure: true
にすると、どのページににアクセスする際にも
filters.ymlで設定されているfilterクラスを通ることになり、
たとえばmyAuthenticationFilterで認証が通らなかった場合には
loginモジュールに飛ばす仕組みになっています
(どのモジュールに飛ばすかの設定は変えられます)
その他、InputFilterやOutpurFileterを追加することで入出力時に
フィルターをかけることができます。
filterを通したくないモジュール(errorモジュールなど)の場合には、
モジュールのディレクトリ内にconf/security.ymlを作成し、
下記のように設定するだけでスキップされます。
all:
    is_secure: false


以上がsymfonyを使ってみた所感になります。
これからsymfonyを使ってみようと考えている方にとって、
少しでもこの情報がお役に立てれば幸いです。


2010年6月 3日

symfonyのfunctional testを携帯のUserAgentにしよう
このエントリーをブックマークに追加 このエントリーをlivedoorクリップに追加

こんにちは市丸です。
symfony コマンドで generate:module をすると、functional test用ファイルが自動生成されます。
symfonyで携帯サービスを作る場合、frontendは携帯端末 backendではPCブラウザでアクセスすることがほとんだと思います。 frontendのfunctional testを携帯のUserAgentする方法を紹介します。

※試したのは、symfony 1.4です。

まずスケルトンをコピー

mkdir -p data/skeleton
cp -r lib/vendor/symfony/lib/task/generator/skeleton/module data/skeleton/

data/skeleton/module/test/actionsTest.phpを以下のように改変。

<?php
include(dirname(__FILE__).'/../../bootstrap/functional_##APP_NAME##.php');

if (!$aBrowser){
  $aBrowser = new sfBrowser();
}
$browser = new sfTestFunctional($aBrowser);

$browser->
  get('/##MODULE_NAME##')->

  with('request')->begin()->
    isParameter('module', '##MODULE_NAME##')->
    isParameter('action', 'index')->
  end()->

  with('response')->begin()->
    isStatusCode(200)->
  end()
;

test/bootstrap/functional.phpをApplicationごとに切り替えるため コピーを作成します。

cp test/bootstrap/functional.php test/bootstrap/functional_frontend.php
cp test/bootstrap/functional.php test/bootstrap/functional_backend.php

functional_frontend.php の最後に以下を追加しましょう

$aBrowser = new sfBrowser();
$aBrowser->setVar('HTTP_USER_AGENT', 'DoCoMo/2.0 N06A3(c100;TB;W24H12)');
$aBrowser->setVar('REMOTE_ADDR','127.0.0.1');
$aBrowser->setVar('HTTP_X_DCMGUID', 'HOGEHOGE'); // iモードIDはこんなかんじ

さらに詳しいfunctional testを行いたい場合は
symfony 機能テストを御覧下さい。

ウノウでは特に最近、積極的にエンジニアを採用しています。
採用ページをご覧になり興味のある方、ぜひご応募ください。
Find Job!でも募集してます!

2010年5月17日

symfony/Doctrineのキャッシュ機能
このエントリーをブックマークに追加 このエントリーをlivedoorクリップに追加

こんばんは、五十川です。

symfony 1.3/1.4で、それまでのPropelに代わってデフォルトのORMとなったDoctrineには、組み込みのキャッシュ機能があり、クエリーの実行結果データを手軽にキャッシュすることができます(これは「結果キャッシュ」と呼ばれます)。また、実行結果だけではなく、DQLに基づいて生成されるSQLをキャッシュする仕組みもあり、これを利用することで、DQL解析のオーバーヘッドを回避して処理の高速化を図ることも可能です(こちらは「 クエリーキャッシュ」という紛らわしい名前で呼ばれます)。

Propelの場合はそれ自体にはキャッシュ機能がなく、必要な場合は自力で頑張る必要がありました(Propel 1.5で、query_cacheビヘイビアが提供されるようになりました)。Doctrineの場合には当初から用意されていたキャッシュ機能が、特にsymfony 1.3/1.4で採用されたDoctrine 1.2において様々な改良が施され、十分採用検討に値するものになっています。

Doctrine 1.2のキャッシュ機能に関する公式のドキュメントは以下にあります。

利用可能なキャッシュドライバ

Doctrine 1.2には以下のキャッシュドライバが用意されています。

  • memcached(Doctrine_Cache_Memcache)
  • APC(Doctrine_Cache_Apc)
  • XCache(Doctrine_Cache_Xcache)
  • データベース(Doctrine_Cache_Db)

なお、この他にDoctrine_Cache_Arrayというドライバもあります。このドライバでは、キャッシュはドライバのプロパティに“保存”され、従ってリクエスト毎に破棄されてしまいますが、機能拡張やキャッシュストレージの準備が必要がないので、テスト用途などで気軽に利用できます。

キャッシュドライバの設定

Doctrineのキャッシュ機能を利用するには、Doctrine_ManagerやDoctine_ConnectionのsetAttribute()メソッドで、適切な属性値を設定します。Doctrine_Managerでの設定はすべてのコネクションでのデフォルトになります。Doctine_Connectionでの設定によってコネクション毎の設定を変更できます。

キャッシュ機能に関する属性には以下のものがあります。

属性キー 属性値
Doctrine_Core::ATTR_RESULT_CACHE 結果キャッシュ用のキャッシュドライバオブジェクト
Doctrine_Core::ATTR_RESULT_CACHE_LIFESPAN 結果キャッシュのデフォルトの存続秒数
Doctrine_Core::ATTR_QUERY_CACHE クエリーキャッシュ用のキャッシュドライバオブジェクト
Doctrine_Core::ATTR_QUERY_CACHE_LIFESPAN クエリーキャッシュのデフォルトの存続秒数

symfonyプロジェクト全体で共通で利用される設定は、ProjectConfigurationクラス(config/ProjectConfiguration.phpファイル)のconfigureDoctrine()メソッド内で行うとよいでしょう。

以下の例では、クエリーキャッシュと結果キャッシュの両方にmemcachedドライバを設定しています。また、それぞれのデフォルトの存続時間を1時間に設定しています。

class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    $this->enablePlugins('sfDoctrinePlugin');
  }

  public function configureDoctrine(Doctrine_Manager $manager)
  {
    // 設定の配列を追加して、利用するmemcachedサーバを追加できます
    $servers = array(
        array(
          'host' => 'localhost',
          'port' => 11211,
          'persistent' => true),
        ); 
    $cacheDriver = new Doctrine_Cache_Memcache(array(
          'servers' => $servers,
          'compression' => false));
    $manager->setAttribute(Doctrine_Core::ATTR_QUERY_CACHE, $cacheDriver);
    $manager->setAttribute(Doctrine_Core::ATTR_QUERY_CACHE_LIFESPAN, 3600);
    $manager->setAttribute(Doctrine_Core::ATTR_RESULT_CACHE, $cacheDriver);
    $manager->setAttribute(Doctrine_Core::ATTR_RESULT_CACHE_LIFESPAN, 3600);
  }
}

クエリーキャッシュはキャッシュドライバが設定されていれば利用され、プログラマーがそれ以上留意しなければならない点はありません。一方結果キャッシュの場合は、プログラムにその利用を明示するコードを追加しない限り利用はされません。

結果キャッシュを利用する

結果キャッシュを利用するには、Doctrine_Query::useResultCache()メソッドを使用します。

$q = Doctrine_Query::create()
  ->from('User u')
  ->where('u.username = ?')
  ->useResultCache(true);
$user = $q->fetchOne('jonwage');

Doctrine_Query::useResultCache()メソッドには3ヶのパラメータがあります。

  1. キャッシュドライバ
    • TRUE: 既定のキャッシュドライバを使う
    • キャッシュドライバオブジェクト: そのキャッシュドライバを使う
    • NULL: キャッシュを利用しない
  2. キャッシュの存続秒数: 省略時はドライバの既定の秒数
  3. キャッシュのキー: 省略時は自動生成されるMD5ハッシュ値

なお、上の例のような単純なクエリーはDoctrine_Tableのファインダーメソッドで置き換え可能で、例えば上のクエリーの実行結果と同じデータは「UserTable::getInstance()->findByUsername('jonwage')」でも得られますが、メソッドをオーバーライドしたりイベントなどを利用する場合を除いて、今のことろファインダーメソッドで直接結果キャッシュを利用する方法は用意されていません。

結果キャッシュを削除する

キャッシュはキャッシュドライバのdelete()メソッドで、キャッシュキーを指定して削除できます。キーが既知の場合、つまりDoctrine_Query::useResultCache()メソッドの3番目のパラメータでキーを指定している場合には、この方法でキャッシュを削除できます。

$q = Doctrine_Query::create()
  ->from('User u')
  ->where('u.username = ?')
  ->useResultCache(true, null, 'user:jonwage');
$user = $q->fetchOne('jonwage');

// キーを指定してキャッシュを削除する
$cacheDriver = UserTable::getInstance()->getAttribute(Doctrine_Core::ATTR_RESULT_CACHE);
$cacheDriver->delete('user:jonwage');

キャッシュドライバには単一のキャッシュを削除するdelete()メソッドの他に、複数のキャッシュをまとめて削除する、deleteByPrefix()メソッドとdeleteByRegex()メソッドがあります。deleteByPrefix()メソッドでは指定の文字列に前方一致するキーのキャッシュが削除されます。deleteByRegex()メソッドでは指定の正規表現に合致するキーのキャッシュが削除されます。

Doctrine_Query::useResultCache()メソッドでキャッシュキーの指定が省略された場合は、DQLの内容に基づくMD5ハッシュ値がキーとなります。キャッシュキーはDoctrine_Query::getResultCacheHash()メソッドで取得できるので、キーの指定を省略した場合でも、キャッシュの生成時と同じDQLが再現できる場合は、以下のようなコードでキャッシュを削除できます。

$q = Doctrine_Query::create()
  ->from('User u')
  ->where('u.username = ?')
  ->useResultCache(true);
$user = $q->fetchOne('jonwage');

// DQLのキャッシュを削除する
$q = Doctrine_Query::create()
  ->from('User u')
  ->where('u.username = ?');
$q->getResultCacheDriver()->delete(
    $q->getResultCacheHash('jonwage'));

なお、Doctrine_Queryにはこれを簡便に行うclearResultCache()メソッドも用意されていますが、このメソッドはgetResultCacheHash()メソッドと異なり、今のところメソッドのパラメータでプレースホルダーの値を指定できません。従って、例えば、以下でclearResultCache()メソッドを実行している2つのコードの前者には、プレースホルダーの値(u.username = 'jonwage')がどこにも指定されていないため、正しいキャッシュを削除できません。後者ではWHERE句でプレースホルダーの値を指定しているので、正しいキャッシュが削除されます。

$q = Doctrine_Query::create()
  ->from('User u')
  ->where('u.username = ?')
  ->useResultCache(true);
$user = $q->fetchOne('jonwage');

// 正しいキャッシュを削除できません
$q = Doctrine_Query::create()
  ->from('User u')
  ->where('u.username = ?');
$q->clearResultCache();

// 正しいキャッシュが削除されます
$q = Doctrine_Query::create()
  ->from('User u')
  ->where('u.username = ?', 'jonwage');
$q->clearResultCache();

また、Doctrineではクエリー結果をハイドレートする方法が指定できますが、同じクエリーでもハイドレーションモードによってキャッシュキーは異なる点に注意してください。

// 以下の2つのクエリーのキャッシュキーは異なります

// オブジェクトにハイドレート
$q = Doctrine_Query::create()
  ->from('User u')
  ->where('u.username = ?')
  ->useResultCache(true);
$user = $q->fetchOne('jonwage');

// 配列にハイドレート
$q = Doctrine_Query::create()
  ->from('User u')
  ->where('u.username = ?')
  ->useResultCache(true);
$user = $q->fetchOne('jonwage', Doctrine_Core::HYDRATE_ARRAY);

イベントフックを利用する

Doctrineのイベントフックを利用すると、レコードの更新時にキャッシュを自動的に削除することができます。

以下の例は、userテーブルのクエリーの実行結果データが、「user:」で始まるキャッシュキーでキャッシュされているという想定で、このテーブルのレコードが更新/削除される際にキャッシュをまとめて削除します。

class User extends BaseUser
{   
  public function postSave(Doctrine_Event $event)
  {
    $this->clearResultCaches();
  }

  public function postDelete(Doctrine_Event $event)
  {
    $this->clearResultCaches();
  }

  protected function clearResultCaches()
  {
    $cacheDriver = $this->getTable()->getAttribute(Doctrine_Core::ATTR_RESULT_CACHE);
    if ($cacheDriver instanceOf Doctrine_Cache_Interface) {
      $cacheDriver->deleteByPrefix('user:');
    }
  }
}

以下の例は、userテーブルに関するすべてのクエリーの実行結果をキャッシュします。

class User extends BaseUser
{
  public function preDqlSelect(Doctrine_Event $event)
  {
    $cacheDriver = $this->getTable()->getAttribute(Doctrine_Core::ATTR_RESULT_CACHE);
    if ($cacheDriver instanceOf Doctrine_Cache_Interface) {
      $event->getQuery()->useResultCache(true);
    }
  }
}

ただし、このコードには注意すべき点があります。このコードでは、クエリー対象にこのテーブルが含まれるすべてのクエリーの実行結果がもれなくキャッシュされます。JOINなどによってこのテーブル以外にもクエリーの対象テーブルが存在するクエリーでは、結果キャッシュを利用したくないテーブルがその中に含まれないように配慮する必要があります。

なお、このコードではpreDqlSelect()フックを利用していますが、preDql*()フック(DQLコールバック)はDoctrine::ATTR_USE_DQL_CALLBACKS属性がTRUEの場合にのみ有効です。この属性値はデフォルトではFALSEで、TRUEに変更することで実行時に多少のオーバーヘッドが生じる点にも注意してください。

// ProjectConfiguration::configureDoctrine()メソッド内などで
$manager->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, true);

イベントリスナーを利用する

複数のテーブルのクラスに、共通のイベントフックコードをコピーペーストしてまわるのはDRYではありません。Doctrineではイベントリスナーやビヘイビアを利用することで、複数のテーブルクラスに共通の動作を一箇所で定義できます。

以下は、上で例に挙げた、あるテーブルに関するすべてのクエリーの実行結果をキャッシュするイベントフックをリスナーに仕立てたものです。

class myAlwaysUseResultCacheListener extends Doctrine_Record_Listener
{
  public function preDqlSelect(Doctrine_Event $event)
  {
    $cacheDriver = $event->getInvoker()->getTable()->getAttribute(Doctrine_Core::ATTR_RESULT_CACHE);
    if ($cacheDriver instanceOf Doctrine_Cache_Interface) {
      $event->getQuery()->useResultCache(true);
    }
  }
}

テーブルのクラスにイベントリスナーを設定するには、schema.ymlでlistenersにリスナーのクラス名を指定します。

User:
  listeners:
    myAlwaysUseResultCacheListener:
  actAs:
    Timestampable:
  columns:
    username: { type: string(20),  notnull: true, unique: true, regexp: "/^[0-9a-z]{3,20}$/" }
    password: { type: string(50),  notnull: true, minlength: 6 }
    email:    { type: string(255), notnull: true, unique: true, email: true }

Doctrine_Queryをオーバーライドする

これまでに見てきたように、Doctrineのキャッシュ機能の中核はDoctrine_Queryクラスに実装されています。Doctineには、Doctrine_Queryをその継承クラスに容易に切り替える手段が用意されており、必要であればその動作を変更することが可能です。

Doctrine_Queryを独自のクラスに切り替えるには、Doctrine_Core::ATTR_QUERY_CLASS属性にクラス名を設定します。Doctrine_Query::create()メソッドは、Doctrine_Core::ATTR_QUERY_CLASS属性に設定されたクラス名があれば、Doctrine_Queryではなく、設定されているクラスのオブジェクトを返します。

// ProjectConfiguration::configureDoctrine()メソッド内などで
$manager->setAttribute(Doctrine_Core::ATTR_QUERY_CLASS, 'myDoctrineQuery');

以下の例では、Doctrine_Query::getResultCacheHash()メソッドをオーバーライドして、キャッシュキーの先頭にクエリー対象のテーブル名を自動的に追加しています。なお、このコードでは、クエリー対象のテーブルが複数ある場合でも、キーに追加されるのはFROM句で指定されるテーブル名のみとなる点に注意してください。

class myDoctrineQuery extends Doctrine_Query
{
  public function getResultCacheHash($params = array())
  {
    $hash = parent::getResultCacheHash($params);
    if ( ! empty($this->_dqlParts['from'])) {
      $fromPart = $this->_dqlParts['from'];
    } elseif ( ! empty($this->_sqlParts['from'])) {
      $fromPart = $this->_sqlParts['from'];
    } else {
      return $hash;
    }   
    list($componentName) = explode(' ', trim($fromPart[0]), 2);
    $tableName = Doctrine_Inflector::tableize($componentName);
    return "$tableName:$hash";
  }
}

以下の例では、前者のキャッシュキーは「user:32bd63cf2cdef179ab2857bc357aeca7」のように、通常のMD5ハッシュ値の前に「user:」が付加されたものになります。後者の例では、useResultCache()メソッドのパラメータで「jonwage」というキャッシュキーを指定していますが、実際のキャッシュキーは「user:jonwage」となります。

// キャッシュキーが指定されていない場合
$q = Doctrine_Query::create()
  ->from('User u')
  ->leftJoin('u.Profile p')
  ->where('u.username = ?')
  ->useResultCache(true);
echo $q->getResultCacheHash('jonwage'); // user:32bd63cf2cdef179ab2857bc357aeca7

// キャッシュキーが指定されている場合
$q = Doctrine_Query::create()
  ->from('User u')
  ->leftJoin('u.Profile p')
  ->where('u.username = ?')
  ->useResultCache(true, null, 'jonwage');
echo $q->getResultCacheHash('jonwage'); // user:jonwage

イベントフックの項でuserテーブルのレコードが更新/削除される際に、キャッシュキーが「user:」で始まるキャッシュをまとめて削除する例を挙げましたが、このmyDoctrineQueryを利用することで、userテーブルのクエリーの実行結果データはもれなく「user:」で始まるキャッシュキーでキャッシュされるようになります。

ということで

ここに挙げたコードはいずれも、例としてわかりやすい簡単なものであることを優先しているため、実のところあまり実用的ではありません。例えば、キャッシュキーの設計は、キャッシュをきちんと制御する上で慎重に検討しなければならない重要な課題ですが、ここで例に選んでいるキーは安易に過ぎます。あるいは例えば、キャッシュドライバのdeleteBy*()メソッドは便利なものではあるのですが、その実装の都合上、膨大な数のキャッシュが存在し得るシステムでは、実用に耐えるパフォーマンスが発揮できるかは疑問です。実際のプログラミングで必要になるコードは、ここで挙げたものよりも多少なりとも複雑にならざるを得ないでしょうが、それでも、組み込みのキャッシュ機能と、イベントフックやビヘイビアなど、Doctrineが提供する多彩な機能を組み合わせることで、より効率的なキャッシュ処理が、より簡単に実現できる可能性があります。御用とお急ぎでない方は是非ご利用になってみてください。

ウノウでは特に最近、積極的にエンジニアを採用しています。
採用ページをご覧になり興味のある方、ぜひご応募ください!!
Find Job!でも募集開始してます!

2008年12月 1日

symfony propelでシーケンス名が省略されてしまう
このエントリーをブックマークに追加 このエントリーをlivedoorクリップに追加

yukiです。

今回は「symfonyでよくあるトラブル」を掲載してからちょうど1年を経過したので、また今年も取り上げてみたいと思います。

前回はsymfony+MySQLを使っての場合でしたが、今回はsymfony+PostgreSQLでお送りします。
とはいえ以前よりもだいぶsymfonyを取り巻く状況も変化し、バグらしいバグもほとんど既知のものだったり、枯れて(?)きたのかな、とも思います。
そんななか、小一時間ほどハマってしまった不具合があったので、今回はそのご紹介のみになります。

symfony propel-build-modelで作成したSQLのシーケンス名が食い違う

どうもテーブル名やカラム名がある程度長いと、途中で省略されてしまったりして、誤認識してしまう。
PostgreSQLの場合英数字63文字(63byte)までは問題ないはずが、どうもそれより短い文字数でも途中で切れてしまう問題。
どうもPropelのバグらしいので、自作パッチを当てるなり、UPDATEで修正されることを期待するしかなさそうです。

$ cat symfony/lib/vendor/propel-generator/classes/propel/engine/platform/PgsqlPlatform.php
中略
    /** 
     * @see        Platform#getMaxColumnNameLength()
     */
    public function getMaxColumnNameLength()
    {   
        return 32; 
    }   

中略

今回もアッサリしたエントリになってしまいましたが、お役に立てれば幸いです。

2008年1月24日

Symfonyプラグインまとめ ~その2~
このエントリーをブックマークに追加 このエントリーをlivedoorクリップに追加

yukiです。

前回のエントリから大分時間がたってしまいましたが、symfonyプラグインまとめ~その2~をお送りしたいと思います。
今日までに追加された分と未紹介の分についてのご紹介です。

  • Dynamic Generators
    sfCssTabsplugin
    Word Press の管理画面風なタブレイアウト
    sfSavvyPlugin
    symfonyの追加ヘルパー
    sfSiteMapPlugin
    簡易サイトマップ作成
    sfSitemap2Plugin
    sfSiteMapPluginを使ってオブジェクト/配列からXML作成
    sfUIPlugin
    データグリッド作成支援
  • JavaScript
    • Not Based on a JS Framework
      sfAjaxUploaderPlugin
      ajaxを利用したアップロードフォームタグヘルパー
      sfJSONRPCPlugin
      JSON-RPCの利用
      sfLlooggPlugin
      LLOOGGコード埋め込みフィルタ
      sfMilonicPlugin
      Milonicを利用したドロップダウンリスト
      sfPJSPlugin
      actionからjsファイルを動的に生成
      sfUJSPlugin
      actionからjsファイルを動的に生成
      sfUrchinPlugin
      Google Analiticsのコードを埋め込む
    • Dojo
      sfDojoPlugin
      DoJo Toolkitの追加
      sfUnobstrusiveDojoPlugin
      Dojoの利用ヘルパー
    • Ext
      sfExtJSPlugin
      Ext1.x系の利用支援
      sfExtjs2Plugin
      Ext2.x系の利用支援
      sfExtjsThemePlugin
      scaffold生成した管理画面でExtを利用
    • jQuery/jQueryUI
      ddJQueryCalendar
      jQuery calenderの追加
      mqThickboxPlugin
      Thickbox の利用
      sfJqueryPlugin
      Jqueryの利用
      sfTaconitePlugin
      JQuery Taconite Pluginの利用
    • Prototype/Scriptalicious
      dwPrototypeTooltipPlugin
      prototypeベースのツールチップ
      dwPrototypeWindowPlugin
      prototypeベースのウィンドウ・ダイアログ
      sfgWidgetsPlugin
      gWidgets libraryの追加
      sfLightboxPlugin
      LightBox2の利用
      sfLightWindowPlugin
      LightWindow v2.0の利用
      sfModalBoxPlugin
      ModalBoxの利用
      sfNiftyPlugin
      Nifty corner cubeの利用
      sfPrototypePlugin
      デフォルトのprototype.jsを置き換えて使えるようにする
    • YUI
      sfYUIPlugin
      YUI(Yahoo UI Library)の利用
  • 画像・動画・Flash・PDF
    • 共通
      sfMediaLibraryPlugin
      アップロードされたメディアの管理支援
      sfMogileFSPlugin
      MogileFSを利用支援
    • Flash
      dwSwfChartPlugin
      SWFChartsを使ったチャート生成
      sfAmChartsPlugin
      amChartを使ったチャート生成
      sfSIFRPlugin
      sIFRの利用
      sfSwfObjectHelperPlugin
      javascriptを利用したSWFObjectヘルパー
    • 画像
      dwJpgraphPlugin
      JpGraphの利用
      sfChartDirectorPlugin
      ChartDirectorの利用
      sfFlickrGalleryPlugin
      FlickrAPIの利用
      sfGallery2Plugin
      Gallery2の利用
      sfSmiliePlugin
      Wordpress Smiliesの利用
      sfTextReplacementPlugin
      GDを使った文字の画像化
      sfThumbnailPlugin
      アップロードされたサムネイルの生成
    • PDF
      sfDomPDFPlugin
      HTMLをPDFに変換
      sfOpenOfficePlugin
      OpenOffice形式の出力支援
      sfPDFLatexPlugin
      LaTexを利用してPDF出力
      sfTCPDFPlugin
      TCPDFを利用してPDF出力
    • 動画
      sfFLVPlayerPlugin
      FLVプレイヤー

参照元:

2007年12月 3日

symfonyでよくあるトラブル
このエントリーをブックマークに追加 このエントリーをlivedoorクリップに追加

yukiです。

今回はsymfony+MySQLを使って開発していた際に遭遇したトラブルとその対処を紹介したいと思います。

(1) ビルドしたPropelクラスで、DATETIME値が'0000-00-00 00:00:00'の時

SQLとしては問題なくとも、getTimeStampで取得する際にエラーになります。
NULL値を使うのが一番なのですが、外部の設計だったりするなど今回は変更できない場合に該当しました。
symfony側でもtrac で認識はされていますが、Creole の問題として対処しないことになっているようですので、今度はCreoleのtrac を見てみると、対処されているのがわかり、freeze化していれば影響は該当サイトのみにとどまりますので、今回は自作パッチを当てて対処しました。
そのうちsymfony側で対応してくれることを期待しておきます。

(2) カラム名に予約語を使わない

これもありがちですが、カラム名に「count」などの予約語を使うと、自動生成されたPropelのBaseオブジェクトを利用しようとすると、オブジェクト定数として利用しているため二重宣言となり、FATALエラーとなります。(phpNameでエイリアスをつけても駄目)propel-build-modelコマンドは普通に通過しますので見落としますが、注意してください。

(3) redirect での引数の型が違う

これも若干はまりどころですが、redirectする際、'module/action'と文字列で'/'で区切って記述しますが、routingを利用して引数も渡したい場合、素直に書くと'foo/bar/variable'となります。
しかしこのように正直に書いても'variable'が絶ち落とされてしまいます。
この場合はどうするかというと、まずrouting.ymlで下記のように書いてあるものとして、

foo_bar_hoge:
  url:      /foo/bar/:hoge
  param:    { module: home, action: bar }

次に、redirectメソッドの引数は素直に書くのではなく、配列で渡します。

$this->redirect(array('module' => 'foo', 'action' => 'bar', 'hoge' => 'variable'));

これで正しくリダイレクトされ、引数も渡されます。

この様に若干ドキュメントに書いていないトラブルはあるものの、逆に書いていない部分で便利な機能があったりしますので(特にヘルパー)、ぜひ一度ソースを読んでみてはいかがでしょうか。

2007年10月 3日

Symfonyプラグインまとめ ~その1~
このエントリーをブックマークに追加 このエントリーをlivedoorクリップに追加

yukiです。

PHPのフレームワークは数多くありますが、みなさん選ぶポイントはどのような点でしょうか。
セキュリティの問題など多々あると思いますが、共通しているのは「楽をすること」だと思います。
最近は自宅で開発する時はsymfonyを使おうかと思い調べてみたところ、優秀なプラグインが多くありました。
今回はその一部をご紹介をしたいと思います。

  • Dynamic Generators
sfCssTabsplugin
Word Press の管理画面風なタブレイアウト
sfSavvyPlugin
symfonyの追加ヘルパー
sfSiteMapPlugin
簡易サイトマップ作成
sfSiteMapPlugin
簡易サイトマップ作成
  • JavaScript
ddJQueryCalendar
jQuery calenderの追加
sfDojoIntegration
DoJo Toolkitの追加
sfWidgetsPlugin
gWidgets libraryの追加
sfJSONRPCPlugin
JSON-RPCの利用
sfLightboxPlugin
LightBox2の利用
sfLightWindowPlugin
LightWindow v2.0の利用
sfMilonicPlugin
Milonicを利用したドロップダウンリスト
sfModalBoxPlugin
ModalBoxの利用
sfNiftyPlugin
Nifty corner cubeの利用
sfPJSPlugin
actionからjsファイルを動的に生成
sfPrototypePlugin
デフォルトのprototype.jsを置き換えて使えるようにする
sfTaconitePlugin
JQuery Taconite Pluginの利用
sfUJSPlugin
actionからjsファイルを動的に生成
sfUnobstrusiveDojoPlugin
Dojoの利用ヘルパー
sfUrchinPlugin
Google Analiticsのコードを埋め込む
sfYUIPlugin
YUI(Yahoo UI Library)の利用

http://trac.symfony-project.com/wiki/SymfonyPluginsご紹介したプラグイン以外にもここに数多くのプラグインが詳しく載っていますのでご参考下さい。
次回はもう少しコード内部に影響するプラグインをご紹介しようと思います。

  [PR] 転職


About symfony

ブログ「ウノウラボ Unoh Labs」のカテゴリ「symfony」に投稿されたすべてのエントリーのアーカイブのページです。過去のものから新しいものへ順番に並んでいます。

前のカテゴリはPythonです。

次のカテゴリはTipsです。

他にも多くのエントリーがあります。メインページアーカイブページも見てください。

ウノウサービス