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

3日目: データモデル

Symfony version
Language
ORM

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

sfDoctrinePluginを有効にする

この記事を読んでいるのであれば、ORMとしてPropelの代わりにDoctrineを選んでJobeetチュートリアルを学習することを決めたことになります。 最初にやるべきことはsfDoctrinePluginを有効にし、sfPropelPluginを無効にするだけです。 config/ProjectConfiguration.class.phpの次のコードで簡単に実現できます:

public function setup()
{
  $this->enablePlugins(array('sfDoctrinePlugin'));
  $this->disablePlugins(array('sfPropelPlugin'));
}

デフォルトですべてのプラグインを有効にしたいのであれば、次のようにできます:

public function setup()
{
  $this->enableAllPluginsExcept(array('sfPropelPlugin', 'sfCompat10Plugin'));
}

note

この変更を行うと、後でsfDoctrineDatabaseを使うようにconfig/databases.ymlファイルを設定するまでエラーが表示されます。

これらの変更をした後で必ずキャッシュをクリアします。

$ php symfony cc

チュートリアルの後半で見るように、それぞれのプラグインはアセット(画像、スタイルシート)を埋め込むことができます。 新しいプラグインをインストールもしくは有効にしたら、plugin:publish-assetsタスクでアセットをインストールします:

$ php symfony plugin:publish-assets

web/sfDoctrinePluginディレクトリも削除してください:

$ rm web/sfDoctrinePlugin

tip

Propelの代わりにDoctrineを使う場合、他に推奨されることは、Propelへの参照がないクリーンなインストール状態を保つためにconfig/propel.iniconfig/schema.ymlを削除することです

$ rm config/propel.ini
$ rm config/schema.yml

リレーションモデル

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

ER図

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

スキーマ

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

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

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

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

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

tip

プロジェクトにconfig/doctrine/ディレクトリがまだ存在しなければ手作業で作成する必要があります:

$ mkdir config/doctrine
# config/doctrine/schema.yml
---
JobeetCategory:
  actAs: { Timestampable: ~ }
  columns:
    name: { type: string(255), notnull: true, unique: true }
 
JobeetJob:
  actAs: { Timestampable: ~ }
  columns:
    category_id:  { type: integer, notnull: true }
    type:         { type: string(255) }
    company:      { type: string(255), notnull: true }
    logo:         { type: string(255) }
    url:          { type: string(255) }
    position:     { type: string(255), notnull: true }
    location:     { type: string(255), notnull: true }
    description:  { type: string(4000), notnull: true }
    how_to_apply: { type: string(4000), notnull: true }
    token:        { type: string(255), notnull: true, unique: true }
    is_public:    { type: boolean, notnull: true, default: 1 }
    is_activated: { type: boolean, notnull: true, default: 0 }
    email:        { type: string(255), notnull: true }
    expires_at:   { type: timestamp, notnull: true }
  relations:
    JobeetCategory: { onDelete: CASCADE, local: category_id, foreign: id, foreignAlias: JobeetJobs } 
 
JobeetAffiliate:
  actAs: { Timestampable: ~ }
  columns:
    url:       { type: string(255), notnull: true }
    email:     { type: string(255), notnull: true, unique: true }
    token:     { type: string(255), notnull: true }
    is_active: { type: boolean, notnull: true, default: 0 }
  relations:
    JobeetCategories:
      class: JobeetCategory
      refClass: JobeetCategoryAffiliate
      local: affiliate_id
      foreign: category_id
      foreignAlias: JobeetAffiliates
 
JobeetCategoryAffiliate:
  columns:
    category_id:  { type: integer, primary: true }
    affiliate_id: { type: integer, primary: true }
  relations:
    JobeetCategory:  { onDelete: CASCADE, local: category_id, foreign: id }
    JobeetAffiliate: { onDelete: CASCADE, local: affiliate_id, foreign: id }

tip

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

$ php symfony doctrine:build-schema

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

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

sidebar

YAMLフォーマット

YAMLの公式サイトによれば、YAMLは"人間にフレンドリーな、すべてのプログラミング言語用のデータシリアライゼーション標準"と説明されています。

言い換えると、YAMLはデータ(文字列、整数、日付、配列およびハッシュ)を記述するためのシンプルな言語です。

YAMLにおいて、構造はインデントで示され、連番のアイテムはダッシュで示され、マップのキー/値の組はコロンによって分離されます。 また、YAMLには同じ構造をより短い行で記述するための省略記法もあります。 配列は[]で、ハッシュは{}で明示的に示されます。

まだYAMLに慣れていなければ、使い始めるよい機会です。 symfonyフレームワークでは広範囲にわたって、設定ファイルでYAMLフォーマットを使っています。

YAMLファイルを編集する際に覚える必要がある大切なことが1つあります: インデントは1つ以上のスペースで行わなければならず、タブは使ってはなりません

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

  • 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 ********

note

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

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

デフォルトのconfig/~databases.yml~には、Propelを参照する接続情報が格納されています。 今回はDoctrineを使うので、このファイルを削除し、Doctrine用のconfig/databases.ymlを再生成します。

$ rm config/databases.yml

次のコマンドを実行すると、Doctrine用の新しいデータベース設定ファイルが生成されます:

$ php symfony configure:database --name=doctrine --class=sfDoctrineDatabase "mysql:host=localhost;dbname=jobeet" root mYsEcret

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

note

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

caution

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

ORM

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

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

$ php symfony doctrine:build-model

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

$ php symfony doctrine:build-sql

doctrine:build-sqlタスクを実行すると、設定したデータベース用に最適化されたSQLステートメントがdata/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;

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

$ php symfony doctrine:insert-sql

tip

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

$ php symfony help doctrine:insert-sql

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

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

$ php symfony doctrine:build-model

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

生成されたファイルを見てみると、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);

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

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

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

あとで見ることになりますが、symfonyはPHPクラスをオートロードします。 つまり、コードの中でrequireを使う必要がないということを意味します。 これはsymfonyが開発者のために自動化してくれる数多くの作業の1つですが1点だけ弱点があります。 それはsymfonyに新しいクラスを追加する際にキャッシュをクリアする必要があることです。 doctrine:build-modelはたくさんのクラスを生成するのでキャッシュをクリアしましょう:

 $ php symfony cache:clear

tip

symfonyタスクは名前空間とタスクの名前で構成されます。 それぞれのタスクは他のタスクとの曖昧性がない限り短くできます。 ですので次のコマンドはcache:clearと同等です:

$ php symfony cache:cl
$ php symfony ca:c

cache:clearタスクはとてもよく使われるので、固有の短縮名を持ちます:

$ php symfony cc

初期データ

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

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

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

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

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

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

# 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://www.sensiolabs.com/
    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:        job@example.com
    expires_at:   '2010-10-10'
 
  job_extreme_sensio:
    JobeetCategory:  design
    type:         part-time
    company:      Extreme Sensio
    logo:         extreme-sensio.gif
    url:          http://www.extreme-sensio.com/
    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:        job@example.com
    expires_at:   '2010-10-10'

note

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

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

tip

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

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

note

Propelでは、ファイルがロードされる順序を決定するためにフィクスチャファイルのプレフィックスを番号にする必要がありました。 Doctrineではこのような必要はありません。 外部キーが適切に設定されていることを確認するために、すべてのフィクスチャがロードされた後、正しい順序で保存されます。

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

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

$ php symfony doctrine:data-load

tip

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

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

$ php symfony doctrine:build-all-reload

ブラウザー上での動作確認

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

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

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

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

doctrine: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モジュール

求人情報を編集しようとすると、Category idのドロップダウンに、すべてのカテゴリ名のリストが表示されていることがわかります。 それぞれのオプションの値は__toString()メソッドから取得されます。

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

// 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/doctrine/JobeetAffiliate.class.php
class JobeetAffiliate extends BaseJobeetAffiliate
{
  public function __toString()
  {
    return $this->getUrl();
  }
}

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

バリデーション

明日お会いしましょう

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

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

他の日に関しては、今日のコードはJobeetのSVNリポジトリから入手できます。 release_day_03タグをチェックアウトしてください:

$ svn co http://svn.jobeet.org/doctrine/tags/release_day_03/ jobeet/

This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.