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

DAY3: MVCアーキテクチャ

おさらい

2日目でデータモデルに基づいたモデルクラスの作成方法と、フォームモデルの作成方法について簡単に行ってみました。
3日目ではsymfonyのアーキテクチャについて理解していきましょう。

その中でsymfonyのアプリケーション、モジュール、アクションという概念を理解しておく必要があるので、Definitive Guideを読んでおいてください。

ref: コントローラー層について
ref: 日本語版(1.1)

MVCモデル

最初はsymfonyのMVC構造をみてみましょう。
symfonyはMVC構造をもつフレームワークであることは最初に説明したとおりで、簡単にしか説明していませんでしたね。

データに関する操作はページを表示する操作とはまったく別モノになっています。そしてデータ操作に関するものがModelとして
./lib/model/以下のディレクトリに配置されます。そして、ページ表示に関する操作はViewとして./app/<アプリケーション名>/modules/<モジュール名>/templatesに配置されます。
そしてこれらを結びつける役割をするのがControllerで./app/<アプリケーション名>/modules/<モジュール名>/actions/に配置されます。

詳しいことは以下のページで理解してください。
ref: 02-Exploring-Symfony-s-Code
ref: symfonyのコードを探求する

レイアウトを変更する

symfonyのテンプレートはヘッダーやフッターなどを共通化できるように、レイアウトテンプレートとその中のコンテンツ部分のテンプレートの2つで構成されています。

ここではそのレイアウトテンプレートをみてみましょう。レイアウトは./app/frontend/templates/layout.phpになります。

$ more ./apps/frontend/templates/layout.php

PHP:
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  3. <head>
  4.  
  5. <?php include_title() ?>
  6.  
  7. <link rel="shortcut icon" href="/favicon.ico" />
  8.  
  9. </head>
  10. <body>
  11.  
  12. <php echo $sf_content ?>
  13.  
  14. </body>
  15. </html>

見て解るように$sf_contentが各コンテンツのテンプレートを出力する部分になります。それ以外はヘッダーやフッター部分が何もありません。

そして、このページに2つのスタイルシートを外部ファイルで適用したいとします。

./web/css/main.css
./web/css/layout.css

方法は2つあります。1つは設定ファイルで記述する方法。もう1つはview.ymlという設定ファイルで指定する方法で、
もう1つはテンプレート内で

use_stylesheet('スタイルシート名')

のように指定する方法です。

本家チュートリアルではview.ymlで設定するようにしていますが、あえてその方法を行わずに
layout.phpにてスタイルシートをヘルパ関数で読み込むようにします。

これにも理由があります。
それは、わざわざymlに分離して設定しなくても、layout.phpで一元管理できるからです。
例えば、デザイナーと共同作業で、デザイナー側がスタイルシート名を追加、変更したい場合に
layout.phpだけで対応できたほうが解りやすいですよね?

というわけで、本チュートリアルではview.ymlで設定せずにlayout.phpを使うようにしています。

PHP:
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  2. <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
  3. <head>
  4.  
  5. <?php echo include_http_metas() ?>
  6. <?php echo include_metas() ?>
  7.  
  8. <?php use_stylesheet('main') ?>
  9. <?php use_stylesheet('layout') ?>
  10.  
  11. <?php echo include_title() ?>
  12.  
  13. <link rel="shortcut icon" href="/favicon.ico" />
  14.  
  15. </head>
  16. <body>
  17.  
  18.   <div id="header">
  19.     <ul>
  20.       <li><?php echo link_to('about', '@homepage') ?></li>
  21.     </ul>
  22.     <h1><?php echo link_to(image_tag('askeet_logo.gif', 'alt=askeet'), '@homepage') ?></h1>
  23.   </div>
  24.  
  25.   <div id="content">
  26.     <div id="content_main">
  27.       <?php echo $sf_content ?>
  28.       <div class="verticalalign"></div>
  29.     </div>
  30.  
  31.     <div id="content_bar">
  32.       <!-- Nothing for the moment -->
  33.       <div class="verticalalign"></div>
  34.     </div>
  35.   </div>
  36.  
  37. </body>
  38. </html>

また、ここで利用しているlayout.css, main.cssは
http://svn.1ms.jp/public/symfony/askeet12/release_day_3/web/cssからダウンロードできます。

そして、次のページにアクセスしてスタイルシートが適用されていることを確認します。

http://symfony.centos5.localhost/askeet/question


環境について

symfonyでは動作環境を複数設定しておくことができます。
たとえば、本番環境(prod),開発環境(dev),検証環境(test)などです。

では、これらの個別に設定した環境は動作時にどうやって指定しているのでしょうか?

それは、symfonyでアプリケーションを作成したときに作成されるコントローラーのファイル名が
_dev.phpで終わっている場合、そのコントローラーで表示させると開発環境として動作します。
コントローラーファイルは./web/ディレクトリに作成されるファイルです。

ファイルの中を見れば環境名を指定しているのがわかります。

また、index.phpというファイルが一番最初に作成されます。
これは設定でコントローラー名を省略する設定「no_script_name」を指定した場合に利用されるコントローラーです。

今は深く考えずに、環境が複数設定でき、それらはコントローラーファイルを変更することで対応できることを覚えておきましょう。

標準のhomepageのルーティングルールを再定義する

ルーティングについては下記で理解しておいてください。

ref: Links And The Routing System
ref: リンクとルーティングシステム

現在のルーティング設定は標準のままのため、設定を変更しておきましょう。

C:
  1. homepage:
  2.   url:   /
  3.   param: { module: question, action: index }

本家チュートリアルではactionがlistになっていますが、symfony1.2ではlistアクションはindexアクションで実装されているので
indexアクションを指定しておきましょう。

テストデータの作成

本家チュートリアルと同じテストデータを作成してみます。
ただし、ファイル名は複数存在した場合は名前順で読み込まれることを考慮し、先頭に番号をいれておきます。

./data/fixtures/test/001.user.yml
./data/fixtures/test/002.question.yml
./data/fixtures/test/003.interest.yml

ここではルールとしてリレーションの関係で読み込ませたい順番が存在すると思います。
そこで、ファイルの先頭に数字をつけておくようにしておきます。

各ファイルは以下のようになります。

./data/fixtures/test/001.user.yml

C:
  1. User:
  2.   anonymous:
  3.     nickname:   anonymous
  4.     first_name: Anonymous
  5.     last_name:  Coward
  6.  
  7.   fabien:
  8.     nickname:   fabpot
  9.     first_name: Fabien
  10.     last_name:  Potencier
  11.  
  12.   francois:
  13.     nickname:   francoisz
  14.     first_name: François
  15.     last_name:  Zaninotto

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

C:
  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.  
  12.   q2:
  13.     title: What can I offer to my step mother?
  14.     User: anonymous
  15.     body:  |
  16.       My stepmother has everything a stepmother is usually offered
  17.       (watch, vacuum cleaner, earrings, del.icio.us account).
  18.       Her birthday comes next week, I am broke, and I know that
  19.       if I don't offer her something sweet, my girlfriend
  20.       won't look at me in the eyes for another month.
  21.  
  22.   q3:
  23.     title: How can I generate traffic to my blog?
  24.     User: francois
  25.     body:  |
  26.       I have a very swell blog that talks
  27.       about my class and mates and pets and favorite movies.

./data/fixtures/test/003.interest.yml

C:
  1. Interest:
  2.   i1: { User: fabien, Question: q1 }
  3.   i2: { User: francois, Question: q1 }
  4.   i3: { User: francois, Question: q2 }
  5.   i4: { User: fabien, Question: q2 }

見て解るように、SQLで作成するよりも直感的でわかりやすいですね。
そして、外部キーは実際にInsertが実行されないとIDが決まらないのですが、各行単位でつけた名前(fabien, anonymouse, francois)で指定できるところがポイントです。

また、PropelでDoctrineで記述方法に違いがあるのことに注意しなくてはなりません。
本家チュートリアルでは外部キーの参照方法が

C:
  1. i1: { user_id: fabien, question_id: q1 }

のように、カラム名は定義しているものと同じで良いのですが、Doctrineの場合は

C:
  1. i1: { User: fabien, Question: q1 }

のようにモデル名を書かなければなりません。
これを間違えると読み込み時にエラーになってしまいますので注意しましょう。

では、実際に初期データを読み込ませてみます。

$ ./symfony doctrine:data-load --dir="data/fixtures/test"
>> doctrine loading data fixtures from "Array"

では実際に画面をみてみましょう


ここで一つ気づく事があります。
created_at, updated_atってsymfonyで勝手に解釈されるといってたのに値が入っていないことです。

正しくはsymfonyで解釈されるのではなくPropel,DoctrineなどのORマッパによって理解されます。
実はDoctrineでの設定を行っていないため、自動で解釈されないのです。

方法は2つあります。1つはschema.ymlで指定する方法。もう1つはモデルクラスに記述する方法です。
schema.ymlに記述する場合は各モデルにactAs: [Timestampable]を追加するだけです。

./config/doctrine/schema.yml

C:
  1. User:
  2.   tableName: ask_user
  3.   actAs: [Timestampable]
  4.   columns:
  5.     id:
  6.       type: integer(4)
  7.       unsigned: 1
  8.       primary: true
  9.       autoincrement: true

一方、モデルクラスに追加する場合は以下のようにsetUpメソッドにactAsでTimestampableを指定します。
./lib/model/doctrine/モデル名.class.php

PHP:
  1. class User extends BaseUser
  2. {
  3.     public function setUp()
  4.     {
  5.         parent::setup();
  6.         $this->actAs('Timestampable');
  7.     }
  8. }

もし、updated_atを使わない場合は第2引数で次のように渡す事で無効にできます。

./lib/model/doctrine/モデル名.class.php

PHP:
  1. class User extends BaseUser
  2. {
  3.     public function setUp()
  4.     {
  5.         parent::setup();
  6.         $this->actAs('Timestampable', array('updated' => array('disabled' => true)));
  7.     }
  8. }

どちらも追加しなければならないという点では同じです。
本チュートリアルではモデルに追記する方法で行います。
理由はschema.ymlはコマンドを実行することで上書きされる可能性があります。
そのときに改めて記述を追加しなければなりません。

それに対して、モデルに書いておけば、コマンドによって上書きされることはありません。
ちょっとした理由ですが、自分だけがコマンドを実行するとは限らないのでこのような配慮は有用です。

ただし、この変更だけでは十分ではありません。
この点については現時点では深くふれずにまずは動くものを作っていきます。

なので、ここまでの変更をUser,Anser,Question,Interest,Relevancyの各ファイルに行っておきます。

モデルでデータの利用

モデルからどうやってデータアクセスしているかはactionクラスを見ればわかります。

./app/frontend/module/question/actions/actions.class.php

PHP:
  1. public function executeIndex()
  2.   {
  3.     $this->questionList = $this->getQuestionTable()->findAll();
  4.   }
  5.   private function getQuestionTable()
  6.   {
  7.     return Doctrine::getTable('Question');
  8.   }

getQuestionTable()は自身のクラスで定義されており、つなげて書くと以下と同じです。

PHP:
  1. public function executeIndex()
  2.   {
  3.     $this->questionList = Doctrine::getTalbe('Question')->findAll();
  4.   }

正しい設計、コーディングの方針からはこの書き方はよくありません。
これはDoctrineだからではなく本家チュートリアルも同じです。
なぜなら万が一PropelにORマッパを切り替えたい場面が発生したときにこのままではDoctrineクラス名がハードコーディングされているので簡単にはできません。
本当はORマッパの存在をラップするクラスを1つ用意することが理想です。ただし、最初から複雑な構造にするとチュートリアルが複雑になるためここではこのまま説明を続けます。

このことについては後日していきます。

話を戻しましょう。

データからDoctrineを通して取得したデータは$this->questionListにセットされています。
このリソースはテンプレートでは$questionListで利用できることは最初に説明したとおりですね。
では、テンプレートを見てみます。

アクション名がindexですのでテンプレートはindexSuccess.phpになります。

./app/frontend/module/question/templates/indexSuccess.php

PHP:
  1. ...
  2.     <?php foreach ($questionList as $question): ?>
  3.     <tr>
  4.       <td><a href="<?php echo url_for('question/edit?id='.$question['id']) ?>"><?php echo $question['id'] ?></a></td>
  5.       <td><?php echo $question['User'] ?></td>
  6.       <td><?php echo $question['title'] ?></td>
  7.       <td><?php echo $question['body'] ?></td>
  8.       <td><?php echo $question['created_at'] ?></td>
  9.       <td><?php echo $question['updated_at'] ?></td>
  10.     </tr>
  11.     <?php endforeach; ?>
  12.  
  13. <a href="<?php echo url_for('question/edit?id='.$question['id']) ?>"></a>

テンプレートではforeachで取得したquestionの一覧をループ処理しています。
本家チュートリアルのPropelの場合と書き方が異なることがわかります。

実は、Doctrineでも同じ書き方でアクセスすることができます。
PHPらしく配列に慣れている方は配列でもアクセスできるよということですね。

Propelで開発になれてきた方には今までの書き方で書いたほうが混乱しなくてよいですよね。

また、Doctrineでは他の書き方もできるので以下に例をあげておきます。

PHP:
  1. //レコードへのアクセス(nameというカラム)
  2. <?php echo $rec->getName() ?>
  3. <?php echo $rec->name ?>
  4. <?php echo $rec->get('name') ?>
  5. <?php echo $rec['name'] ?>

一覧画面を修正する

では、一覧画面を修正し本家のチュートリアルと同じような画面にしてみます。
異なる点は、リレーションのテーブル参照が複数形(getInterests())でなく単数系(getInterest())になっている点だけですね。

./app/frontend/module/question/templates/indexSuccess.php

PHP:
  1. <?php use_helper('Text') ?>
  2.  
  3. <h1>popular questions</h1>
  4.  
  5. <?php foreach($questionList as $question): ?>
  6.   <div class="question">
  7.     <div class="interested_block">
  8.       <div class="interested_mark" id="mark_<?php echo $question->getId() ?>">
  9.         <?php echo count($question->getInterest()) ?>
  10.       </div>
  11.     </div>
  12.  
  13.     <h2><?php echo link_to($question->getTitle(), 'question/show?id='.$question->getId()) ?></h2>
  14.  
  15.     <div class="question_body">
  16.       <?php echo truncate_text($question->getBody(), 200) ?>
  17.     </div>
  18.   </div>
  19. <?php endforeach; ?>

スタイルシートが適用されていれば次のような画面になるはずです。


ここで、詳細表示のリンク先がquestion/showとなっています。
しかし現在のアクションクラスにはexecuteShowというメソッドは用意されていません。

これはsymfony1.2からのCRUDではshowアクションはオプションで指定する必要があります。

なので、再度オプション(--with-show)を指定してshowアクションを作成しておきましょう。

C:
  1. $./symfony doctrine:generate-crud --with-show frontend question Question

あと、このようにタスクを実行した場合にキャッシュが邪魔する場合があるのでキャッシュをクリアしておきます。
これはもはや呪文です。

$./symfony cc

そして、本家と同じく不要な以下のアクションを削除しておくとします。

./app/frontend/module/question/actions/actions.class.php

* executeEdit
* executeUpdate
* executeCreate
* executeDelete

そして修正用のテンプレートファイルを削除します。

./app/frontend/module/question/templates/editSuccess.php

ここまでのソースは以下のリポジトリからチェックアウトすることができます。

http://svn.1ms.jp/public/symfony/askeet12/tags/release_day_3

では、また明日。

関連するその他の記事

Comments

Leave a Reply