復習
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.css
とlayout.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.php
とindex.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_at
andupdated_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リクエストは同じオブジェクト、設定にアクセスする事が必要であり、バッチもそれに漏れないからです。
データのインポート
バッチの大元が準備できたところで、仕事を割り当てましょう。このバッチでは:
- YAMLファイルを読み込む
- Propelオブジェクトのインスタンスを作る
- データベース上のテーブルにレコードを追加する
なんだか難しそうですが、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
バンザーイ。データが反映されてますね。
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; ?>
ステップごとで見てみるとこのようになります:
- アクションは、空の抽出基準での
Question
テーブルレコードを引き出します。 - つまりはQuestionの全てのデータ - レコードのリストは配列(
$questions
)に入れられ、テンプレートに渡されます - テンプレートはアクションから渡された全ての質問データを順に実行します
- テンプレートは各レコードの各項目を表示します
->getId()
、->getTitle()
、->getBody()
のようなメソッドは、事前にsymfony propel-build-model
コマンドの呼び出し (昨日 の覚えてます?)生成されており、これらがid
、title
、body
などの項目データを取得します。これらは標準的なゲッターで、接頭辞の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
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.