add to hatena hatena.comment (0) add to del.icio.us (0) add to livedoor.clip (0) add to Yahoo!Bookmark (0) Total: 0

DAY9: ローカルの改善

これまでのおさらい

symfonyで簡単なよくあるAJAXを実装するための関数や機能が用意されているのがわかりました。
また、外部ライブラリがコアのプラグインとして提供されるようになったということがわかりました。
9日目はテキストエリアのMarkdown対応と、ルーティングの変更です。

Markdown対応

MarkdownとはHTMLをwiki構文の1種で、HTMLをタグを書かなくても簡単に記述できる構文です。
詳しいことは本家チュートリアルとMarkdownのページを見てもらうとしてここは実装を行っていきます。
PHP版のMarkdownをダウンロードしてきて./lib/venderに配置しておきます。

./lib/vender/PHP Markdown 1.0.1m

C:
  1. ./lib/vender/
  2.  └PHP Markdown 1.0.1m/
  3.      License.text
  4.      PHP Markdown Readme.text
  5.      markdown.php

あとはこのmarkdown.phpを読み込めばいいのですが、symfonyのインクルードパスに追加しておく必要があります。
そうすれば毎回パスを通す必要がないからです。
そこで、ProjectConfigration.class.phpにパスを追加するように書いておきます。
この書き方はsymfony1.2から利用できる書き方です。

./config/ProjectConfiguration.class.php

PHP:
  1. public function setup()
  2. {
  3. ...
  4.   // add include path
  5.   sfToolkit::addIncludePath(array(
  6.     sfConfig::get('sf_root_dir') . '/lib/vender/PHP Markdown 1.0.1m',
  7.   ));
  8. }

また、HTMLに変換したデータをhtml_bodyというカラムに保存するようにします。
そのために新しいカラムをスキーマに定義してテーブルの再構築を行っておきます。

./config/doctrine/schema.yml

CSS:
  1. Question:
  2.   columns:
  3.     ....
  4.     html_body:
  5.       type: clob
  6.       default: ''
  7.       notnull: true

CREATE文の再作成、テーブルとモデルの再構成を行います。

C:
  1. $ ./symfony doctrine:build-model
  2. $ ./symfony doctrine:build-sql

ではQuestionモデルにMarkdownで入力値を変換するようにかいておきます。
パスは通っているので、普通にrequre_onceを行うだけです。

./lib/model/doctrine/Question.class.php

PHP:
  1. public function setBody($v)
  2. {
  3.   require_once('markdown.php');
  4.   $this->_set('body', $v);
  5.   // strip all HTML tags
  6.   $v = htmlentities($v, ENT_QUOTES, sfConfig::get('sf_charset'));
  7.   $this->_set('html_body', markdown($v));
  8. }

本家チュートリアルと異なるのは文字コードの設定をハードコーディングではなく、setting.ymlて定義したcharsetの値を利用している点です。

また、bodyやhtml_bodyなどのプロパティを上書きする場合はマジックメソッドを使わずに$this->setメソッドを使うようにしなければなりません。そうしないと永久ループによるセグメンテーションエラーなどが発生する場合があります。

では、本家と同じくテストデータを用意して読み込ませてみます。

./data/fixtures/test/002.question.yml

CSS:
  1. Question:
  2.   q1:
  3.     title: What shall I do tonight with my girlfriend?
  4.     User: fabien
  5.     body:  |
  6.       We shall meet in front of the __Dunkin'Donuts__ before dinner,
  7.       and I haven't the slightest idea of what I can do with her.
  8.       She's not interested in _programming_, _space opera movies_ nor _insects_.
  9.       She's kinda cute, so I __really__ need to find something
  10.       that will keep her to my side for another evening.
  11.   q2:
  12.     title: What can I offer to my step mother?
  13.     User: anonymous
  14.     body:  |
  15.       My stepmother has everything a stepmother is usually offered
  16.       (watch, vacuum cleaner, earrings, [del.icio.us](http://del.icio.us) account).
  17.       Her birthday comes next week, I am broke, and I know that
  18.       if I don't offer her something *sweet*, my girlfriend
  19.       won't look at me in the eyes for another month.
  20.   q3:
  21.     title: How can I generate traffic to my blog?
  22.     User: francois
  23.     body:  |
  24.       I have a very swell blog that talks
  25.       about my class and mates and pets and favorite movies.

用意したデータを再読み込みします。

C:
  1. $ mysql -uyourname -p askeet <data/sql/drop.sql
  2. Enter password:
  3. $ mysql -uyourname -p askeet <data/sql/schema.sql
  4. Enter password:
  5. $ ./symfony doctrine:data-load

あとは表示するカラムをgetBodyではなくgetHtmlBodyに変更します。

./apps/frontend/modules/question/templates/showSuccess.php

PHP:
  1. <div class="question_body">
  2.   <?php echo $question->getHtmlBody() ?>
  3. </div>

./apps/frontend/modules/question/templates/_list.php

PHP:
  1. <div class="question_body">
  2.     <?php echo truncate_text($question->getHtmlBody(), 200) ?>
  3.   </div>

ここでWebアプリケーションの開発を正しく行える開発者の方は気づくはずです
出力時にHTMLをエスケープできていないのでは?
そうです。今のままではできていませんよね。

データを保存するときにHTMLに変換し、その値を出力するので安全だとはいえ、望ましい出力の方法は
デフォルトはエスケープされているようにして、エスケープしたくないときが例外処理のはずと

本家ではここでは何も触れていませんが、きちんと設定を行っておく事にします。

symfonyではデフォルトでは出力時のエスケープ処理を行わないようになっています。
ただし、標準でエスケープするようにsetting.ymlでescaping_strategyをOn指定するだけで対応できます。

CSS:
  1. all:
  2.   .settings:
  3.     # Output escaping settings
  4.     escaping_strategy:      On
  5.     escaping_method:        ESC_SPECIALCHARS

また、symfony1.2からは本当はアプリケーション作成時に以下のようにオプションで指定することができるのですが忘れてしまっていました。

C:
  1. $ ./symfony generate:app frontend --escaping-strategy=on

あとは、デフォルトでエスケープされるようになったので、html_bodyはエスケープさせないように設定します。
getHtmlBodyのメソッドに引数としてESC_RAWという定数を渡すだけなのです。簡単ですね。

PHP:
  1. <?php echo $question->getHtmlBody(ESC_RAW) ?>

ルーティングの改善

本日のもう1つの課題はルーティングを変えることです。
URLにidが含まれるのはSEO的にイケテないよねとのことからのようです。
ルーティングの設定方法そのものはsymfony1.0からと大きく変更はないので、そのままでいけると思います。

ユーザーidからnicknameに

userモジュールのshowアクションをnicknameで表示できるように変更します。

./apps/frontend/modules/user/actions/actions.class.php

PHP:
  1. public function executeShow(sfWebRequest $request)
  2.   {
  3.     $this->subscriber = User::findByNickname($request->getParameter('nickname'));
  4.     $this->forward404Unless($this->subscriber);
  5.     $this->interests = $this->subscriber->getInterest();
  6.     $this->answers = $this->subscriber->getAnswer();
  7.     $this->questions = $this->subscriber->getQuestion();
  8.   }

./lib/model/doctrine/User.class.php

PHP:
  1. static public function findByNickname($nickname)
  2.   {
  3.     $subscribers = Doctrine::getTable('User')->findByNickname($nickname);
  4.     return ($subscribers->count() == 1 )? $subscribers[0] : null;
  5.   }

URLにnicknameというキーがなくても済むようにルーティングを変更します。
また、本家のチュートリアルと同様にいくつかルーティングを追加しておきます。

./apps/frontend/config/routing.yml

CSS:
  1. # default rules
  2. # quetion
  3. homepage:
  4.   url:   /
  5.   param: { module: question, action: index }
  6. popular_questions:
  7.   url:   /index/:page
  8.   param: { module: question, action: list, page: 1 }
  9. recent_questions:
  10.   url:   /recent/:page
  11.   param: { module: question, action: recent, page: 1 }
  12. add_question:
  13.   url:   /add_question
  14.   param: { module: question, action: add }
  15. # answer
  16. recent_answers:
  17.   url:   /recent/answers/:page
  18.   param: { module: answer, action: recent, page: 1 }
  19. # user
  20. user_profile:
  21.   url:   /user/:nickname
  22.   param: { module: user, action: show }
  23. login:
  24.   url:   /login
  25.   param: { module: user, action: login }
  26. default_symfony:
  27.   url:   /symfony/:action/*
  28.   param: { module: default }
  29. default_index:
  30.   url:   /:module
  31.   param: { action: index }
  32. default:
  33.   url:   /:module/:action/*

テンプレートのリンクの指定をルーティング名(@XXX)を利用する書き方に変更します。
./apps/frontend/modules/question/templates/showSuccess.php
./apps/frontend/modules/question/templates/_list.php

PHP:
  1. <div>asked by <?php echo link_to($question->getUser(), '@user_profile?nickname='.$question->getUser()->getNickname()) ?>

sidebarのリンク指定も変更しておきます。
./apps/frontend/modules/sidebar/templates/_default.php

PHP:
  1. <?php echo link_to('ask a new question', '@add_question') ?>
  2. <ul>
  3.   <li><?php echo link_to('popular questions', '@popular_questions') ?></li>
  4.   <li><?php echo link_to('latest questions', '@recent_questions') ?></li>
  5.   <li><?php echo link_to('latest answers', '@recent_answers') ?></li>
  6. </ul>

このようにルーティング名を使ってリンクを簡単に指定できるところはsymfony1.2でも同じです。

また明日

新しいことはインクルードパスを追加するために便利なメソッドが用意されているぐらいでした。
今回のような場合ではわざわざ利用するまでもないかもしれませんね。

また、ルーティング名を使ったリンクの指定はできるだけ利用したほうが変更に強いアプリケーションになります。

ここまでのソースは以下のリポジトリからチェックアウトすることができます。
http://svn.1ms.jp/public/symfony/askeet12/tags/release_day_9

関連するその他の記事

Comments

Leave a Reply