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

DAY7: モデルとビューの操作

これまでのおさらい

ようやく6日間がおわったというところです。本チュートリアルの目的はsymfony1.0との違いを確認したいということだったのですが、sfFormを使う時点でかなり違ったものになった印象を感じてしまいます。
ただ、フォーム処理を切り分けるという意識がsymfony1.0のころに比べてはっきりしているため以前あったバリデーションをアクションでやりくりしなくてはいけない場合があった違和感はなくなりました。
総合的にみて、symfony1.2のsfFormはWebアプリケーションを開発するためにはフォームヘルパーよりも数段優れている印象を受けます。

さて、今日も本家のチュートリアルの内容をsymfony1.2で作成していきます。目標はモデルとビューの操作です。

プレファタリング(事前のリファクタリング)

本家チュートリアルではこのリファクタリングでパジネーションのHTML作成をヘルパーとして実装し直しています。
ヘルパーとはテンプレートで利用することを前提とした関数群のことで、今までもリンク先を指定してアンカータグを作成するlink_to関数などを利用してきました。

この機能そのものはsymfony1.2でも変わりません。
なので、本家チュートリアルと同様に以前作成したパジネーションをそのままヘルパーとして登録してみます。

まず、ヘルパー名をGlobalHelper.phpとします。
配置場所はアプリケーションのlib/helperディレクトリに配置します。

./apps/frontend/lib/helper/GlobalHelper.php

PHP:
  1. <?php
  2. function pager_navigation($pager, $uri) {
  3.   $html = "";
  4.   if ($pager->haveToPaginate()) {
  5.     if ($pager->getPage() != 1) {
  6.       $html .= link_to(image_tag('first.gif', 'align=absmiddle'), sprintf('%s?page=1', $uri));
  7.       $html .= link_to(image_tag('previous.gif', 'align=absmiddle'), sprintf('%s?page=%d', $uri, $pager->getPreviousPage()));
  8.     }
  9.     for ($page_no = 1, $max = $pager->getLastPage(); $page_no <= $max; $page_no++) {
  10.       $html .= link_to_unless($page_no == $pager->getPage(), $page_no, sprintf('%s?page=%d', $uri, $page_no));
  11.       $html .= ($page_no != $pager->getLastPage()) ? '&nbsp;&nbsp;' : '';
  12.     }
  13.     if ($pager->getPage() != $pager->getLastPage()) {
  14.       $html .= link_to(image_tag('next.gif', 'align=absmiddle'), sprintf('%s?page=%d', $uri, $pager->getNextPage()));
  15.       $html .= link_to(image_tag('last.gif', 'align=absmiddle'), sprintf('%s?page=%d', $uri, $pager->getLastPage()));
  16.     }
  17.   }
  18.   return $html;
  19. }

では、このヘルパーを呼び出すパーシャル(_list.php)でヘルパーの使用を宣言し、コードを置き換えます。
ヘルパーの宣言はコード中であればどこでも可能ですが、ファイルの先頭で行うようにルールを決めておくと解りやすくておすすめです。

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

PHP:
  1. <?php use_helper('Text', 'Global', 'Date') ?>
  2. ....
  3. <div id="question_pager">
  4. <?php echo pager_navigation($pager, 'question/list') ?>
  5. </div>

画像も本家のリポジトリからもってくると本家のチュートリアルと同じようなパジネーションのできあがりです。


最新の質問一覧

最新の質問一覧ページを作成していきます。
名前はrecentアクションとします。
本家チュートリアルではPropelを使っているため少しばかり実装方法が異なります。
Propelの場合、ページャから質問データを取得することができるようです。
ただし、Doctrineのページャはページャを作成する途中でリストを取得できるようになっています。
そのため、recentアクションではページャとリストの両方をアサインするようにします。

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

PHP:
  1. public function executeRecent(sfWebRequest $request)
  2. {
  3.   $q = Question::createGetRecentDql();
  4.   $pager = new Doctrine_Pager($q, $request->getParameter('page'), sfConfig::get('app_pager_homepage_max'));
  5.   $this->questionList = $pager->execute();
  6.   $this->pager = $pager;
  7. }

またDQLを作成するメソッドをQuestion.class.phpに用意します。
このときDoctrineのメソッドチェーンをうまく利用すれば以下のようにすっきりとかけます。

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

PHP:
  1. static public function createGetRecentDql()
  2. {
  3.   return self::createGetAllDql()
  4.      ->orderby('created_at desc');
  5. }

既に用意してある全取得用のDQLにOrderByの条件を追加したものを返しているだけです。

次に、テンプレートを用意します。recentアクションの結果ですのでrecentSuccess.phpになります。
ここで、以前作成したlistパーシャルを呼び出す事で表示部分を再利用しています。
また、パジネーションのリンク先をquestion/recentに変更しなければならないので、新しくruleという変数でパーシャルに渡すようにします。

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

PHP:
  1. <h1>recent question</h1>
  2. <?php include_partial('list', array('pager' => $pager, 'questionList' => $questionList, 'rule' => 'question/recent')) ?>

それに伴い、indexSuccess.php, _list.phpも修正しておきます。

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

PHP:
  1. <?php include_partial('list', array('questionList' => $questionList, 'pager' => $pager, 'rule' => 'question/list')) ?>

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

PHP:
  1. <?php echo pager_navigation($pager, $rule) ?>

最新の回答一覧

次に最新の回答一覧を作成します。これは質問の一覧を作成したのと同じなので、以下実際の手順のみ記しておきます。

アンサーモジュールの作成

$ ./symfony generate:module frontend answer

recentアクションの実装

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

PHP:
  1. public function executeRecent(sfWebRequest $request)
  2. {
  3.   $q = Answer::createGetRecentDql();
  4.   $pager = new Doctrine_Pager($q, $request->getParameter('page'), sfConfig::get('app_pager_homepage_max'));
  5.   $this->answerList = $pager->execute();
  6.   $this->pager = $pager;
  7. }

アンサーモデルの実装

./lib/model/doctrine/Answer.class.php

PHP:
  1. static public function createGetAllDql()
  2. {
  3.   return Doctrine_Query::create()
  4.      ->from('Answer');
  5. }
  6. static public function createGetRecentDql()
  7. {
  8.   return self::createGetAllDql()
  9.      ->orderby('created_at desc');
  10. }

アンサーテンプレートの作成

リレーション先の呼び出し方がPropelとDoctrineで微妙に異なっていたりします。
Doctrineではレコードにcountメソッドが用意されているので、$answer->getRelevancy()->count()というように呼び出しているところが特徴です。

./apps/frontend/modules/answer/templates/recentSuccess.php

PHP:
  1. <?php use_helper('Date', 'Global') ?>
  2. <h1>recent answer</h1>
  3. <div id="answers">
  4. <?php foreach ($answerList as $answer): ?>
  5.   <div class="answer">
  6.     <h2><?php echo link_to($answer->getQuestion()->getTitle(), 'question/show?id='.$answer->getQuestion()->getId()) ?></h2>
  7.   <?php echo $answer->getRelevancy()->count() ?> points
  8.     posted by <?php echo link_to($answer->getUser(), 'user/show?id='.$answer->getUser()->getId()) ?>
  9.     on <?php echo format_date($answer->getCreatedAt(), 'p') ?>
  10.     <div>
  11.       <?php echo $answer->getBody() ?>
  12.     </div>
  13.   </div>
  14. <?php endforeach ?>
  15. </div>       
  16.  
  17. <div id="question_pager">
  18.   <?php echo pager_navigation($pager, 'answer/recent') ?>
  19. </div>

ブラウザで確認

http://symfony.centos5.localhost/askeet/frontend_dev.php/answer/recent

にアクセスして以下のようが画面がでればOKです。



ユーザープロフィール

つぎにユーザープロフィールの画面を作成していきます。
まずは表示するためのアクション(show)を実装していきます。

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

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

ユーザーの情報をリクエストパラメータのIDから、なければログインユーザーのIDから取得しています。
この部分の本家チュートリアルとの違いはORMの違いによるものです。

次にテンプレート(showSuccess.php)を用意します。

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

PHP:
  1. <h1><?php echo $subscriber->getNickname() ?> profile</h1>
  2.  
  3. <h2>Interests</h2>
  4.  
  5. <ul>
  6. <?php foreach ($interests as $interest): $question = $interest->getQuestion() ?>
  7.    <li><?php echo link_to($question->getTitle(), sprintf('question/show?id=%d', $question->getId())) ?></li>
  8. <?php endforeach; ?>
  9. </ul>
  10.  
  11. <h2>Contributions</h2>
  12.  
  13. <ul>
  14. <?php foreach ($answers as $answer): $question = $answer->getQuestion() ?>
  15.   <li>
  16.    <?php echo link_to($question->getTitle(), sprintf('question/show?id=%d',$question->getId())) ?><br />
  17.     <?php echo $answer->getBody() ?>
  18.   </li>
  19. <?php endforeach; ?>
  20. </ul>
  21.  
  22. <h2>Questions</h2>
  23.  
  24. <ul>
  25. <?php foreach ($questions as $question): ?>
  26. <li><?php echo link_to($question->getTitle(), sprintf('question/show?id=%d', $question->getId())) ?></li>
  27. <?php endforeach; ?>
  28. </ul>

これで以下のようが画面が完成しました。


最後に誰からの質問なのかもわかるように以下のようなdivタグをquestion_body divの前に挿入しておきます。

./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/show?id='.$question->getUser()->getId()) ?> on <?php echo format_date($question->getCreatedAt(), 'yyyy-MM-dd') ?></div>

また、date_formatの書式を"yyyy-MM-dd"にして日本人が見たときに違和感がないようにしています。

ナビゲーションバーの追加

コンポーネントスロットというsymfonyのビューの仕組みを利用してナビゲーションバーを実装していきます。
ref: Definitive Guide 7章

一番の特徴は設定ファイルで表示内容を変更することができるというところですね。

本家と同じく、sidebarという名前でコンポーネントスロットを作成します。

レイアウトにコンポーネントスロットを追加

まず、レイアウトにコンポーネントスロットの関数を埋め込みます。

./apps/frontend/templates/layout.php

PHP:
  1. <div id="content_bar">
  2.       <?php include_component_slot('sidebar') ?>
  3.       <div class="verticalalign"></div>
  4.     </div>

コンポーネントスロットの定義を追加

コンポーネントスロットはview.ymlでどのコンポーネントを呼び出すかを定義できます。以下のようにsidebarモジュールのコンポーネントに定義されたdefaultメソッドを呼び出すようにします。

./apps/frontend/config/view.yml

CSS:
  1. default:
  2.   components:
  3.     sidebar: [sidebar, default]

sidebarコンポーネントを実装

コンポーネント(アクションの小さなもの)を用意します。
まず、sidebarモジュールが存在しないのでモジュールを作成します。

PHP:
  1. $ ./symfony generate:module frontend sidebar

そしてコンポーネントクラスにexecuteDefaultメソッドを用意します。
リンクを表示するだけなので、ロジックは必要ありません。そのためメソッドの中は空です。
symfony1.2からはアクションはメソッドの引数にsfWebRequestインスタンスを持つようになりましたが、これはコンポーネントでも同じです。

./apps/frontend/modules/sidebar/actions/components.class.php

PHP:
  1. <?php
  2. class sidebarComponents extends sfComponents
  3. {
  4.   public function executeDefault(sfWebRequest $request)
  5.   {
  6.   }
  7. }

次にdefaultメソッドから呼ばれるテンプレートを用意します。テンプレートの名前はパーシャルと同じアンダーバーから始まるファイル名になります。

./apps/frontend/module/sidebar/templates/_default.php

PHP:
  1. <?php echo link_to('ask a new question', 'question/add') ?>
  2. <ul>
  3.   <li><?php echo link_to('popular questions', 'question/list') ?></li>
  4.   <li><?php echo link_to('latest questions', 'question/recent') ?></li>
  5.   <li><?php echo link_to('latest answers', 'answer/recent') ?></li>
  6. </ul>

最後に念のためにキャッシュをクリアしておきます。修正が反映されないときはsymfony cc!。これが基本です。

C:
  1. $ ./symfony cc

実際に画面をみてみましょう。
以下のようにサイドバーが表示されていればOKです。



ビューの設定

symfonyは設定ファイルが多いのが特徴です。
これがややこしくさせている理由でもありますが、一方でソースを編集しなくても動作を変えることができるという特徴があります。
本家チュートリアルではビューの設定をview.ymlで行えることを説明してくれています。

ただし、かならずしもview.ymlで設定を行う必要はないと思いと私は考えています。
たとえば、ページタイトルなどは各テンプレートで異なるでしょう。それを毎回view.ymlで設定しなくてはいけないというルールではさすがにデザイナーからは不満がでると思います。
デザイナーと分業する上ではできるだけテンプレートに関する作業はテンプレート内で終わらせるようにしてあげるように考慮することも大事かと思います。

なので、ここからview.ymlで設定を行いますが、デフォルトの設定のために用意するという感じでしょうか。

./apps/frontend/config/view.yml

CSS:
  1. default:
  2.   http_metas:
  3.     content-type: text/html
  4.  
  5.   metas:
  6.     title:        askeet!
  7.     description:  'symfony 1.2 で askeet! 実践中'
  8.     keywords:     symfony, project, symfony1.2
  9.     language:     ja
  10.     robots:       index, follow
  11.  
  12.   stylesheets:    [main, layout]
  13.  
  14.   javascripts:    []
  15.  
  16.   has_layout:     on
  17.   layout:         layout
  18.  
  19.   components:
  20.     sidebar:  [sidebar, default]

また明日

今日はここまでです。本家チュートリアルとは異なりDoctrineで実装していますが、リレーション先のデータ取得などはPropelより簡単な印象です。

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

8日目はAJAXを利用してみます。

関連するその他の記事

Comments

Leave a Reply