Caution: You are browsing the legacy symfony 1.x part of this website.
SymfonyWorld Online 2020
100% online
30+ talks + workshops
Live + Replay watch talks later

7日目: モデルとビューの操作

1.0

復習

すでに6日が過ぎ、アプリケーションは今のところ、便利ではないと考えている方もいらっしゃるかもしれません。なぜなら利用可能なページ数によってアプリケーションの実用性を考える人がいて、askeetは質問リストを表示できる、それに対する回答を表示できる、ユーザーーセッションを扱うことしかできないことを見ているからです。

我々がページ数を重要視しないのはsymfonyに新しいページを追加するのが簡単だからです。証明して欲しいですか?よろしい、今日は最後に求められた質問リストを表示し、最後に投稿された回答リストを表示し、質問に関心を示したユーザーリスト、ユーザーのプロファイルを表示し、それらの機能にアクセスするすべてのページにナビゲーションバーを追加します。作業は一時間程度もないので、ビューの設定もセットアップして、今週何が行われたのか一覧します。準備はいいですか?では行きましょう。

リファクタリング

question/templates/_list.phpに存在するものと似たページ分割機能によって番号つきのリストを追加しようとしています。私たちは同じ作業を繰り返したくないので、ページ分割コードをこのパーシャルからカスタムヘルパーに抽出します。ヘルパーはテンプレートにアクセス可能なPHP関数です(link_to()format_date()ヘルパーのようなものです)。

askeet/apps/frontend/lib/helperGlobalHelper.phpを作成し、次のコードを追加します:

<?php
 
function pager_navigation($pager, $uri)
{
  $navigation = '';
 
  if ($pager->haveToPaginate())
  {  
    $uri .= (preg_match('/\?/', $uri) ? '&' : '?').'page=';
 
    // 最初のページと以前のページ
    if ($pager->getPage() != 1)
    {
      $navigation .= link_to(image_tag('first.gif', 'align=absmiddle'), $uri.'1');
      $navigation .= link_to(image_tag('previous.gif', 'align=absmiddle'), $uri.$pager->getPreviousPage()).'&nbsp;';
    }
 
    // 1つずつのページ
    $links = array();
    foreach ($pager->getLinks() as $page)
    {
      $links[] = link_to_unless($page == $pager->getPage(), $page, $uri.$page);
    }
    $navigation .= join('&nbsp;&nbsp;', $links);
 
    // 次と最後のページ
    if ($pager->getPage() != $pager->getCurrentMaxLink())
    {
      $navigation .= '&nbsp;'.link_to(image_tag('next.gif', 'align=absmiddle'), $uri.$pager->getNextPage());
      $navigation .= link_to(image_tag('last.gif', 'align=absmiddle'), $uri.$pager->getLastPage());
    }
 
  }
 
  return $navigation;
}    

ページ分割のナビゲーションヘルパーは以前私たちが書いたコードを改善します。最初のページにpreviousリンクを表示せず、最後のページに'next'リンクを表示しないようにするルーティングルールを使用します。リンクの見栄をよくするために4つの新しい画像(first.gifprevious.gifnext.giflast.gif)も追加しました。askeetのSVNリポジトリから入手してください。将来のあなた自身のプロジェクトでもこのヘルパーを再利用できるでしょう。

question/templates/_list.phpフラグメントに存在するこのヘルパーを使うため、次のようにヘルパー関数を呼び出してください:

<?php use_helper('Text', 'Global') ?>
 
<?php foreach($question_pager->getResults() as $question): ?>
  <div class="question">
    <div class="interested_block">
      <?php include_partial('interested_user', array('question' => $question)) ?>
    </div>
 
    <h2><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></h2>
 
    <div class="question_body">
      <?php echo truncate_text($question->getBody(), 200) ?>
    </div>
  </div>
<?php endforeach; ?>
 
<div id="question_pager">
  <?php echo pager_navigation($question_pager, 'question/list') ?>
</div>

Globalという名称はさきほど作成したGlobalHelper.phpファイルを参照します。

リクエストによってすべてが以前と同じように動作することを確認してください:

http://askeet/frontend_dev.php/

リファクタリングされたページャのナビゲーション

最近の質問リスト

questionモジュールに、新しいアクションであるrecentを作成します:

public function executeRecent()
{
  $this->question_pager = QuestionPeer::getRecentPager($this->getRequestParameter('page', 1));
}

このように簡単です。最新の質問を取り込む機能はQuestionPeerクラスのメソッドが妥当であると私たちは考えています。-Peerクラスは与えられたクラスのオブジェクトのリストを返すことに特化されています。これはsymfony bookのモデルの章に詳細が説明されています。しかしgetRecent()クラスメソッドはまだ作成されていません。askeet/lib/model/QuestionPeer.phpクラスを開き、次のコードを追加してください:

public static function getRecentPager($page)
{
  $pager = new sfPropelPager('Question', sfConfig::get('app_pager_homepage_max'));
  $c = new Criteria();
  $c->addDescendingOrderByColumn(self::CREATED_AT);
  $pager->setCriteria($c);
  $pager->setPage($page);
  $pager->setPeerMethod('doSelectJoinUser');
  $pager->init();
 
  return $pager;
}

日付の降順の基準は最新の質問を選択します、このメソッドはparentの代わりにselfを使用します。なぜならクラスメソッドであって、オブジェクトメソッドではないからです。単純なdoSelect()のかわりにdoSelectJoinUser()をここで使う理由はテンプレートは質問の作者の詳細を必要とするからです。質問リストのための最初のリクエストを意味し、さらに関連ユーザーを取得するための1つの質問ごとのリクエストも意味します。私たちが求めたとき、doSelectJoinUser()メソッドは1つのリクエストにおいてすべてを行います:

$question->getUser();

...データベースにはリクエストがまったく送信されません。joinUserは1つ以上のリクエストをたった1つの質問に減らすことができるようにします。データベースはこの簡単な最適化をした私たちに感謝してくれることでしょう。

Propelのドキュメントにこのすばらしい機能についてのすべての説明があります。

最近の質問リストのテンプレートはホームページに表示された質問リストのように見えます。askeet/apps/frontend/module/question/templates/recentSuccess.phpを作成してください:

<h1>recent questions</h1>
 
<?php include_partial('list', array('question_pager' => $question_pager)) ?>

あなたは今、5日目になぜ私たちが質問リストをフラグメントにリファクタリングしたのか理解しています。最後に、4日目に触れたように、frontend/confg/routing.yml設定ファイルにrecent_questionsルールを追加する必要があります:

recent_questions:
  url:   /question/recent/:page
  param: { module: question, action: recent, page: 1 }

しかしお待ちください: question/_listフラグメントはルーティングルールを伴ったquestion/listリンクを作成しますので、これを使っては最近の質問リストは動作しません。私たちはさまざまなページャのために再利用できるようにするためにルーティングルールをパラメータとしてフラグメントに渡す必要があります。ですのでrecentSuccess.phpの最後の行を次のように変更してください:

<?php include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/recent')) ?>

_list.php フラグメントの最後の行を次のように変更してください:

<div id="question_pager">
  <?php echo pager_navigation($question_pager, $rule) ?>
</div>

modules/question/templates/listSuccess.php_listフラグメントの呼び出しにルールパラメータを追加することを忘れないでください。

<h1>popular questions</h1>
 
<?php echo include_partial('list', array('question_pager' => $question_pager, 'rule' => 'question/list')) ?>

キャッシュをクリアして(設定は修正されました)、これでお終いです。

最近の質問リストを表示するには、次のURLブラウザバーに入力します:

http://askeet/question/recent

最近の質問リスト

最近の回答リスト

上記の作業とまったく同じなので、まっしぐらに進みます:

  • answerモジュールを作成してください:

    $ symfony init-module frontend answer
    
  • 新しいアクションであるrecentを作成してください:

    public function executeRecent()
    {
      $this->answer_pager = AnswerPeer::getRecentPager($this->getRequestParameter('page', 1));
    }   
  • AnswerPeerクラスを拡張してください:

    public static function getRecentPager($page)
    {
      $pager = new sfPropelPager('Answer', sfConfig::get('app_pager_homepage_max'));
      $c = new Criteria();
      $c->addDescendingOrderByColumn(self::CREATED_AT);
      $pager->setCriteria($c);
      $pager->setPage($page);
      $pager->setPeerMethod('doSelectJoinUser');
      $pager->init();
     
      return $pager;
    }
  • 新しいrecentSuccess.phpテンプレートを作成してください:

    <?php use_helper('Date', 'Global') ?>
     
    <h1>recent answers</h1>
     
    <div id="answers">
    <?php foreach ($answer_pager->getResults() as $answer): ?>
      <div class="answer">
        <h2><?php echo link_to($answer->getQuestion()->getTitle(), 'question/show?stripped_title='.$answer->getQuestion()->getStrippedTitle()) ?></h2>
        <?php echo count($answer->getRelevancys()) ?> points
        posted by <?php echo link_to($answer->getUser(), 'user/show?id='.$answer->getUser()->getId()) ?> 
        on <?php echo format_date($answer->getCreatedAt(), 'p') ?>
        <div>
          <?php echo $answer->getBody() ?>
        </div>
      </div>
    <?php endforeach ?>
    </div>        
     
    <div id="question_pager">
      <?php echo pager_navigation($answer_pager, 'answer/recent') ?>
    </div>
  • ブラウザでテストしてください:

    http://askeet/answer/recent
    

最近の回答リスト

慣れてきましたか?

note

4日目に注意を払った方々なら回答の詳細を表示するコードのチャンクを認識したでしょう。コードは少なくとも2つの場所で使われているので、私たちはリファクタリングして、qestion/showanswer/recentの両方を使用されている_answer.phpの一部分を作成します。詳細はaskeetのSVNリポジトリで見ることができます。

ユーザーのプロファイル

回答にあるユーザー名はまだ書かれていないuser/showアクションにリンクをします。これはユーザープロファイルになり、ユーザーについてのわずかな詳細と同じように最近投稿された質問と回答を表示します。

最初に行うべきはことはアクションを作成することです:

public function executeShow()
{
  $this->subscriber = UserPeer::retrieveByPk($this->getRequestParameter('id', $this->getUser()->getSubscriberId()));
  $this->forward404Unless($this->subscriber);
 
  $this->interests = $this->subscriber->getInterestsJoinQuestion();
  $this->answers   = $this->subscriber->getAnswersJoinQuestion();
  $this->questions = $this->subscriber->getQuestions();
}

->getInterestsJoinQuestion()->getAnswersJoinQuestion()メソッドはUserクラスのネイティブメソッドです。どのように動くのか理解するにはaskeet/lib/model/om/BaseUser.phpクラスを調べます。

askeet/apps/frontend/modules/user/templates/showSuccess.phpテンプレートで問題が起きることはないでしょう:

<h1><?php echo $subscriber ?>'s profile</h1>
 
<h2>Interests</h2>
 
<ul>
<?php foreach ($interests as $interest): $question = $interest->getQuestion() ?>
  <li><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></li>
<?php endforeach; ?>
</ul>
 
<h2>Contributions</h2>
 
<ul>
<?php foreach ($answers as $answer): $question = $answer->getQuestion() ?>
  <li>
    <?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?><br />
    <?php echo $answer->getBody() ?>
  </li>
<?php endforeach; ?>
</ul>
 
<h2>Questions</h2>
 
<ul>
<?php foreach ($questions as $question): ?>
  <li><?php echo link_to($question->getTitle(), 'question/show?stripped_title='.$question->getStrippedTitle()) ?></li>
<?php endforeach; ?>
</ul>

もちろん、あなたは、並び順と同様に、->getInterestsJoinQuestion()->getAnswersJoinQuestion()UserオブジェクトのgetQuestion()の各メソッドによって返された結果の数字を制限できることを望んでいるでしょう。askeet/lib/model/User.phpクラスファイルに存在するこれらのメソッドをオーバーライドすることで簡単に実現できます。そして、どのように実現するのかここで終わらせることはしません - しかし今日のリリースはこれを含みます。

テストの最終段階です。最初のユーザーが行ったことを見てみましょう:

http://askeet/user/show/id/1

ユーザーのプロファイル

今、質問からユーザープロファイルにリンクできます。quesiton/templates/showSuccess.phpquestion/templates_list.phpのdiv要素のquestion_bodyの始めに次のリンクを追加してください:

<div>asked by <?php echo link_to($question->getUser(), 'user/show?id='.$question->getUser()->getId()) ?> on <?php echo format_date($question->getCreatedAt(), 'f') ?></div>

_list.phpの中でDateヘルパーの使用を宣言することを忘れないでください。

ナビゲーションバーを追加する

横のバーを追加するためにグローバルレイアウトを変更します。このバーは動的なコンテンツを格納しますが、レイアウトのこの位置に置きたいですが、それぞれのテンプレートの一部になることはできません。加えて、テンプレートにバーのコードを加えることはたくさんの繰り返しになるので、あなたは私たちがそんなことをしたくないことをご存じでしょう。

バーがコンポーネント(component)である理由はそういうわけです。コンポーネントは変数の中で利用可能なアクションです(すなわち、HTMLコードはテンプレート実行からの結果)。symfony bookのビューの章ではコンポーネントとは何か、コンポーネントとフラグメントの違いを説明しています。

レイアウトにコンポーネントを追加する

グローバルレイアウト(askeet/apps/frontend/templates/layout.php)を開いてください。コードのこの部分を覚えてますか:

<div id="content_bar">
  <!-- Nothing for the moment -->
  <div class="verticalalign"></div>
</div>

コメントを次のコードで置き換えてください。

<?php include_component_slot('sidebar') ?>

それでお終いです。

どのアクションがコンポーネントになるのか定義する

シンプルなコンポーネントよりも小さくて強力な機能を使うことに決めました。これはコンポーネントスロットです。コンテキスト上の内容を許可する呼び出し元のアクションに従ってそのアクションを修正するのが可能なコンポーネントです。ビューの構成(view.ymlファイル)はアクションがコンポーネントスロットに対応するのかを定義します:

default:
  components:
    sidebar:      [sidebar, default]

この例の場合、sidebarという名前のコンポーネントスロットはsidebarモジュールのdefaultアクションの結果として宣言されます。

全体のアプリケーション(askeet/apps/frontend/config/ディレクトリ)もしくは特別なモジュール(askeet/apps/frontend/modules/mymodule/configディレクトリ)のためにビューを定義するのは可能です。私たちの事例では、全体のアプリケーションに対して定義を行い、サイドバーのコンテキスト上の特別なリンクが必要な時にオーバーライドすることにします。

ですのでaskeet/apps/frontend/config/view.ymlを開き、上記で示されたコンポーネントスロットの設定に追加してください。synmfony bookの関連する章でビューの設定について詳しい情報が得られます。

sidebar/defaultアクションとテンプレートを書く

最初に、新しいsidebarモジュールを初期化します:

$ symfony init-module frontend sidebar

次に、デフォルトのコンポーネントを書く必要があります。askeet/apps/frontend/modules/sidebar/actionsディレクトリにおいてactions.class.phpcomponents.class.phpにリネームをして内容を次のように変更します:

<?php 
 
class sidebarComponents extends sfComponents
{
  public function executeDefault()
  {
  }
}

コンポーネントビューはテンプレートで、あたかもアクションのために存在します。違いはネーミングにあります: コンポーネントビューは通常のテンプレート(Successで終わる)というよりもフラグメント(_で始まる)のように命名されました。askeet/apps/frontend/modules/sidebar/templates/_default.phpフラグメント(使われないindexSuccess.phpは削除してください)を次のように作成してください:

<?php echo link_to('ask a new question', 'question/add') ?>
 
<ul>
  <li><?php echo link_to('popular questions', 'question/list') ?></li>
  <li><?php echo link_to('latest questions', 'question/recent') ?></li>
  <li><?php echo link_to('latest answers', 'answer/recent') ?></li>
</ul>

askeetのWebサイトのページを移動しようとすると、エラーになるでしょう。設定がキャッシュされ、それぞれのリクエストで解析されない本番環境でサイトをナビゲートしているからです。view.yml設定ファイルを修正しますが、本番環境のアクションは理解できません。キャッシュバージョンを使用します - コンポーネントスロットを含まないものです。変更を見たい場合、キャッシュをクリアするか開発環境でナビゲートしてください:

$ symfony clear-cache

もしくは

http://askeet/frontend_dev.php/

ナビゲーションバーはすべてのページ上で正しく表示されます

サイドバー

note

本番環境の設定の一般的な効果です。ですので開発フェーズの間(設定をたくさん変更しているとき)、開発環境を使っていることを覚えておくこと、および本番環境をナビゲートするとき、設定を変更したあとにキャッシュをクリアすることを覚えておく必要があります。

少々のビュー構成

私たちが目指している間に、apps/config/にあるアプリケーションのview.yml設定ファイルを見てみましょう:

default:
  http_metas:
    content-type: text/html; charset=utf-8

  metas:
    title:        symfony project
    robots:       index, follow
    description:  symfony project
    keywords:     symfony, project
    language:     en

  stylesheets:    [main, layout]

  javascripts:    []

  has_layout:     on
  layout:         layout

  components:
    sidebar:      [sidebar, default]

metasセクションは全体のサイトのメタタグのための設定を含みます。titleキーはブラウザウィンドウのバーに表示されるタイトルも定義します。このタイトルは非常に重要です。なぜなら、ユーザーが検索インデックスによって見つけたときに最初にサイトで見るものだからです。それゆえ、askeetサイトに採用されたものを変更するために必要です:

  metas:
    title:        askeet! ask questions, find answers
    robots:       index, follow
    description:  askeet!, a symfony project built in 24 hours
    keywords:     symfony, project, askeet, php5, question, answer
    language:     en

現在のページをリフレッシュしてください。変化が見られない場合は、本番環境にいるからです。適切なウィンドウタイトルが表示されるためにはまずキャッシュをクリアする必要があります:

ウィンドウのタイトル

note

加えて、プロジェクトページのデフォルトタイトルを提供するために、Webのrootディレクトリ(askeet/web/)でデフォルトのrobots.txtfavicon.icoを作成します。これらも変更することを忘れないでください!

Note: あなたのサイトの各ページのタイトルを変更する必要があるかもしれません。各モジュールのためのカスタムview.yml設定ファイルを定義できます。しかし、静的なタイトルのみです。ビュー構成の章に書かれているように、代わりに->setTitle()メソッドによるアクションから動的な値を使うことができます:

  [php]
  $this->getResponse()->setTitle($title);

私たちが何を実現したのか見てみましょう

作業を止めて7日目までに実現したことを見直しましょう。現在のデータモデルと利用可能なアクションを含む少しの内容のドキュメントを作るのによい機会です。

実際のところ、書いている間にコードを記録した方がよいです。たとえば各メソッドごとにphpDocumentorスタイルのコメントなどです。symfonyのプロジェクトにおいて、よく使われるメソッドや関数で使用される名前はそれぞれの目的と利用の説明として提供されます。メソッドは短くとても読みやすいです。多くの時間において、テンプレートは一目瞭然のforeachとifステートメントしか使用しません。なぜaskeetのSVNリポジトリで見かけるコードが多くのドキュメントを含まないのはそういうわけです - 加えて、私たちが7時間書いたという事実は私たちが行ったことの説明に値します!

更新されたエンティティのリレーションダイアグラムを見てみましょう:

ERD

利用可能なリストは以下の通りです:

answer/
  recent
question/
  list
  show
  recent
sidebar/
  default (component)
user/
  show
  login
  logout
  handleErrorLogin

モデルは次のメソッドも含みます:

Anwser()
  getRelevancyUpPercent()
  getRelevancyDownPercent()
AnswerPeer::
  getRecentPager()
Interest->
  save()
Question->
  setTitle()
QuestionPeer::
  getQuestionFromTitle()
  getHomepagePager()
  getRecentPager()
Relevancy
  save()
User->
  __toString()
  setPassword()

myUser->
  signIn()
  signOut()
  getSubscriberId()
  getSubscriber()
  getNickName()

...加えて、カスタムツールクラスとカスタムバリデータはaskeet/appss/frontend/lib/ディレクトリに設置されています。

そのために7時間は悪くないでしょう?

それではまた明日

今日はアプリケーションの開発が大きく進展し、またとても早かったです。人間とコンピュータとやりとりにおいて、AJAXを投入する準備はすべて整いました。明日はユーザーがログインとAJAXを使用して、質問に対する関心を宣言できるようにします。見逃さないでください。

release_day_7とタグづけされたaskeetのSVNリポジトリからまだ今日の分の全コードをダウンロードできます。askeetのメーリングリストは光よりも速くあなたの質問に回答してくれます。