Doctrineは柔軟なイベントリスナーアーキテクチャを提供します。このアークテクチャは異なるイベントのリスニングだけでなくリスニングされるメソッドの実行を変更することも可能にします。
様々なDoctrineコンポーネント用の異なるリスナーとフックがあります。リスナーは個別のクラスであるのに対してフックはリスニングされるクラスの範囲内の空のテンプレートメソッドです。
フックはイベントリスナーよりもシンプルですがこれらは異なるアスペクトの分離が欠けています。Doctrine_Recordフックを使用する例は次の通りです:
// models/BlogPost.php
class BlogPost extends Doctrine_Record
{
// ...
public function preInsert($event)
{
$invoker = $event->getInvoker();
$invoker->created = date('Y-m-d', time());
}
}
これまでたくさんのモデルを定義してきたので、BlogModelに対して独自のsetTableDefinition()を定義できます。もしくは独自のカスタムモデルを作りましょう!
次のコードで上記のモデルを使うことができます。title、bodyとcreatedカラムをモデルに追加することを前提とします:
// test.php
// ...
$blog = new BlogPost();
$blog->title = 'New title';
$blog->body = 'Some content';
$blog->save();
echo $blog->created;
上記の例はPHPが理解する現在の日付を出力します。
それぞれのリスナーとフックメソッドは Doctrine_Eventオブジェクトを1つのパラメータとして受け取ります。Doctrine_Eventオブジェクトは問題のイベントの情報を格納しリスニングされるメソッドの実行を変更できます。
ドキュメントの目的のために多くのメソッドテーブルはparamsという名前のカラムで提供されます。このカラムはパラメータの名前は与えられたイベント上でイベントオブジェクトが保有するパラメータの名前を示します。例えばpreCreateSavepointイベントは作成されたsavepointの名前を持つ1つのパラメータを持ちます。
接続リスナーはDoctrine_Connectionとそのモジュール(Doctrine_Transactionなど)のメソッドをリスニングするために使われます。すべてのリスナーメソッドはリスニングされるイベントの情報を格納するDoctrine_Eventオブジェクトを1つの引数として受け取ります。
リスナーを定義する方法は3つあります。最初にDoctrine_EventListenerを継承するクラスを作成することでリスナーを作成できます:
class MyListener extends Doctrine_EventListener
{
public function preExec(Doctrine_Event $event)
{
}
}
Doctrine_EventListenerを継承するクラスを定義することでDoctrine_EventListener_Interfaceの範囲内ですべてのメソッドを定義する必要はありません。これはDoctrine_EventListenerが既にこれらすべてのメソッド用の空のスケルトンを持つからです。
ときにDoctrine_EventListenerを継承するリスナーを定義できないことがあります(他の基底クラスを継承するリスナーを用意できます)。この場合Doctrine_EventListener_Interfaceを実装させることができます。
class MyListener implements Doctrine_EventListener_Interface
{
public function preTransactionCommit(Doctrine_Event $event) {}
public function postTransactionCommit(Doctrine_Event $event) {}
public function preTransactionRollback(Doctrine_Event $event) {}
public function postTransactionRollback(Doctrine_Event $event) {}
public function preTransactionBegin(Doctrine_Event $event) {}
public function postTransactionBegin(Doctrine_Event $event) {}
public function postConnect(Doctrine_Event $event) {}
public function preConnect(Doctrine_Event $event) {}
public function preQuery(Doctrine_Event $event) {}
public function postQuery(Doctrine_Event $event) {}
public function prePrepare(Doctrine_Event $event) {}
public function postPrepare(Doctrine_Event $event) {}
public function preExec(Doctrine_Event $event) {}
public function postExec(Doctrine_Event $event) {}
public function preError(Doctrine_Event $event) {}
public function postError(Doctrine_Event $event) {}
public function preFetch(Doctrine_Event $event) {}
public function postFetch(Doctrine_Event $event) {}
public function preFetchAll(Doctrine_Event $event) {}
public function postFetchAll(Doctrine_Event $event) {}
public function preStmtExecute(Doctrine_Event $event) {}
public function postStmtExecute(Doctrine_Event $event) {}
}
すべてのリスナーメソッドはここで定義しなければなりません。さもないとPHPは致命的エラーを投げます。
リスナーを作成する3番目の方法はとても優雅です。Doctrine_Overloadableを実装するクラスを作成します。インターフェイスは1つのメソッド: __call()のみを持ちます。このメソッドは*すべての*イベントと補足するために使われます。
class MyDebugger implements Doctrine_Overloadable
{
public function __call($methodName, $args)
{
echo $methodName . ' called !';
}
}
setListener()でリスナーを接続に追加できます。
$conn->setListener(new MyDebugger());
複数のリスナーが必要な場合はaddListener()を使います。
$conn->addListener(new MyDebugger());
$conn->addListener(new MyLogger());
下記のリスナーのすべてはDoctrine_Connectionクラスに含まれます。これらすべてはDoctrine_Eventのインスタンスです。
| メソッド | リスニング | パラメータ |
|---|---|---|
| preConnect(Doctrine_Event $event) | Doctrine_Connection::connection() | |
| postConnect(Doctrine_Event $event) | Doctrine_Connection::connection() |
下記のリスナーのすべてはDoctrine_Transactionクラスに含まれます。これらすべてにDoctrine_Eventのインスタンスが渡されます。
| メソッド | リスニング | パラメータ |
|---|---|---|
| preTransactionBegin() | beginTransaction() | |
| postTransactionBegin() | beginTransaction() | |
| preTransactionRollback() | rollback() | |
| postTransactionRollback() | rollback() | |
| preTransactionCommit() | commit() | |
| postTransactionCommit() | commit() | |
| preCreateSavepoint() | createSavepoint() | savepoint |
| postCreateSavepoint() | createSavepoint() | savepoint |
| preRollbackSavepoint() | rollbackSavepoint() | savepoint |
| postRollbackSavepoint() | rollbackSavepoint() | savepoint |
| preReleaseSavepoint() | releaseSavepoint() | savepoint |
| postReleaseSavepoint() | releaseSavepoint() | savepoint |
class MyTransactionListener extends Doctrine_EventListener
{
public function preTransactionBegin(Doctrine_Event $event)
{
echo 'beginning transaction... ';
}
public function preTransactionRollback(Doctrine_Event $event)
{
echo 'rolling back transaction... ';
}
}
下記のリスナーのすべてはDoctrine_ConnectionとDoctrine_Connection_Statementクラスに含まれます。そしてこれらすべてはDoctrine_Eventのインスタンスです。
| メソッド | リスニング | パラメータ |
|---|---|---|
| prePrepare() | prepare() | query |
| postPrepare() | prepare() | query |
| preExec() | exec() | query |
| postExec() | exec() | query, rows |
| preStmtExecute() | execute() | query |
| postStmtExecute() | execute() | query |
| preExecute() | execute() * | query |
| postExecute() | execute() * | query |
| preFetch() | fetch() | query, data |
| postFetch() | fetch() | query, data |
| preFetchAll() | fetchAll() | query, data |
| postFetchAll() | fetchAll() | query, data |
Doctrine_Connection::execute()がプリペアードステートメントパラメータで呼び出されるときにのみpreExecute()とpostExecute()は起動します。そうではない場合Doctrine_Connection::execute()はprePrepare()、postPrepare()、preStmtExecute()とpostStmtExecute()を起動します。
ハイドレーションリスナーは結果セットのハイドレーション処理をリスニングするために使われます。ハイドレーション処理をリスニングするために2つのメソッド: preHydrate()とpostHydrate()が存在します。
ハイドレーションリスナーを接続レベルで設定する場合、preHydrate()とpostHydrate()ブロックの範囲内のコードは複数のコンポーネントの結果セットの範囲内ですべてのコンポーネントによって実行されます。テーブルレベルで同様のリスナーを追加する場合、テーブルのデータがハイドレイトされているときのみ起動します。
フィールド: first_name、last_nameとageを持つUserクラスを考えてみましょう。次の例ではfirst_nameとlast_nameフィールドに基づいてfull__nameと呼ばれる生成フィールドを常にビルドするリスナーを作成します。
// test.php
// ...
class HydrationListener extends Doctrine_Record_Listener
{
public function preHydrate(Doctrine_Event $event)
{
$data = $event->data;
$data['full_name'] = $data['first_name'] . ' ' . $data['last_name'];
$event->data = $data;
}
}
行う必要があるのはUserレコードにこのリスナーを追加して複数のユーザーを取得することです:
// test.php
// ...
$userTable = Doctrine_Core::getTable('User');
$userTable->addRecordListener(new HydrationListener());
$q = Doctrine_Query::create()
->from('User');
$users = $q->execute();
foreach ($users as $user) {
echo $user->full_name;
}
Doctrine_RecordはDoctrine_Connectionとよく似たリスナーを提供します。グローバル、接続、テーブルレベルでリスナーを設定できます。
利用可能なすべてのリスナーメソッドの一覧は次の通りです:
下記のリスナーすべてがDoctrine_RecordとDoctrine_Validatorクラスに含まれます。そしてこれらすべてにDoctrine_Eventのインスタンスが渡されます。
| メソッド | リスニング |
|---|---|
| preSave() | save() |
| postSave() | save() |
| preUpdate() | レコードがDIRTYのときsave() |
| postUpdate() | レコードがDIRTYのときsave() |
| preInsert() | レコードがDIRTYのときsave() |
| postInsert() | レコードがDIRTYのときsave() |
| preDelete() | delete() |
| postDelete() | delete() |
| preValidate() | validate() |
| postValidate() | validate() |
接続リスナーと同じようにレコードリスナーを定義する方法は3つあります: Doctrine_Record_Listenerを継承する、Doctrine_Record_Listener_Interfaceを実装するもしくはDoctrine_Overloadableを実装するです。
次の例ではDoctrine_Overloadableを実装することでグローバルレベルのリスナーを作成します:
class Logger implements Doctrine_Overloadable
{
public function __call($m, $a)
{
echo 'caught event ' . $m;
// do some logging here...
}
}
マネージャーにリスナーを追加するのは簡単です:
$manager->addRecordListener(new Logger());
マネージャーレベルのリスナーを追加することでこれらの接続の範囲内ですべてのテーブル/レコードに影響を及ぼします。次の例では接続レベルのリスナーを作成します:
class Debugger extends Doctrine_Record_Listener
{
public function preInsert(Doctrine_Event $event)
{
echo 'inserting a record ...';
}
public function preUpdate(Doctrine_Event $event)
{
echo 'updating a record...';
}
}
接続にリスナーを追加するのも簡単です:
$conn->addRecordListener(new Debugger());
リスナーが特定のテーブルのみにアクションを適用するようにリスナーをテーブル固有のものにしたい場合がよくあります。
例は次の通りです:
class Debugger extends Doctrine_Record_Listener
{
public function postDelete(Doctrine_Event $event)
{
echo 'deleted ' . $event->getInvoker()->id;
}
}
このリスナーを任意のテーブルに追加するのは次のようにできます:
class MyRecord extends Doctrine_Record
{
// ...
public function setUp()
{
$this->addListener(new Debugger());
}
}
| メソッド | リスニング |
|---|---|
| preSave() | save() |
| postSave() | save() |
| preUpdate() | レコード状態がDIRTYであるときsave() |
| postUpdate() | レコード状態がDIRTYであるときsave() |
| preInsert() | レコード状態がDIRTYであるときsave() |
| postInsert() | レコード状態がDIRTYであるときsave() |
| preDelete() | delete() |
| postDelete() | delete() |
| preValidate() | validate() |
| postValidate() | validate() |
preInsert()とpreUpdate()メソッドを利用するシンプルな例は次の通りです:
class BlogPost extends Doctrine_Record
{
public function setTableDefinition()
{
$this->hasColumn('title', 'string', 200);
$this->hasColumn('content', 'string');
$this->hasColumn('created', 'date');
$this->hasColumn('updated', 'date');
}
public function preInsert($event)
{
$this->created = date('Y-m-d', time());
}
public function preUpdate($event)
{
$this->updated = date('Y-m-d', time());
}
}
レコードリスナーをグローバル、それぞれの接続で、もしくは特定のレコードインスタンスで追加することができます。Doctrine_QueryはpreDql*()フックを実装します。これはクエリが実行されるときに、追加されたレコードリスナーもしくはモデルインスタンス自身でチェックされます。フックを起動したクエリを変更できるフックのためにクエリはクエリのfrom部分に関連するすべてのモデルをチェックします。
DQLで使うことができるフックのリストは次の通りです:
| メソッド | リスニング |
|---|---|
| preDqlSelect() | from() |
| preDqlUpdate() | update() |
| preDqlDelete() | delete() |
下記のコードはUserモデル用のSoftDelete機能を実装するモデルにレコードリスナーを直接追加する例です。
SoftDeleteの機能はDoctrineのビヘイビアとして含まれます。このコードは実行されるクエリを修正するためにDQLリスナーをselect、delete、updateする方法を実演しています。Doctrine_Record::setUp()の定義で$this->actAs('SoftDelete')を指定することでSoftDeleteビヘイビアを使うことができます。
class UserListener extends Doctrine_EventListener
{
/**
* Skip the normal delete options so we can override it with our own
*
* @param Doctrine_Event $event
* @return void
*/
public function preDelete(Doctrine_Event $event)
{
$event->skipOperation();
}
/**
* Implement postDelete() hook and set the deleted flag to true
*
* @param Doctrine_Event $event
* @return void
*/
public function postDelete(Doctrine_Event $event)
{
$name = $this->_options['name'];
$event->getInvoker()->$name = true;
$event->getInvoker()->save();
}
/**
* Implement preDqlDelete() hook and modify a dql delete query so it updates the deleted flag
* instead of deleting the record
*
* @param Doctrine_Event $event
* @return void
*/
public function preDqlDelete(Doctrine_Event $event)
{
$params = $event->getParams();
$field = $params['alias'] . '.deleted';
$q = $event->getQuery();
if ( ! $q->contains($field)) {
$q->from('')->update($params['component'] . ' ' . $params['alias']);
$q->set($field, '?', array(false));
$q->addWhere($field . ' = ?', array(true));
}
}
/**
* Implement preDqlDelete() hook and add the deleted flag to all queries for which this model
* is being used in.
*
* @param Doctrine_Event $event
* @return void
*/
public function preDqlSelect(Doctrine_Event $event)
{
$params = $event->getParams();
$field = $params['alias'] . '.deleted';
$q = $event->getQuery();
if ( ! $q->contains($field)) {
$q->addWhere($field . ' = ?', array(false));
}
}
}
オプションとして上記のリスナーのメソッドは下記のユーザークラスに設置できます。Doctrineはそこでフックを同じようにチェックします:
class User extends Doctrine_Record
{
// ...
public function preDqlSelect()
{
// ...
}
public function preDqlUpdate()
{
// ...
}
public function preDqlDelete()
{
// ...
}
}
これらのDQLコールバックがチェックされるようにするには、これらを明示的に有効にしなければなりません。これはそれぞれのクエリに対して少量のオーバーヘッドを追加するので、デフォルトでは無効です。以前の章で既にこの属性を有効にしました。
思い出すためにコードを再掲載します:
// bootstrap.php
// ...
$manager->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, true);
Userモデルとやりとりをするとき削除フラグが考慮されます:
レコードインスタンスを通してユーザーを削除します:
$user = new User();
$user->username = 'jwage';
$user->password = 'changeme';
$user->save();
$user->delete();
$user->delete()を呼び出しても実際にはレコードは削除されず代わりに削除フラグがtrueに設定されます。
$q = Doctrine_Query::create()
->from('User u');
echo $q->getSql();
SELECT
u.id AS u__id,
u.username AS u__username,
u.password AS u__password,
u.deleted AS u__deleted
FROM user u
WHERE u.deleted = ?
"u.deleted = ?"がtrueのパラメータの値でwhere条件に自動的に追加されたことに注目してください。
異なるイベントリスナーを連結することができます。このことは同じイベントをリスニングするために複数のリスナーを追加できることを意味します。次の例では与えられた接続用に2つのリスナーを追加します:
この例ではDebuggerとLoggerは両方ともDoctrine_EventListenerを継承します:
$conn->addListener(new Debugger());
$conn->addListener(new Logger());
getInvoker()を呼び出すことでイベントを起動したオブジェクトを取得できます:
class MyListener extends Doctrine_EventListener
{
public function preExec(Doctrine_Event $event)
{
$event->getInvoker(); // Doctrine_Connection
}
}
Doctrine_Eventは定数をイベントコードとして使用します。利用可能なイベントの定数の一覧は下記の通りです:
フックの使い方と返されるコードの例は次の通りです:
class MyListener extends Doctrine_EventListener
{
public function preExec(Doctrine_Event $event)
{
$event->getCode(); // Doctrine_Event::CONN_EXEC
}
}
class MyRecord extends Doctrine_Record
{
public function preUpdate(Doctrine_Event $event)
{
$event->getCode(); // Doctrine_Event::RECORD_UPDATE
}
}
getInvoker()メソッドは与えられたイベントを起動したオブジェクトを返します。例えばイベント用のDoctrine_Event::CONN_QUERYインボーカーはDoctrine_Connectionオブジェクトです。
Doctrine_Recordインスタンスが保存されupdateがデータベースに発行されるときに起動するpreUpdate()という名前のレコードフックの使い方の例は次の通りです:
class MyRecord extends Doctrine_Record
{
public function preUpdate(Doctrine_Event $event)
{
$event->getInvoker(); // Object(MyRecord)
}
}
リスナーチェーンのビヘイビアの変更と同様にリスニングされているメソッドの実行の変更のためにDoctrine_Eventは多くのメソッドを提供します。
多くの理由からリスニングされているメソッドの実行をスキップしたいことがあります。これは次のように実現できます(preExec()は任意のリスナーメソッドにできることに注意してください):
class MyListener extends Doctrine_EventListener
{
public function preExec(Doctrine_Event $event)
{
// some business logic, then:
$event->skipOperation();
}
}
リスナーのチェーンを使うとき次のリスナーの実行をスキップしたいことがあります。これは次のように実現できます:
class MyListener extends Doctrine_EventListener
{
public function preExec(Doctrine_Event $event)
{
// some business logic, then:
$event->skipNextListener();
}
}