Doctrineのアクセサとオーバーライドについて

小川です。WEB+DB PRESS Vol.46のプレゼントで応募した「はまちちゃんのセキュリティ講座の生イラスト&直筆サイン入り色紙」が当たってしまいました。感激です。ありがとうございます。

先月末に第2回symfony勉強会があり、そこでDoctrineについて簡単に発表を行ってきました。
そこでDoctrineのアクセサ(Getter/Setter)について色々と疑問を抱いてる方がいらっしゃったので、今日はそれについてまとめてみようと思います。

まず、DoctrineのGetterがどのようなものかを解説していきます。
基本的に僕はDoctrineを単体ではなくsymfonyとあわせて使っているので、sfDoctrinePluginを使っていることを前提として進めていきます。
またDoctrineは1.0系を想定しています。

Doctrineのプロパティには以下のようなアクセス方法があります。


<?php
$product = Doctrine::getTable('Product')->find($id);
// Titleプロパティを取得
$product['price'];        // No.1
$product->price;          // No.2
$product->get('price');   // No.3
$product->getPrice();     // No.4

上記の4種類があります。結論からいうと、上記はすべて同じ挙動をとります。
では具体的にどのような挙動をとるかを説明していきます。

まず、Productクラスを例に、継承構造をさかのぼってクラス定義を並べると、


<?php
// Product
class Product extends BaseProduct {}
// BaseProduct
abstract class BaseProduct extends sfDoctrineRecord {}
//sfDoctrineRecord
abstract class sfDoctrineRecord extends Doctrine_Record {}
// Doctrine_Record
abstract class Doctrine_Record extends Doctrine_Record_Abstract implements Countable, IteratorAggregate, Serializable {}
// Doctrine_Record_Abstract
abstract class Doctrine_Record_Abstract extends Doctrine_Access {}
// Doctrine_Access
abstract class Doctrine_Access extends Doctrine_Locator_Injectable implements ArrayAccess {}
// Doctrine_Locator_Injectable
class Doctrine_Locator_Injectable {}

上記のようになっています。ここでキモになるのがDoctrine_Accessクラスです。
Doctrine_Accessクラスは何をやっているかというと、No.1とNo.2の形式でアクセスしてきたとき、すべてNo.3のget('price')を呼び出すようになっています。

そしてDoctrine_Recordクラスにアクセサの実処理が記述されています。
get()メソッドの定義は以下のようになっています。


<?php
public function get($fieldName, $load = true)
{
    if ($this->_table->getAttribute(Doctrine::ATTR_AUTO_ACCESSOR_OVERRIDE)) {
        $componentName = $this->_table->getComponentName();
        $accessor = isset(self::$_customAccessors[$componentName][$fieldName])
            ? self::$_customAccessors[$componentName][$fieldName]
            : 'get' . Doctrine_Inflector::classify($fieldName);
        if (isset(self::$_customAccessors[$componentName][$fieldName]) || method_exists($this, $accessor)) {
            self::$_customAccessors[$componentName][$fieldName] = $accessor;
            return $this->$accessor($load);
        }
    }
    return $this->_get($fieldName, $load);
}

if文で囲まれている範囲は、getPrice()が実装されていればgetPrice()を読みに行く、という処理です。
通常は実装されていませんので、_get()を読みに行くようになっています。
そしてこの_get()メソッドこそが実際に値を取得する処理になっています。
具体的な処理は長いので割愛しますが、テーブル上のプロパティを内部で配列で保持しており、そこから取得する、ということを行っています。

また、if文の条件はデフォルトではfalseですが、sfDoctrinePluginがtrueに設定してありますので基本的にはif文は通るようになっています。

Doctrine側だけで行っているのは基本的には以上です。
ですがこのままではgetPrice()メソッドが実装されていない場合、No.4でアクセスした場合に例外が発生してしまいます。
この部分を変更しているのがsfDoctrineRecordです。

sfDoctrineRecordはその名の通りsfDoctrinePluginが提供するクラスです。
このsfDoctrineRecordの__call()というマジックメソッド内で、上記の場合にNo.3を呼ぶようになっています。

長々と書いてきましたが、まとめると

◆getPrice()が実装されていればgetPrice()
◆実装されていなければ_get('price')

どのようにアクセスしても、上記のように処理されるような仕組みになっています。
ちなみにSetterに関しても同様です。

ここまで把握してないと苦労するのがアクセサのオーバーライドです。
例えば、


<?php
public function getPrice()
{
  return $this->get('price') * 1000;
}

上記のようにオーバーライドした場合、get('price')がgetPrice()を読みに行くため、ループが発生してしまいます。
この場合は、


<?php
public function getPrice()
{
  return $this->_get('price') * 1000;
}

このように_get('price')を使用するようにします。

ちなみに0.1系ではこのあたりの実装が違っており、_get('price')ではなくrawGet('price')を使うとだけ思っていてください。

このあたりを把握していないと、何かと開発時に困ることが多いかと思います。
気になる方は、Doctrine_RecordやDoctrine_Accessのソースコードを見てみてはいかがでしょうか。
このエントリーが皆様の開発に少しでも役立てば幸いです。