メイン

2011年3月23日

symfonyエキスパートになるために必須の1冊
このエントリーをはてなブックマークに追加 このエントリーをlivedoorクリップに追加

突然ですが、symfonyのエキスパートになりませんか?HIROKIです。

PHPのフルスタックフレームワークであるsymfonyについて共著した書籍が発売されました。私はテスト駆動開発(TDD)とsymfonyによる自動テストについて執筆しました。

具体的にはLimeを利用したユニットテストをはじめ、sfBrowserを利用して、ブラウザアクセスをエミュレーションした自動テストを書くことによって、実際に人間がブラウザを通してアクセスしている内容まで自動テストにします。これを、テストファーストのアプローチで開発する内容になっています。

TDDや自動テストを書くというのは、プログラマーとして、プロとしてのスキルです。これを機会に、ばっちりテストコード書けるぜ!という人が増えることを願っております。

sf14book-image.jpeg

本書は

  • symfonyプロジェクトに存在するドキュメントやリファレンスを読む前に必要な前提知識
  • 詳しいインストール方法
  • 少しハードルの低いチュートリアルといった入門者向け

といった入門者向けの内容と

  • 公式ドキュメントに目を通した人が次のレベルにステップアップする為の知識
  • 実開発、実運用で役立つTips

といった上級者向けの内容が盛り込まれています。

ソースコードもダウンロードできるので、実際に手を動かしながらsymfonyを体感してください。

続きを読む "symfonyエキスパートになるために必須の1冊" »

2010年12月20日

Symfony2のFormsを使ってみる
このエントリーをはてなブックマークに追加 このエントリーをlivedoorクリップに追加

こんにちは。目下Symfony2を勉強中のtakaです。

Symfony1.4ではsfFormを使った事があるのですが、いくらか苦戦したというか、sfWidgetFormが面倒というか、これは使えば楽なのか?何なのか?良く判らなかったので、今回Symfony2でどのように実装していくのかFormsを試してみました。

まず、Symfony2についてですが、12/01時点ではSymfony2 PR4が公式サイトにて公開されていました。今回はこれを使ってやってみます。環境は適当にCentOS5.5にPHP5.3などなどを入れてcheck.phpが動く事を確認しました。ダウンロードして展開するとsandboxとありHelloアプリケーションが既に用意されています。

Controller名はhello/signupとします。

src/Application/HelloBundle/Resources/config/routing.yml
  1 signup:
  2     pattern:  /hello/signup
  3     defaults: { _controller: HelloBundle:Hello:signup }
  4 
  5 hello:
  6     pattern:  /hello/:name
  7     defaults: { _controller: HelloBundle:Hello:index }

Formの内容Entityを定義するところからはじめます。

src/Application/HelloBundle/Entity/Customer.php
  1 <?php
  2 namespace Application\HelloBundle\Entity;
  3 
  4 class Customer
  5 {
  6     public $name;
  7     private $age = 20;
  8 
  9     public $address;
 10 
 11     public $email;
 12 
 13     public $emails = array('1' => 'a', '2' => 'b', '3' => 'c');
 14 
 15     public function getAge()
 16     {
 17         return $this->age;
 18     }
 19 
 20     public function setAge($age)
 21     {
 22         $this->age = $age;
 23     }
 24 }

顧客情報として名前や年齢などを定義します。publicで定義した項目については自動でアクセッサが用意されるようですが、privateで定義した項目については(ここでは年齢)アクセッサを自前で用意してあげる必要があるようです。

src/Application/HelloBundle/Entity/Address.php
  1 <?php
  2 namespace Application\HelloBundle\Entity;
  3 
  4 class Address
  5 {
  6     public $street;
  7     public $zipCode;
  8 }

住所を定義する場合はこのように分けて、目的毎にしておくと良いようです。また、Formの定義はすべてEntity以下に配置します。

Formの定義が終わったら次はViewです。

src/Application/HelloBundle/Resources/views/Hello/signup.php
  1 <?php $view->extend('HelloBundle::layout.php') ?>
  2 
  3 <form action="#" method="post">
  4     <?php echo $view['form']->render($form) ?>
  5 
  6     <input type="submit" value="Send!" />
  7 </form>

共通部品のlayout.phpとformタグのみと至ってシンプルです。Formオブジェクトをrenderに渡すだけです。Formオブジェクトの中身はControllerの方で実装します。

FormといえばValidationです。Formの定義同様にEntityに配置します。

src/Application/HelloBundle/Entity/Registration.php
  1 <?php
  2 namespace Application\HelloBundle\Entity;
  3 
  4 class Registration
  5 {
  6     /** @validation:Vaild */
  7     public $customer;
  8 
  9     /** @validation:AssertTrue(message="Please accept the terms and conditions.") */
 10     public $termsAccepted = false;
 11 
 12     public function process()
 13     {
 14         // 特にエラーもなければ保存やリダイレクト等の処理
 15     }
 16 }

アノテーションを使って定義します。顧客情報の入力と規約に同意することを定義しています。もし$termsAcceptedがTrueで無い場合はmesseage以下の文字列が表示されエラーとなります。process()メソッドは成功時にPOSTデータの登録なり確認なりの処理を実装します。

Controllerです。ここではsignupActionを実装しています。

src/Application/HelloBundle/Controller/HelloController.php
  1 <?php
  2 
  3 namespace Application\HelloBundle\Controller;
  4 
  5 use Symfony\Bundle\FrameworkBundle\Controller\Controller;
  6 
  7 use Application\HelloBundle\Entity\Customer;
  8 use Application\HelloBundle\Entity\Registration;
  9 use Symfony\Component\Form\Form;
 10 use Symfony\Component\Form\TextField;
 11 use Symfony\Component\Form\IntegerField;
 12 use Symfony\Component\Form\FieldGroup;
 13 use Symfony\Component\Form\RepeatedField;
 14 use Symfony\Component\Form\CollectionField;
 15 use Symfony\Component\Form\CheckboxField;
 16 
 17 class HelloController extends Controller
 18 {
 19     public function indexAction($name)
 20     {
 21         return $this->render('HelloBundle:Hello:index.twig', array('name' => $name));
 22 
 23         // render a PHP template instead
 24         // return $this->render('HelloBundle:Hello:index.php', array('name' => $name));
 25     }
 26 
 27     public function signupAction()
 28     {
 29         $registration = new Registration();
 30         $registration->customer = new Customer();
 31 
 32         $form = new Form('registration', $registration, $this->get('validator'));
 33         $form->add(new CheckboxField('termsAccepted'));
 34 
 35         $group = new FieldGroup('customer');
 36 
 37         $group->add(new TextField('name'));
 38         $group->add(new IntegerField('age'));
 39         $form->add($group);
 40 
 41         /* グループ化したり様々なフィールドを追加できる
 42         $group = new FieldGroup('address');
 43         $group->add(new TextField('street'));
 44         $group->add(new TextField('zipCode'));
 45         $form->add($group);
 46 
 47         $form->add(new RepeatedField(new TextField('email')));
 48 
 49         $form->add(new CollectionField(new TextField('emails')));
 50         */
 51 
 52         if ('POST' === $this->get('request')->getMethod()) {
 53             $form->bind($this->get('request')->request->get('registration'));
 54 
 55             if ($form->isValid()) {
 56                 // Validation True
 57                 $registration->process();
 58             }
 59         }
 60 
 61         return $this->render('HelloBundle:Hello:signup.php', array(
 62             'form' => $form
 63         ));
 64     }
 65 }

基本的なテキストボックスやチェックボックス、数値や日付といったFormコンポーネントが用意されており、適宜useで読み込みます。Formオブジェクトのaddメソッドで簡単に追加できます。FieldGroupは項目をグループ化するもので、Formタグ上ではregistration[customer][name]のようにアクセスできます。

55行目の$form->isValid()はValidationが成功時にTrueとなり処理が続行され、失敗時にはFalseとなりエラーメッセージと共にViewに表示されます。

Formオブジェクトにはエラーであれば$form['name']->getErrors()、Formデータであれば$form['name']->getData()のようにしてアクセスする事が可能です。また、registration[_token]という形でCSRFプロテクトが埋め込まれるようになっていました。

目的毎に切り分けておくことができ、組み合わせて使いやすいのではないかと思います。公式サイトのドキュメントにはこれ以外にも沢山のコンポーネントが用意されている事が書かれてありますので、今後色々と組み合わせて試してみたいと思います。


第1回Symfony2勉強会に引き続き勉強会を行うそうです。興味のある方は参加してみてはいかがでしょうか。

ATND 第2回 Symfony2勉強会 (1/15)


Zynga Japanでは積極的にエンジニアを採用しています。
採用ページをご覧になり、興味のある方はぜひご応募ください。

2010年12月16日

フィードバックサイクルを素早く回すために
このエントリーをはてなブックマークに追加 このエントリーをlivedoorクリップに追加

この記事は、Symfony アドベントカレンダー 2010に参加しています。

こんにちは。murahashi sanemat kenichi です。開発で重要なのはフィードバックのサイクルを素早く回すことですよね。

こうなってくれるとうれしい。
落ちるテストを保存するとred
for-quick-feedback-cycle-red.png
通る実装を保存するとgreen
for-quick-feedback-cycle-green.png

フィードバックサイクル

簡単に達成するために必要なのは次の4つです。
テストコードのグルーピング、ファイル更新検知、ファイル対応関係、screenのstatus line
Symfonyの場合の話を順にみていきます。
一発でグループ分けされたテストコードが走る仕組み
Symfonyは簡単
$ php symfony test:unit #=> 全ユニットテストが走る
$ php symfony test:all #=> 全テストが走る
$ php symfony test:functional NAME #=> NAMEの機能テストが走る
ファイル更新検知をして、それをトリガに別のスクリプトを呼び出す仕組み
Symfonyのアプリルートを watchr で監視して、レシピは symfony-watchr 書きました。
あるファイルを更新したときに、対応するテストがわかりやすい仕組み そしてその逆も
Symfonyは設定より規約 convention over configuration のルールに従っているので、対応するファイルがわかりやすいです。
テスト結果をparseして、GNU Screenのstatus lineに流し込む仕組み
autotest_screen から、screenのstatus lineに表示する部分をはがして screenout に切り出しました。

これで楽しいdeveloper testing lifeがおくれるようになりました。

$ watchr ~/work/symfony_watchr/bin/symfony_watchr.rb


screenoutとwatchrとsymfony_watchr組み合わせた時の欠点

欠点1
ファイルの対応関係を雑に書いたので、粒度が大きすぎて素早いフィードバックが回らなかった。
  • modelかunit以下のファイルを更新したら全unit testが走る
  • test/functional 以下のファイルを更新したら、そのfunctional testが走る
欠点2
コードが悪くて、会社環境だと100MB近くメモリ食ってた
そしてどこがわるいかよくわからない

車輪の再発明感満載なので、どなたか、もっとまともな組み合わせがあれば教えてください。
cpan探したのですがどこみていいかよくわからなくて、みんなどんなの使ってるんでしょう。

というわけで、 rspecautotestautotest_screen の説明をお送りしました。



参考URL



Symfony Advent 2010

Symfony Advent 2010 では12月1日から12月24日までを使って日替わりで symfony でイイなと思った小さな tips から内部構造まで迫った解説などを ブログ記事にして公開していくイベントです。

※Syfony Advent 2010はsymfony好きな有志で集まったチームです。

以上デース

Zynga Japanでは積極的にエンジニアを採用しています。
採用ページをご覧になり、興味のある方はぜひご応募ください。

2010年12月 6日

Symfony2勉強会に参加してみた
このエントリーをはてなブックマークに追加 このエントリーをlivedoorクリップに追加

こんにちは、小俣です。Symfony1.4にはだいぶ慣れてきた今日この頃、近々登場するらしいSymfony2が速いとかすごいとか噂されているので、乗り遅れないよう情報を仕入れておかねば!ということで、先日ちょうど弊社オフィスを会場に開催されたSymfony2勉強会に参加してみました。そのとき仕入れた情報をご紹介しようと思います。


はじめに:Symfony2は1.x系と内部構造が異なるので戸惑う

Symfony1.xに慣れている方にとっては、初めのうちはディレクトリ構造が違いすぎて何処に何があるのか混乱しがちです。私も何度も迷子になりました。少しずつ慣れていくしかありませんが。。。下記にSymfony2の特徴をいくつか紹介してみます。

Symfony2はすべてBundleで構成されている

Symfony2では、新しい概念がいくつか導入されていて、まずはBundle(直訳:束)の理解が必要です。Bundleとはそれ自体が1つのfeature(機能)を成していて、HogeBundleディレクトリにはHogeBundle.phpとController(/HogeController.php)とResources(/configと/views)とTestsがあります。configではroutingの設定ができ、viewsにはテンプレートを置きます。このようにBundleごとにソースがまとまっているため、機能を追加したり削除するときにBundleごと追加したり削除すればよいので、どの機能があるのかないのか視覚的に分かりやすく、また友達とBundleをshareするのも簡単!とのことです。

routingやlayoutをBundleごとにカスタマイズできる

app/views/layout.phpをBundle内のlayout.phpで継承しているので、Bundleごとにlayoutに違いを出したりするときに便利のようです。また、routingもBundleごとに設定できるようになっています。

DIコンテナを理解せずしてSymfony2の内部構造は理解できない

Symfony2では、DIコンテナ(Dependency Injection Container)が非常に重要な役割を果たしていて、シンプルなYAMLやXMLで設定できるコンフィグレーションシステムの裏には、すごい内部構造が隠されているようです。典型的なプロジェクトにおいては、DIコンテナには触ることはないでしょうとドキュメントに書いてありますが、知っておいた方がExtensionを扱うときにスムーズのようです。

それ以外のSynfony1.xからの変更点や違いについては、勉強会でLTもされていたアシアル小川さんの記事をご参照ください。


Symfony2を動かしてみよう

まず、Symfony2を動かすには、PHP5.3.2以上が必要です。その準備ができたらhttps://github.com/symfony/symfony-sandboxからSymfony2のsandboxをゲットし、cacheとlogsの権限を777に変えて、ブラウザからアクセスできるようにします(Apacheで公開するなど)。そこまでできたらsandbox/web/check.phpで動作環境のチェックもしておきましょう。

※ちなみに以下のレッスンの内容は、ワークショップで行われた内容を元に、自分でいろいろ内容を変えてしまっていますので参考程度にしてください。

レッスン1:HelloBundleのHelloControllerにtestActionを追加してみよう

まず、あらかじめ用意されているHelloBundleがどこかというと

sandbox/src/Application/HelloBundle
その中に
HelloBundle/
└ Controller/
 └ HelloController.php
└ HelloBundle.php
└ Resources/
 └ config/
  └ routing.yml(設定により.xml .phpも可)
 └ views/
  └ layout.php(設定により.twigも可)
  └ Hello/
   └ index.php(設定により.twigも可)
└ Tests/
 └ Controller/
  └ HelloControllerTest.php

があります。 これをこのまま動かすと、app.php/hello/hogeだと「Hello hoge!」と表示されます。
ではまず、HelloController.phpを下記にように変更します(testAction追加と同時に、indexActionから$nameを取り除いています)。
# HelloController.php
public function indexAction()
{
 return $this->render('HelloBundle:Hello:index.php');
}
public function testAction()
{
 return $this->render('HelloBundle:Hello:test.php', array('time' => date('H:i:s')));
}
続いて下記のようにテンプレートを追加します。
# views/Hello/test.php
ただいまの時刻:<?php echo $time ?>
そして最後にrouting追加。
# config/routing.yml
test:
 pattern:  /hello/test
 defaults: { _controller: HelloBundle:Hello:test }
hello:
 pattern:  /hello ← /:nameを外しました
 defaults: { _controller: HelloBundle:Hello:index }
app_dev.php/Hello/test(dev) app.php/Hello/test(prod) で表示の確認をしてみてください。 「ただいまの時刻:H:i:s」と表示される単純なものです。
devで見てみると、デバッグツールバーが表示されています。その説明はしないのですが、いろいろいじってみてください。
ちなみにドキュメントによると、単純なテンプレートでは$this->render()よりも$this->createResponse();を使うことで数ミリ秒の時間をセーブできるとのこと。

レッスン2:共通メニューを作って、layoutに入れてみよう

ダミーの共通メニューを作って、layoutに入れてみます。

# views/menu.php
メニュー1<br />
メニュー2<br />
メニュー3<br />
を作って、layout.phpで
# views/layout.php
<?php echo $view->render('HelloBundle::menu.php') ?>
を追記します。 これでメニューが表示されるようになります。

レッスン3:テンプレート側から指定アクションの結果を引いて表示してみよう

テンプレートで、別アクションの結果を表示することができるんです!これは便利じゃないでしょうか。

# views/Hello/index.php
<?php $name ?> ← $nameを使ってないので削除
<?php echo $view['actions']->render('HelloBundle:Hello:test') ?> 追記
これで、Hello/testの結果がHello/indexでも表示されます。

レッスン4:Edge Side Includes を試してみよう

ESI(Edge Side Includes)は、簡単に言うとWEBページをパーツごとにキャッシュして、更新が必要な箇所だけキャッシュの更新ができる技術です。これによりWebコンテンツ配信の高速化が可能になるとのことで、Symfony2もそれに対応しています。

これを使うと、レイアウト部分のキャッシュは30分毎に更新、コンテンツ部分のキャッシュは3秒毎に更新、という風にキャッシュの更新頻度を分けられる!とのことで、ワクワクしながら私も試してみたのですが、残念なことに私の環境ではうまくいかなかったので、皆さん頑張ってみてください!
ちなみに、$this->render()の結果をすぐにreturnせずに、キャッシュ制御してから返すようにします。

# HelloController.php
    $response = $this->render('HelloBundle:Hello:index.php');
    $response->setSharedMaxAge(30);    ←ここでキャッシュ制御
    return $response;
また、AppKernelをAppCacheでラップしたり、テンプレートでの書き方も変えたりする必要があるのですが、その辺の細かい 設定方法などの詳細はスライドドキュメントをご参照ください。


最後に

最後に余談となりますが、勉強会に行くのが初めてでドキドキの私でしたが、隣に座っていた@uechocoさんがいろいろ優しく教えてくれたので、安心して参加できました。その節はありがとうございました。
ちなみに、この勉強会に参加されてた@chobi_eさんが今、同じチームの仲間となって働いているんです!これをご覧になっている方で、ソーシャルアプリにご興味ある方は、ぜひ一緒に働きましょ〜。


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

2010年10月25日

MemcacheとMySQLのデータ不整合をPropelPDOで解決してみる
このエントリーをはてなブックマークに追加 このエントリーをlivedoorクリップに追加

こんにちは市丸です。

Zynga Japanでは単純なPrimaryKeyをキャッシュする際、symfonyのPeerをオーバーライドし自動的にキャッシュ&クリアしています。

UserPeer.php

class UserPeer extends BaseUserPeer
{
    public static function retrieveByPK($pk, PropelPDO $con = null)
    {
        if (!$data = /* Cacheからとるよ */) {
            $data = parent::retrieveByPK($pk, $con);
            /* "User_$pk"みたいなキーでCacheするよ */
        }
        return $data;
    }
    public static function doInsert($values, PropelPDO $con = null)
    {
        /* Cacheクリアするよ */
        return parent::doInsert($values, $con);
    }

    public static function doUpdate($values, PropelPDO $con = null)
    {
        /* Cacheクリアするよ */
        return parent::doUpdate($values, $con);
    }

    public static function doDelete($values, PropelPDO $con = null)
    {
        /* Cacheクリアするよ */
        return parent::doDelete($values, $con);
    }
}

ただし、ソーシャルゲームのように、2人以上のユーザーが同じテーブルを参照する場合、MemcacheとMySQLのデータ不整合に気をつけないといけません。

bad.png

このように、トランザクションのcommit前に別のユーザーからアクセスされると、CacheとMySQLのデータに不整合が起こります。

good.png

正しくは、このようにcommit後にキャッシュを削除しなければなりません。

そこで、PropelPDOを改変しcommit後にキャッシュ削除しましょう。

MemPropelPDO.php

class MemPropelPDO extends PropelPDO {
    protected $memcacheKeys = array();

    public function addMemcacheKey($key) {
        $this->memcacheKeys[] = $key;
    }

    public function commit(){
        parent::commit();
        foreach($this->memcacheKeys as $key){
            // よしなにCache削除してくだされ。
        }
    }    
}

database.yml

all:
 db1:
   class:        sfPropelDatabase
   param:
     classname:  MemPropelPDO
     (略)

User.php

class User extends BaseUser
{
    public function save(PropelPDO $con = null)
    {
        if (!is_null($con)) {
            $con->addMemcacheKey("User_".$this->getPrimaryKey())
        }
        return parent::save($con);
    }
}

【仕上げ】トランザクションを使う場合はsaveメソッドにMemPropelPDOを渡します

        $userModel = UserPeer::retrieveByPK(1);
        $userModel->setStatus(2); //statusを1->2にしてみる

        $con = Propel::getConnection(); // database.yml で指定したMemPropelPDO
        $con->beginTransaction();
        try {
            $userModel->save($con);
            $con->commit();
        } catch (Exception $e) {
            $con->rollback();
            throw $e;
        }

まあ、忘れずにcommit後にキャッシュクリアすればいいんだけどね!(爆)

2010年10月 6日

絵文字のためにsymfonyのカスタムハンドラーを作ってみました
このエントリーをはてなブックマークに追加 このエントリーをlivedoorクリップに追加

こんにちは、kayです。

携帯サイトのdocomo絵文字が文字と同じ色になっていると気になって気になって仕方がない今日この頃なのですが、YAMLでデフォルトの色を設定して絵文字を出力するときに自動的にその色にしてくれるなんてことをしたいなぁと思っていました。

しかし、sfYaml::load()をすると毎回パースしてしまうのでとっても頻繁に呼び出される絵文字の色にそれを使ってしまうと大変なことになってしまいます。

app.ymlなどのsymfony側の設定YAMLはパース結果をキャッシュしてくれるのですが、それを絵文字の色の設定でもすればいいと大先生に言われましたのでカスタムハンドラーを作って対応することにしました。

symfonyのビルドインハンドラー

まず、symfonyにはビルトインハンドラーが多数存在しますが、今回は汎用的に使えるsfDefineEnvironmentConfigHandlerとsfSimpleYamlConfigHandlerを見てみました。

  • sfDefineEnvironmentConfigHandler
    app.ymlのようにdev, prodなどの環境に応じて異なる値が設定できる汎用ハンドラー
    この場合環境によって設定が異なる訳ではないのでリッチすぎる
  • sfSimpleYamlConfigHandler
    パースした結果をキャッシュしてくれるだけのハンドラー
    sfConfig::add()をしてくれないので、読みだしただけではsfConfig::get()が使えない

結果、sfSimpleYamlConfigHandlerプラスアルファでsfConfig::add()をしてくれるだけでそこそこ使えるものになるなぁと思いました。

YAMLを用意

# config/hoge.yml
fuga: 1
foo:
  bar: 2

config_handlers.ymlを編集

config_handlers.yml内に、上記のYAMLファイルを読むのに使用するハンドラーを指定します。
ファイルパスを指定しておかないとデバッグモードが無効な状態でエラーになってしまうので要注意です。

config/hoge.yml:
  class: myHogeConfigHandler
  file: %SF_ROOT_DIR%/lib/config/myHogeConfigHandler.class.php
  param:
    # sfConfig::get()で値を取得する時のキープレフィクス
    prefix: hoge_

ハンドラー作成

あくまでも例ですが、下記のようにシンプルに出来ます。

<?php

class myHogeConfigHandler extends sfYamlConfigHandler
{
    public function execute($configFiles)
    {
        $prefix = strtolower($this->getParameterHolder()->get('prefix', ''));
        $config = self::parseYamls($configFiles);

        $hogeConfig = array();
        foreach ($config as $key => $value) {
            // sfConfig::get()で使うキープレフィックスを付ける
            $hogeConfig[$prefix . $key] = $value;
        }

        // include等した際に同時に中身をsfConfig::add()もするようにする
        return sprintf("<?php\n"
                . "// auto-generated by %s\n"
                . "// date: %s\n"
                . "sfConfig::add(%s);\n",
                __CLASS__,
                date('Y/m/d H:i:s'),
                var_export($hogeConfig, true));
    }
}

sfConfigCache::import()で設定を読む

  • sfConfigCache::checkConfig()してキャッシュファイルをインクルードしてくれます
  • インクルードの帰り値を返してはくれないけどこの場合sfConfig::get()で値を取得出来るようになっているので大丈夫です

YAMLの設定値をとにかく読み込ませておきたい場合はApplicationConfiguration::initialize()等に入れておきます

<?php

abstract class myApplicationConfiguration extends sfApplicationConfiguration
{
    public function initialize()
    {
        $this->getConfigCache()->import('config/hoge.yml', true, true);
    }
}

sfConfig::get()で設定値を取得

  • sfConfig::get('hoge_fuga')は1
  • sfConfig::get('hoge_foo')はarray('bar' => 2)
  • sfConfig::get('hoge_foo_bar')は2
とそれぞれ設定した値を返してくれるようになりました!

感想

マスターデータ系はDBではなくYAML等のファイルで設定するのが好きなのですが、その際の問題点を解決出来たので良かったです。みなさまもぜひぜひ使いやすいカスタムハンドラーを作ってみてはいかがでしょうか?

2010年9月24日

携帯とスマートフォンでsymfonyのテンプレートを切り替える
このエントリーをはてなブックマークに追加 このエントリーをlivedoorクリップに追加

こんにちは。中村です。

先日、まちつく!mixi版がスマートフォンに対応しました。

スマートフォン対応にあたって、元々まちつく!が携帯専用アプリであったために変更しなければいけない点がいくつかありましたが、その中でも今回はテンプレートの切り替えについて書きたいと思います。

時間的な都合もあり方針としては、基本的には携帯用のテンプレートを表示できるようにして、よく使われる機能についてはスマートフォン用のテンプレートを用意して表示するようにしました。

※まちつく!で利用しているsymfonyのバージョンとは違いますが、ここでは最新バージョンの1.4.7の場合になります。

独自のviewクラスを定義する

どのページがスマートフォンに対応しているかを管理することなく対応したかったので、アクセスしている端末とテンプレートファイルの有無によって挙動を変更することにしました。読み込むテンプレートの切り替えを行うために独自のviewクラスを定義します。アプリケーション名がfrontendだった場合は apps/frontend/config/module.yml に次のような設定をします。

all:
  view_class: myPHP

次にここで指定したmyPHPクラスを apps/frontend/lib/myPHPView.class.php として作成します。

テンプレートを切り替える

myPHPView.class.php の内容は次のような感じです(※コードはあくまでもサンプルです)。

<?php
class myPHPView extends sfPHPView
{

    const TOUCH_TEMPLATE_SUFFIX = 'Touch';

    public function initialize($context, $moduleName, $actionName, $viewName)
    {
        $config = sfProjectConfiguration::getActive();

        $touchViewName = $viewName . self::TOUCH_TEMPLATE_SUFFIX;

        // スマートフォン用のテンプレートファイルパス
        $touchTemplatePath = $config->getTemplateDir($moduleName, $this->getTemplate())
                           . DIRECTORY_SEPARATOR
                           . $actionName . $touchViewName . $this->getExtension();

        // スマートフォンからのアクセスでかつテンプレートが存在する場合
        if ($this->isTouch() && is_file($touchTemplatePath)) {
            parent::initialize($context, $moduleName, $actionName, $touchViewName);
            $this->setDecoratorTemplate(strtolower(self::TOUCH_TEMPLATE_SUFFIX));
        } else {
            parent::initialize($context, $moduleName, $actionName, $viewName);
        }

        return true;
    }

    public function isTouch()
    {
        $ua = $_SERVER['HTTP_USER_AGENT'];
        return (preg_match('/iPhone/', $ua) || preg_match('/Android/', $ua));
    }

}

スマートフォンからのアクセスでかつスマートフォン用のテンプレートファイルがある場合は parent::initialize にてスマートフォン用のテンプレートを指定しています。

また、setDecoratorTemplate メソッドを利用して、デフォルトで読み込まれる apps/frontend/templates/layout.php を apps/frontend/templates/touch.php に変更してヘッダ・フッタも変更できるようにしています。

まとめ

sfPHPViewを継承することでテンプレート読み込みに条件分岐を追加しました。独自のviewクラスはテンプレートファイルの切り替えもそうですがviewの挙動を変更するのに有効です。

2010年9月10日

Propel1.5で階層構造が扱えるnested_setビヘイビア
このエントリーをはてなブックマークに追加 このエントリーをlivedoorクリップに追加

yukiです。
今回は去年やりました階層構造についてです。Propel1.5でビヘイビアとして実装されたので早速使ってみました。
以下の例ではPropel単体ではなく、symfonyのsfPropel15Pluginを利用しています。

前回と同様のデータを利用したいので、対象となるテーブルのschema.ymlを以下のように定義しましました。

% cat schema.yml
propel:
  jojo:
    _attributes: { phpName: Jojo }
    _propel_behaviors:
      nested_set:
      timestampable:
    id: ~
    name:
      type: varchar
      required: true
      primaryString: true

するとテーブルのCREATEのSQLは以下のように出力されます。

% cat lib.model.schema.sql

# This is a fix for InnoDB in MySQL >= 4.1.x
# It "suspends judgement" for fkey relationships until are tables are set.
SET FOREIGN_KEY_CHECKS = 0;

#-----------------------------------------------------------------------------
#-- jojo
#-----------------------------------------------------------------------------

DROP TABLE IF EXISTS `jojo`;


CREATE TABLE `jojo`
(
        `id` INTEGER  NOT NULL AUTO_INCREMENT,
        `name` VARCHAR(255)  NOT NULL,
        `tree_left` INTEGER,
        `tree_right` INTEGER,
        `tree_level` INTEGER,
        `created_at` DATETIME,
        `updated_at` DATETIME,
        PRIMARY KEY (`id`)
) ENGINE=InnoDB;

# This restores the fkey checks, after having unset them earlier
SET FOREIGN_KEY_CHECKS = 1;

ご覧のように、tree_left, tree_right, tree_levelが作成されます。

実際の使い方ですが、以下のように通常のモデルのメソッドとしてビヘイビアメソッドが使えます。
コード中のコメントは出力結果です。

$george = new Jojo();
$george->setName('george');
$george->makeRoot();
$george->save();

/**
 * mysql> select id, name, tree_left, tree_right, tree_level from jojo;
 * +----+--------+-----------+------------+------------+
 * | id | name   | tree_left | tree_right | tree_level |
 * +----+--------+-----------+------------+------------+
 * |  1 | george |         1 |          2 |          0 |
 * +----+--------+-----------+------------+------------+
 * 1 row in set (0.00 sec)
 */

$jonathan = new Jojo();
$jonathan->setName('jonathan');
$jonathan->insertAsFirstChildOf($george);
$jonathan->save();

/**
 * mysql> select id, name, tree_left, tree_right, tree_level from jojo;
 * +----+----------+-----------+------------+------------+
 * | id | name     | tree_left | tree_right | tree_level |
 * +----+----------+-----------+------------+------------+
 * |  1 | george   |         1 |          4 |          0 |
 * |  2 | jonathan |         2 |          3 |          1 |
 * +----+----------+-----------+------------+------------+
 * 2 rows in set (0.00 sec)
 */

$george2nd = new Jojo();
$george2nd->setName('george the 2nd');
$george2nd->insertAsFirstChildOf($jonathan);
$george2nd->save();

/**
 * mysql> select id, name, tree_left, tree_right, tree_level from jojo;
 * +----+----------------+-----------+------------+------------+
 * | id | name           | tree_left | tree_right | tree_level |
 * +----+----------------+-----------+------------+------------+
 * |  1 | george         |         1 |          6 |          0 |
 * |  2 | jonathan       |         2 |          5 |          1 |
 * |  3 | george the 2nd |         3 |          4 |          2 |
 * +----+----------------+-----------+------------+------------+
 * 3 rows in set (0.00 sec)
 */

$george = JojoQuery::create()->findRoot();
var_dump(get_class($george)); //string(4) "Jojo"
var_dump($george->getName()); //string(6) "george"
var_dump($george->isRoot());  //bool(true)
var_dump($george->isLeaf());  //bool(false)

$jonathan = $george->getFirstChild();
var_dump($jonathan->getName()); //string(8) "jonathan"
var_dump($jonathan->isRoot());  ///bool(false)
var_dump($jonathan->isLeaf());  ///bool(false)

$george2nd = $jonathan->getFirstChild();
var_dump($george2nd->getName()); //string(14) "george the 2nd"
var_dump($george2nd->isRoot());  ///bool(false)
var_dump($george2nd->isLeaf());  ///bool(true)

このように、DoctrineのNestedSetビヘイビアに非常に近い操作感で扱えるので、階層構造を使う際にPropelを選択肢に入れることができるようになりました。
何かのお役に立てば幸いです。

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ご紹介したプラグイン以外にもここに数多くのプラグインが詳しく載っていますのでご参考下さい。
次回はもう少しコード内部に影響するプラグインをご紹介しようと思います。