復習
私たちはXMLのAPIを通してaskeetのサービスを利用できるようにする方法を見ました。今日のプログラムはフィルタに焦点を当てます。askeetでサブドメインを作成することでこれらの利用方法を説明します。たとえば、'php.askeet.com'はPHPとタグづけされた質問だけを表示し、このドメインに投稿された質問はすべて'php'とタグづけされます。この新しい機能を'askeet universe'(ユニバース)と呼ぶことにして、すぐに開発を始めましょう。
設定可能な機能
最初に、この新しい機能はオプションでなければなりません。askeetはどんな構成でもインストールできるソフトウェアのピースであることが前提としています。たとえば企業のイントラネットではサブドメインが許可されないかもしれません。
ですのでアプリケーションの設定に新しいパラメータを追加します。universe機能を有効にするには、on
に設定しなければなりません。カスタムパラメータを追加するには、askeet/apps/frontend/confg/app.yml
ファイルを開き、次のコードを追加します:
all: .global: universe: on
このパラメータはアプリケーションのすべてのアクションで利用可能です。値を得るには、sfConfig::get('app_universe')
を呼び出します。
symfony bookの設定の章でカスタムの設定方法を調べることができます。
フィルタを作成する
フィルタはすべてのアクションの前に実行されるコードのピースです。ドメインにおいてタグ名を検索するときに、すべてのアクションより先にホスト名を検査するために必要なものです。
askeet/apps/frontend/config/filters.yml
ファイルにおいて、実行される特別な設定ファイルで宣言されなければなりません。アプリケーションを初期化するとき、このファイルはデフォルトで作成され内容は空です。このファイルを開き次のコードを追加します:
myTagFilter: class: myTagFilter
新しいmyTagFilter
フィルタを宣言します。全体のfrontend
のアプリケーションを利用可能にするためにmyTagFilter.class.php
クラスファイルを作成します:
<?php class myTagFilter extends sfFilter { public function execute ($filterChain) { // 一度だけこのフィルタリングを行う if (sfConfig::get('app_universe') && $this->isFirstCall()) { // 何かを行う } // 次のフィルタを実行する $filterChain->execute(); } } ?>
これはフィルタの一般的な構造です。app_universe
パラメータがon
に設定されていない場合、フィルタは実行されません。私たちが1つのリクエストごとに1回だけフィルタが実行されることを望んでいるので(forwardsを使用しているので、一つのリクエストごとに複数のアクションかもしれませんが)、->isFirstCall()
メソッドをチェックします。与えられたパラメータで実行された最初の時のみ、true
です。
filterChain
オブジェクトについて一言です: リクエスト(設定、フロントコントローラ、アクション、ビュー)実行のすべてのステップはフィルタのチェーンです。フィルタチェーンの他のステップの実行を停止させる必要はありません。カスタムフィルタが常に$filterChain->execute()
によって終了するのはそういうわけです。自前の方法でフィルタパラメータを処理する必要がある場合、カスタムフィルタでオーバーライドできます。
note
sfFilter
クラスにはフィルタオブジェクトが作成されたときに実行されるinitialize()
メソッドがあります。自前の方法でフィルタパラメータを処理する必要がある場合、カスタムフィルタでオーバーライドできます。
ドメイン名からパーマネントタグを取得する
タグを保有するサブドメイン名を含むかチェックをするためにホストネームを検査する方法を考えます。'www'や'askeet'のようなタグは無視されなければなりません。加えて、無視するサブドメインのルールを修正できるようにすることを考えます。たとえば、'www'や'www2'といった代替のドメイン名でロードバランサー技術を使うかです。これがfilters.yml
設定ファイルのパラメータで無視するuniverseのルールを設定することを決めた理由です:
myTagFilter: class: myTagFilter param: host_exclude_regex: /^(www|askeet)/
フィルタのexecute()
アクションの内容を見る段階です(// do things
コメントを置き換えます):
// ホスト名の中にタグが存在するか? $hostname = $this->getContext()->getRequest()->getHost(); if (!preg_match($this->getParameter('host_exclude_regex'), $hostname) && $pos = strpos($hostname, '.')) { $tag = Tag::normalize(substr($hostname, 0, $pos)); // パーマネントタグのカスタム設定パラメータを追加する sfConfig::set('app_permanent_tag', $tag); // カスタムのスタイルシートを追加する $this->getContext()->getResponse()->addStylesheet($tag); }
フィルタはURIにおいておそらく継続的なタグを探します。1つが見つかると、カスタムパラメータとして追加され、ビューにカスタムスタイルシートが追加されます。では、例です:
// PHP universeを表示するために下記のURIを呼び出す http://php.askeet.com // 定数を作る sfConfig::set('app_permanent_tag', 'php'); // ビューでカスタムのスタイルシートをインクルードする <link rel="stylesheet" type="text/css" media="screen" href="http://www.symfony-project.org/css/php.css" />
note
カスタムフィルタの実行はビューの設定解析よりもフィルタチェインでかなり初期に行われますので、出力結果のHTMLファイルの中で他のスタイルシートの前でカスタムスタイルシートが出現します。カスタムスタイルシートにおいてメインのaskeetサイトのスタイル設定をオーバーライドしなければならないとき、これらの設定は!important
を宣言する必要があります。
モデルの修正
パーマーネントタグを考慮するアクションとモデルメソッドを修正する必要があります。モデルロジックをモデルレイヤー内部にとどめたいのとリファクタリングが本当に必要になるので、アクションからPropelリクエストを取得するためのパーマネントタグの修正を利用します。askeetのtracにおいて今日のリリースの修正リストを見ましたら、いくつかの新しいモデルのメソッドが作成され、アクションがそれら自身によるdoSelect()
を実行する代わりにこれらのメソッドを呼び出します:
Answer->getRecent() Question->getPopularAnswers() QuestionPeer::getPopular() QuestionPeer::getRecent() QuestionTagPeer::getForUserLike()
パーマネントタグに対応するフィルタリスト
質問リスト、タグ、回答がaskeet universeに表示されたとき、すべてのリクエストは新しいサーチパラメータを考慮に入れなければなりません。symfonyにおいて、検索パラメータはCriteria
オブジェクトの->add()
メソッドの呼び出しです。
QuestionPeer
とAnswerPeer
クラスに次のメソッドを追加します:
private static function addPermanentTagToCriteria($criteria) { if (sfConfig::get('app_permanent_tag')) { $criteria->addJoin(self::ID, QuestionTagPeer::QUESTION_ID, Criteria::LEFT_JOIN); $criteria->add(QuestionTagPeer::NORMALIZED_TAG, sfConfig::get('app_permanent_tag')); $criteria->setDistinct(); } return $criteria; }
universeでフィルタリングされなければならないリストを返すすべてのモデルメソッドを探す必要があります。そして次の行でCriteria
の定義を追加します:
$c = self::addPermanentTagToCriteria($c);
たとえば、QuestionPeer::getHomepagePager()
はこのように修正されます:
public static function getHomepagePager($page) { $pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max')); $c = new Criteria(); $c->addDescendingOrderByColumn(self::INTERESTED_USERS); // 下記の行を追加する $c = self::addPermanentTagToCriteria($c); $pager->setCriteria($c); $pager->setPage($page); $pager->setPeerMethod('doSelectJoinUser'); $pager->init(); return $pager; }
同じ修正は次のメソッドにおいて、数回ほど繰り返されなければなりません:
QuestionPeer::getHomepagePager() QuestionPeer::getPopular() QuestionPeer::getPopular() QuestionPeer::getRecentPager() QuestionPeer::getRecent() AnswerPeer::getPager() AnswerPeer::getRecentPager() AnswerPeer::getRecent()
複雑なリクエストのためにCriterita
オブジェクトを使用せず、私たちはパーマメントタグをSQLコードのWHERE
ステートメントとして追加する必要があります。どのように行ったのかaskeetのtracもしくはSVNリポジトリで、QuesitonTagPeer::getPopularTags()
とQuestionTagPeer::getPopularTagsFor()
メソッドを確認してください。
質問もしくはユーザーのためのタグのリスト
すべての'PHP'のuniverseの質問は'php'とタグづけされています。しかし、ユーザーが 'PHP 'universeで質問をブラウジングしている場合、'php'タグはタグリストに表示されてはなりません。universeで質問またはユーザーのためにタグリストを出力しているとき、パーマネントタグを削除しなければなりません。ループ、たとえば、Question->getTags()
メソッドなどにおける回避によって簡単に実現できます:
public function getTags() { $c = new Criteria(); $c->add(QuestionTagPeer::QUESTION_ID, $this->getId()); $c->addGroupByColumn(QuestionTagPeer::NORMALIZED_TAG); $c->setDistinct(); $c->addAscendingOrderByColumn(QuestionTagPeer::NORMALIZED_TAG); $tags = array(); foreach (QuestionTagPeer::doSelect($c) as $tag) { if (sfConfig::get('app_permanent_tag') == $tag) { continue; } $tags[] = $tag->getNormalizedTag(); } return $tags; }
同じ種類のテクニックは次のメソッドで使用されます:
Question->getTags() Question->getPopularTags() User->getTagsFor() User->getPopularTags()
新しい質問にパーマネントタグを付け足す
質問がaskeetのuniverseに作成されたとき、ユーザーによって入力されたタグに加え、パーマネントタグをタグづけしなければなりません。復習として、quesiton/add
メソッドにおいて、Question->addTagsForUser()
メソッドが呼び出されます:
$question->addTagsForUser($this->getRequestParameter('tag'), $sf_user->getId());
ユーザーによって入力されたタグを含むtag
リクエストパラメータは、空白によって分割されます(私たちはこれを'フレーズ(phrase)'と呼んでいます)。addTagsForUser
メソッドの最初の行にphraseのパーマネントタグを追加します:
public function addTagsForUser($phrase, $userId) { // フレーズを個別のタグに分割する $tags = Tag::splitPhrase($phrase.(sfConfig::get('app_permanent_tag') ? ' '.sfConfig::get('app_permanent_tag') : '')); // タグを追加する foreach ($tags as $tag) { $questionTag = new QuestionTag(); $questionTag->setQuestionId($this->getId()); $questionTag->setUserId($userId); $questionTag->setTag($tag); $questionTag->save(); } }
これでお終いです: まだユーザーがパーマネントタグに含まれない場合、新しい質問に渡されるタグリストに追加されます。
サーバーの構成
新しいドメインを利用可能にするには、Webサーバーの設定を修正する必要があります。
ローカルにおいて、すなわち、askeetサイトのDNSをコントロールしない場合、それぞれの新しいuniverseのために新しいホストを追加します(Linuxの場合は/etc/hosts
ファイル、Windowsの場合はC:\WINDOWS\system32\drivers\etc\hosts
ファイルです):
127.0.0.1 php.askeet 127.0.0.1 senseoflife.askeet 127.0.0.1 women.askeet
note
この作業を行うには管理者権限が必要です。
すべてのケースにおいて、バーチャルホストの設定(Apacheのhttpd.conf
ファイル)にサーバーのエイリアスを追加する必要があります:
<VirtualHost *:80> ServerName askeet ServerAlias *.askeet DocumentRoot "/home/sfprojects/askeet/web" DirectoryIndex index.php Alias /sf /usr/local/lib/php/data/symfony/web/sf <Directory "/home/sfprojects/askeet/web"> AllowOverride All </Directory> </VirtualHost>
Webサーバーを再起動した後で、たとえば、リクエストによってuniverseの1つをテストできます:
http://php.askeet/
それではまた明日
フィルタは強力で、あらゆることのために使われます。タグによって特定のテーマに沿ってコンテンツをカスタマイズできるようになります。タグとフィルタの組み合わせはaskeetをいくつかのuniverseに分割するための手助けになります。特化したaskeetサイト(music.askeet.com、programming.askeet.comやdoityourself.askeet.comを考えてください)の実現方法は無数にあります。これらすべてのサイトは異なるスキン(外観)にすることが可能で、特化したサイトのコンテンツは依然としてグローバルなaskeetサイトでも見ることができるので、askeetはコミュニティベースのWebアプリケーションを上回ります。universe機能はコミュニティの構築をするために最小限の十分な規模を持ち、そしてグローバルサイトはあらゆる種類の質問への回答を探すのに最良の場所となります。
明日は、パフォーマンスに焦点を当てて、HTMLキャッシュが複雑なページの配信時間を短くする方法を理解します。秘密の機能は3日以内にやってきます。ベストなアイディアを投票する時間はまだありますよ。askeetのフォーラムに訪問して、askeetのWebサイトがオンライン上でどのように動作するのか見ることができます。
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.