Asial Blog

Recruit! Asialで一緒に働きませんか?

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

カテゴリ :
バックエンド(プログラミング)
タグ :
Tech
symfony
Doctrine
小川です。WEB+DB PRESS Vol.46のプレゼントで応募した「はまちちゃんのセキュリティ講座の生イラスト&直筆サイン入り色紙」が当たってしまいました。感激です。ありがとうございます。

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

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

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

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

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

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

  1. <?php
  2. // Product
  3. class Product extends BaseProduct {}
  4.  
  5. // BaseProduct
  6. abstract class BaseProduct extends sfDoctrineRecord {}
  7.  
  8. //sfDoctrineRecord
  9. abstract class sfDoctrineRecord extends Doctrine_Record {}
  10.  
  11. // Doctrine_Record
  12. abstract class Doctrine_Record extends Doctrine_Record_Abstract implements Countable, IteratorAggregate, Serializable {}
  13.  
  14. // Doctrine_Record_Abstract
  15. abstract class Doctrine_Record_Abstract extends Doctrine_Access {}
  16.  
  17. // Doctrine_Access
  18. abstract class Doctrine_Access extends Doctrine_Locator_Injectable implements ArrayAccess {}
  19.  
  20. // Doctrine_Locator_Injectable
  21. class Doctrine_Locator_Injectable {}

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

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

  1. <?php
  2. public function get($fieldName, $load = true)
  3. {
  4.     if ($this->_table->getAttribute(Doctrine::ATTR_AUTO_ACCESSOR_OVERRIDE)) {
  5.         $componentName = $this->_table->getComponentName();
  6.  
  7.         $accessor = isset(self::$_customAccessors[$componentName][$fieldName])
  8.             ? self::$_customAccessors[$componentName][$fieldName]
  9.             : 'get' . Doctrine_Inflector::classify($fieldName);
  10.  
  11.         if (isset(self::$_customAccessors[$componentName][$fieldName]) || method_exists($this, $accessor)) {
  12.             self::$_customAccessors[$componentName][$fieldName] = $accessor;
  13.             return $this->$accessor($load);
  14.         }
  15.     }
  16.  
  17.     return $this->_get($fieldName, $load);
  18. }

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に関しても同様です。


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

  1. <?php
  2. public function getPrice()
  3. {
  4.   return $this->get('price') * 1000;
  5. }

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

  1. <?php
  2. public function getPrice()
  3. {
  4.   return $this->_get('price') * 1000;
  5. }

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

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


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