symfonyでDoctrineのテンプレートを実装する

こんにちは。先日十数年ぶりに歯医者で泣かされた小川です。

先日Doctrineのテンプレートというものについて使う機会があったので、今回はそれを紹介していきます。

Doctrineに関しては過去に森川さんが書いたブログをみていただければと思います。
symfony & Doctrine
symfony & Doctrine その2

説明の前に、せっかくなので報告です。
The wait is over: symfony 1.1 released
ついにsymfony 1.1が正式にリリースされました。今日は残念ながらsymfony 1.1のネタはありませんが、森川さんの記事があるのでぜひそちらをみてください。それでは紹介していきましょう。

さて、Doctrineにはデフォルトでいくつかテンプレートが存在しています。特にかかせないのがTimestampableテンプレートです。
Propelの場合、モデルを保存したときにcreated_atとupdated_atが自動更新されますが、DoctrineではこのTimestampableテンプレートを実装しなければ自動更新は有効にならないのです。
逆に考えましょう。Timestampableテンプレートを実装したモデルは「created_atとupdated_atというカラムがある場合、INSRT/UPDATE時に自動でタイムスタンプを入れる」という共通機能を実装できます。この「モデルに共通機能を実装する仕組み」がテンプレートです。

先にテンプレートの実装方法について説明します。以下はTimestampableテンプレートをArticleモデルに実装した例です。


Article:
  columns:
    ...
  templates:
    Doctrine_Template_Timestampable:

このように連想配列で指定します。この状態でdoctrine-build-modelを実行するとTimestampableテンプレートが実装されます。ちなみにテンプレートの指定は配列でも連想配列でも構いません。ただしどちらかに統一する必要はあります。


<?php
$this->loadTemplate('Doctrine_Template_Timestampable');

lib/model/doctrine/generated/BaseArticle.class.phpのsetUpメソッドの最後に上記が書いてあれば成功です。

それではここまでわかったところで、自作テンプレートを作成してみましょう。作り方は簡単です。Doctrine_Templateクラスを継承したクラスを作ればいいだけです。場所はパスが通っていればどこでもいいと思いますが、僕はなんとなくlib/modelに配置しています。


<?php
class UpperNameTemplate extends Doctrine_Template
{
  protected $_options = array();
  public function __construct(array $options = array())
  {
    $this->_options = Doctrine_Lib::arrayDeepMerge($this->_options, $options);
  }
  public function setTableDefinition()
  {
    $this->addListener(new UpperNameTemplateListener($this->_options));
  }
  public function getUpperName()
  {
    return strtoupper($this->getInvoker()->getName());
  }
}

上記はnameフィールドの値を大文字にして返すメソッドを実装したテンプレートです。$this->getInvoker()というのは、呼び出し元のモデルオブジェクトを取得するメソッドです。というのも共通機能の実装とはいえ動的にクラスにメソッドを追加するのではなく__callメソッド内でテンプレートオブジェクトに対してメソッドを発行しているので、$this->getName()とはできないのです。
ただテーブルオブジェクトはRecordオブジェクトと同じく_tableプロパティに格納された状態なので、わざわざ$this->getInvoker()->getTable()とせず$this->_tableでアクセス可能です。

あとはテンプレートリスナーについて説明しなければいけません。これは何かというと、preSaveなどのフックメソッドをテンプレートとセットで実装するための仕組みです。


<?php
class UpperNameTemplateListener extends Doctrine_Record_Listener
{
  public function __construct(array $options = array())
  {
    $this->_options = $options;
  }
  public function preSave(Doctrine_Event $event)
  {
    $lowerName = strtolower($event->getInvoker()->getName());
    $event->getInvoker()->setName($lowerName);
  }
}

上記はsave時にnameを全て小文字に変換するテンプレートリスナーです。Doctrine_Record_Listerクラスを継承しているところと、preSaveの引数にDoctrine_Eventオブジェクトを受け取っているところに注意してください。先ほどと違い、Doctrine_EventオブジェクトにgetInvoker()を発行してレコードを取得しています。

ちなみにテンプレートのコンストラクタでオプション配列を受け取っていますが、YAMLに連想配列を記述するだけで定義できます。


Log:
  columns:
    ...
  templates:
    Doctrine_Template_Timestampable:
      updated:
        disable:  true

非常に簡単ですね。ちなみに上記は、Timestampableテンプレートを使った場合にupdated_atの自動更新を無効にする設定です。

とりあえず以上で一通りの説明は終わりです。使わなくて済むケースも多いと思いますが、いざというときに知っておくと便利な機能です。僕の場合はRailsのacts_as_listのような機能を共通実装するためにテンプレートを作成したのですが、だいぶ開発が楽になりました。今までコピー&ペーストでやりくりしてた、なんていう方はぜひテンプレートを使ってみてください。