Caution: You are browsing the legacy symfony 1.x part of this website.

3日目: MVC構造に飛び込む

1.0
Language

復習

2日目 リレーショナルデータモデルからオブジェクトモデルを作る方法を勉強し、できあがったオブジェクトからscaffolding(足場)の生成を行いました。また、前回までに作成したアプリケーションのコードはaskeetのSVNリポジトリから見ることができます:

http://svn.askeet.com/

3日目の目標は、もっと良いサイトのレイアウトを持つこと、トップページに質問のリストを持つこと、質問ごとに関心を持ったユーザーの数を表示すること、テストデータを用意するためにサンプルのテキストファイルからデータベースにデータを入れ込む事です。

このチュートリアルに関しては、symfony bookのコントローラの章で説明する symfonyのプロジェクト、アプリケーション、モジュール、アクションの概念をよく理解しておく必要があります。

MVCモデル

今日はMVC構造の世界への初のダイブです。どういうことかというと、いろいろな場所のファイルから1つのページを構成すると言うことです。

データを操作するためのコードがページそのものを表示する部分から独立しているのなら、それはモデル (通常はaskeet/lib/model/)に設置すべきです。最終的に表示する部分であれば、ビュー; に設置すべきで、symfonyでは、ビューレイヤーはテンプレートディレクトリ(たとえばaskeet/apps/frontend/modules/question/templates/)と設定ファイルになります。これら全てを統括し、サイトを組み立てる部分はコントローラで、symfonyでは、特定のページのコントローラはアクションと呼ばれます(askeet/apps/frontend/modules/question/actions/のアクションをご覧ください)。このモデルの詳細は、symfony bookの symfonyに組み込まれているMVCの章をご覧ください。

今回作業を行うビューの中では変更は少ししかありませんが、多くのファイルを変更することになりますので、やっていることを見失わないように気をつけてください。ファイルの構成やいろいろなレイヤーへのコードの分割は後々にはよりいっそう意味を持ち、とても便利な物になってくるはずです。

レイアウトの変更

アプリケーションにおいての Decoratorパターンでは、アプリケーションによって呼び出されたテンプレートの内容は、全体共通なテンプレートに取り込まれるか、レイアウト自体へ取り込まれます。言い換えれば、レイアウトというのは、全体に共通なものを持ち、アクションの最終結果を”装飾”するようなものです。デフォルトのレイアウト(askeet/apps/frontend/templates/layout.php)を開き、次のように変更してみてください:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
 
<?php echo include_http_metas() ?>
<?php echo include_metas() ?>
 
<?php echo include_title() ?>
 
<link rel="shortcut icon" href="/favicon.ico" />
 
</head>
<body>
 
  <div id="header">
    <ul>
      <li><?php echo link_to('about', '@homepage') ?></li>
    </ul>
    <h1><?php echo link_to(image_tag('askeet_logo.gif', 'alt=askeet'), '@homepage') ?></h1>
  </div>
 
  <div id="content">
    <div id="content_main">
      <?php echo $sf_data->getRaw('sf_content') ?>
      <div class="verticalalign"></div>
    </div>
 
    <div id="content_bar">
      <!-- Nothing for the moment -->
      <div class="verticalalign"></div>
    </div>
  </div>
 
</body>
</html>

note

HTMLの構造はできるだけセマンティックになるようにしています。また、スタイルはできるだけCSSに収まるようにしています。CSSの文法はこのチュートリアルの範囲内ではないため、スタイルシートについてはここでは説明しませんが、必要な方は、SVN リポジトリからダウンロードできます。

2つのスタイルシートを用意します(main.csslayout.css)。これらをaskeet/web/css/ディレクトリにコピーし、これらのスタイルシートをオートロードできるようにfrontend/config/view.ymlを変更してください:

stylesheets:    [main, layout]

現在のレイアウトはまだ簡単なものですが、1週間ぐらいの内に再設定します。このテンプレートで重要な箇所は、<head>の部分で、ここは通常自動生成され、sf_content変数はアクションの結果を保有します。

変更した部分が反映されているか、トップページを見てチェックしてください。 - 今回は開発環境(dev)を呼び出してみましょう:

http://askeet/frontend_dev.php/

更新されたレイアウト

環境について少し

もし、http://askeet/frontend_dev.php/http://askeet/がどのように違うのか分からない場合は、symfony bookの設定の章 を読んでみてください。今のところこれらの違いは、違う環境だが同じアプリケーションを呼び出していると言うこと、と理解しておいてください。 環境というのはそれぞれ独自の設定であり、それぞれにおいて、フレームワークの機能がONだったりOFFだったりしています。

この/frontend_dev.php/である場合、URLは開発環境を呼び出します。ここでは設定ファイルは毎回のリクエストで読み込まれ、HTMLのキャッシュは行われず、半透明になったデバッグ用のツールが右上に表示されます。/URL(/index.php/に同じ)では、本番環境設定ファイルは”コンパイル”されていて、デバックツールは表示されず、より早く表示される事を目的に設定されています。

これら2つのPHPスクリプト(frontend_dev.phpindex.php)は フロントコントローラ によって呼び出され、アプリケーション全てのリクエストはこのコントローラによって処理されます。コントローラはaskeet/web/ディレクトリにあるので確認してみてください。実際は、index.phpファイルは、frontend_prod.phpと命名されるべきですが、frontendは最初に作ったアプリケーションなので、symfonyはデフォルトのアプリケーションとして理解して、index.phpに名前を変更しています。これによって、/を呼び出すことだけで本番環境のアプリケーションを呼び出すことができます。フロントコントローラや、一般的なMVCモデルにおけるコントローラ層について詳しく知りたい場合は、symfony bookのコントローラの章を参照してください。

一般的な方法 / 使い分けとしては、自分で盛り込みたい機能を十分納得がいくまで開発環境においてテストし、その後、本番環境に移行して処理スピードや”より適切な”URLを確認してください。

note

本番環境において正しい結果を表示するために、新しいクラスや設定ファイルを変更した時は、常にキャッシュをクリアすることを忘れないでください。

デフォルトのホームページ(トップページ)への再定義

今のところ、トップページを表示した時には、'Congratulations'としか表示されません。トップページとしてよりよくするために、質問のリストを表示します(questionモジュールのlistアクションであるquestion/listを呼び出します)。これを実現するには、askeet/apps/frontend/config/routing.ymlにあるfrontendアプリケーションのルーティング設定ファイルを開き、homepage:セクションを次のように変更してください:

homepage:
  url:   /
  param: { module: question, action: list }

トップページを開発環境で更新してみてください(http://askeet/frontend_dev.php/); 質問のリストが表示されているはずです。

note

好奇心のある方なら、'Congratulations'を表示するページを探したかもしれません。結果としてそのようなページは askeetプロジェクトディレクトリには見つからず不思議に思っていたのではないでしょうか?実は、default/indexアクションのテンプレートディレクトリはdataディレクトリというプロジェクトから独立した場所で定義されています。オーバーライド(上書き)したい場合は、defaultモジュールを作りそれぞれのアクションを定義できます。

このルーティングシステムの機能は近いうちに詳細を紹介しますが、ご興味があれば、symfonyブックのルーティングの章をご覧ください。

テストデータの定義

トップページに表示されているリストは、質問を追加するまではまだ空のままの状態です。アプリケーションを開発する場合、必要となるテストデータを用意すると便利です。しかしながら、テストデータを手で入れるとなると(又は、データベースのCRUDインターフェイスから直に)、かなり手間です。これを解消するには、symfonyではテキストファイルからデータをデータベースに設定することができるようになっています。

それではaskeet/data/fixtures/ディレクトリにテストデータを作っていきましょう (このディレクトリは用意されてないので作成してください)。このディレクトリにtest_data.ymlという名前でファイルを作り、下の内容を入れてください:

User:
  anonymous:
    nickname:   anonymous
    first_name: Anonymous
    last_name:  Coward

  fabien:
    nickname:   fabpot
    first_name: Fabien
    last_name:  Potencier

  francois:
    nickname:   francoisz
    first_name: François
    last_name:  Zaninotto

Question:
  q1:
    title: What shall I do tonight with my girlfriend?
    user_id: fabien
    body:  |
      We shall meet in front of the Dunkin'Donuts before dinner, 
      and I haven't the slightest idea of what I can do with her. 
      She's not interested in programming, space opera movies nor insects.
      She's kinda cute, so I really need to find something 
      that will keep her to my side for another evening.

  q2:
    title: What can I offer to my step mother?
    user_id: anonymous
    body:  |
      My stepmother has everything a stepmother is usually offered
      (watch, vacuum cleaner, earrings, del.icio.us account). 
      Her birthday comes next week, I am broke, and I know that 
      if I don't offer her something sweet, my girlfriend 
      won't look at me in the eyes for another month.

  q3:
    title: How can I generate traffic to my blog?
    user_id: francois
    body:  |
      I have a very swell blog that talks 
      about my class and mates and pets and favorite movies.

Interest:
  i1: { user_id: fabien, question_id: q1 }
  i2: { user_id: francois, question_id: q1 }
  i3: { user_id: francois, question_id: q2 }
  i4: { user_id: fabien, question_id: q2 }

まず始めに、YAMLに気づくかと思います。symfonyに詳しい人でなければ、このYAMLフォーマットは知らないかもしれません。このフォーマットはsymfonyフレームワークの設定では頻繁に使われるフォーマットですが、別段特別なものではありません。 - XMLやINIファイルを使いたければ、簡単に設定ファイルハンドラを追加可能で、symfonyに読み込ませることができます。時間があれば、YAMLとsymfony設定ファイルについてをsymfony bookの設定の実践の章を読んでください。今のところは、YAML文法についてよく分からなければ、このチュートリアルではYMALを広範囲で使っているので、 ここを今すぐ読み始めて ください(訳注:こっちの方が分かりやすいかも http://ja.wikipedia.org/wiki/YAML)。

それでは、テストデータのファイルに話を戻しましょう。ここではオブジェクトのインスタンスを定義し、内部で使われる名前として命名しています。この名前はとても便利で、id (一般的にはオートインクリメントで設定不要なもの)を定義せずともオブジェクト間でリレーションを持つことができます。たとえば、最初に作ったオブジェクトはUserクラスに所属し、fabienとラベルづけされます。最初のQuestionはq1とラベルづけされます。関連オブジェクトの名前を使えば、Interestオブジェクトのインスタンスを作ることが簡単になります:

Interest:
  i1:
    user_id: fabien
    question_id: q1

先程のデータファイルで使っていたYAML簡略構文と同じ意味です。このようなデータの設定についての詳細は、symfony bookのデータファイルの章を読んでください。

note

created_atandupdated_atの項目には、値の設定は必要ありません。symfony はこれらの項目について、デフォルトで何を設定すべきか理解しています(訳注:作成日時 更新日時は設定せずともデータを追加 / 更新する際に更新される)。

データを設定するためのバッチ作成

次のステップとして、実際にデータを設定していきます。これにはコマンドラインから呼び出せるPHPスクリプトを使っていきます - バッチですね。

バッチのスケルトン

load_data.phpというファイルをaskeet/batch/ディレクトリに作って次の内容を記入してください:

<?php
 
define('SF_ROOT_DIR',    realpath(dirname(__FILE__).'/..'));
define('SF_APP',         'frontend');
define('SF_ENVIRONMENT', 'dev');
define('SF_DEBUG',       true);
 
require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php');
 
// initialize database manager
$databaseManager = new sfDatabaseManager();
$databaseManager->initialize();
 
?>

ここではまだ何もしてません: パス、アプリケーション、環境を定義して設定を呼び出し、読込み、そしてデータベースマネージャーを初期化してます。何か色々やってますよね:でもこれによってこのコードから下は、symfonyのオートロード、Propelオブジェクトへの透過的な接続、 symfonyのルートクラスの呼び出しなどさまざまな機能が利用可能になります。

note

もしsymfonyのフロントコントローラ(たとえばaskeet/web/index.php)を見たことがあるなら、とてもよく似てると気づかれたかもしれません。それはすべてのWebリクエストは同じオブジェクト、設定にアクセスする事が必要であり、バッチもそれに漏れないからです。

データのインポート

バッチの大元が準備できたところで、仕事を割り当てましょう。このバッチでは:

  1. YAMLファイルを読み込む
  2. Propelオブジェクトのインスタンスを作る
  3. データベース上のテーブルにレコードを追加する

なんだか難しそうですが、symfonyではsfPropelDataのおかげで、これらをたったの2行で行うことができます。次のコードをaskeet/batch/load_data.php?>の前に追加してください:

$data = new sfPropelData();
$data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');

たったこれだけです。sfPropelDataが作成され、指定されたfixturesディレクトリに存在する全てのファイルを読み込み、databases.yml設定ファイルに定義されているデータベースにデータを追加します。

note

DIRECTORY_SEPARATOR定数はWindowsやUnix系プラットフォームで互換性を持つように使われています。

バッチの実行

やっと最後ですが、さっきの数行のコードが手入力などの手間から解放されるか確認してみましょう。次をコマンドラインから入力してください:

$ cd /home/sfprojects/askeet/batch
$ php load_data.php

データベースへの変更を確認するには、開発環境のトップページを更新してみてください:

http://askeet/frontend_dev.php

loaded data

バンザーイ。データが反映されてますね。

note

デフォルトでは、sfPropelDataオブジェクトは、新しいデータを追加する前に全てのデータをいったん削除します。随時追加するには下のようにしてください:

$data = new sfPropelData();
$data->setDeleteCurrentData(false);
$data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');

モデル内でのデータアクセス

questionモジュールのlistアクションを呼び出した時に表示されるページは、executeList()メソッドの処理を (askeet/apps/frontend/modules/question/actions/action.class.phpにあります) このテンプレートであるaskeet/apps/frontend/modules/question/templates/listSuccess.phpに渡した結果です。これはsymfony bookの コントロールの章 説明されている名前変換によるものです。実行されているコードを見てみましょう:

actions.class.php:

public function executeList ()
{
  $this->questions = QuestionPeer::doSelect(new Criteria());
}

listSuccess.php:

...
<?php foreach ($questions as $question): ?>
<tr>
    <td><?php echo link_to($question->getId(), 'question/show?id='.$question->getId()) ?></td>
    <td><?php echo $question->getTitle() ?></td>
    <td><?php echo $question->getBody() ?></td>
    <td><?php echo $question->getCreatedAt() ?></td>
    <td><?php echo $question->getUpdatedAt() ?></td>
  </tr>
<?php endforeach; ?>

ステップごとで見てみるとこのようになります:

  1. アクションは、空の抽出基準でのQuestionテーブルレコードを引き出します。 - つまりはQuestionの全てのデータ
  2. レコードのリストは配列($questions)に入れられ、テンプレートに渡されます
  3. テンプレートはアクションから渡された全ての質問データを順に実行します
  4. テンプレートは各レコードの各項目を表示します

->getId()->getTitle()->getBody()のようなメソッドは、事前にsymfony propel-build-modelコマンドの呼び出し (昨日 の覚えてます?)生成されており、これらがidtitlebodyなどの項目データを取得します。これらは標準的なゲッターで、接頭辞のgetとcamelCase(キャメルケース)された項目名で構成されています - そしてPropelは標準のセッターも兼ね備えており、その接頭辞はsetです。 Propelのドキュメント を読むと、この各クラスへのアクセサが解説されています。

また不思議なQuestionPeer::doSelect(new Criteria())の呼び出しは、Propelの標準メソッドです。Propelドキュメントに詳しく書かれています。

まぁ分からなくても気にしないでください。このチュートリアルを進めて行くにしたがって分かってきます。

question/listテンプレートの変更

データベースに質問(question)への関心(interest)データが設定されました。これによって1つの質問での関心の数が簡単に引き出せるはずです。Propelによって生成されたBaseQuestion.phpクラスを見たことがあるならaskeet/lib/model/om/ディレクトリ、->getInterests()メソッドに気づいたかもしれません。テーブル定義の際にPropelはquestion_id外部キーがInterestテーブル定義にあったのを知っていて、1つの質問には複数の関心が関連づけられているだろうと理解しています。これによってとても簡単に、質問に対する関心の数を表示するという事がaskeet/apps/frontend/modules/question/templates/にあるlistSuccess.phpテンプレートを編集することで可能になります。加えて、汚いテーブル構造を止め、綺麗なdivに変更します:

<?php use_helper('Text') ?>
 
<h1>popular questions</h1> 
 
<?php foreach($questions as $question): ?>
  <div class="question">
    <div class="interested_block">
      <div class="interested_mark" id="mark_<?php echo $question->getId() ?>">
        <?php echo count($question->getInterests()) ?>
      </div>
    </div>
 
    <h2><?php echo link_to($question->getTitle(), 'question/show?id='.$question->getId()) ?></h2>
 
    <div class="question_body">
      <?php echo truncate_text($question->getBody(), 200) ?>
    </div>
  </div>
<?php endforeach; ?>

気づいているかと思いますが、foreachループは元々のlistSuccess.phpと同じです。link_to()truncate_text()関数はsymfonyが提供するテンプレートヘルパーです。前者は同じモジュール内の別アクションへハイパーリンクを作っているもので、後者は質問の内容を200文字以内に切りつめています。link_to()ヘルパーは自動的にロードされていますが、truncate_text()を使うには、Textヘルパーグループの呼び出しを定義しなければなりません。

では、開発環境のトップページを更新して新しいテンプレートを確認してみましょう。

http://askeet/frontend_dev.php/

改善された質問リスト

関心を示しているユーザーの数が各質問の横に表示されています。このキャプチャ画像のような表示をさせるには、main.cssスタイルシートをダウンロードして、askeet/web/css/ディレクトリに配置してください。

クリーンアップ

propel-generate-crudコマンドは必要のないアクションやテンプレートまで生成します。必要のないものは今回で削除してしまいましょう。

askeet/apps/frontend/modules/question/actions/actions.class.phpでの要らないアクションは:

*executeIndex *executeEdit *executeUpdate *executeCreate *executeDelete

askeet/apps/frontend/modules/question/templates/での要らないテンプレートは:

*editSuccess.php

それではまた明日

今日はMVCへの大きな第一歩でした。レイアウト、テンプレート、アクション、そしてPropelオブジェクトモデルでのオブジェクトに触れることにより、MVCベースのアプリケーションでの各レイヤーへも触れることができました。 各レイヤーでの連携がまだ分からなくても心配しないでください。徐々に分かるようになってきます。

いろんなファイルを開いてきましたが、プロジェクト内でどのようにファイルが構成されているか確認するには、symfonyブックの ファイル構造の章 を読んでみてください。

明日はまた良い一日です: ビューをいじったり、より複雑なルーティングポリシーをセットアップしたり、モデルの変更や、より複雑なデータ操作やテーブル間の連携を勉強していきます。

それまでは、よく寝て、今日のチュートリアルのソースを眺めてみてください(release_day_3タグ):

http://svn.askeet.com/tags/release_day_3