2010/01/02

トラベリングはネットワーキング

今回に限った話ではなく、いつも旅行をしていて思うことがある。それは、旅行を通じて知り合う人の多いことだ。もちろんただ一人でフラッと出かけて、いつも一人でいてもいいのだけども、それとは違う楽しみ方として、旅行先で知り合う人と仲良くなるというのもいいものだ。

これは私みたいな個人旅行だけではなく、パッケージツアーでも同じだと思う。つまり、何か特定のことを一緒にする機会があると、一気に距離が近くなり、ご飯を一緒に食べたり、お酒を飲んだりしながら楽しむことができる。私のような個人旅行者とパッケージ旅行が違うのは、個人旅行者の方が選択の幅が広いということくらいか。

そんなこんなで今回の旅行でメールアドレスを交換した人は、15人以上にもなる。

  • 飛行機で隣になったパキスタン人の男性
  • フェリーで同じになったオランダ人夫婦と子供
  • ダイビングで仲良くなったフィンランド人夫婦
  • ダイビングで仲良くなった中華系マレーシア人 3人(ラヤンラヤンに行こう!と日付まで決めてくれてる)
  • ダイビングで毎日私のバディとなってくれたタイ在住のスイス人
  • ダイビングで仲良くなったスウェーデン人カップル
  • ダイビングで仲良くなった中華系マレーシア人とインド系マレーシア人のカップル
  • ダイビングで仲良くなった中華系マレーシア人カップル
  • ダイビングで仲良くなったスイス人と台湾人の夫婦
  • ダイビングで写真を撮ってあげた中華系タイ人
  • 私のダイブガイドのカナダ人

私の場合は特にダイビングがネットワーキングの強力な媒介活動となっているようだ。つまり、一緒にダイビングをする人とは、同じボートに乗るし、そこで旅行の話をしたり、それぞれの国のことを話したりする。そして、帰ってきてから晩ご飯を一緒にしたりしながら、さらに関係を深める。私の場合は、水中写真も撮るので、彼らの写真を撮ってあげたりすると、後で送って欲しいとか言われ、メールアドレスの交換をすることが多い。

実は、今までも旅行に行っていろんな人にあったのだけども、日本で社交パーティなんかに行くよりもよっぽど不思議で魅力的な人たちに会えると思っている。以前、ホンジュラスで結構高いリゾートに泊まったときなんかは、ほとんどの客がアメリカ人老夫婦(成功者たち)だった。そこでも音楽家、科学者、大学の先生、弁護士などがたくさんいたりする。だいたい私が彼らの子供くらいの年齢のせいか、相当仲良くしてもらったものだ。ホンジュラスにいたときは、旅行、そして、ダイビングをしながら仕事をするということが目的だったので、あまり一緒の時間を過ごすことはしなかったが、今回は完全なるダイビング旅行なので、毎日仲良くなった人と飲んだくれていた。旅行は旅行として楽しむのがいいのかもしれない。もしくは、もう少し時間に余裕を持って、毎日潜るのではなく二日に1回のペースで潜れば、いいとこ取りができるかもしれない。さすがに8日とか短すぎるので毎日潜りたくなるけど、1ヶ月の滞在だったらそんなに潜らなくてもいいしね。

で、出会った人とどこが旅行先としていい場所だとか、聞く。やっぱり昔から狙っているウェ島は良い場所みたいで、3人くらいから行った方がいいと言われた。他にもフィリピンのアポ島に行け、と激しく言われたし、紅海もしかり。また、マレーシア人のグループにはラヤンラヤンに行こうと誘ってくれたので、今はマジメにスケジュールを調整しようとしている。旅行の話をいろいろ聞くのは楽しい。次にどこに行こうか考えるだけでワクワクするので。

他には、旅行のチップス。タイに長い人にとっては普通のことなのかもしれないけども、パソコンからタイの携帯から直接ダイアルアップしてインターネットにつなぐことができるらしいのだ。しかも、安いときた。私は基本的に動画は見ないので、テキストだけで十分なのだけども、これができたら、ほぼタイ全域カバーできるんじゃないか、と思えてきた。私の今回宿泊した、Varin Resortは24時間インターネットで500バーツとかボッたくっているので、次回は、タイで携帯電話とsimカードを買って、セブンイレブン等で、通話時間のカードを買えば、タイ全域でインターネットが可能になるんだ。しかも、超安いらしい。これを聞いたとき、やべぇ。イーモバイルとか言っている場合じゃねぇよ。と思えた。

他には、旅行の話だけではなく、互いの国で流行っていることなどを聞く。新しいビジネスになるかもしれないしね。例えば、ある人は、タイで格闘技のサンドバッグやグローブと輸入して、自国で売っているらしい。他にもスウェーデンの優れたジャケットの話や、パーティサイトの話。そして、今私の頭で考えているP2P通貨交換サイト。円、ドルしか変更しない人には、あまり魅力的ではないのだけども、それは、円もドルも信頼度が高いので、レートが安定しているんだよね。でも、バーツとかだとレートが全然違うのよ。まぁ、それをコミュニティサイトとしてできるか設計して、やってみるです。本当はいろんなマネタイズが考えられるのだけども、とりあえずそれは放置。まず動くものを作らんとね。

pooh and mePoohのウェブサイトを修正した後に
Poohと一緒に撮ってもらったもの

また、今回行ったリペ島は、3回目なので、勝手を知っていることも大きい。実際は宿泊先として泊まれなかったし、他のダイブショップを使ったので、直接は関わっていないはずなのだけども、Pooh Bar のオーナーは、覚えてくれていて、次のように言ってくれた。
「オー、ジャパニーズエンジニアー、ユー、フィックスト、マイ、コンピュータ、ラスト、タイム」
「ユー、フィックス、マイ、ウェブサイト、ディス、タイム」
なんというか、不思議な英語を話すので、カタカナで書いた方がわかりやすいような内容だった。あのしゃべり方はわざとだろうな。。。って、いきなりかよって感じだったが、まぁ、せっかっくなので、ビールを飲みながら修正してあげて、彼のウェブサイトのガイドたちの写真の入れ替え等をしてあげた。もちろんビールは彼のおごり。というか、自分の店だしね。 まぁ、写真を入れ替えるだけの簡単なお仕事ですよ。というわけで、この2つは私がしてあげました。Lotus DivePooh lipeのサイト。いきなりDreamweaverを立ち上げて、「ユー、フィックス、イット、ヒアー」とか言われたので、困ったけど、なんとなくやってできました。初めてDreamweaverを使いましたw 写真は前からあったのだけども、一度も使ったことのない画像加工ソフトでサイズを調整してあげて、そしてそれぞれのIDをゲットするために、いきなりそこでダイブガイド4人集合して、その場でやっつけました。しかし、Poohは経営がメインなのでダイビングしないと思うのだが。。。

あと関係ないけど、PoohはiPhoneまで持っていたりする。常にbluetoothのヘッドホンを首にしているし。こいつガジェット好きか?

こうやって知っている人もいるし、ほとんどの新しい人に会うのも楽しい。もしかしたら、将来何かのつながりでビジネスをするかもしれない。単なる名刺交換ではなく、経験を共有した仲間なので思い入れも深い。なので、今せっせとメールを書いているところ。あと、いつも思うのだけども、facebookと旅行の組み合わせはかなり良い。私は旅行に行くためにfacebookを使っているんじゃないか、と思うくらい。最近ログインしていないけど、次ログインしたときに、今回会った人をどんどんfriendにリクエストしていく予定。

2010/01/01

たまには旅行の話でも

最近めっきり旅行から離れてしまっていたのですが、久しぶりに12月23日から12月31日まで海外に出ていました。8日という短い期間なので、あまりチャレンジはできないと思ったので、ハズレのないところに行ってしまいました。

というわけで困ったときは、私のホームグラウンドのリペ島。ここはたぶん合計では1ヶ月くらいいたことがあるくらい好きな島。毎回、来る度に開発が進んでいるのを感じて、うれしいやら寂しいやら。。。まだツアーなどではこの島に来ることはないし(たぶん)、ATMや銀行がないので(余分にチャージされることを許容できるのならばクレジットからキャッシュ化できる)、全部旅行会社に任せてしまう人には敷居の高い島です。おかげで行く直前でも泊まる場所が取れました(シミランツアーや、ピピ島は無理だった)。また、同じフェリーで訪れていたマレーシア人の人もいきなり宿に行って宿泊先をゲットしていましたよ。こういうノリの方が好きなんだけど、8日しかないとさすがにその手のチャレンジャーは無理っす。最近ストレスが溜まりすぎて、確実にリラックスしたかったので、チャレンジは次の余裕のあるときにとっておくですよ。

実際にはリペ島に行く方法は簡単で、ランタ島とかからフェリーで来てもいいし、パクバラからでもいい。私は面倒なので、ランカウイまで飛行機で飛んで、そこからフェリー。行きは一泊ランカウイで宿泊しなければいけなかったのだけど、泊まるだけだったのでgeoparkのドミトリーで泊まりました。びっくりしたのが、23日にそこに泊まったのは、私一人だけ。帰りは、スムーズに行けて、16時くらいのフェリーに乗って、ランカウイに到着して、そのまま飛行機で、クアラルンプール経由で帰国。

簡単すぎる。。。手ぶらで行ってもいいくらい。実際、今回持っていったものは、Tシャツ2枚と海パン2枚、半ズボン1枚、下着4枚。まぁ、ウェットスーツやら水中カメラの機材やら、パソコンやらで思ったよりも物が増えてしまったのだけど、今回はダイビングのみがメインだったので、これだけでちょうど良かったと思います。

日本を出るときが寒いので、ユニクロのヒートテックのロングTシャツ、カシミアのセーター。ジーンズ、靴くらいで、ジーンズ以外はかさばらなく軽いものだけ。もちろんこれらは行きと帰りしか着ないですね。リペ島に着いたら、だいたい朝から3時くらいまでずっと海に出ているし、それからもビーチでゴロゴロしたり、マッサージをしてもらったり、バーに飲みに行くくらいだから、太陽が強すぎるときだけTシャツを着るけど、一日の半分以上は上半身裸、裸足で生活ができるんですね。久しぶりにだいたい毎日飲み歩いていました。毎晩ご飯は、豪勢に600バーツくらい払って、ロブスターやらタイガープローンやらをガツガツ食べたった。30歳間近なんだけども、こんな感じで旅行できるなんて、まだまだ若いなぁー。

写真はビーチと猫の写真。動物には狂牛病等の危険性があるので、基本的に触らない方がいいですが、まぁ神経質になるのもどうかと。しかし、初めてwordpressのgalleryという機能を使ったけど、使い慣れていないせいか、イマイチですね。

一応、一つだけ気を付けないといけないのは、キャッシュをどうするか、ということですね。日本でタイバーツにしていってもいいのだけども、レートが悪いので(3.1くらい)、私はマレーシアで円を9万円くらい全部バーツに交換しました。レートは、3.3から3.4だったかな。一度リンギットに変えてからバーツに変えるので、レートは少し悪くなってしまうのだけども、リペ島に直接行くので、タイの銀行に行けないし、日本よりレートがいいので、こっちでだいたい変えます。アメリカとかだったらほとんどクレジットでやってしまうけど、この手の旅行はキャッシュの方がいいと思います。

えと、今回の出費は、だいたい20万円くらいかな。安い安い。
飛行機:90000円(年末だというのと、直前に取ったというのでちょっと高い)
フェリー:7000円
空港迎費:10000円(関空シャトル往復と、ランカウイの空港往復)
宿泊費:15000円(8泊)
ダイビング:45000円(13ダイブ)
飲食費:25000円(晩飯と酒代。昼はダイビングのお金に含まれているので。うーん。豪遊しすぎw)
その他;10000円(なんかしらんけど使ったお金。おみやげとかインターネットとかマッサージとか)

ところで、今回の旅行関係のポストはあと2つ書く予定です。

1つは、「トラベリングはネットワーキング」
そしてもう1つは、「ダイビング in リペ島」

2009/12/01

symfonyの中級者向けチュートリアルができましたね

それと、symfony1.4系(LTS版)が出ましたね。今から新しいプロジェクトをする人は、1.4系で作ってください。そして、リリースに合わせて中級者向けチュートリアルが出ましたよ。

More with symfony

さてさて、今回のチュートリアルで関わった著者、翻訳者、査読者の皆さん、お疲れさまでした。Fabienも書いていることなのですが、この本ってすごいんですね。中身ももちろんですが、Fabienが9月に思い立った「中級者向けの本を書く」ということを、12月1日までに実現してしまうのですから。しかも、5つの言語での翻訳版も同時に。

翻訳版も同時ということは、翻訳する時間も用意しておかないといけないのですね。なんというアジャイルなコミュニティなんだろう、と思いましたよ。日本語翻訳チームも同様で、symfonyをバリバリ使っているユーザの方が参加して、一気に進めることができました。本当にphp conferenceで会った人たちと、その場で話を始めたんですね。そして、その後にakkyが企画したsymfony meetupで一気に翻訳チームが結成されたのですね。やっぱり勢いだよ。勢い。

また、Fabienの人柄にも注目ですね。Fabienが「中級者向けの本をコミュニティベースで書きたい」と言ったことに対して、それに貢献をしてくれる人たち。著者、翻訳者、査読者の人が喜んでこの一大プロジェクトを成し遂げました。symfonyを本当に好きじゃないとこれはできないです。やっぱり愛だよ。愛。

そして、今回のチュートリアルを書いているメンバーは、バリバリsymfonyを使っている人たちで、言ってみれば、symfony userのオールスターなんですね。統一性が欠けるというものと、それぞれの著者の面白みが読めるということは、ある程度トレードオフだと思うのですが、後者の利点が大きすぎます。それに、全部独立していますし。ということで、統一性は個々の章で、ということでお願いします。まだ私も全部読んだわけではないのですが、超おすすめです。特に自分がメインで訳したコンフィグハンドラーの章は、お気に入りです。

そして、私個人としては、こうやって多くの人が関わって作る書籍やプロジェクトのダイナミズムというものを感じました。新しい本の書き方として、おもしろいプラクティスですよね。まぁ、その半面、私みたいな初めて訳をする人の文章があるなど、荒削りな面もありますが、そこは、適宜直していくということで、堪忍してください。

さて、中身ですが、symfonyを使っていろいろカスタマイズをしたい人向けなんですね。なので、かなり突っ込んだ内容が書かれています。全くもって、初心者向きではないです。でも、実際にアプリを書こうと思ったら、初心者用のチュートリアルだけでは無理がありますよね。そのための本です。

チュートリアルの中身などの技術的な興味もあるのですが、個人的には、コミュニティベースで、さらにオープンソースだと、こういう成果を上げることができるという経営的な興味もそそられる活動でした。無償が必ずしもいいとは思いませんし、フリーにしないといけないということではありません。ただ、このような組織形態だからこそ可能となったアジャイル性は、もっと注目されてもいいと思います。

本当にお疲れさまでした。私の初めての翻訳参加になりました。私の力もコミュニティに還元ができてうれしく思います。

関連する日本語翻訳者の方が以下のように喜びを書いています。

2009/11/23

admin generatorでrelationしているtableもfilterする

はじめに

filterって名前が紛らわしいですね。Form Filterなのか、Filter Chainなのかで全然違いますし。今回は、Form Filterに関してです。

Form Filter使っていますか?私はまったく使っていませんでしたw 使うのは、admin generatorで使うときのみでした。generator.ymlを編集して、filterさせたいフィールドだけ選択させたりくらいだけで、なかなか内部まで見れていませんでした。今でも、内部まで見ていないです。。。

しかし、ちょっと拡張してみると便利だということに気づきました。というわけで、これでもっとfilterを使おうかな、という気分になりました。今回は、propelベースで話をします。遊びプロジェクトは、doctrineで書いていたりするのですが、まだ、本番用でdoctrineを使えていないです。なんというか、doctrineで書くと、メソッドチェーンで書くことができるので、行数とか少なくて済むのですね。調子に乗ってactionに全部書いてしまって、modelにどうやって書こうか、悩んでしまうのですね。

問題

さて、デフォルトのadmin generatorの一覧画面には、右端に検索ボックスが出てきますよね。admin generatorの際に指定したmodelに関する検索です。なかなか便利だなー、とは思っていました。しかし、modelと一対なので、JOINしたりさせようとするとちょっと面倒なんですよね。一つのテーブルだけを検索できてもあまり使えないと思うので、JOINさせてfilterさせたいじゃないですか。

方法

いろんな方法が考えられるのですが、一番簡単な方法だと私が思っているのは、FormfFilterを拡張して、リレーションしたいテーブルのmodelに対応するFilterFormをmergeFormします。マジでmergeです。そして、そのFormFilterクラスのbuildCriteriaメソッドも拡張して、mergeしてあげたFilterFormでfilterしたいフィールドを追加してあげます。

準備1

つーか、簡単すぎてソースコードいらないね。。。それ用にアプリ書くの面倒ですし、プロダクトのコードそのまま持ってきたくないですし。。。と言いつつ、ここでテストアプリを作りながら、書いていくことにします。今回のは、symfony.1.2系で、propelです。すいません。
まず、schema.ymlを追加してみましょう。

propel:
  item:
    id:
    category_id:
    name:
      type: varchar(32)
      required: true
    description:
      type: longvarchar
      required: true
    created_at:
    updated_at:

  category:
    id:
    name:
      type: varchar(32)
      required: true
  is_special:
      type: boolean

data/fixtures/fixture.yml

Category:
<?php for ($i = 0; $i < 5; $i++): ?>
  category_<?php echo $i ?>:
    name: category_name_<?php echo $i ."\n" ?>
    is_special: <?php echo rand(0, 1) . "\n" ?>
<?php endfor; ?>

Item:
<?php for ($i = 0; $i < 30; $i++): ?>
  item_<?php echo $i ?>:
    name: item_name_<?php echo $i ."\n" ?>
    description: "test description"
    category_id: category_<?php echo rand(0, 2) ."\n" ?>
<?php endfor; ?>

そして、build-all-loadして、model, form, filterも作って、データベースにも突っ込んでおきましょう。ついでに、テストデータも入れておきましょう。なお、最初にdatabases.ymlなどを編集して、データベースは接続できるようにしておいてください。

$ ./symforny propel:build-all

そして、applicationを作ります。

$ ./symfony generate:app backend

で、moduleは、admin generatorで作ってしまいます。

$ ./symfony propel:generate-admin backend Item

生成されたモジュールを見ようとすると、「toStringを実装しろ」と怒られるので、実装してnameでも返してあげます。

class Category extends BaseCategory
{
  public function __toString()
  {
    return $this->name;
  }
}

before
さて、これで準備ができました。上の画像のようになりますね。fixtureでテストデータの作り方とかが、もしかしたら参考になる人がいるかもしれないですね。つまり、forしたり、randしている内容はチェックしてみてください。改行コードを入れないといけないのが、ちょっと嫌ですが。

準備2

と言いつつ、まだ準備です。次に、一覧として表示したいものを選びます。created_atとか表示しなくてもいいじゃないですか。それに、descriptionが長いときもあるので、表示させないように削ってしまいましょう。代わりにcategoryの情報が欲しいですね。is_specialというフィールドがcategoryにありますが、これを表示したいと思いますので、listに追加してあげましょう。

まず、そのためには、モデルのItemクラスに、getHogehogeを追加してあげましょう。今回は、getCategoryNameとgetCategoryIsSpecialメソッドを追加して、admin moduleからそのまま持ってくるようにしました。

class Item extends BaseItem
{
  public function getCategoryName()
  {
    if (!is_null($this->getCategory())) {
      return $this->getCategory()->getName();
    }
    return null;
  }
  public function getCategoryIsSpecial()
  {
    if (!is_null($this->getCategory())) {
      return $this->getCategory()->getIsSpecial();
    }
    return null;
  }
}

あとは、itemモジュールのgenerator.ymlを修正しましょう。

generator:
  class: sfPropelGenerator
  param:
    model_class:           Item
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          item
    with_propel_route:     1

    config:
      actions: ~
      fields:  ~
      list:
        peer_method: doSelectJoinCategory
        display: [ name, category_name, category_is_special, updated_at ]
      filter:  ~
      form:    ~
      edit:    ~
      new:     ~

これでとりあえず表示したいフィールドはできました。さらに、peer_methodを変更しないとクエリーを実行しすぎなので、joinするものに変えてあげました。
ようやく準備ができたかな。labelも変えてあげたいところですが、面倒なのでry

ソースコード

さて、ようやく今回のネタです。
lib/filter/ItemFormFilter.class.php

class ItemFormFilter extends BaseItemFormFilter
{
  public function configure()
  {
    $this->mergeForm(new CategoryFormFilter());
  }

  public function buildCriteria(array $values)
  {
    $criteria = parent::buildCriteria($values);
    $criteria->addJoin(ItemPeer::CATEGORY_ID, CategoryPeer::ID);
    if (isset($values['is_special'])) {
      $criteria->add(CategoryPeer::IS_SPECIAL, $values['is_special']);
    }
    return $criteria;
  }
}

そして、apps/backend/modules/item/config/generator.yml

generator:
  class: sfPropelGenerator
  param:
    model_class:           Item
    theme:                 admin
    non_verbose_templates: true
    with_show:             false
    singular:              ~
    plural:                ~
    route_prefix:          item
    with_propel_route:     1

    config:
      actions: ~
      fields:  ~
      list:
        peer_method: doSelectJoinCategory
        display: [ name, category_name, category_is_special, updated_at ]
      filter:
        display: [ name, category_id, is_special ]
      form:    ~
      edit:    ~
      new:     ~

なんてこったい。今回の本ネタが一番短いな。。。まぁ、いいや。
after

結果上のような見た目になりましたね。実際、SQL文を見ると以下のようになっていると思います。

SELECT item.ID, item.CATEGORY_ID, item.NAME, item.DESCRIPTION, item.CREATED_AT, item.UPDATED_AT, category.ID, category.NAME, category.IS_SPECIAL FROM `item` LEFT JOIN category ON (item.CATEGORY_ID=category.ID) WHERE category.IS_SPECIAL=1 AND item.CATEGORY_ID=category.ID LIMIT 20

これで、完成です。

解説

FormFilterを拡張して、追加したいフィールドを持っているFormFilterをmergeしてあげます。これで、admin generatorを指定してあげると、filterの場所には、全フィールドが出てきますね?

そこで、filterを拡張したいモジュールのgenerator.ymlのfiltersを修正します。ここで、relationしているtableでfilterしたフィールドも追加してしまいます。もちろん対応するフィールドがFormFilterにないとエラーが出ます。前にmergeをしていましたので、実際はフィールドがFormFIlterにあることになります。

これでできたらうれしいのですが、実際は、もう少しだけ修正する必要があります。つまり、検索条件を加えるといったことです。そこで、またFormFilterに戻って、buildCriteriaをオーバーライドして、Criteriaに指定したいテーブルをJOINして、ついでに、条件を加えてあげましょう。

まとめ

admin generatorでmodelをまたいでfilterを追加する方法を説明しました。

方法としては、admin generatorで使用するFilterFormを拡張して、relationしているtableもfilterの項目に追加するようにしました。実際は、FilterFormを拡張して、追加したいFilterFormをmergeし、Criteriaを作成する際に、mergeしたFilterFormの値も追加して作成するために、buildCriteriaメソッドをオーバーライドしてみました。admin generatorが自動生成するactionなどははいじっていません。

さて、今回の実装は、具体的な実装の話でした。つまり、アプリレベルの開発の話です。どこまで抽象化するというのは、いつも悩みの種で、抽象化しすぎると複雑になるし、実際いらないということもよくあります。YAGNIです。YAGNI。抽象的にやろうと思えば、もっと複雑になるし、その辺は、DbFilterプラグインとかがよろしくやってくれそうなことを前に読んだ気もするけど、どうなんでしょうね。時間があれば使ってみます。

まぁ、今回のような話は、それぞれのアプリの仕様によることが多いので、具体的に実装してしまいましょう。

今後のちょっとした発展系としては、ヘッダの行をクリックするとソートできるようにしたらいいですね。同じような要領なので、がんばればできるでしょう。こちらは、actionを上書きしないといけないのかもしれません。

うーん。一番時間がかかってしまうのが、準備段階というのも考えものですね。。。

2009/10/30

conditionalBasicSecurityFilterってのもアリかな

はじめに

conditionalシリーズをもう少し考えてみました。今回はキャッシュではなくて、securityFilterです。

symfonyでは、settings.ymlを用いて、ユーザがログインしていないときに呼び出すアクションを指定することができます。その際に使用するのが、login_moduleとlogin_actionです。

また、ログインはしているが、権限がない(Credentialがない)ときに呼び出すアクションも指定することができます。その際に使用するのが、secure_module, secure_actionです。この二つとsecurity.ymlを使うことによって、is_secureなアクションにログインしていないユーザがアクセスした際に指定したログイン画面を出すことが可能となります。

今回は、この2つを拡張してみることにします。つまり、ユーザがログインしていないときに呼び出すアクションをもう少し条件を付けて、特定のページには違うlogin_actionを渡すようなことです。

問題

確かにsymfonyではlogin_module、login_actionで指定することができるのいいのですが、もっと細かに指定したいときってどうしたらいいんだろうという話を、先日耳にしました。今まで私はそんな需要がなかったので、考えたことはなかったのですが、「私だったらこうやるかな」という方法をいきなり午前1時より作り始めてしまいましたので、その勢いでブログにも書いていますw
secureの方の動作は確認していないのでアレなのですが、loginの方は確認しましたので、これで対応することができたと思います。実装環境は相変わらずsymfony1.3-ALPHA2なのですが、たぶんsymfony1.2系でも動くと思います。

方法

login_module, login_actionなどの指定が実際どこで呼ばれているかを調査しますと、sfBasicSecurityFilter.class.phpの中で使われています。このsfBasicSecurityFilterは、filters.ymlに書かれているsecurityのデフォルトとして使われるfilterです。このsfBasicSecurityFilterを読んでみると、login_module, login_actionは固定値としてsfConfig::get(’sf_login_module’)のように使われていますので、簡単には変更することができないようになっています。というわけでこれではダメなので、考えてみました。

さて、symfonyのプラグインの中で一番ダウンロードが多いのがsfGuardPluginなのですが、そこでfilterのsecurityFilterを書き換えますよね?securityFilterは変更可能なのですね。というわけで、同様に、今回指定したconditionalBasicSecurityFilterというものをsecurityFilterとして使用できるように指定します。そして、一緒に渡すパラメータもfilters.ymlで指定してみます。yamlで指定する値が増えすぎてわからなくなってしまうかもしれないのですが、conditionalCacheFilterと同じようにpageごとに指定ができるようにしてみました。

ソースコード

/apps/frontend/config/filters.yml

rendering: ~
#security:  ~
security:
  class: conditionalBasicSecurityFilter
  param:
    pages:
      - { module: post, action: new, login: { module: user, action: loginNew } secure: { module: default, action: error403 } }
      - { module: post, action: edit, login: { module: user, action: loginEdit } secure: { module: default, action: error403 } }

# insert your own filters here

cache:     ~
execution: ~
~

/apps/frontend/lib/conditionalBasicSecurityFilter.class.php

class conditionalBasicSecurityFilter extends sfBasicSecurityFilter
{

  public function execute($filterChain)
  {
    if (!$pages = ($this->getParameter('pages', false))) {
      parent::execute($filterChain);
      return;
    }

    foreach ($pages as $page) {
      $module = isset($page['module']) ? $page['module'] : null;

      $action = isset($page['action']) ? $page['action'] : $this->context->getActionName();

      $login = isset($page['login']) ? $page['login'] : null;

      $secure = isset($page['secure']) ? $page['secure'] : null;

      // skip
      if ($module !== $this->context->getModuleName()) {
        continue;
      }

      // skip
      if ($action != $this->context->getActionName()) {
        continue;
      }

      if (isset($login) and !$this->context->getUser()->isAuthenticated()) {
        $loginModule = isset($login['module']) ? $login['module'] : null;
        $loginAction = isset($login['action']) ? $login['action'] : null;
        if (isset($loginModule) and isset($loginAction)) {
          $this->forwardToAction($loginModule, $loginAction);
        }
      }

      $credential = $this->getUserCredential();
      if (isset($secure) and !is_null($credential) and !$this->context->getUser()->hasCredential($credential)) {
        $secureModule = isset($secure['module']) ? $secure['module'] : null;
        $secureAction = isset($secure['action']) ? $secure['action'] : null;
        if (isset($secureModule) and isset($secureAction)) {
          $this->forwardToAction($secureModule, $secureAction);
        }
      }
    }

    parent::execute($filterChain);
  }

  protected function forwardToAction($module, $action)
  {
    $this->context->getController()->forward($module, $action);

    throw new sfStopException();
  }
} 

解説

filters.ymlでsecurityFilterで使用するクラスとしてconditionalBasicSecurityFilterを指定します。そして、その中で受け取るパラメータを同時に指定させます。今回はいいサンプルのアイデアがなかったのですが、postモジュールで、アクションがnewするときとeditするときに違うloginフォームを出したい、という要望があったとして考えてみました。つまり、newの時はuserモジュールのloginNewアクションを呼びたいとします。そして、editの時はuserモジュールのloginEditアクションを呼びたいとします。secureは、とりあえずこんな感じで指定できるかな、という感じで書いてみましたので、今回は説明しません。

conditionalBasicSecurityFilter.class.phpに関しては、前回書きましたconditionalCacheFilterと同じロジックでpagesの分だけループで回し、それがloginが必要なものであったり、credentialのチェックが必要なものであった際には、forwardをするというようにしました。これで、newのときには、loginNewにforwardされ、editのときにはloginEditにforwardされるようになります。

まとめ

今回は、そのsfBasicSecurityFilterを拡張してconditionalBasicSecurityFilterを作ってみました。そこでは、モジュールやアクションによって、login_module, login_action,security_module, security_actionを分けて指定ができるようにしました。
今回は、午前1時になっていきなり、思い立ったように書いてみましたので、適当なことを言っているかもしれないのですが、せっかくなので、ソースコードまで落として、さらにブログにも書いてみました。ログインの際のロジックを拡張したい際にはsfBasicSecurityFilterを拡張して対応するというのは、結構使えるのではないかな、と思います。
しかし、ここまで書いた後に、よく考えてみるとこれはアプリケーションを別にして作ればある程度は回避できる問題なんだろうなぁ、と少し思っています。それでも、上に書いたように分けたい場合はこんな感じで対応してみてはどうでしょうか?

そろそろ寝ないと。。。勢いで書いたので、間違っているところもあると思います。それは後ほど修正します。

2009/10/26

conditionalCacheFilterをさらに考える

はじめに

前回、conditionalCacheを説明した後に、さらにそれを使った事例を考えていたのですが、考えていたところ直したいところがいくつか出てきたので、さらにconditionalCacheを考えてみました。一つは、enabledオプションによって、キャッシュをクリアすることは必要ないと考えたことです。本来の動作としては、enabledがfalseであれば、キャッシュを使わずに、そのまま表示するという方法を取るのが一番でしょうね。これはviewCacheManagerのisCacheableメソッドを直せば可能となると思います。とりあえずその方法は後に説明するということで、今回はenabledオプションを消しました。
そして、今回は、セッション関係を持つuserクラスを使って、キャッシュをするか、どうかを追加してみることにしましたので、それを説明しようと思います。

問題

ユーザ認証しているかどうかだけではなくて、ユーザ認証して、かつ自分のコントロールできるページか否かというオプションがあったらウレシイですよね。つまり、そのユーザの投稿やプロフィールページであれば、「編集する」や「削除する」というアクションが表示されるのが普通でしょう。ユーザ認証をしていても、他のユーザの内容であれば、編集や削除ができたら困りますよね。ここでもconditionalCacheの出番が必要ではないか、と考えました。

方法

セッションを操るuserを使いましょう。filters.ymlで条件としてuser_conditionを追加してみます。そこで、user_conditionに、キャッシュ可能かどうかを条件判断させるメソッドを文字列で渡してみます。

ソースコード

/apps/frontend/config/filters.yml

rendering: ~
security:  ~

# insert your own filters here
conditionalCacheFilter:
  class: conditionalCacheFilter
  param:
    pages:
      - { module: post, action: index, format: [xml] }
#      - { module: post, action: index, format: [html], user_condition: usesCache }
      - { module: post, action: index, format: [html], user_condition: isNotAuthenticated, with_layout: true }

cache:     ~
execution: ~

/apps/frontend/lib/conditonalCacheFilter.class.php

class conditionalCacheFilter extends sfFilter
{
  protected
    $cacheManager    = null,
    $request         = null,
    $user            = null,
    $uri             = null,
    $defaultLifetime = null;

  public function initialize($context, $parameters = array())
  {
    parent::initialize($context, $parameters);

    $this->cacheManager    = $context->getViewCacheManager();
    $this->request         = $context->getRequest();
    $this->user            = $context->getUser();
    $this->uri             = $context->getRouting()->getCurrentInternalUri();

    $lifetime = (isset($this->cacheManager)) ? $this->cacheManager->getLifeTime($this->uri) : 0;
    $this->defaultLifetime = ($lifetime > 0) ? $lifetime : 86400;
  }

  public function execute($filterChain)
  {
    if ((!isset($this->cacheManager)) or !($this->getParameter('pages', false))) {
      $filterChain->execute();
      return;
    }

    foreach ($this->getParameter('pages') as $page) {

      $module = isset($page['module']) ? $page['module'] : null;

      $action = isset($page['action']) ? $page['action'] : $this->request->getParameter('action');

      $format = isset($page['format']) ? $page['format'] : array();
      if (!is_array($format)) {
        $format = array($format);
      }

      $user_condition = isset($page['user_condition']) ? $page['user_condition'] : null;

      // maybe better to throw exception?
      if (is_null($module)) {
        continue;
      }
      // skip
      if ($module !== $this->request->getParameter('module')) {
        continue;
      }
      // skip
      if (!empty($format) and !in_array($this->request->getParameter('sf_format', 'html'), $format)) {
        continue;
      }

      // skip
      if (isset($user_condition)) {
        $func = array($this->user, $user_condition);
        if (!(is_callable($func) and call_user_func($func, $this->request))) {
          continue;
        }
      }

      $options['lifeTime'] = isset($page['lifetime']) ? $page['lifetime'] : $this->defaultLifetime;
      // if the page is not cached already, you can set with_layout option
      // xxx symfony page cache cannot have with_layout and without_layout cache same time
      if (!$this->cacheManager->isCacheable($this->uri)) {
        $options['withLayout'] = isset($page['with_layout']) ? $page['with_layout'] : false;
      }

      //add
      $this->cacheManager->addCache($module, $action, $options);
    }

    // chain
    $filterChain->execute();
  }
}

/apps/frontend/lib/myUser.class.php

class myUser extends sfBasicSecurityUser
{
// snip some methods for user signin or signout methods.
//
  public function getUsername()
  {
    if ($this->isAuthenticated()) {
      return $this->getAttribute('username', '', 'test');
    }
    return null;
  }

  public function usesCache($request)
  {
    $username = $request->getParameter('username', false);
    return $this->getUsername() !== $username;
  }

  public function isNotAuthenticated()
  {
    return !$this->isAuthenticated();
  }
}

解説

今回は、userクラスで条件判断をつけるuser_conditionのキーをfilters.ymlで選択させるようにしてさらに柔軟にしていました。つまり、user_conditionに与えるメソッドによって、キャッシュするかどうかを変更することができます。ここでは、isNotAuthenticatedによって、認証されていないユーザにはキャッシュをするというように与えています。前回のis_authenticatedオプションではなく、直接userクラスに判断させるようにしてあります。user_conditionが指定していなければ、認証していようが、いまいが、キャッシュが有効になります。そして、認証している際にさらに条件を加えたい場合は、user_conditionでもう少し詳しい内容を実装したメソッドを指定してみましょう。

例えば、user_conditionにusesCacheを指定すれば、リクエストパラメターにusernameがあった際に、セッションに入っているusername(ログインした際に持っておく)と比較をし、同じものであれば(そのユーザに関するページ)、キャッシュをしない、ということも可能となります。

さて、with_layoutオプションですが、これは実は少し厄介なのです。現在のsymfonyでは、同じページのキャッシュを複数持つことができないのですね。つまり、with_layoutとwithout_layoutのキャッシュが同時に持てずに、切り分けることができないのです。実際は、キャッシュといっても、シリアライズされた内容をキャッシュディレクトリに格納しているのですが、with_layoutの方は、オブジェクトなのに対し、without_layoutの方はarrayでシリアライズされているので、ここで問題が起きてしまいます。これは、すでにcache.ymlでキャッシュを指定していた際のwith_layoutの値と違う値を指定するとエラーが起きてしまうので、このようにすでになければ指定可能だが、すでにあれば、with_layoutオプションは使えない、というようにしました。

まとめ

前回の内容からさらにconditionalCacheを考えて、userクラスのメソッドにそれを判断させるキーをfilters.ymlに追加しました。このことにより、認証済みでも、さらに自分に関するページに関してはキャッシュをしない、等の処理が可能となりました。つまり、認証していても他のユーザのページであれば、キャッシュを使えるということになります。

課題

キャッシュに関しては、まだ考えているところがあります。cache.ymlにて、enabledをtrueにしているが、conditionalCacheFilterに渡すenabledをfalseにした際に、キャッシュをクリアするのではなく、そのまま置いておいて、そのページはキャッシュを使わずに表示させたいと思います。また、パーシャルキャッシュについても考えた方が良いでしょう。前回と今回に関しましては、ページキャッシュを扱いましたが、パーシャルキャッシュもログイン状態によって切り分けたいときがあると思います。それに関しても考えていきたいと思います。

ちなみに、ここで私が載せている内容は、正しい唯一の方法ではなく、「現在の私だったら、こうやって実装するかな。」という内容です。なので、時間が経つに連れて、より良い方法が見つかった際には、そちらに移行すると思います。その際には、また、ブログに書くでしょう。

2009/10/24

conditionalCacheFilterをより一般的に考えてみる

はじめに

symfonyでアプリケーションを作ろうとする際にキャッシュを使っていますか?キャッシュと一言に言ってもいろんなレイヤーでキャッシュを実現する方法があるので、なんとも答えに困る質問ですね。しかし、webアプリを作る際に言われているキャッシュであれば、

  1. apcなどでphpスクリプトをキャッシュ
  2. ページ出力内容を静的ファイルもしくは、メモリ上にキャッシュ
  3. クライアントのブラウザ側に残してもらうキャッシュ

等が一般的に考えられるのではないかと思います。おそらく、opcodeに関しては、多くの人はapcなどを使うとしても、チューニングの設定等を意識する必要はあると思いますが、実際に中でどう動いているのかを考えることが少ないと思います。「普通のやつらの下を行く」ことを目的とするユーザ(下のレイヤーで勝負するという意味)はそちらを調べてもらうとして、Webアプリ作成屋としては一番腕の見せ所は2番目ではないかと思います。つまり、どの出力をキャッシュ化して、どの出力をキャッシュ化しない、といったようなことです。そして、キャッシュを絶妙のタイミングでクリアさせることです。これによって、データベースへの接続の際のクエリーが激減させることができますので、大幅なパフォーマンスの改善が望めるからです。

今回のポストでは、この2番目の内容をもう少し詳細まで扱おうと思います。

問題

symfonyでは、cacheをサポートしており、cache.ymlで指定させることによって、次に上げるようなページキャッシュが可能となっています。

  1. layoutを含むキャッシュなのか、そうでないのか、という指定
  2. キャッシュの有効期間の指定
  3. partial, component等のページ全体ではなくて、一部のみのキャッシュの指定

これでだいたいの場合はキャッシュの使用として解決ができると思います。しかし実際にアプリケーションを作成していると、もう少し詳細なキャッシュ機構があったらいいな、というときがあります。たとえば、Jobeetのfeedの章であったようなことです。つまり、同じactionを使用するが、sf_formatの値によって、出力をatomにするのか、htmlにするのか、切り分ける場合です。atomの出力とhtmlの出力で同じキャッシュの指定をしたいですか?もし、同じでいいのであればいいのですが、私は分けたいと思いました。htmlの方ではログインしているかどうかも関係してくるのですが、atomの場合は、ログインしていようがいまいが同じ出力を返すからです。

同様に、ログインしているかどうかによって切り分けるページもあるでしょう。例えば、ログインしていると、ヘッダに、「hogehogeさん」って名前が出てくるようなものです。見せるコンテンツはログインしていようがいまいが同じだけども、ログインしていない際にはページ全体をキャッシュさせたいと思いますし、ログインしていたら、キャッシュ化させる内容は分けたいと思います。現在は、同じページでwith_layoutとwithout_layoutで切り分けることはできません。なので、ログインしてない際にはキャッシュを有効にさせ、ログインしている際にはページを無効にさせるという方法を取ります。

これらの内容は、cache.ymlだけでは実現が不可能ですので、他の方法が必要となります。

方法

symfonyにおいて、キャッシュを使う方法はいくつか用意されています。一番大きな方法としては、cache.ymlで指定することです。次にviewの中でキャッシュを使用するかどうかを指定することでしょう。さらにcacheManagerを通してキャッシュのロジックを変更することができます。キャッシュを自分の好きなタイミングでクリアしたり、有効にしたりすることができます。このcacheManagerを通してキャッシュを使用するには、filterが一番相性がいいようです。クリアするのは、データの変更後などになりますので、actionに書くことになってしまいますが、requestによってキャッシュを使用するしないを変更するにはfilterを使うことになります。The Definitive Guilde to symfonyのcacheの章のconditionalCacheFilterのような方法です。今回のポストでは、このconditionalCacheFilterをもう少し拡張してみることにします。

ソースコード

今回使用したsymfonyのバージョンは、1.3.0-ALPHA2です。1.2系は同じようにできると思いますが、1.0系はおそらく実装が異なると思います。

/apps/your_apps/config/filters.yml

rendering: ~
security:  ~

# insert your own filters here
conditionalCacheFilter:
  class: conditionalCacheFilter
  param:
    pages:
#      - { module: post, action: index, format: [atom,xml] }
#      - { module: post, action: index, format: [xml], enabled: false }
      - { module: post, format: [xml], enabled: true , is_authenticated: false }

cache:     ~
execution: ~

/apps/your_app/lib/conditionalCacheFilter.class.php

class conditionalCacheFilter extends sfFilter
{
  protected
    $cacheManager    = null,
    $request         = null,
    $user            = null,
    $uri             = null,
    $defaultLifetime = null;

  public function initialize($context, $parameters = array())
  {
    parent::initialize($context, $parameters);

    $this->cacheManager    = $context->getViewCacheManager();
    $this->request         = $context->getRequest();
    $this->user            = $context->getUser();
    $this->uri             = $context->getRouting()->getCurrentInternalUri();

    $lifetime = (isset($this->cacheManager)) ? $this->cacheManager->getLifeTime($this->uri) : 0;
    $this->defaultLifetime = ($lifetime > 0) ? $lifetime : 86400;
  }

  public function execute($filterChain)
  {
    if ((!isset($this->cacheManager)) or !($this->getParameter('pages', false))) {
      $filterChain->execute();
      return;
    }

    foreach ($this->getParameter('pages') as $page) {

      $module = isset($page['module']) ? $page['module'] : null;

      $action = isset($page['action']) ? $page['action'] : $this->request->getParameter('action');

      $format = isset($page['format']) ? $page['format'] : array();
      if (!is_array($format)) {
        $format = array($format);
      }

      $is_authenticated = isset($page['is_authenticated']) ? $page['is_authenticated'] : null;

      // maybe better to throw exception?
      if (is_null($module)) {
        continue;
      }
      // skip
      if ($module !== $this->request->getParameter('module')) {
        continue;
      }
      // skip
      if (!empty($format) and !in_array($this->request->getParameter('sf_format', 'html'), $format)) {
        continue;
      }

      if (is_null($is_authenticated) or ($is_authenticated === $this->user->isAuthenticated())) {
        // remove
        if (isset($page['enabled']) and $page['enabled'] === false) {
          $this->cacheManager->remove($this->uri);
          continue;
        }

        //add
        $this->cacheManager->addCache($module, $action, array(
          'lifeTime' => isset($page['lifetime']) ? $page['lifetime'] : $this->defaultLifetime
        ));
      }
    }

    $filterChain->execute();
  }
}

解説

filters.ymlにて、cache filterが通る前にconditionalCacheFilterを呼びます。filterに渡すことのできるパラメターとしては、The Definitive Guilde to symfonyのcacheの章にあるようにpagesとします。そして、そこに配列として指定したいモジュール名とアクション(ない場合はそのモジュールに属すすべてのアクション)format, enabled, is_authenticated, lifetimeなどのパラメターを指定できるようにしました。is_authenticatedに関しては3つのパターンがありますね。つまり、認証済みの場合、非認証の場合、認証しているかどうかは関係のない場合です。

また、symfony1.3からは、yamlのバージョンの扱いが1.2になりますので、on/off, yes/noは使えなくなりますので、気をつけてください。true/falseで切り分けるようにしましょう。

そして、conditionalCacheFilter.class.phpにおいては、それぞれのパラメターによってキャッシュの使用について分けるようにしました。cacheManagerに関しましては、キャッシュを有効化していないとnullを持ちますので、そのチェックが必要です。デフォルトではenableとなっているページもこのfilters.ymlでenabledをfalseに指定することにより、毎回キャッシュをクリアするようにできますので、より柔軟にキャッシュの変更ができるようになっています。

まとめ

今回のポストでは、symfonyのキャッシュフレームワークを使用する際に役に立つtipsとしてconditionalCacheFilterを一般的な使用方法で使えるように拡張しました。この拡張によって、次の2つが可能となりました。

  1. sf_formatによってキャッシュをするかどうかを切り分ける
  2. ユーザが認証済みかどうかでキャッシュをするかどうかを切り分ける

sf_formatによってキャッシュをするかどうかを切り分けることによって、例えば、feedのようなxml形式のものはある程度キャッシュをしつつ、アプリケーションとしてwebで表示されるviewであるhtml形式のものはキャッシュを使用しなくするということが可能となります。xml以外にも考えられるものはjsonなどのAPI提供です。これらは特別な理由がない限りキャッシュ化されたものを返すのがいいと考えています。

また、ユーザが認証済みかどうかでキャッシュをするかどうかを切り分けることができることにより、認証をしていないユーザに返すレスポンスに速くなることでしょう。これはWebサイトに訪問してくれたユーザのみならず、Googleなどの検索エンジンのクローラーにも言えることです。クローラーによってアクセスさせるものに毎回DBアクセスをさせるのではなく、キャッシュを返して、定期的にそのキャッシュを更新させることによって、負荷を下げましょう。

拡張しようと思えば、その余地を残してくれているのがsymfonyフレームワークです。今回のconditonalCacheFilterは、特定のアプリケーションに依存するというよりは、より一般的なアプリケーションに必要な機能だと考えていますので、plugin化して、置いておくのもいいかもしれません。もちろんバグがあることもあると思いますので、それに関しては教えてください。

やっぱり日本語でブログを書く

最近ブログを英語で書いていたのだけども、やっぱりこのブログには日本語で書くことにした。そして英語の方は、以前から眠らせているドメインに書くことにする。

しかし、ブログって難しいのぅ。他人が書いてないことを書きたいんだけども、それをすると調べるのに時間がかかってしまうし、そして、自分で実践するのにさらに時間がかかってしまう。さらにさらに、日本語が下手なので、ここでも時間がかかる。。。うーん。><

2009/09/01

I’vRead launched

Akky’s yonda4 has a sensational debut last couple of months in Japan. “Yonda” means “I have read” in Japanese. If you are not familiar with Japanese, you have no idea why there is “4″, but in Japanese “4″ can be pronounced as “yo”, so literally, it is “yondayo”. Actually “yonda” and “yondayo” is a quite same meaning, but latter is more friendly and it has meaning to tell people. You are not saying to yourself, but to tell somebody.

Since yonda4 has launched successfully, akky was thinking for English version of yonda4. Yep, it is ivread, and has just launched a few days ago! Actually, I also helped this project. :D Since we launched narabe / narabete 2 years ago, we have developed web applications with symfony. Yes, both yonda4 and ivread are developed with symfony framework.

So, akky asked me for help to i18n of yonda4 last week, and setting up the environment. At that time, I was working on his other projects, but I thought ivread is a good challenge, and I also liked his idea. Since I’m using symfony in last two years, it didn’t take any time to join this project. I was just ready. :) Again, I’m happy with symfony for easy development and its i18n support.

So, I helped set up product environment in ec2, and automatically backup feature for s3 storage. Yep, we are using ec2 now to start new service. I like these services, it is easy to set up environment, and once you set it up, you can store private AMI to your s3 storage. So, we use s3 for back up for both AMI and database dump. Some might says ec2 is too expensive, but you can pay per hour. I hope it won’t happen, but if service you are running on ec2 did not work well, you can always stop the server. Just after you stopped, you are not going to be changed.

We’re still working on ivread to parse tweet. There are some rule to follow in usage page, but it seems that not many people read it, and tweet whatever they like. So, we will try to cover as much as we can.

BTW, in this weekend, symfony’s core developer, Fabien is coming to Tokyo!!!! I live at Kyoto where is a bit far from Tokyo. BUT, I’m going to see him in Tokyo this weekend. :D

All right, tech part is done. The reason I like about ivread is not only symfony base application and I could use ec2. I liked akky’s idea very much. Both ivread and yonda4 provide users the way to affiliate their books. I do not like ads most of time, because it does not relate with the contents in many cases. I like ads when you really recommend it. At least for me, I do not want any ads for products or services I have never used. If I have used it, and liked it, then I would be happy to add ads of them. So, ivread is real users’ voice. You can add a short comment with tweeting to ivread as well.

As I mentioned above, and Cool Cat Teacher Blog mentioned, if you tweet their amazon affiliate id to ivread, you have chances to get some cash back. I think this motivates people to tweets to ivread. Akky also said Twitter can be platform for our life. I strongly agree with his idea.

Well, we are still on the way to make ivread better, but I believe ivread has a bright future! :)

2009/07/22

still working on routing cache issue

I’m getting know routing cache problem now, but still struggling how to fix this. :( In my last post, I introduced lazy_routes_deserialize is the way to avoid this problem. Well, I believe some is true, but not everything. After I fixed my factories.yml configuration for sfPatterRouting, it seems that the performance of the application got a lot better, and I no longer across the error messages which I was frustrated very much. :)

However, I ran the application for a couple days, and found different problem. As the symfony blog mentioned , it generates too many routing cache files!!!!! It was more than 600000. 600000 in one directory is not very healthy. So, I changed my idea to use lazy_routes_deserialize. Instead, I decided to use sfAPCCache. The application was using APC anyway, so I thought put every routing cache in it. It worked fine, but I came across the exact same problem again, which is the size of routing cache gets huge! It ate almost 150M in 3 days, and it seems it is going to consume more in the near future. For now, I just run apache reload task one in a day, so it is working great. However, I don’t like the idea I’m using now. I have to find other strategies for this routing cache.

I think the base of this problem is my routing configuration. It gets so huge, and I need to find out better way to avoid it. The more specific URL you have, the bigger your routing cache gets. I understand it, but it seems the number it grows exponentially! :( It is not fair!

Is there any way to optimize my routing cache issue?

O.K, I found an answer. Do not use routing cache. Set sfNoCache instead.

Bloglines feedburner