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

2日目:データモデルのセットアップ

復習

このチュートリアルの1日目では、symfonyフレームワークをインストールして、新しいアプリケーションと開発環境をセットアップする方法と、バージョン管理システムを使ってコードを安全に管理する方法について学びました。なお、初日に作ったアプリケーションのコードは、askeetのSVNリポジトリから入手できます:

http://svn.askeet.com/ 

2日目の目的は、最終的に必要となる機能を定義し、データモデルを構築し、コーディングを始めることです。ORM(object-relational mapping)の生成およびこれを使用してアプリケーションのscaffoldingでデータベースのレコードを対話的に作成、読み取りと更新することも含まれます。

かなり分量が多くなります。それでは 始めましょう。

プロジェクトを公開する

何を知りたいですか?というのは、とても興味深い質問です。世の中には、多くの興味深い質問があります。たとえば:

  • 今夜、彼女と一緒に何をすべきか?
  • どうやればブログのアクセス数を増やすことができるか?
  • 最良のWebアプリケーションのフレームワークは何か?
  • パリで最も手頃なレストランはどこか?
  • 人生、宇宙、すべての答えは?

これらの質問は、答えが1つに決まりません。どの答えがもっとも正しいと考えるかは人それぞれでしょう。実際、1つの答えしかない質問は、たいてい全くおもしろくなく(たとえば、1 + 1 とか)、Web上で解決することができます。それは不公平です。

askeetは、人々が自分の質問に対する答えを見つけることを助けるために作られたWebサイトです。しかし、誰がそんな扱いにくい問題に答えるのでしょうか?サイトにアクセスした人全員です。そして、全員が他の人の答えを評価することができるので、人気のある答えは多くの目に触れるようになります。質問の数が増加するに従って、カテゴリとサブカテゴリでそれらを分類するのは不可能になります。そのため、del.icio.usのように質問者は好きなように単語をタグづけすることができます。タグの人気度はタグクラウドの変化で表されることになるでしょう。特定の質問の答えを追跡したい人は、質問のRSS配信を読むこともできます。これらのすべての機能性が洗練され、軽量でなければならないので、新しいページを必要としないインタラクションは、AJAXにするべきです。また、スパムとして報告された質問と答えを管理することや、管理者が奨励する質問を勧めるために、バックエンドが必要です。

次に、こう尋ねるかもしれません:すでに同じようなWebサイトを見たことがなかったかな?…もし、実際にこのようなサイトを見たことがあるなら、それは私たちの失敗ですが、もしfaqtseHowAsk Jeevesや、それに類するWebサイト、つまり、共同作業的に答えるのではなく、AJAXを使用しておらず、RSSやtagが提供されていないものを考えているなら、Askeetはそれと同類のWebサイトではありません。ここではWEB2.0アプリケーションを扱っているのです。

askeetについて重要なことは、これがWebサイトで提供されるだけではなく、誰でもダウンロードして、家や会社のイントラネットにインストールし、カスタマイズして、機能を追加できるアプリケーションであるということです。ソースコードはオープンソースライセンスでリリースされます。上司はナレッジ・マネジメントシステムを探していませんか?車の修理に関して学んだすべての裏技を記録したくありませんんか?Webサイトのよくある質問の部分を発展させたくはありませんか?もう探す必要はありません。Askeetがあります。これは、私たちのクリスマス・プレゼントなのです。

どこから始めるか

では、どのようにsymfonyアプリケーションを始めたらよいでしょう?すべてあなた次第です。XPに慣れているなら、ストーリーを書き、ゲームを企画し、ペアプログラミングのパートナーを探せばよいのです。また、あなたがUMLが好きならすべてのオブジェクト、ステート、インタラクションなどのスケッチと共にWebサイトの詳細な仕様書を書けばよいのです。

しかし、このチュートリアルは、一般的なアプリケーション開発に関するものではないので、基本的なリレーショナルデータモデルから始めて、機能をひとつずつ加えるつもりです。何も出力しない膨大な書きかけのコードではなく、毎日、使える状態のアプリケーションを作ることを目的とします。理想としては、付け加えられるそれぞれの機能のためにユニットテストを書くべきですが、正直に言ってそれほどの時間はありません。チュートリアルのうち、1日がユニットテストに使われることになるので、読み続けてください。

このプロジェクトでは、整合性制約とトランザクションを利用するため、InnoDBテーブルタイプを持ったMySQLデータベースを使います。データベースのセットアップを避けるため、第一歩としてSQLiteデータベースを使用することもできます。databases.ymlファイルを、ほんの少し書き換える必要がありますが、練習として調べてもらうため、ここではその方法については触れません。

データモデル

リレーショナルモデル

まず、'question'と'answer'テーブルが必要でしょう。'user'テーブルも必要でしょうし、質問に対するユーザーの関心を'interest'テーブルに、人が評価した答えの妥当性を'relevancy'テーブルに格納します。

ユーザーは、質問を追加したり、回答の妥当性を評価したり、質問への関心を宣言する時に認証される必要があります。回答を追加する時には、ユーザーの認証は必要はないでしょうが、よい回答をしたユーザーを区別することができるように、回答とユーザーはつねにリンクされるべきでしょう。認証なしで入力された答えは、'Anonymous Coward(匿名の臆病者)'と呼ばれる一般的なユーザーの回答として示されます。Entity関係図を見れば、より簡単に分かります:

ERD

各テーブルでcreated_atフィールドを宣言したことに注目してください。symfonyはこのようなフィールドを認識して、レコードが生成された時間をシステム時間にセットします。updated_atフィールドについても同じです。レコードを更新するたびにシステム時間がセットされます。

schema.xml

symfonyに読み込ませるために、リレーショナルモデルを設定ファイルに変換しなければなりません。これがaskeet/config/ディレクトリに設置されるschema.xmlもしくはschema.ymlファイルの目的です。symfonyはXMLもしくはYAMLフォーマットのスキーマをサポートします。

このファイルを書く方法は2つあります:1つは、手書きすること(筆者はこれを好みます)で、もう1つは、既存のデータベースから書き出すことです。では、最初の方法を見ていきましょう。まず、インストールされたサンプルのデフォルトの名前を変える必要があります:

最初に、デフォルトでインストールされたYAMLファイルのサンプルを削除する必要があります:

$ svn delete config/schema.yml

Propelの公式サイトで詳細に説明されていますが、schema.xmlの構文は比較的簡単です: <table>タグが<column><foreign-key>、および <index>タグを含んだ、XMLファイルになっています。一度書いてみれば、すべて。以下は、書けるようになるでしょう。前に説明したリレーショナルモデルに対応するschema.xmlです:

<?xml version="1.0" encoding="UTF-8"?>
 <database name="propel" defaultIdMethod="native" noxsd="true">
   <table name="ask_question" phpName="Question">
     <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />
     <column name="user_id" type="integer" />
     <foreign-key foreignTable="ask_user">
       <reference local="user_id" foreign="id"/>
     </foreign-key>
     <column name="title" type="longvarchar" />
     <column name="body" type="longvarchar" />
     <column name="created_at" type="timestamp" />
     <column name="updated_at" type="timestamp" />
   </table>
 
   <table name="ask_answer" phpName="Answer">
     <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />
     <column name="question_id" type="integer" />
     <foreign-key foreignTable="ask_question">
       <reference local="question_id" foreign="id"/>
     </foreign-key>
     <column name="user_id" type="integer" />
     <foreign-key foreignTable="ask_user">
       <reference local="user_id" foreign="id"/>
     </foreign-key>
     <column name="body" type="longvarchar" />
     <column name="created_at" type="timestamp" />
   </table>
 
   <table name="ask_user" phpName="User">
     <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" />
     <column name="nickname" type="varchar" size="50" />
     <column name="first_name" type="varchar" size="100" />
     <column name="last_name" type="varchar" size="100" />
     <column name="created_at" type="timestamp" />
   </table>
 
   <table name="ask_interest" phpName="Interest">
     <column name="question_id" type="integer" primaryKey="true" />
     <foreign-key foreignTable="ask_question">
       <reference local="question_id" foreign="id"/>
     </foreign-key>
     <column name="user_id" type="integer" primaryKey="true" />
     <foreign-key foreignTable="ask_user">
       <reference local="user_id" foreign="id"/>
     </foreign-key>
     <column name="created_at" type="timestamp" />
   </table>
 
   <table name="ask_relevancy" phpName="Relevancy">
     <column name="answer_id" type="integer" primaryKey="true" />
     <foreign-key foreignTable="ask_answer">
       <reference local="answer_id" foreign="id"/>
     </foreign-key>
     <column name="user_id" type="integer" primaryKey="true" />
     <foreign-key foreignTable="ask_user">
       <reference local="user_id" foreign="id"/>
     </foreign-key>
     <column name="score" type="integer" />
     <column name="created_at" type="timestamp" />
   </table>
 
 </database>

実際のデータベース名にかかわらず、このファイルの中ではデータベース名がpropelにセットされていることに注意してください。このパラメータは、Propelレイヤーをsymfonyフレームワークに接続するために使用されます。データベースの実際の名前はdatabases.yml構成ファイルで定義します(以下を見てください)。

既存のデータベースがあれば、schema.xmlを作成する別の方法もあります。グラフィカルなデータベースデザインツールに慣れていれば、生成されたMySQLデータベースからスキーマを作ると良いでしょう。それを行う前に、askeet/config/ディレクトリに設置されるpropel.iniファイルを編集して、データベースへの接続設定を入力する必要があります:

propel.database.url = mysql://username:[email protected]/databasename

usernamepasswordlocalhostdatabasenameはデータベースの実際の接続設定に置き換えてください。データベースからschema.xmlを作成するため、propel-build-schemaコマンドをaskeet/ディレクトリにて実行してください:

$ symfony propel-build-schema

note

いくつかのツールでは、グラフィカルな環境でデータベースを構築でき (例:Fabforce's Dbdesigner)、その上、schema.xmlを直接生成することができます( DB Designer 4 TO Propel Schema Converterを利用)。

schema.xmlファイルを作る代わりに、YAMLスキーマフォーマットを使用してschema.ymlファイルを作成することもできます:

propel:
  _attributes:   { noXsd: false, defaultIdMethod: none, package: lib.model }
 
  ask_question:
    _attributes: { phpName: Question, idMethod: native }
    id:          { type: integer, required: true, primaryKey: true, autoIncrement: true }
    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id }
    title:       { type: longvarchar }
    body:        { type: longvarchar }
    created_at:  ~
    updated_at:  ~
 
  ask_answer:
    _attributes: { phpName: Answer, idMethod: native }
    id:          { type: integer, required: true, primaryKey: true, autoIncrement: true }
    question_id: { type: integer, foreignTable: ask_question, foreignReference: id }
    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id }
    body:        { type: longvarchar }
    created_at:  ~
 
  ask_user:
    _attributes: { phpName: User, idMethod: native }
    id:          { type: integer, required: true, primaryKey: true, autoIncrement: true }
    nickname:    { type: varchar(50), required: true, index: true }
    first_name:  varchar(100)
    last_name:   varchar(100)
    created_at:  ~
 
  ask_interest:
    _attributes: { phpName: Interest, idMethod: native }
    question_id: { type: integer, foreignTable: ask_question, foreignReference: id, primaryKey: true }
    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id, primaryKey: true }
    created_at:  ~
 
  ask_relevancy:
    _attributes: { phpName: Relevancy, idMethod: native }
    answer_id:   { type: integer, foreignTable: ask_answer, foreignReference: id, primaryKey: true }
    user_id:     { type: integer, foreignTable: ask_user, foreignReference: id, primaryKey: true }
    score:       { type: integer }
    created_at:  ~

オブジェクトモデルのビルド

InnoDBエンジンを使用するには、askeet/config/ディレクトリのpropel.iniファイルに1行追加しなければなりません:

propel.mysql.tableType = InnoDB

schema.xmlがいったん構築されると、リレーショナルモデルに基づいたオブジェクトモデルを作成することができます。symfonyでは、O/Rマッピングは、Propelによって制御されますが、symfonyコマンドにカプセル化されます:

$ symfony propel-build-model

このコマンドは、askeetプロジェクトのrootディレクトリから呼びだす必要があり、標準のアクセサ( ->get()、->set()メソッド)とともに、schemaで定義されたテーブルに対応するクラスを生成します。生成コードはaskeet/lib/model/om/ディレクトリにあります。1テーブルあたり2つのクラスがあることについては、symfony bookの モデルの章を参照してください。これらのクラスは、build-modelを実行するたびにオーバーライドされることになりますが、これはこのプロジェクトでは何回も実行されます。したがって、オブジェクトモデルにメソッドを加える必要がある場合、askeet/lib/model/ディレクトリにあるクラスを変更しなければなりません。これらのクラスは /omディレクトリ下のクラスを継承しています。

データベース

接続

ここまでの間に、symfonyはデータベースのオブジェクトモデルを持つようになりました。ここで、プロジェクトをMySQLデータベースに関連づけましょう。まず最初に、MySQLにデータベースを作成しなければなりません:

$ mysqladmin -u youruser -p create askeet

askeet/config/databases.yml設定ファイルを開いてください。初めての場合、設定ファイルが YAMLで書かれていることに気づくでしょう。構文は非常に簡単ですが、YAMLファイルには1つの重要な決まりごとがありますタブを決して使わず、必ずスペースを使うことです。それがわかれば、ファイルを編集し、all:カテゴリの下にあるデータベースの接続設定を入力する準備ができています:

all:
  propel:
    class:          sfPropelDatabase
    param:
      phptype:  mysql
      host:     localhost
      database: askeet
      username: youruser
      password: yourpasswd

symfonyの設定とYAMLファイルに関してさらに知りたい場合、symfony bookの設定の実践の章を読んでください。

構築

手で schema.xmlファイルを書かなかった場合は、データベースに、対応するテーブルをすでに持っている可能性があります。その場合、この部分をとばすことができます。

キーボードファンの方に驚きのニュースがあります。MySQLデータベースに、テーブルとコラムを作成する必要はありません。schema.xmlですでに一度行っているので、symfonyがあなたの代わりにSQLステートメントを構築してくれます。

$ symfony propel-build-sql

このコマンドはaskeet/data/sql/ディレクトリにlib.model.schema.sqlを作成します。MySQLのSQLコマンドとして使用してください:

$ mysql -u youruser -p askeet < data/sql/lib.model.schema.sql

代わりに、propel-insert-sqlタスクを使うこともできます:

$ symfony propel-insert-sql

CRUDを通したテストデータへのアクセス

これまで行ったことがきちんと役に立っていることを確認するのは、うれしいものです。これまで、ブラウザを少しも使いませんでしたが、私たちはWebアプリケーションを構築することになっています… それではここで、基本的なsymfonyテンプレートのセットとアクションを作成し、'question'テーブルのデータを操作しましょう。これで、いくつかの質問を作成して、それらを表示することができるようになります。

askeet/ディレクトリで、以下のように入力してください:

$ symfony propel-generate-crud frontend question Question

これで、frontendアプリケーションのquestionモジュールに、QuestionPropelのオブジェクトモデルに基づいて、基本的なCreate、Retrieve、Update、Deleteアクション(CRUDと略される)を持った足場を作成されます。なお、勘違いしないでしないでください。足場は完成されたアプリケーションではなく、新機能をつけ、ビジネスルールを付け加え、ルック&フィールをカスタマイズできる基本構造です。

CRUDジェネレータによって作成されたアクションのリストは以下の通りです。

アクション名 説明
list テーブルの全レコードを表示します
index リストへ転送する
show 任意のレコードのすべてのフィールドを示します
edit 新しいレコードを作成するか、既存のものを編集するためのフォームを表示します
update リクエストの任意のパラメータに従ってレコードを変更し、showに転送します
delete 与えられたレコードをテーブルから削除します

アクションについて、詳しくは、symfony bookのscaffoldingの章を参照してください。

askeet/apps/frontend/modules/ディレクトリでは、新しいquestionモジュールに注意して、ソースを眺めてください。

自動的に読み込む必要がある新しいクラスを作るときは、設定キャッシュをクリアする(オートロード設定のキャッシュを再読み込みする)ことを忘れないようにしてください。

$ symfony cc frontend config

以下のアドレスで、オンラインでテストできます。

http://askeet/question

新しいレコードを作成する List all records

遊んでみてください。いくつかの質問を加え、編集し、表示し、削除してみてください。作動した場合、オブジェクトモデルが正しく、データベースとの接続が正しく、データベースのリレーショナルモデルとsymfonyのオブジェクトモデルの間のマッピングが正しいことを意味します。良い機能テストになります。

ではまた、明日

PHPを一行も書きませんでしたが、使用できる基本的なアプリケーションが手に入りました。2日目としては悪くはありません。明日、質問のリストを表示し、訪問者を歓迎するホームページを持つためにコードを書き始めましょう。また、バッチプロセスを使用してデータベースにテストデータを追加し、モデルを拡張する方法を学びましょう。

アプリケーションが何をするのかわかったことで、追加機能を思いついたかもしれませんん。askeetメーリングリストに気軽に提案してください。評判のいいアイディアはsymfonyアドベントカレンダーの21日目に付け加えられるでしょう。

遠慮なく、以下のアドレスで今日のチュートリアル(タグ release_day_2)のソースを眺めてください。

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