この章はDoctrineを構成するすべてのメインコンポーネントとそれらの連携方法を鳥の目から見ることを目的としています。前の章で既に大半のコンポーネントを検討しましたがこの章ではすべてのコンポーネントとそれらのジョブの理解が進みます。
Doctrine_ManagerクラスはSingletonで構成階層のrootでありDoctrineのいくつかの面をコントロールするFacadeです。次のコードでSingletonインスタンスを読み取ることができます。
// test.php
// ...
$manager = Doctrine_Manager::getInstance();
// test.php
// ...
$connections = $manager->getConnections();
foreach ($connections as $connection) {
echo $connection->getName() . "\n";
}
Doctrine_Managerはイテレータを実装するので接続をループするために変数$managerをループできます。
// test.php
// ...
foreach ($manager as $connection) {
echo $connection->getName() . "\n";
}
Doctrine_Connectionはデータベース用のラッパーです。接続は典型的なPDOのインスタンスですが、Doctrineの設計のおおかげで、PDOが提供する機能を模倣する独自アダプタを設計することが可能です。
Doctrine_Connectionクラスは次のことを対処します:
DoctrineはPDOがサポートするデータベース用のすべてのドライバを持ちます。サポートされるデータベースは次の通りです:
// bootstrap.php
// ...
$conn = Doctrine_Manager::connection('mysql://username:password@localhost/test', 'connection 1');
前の章で既に新しい接続を作成しました。上記のステップをスキップして既に作成した接続を使うことができます。Doctrine_Manager::connection()メソッドを使用して読み取ることができます。
新しいUserレコードを作成するときレコードは接続をflushしてその接続に対して保存されていないすべてのオブジェクトを保存します。下記は例です:
// test.php
// ...
$conn = Doctrine_Manager::connection();
$user1 = new User();
$user1->username = 'Jack';
$user2 = new User();
$user2->username = 'jwage';
$conn->flush();
Doctrine_Connection::flush()を呼び出せばその接続に対する未保存のレコードインスタンスが保存されます。もちろんオプションとしてそれぞれのレコードごとにsave()を呼び出して同じことができます。
// test.php
// ...
$user1->save();
$user2->save();
Doctrine_Tableはコンポーネント(レコード)によって指定されるスキーマ情報を保有します。例えばDoctrine_Recordを継承するUserクラスがある場合、それぞれのスキーマ定義の呼び出しは後で使う情報を保有するユニークなテーブルオブジェクトにデリゲートされます。
それぞれのDoctrine_TableはDoctrine_Connectionによって登録されます。下記に示されるそれぞれのコンポーネント用のテーブルオブジェクトを簡単に取得できます。
例えば、Userクラス用のテーブルオブジェクトを読み取りたい場合を考えます。これはUserをDoctrine_Core::getTable()メソッドの第一引数として渡すことで可能です。
指定するレコードのテーブルオブジェクトを取得するには、Doctrine_Record::getTable()を呼び出すだけです。
// test.php
// ...
$accountTable = Doctrine_Core::getTable('Account');
適切なDoctrine_Tableメソッドを使用することでDoctrine_Recordのカラム定義セットを読み取ることができます。すべてのカラムのすべての情報が必要な場合は次のように行います:
// test.php
// ...
$columns = $accountTable->getColumns();
$columns = $accountTable->getColumns();
foreach ($columns as $column)
{
print_r($column);
}
上記の例が実行されるときに次の内容が出力されます:
$ php test.php
Array
(
[type] => integer
[length] => 20
[autoincrement] => 1
[primary] => 1
)
Array
(
[type] => string
[length] => 255
)
Array
(
[type] => decimal
[length] => 18
)
ときにこれがやりすぎであることがあります。次の例はカラムの名前を配列として読み取る方法を示しています:
// test.php
// ...
$names = $accountTable->getColumnNames();
print_r($names);
上記の例が実行されるとき次の内容が出力されます:
$ php test.php
Array
(
[0] => id
[1] => name
[2] => amount
)
次のようにDoctrine_Table::getRelations()を呼び出すことですべてのDoctrine_Relationオブジェクトの配列を取得できます:
// test.php
// ...
$userTable = Doctrine_Core::getTable('User');
$relations = $userTable->getRelations();
foreach ($relations as $name => $relation) {
echo $name . ":\n";
echo "Local - " . $relation->getLocal() . "\n";
echo "Foreign - " . $relation->getForeign() . "\n\n";
}
上記の例が実行されるとき次の内容が出力されます:
$ php test.php
Email:
Local - id
Foreign - user_id
Phonenumbers:
Local - id
Foreign - user_id
Groups:
Local - user_id
Foreign - group_id
Friends:
Local - user1
Foreign - user2
Addresses:
Local - id
Foreign - user_id
Threads:
Local - id
Foreign - user_id
Doctrine_Table::getRelation()メソッドを使用することで個別のリレーション用のDoctrine_Relationオブジェクトを取得できます。
// test.php
// ...
$relation = $userTable->getRelation('Phonenumbers');
echo 'Name: ' . $relation['alias'] . "\n";
echo 'Local - ' . $relation['local'] . "\n";
echo 'Foreign - ' . $relation['foreign'] . "\n";
echo 'Relation Class - ' . get_class($relation);
上記の例が実行されるとき次の内容が出力されます:
$ php test.php
Name: Phonenumbers
Local - id
Foreign - user_id
Relation Class - Doctrine_Relation_ForeignKey
上記の例において変数$relation}は}配列としてアクセスできる{{Doctrine_Relation_ForeignKeyのインスタンスを格納していることに注目してください。多くのDoctrineのクラスのように、これがArrayAccessを実装するからです。
toArray()メソッドとprint_r()を使用することでリレーションのすべての情報を検査してデバッグすることができます。
// test.php
// ...
$array = $relation->toArray();
print_r($array);
Doctrine_Tableは基本的なファインダーメソッドを提供します。これらのファインダーメソッドはとても速く書けるので1つのデータベーステーブルからデータを取得する場合に使われます。いくつかのコンポーネント(データベーステーブル)を使用するクエリが必要な場合 Doctrine_Connection::query()を使います。
主キーで個別のユーザーを簡単に見つけるにはfind()メソッドを使用します:
// test.php
// ...
$user = $userTable->find(2);
print_r($user->toArray());
上記の例が実行されるとき次の内容が出力されます:
$ php test.php
Array
(
[id] => 2
[is_active] => 1
[is_super_admin] => 0
[first_name] =>
[last_name] =>
[username] => jwage
[password] =>
[type] =>
[created_at] => 2009-01-21 13:29:12
[updated_at] => 2009-01-21 13:29:12
)
データベースのすべてのUserレコードのコレクションを読み取るためにfindAll()メソッドを使うこともできます:
// test.php
// ...
foreach ($userTable->findAll() as $user) {
echo $user->username . "\n";
}
上記の例が実行されるとき次の内容が出力されます:
$ php test.php
Jack
jwage
findAll()メソッドは推奨されません。このメソッドがデータベースのすべてのレコードを返しリレーションから情報を読み取る場合高いクエリカウントを引き起こしながらそのデータを遅延ロードするからです。DQL (Doctrine Query Language)の章を読めばレコードと関連レコードを効率的に読み取る方法を学べます。
findByDql()メソッドを使用して DQLでレコードのセットを読み取ることもできます:
// test.php
// ...
$users = $userTable->findByDql('username LIKE ?', '%jw%');
foreach($users as $user) {
echo $user->username . "\n";
}
上記の例が実行されるときに次の内容が出力されます:
$ php test.php
jwage
Doctrineは追加のマジックファインダーメソッドも提供します。この内容はDQLの章の[doc dql-doctrine-query-language:magic-finders :name]セクションで読むことができます。
Doctrine_Tableによって提供される下記のすべてのファインダーメソッドはクエリを実行するためにDoctrine_Queryのインスタンスを使用します。オブジェクトは内部で動的に構築され実行されます。
カスタムのテーブルクラスを追加するのはとても楽です。行う必要のあるのはクラスを[componentName]Tableとして名付けこれらにDoctrine_Tableを継承させます。Userモデルに関して次のようなクラスを作ることになります:
// models/UserTable.php
class UserTable extends Doctrine_Table
{
}
カスタムのテーブルオブジェクトにカスタムのファインダーメソッドを追加できます。これらのファインダーメソッドは速いDoctrine_TableファインダーメソッドもしくはDQL API (Doctrine_Query::create())を使用できます。
// models/UserTable.php
class UserTable extends Doctrine_Table
{
public function findByName($name)
{
return Doctrine_Query::create()
->from('User u')
->where('u.name LIKE ?', "%$name%")
->execute();
}
}
DoctrineはgetTable()を呼び出すときにDoctrine_Tableの子クラスであるUserTableが存在するかチェックしそうである場合、デフォルトのDoctrine_Tableの代わりにそのクラスのインスタンスを返します。
カスタムのDoctrine_Tableクラスをロードするには、下記のようにbootstrap.phpファイルでautoload_table_classes属性を有効にしなければなりません。
// boostrap.php
// ...
$manager->setAttribute(Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES, true);
これでUserテーブルオブジェクトに問い合わせるとき次の内容が得られます:
$userTable = Doctrine_Core::getTable('User');
echo get_class($userTable); // UserTable
$users = $userTable->findByName("Jack");
findByName()メソッドを追加する上記の例はマジックファインダーメソッドによって自動的に利用可能になります。DQLの章の[doc dql-doctrine-query-language:magic-finders :name]セクションで読むことができます。
DoctrineはDoctrine_Record子クラスを用いてRDBMSのテーブルを表します。これらのクラスはスキーマ情報、お婦四、属性などを定義する場所です。これらの子クラスのインスタンスはデータベースのレコードを表しこれらのオブジェクトでプロパティの取得と設定ができます。
Doctrine_Recordのそれぞれ割り当てられたカラムプロパティはデータベースのテーブルカラムを表します。Defining Modelsの章でモデルの定義方法の詳細を学ぶことになります。
カラムへのアクセスは簡単です:
// test.php
// ...
$userTable = Doctrine_Core::getTable('User');
$user = $userTable->find(1);
オーバーロードを通してプロパティにアクセスする
// test.php
// ...
echo $user->username;
get()でプロパティにアクセスする
// test.php
// ...
echo $user->get('username);
ArrayAccessでプロパティにアクセスする
// test.php
// ...
echo $user['username'];
カラムの値にアクセスする推奨方法はArrayAccessを使うことです。これによって必要に応じてレコードと配列取得を切り替えるのが簡単になるからです。
レコードのプロパティのイテレーションは配列のやり方と似ています。foreachコンストラクトを使用します。Doctrine_RecordはIteratorAggregateインターフェイスを実装するのでこれは実現可能です。
// test.php
// ...
foreach ($user as $field => $value) {
echo $field . ': ' . $value . "\n";
}
配列に関してプロパティの存在のチェックにはisset()を、プロパティをnullに設定するにはunset()が利用できます。
if文で'name'という名前のプロパティが存在するか簡単にチェックできます:
// test.php
// ...
if (isset($user['username'])) {
}
nameプロパティの割り当てを解除したい場合PHPのunset()関数を使うことができます:
// test.php
// ...
unset($user['username']);
レコードプロパティ用に値を設定するときDoctrine_Record::getModified()を使用して修正されたフィールドと値の配列を取得できます。
// test.php
// ...
$user['username'] = 'Jack Daniels';
print_r($user->getModified());
上記のコードが実行されるとき次の内容が出力されます:
$ php test.php
Array
(
[username] => Jack Daniels
)
Doctrine_Record::isModified()メソッドを使用してレコードが修正されることをチェックすることもできます:
// test.php
// ...
echo $user->isModified() ? 'Modified':'Not Modified';
ときどき任意のレコードのカラムカウントを読み取りたいことがあります。これを行うにはcount()関数にレコードを引数として渡します。Doctrine_RecordがCountableインターフェイスを実装するのでこれは可能です。他にはcount()メソッドを呼び出す方法があります。
// test.php
// ...
echo $record->count();
echo count($record);
Doctrine_Recordは任意のレコードの識別子にアクセスするための特別なメソッドを提供します。このメソッドはidentifier()と呼ばれキーが識別子のフィールド名であり、値が、関連プロパティの値である配列を返します。
// test.php
// ...
$user['username'] = 'Jack Daniels';
$user->save();
print_r($user->identifier()); // array('id' => 1)
よくあるのは配列の値を任意のレコードに割り当てることです。これらの値を個別に設定するのはやりずらいと思うかもしれません。しかし悩む必要はありません。Doctrine_Recordは任意の配列もしくはレコードを別のものにマージする方法を提供します。
merge()メソッドはレコードもしくは配列のプロパティをイテレートしてオブジェクトに値を割り当てます。
// test.php
// ...
$values = array(
'username' => 'someone',
'age' => 11,
);
$user->merge($values);
echo $user->username; // someone
echo $user->age; // 11
次のように1つのレコードの値を別のものにマージすることもできます:
// test.php
// ...
$user1 = new User();
$user1->username = 'jwage';
$user2 = new User();
$user2->merge($user1);
echo $user2->username; // jwage
Doctrine_RecordはfromArray()メソッドを持ちます。このメソッドはmerge()に理想的なものでtoArray()メソッドとの一貫性を保つためだけに存在します。
オブジェクトの更新は非常に簡単で、Doctrine_Record::save()メソッドを呼び出すだけです。他の方法はDoctrine_Connection::flush()を呼び出す方法でこの場合すべてのオブジェクトが保存されます。flushはsaveメソッドを呼び出すだけよりも重たいオペレーションであることに注意してください。
// test.php
// ...
$userTable = Doctrine_Core::getTable('User');
$user = $userTable->find(2);
if ($user !== false) {
$user->username = 'Jack Daniels';
$user->save();
}
ときどき直接更新を行いたいことがあります。直接の更新においてオブジェクトはデータベースからロードされません。むしろデータベースの状態が直接更新されます。次の例においてすべてのユーザーを更新するためにDQL UPDATE文を使います。
すべてのユーザー名を小文字にするクエリを実行します:
// test.php
// ...
$q = Doctrine_Query::create()
->update('User u')
->set('u.username', 'LOWER(u.name)');
$q->execute();
レコードの識別子が既知であればオブジェクトを利用して更新を実行することもできます。Doctrine_Record::assignIdentifier()メソッドを使うときこれはレコード識別子を設定し状態を変更するのでDoctrine_Record::save()の呼び出しはinsertの代わりにupdateを実行します。
// test.php
// ...
$user = new User();
$user->assignIdentifer(1);
$user->username = 'jwage';
$user->save();
レコードを置き換えるのはシンプルです。まずは新しいオブジェクトをインスタンス化して保存します。次にデータベースに既に存在する同じ主キーもしくはユニークキーの値で新しいオブジェクトをインスタンス化すればデータベースで新しい列をinsertする代わりに列を置き換え/更新が行われます。下記は例です。
最初に、ユーザー名がユニークインデックスであるUserモデルを想像してみましょう。
// test.php
// ...
$user = new User();
$user->username = 'jwage';
$user->password = 'changeme';
$user->save();
次のクエリを発行します。
INSERT INTO user (username, password) VALUES (?,?) ('jwage', 'changeme')
別の新しいオブジェクトを作り同じユーザー名と異なるパスワードを設定します。
// test.php
// ...
$user = new User();
$user->username = 'jwage';
$user->password = 'newpassword';
$user->replace();
次のクエリが発行されます
REPLACE INTO user (id,username,password) VALUES (?,?,?) (null, 'jwage', 'newpassword')
新しいレコードがinsertされる代わりにレコードが置き換え/更新されます。
ときにデータベースからのデータでレコードをリフレッシュしたいことがあります。Doctrine_Record::refresh()を使います。
// test.php
// ...
$user = Doctrine_Core::getTable('User')->find(2);
$user->username = 'New name';
Doctrine_Record::refresh()メソッドを使う場合データベースからデータが再度選択されインスタンスのプロパティが更新されます。
// test.php
// ...
$user->refresh();
Doctrine_Record::refresh()メソッドは既にロードされたレコードのリレーションをリフレッシュすることもできますが、オリジナルのクエリでこれらを指定する必要があります。
最初に関連GroupsでUserを読み取りましょう:
// test.php
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Groups')
->where('id = ?');
$user = $q->fetchOne(array(1));
関連UsersでGroupを読み取りましょう:
// test.php
// ...
$q = Doctrine_Query::create()
->from('Group g')
->leftJoin('g.Users')
->where('id = ?');
$group = $q->fetchOne(array(1));
UserGroupインスタンスで読み取られたUserとGroupをリンクしましょう:
// test.php
// ...
$userGroup = new UserGroup();
$userGroup->user_id = $user->id;
$userGroup->group_id = $group->id;
$userGroup->save();
GroupをUserに追加するだけでUserをGroupにリンクすることもできます。DoctrineはUserGroupインスタンスの作成を自動的に引き受けます:
// test.php
// ...
$user->Groups[] = $group;
$user->save()
Doctrine_Record::refresh(true)を呼び出す場合新しく作成された参照をロードするレコードとリレーションがリフレッシュされます:
// test.php
// ...
$user->refresh(true);
$group->refresh(true);
Doctrine_Record::refreshRelated()を使用してモデルの定義されたすべてのリレーションを遅延リフレッシュすることもできます:
// test.php
// ...
$user = Doctrine_Core::getTable('User')->findOneByName('jon');
$user->refreshRelated();
リレーションを個別に指定してリフレッシュしたい場合リレーションの名前をrefreshRelated()メソッドに渡せばリレーションは遅延ロードされます:
// test.php
// ...
$user->refreshRelated('Phonenumber');
Doctrineでのレコード削除はDoctrine_Record::delete()、Doctrine_Collection::delete()とDoctrine_Connection::delete()メソッドによって処理されます。
// test.php
// ...
$userTable = Doctrine_Core::getTable("User");
$user = $userTable->find(2);
// ユーザーと関連コンポジットオブジェクトすべてを削除する
if($user !== false) {
$user->delete();
}
UserレコードのDoctrine_Collectionがある場合delete()を呼び出すとDoctrine_Record::delete()が呼び出されてすべてのレコードがループされます。
// test.php
// ...
$users = $userTable->findAll();
Doctrine_Collection::delete()を呼び出すことですべてのユーザーと関連コンポジットオブジェクトを削除できます。deleteを1つずつ呼び出すことでコレクションのすべてのUsersがループされます:
// test.php
// ...
$users->delete();
SQLの式をカラムの値として使う必要のある状況があります。これはポータブルなDQL式をネイティブなSQL式に変換するDoctrine_Expressionを使用することで実現できます。
timepoint(datetime)とname(string)のカラムを持つeventという名前のクラスがある場合を考えてみましょう。現在のタイムスタンプによるレコードの保存は次のように実現されます:
// test.php
// ...
$user = new User();
$user->username = 'jwage';
$user->updated_at = new Doctrine_Expression('NOW()');
$user->save();
上記のコードは次のSQLクエリを発行します:
INSERT INTO user (username, updated_at_) VALUES ('jwage', NOW())
更新された値を取得するためにオブジェクトでDoctrine_Expressionを使うときrefresh()を呼び出さなければなりません。
// test.php
// ...
$user->refresh();
それぞれのDoctrine_Recordは状態を持ちます。最初のすべてレコードは一時的もしくは永続的になります。データベースから読み取られたすべてのレコードは永続的に新しく作成されたすべてのレコードは一時的なものと見なされます。Doctrine_Recordがデータベースから読み取られるが唯一ロードされたプロパティが主キーである場合、このレコードはプロキシと呼ばれる状態を持ちます。
一時的もしくは永続的なすべてのDoctrine_Recordはcleanもしくはdirtyのどちらかです。Doctrine_Recordはプロパティが変更されていないときはcleanで少なくともプロパティの1つが変更されたときはdirtyです。
レコードはlockedと呼ばれる状態を持つこともできます。まれに起きる循環参照の場合に無限反復を避けるためにDoctrineは現在レコードで操作オペレーションが行われていることを示すこの状態を内部で使用します。
レコードがなり得るすべての異なる状態と手短な説明を含むテーブルは下記の通りです:
| 名前 | 説明 |
|---|---|
| Doctrine_Record::STATE_PROXY | レコードがproxyの状態にある一方で、永続性とすべてではないプロパティがデータベースからロードされる。 |
| Doctrine_Record::STATE_TCLEAN | レコードが一時的にcleanである一方で、一時性が変更されプロパティは変更されない。 |
| Doctrine_Record::STATE_TDIRTY | レコードが一時的にdirtyである一方で、一時性とプロパティの一部が変更される。 |
| Doctrine_Record::STATE_DIRTY | レコードがdirtyである一方で永続性とプロパティの一部が変更される。 |
| Doctrine_Record::STATE_CLEAN | レコードがcleanである一方で、永続性は変更されプロパティは変更されない。 |
| Doctrine_Record::STATE_LOCKED | レコードがロックされる。 |
Doctrine_Record::state()メソッドを使用してレコードの状態を簡単に取得できます:
// test.php
// ...
$user = new User();
if ($user->state() == Doctrine_Record::STATE_TDIRTY) {
echo 'Record is transient dirty';
}
上記のオブジェクトはTDIRTYです。これがスキーマで指定されたデフォルトの値をいくつか持つからです。デフォルトの値を持たないオブジェクトを使い新しいインスタンスを作成するとTCLEANが返されます。
// test.php
// ...
$account = new Account();
if ($account->state() == Doctrine_Record::STATE_TCLEAN) {
echo 'Record is transient clean';
}
ときにオブジェクトのコピーを手に入れたいことがあります(コピーされたすべてのプロパティを持つオブジェクト)。Doctrineはこのためのシンプルなメソッド: Doctrine_Record::copy()を提供します。
// test.php
// ...
$copy = $user->copy();
copy()でレコードをコピーすると古いレコードの値を持つ新しいレコード(TDIRTYの状態)が返され、そのレコードのリレーションがコピーされることに注意してください。リレーションもコピーしたくなければ、copy(false)を使う必要があります。
リレーション無しのユーザーのコピーを入手する
// test.php
// ...
$copy = $user->copy(false);
PHPのcloneキーワードを使えばこのcopy()メソッドが内部で使用されます:
// test.php
// ...
$copy = clone $user;
デフォルトでは未修整のレコードでsave()メソッドが呼び出されているときDoctrineは実行しません。レコードが修正されていなくてもレコードを強制的にINSERTしたい状況があります。これはレコードの状態をDoctrine_Record::STATE_TDIRTYを割り当てることで実現できます。
// test.php
// ...
$user = new User();
$user->state('TDIRTY');
$user->save();
カスタムの値をレコードにマッピングしたい状況があります。例えば値が外部のリソースに依存しておりこれらの値をデータベースにシリアライズして保存せずに実行時に利用可能にすることだけを行いたい場合があります。これは次のように実現できます:
// test.php
// ...
$user->mapValue('isRegistered', true);
$user->isRegistered; // true
ときにレコードオブジェクトをシリアライズしたいことがあります(例えばキャッシュを保存するため):
// test.php
// ...
$string = serialize($user);
$user = unserialize($string);
レコードがデータベースに存在するか知りたいことがとてもよくあります。任意のレコードがデータベースの列の同等の内容を持つかを確認するためにexists()メソッドを使うことができます:
// test.php
// ...
$record = new User();
echo $record->exists() ? 'Exists':'Does Not Exist'; // Does Not Exist
$record->username = 'someone';
$record->save();
echo $record->exists() ? 'Exists':'Does Not Exist'; // Exists
Doctrine_Recordはカラムを呼び出すコールバックを添付する方法を提供します。例えば特定のカラムをトリムしたい場合、次のメソッドを使うことができます:
// test.php
// ...
$record->call('trim', 'username');
Doctrine_Collectionはレコードのコレクションです(Doctrine_Recordを参照)。レコードに関してコレクションはDoctrine_Collection::delete()とDoctrine_Collection::save()をそれぞれ使用して削除と保存ができます。
DQL API(Doctrine_Queryを参照)もしくはrawSql API(Doctrine_RawSqlを参照)のどちらかでデータベースからデータを取得するとき、デフォルトではメソッドはDoctrine_Collectionのインスタンスを返します。
次の例では新しいコレクションを初期化する方法を示しています:
// test.php
// ...
$users = new Doctrine_Collection('User');
コレクションにデータを追加します:
// test.php
// ...
$users[0]->username = 'Arnold';
$users[1]->username = 'Somebody';
コレクションの削除と同じように保存もできます:
$users->save();
set()とget()メソッドもしくはArrayAccessインターフェイスでDoctrine_Collectionの要素にアクセスできます。
// test.php
// ...
$userTable = Doctrine_Core::getTable('User');
$users = $userTable->findAll();
ArrayAccessインターフェイスで要素にアクセスする
// test.php
// ...
$users[0]->username = "Jack Daniels";
$users[1]->username = "John Locke";
get()で要素にアクセスする
echo $users->get(1)->username;
存在しないコレクションの単独の要素とこれらの要素(レコード)にアクセスするときDoctrineはこれらを自動的に追加します。
次の例ではデータベースからすべてのユーザー(5人)を取得しコレクションにユーザーの組を追加します。
PHP配列に関してインデックスはゼロから始まります。
// test.php
// ...
$users = $userTable->findAll();
echo count($users); // 5
$users[5]->username = "new user 1";
$users[6]->username = "new user 2";
オプションとして配列インデックスから5と6を省略可能でその場合通常のPHP配列と同じように自動的にインクリメントされます:
// test.php
// ...
$users[]->username = 'new user 3'; // キーは7
$users[]->username = 'new user 4'; // キーは8
Doctrine_Collection::count()メソッドはコレクションの現在の要素の数を返します。
// test.php
// ...
$users = $userTable->findAll();
echo $users->count();
Doctrine_CollectionはCountableインターフェイスを実装するの以前の例に対する妥当な代替方法はcount()メソッドにコレクションを引数として渡すことです。
// test.php
// ...
echo count($users);
Doctrine_Recordと同じようにコレクションはsave()メソッドを呼び出すことで保存できます。save()が呼び出されるときDoctrineはすべてのレコードに対してsave()オペレーションを実行しトランザクション全体のプロシージャをラップします。
// test.php
// ...
$users = $userTable->findAll();
$users[0]->username = 'Jack Daniels';
$users[1]->username = 'John Locke';
$users->save();
Doctrine Recordsとまったく同じようにdelete()メソッドを呼び出すだけでDoctrine Collectionsは削除できます。すべてのコレクションに関してDoctrineはsingle-shot-deleteを実行する方法を知っています。これはそれぞれのコレクションに対して1つのデータベースクエリのみが実行されることを意味します。
例えば複数のコレクションがある場合を考えます。ユーザーのコレクションを削除するときDoctrineはトランザクション全体に対して1つのクエリのみを実行します。クエリは次のようになります:
DELETE
FROM user
WHERE id IN (1,2,3,
... ,N)
ときにコレクションの要素用の通常のインデックス作成をしたくないことがあります。その場合例えば主キーをコレクションとしてマッピングすることが役に立つことがあります。次の例はこれを実現する方法を実演しています。
idカラムをマッピングします。
// test.php
// ....
$userTable = Doctrine_Core::getTable('User');
$userTable->setAttribute(Doctrine_Core::ATTR_COLL_KEY, 'id');
これでuserコレクションはidカラムの値を要素インデックスとして使用します:
// test.php
// ...
$users = $userTable->findAll();
foreach($users as $id => $user) {
echo $id . $user->username;
}
nameカラムをマッピングするとよいでしょう:
// test.php
// ...
$userTable = Doctrine_Core::getTable('User');
$userTable->setAttribute(Doctrine_Core::ATTR_COLL_KEY, 'username');
これでユーザーコレクションはnameカラムの値を要素インデックスとして使用します:
// test.php
// ...
$users = $userTable->findAll();
foreach($users as $username => $user) {
echo $username . ' - ' . $user->created_at . "\n";
}
スキーマでusernameカラムがuniqueとして指定された場合のみこれは利用可能であることに注意してください。そうでなければ重複するコレクションのキーのためにデータは適切にハイドレイトされない事態に遭遇することになります。
Doctrineはすべてのレコード要素用のすべての関連レコードを効率的い読み取る方法を提供します。これは例えばユーザーのコレクションがある場合loadRelated()メソッドを呼び出すだけですべてのユーザーのすべての電話番号をロードできることを意味します。
しかしながら、大抵の場合関連要素を明示的にロードする必要はなく、むしろ行うべきはDQL APIとJOINを使用して一度にすべてをロードすることを試みることです。
次の例ではユーザー、電話番号とユーザーが所属するグループを読み取るために3つのクエリを使用します。
// test.php
// ...
$q = Doctrine_Query::create()
->from('User u');
$users = $q->execute();
すべてのユーザーの電話番号をロードしてみましょう:
// test.php
// ...
$users->loadRelated('Phonenumbers');
foreach($users as $user) {
echo $user->Phonenumbers[0]->phonenumber;
// ここでは追加のDBクエリは不要
}
loadRelated()はリレーション、アソシエーションに対しても動作します:
// test.php
// ...
$users->loadRelated('Groups');
foreach($users as $user) {
echo $user->Groups[0]->name;
}
下記の例はDQL APIを使用してより効率的にこれを行う方法を示します。
1つのクエリですべてをロードするDoctrine_Queryを書きます:
// test.php
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Phonenumbers p')
->leftJoin('u.Groups g');
$users = $q->execute();
PhonenumbersとGroupsを使うとき追加のデータベースクエリは必要ありません:
// test.php
// ...
foreach($users as $user) {
echo $user->Phonenumbers[0]->phonenumber;
echo $user->Groups[0]->name;
}
DoctrineのバリデーションはMVCアーキテクチャのモデル部分でビジネスルールを強制する方法です。このバリデーションを永続的なデータ保存が行われる直前に渡される必要のあるゲートウェイとみなすことができます。これらのビジネスルールの定義はレコードレベル、すなわちactive recordモデルクラスにおいて行われます(Doctrine_Recordを継承するクラス)。この種のバリデーションを使うために最初に行う必要のあることはこれをグローバルに設定することです。これはDoctrine_Managerを通して行われます。
// bootstrap.php
// ...
$manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, Doctrine::VALIDATE_ALL);
バリデーションを有効にすると、一連のバリデーションが自動的に使えるようになります:
レコードのカラムが'integer'型である場合、Doctrineはそのカラムに割り当てられた値がその型であるかをバリデートします。PHPはゆるい型の言語なのでこの種の型バリデーションはできる限りスマートであるように試みます。例えば2は"7"と同じように有効な整数型である一方で"3f"はそうではありません。型バリデーションはすべてのカラムで行われます(すべてのカラム定義は型を必要とするからです)。
次の定数: VALIDATE_ALL、VALIDATE_TYPES、VALIDATE_LENGTHS、VALIDATE_CONSTRAINTS、VALIDATE_NONEをビット演算子で結びつけることができます。
例えば長さバリデーション以外のすべてのバリデーションを有効にするには次のように行います:
// bootstrap.php
// ...
$manager->setAttribute(Doctrine_Core::ATTR_VALIDATE, VALIDATE_ALL & ~VALIDATE_LENGTHS);
Data Validationの章でこのトピックの詳細を読むことができます。
型と長さバリデーションは手軽ですが大抵の場合これらだけでは十分ではありません。それゆえDoctrineはデータをより詳しくバリデートするために利用できるメカニズムを提供します。
バリデータはさらにバリデーションを指定するための簡単な手段です。Doctrineはemail、country、ip、rangeとregexpバリデータなど頻繁に必要とされるたくさんのバリデータを事前に定義しています。Data Validationの章で利用可能なバリデータの全リストが見つかります。hasColumn()メソッドの4番目の引数を通してどのバリデータをどのカラムに適用するのかを指定できます。これが十分ではなく事前に定義されたバリデータとして利用できない特別なバリデータが必要な場合、3つの選択肢があります:
最初の2つのオプションが推奨されます。バリデーションが一般的に利用可能で多くの状況に適用できるからです。このケースにおいて新しいバリデータを実装するのは良い考えです。しかしながら、バリデーションが特別なものでなければDoctrineが提供するフックを使う方がベターです:
active recordで特殊なバリデーションが必要な場合active recordクラス(Doctrine_Recordの子孫)でこれらのメソッドの1つをオーバーライドできます。フィールドをバリデートするためにこれらのメソッドの範囲内でPHPのすべての力を使うことができます。フィールドがバリデーションを渡さないときエラーをレコードのエラーに追加できます。次のコードスニペットはカスタムバリデーションと一緒にバリデータを定義する例を示しています:
// models/User.php
class User extends BaseUser
{
protected function validate()
{
if ($this->username == 'God') {
// Blasphemy! Stop that! ;-)
// syntax: add(<fieldName>, <error code/identifier>)
$errorStack = $this->getErrorStack();
$errorStack->add('name', 'You cannot use this username!');
}
}
}
// models/Email.php
class Email extends BaseEmail
{
// ...
public function setTableDefinition()
{
parent::setTableDefinition();
// ...
// 使われる'email'と'unique'バリデータ
$this->hasColumn('address','string', 150, array('email', 'unique'));
}
}
YAMLフォーマットでの同じ例は次の通りです。YAML Schema Filesの章でYAMLの詳細を読むことができます:
---
# schema.yml
# ...
Email:
columns:
address:
type: string(150)
email: true
unique: true
モデルでビジネスルールを指定する方法を理解したので、アプリケーションの残りの部分でこれらのルールを扱う方法を見てみましょう。
($record->save()の呼び出しを通して)レコードが永続的データとして保存されているときバリデーションの全手続きが実行されます。そのプロセスの間にエラーが起きるとDoctrine_Validator_Exception型のエラーが投げられます。例外を補足してDoctrine_Validator_Exception::getInvalidRecords()インスタンスメソッドを使用してエラーを解析できます。このメソッドはバリデーションをパスしなかったすべてのレコードへの参照を持つ通常の配列を返します。それぞれのレコードのエラースタックを解析することでそれぞれのレコードのエラーを詳しく調査することができます。レコードのエラースタックはDoctrine_Record::getErrorStack()インスタンスメソッドで取得できます。それぞれのエラースタックはDoctrine_Validator_ErrorStackクラスのインスタンスです。エラースタックはエラーを検査するためのインターフェイスを簡単に使う方法を提供します。
任意のときに任意のレコードに対してバリデーションを明示的に実行できます。この目的のためにDoctrine_RecordはDoctrine_Record::isValid()インスタンスメソッドを提供します。このメソッドはバリデーションの結果を示す論理型を返します。このメソッドがfalseを返す場合、例外が投げられないこと以外は上記と同じ方法でエラースタックを検査できるので、Doctrine_Record::getErrorStack()を通したバリデーションがパスしなかったレコードのエラースタックを得られます。
次のコードスニペットはDoctrine_Validator_Exceptionによって引き起こされる明示的なバリデーションの処理方法の例です。
// test.php
// ...
$user = new User();
try {
$user->username = str_repeat('t', 256);
$user->Email->address = "drink@@notvalid..";
$user->save();
} catch(Doctrine_Validator_Exception $e) {
$userErrors = $user->getErrorStack();
$emailErrors = $user->Email->getErrorStack();
foreach($userErrors as $fieldName => $errorCodes) {
echo $fieldName . " - " . implode(', ', $errorCodes) . "\n";
}
foreach($emailErrors as $fieldName => $errorCodes) {
echo $fieldName . " - " . implode(', ', $errorCodes) . "\n";
}
}
$e->getInvalidRecords()を使うことができます。扱っているレコードを知っているときは上記の内容を直接使う方がシンプルです。
アプリケーションで簡単に使えるように読みやすく整形されたエラースタックを読み取ることもできます:
// test.php
// ...
echo $user->getErrorStackAsString();
次のようにエラー文字列が出力されます:
Validation failed in class User
1 field had validation error:
* 1 validator failed on username (length)
Doctrine_Connection_ProfilerはDoctrine_Connection用のイベントリスナーです。これは柔軟なクエリプロファイリングを提供します。SQL文字列に加えクエリプロファイルはクエリを実行するための経過時間を含みます。これによってモデルクラスにデバッグコードを追加せずにクエリのインスペクションの実行が可能になります。
Doctrine_Connection_ProfilerはDoctrine_Connection用のイベントリスナーとして追加されることで有効になります。
// test.php
// ...
$profiler = new Doctrine_Connection_Profiler();
$conn = Doctrine_Manager::connection();
$conn->setListener($profiler);
ページの中にはロードが遅いものがあるでしょう。次のコードは接続から完全なプロファイラーレポートを構築する方法を示しています:
// test.php
// ...
$time = 0;
foreach ($profiler as $event) {
$time += $event->getElapsedSecs();
echo $event->getName() . " " . sprintf("%f", $event->getElapsedSecs()) . "\n";
echo $event->getQuery() . "\n";
$params = $event->getParams();
if( ! empty($params)) {
print_r($params);
}
}
echo "Total time: " . $time . "\n";
symfony、Zendなどのフレームワークはウェブデバッグツールバーを提供します。Doctrineはそれぞれのクエリにかかる時間と同様にすべてのページで実行されるクエリの回数をレポートする機能を提供します。
'トランザクション(Transaction)'という用語はデータベースのトランザクションではなく一般的な意味を示します。
ロックは並行処理をコントロールするメカニズムです。最もよく知られるロック戦略は楽観的と悲観的ロックです。次のセクションでこれら2つの戦略の手短な説明を行います。現在Doctrineがサポートしているのは悲観的ロックです。
トランザクションが開始するときオブジェクトの状態/バージョンに注目されます。トランザクションが終了するとき注目された状態/バージョンの参与しているオブジェクトが現在の状態/バージョンと比較されます。状態/バージョンが異なる場合オブジェクトは他のトランザクションによって修正され現在のトランザクションは失敗します。このアプローチは'楽観的'(optimistic)と呼ばれます。複数のユーザーが同時に同じオブジェクト上のトランザクションに参加しないことを前提としているからです。
トランザクションに参加する必要のあるオブジェクトはユーザーがトランザクションを開始した瞬間にロックされます。ロックが有効な間、他のユーザーがこれらのオブジェクトで作動するトランザクションを始めることはありません。これによってトランザクションを始めるユーザー以外のユーザーが同じオブジェクトを修正しないことが保証されます。
Doctrineの悲観的オフラインロック機能はHTTPリクエストとレスポンスサイクルと/もしくは完了させるためにたくさんの時間がかかるアクションもしくはプロシージャの並行処理をコントロールするために使うことができます。
次のコードスニペットはDoctrineの悲観的オフラインロック機能の使い方を実演しています。
ロックがリクエストされたページでロックマネージャーインスタンスを取得します:
// test.php
// ...
$lockingManager = new Doctrine_Locking_Manager_Pessimistic();
300秒 = 5分のタイムアウトをロックしようとする前に、タイムアウトした古いロックを必ず解放してください。これはreleaseAgedLocks()メソッドを使用することで可能です。
// test.php
// ...
$user = Doctrine_Core::getTable('User')->find(1);
try
{
$lockingManager->releaseAgedLocks(300);
$gotLock = $lockingManager->getLock($user, 'jwage');
if ($gotLock)
{
echo "Got lock!";
}
else
{
echo "Sorry, someone else is currently working on this record";
}
} catch(Doctrine_Locking_Exception $dle) {
echo $dle->getMessage();
// handle the error
}
トランザクションが終了するページでロックマネジャーのインスタンスを取得します:
// test.php
// ...
$user = Doctrine_Core::getTable('User')->find(1);
$lockingManager = new Doctrine_Locking_Manager_Pessimistic();
try
{
if ($lockingManager->releaseLock($user, 'jwage'))
{
echo "Lock released";
}
else
{
echo "Record was not locked. No locks released.";
}
}
catch(Doctrine_Locking_Exception $dle)
{
echo $dle->getMessage();
// handle the error
}
悲観的オフラインロックマネージャーはロックをデータベースで保存します(それゆえ'オフライン'です)。マネージャーをインスタンス化してATTR_CREATE_TABLESがTRUEに設定されているときに必要なロックテーブルは自動的に作成されます。インストール用の集中化と一貫したテーブル作成のプロシージャを提供するために将来この振る舞いが変更される可能性があります。
データベースビューは複雑なクエリのパフォーマンスを多いに増大できます。これらをキャッシュされたクエリとして見なすことができます。Doctrine_ViewはデータベースビューとDQLクエリの統合を提供します。
データベースでビューを使うのは簡単です。Doctrine_Viewクラスは既存のビューの作成と削除をする機能を提供します。
Doctrine_Queryによって実行されるSQLを保存することでDoctrine_ViewクラスはDoctrine_Queryクラスを統合します。
最初に新しいDoctrine_Queryインスタンスを作成しましょう:
// test.php
// ...
$q = Doctrine_Query::create()
->from('User u')
->leftJoin('u.Phonenumber p')
->limit(20);
データベースビューを指定するためのnameと同じようにDoctrine_Viewインスタンスを作成しDoctrine_Queryインスタンスにこれを渡しましょう:
// test.php
// ...
$view = new Doctrine_View($q, 'RetrieveUsersAndPhonenumbers');
Doctrine_View::create()メソッドを使用してビューを簡単に作成できます:
// test.php
// ...
try {
$view->create();
} catch (Exception $e) {}
代わりにデータベースビューを削除したい場合Doctrine_View::drop()メソッドを使います:
// test.php
// ...
try {
$view->drop();
} catch (Exception $e) {}
ビューの使用はとても簡単です。Doctrine_Queryオブジェクトと同じようにビューの実行と結果の取得にはDoctrine_View::execute()を使います:
// test.php
// ...
$users = $view->execute();
foreach ($users as $user) {
print_r($us->toArray());
}