3日目: ~データモデル~

テキストエディタを開いて PHP コードを書きたい方は、今日のチュートリアルで開発を進めることを知ったらしあわせになるでしょう。Jobeet のデータモデルを定義し、データベースとの情報のやりとりに ORM を使い、アプリケーションの最初のモジュールを作ります。symfony が多くの作業を私たちの代わりにやってくれるので、PHP コードをあまり書かなくても十分な機能をもつ Web モジュールが手に入ります。

リレーションシップ~モデル|Model~

昨日のユーザーストーリーではプロジェクトの主要なオブジェクト: jobs (求人)、affiliates (アフィリエイト)、categories (カテゴリ) を詳しく説明しました。下図は対応するエンティティ関係図です:

ER図

ストーリーで説明したカラムに加えて、いくつかのテーブルには created_at フィールドが追加されています。このフィールドには、レコードが生成されたときの現在のシステム時刻が symfony によって自動的にセットされます。updated_at フィールドも同様です。レコードが更新されたときのシステム時刻がセットされます。

~スキーマ~

求人、アフィリエイト、カテゴリを保存するために、当然リレーショナルデータベースが必要となります。

しかし symfony はオブジェクト指向のフレームワークですから、可能ならいつでも~オブジェクト|OOP~として操作したいでしょう。たとえば、データベースからレコードを取得する SQL 文を書くのではなく、オブジェクトを使います。

リレーショナルデータベースの情報をオブジェクトモデルとしてマッピングする必要があります。このマッピングには ORM ツールを使いますが、symfony には2つの ORM (PropelDoctrine) が搭載されています。このチュートリアルでは ##ORM## を使います。

ORM には、関連するクラスを生成するために、テーブルとリレーションシップ (関係) についての定義が必要になります。スキーマの記述には2つの方法があります。既存のデータベースからスキーマを作る方法と、手書きでスキーマを作る方法です。

(Fabforce の Dbdesignerなど) でデータベースをグラフィカルにビルドしたり (DB Designer 4 TO Propel Schema Converter で) schema.xmlを直接生成できます。

データベースがまだ存在していないのと、Jobeet をデータベースエンジンに依存しないようにするために、空の ~config/schema.yml|データベーススキーマ~ ファイルを編集してスキーマファイルを手作業で作りましょう (注: コピー&ペーストする際には➥ の箇所の修正をお忘れなく):

#

データベースがまだ存在していないのと、Jobeet をデータベースエンジンに依存しないようにするために、空の config/doctrine/schema.yml ファイルを編集してスキーマファイルを手作業で作りましょう:

#

SQL 文を書いてテーブルを作る場合は、propel:build-schema もしくは doctrine:build-schema タスクを実行すると、対応する schema.yml 設定ファイルを生成できます:

$ php symfony propel:build-schema

このタスクを実行する前に、databases.yml でデータベースに関する情報を設定しておく必要があります。後のステップでデータベースの設定の仕方を説明します。現時点では、スキーマをビルドする対象のデータベースがわからないので、タスクを実行しようとしても動作しません。

スキーマは、ER 図の内容を YAML フォーマットで記述したものです。

schema.yml ファイルには、すべてのテーブルとカラムの説明を記述します。各カラムの説明の記述には、次の情報を使います:

* type: カラムの型 (booleantinyintsmallintintegerbigintdoublefloatrealdecimalcharvarchar(size)longvarchardatetimetimestampblob および clob) * required: カラムを必須にしたい場合は true にセットする * ~index|データベースインデックス~: カラム用にインデックスを作りたい場合は true にセットする。カラムでユニークインデックスを作りたい場合は unique にセットする * primaryKey: カラムをテーブル用の~主キー~として定義する * foreignTableforeignReference: 別のテーブルへの~外部キー~としてカラムを定義する

~は YAML では null を意味します。カラムの値を~に設定 (idcreated_atupdated_at) すると、symfony はベストな設定を推測します (id に対しては主キー、created_atupdated_at に対してはタイムスタンプ)。

onDelete 属性を使って外部キーの ON DELETE ~ビヘイビア|整合性制約~を定義でき、Propel では CASCADESETNULL および RESTRICT をサポートしています。たとえば、job レコードが削除されると、データベースエンジンによって、またはデータベースエンジンでサポートされていなければ Propel によって、jobeet_category_affiliate テーブルにあるすべての関連レコードが自動的に削除されます。 * type: ~カラムの型~ (booleanintegerfloatdecimalstringarrayobjectblobclobtimestamptimedateenumgzip) * notnull: カラムを必須にしたい場合は true にセットする * unique: カラム用のユニークインデックスを作りたい場合は true にセットする

NOTE onDelete 属性を使って外部キーの ON DELETE ビヘイビアを定義でき、Doctrine では CASCADESET NULL および RESTRICT をサポートしています。たとえば、job レコードが削除されると、データベースエンジンによって jobeet_category_affiliate テーブルにあるすべての関連レコードが自動的に削除されます。

~データベース~

symfony フレームワークは、PDO がサポートするすべてのデータベースをサポートします (MySQL、PostgreSQL、SQLite、Oracle、MSSQLなど)。~PDO~ は PHP に搭載されている~データベース抽象化レイヤー~です。

このチュートリアルでは ~MySQL~ を使いましょう:

$ mysqladmin -uroot -p create jobeet
Enter password: mYsEcret ## The password will echo as ********

使う~データベースエンジン~はご自由に選んでください。私たちに代わって ORM が SQL を生成するので、これから書くコードをデータベースエンジンに合わせることは難しくありません。

symfony で、Jobeet プロジェクト用にこのデータベースを使うよう指定します:

$ php symfony configure:database
  ➥ "mysql:host=localhost;dbname=jobeet" root mYsEcret

configure:database タスクはデータベースにアクセスするために3つの引数: ~PDO の DSN~、ユーザー名およびパスワードを受け取ります。開発サーバーでデータベースにアクセスするパスワードが不要であれば、第3引数を省略します。

configure:database ~タスク~は config/databases.yml ファイルに~データベースコンフィギュレーション~を保存します。タスクを使う代わりに手動で編集することもできます。

コマンドラインでデータベースパスワードを渡すのは手軽ですが~安全ではありません|セキュリティ~。環境にアクセスできる人によっては、config/databases.yml を編集してパスワードを変更するとよいでしょう。もちろん、パスワードを安全に保つために、設定ファイルのアクセスモードも制限すべきです。

~ORM~

schema.yml に記述したデータベース定義を使い、##ORM## の組み込みタスクを利用して、データベースにテーブルを生成するための ~SQL~ を作れます:

SQL を生成するためには、最初にスキーマファイルからモデルを生成しなければなりません。

$ php symfony doctrine:build --model

モデルが生成されたので、SQL を生成してインサートできます。

$ php symfony propel:build --sql

propel:build --sql タスクを実行すると、設定したデータベース用に最適化された SQL 文が data/sql/ ディレクトリに生成されます:

[sql] # data/sql/lib.model.schema.sql からのスニペット CREATE TABLE jobeet_category ( id INTEGER NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, PRIMARY KEY (id), UNIQUE KEY jobeet_category_U_1 (name) )Type=InnoDB; [sql] # data/sql/schema.sql からのスニペット CREATE TABLE jobeet_category (id BIGINT AUTO_INCREMENT, name VARCHAR(255) NOT NULL COMMENT 'test', created_at DATETIME, updated_at DATETIME, slug VARCHAR(255), UNIQUE INDEX sluggable_idx (slug), PRIMARY KEY(id)) ENGINE = INNODB;

実際にデータベース上でテーブルを生成するには、propel:insert-sql タスクを実行する必要があります:

$ php symfony propel:insert-sql

~コマンドライン~ツールに関して、symfony は引数とオプションを受け取ることができます。それぞれのタスクにはヘルプメッセージが組み込まれ help タスクを実行すると表示されます:

$ php symfony help propel:insert-sql

ヘルプメッセージは利用可能なすべての引数とオプションの一覧を表示し、それぞれのデフォルト値、および便利な使い方の例を示します。

ORM を使って、テーブルのレコードをオブジェクトにマッピングする PHP クラスを生成することもできます:

$ php symfony propel:build --model

propel:build --model タスクを実行すると、データベースと情報をやりとりするために使用する PHP ファイルが lib/model/ ディレクトリに生成されます。

生成されたファイルを見てみると、Propel によって~テーブル|テーブル (データベース)~ごとに4つのクラスが生成されていることがわかります。たとえば jobeet_job テーブルの場合は次のようになります:

  • JobeetJob: このクラスのオブジェクトは jobeet_job テーブルの単独の~レコード|データベースのレコード~を表します。デフォルトではこのクラスは空です。
  • BaseJobeetJob: JobeetJob の親クラス。propel:build --model を実行するたびにこのクラスは上書きされるので、すべてのカスタマイズは JobeetJob クラスで行わなければなりません。

  • JobeetJobPeer: このクラスでは、JobeetJob オブジェクトのコレクションを返すスタティックメソッドなどを定義します。デフォルトではこのクラスは空です。

  • BaseJobeetJobPeer: JobeetJobPeer の親クラス。propel:build --model を実行するたびにこのクラスは上書きされるので、カスタマイズは JobeetJobPeer クラスで行わなければなりません。

生成ファイルを見てみると、Doctrine によってテーブルごとに3つのクラスが生成されていることがわかります。たとえば jobeet_job テーブルの場合は次のようになります:

  • JobeetJob: このクラスのオブジェクトは jobeet_job テーブルの単独のレコードを表します。デフォルトではこのクラスは空です。
  • BaseJobeetJob: JobeetJob の親クラス。doctrine:build --model を実行するたびにこのクラスは上書きされるので、すべての~カスタマイズ~は JobeetJob クラスで行わなければなりません。
  • JobeetJobTable: このクラスでは、JobeetJob オブジェクトのコレクションを返すメソッドなどを定義します。デフォルトではこのクラスは空です。

レコードのカラム値は、モデルオブジェクトの~アクセサ~ (get*() メソッド) やミューテータ (set*() メソッド) を使って操作できます:

$job = new JobeetJob();
$job->setPosition('Web developer');
$job->save();
 
echo $job->getPosition();
 
$job->delete();
 

オブジェクトをリンクすることで、直接~外部キー~を定義できます:

$category = new JobeetCategory();
$category->setName('Programming');
 
$job = new JobeetJob();
$job->setCategory($category);
 

propel:build --all タスクは、この章で行ったタスクを一括して実行するショートカットです。Jobeet モデルクラス用のフォームやバリデータを生成するために、このタスクを今実行します:

$ php symfony propel:build --all --no-confirmation

今日の最後にアクションにおけるバリデータを見ることができます。フォームに関しては、10日目に詳しく説明する予定です。

初期データ

データベースにテーブルが作成されました。しかしデータがありません。Web アプリケーションには3種類のデータがあります:

  • 初期データ: アプリケーションを動作させるのに必要なデータ。たとえば、Jobeet ではカテゴリが必要となります。もしカテゴリがなければ誰も仕事を投稿できなくなります。また、backend にログインできる admin ユーザーが必要になります。

  • テストデータ: アプリケーションのテストに必要です。開発者にとって、ストーリーどおりに Jobeet が動作するのを確認するためにテストを書きます。自動化テストを書くのが1番よい方法です。テストを実施するたびに~テストデータ~でデータベースをクリアする必要があります。

  • ユーザーデータ: アプリケーションの運用中にユーザーによって作られたデータ

symfony がデータベースのテーブルを作成するたびに、すべてのデータは失われます。初期データのあるデータベースを生成するには、PHP スクリプトを作るか、もしくは mysql プログラムで SQL ステートメントを実行します。しかしこれはよくある要件なので、symfony ではよりよい方法を用意しました: data/fixtures/ ディレクトリに YAML ファイルを作り、propel:data-load タスクを使って YAML ファイルのデータをデータベースにロードします。

最初に、次の~フィクスチャ~ファイルを作ります:

[yml] # data/fixtures/010_categories.yml JobeetCategory: design: { name: Design } programming: { name: Programming } manager: { name: Manager } administrator: { name: Administrator }

---
# data/fixtures/020_jobs.yml
JobeetJob:
  job_sensio_labs:
    category_id:  programming
    type:         full-time
    company:      Sensio Labs
    logo:         sensio-labs.gif
    url:          http
    position:     Web Developer
    location:     Paris, France
    description:  |
      You've already developed websites with symfony and you want to:
      work with Open-Source technologies. You have a minimum of 3:
      years experience in web development with PHP or Java and you:
      wish to participate to development of Web 2.0 sites using the:
      best frameworks available.:
    how_to_apply: |
      Send your resume to fabien.potencier [at] sensio.com:
    is_public:    true
    is_activated: true
    token:        job_sensio_labs
    email:        [email protected]
    expires_at:   2010-10-10

  job_extreme_sensio:
    category_id:  design
    type:         part-time
    company:      Extreme Sensio
    logo:         extreme-sensio.gif
    url:          http
    position:     Web Designer
    location:     Paris, France
    description:  |
      Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do:
      eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut:
      enim ad minim veniam, quis nostrud exercitation ullamco laboris:
      nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor:
      in reprehenderit in.:

      Voluptate velit esse cillum dolore eu fugiat nulla pariatur.:
      Excepteur sint occaecat cupidatat non proident, sunt in culpa:
      qui officia deserunt mollit anim id est laborum.:
    how_to_apply: |
      Send your resume to fabien.potencier [at] sensio.com:
    is_public:    true
    is_activated: true
    token:        job_extreme_sensio
    email:        [email protected]
    expires_at:   2010-10-10

[yml] # data/fixtures/categories.yml JobeetCategory: design: name: Design programming: name: Programming manager: name: Manager administrator: name: Administrator

---
# data/fixtures/jobs.yml
JobeetJob:
  job_sensio_labs:
    JobeetCategory: programming
    type:         full-time
    company:      Sensio Labs
    logo:         sensio-labs.gif
    url:          http
    position:     Web Developer
    location:     Paris, France
    description:  |
      You've already developed websites with symfony and you want to work:
      with Open-Source technologies. You have a minimum of 3 years:
      experience in web development with PHP or Java and you wish to:
      participate to development of Web 2.0 sites using the best:
      frameworks available.:
    how_to_apply: |
      Send your resume to fabien.potencier [at] sensio.com:
    is_public:    true
    is_activated: true
    token:        job_sensio_labs
    email:        [email protected]
    expires_at:   '2010-10-10'

  job_extreme_sensio:
    JobeetCategory:  design
    type:         part-time
    company:      Extreme Sensio
    logo:         extreme-sensio.gif
    url:          http
    position:     Web Designer
    location:     Paris, France
    description:  |
      Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do:
      eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut:
      enim ad minim veniam, quis nostrud exercitation ullamco laboris:
      nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor:
      in reprehenderit in.:

      Voluptate velit esse cillum dolore eu fugiat nulla pariatur.:
      Excepteur sint occaecat cupidatat non proident, sunt in culpa:
      qui officia deserunt mollit anim id est laborum.:
    how_to_apply: |
      Send your resume to fabien.potencier [at] sensio.com:
    is_public:    true
    is_activated: true
    token:        job_extreme_sensio
    email:        [email protected]
    expires_at:   '2010-10-10'

job フィクスチャファイルは2つの画像を参照します。(http://www.symfony-project.org/get/jobeet/sensio-labs.gifhttp://www.symfony-project.org/get/jobeet/extreme-sensio.gif) からダウンロードして web/uploads/jobs/ ディレクトリに設置してください。

フィクスチャファイルは YAML で記述され、一意性のある名前でラベルづけされたモデルオブジェクトを定義できます。たとえば、job_sensio_labsjob_extreme_sensio のラベルがつけられた2つの求人を定義しました。このラベルは、~主キー~を定義しなくても関連オブジェクトをリンクするためにとても便利です (主キーにはよく auto-increment が使われ、値をセットできません)。たとえば、job の job_sensio_labs カテゴリは programming で、これは 'Programming' カテゴリを示すラベルです。

YAML ファイルにおいて、(job フィクスチャファイルの description カラムのように) 文字列が改行を含むとき、文字列がいくつかの行に分割されることを示すためにパイプ (|) を利用します。

1つのフィクスチャフィルで、1つもしくは複数のモデルからオブジェクトを格納できますが、今回は Jobeet フィクスチャとして、1つのモデルごとに1つのファイルを作るようにしましょう。

ファイルの名前の~プレフィックス~に数字がついてることに注意が必要です。これはデータロードの順序をコントロールする簡単な方法です。プロジェクトで将来、新しいフィクスチャファイルをいくつか挿入することになった場合は、現在使ってる番号の間で番号づけをするだけです。 NOTE Propel では、ファイルがロードされる順序を決定するためにフィクスチャファイルのプレフィックスを番号にする必要がありました。Doctrine ではこのような必要はありません。外部キーが適切に設定されていることを確認するために、すべてのフィクスチャがロードされた後、正しい順序で保存されます。

フィクスチャファイルにおいて、すべてのカラムの値を定義する必要はありません。カラムの値が定義されていない場合、symfony はデータベーススキーマで定義されたデフォルト値を使います。symfony では ##ORM## を使用してデータベースにデータをロードするので、すべての組み込みの~ビヘイビア|ビヘイビア (ORM)~(たとえば created_at もしくは updated_at カラムを自動的に設定するもの) とモデルクラスに追加されたカスタムビヘイビアが有効になります。

データベースに初期データをロードするには propel:data-load タスクを実行するだけです:

$ php symfony propel:data-load

propel:build --all --and-load タスクは、propel:data-load タスクが後に続く propel:build --all --and-load タスク用のショートカットです。

スキーマから可能なすべてを生成するには、doctrine:build --all --and-load タスクを実行します。このタスクによってフォーム、フィルタ、モデルを生成し、データベースを削除してからすべてのテーブルを再作成します。

$ php symfony doctrine:build --all --and-load

ブラウザ上での動作確認

たくさんの CLI を使いましたが、あまりおもしろいものではありませんね。とりわけ Web プロジェクトとしては。ようやく、データベースと情報をやり取りする Web ページを作る準備ができました。

求人の一覧の表示方法、既存の求人を編集する方法、求人を削除する方法を見てみましょう。1日目で説明したように、symfony プロジェクトは複数のアプリケーションで構成されます。それぞれの~アプリケーション~は~モジュール~ (module) に分割されます。1つのモジュールは自己完結した PHP コードの集まりで、アプリケーションの機能 (たとえば API モジュール)、もしくはユーザーがモデルオブジェクトで実行可能な操作の一式 (たとえば job モジュール) をあらわします。

symfony では、指定したモデル用の、基本的な操作機能を提供するモジュールを自動生成できます:

$ php symfony propel:generate-module --with-show
  ➥ --non-verbose-templates frontend job JobeetJob

propel:generate-module タスクで、frontend アプリケーションに JobeetJob モデルの job モジュールが生成されます。たいていの symfony タスクと同じように、ファイルとディレクトリは apps/frontend/modules/job/ ディレクトリの元に作られます:

ディレクトリ 説明
actions/ モジュールのアクション
templates/ モジュールのテンプレート

actions/actions.class.php ファイルは job モジュールに対して利用可能なすべての~アクション~を定義します:

アクションの名前 説明
index テーブルのレコードを表示する
show 任意のレコード用のフィールドと値を表示する
new 新しいレコードを作成するフォームを表示する
create 新しいレコードを作成する
edit 既存のレコードを編集するフォームを表示する
update ユーザーが投稿した値に応じてレコードを更新する
delete 渡されたレコードをテーブルから削除する

ブラウザで job モジュールをテストできます:

 http://jobeet.localhost/frontend_dev.php/job

job モジュール

求人情報を編集しようとすると、カテゴリオブジェクトのテキスト表現が必要なため、例外が表示されます。PHP オブジェクトのテキスト表現は~__toString()~ というマジックメソッドを使って定義できます。カテゴリレコードのテキスト表現は、JobeetCategory モデルクラスで定義します:

// lib/model/JobeetCategory.php
class JobeetCategory extends BaseJobeetCategory
{
  public function __toString()
  {
    return $this->getName();
  }
}
 

これで、symfony がカテゴリのテキスト表現を必要とするたびに ~__toString()~ メソッドが呼び出され、カテゴリ名が返されます。ほかのモデルクラスでも必要となるので、すべてのモデルに対して __toString() を定義しましょう: 求人情報を編集しようとすると、Category id のドロップダウンに、すべてのカテゴリ名のリストが表示されていることがわかります。それぞれのオプションの値は __toString() メソッドから取得されます。

Doctrine は titlenamesubject など説明用のカラム名を推測して、基本的な __toString() メソッドを提供しようとします。何かをカスタマイズしたい場合は、次のように独自の __toString() メソッドを追加する必要があります。JobeetCategory モデルでは、jobeet_category テーブルの name カラムを利用して __toString() メソッドを推測できます。

 
 

// lib/model/JobeetJob.php // lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function __toString() { return sprintf('%s at %s (%s)', $this->getPosition(), ➥ $this->getCompany(), $this->getLocation()); } }

// lib/model/JobeetAffiliate.php // lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate { public function __toString() { return $this->getUrl(); } }

これで求人情報の作成と編集が使えるようになりました。必須項目を空にしたり、無効な日付を入力して保存してみてください。 symfony によって、データベーススキーマから基本的なバリデーションルールが自動生成されていることがわかります。

バリデーション

明日お会いしましょう

今日はここまでです。導入部分で予告しました。今日は、PHP コードをほとんど書いていませんが、job モデル用の web モジュールに取り組み、調整とカスタマイズする準備はできています。PHP コードがないことは、バグが存在しないことも意味することを覚えておいてください!

まだエネルギーが残っていたら、モジュールとモデル用に生成されたコードを読んでどのように動くのか理解を深めてください。 そうでなければ、気にせずによく寝てください。明日は、Web フレームワークでもっともよく使われるパラダイムの1つである MVC デザインパターンについて話します。

ORM