アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

Doctrineのオブジェクト単位での悲観的ロックについて

皆さん、こんばんは。
笹亀です。

ついにiphone4の白を待ちきれずに黒を買いました。
いまさらながらiphoneの素晴らしさを体感しております。

さて本日はDoctrineのオブジェクト単位での悲観的ロックについて解説をさせていただきます。
Doctrineのトランザクション処理を行う際にConnection単位で行うのが一般的な方法ですが、あまり知られていませんが、Doctrineには通常のデータベースで行う方法とは別にオブジェクト単位でロックをかけることができます。
http://www.doctrine-project.org/documentation/manual/1_1/ja/component-overview#%E3%83%9E%E3%83%8D%E3%83%BC%E3%82%B8%E3%83%A3%E3%83%BC%E3%82%92%E3%83%AD%E3%83%83%E3%82%AF%E3%81%99%E3%82%8B

今回は実際にコードを作成して試してみます。
テスト用に事前に作成したMemberオブジェクトに対してロックをかけます。

実装の確認をするためにActionは2つ用意します。
1.テーブルにロックをかける側のAction


  public function executeIndex(sfWebRequest $request)
  {
    echo 'Lock Start!';
    
    //Doctrineのコネクションを取得
    $conn = Doctrine_Manager::getInstance()->getCurrentConnection();

    //コネクションを渡し、ロックマネージャーを生成する
    $lockingManager = new Doctrine_Locking_Manager_Pessimistic($conn);

    //適当なMemberオブジェクトを取得
    $member = Doctrine::getTable('Member')->find(1);

    try {
      //ロックのタイムアウトを設定
      $lockingManager->releaseAgedLocks(300);

      //オブジェクトに対してロックする
      $getLock = $lockingManager->getLock($member, 'jwage');

      //ロックがかかったならばデータを更新
      if ($getLock) {
        echo 'Got Lock!!';
        $member->name  = 'testhoge';
        $member->email = 'sasa@asial.co.jp';
        $member->save();
        sleep(10);
      } else {
        echo 'Sorry, someone else is currently working on this record';
      }
      //ロックを解除する
      $lockingManager->releaseLock($member, 'jwage');

    //例外が発生
    } catch(Doctrine_Lock_Exception $e) {
      echo $e->getMessage();
      //ロックを解除する
      $lockingManager->releaseLock($member, 'jwage');
    }

    echo '';
    echo 'Lock Finished!';
    exit;
  }



2.テーブルロック中にアクセスする側のAction(Memberオブジェクトにアクセスしてデータを表示する


  public function executeTestLock()
  {
    echo 'Get Start';

    $member_obj = Doctrine::getTable('Member')->find(1);

    if ($member_obj) {
      print_r($member_obj->toArray());
    }
    echo '';
    echo 'Get Finished!';
    exit;
  }


実際にロックをかけてからオブジェクトに対してアクセスをして試してみます。

まずは、作成した1のロックする処理を実行します。


次にsleepしている間に2を実行します。



画像では伝えきれませんが、1の処理でロックが掛かっている間は2は実行されずに、1の処理が完了後に2が実行されデータが更新されます。



一般的な方法に比べてスクリプトのコストが低く利用しやすいのが特徴だと思います。オブジェクト単位でロックをかけて情報を操作したいときなどに利用してみてはいかがでしょうか。
※重要なデータを扱う場合はやはりちゃんとトランザクション処理をしてテーブルロックをかけて行う方が一般的ですので、オブジェクトでのロックを使用して実装する際には自己責任でお願い致します。

DoctrineのMaster&Slaveのコネクションを操作するクラスを作成する方法

こんにちは。笹亀です。

symfonyはバージョン2かはSymfonyと頭文字が大文字表記となるとのことで、1.0のころに間違えてSymfonyと書いてツッコミを入れられたことを思い出しました。

さて本日はDoctrineのコネクションをMaster(更新 INSERT,UPDATE,DELETE)とSlave(選択 SELECT)で切り替えを行うProjectConfigurationとDoctrine_Connectionを継承したコネクションを操作するクラスを作成する方法について、ご紹介していきたいと思います。

ある程度の規模の開発をするときにどうしても必要になり、PropelにはあるのになぜDoctrineにはないのだと思い、いろいろソースとWebページを参考に調べながら作成しました。

config/ProjectConfiguration.class.php


<?php
require_once dirname(__FILE__).'/../lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';
sfCoreAutoload::register();

class ProjectConfiguration extends sfProjectConfiguration
{
  protected
    $masterConnection = null,
    $slaveConnection  = null;
 
  public function initializeConnections()
  {
    //Databaseへの接続情報を取得してコネクションのセット(Slave情報のみを取得)
    $file = sfConfig::get('sf_config_dir').'/database_slaves.yml';
    $config = file_exists($file) ? sfYaml::load($file) : array();
    $slave_connections  = array();
    foreach ($config['all'] as $name => $connection) {
      switch ($name) {
        case 'master':
          break;
        default:
          $dsn['slave'][] = $connection['param']['dsn'];
          break;
      }
    }
    
    //Slaveの振り分け処理(とりあえずはランダム選択
    $slave_num = rand(0,count($dsn['slave']) - 1);
    Doctrine_Manager::connection($dsn['slave'][$slave_num], 'slave');
    
    //Masterを必ずCurrentConnectionとしておく
    Doctrine_Manager::getInstance()->setCurrentConnection('master');
    
    //Slaveとマスタのコネクションのデフォルトセット
    $slaves = array();
    foreach (Doctrine_Manager::getInstance()->getConnections() as $name => $conn) {
      switch (true) {
        case 'master' == $name:
          $this->masterConnection = $conn;
          break;
        case 0 === strpos($name, 'slave'):
          $slaves[] = $conn;
          break;
      }
    }
    if (is_null($this->masterConnection)) {
      $this->masterConnection = Doctrine_Manager::connection();
    }
    
  }
 
  //Masterのコネクションを取得する
  public function getMasterConnection()
  {
    $this->masterConnection || $this->initializeConnections();
    return $this->masterConnection;
  }
 
  //Slaveのコネクションを取得する
  public function getSlaveConnection()
  {
    $this->slaveConnection || $this->initializeConnections();
    return $this->slaveConnection;
  }
  
  public function configureDoctrineConnection(Doctrine_Connection $conn)
  {
    $listener = new ConnectionListener(
      $this->getMasterConnection()->getDbh(),
      $this->getSlaveConnection()->getDbh()
    );
 
    $conn->addListener($listener);
  }

  
  public function setup()
  {
    $this->enablePlugins('sfDoctrinePlugin');
  }

  public function configureDoctrine($manager)
  {
    $manager->setAttribute(Doctrine::ATTR_USE_DQL_CALLBACKS, true);
  }
}


Doctrine_EventListener_Interfaceのimplementsとして作成します。preQuery,prePrepareをフックして使用するコネクションを変更します。トランザクション処理をしているときは必ずMaster側のコネクションを使用することを気をつけながら実装が必要です。

lib/ConnectionListener.class.php


<?php
 
class ConnectionListener extends Doctrine_Connection
  implements Doctrine_EventListener_Interface
{
  protected
    $master = null,
    $slave  = null;
 
  public function __construct(PDO $master, PDO $slave)
  {
    $this->master = $master;
    $this->slave  = $slave;
  }
 
  public function preQuery(Doctrine_Event $event)
  {
    //Transaction時は必ずmasterにコネクションをはるようにする
    //コネクション情報にmodulesにトランザクションがつかわれているPraivate変数がある
    //$conn->transaction->getState();  0 = sleep ,1 = active, 2 = busy
    $conn = $event->getInvoker();
    if ($conn->transaction->getState() == 0) {
      $this->forceDbh($conn, 'slave');
    } else {
      $this->forceDbh($conn, 'master');
    }
    
  }
 
  public function postQuery(Doctrine_Event $event)
  {
    $this->restoreDbh($event->getInvoker());
  }
 
  public function prePrepare(Doctrine_Event $event)
  {
    //コネクション情報を取得
    //トランザクション中かチェック()
    //$conn->transaction->getState();  0 = sleep ,1 = active, 2 = busy
    $conn = $event->getInvoker();
    if ($conn->transaction->getState() == 0) {
      $use = 0 === strpos(trim(strtolower($event->getQuery())), 'select') ?
        'slave' : 'master';
      $this->forceDbh($conn, $use);
    } else {
      $this->forceDbh($conn, 'master');
    }
  }
 
  public function postStmtExecute(Doctrine_Event $event)
  {
    $this->restoreDbh($event->getInvoker()->getConnection());
  }
 
  public function preExec(Doctrine_Event $event)
  {
    $this->forceDbh($event->getInvoker(), 'master');
  }
 
  public function postExec(Doctrine_Event $event)
  {
    $this->restoreDbh($event->getInvoker());
  }
 
  // protected
  protected function forceDbh($conn, $type)
  {
    if ($this->$type !== $conn->dbh)
    {
      $conn->options['previous_dbh'] = $conn->dbh;
      $conn->dbh = $this->$type;
    }
  }
 
  protected function restoreDbh($conn)
  {
    if (isset($conn->options['previous_dbh']))
    {
      $conn->dbh = $conn->options['previous_dbh'];
      unset($conn->options['previous_dbh']);
    }
  }
 //
 
  // the remaining methods required by 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 postPrepare(Doctrine_Event $event) { }
  public function preStmtExecute(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) { }
}


データベースの接続情報を作成、変更します。databases.ymlだけではどうしてもうまく実装ができなかったので、Masterはdatabases.ymlに記載して、slaveはdatabase_slaves.ymlを作成して設置することにしました。

databases.yml


all:
  master:
    class: sfDoctrineDatabase
    param:
      dsn:      mysql:host=localhost;dbname=test
      username: user
      password: hogehoge



database_slaves.yml


all:
 slave_1:
    class: sfDoctrineDatabase
    param:
      dsn: mysql://user:hogehoge@localhost/test
 slave_2:
    class: sfDoctrineDatabase
    param:
      dsn: mysql://user:hogehoge@localhost/test


作成当時はまだDocrtineのMaster&Slave構成のConnection操作に対応をしておりませんでしたが、2010年2月24日にプラグインとして正式にリリースされました。
http://www.symfony-project.org/plugins/sfDoctrineMasterSlavePlugin

自分が作成するときに参考にさせていただいたsymfonyのプロジェクトチームのKris Wallsmith氏がリリースしたプラグインですので、信頼してご利用いただけるのではないでしょうか。

※この前、試しに使用してみたらうまく動かすことができませんでした;;
動かしたことがある方、是非とも教えていただけますと幸いです。

symfony DoctrineのTIPS その2

こんばんは。牧野です。

最近、会社で使っているPCも自分用のノートPCもハードディスク容量が少ないせいか、重く感じるようになっていました。
そんな時に発表されたsonyの新しいVAIO Z。シミュレータで出てきた価格にびっくりしつつも、心ひかれています。。。

さて、今日はsymfonyと使うDoctrineのTIPSその2です。ここのところ、またsymfonyを触る機会が増えていました。

1.生SQLを使う
schema.ymlをちゃんと書けばDQLでたいていのことはできるのですが、そのままSQLを実行してデータを取りたい時にどうぞ。
Doctrine_Connectionオブジェクトがあれば、とりあえず何でもできます。


$con = Doctrine::getTable(適当なテーブル)->getConnection();
$con = Doctrine_Manager::getInstance()->getConnection(コネクション名);
$sql = "select * from hogehgoe";
$results = $con->fetchAll($sql);

複数のデータを取得するfetchAll(=fetchAssoc)メソッド、一つのデータを連想配列で取得するfetchRowメソッドあたりが便利だと思います。


2.データ更新時の注意
Doctrineは関連するデータを簡単に引っ張ってこられるところが便利なのですが、データ更新をする際には気をつけたいことがあります。
あるモデルオブジェクトのデータ変更内容を確定させる前に、同じモデルオブジェクトを取得するようなことをすると、確定前のデータ変更内容はリセットされます。

具体例な例を見てみましょう。

config/schema.yml



DvdSeries:
  tableName: dvd_series
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    series_title:
      type: string(255)
      notnull: true

DvdPackage:
  tableName: dvd_package
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    dvd_series_id:
      type: integer(4)
    number:
      type: integer(4)
    package_title:
      type: string(255)
    price:
      type: integer(11)
    release_date:
      type: date
  relations:
    DvdSeries:
      local: dvd_series_id
      foreign: id
      class: DvdSeries
      foreignAlias: DvdPackages




DBの中身


mysql> select * from dvd_series;
+----+-----------------+
| id | series_title    |
+----+-----------------+
|  1 | シーズン1      | 
+----+-----------------+
1 row in set (0.00 sec)

mysql> select * from dvd_package\G
*************************** 1. row ***************************
           id: 1
dvd_series_id: 1
       number: 1
package_title: テストタイトルその1
        price: 3000
 release_date: 2010-03-01
*************************** 2. row ***************************
           id: 2
dvd_series_id: 1
       number: 2
package_title: テストタイトルその2
        price: 4000
 release_date: 2010-04-02
*************************** 3. row ***************************
           id: 3
dvd_series_id: 1
       number: 3
package_title: テストタイトルその3
        price: 4000
 release_date: 2010-05-03
*************************** 4. row ***************************
           id: 4
dvd_series_id: NULL
       number: NULL
package_title: テストタイトル
        price: 3000
 release_date: 2010-06-04
4 rows in set (0.00 sec)


lib/model/doctrine/DvdPackage.class.php


class DvdPackage extends BaseDvdPackage
{
  /**
   * 同一シリーズのDVD発売日をまとめてずらす。
   *
   */
  public function changeAllSeriesReleaseDate($date_string)
  {
    $old_timestamp = strtotime($this->release_date);
    $new_timestamp = strtotime($date_string);
    $today_timestamp = strtotime(date('Y-m-d'));

    $this->release_date = $date_string;

	//変更前、または変更後の発売日が以前の場合は、他のDVDの発売日は変更しない。
    if ($today_timestamp > $old_timestamp || $today_timestamp > $new_timestamp) {
      return $this->save();
    }

    $change_timestamp = $new_timestamp - $old_timestamp;

    if ($this->DvdSeries) {
      foreach ($this->DvdSeries->DvdPackages as $key => $dvd) {
        if ($dvd->id == $this->id) {
          continue;
        }
        $this->DvdSeries->DvdPackages[$key]->release_date = date('Y-m-d', strtotime($dvd->release_date) + $change_timestamp);
      }
    }
    return $this->save();
  }
}


テスト用のアクションクラス


class testActions extends sfActions{
  public function executeIndex(sfWebRequest $request)
  {
    echo "<pre>";
    $con = Doctrine::getTable('DvdPackage')->getConnection();

    //dvd_seriesをもつデータ
    $test_dvd = Doctrine::getTable('DvdPackage')->find(1);

    var_dump($test_dvd->toArray());

    $test_dvd->changeAllSeriesReleaseDate('2010-04-01');

    var_dump($test_dvd->toArray());

    echo "</pre>";
    exit;
  }
}



上のアクションでは、idが1のdvd_packageデータの発売日(release_date)を1ヶ月遅らせて、
関連するdvd_packageデータの発売日も1ヶ月遅らせようとしています。
結果をみると、、


肝心のidが1のdvd_packageデータが2010-03-03のままで、更新できていません。
これは、changeAllSeriesReleaseDateメソッドのforeachの部分が原因です。
$dvdとして自分自身を取得した際に、保存前のrelease_dateが消えてしまったのです。

保存前、というのが問題なので、
$this->release_date = $date_string;
の後に
$this->save();
を入れれば大丈夫です。

今まで2回ほどこのようなケースに出会ったことがありましたので、どこかで役立つことがあるかもしれません。

symfonyのフォームフィルターの活用 +α

こんにちは。小川です。今日はsymfonyのフォームフィルターをご紹介します。

本題に入る前に、symfonyに関連するお知らせが何点かあります。

まず、12月1日にsymfony 1.3/1.4がリリースされました!
symfony 1.3は1.2までとの互換性を保ち古い機能を残したバージョン、symfony 1.4は1.3から古い機能を削除したバージョンになります。

大きな変更点として、以下のような内容があげられます。
◆SwiftMailerメール送信ライブラリを標準で搭載
◆フォームクラスの改良
◆標準のORMがDoctrineに変更
◆Doctrineが1.0から1.2、Propelが1.3から1.4へバージョンアップ

詳しいことについては、以下のリンクをご参照ください。
symfony 1.3/1.4 の新しい機能
プロジェクトを1.2から1.3/1.4にアップグレードする

あともう1点、ぜひ見ていただきたいのが2009年のアドベントカレンダーMore with symfony」(日本名:もっと知りたいsymfony)です。昨年のJobeetのように12月1日から24日まで1日ずつ公開していく形式ですが、今回は前回と大きな違いがあります。

なんと今回のアドベントカレンダーは日本語版も同時公開となっています。実は今回僕も翻訳に参加し、11/12日目の「Doctrine の高度な使用方法、14/15日目の「Doctrine のテーブル継承の活用」の翻訳をさせていただいてます。
また、Jobeetがsymfonyチュートリアルだったのに対し、今回のMore with symfonyはレシピブックのような形で、symfonyをすでにお使いになっている方々の様々なニーズにも応えられる内容となっています。
Jobeetも1.3/1.4用に更新されたものが公開されていますし、symfonyをまだ使ったことのない人はJobeetを、symfonyに慣れてきてもっと色々なことが知りたい場合はMore with symfonyを、といったように、symfonyの学習の流れが出来上がってきたのかなあという感じです。

現時点ですべての章が公開されているわけではありませんが、symfonyをもっと知りたいすべての方々にお勧めできる内容ですので、ぜひぜひ見てみてください!
また、おかしな部分がありましたらこのブログのコメントでも結構ですのでご連絡いただけましたら幸いです。


さて、ここからが本題です。フォームフィルターとはsymfonyのフォームオブジェクトの検索用の拡張をしたクラスです。今回はこのフォームフィルターの仕組みや活用方法などをご紹介していきます。
前述の通りフォームフィルターはフォームの拡張です。ですので、入力フォームを制御するためのウィジェットやバリデータを管理する機構はフォームと同様に持っており、入力フォームを作成し、入力内容のバリデーションをおこなう流れはフォームオブジェクトとほぼ同じです。また、フォームと同様に各ORMごとの拡張も用意されています。

実際には入力された値をもとにDBから取得するオブジェクトのフィルタリングを行うのが主な使い方になると思いますので、今回はsfFormFilterDoctrineに絞ってみていきます。


◆フォームフィルターを使って検索フォームを実装する

何はともあれ、フォームフィルターオブジェクトを実際に作成してみましょう。せっかくですのでsymfony 1.4で動かしていきます。まずは以下のスキーマとフィクスチャーを用意しましょう。

config/doctrine/schema.yml


sfClass:
  columns:
    name:
      type: string(255)
      notnull: true
    namespace:
      type: string(255)

sfMethod:
  columns:
    class_id:
      type: integer
      notnull: true
    name:
      type: string(255)
      notnull: true
    description:
      type: string(10000)
      notnull: true
  relations:
    sfClass:
      local: class_id
      foreignAlias: sfMethods


data/fixtures/fixtures.yml


sfClass:
  ClassLoader:
    name: ClassLoader
    namespace: Symfony\Foundation
  Container:
    name: Container
    namespace: Symfony\Components\DependencyInjection
  EventDispatcher:
    name: EventDispatcher
    namespace: Symfony\Components\EventDispatcher


sfMethod:
  ClassLoader_method_1:
    sfClass: ClassLoader
    name: loadClass
    description: "Loads the given class or interface."
  Container_method_1:
    sfClass: Container
    name: setService
    description: "Sets a service."
  Container_method_2:
    sfClass: Container
    name: getService
    description: "If a service is both defined through a setService() method and with a set*Service() method, the former has always precedence."
  EventDispatcher_method_1:
    sfClass: EventDispatcher
    name: connect
    description: "Connects a listener to a given event name."
  EventDispatcher_method_2:
    sfClass: EventDispatcher
    name: notify
    description: "Notifies all listeners of a given event."


今回はサンプルとして、symfonyのメソッドを検索するフォームを作ります。sfClassはクラス、sfMethodはメソッドです。
symfony 1.2まではこのスキーマとフィクスチャーを読み込んでDBへ反映と各クラスの生成をするタスクはdoctrine:build-all-reloadでした。これ以外にもdoctrine:build-で始まるタスクがいくつもあったためか、 symfony 1.3/1.4からはdoctrine:buildというタスク1つにまとめられ、--allなどのオプションを付けてビルドしたものを色々と指定できるようになりました。

というわけで、コマンドを実行してスキーマとフィクスチャーを読み込ませましょう。databases.ymlは各環境に合わせて設定してください。今回はちょっとしたサンプルなのでSQLiteを使います。

config/databases.yml


all:
  doctrine:
    class: sfDoctrineDatabase
    param:
      dsn: sqlite:///<?php echo realpath(dirname(__FILE__).'/..').'/data/doctrine.db' . "\n" ?>




$ symfony doctrine:build --all --and-load
$ symfony cc


細かいオプションなどが知りたい場合は、以下のコマンドを実行してヘルプを参照してください。



$ symfony help doctrine:build


これで、データベースとテーブルの作成、モデル・フォーム・フォームフィルタークラスの作成、フィクスチャーの読み込みが行われました。毎度のことながら楽にできて助かります。
フォームフィルタークラスはlib/filter/doctrineディレクトリ内にあります。BaseFormFilterDoctrine、sfClassFormFilter、sfMethodFormFilterの3つのクラスファイルとbaseディレクトリが含まれており、BaseFormFilterDoctrineは共通の親クラス、sfClassFormFilterとsfMethodFormFilterはそれぞれのモデルに対応したフォームフィルタークラスになります。

じっくりクラスファイルを見てもいいのですが、動かすほうが楽しいと思うので、一気にモジュールとアクションを作成して動かしましょう。



$ symfony generate:app frontend
$ symfony generate:module frontend method


generate:appタスクですが、1.3/1.4からはデフォルトでエスケーピングの有効化とCSRFトークンの指定を行ってくれるようになりました。ですのでこの状態でXSSCSRF対策はされています。

これでmethodモジュールができたのでindexアクション内で検索ロジックを実装しましょう。

apps/frontend/modules/method/actions/actions.class.php


<?php
class methodActions extends sfActions
{
  public function executeIndex(sfWebRequest $request)
  {
    $this->form_filter = new sfMethodFormFilter();

    $this->methods = array();
    if ($request->hasParameter($this->form_filter->getName()))
    {
      $this->form_filter->bind($request->getParameter($this->form_filter->getName()));
      if ($this->form_filter->isValid())
      {
        // 入力値をもとにクエリ-を作成して、オブジェクトを取得
        $this->methods = $this->form_filter->getQuery()->execute();
      }
    }
  }
}


フォームオブジェクトをご存知の方ならば見慣れたコードでしょう。フォームオブジェクトのインスタンスを作成し、リクエストからパラメータのバインドを行う流れはフォームオブジェクトとほぼ同じです。検索なのでPOSTではなくGETで行うため、値が渡ってきているかどうかで判定しています。
フォームと違うのはフォームフィルターオブジェクトに対してgetQuery()というメソッドを発行しているところでしょう。フォームでは通常、バリデーションをパスした後にsave()メソッドを実行して、入力内容をデータベースに保存します。
フォームフィルターの目的は検索をすることなので、getQuery()メソッドを通じて検索条件が指定された状態のDoctrine_Queryオブジェクトを取得します。なお今回は触れませんが、Propelの場合はQueryがCriteriaになると思ってください。

残るはテンプレートです。早く動かしたいのでできる限り簡潔にします。

apps/frontend/modules/method/templates/indexSuccess.php


<?php echo $form_filter->renderFormTag(url_for('method/index'), array('method' => 'get')) ?>
  <table>
    <tfoot>
      <tr>
        <td colspan="2">
          <input type="submit" value="Search" />
        </td>
      </tr>
    </tfoot>
    <tbody>
      <?php echo $form_filter ?>
    </tbody>
  </table>
</form>
<hr />

<?php if (count($methods)): ?>

<?php foreach ($methods as $method): ?>
  <table style="margin: 10px;">
    <tr>
      <th>メソッド名</th>
      <td>
        <?php echo $method->getName() ?>
      </td>
    </tr>
    <tr>
      <th>説明</th>
      <td>
        <?php echo $method->getRawValue()->getDescription() ?>
      </td>
    </tr>
    <tr>
      <th>クラス</th>
      <td>
        <?php echo $method->getsfClass()->getQualifiedName() ?>
      </td>
    </tr>
  </table>
  <hr />
<?php endforeach; ?>

<?php elseif ($form_filter->isBound()  & & $form_filter->isValid()): ?>
該当するメソッドはありません
<?php endif; ?>


フォームフィルターはフォームを継承しているため、フォームのようにオブジェクト自身をechoすることでウィジェットレンダリングをまとめて行えます。
後はアクションですでに取得しているメソッドオブジェクトのコレクションを表示するためのものです。
クラス名の表示をしている個所がありますが、今回は名前空間に対応したテーブル定義をおこなっているので、修飾されたクラス名を出すようにしましょう。getQualifiedName()メソッドがそれです。sfClassクラスに実装しましょう。

lib/model/doctrine/sfClass.class.php


<?php
class sfClass extends BasesfClass
{
  public function getQualifiedName()
  {
    $name = $this->getName();

    if (!is_null($this->getNamespace()))
    {
      $name = $this->getNamespace() . '\\' . $this->getName();
    }

    return $name;
  }

  public function __toString()
  {
    return $this->getQualifiedName();
  }
}


__toString()マジックメソッドも、修飾名を返すようにオーバーライドしておきます。さてこれでひとまず実装完了です。
ブラウザからこのアクションを確認してみると、検索フォームが出来上がっていると思います。




◆フォームフィルターの仕組みと拡張方法

フォームフィルターの内部を探るため、フォームとフォームフィルターを見比べてみましょう。
lib/filter/doctrine/base/BasesfMethodFormFilter.class.phpとlib/form/doctrine/base/BasesfMethodForm.class.phpを開いてみてください。

まずはウィジェットとバリデータの定義が違います。入力された値の使い方が違うのでこれは当然ですね。フォームフィルター側のnameとdescriptionのウィジェットがsfWidgetFormFilterInputになっていますが、これはフォームフィルター用のウィジェットです。with_emptyオプションをtrueにすると、値が空であるかどうかを判定するためのチェックボックスが付くようになります。試しにdescriptionに対してwith_emptyをtrueに設定してみると、ブラウザでは以下のようになります。



(今気づいたのですが、sfMethodFormをそのままレンダリングすると、各入力フォームのname属性がsf_method[name]のようになりますね。。。これではルーティングのsf_methodとかぶるので、nameFormatを変更するなりしないといけないですね。classが予約語なのでsfを付けてみたのですが、アダになっちゃいましたね。今回はsfMethodFormは使わないのでこのまま進めます。)

この2つのクラスファイルの中でもっとも異なる部分と言えば、フォームフィルター側にあるgetFields()メソッドの存在でしょう。



<?php
public function getFields()
{
  return array(
    'id'          => 'Number',
    'class_id'    => 'ForeignKey',
    'name'        => 'Text',
    'description' => 'Text',
  );
}


これはそれぞれのフィールドが、どのようにして入力された値を検索条件としてクエリ-に追加するかを指定しています。ここで使われているForeignKey、Textなどは汎用的に使われるもので、sfFormFilterDoctrineクラスにadd{$type}Queryというメソッド名で実装されています。symfony 1.4.0では以下のようなものが実装されています。


◆addForeignKeyQuery
配列での指定の場合はIN句、単一の場合は一致しているか指定

◆addEnumQuery
一致しているか指定

◆addTextQuery
is_emptyにチェックが入っている場合はNULLかどうか、そうでなければ入力値をLIKE '%text%'形式で指定

◆addNumberQuery
is_emptyにチェックが入っている場合はNULLかどうか、そうでなければ一致しているか指定

◆addBooleanQuery
一致しているか指定

◆addDateQuery
fromとtoに対してどちらもis_emptyにチェックが入っている場合はNULLかどうか、それ以外の場合はfromに入力があればそれ以降、toに入力があればそれ以前の日付を指定

なお、TextとNumberの場合は必ずsfWidgetFormFilterInputウィジェットを、Dateの場合はsfWidgetFormFilterDateを使うようにしてください。またそれ以外ではこの2つのウィジェットは使用してはいけません。
is_emptyが入る関係で、method[name][text]に値が、method[name][is_empty]に空かどうかのフラグが入って渡されるので、うまく条件指定ができなくなってしまいます。
また、値が空の場合はそもそもメソッドが呼ばれずに処理が飛ばされます。つまり、空であることに対して処理を追加することはできません。ただし前述の通りsfWidgetFormFilterInputはテキストボックスに何も入力しない場合でも、array('text' => '')となり空とは判定されないので、拡張をする場合には注意が必要です。

ちなみにこれらのデフォルトのタイプがどのように判定されるかは、sfDoctrineFormFilterGenerator::getType($column)に記述されています。
以下のような条件でカラムの型を判定して設定しています。



<?php
public function getType($column)
{
  if ($column->isForeignKey())
  {
    return 'ForeignKey';
  }

  switch ($column->getDoctrineType())
  {
    case 'enum':
      return 'Enum';
    case 'boolean':
      return 'Boolean';
    case 'date':
    case 'datetime':
    case 'timestamp':
      return 'Date';
    case 'integer':
    case 'decimal':
    case 'float':
      return 'Number';
    default:
      return 'Text';
  }
}


ちなみにこれらの汎用的な条件指定メソッドはもちろん自分で追加することができます。自動生成したDoctrineのフォームフィルター全てで使いたい場合はBaseFormFilterDoctrineクラスに、それぞれのクラス内でいくつも使いまわす場合はそのクラス内で、add{$type}Queryメソッドを実装すれば、それらが使えるようになります。以下はTextの前方一致版であるTextPrefixタイプをBaseFormFilterDoctrineに実装する例です。

lib/filter/doctrine/BaseFormFilterDoctrine.class.php


<?php
protected function addTextPrefixQuery(Doctrine_Query $query, $field, $values)
{
  $fieldName = $this->getFieldName($field);

  if (is_array($values)  & & isset($values['is_empty'])  & & $values['is_empty'])
  {
    $query->addWhere(sprintf('%s.%s IS NULL', $query->getRootAlias(), $fieldName));
  }
  else if (is_array($values)  & & isset($values['text'])  & & '' != $values['text'])
  {
    $query->addWhere(sprintf('%s.%s LIKE ?', $query->getRootAlias(), $fieldName), $values['text'].'%');
  }
}


といってもaddTextQuery()をパクってきただけですが。引数として、ベースとなるクエリ-オブジェクト、フィールド名、値が渡されます。これらをもとにしてクエリ-を変更していきます。ここで注意したいのは、クエリ-のルートとなるモデルのみにしか指定ができないことです。つまり、sfMethodFormFilterの内部でこれらのメソッドが呼び出された場合は、sfMethodモデルのフィールドのみにしか設定ができないということです。

では、たとえばsfMethodFormFilterでsfClassのnamespaceを検索対象にするにはどうすればいいのでしょうか。この場合の問題は、(1)どうやってクエリ-オブジェクトにsfClassをJOINするか、(2)どうやってJOINしたsfClassオブジェクトのnamespaceを指定するかの2点になります。

まずは(1)のクエリ-オブジェクトにJOINする方法です。そもそもデフォルトではどのようなクエリ-オブジェクトが作られているのでしょうか?これを作成しているのは、実際にクエリーの構築を行っている、sfFormFilterDoctrine::doBuildQuery()メソッドになります。クエリーを作成している部分をみてみましょう。



<?php
protected function doBuildQuery(array $values)
{
  $query = isset($this->options['query']) ? clone $this->options['query'] : $this->getTable()->createQuery('r');

  if ($method = $this->getTableMethod())
  {
    $tmp = $this->getTable()->$method($query);

    // for backward compatibility
    if ($tmp instanceof Doctrine_Query)
    {
       $query = $tmp;
    }
  }

  // ...
}


オプションにqueryがセットされていればそのクエリーが使われ、デフォルトでは対応するモデルのテーブルからcreateQuery()メソッドでクエリーを作成しています。デフォルトではrというエイリアスが割り当てられています。
さらに、$this->getTableMethod()からメソッド名を取得して、クエリーに処理をフックさせることも可能なようです。getTableMethod()メソッドはオプションにtable_methodがセットされている場合はそれを取得するようです。

オプションの指定はコンストラクタの第2引数になります。new sfMethodFormFilter(array(), array('query' => ..., 'table_method' => ...))のように指定可能です。オプションはsetOption()メソッドでもセット可能ですが、オブジェクト作成後にセットする場合は、setTableMethod()やsetQuery()メソッドを使用したほうがよいでしょう。

今回は外部からではなく、sfMethodFormFilter::configure()メソッド内でクエリーをセットするようにします。



<?php
class sfMethodFormFilter extends BasesfMethodFormFilter
{
  public function configure()
  {
    if (!isset($this->options['query']))
    {
      $query = $this->getTable()->createQuery('r')->leftJoin('r.sfClass c');
      $this->setQuery($query);
    }
  }
}


dev環境でアプリケーションを開き、検索を行った後にWebデバッグツールバーでクエリーを確認すると、ちゃんとsfClassがJOINされていると思います。では名前空間を指定するためのウィジェットとバリデータ、そして検索用のメソッドを追加していきましょう。先ほどの汎用的なタイプを指定する形式では、JOINしたモデルのフィールドを指定することはできません。そこで、namespaceに対する専用のメソッドを用意します。

特定のフィールドに対して独自のメソッドを適応させる場合は、add{$column}ColumnQuery()というメソッドを実装します。ちなみにフィールドとカラムという2つの表現を使っていますが、Doctrineではフィールドはデータベース上の名前(アンダースコア)、カラムはプログラム上で扱う際の名前(キャメルケース)といったような使い分けをしています。Columnといった場合はキャメルケースになる、と覚えておいてください。

では実装をしていきます。特にis_emptyなどは使わないので、sfWidgetFormInputTextウィジェットを使用します。



<?php
class sfMethodFormFilter extends BasesfMethodFormFilter
{
  public function configure()
  {
    if (!isset($this->option['query']))
    {
      $query = $this->getTable()->createQuery('r')->leftJoin('r.sfClass c');
      $this->setQuery($query);
    }

    $this->widgetSchema['namespace'] = new sfWidgetFormInputText();
    $this->validatorSchema['namespace'] = new sfValidatorPass(array('required' => false));

    $this->useFields(array('namespace', 'class_id', 'name'), $orderd = true);
  }

  protected function addNamespaceColumnQuery(Doctrine_Query $query, $field, $value)
  {
    // 完全修飾名の場合は先頭のセパレーターをとる
    $isFullyQualified = false;
    if ('\\' === substr($value, 0, 1))
    {
      $isFullyQualified = true;
      $value = substr($value, 1);
    }

    $value .= '%';
    if (!$isFullyQualified)
    {
      $value = '%' . $value;
    }

    $query->addWhere('c.namespace LIKE ?', $value);
  }
}





まずはnamespace用のウィジェットとバリデーターの定義を追加しました。また、descriptionは検索項目として必要ないので検索対象から除外しました。その処理はuseFields()という、symfony 1.3/1.4で新しく追加されたメソッドで、いままでunset($this['created_at'], $this['updated_at'], ...)とやっていたところを、使いたいものだけ指定することができるメソッドです。このメソッドの第2引数にtrueを渡すと、ウィジェットの順番を指定した通りに並び変えてくれます。

そしてaddNamespaceColumnQuery()メソッドで検索条件の指定を行っています。せっかくなのでちょっとしたギミックを加えています。名前空間を絶対指定された場合でも検索可能にし、絶対指定の場合は前方一致になるようにしてみました。

getFields()メソッドは特に変更はしていませんが、これはあくまでもタイプを設定するためのもので、getFields()にないフィールドでもadd{$column}ColumnQuery()が実装されていれば受け付けてくれます。


以上まででフォームフィルターの説明は終わりです。非常に便利な機能なのでぜひぜひ使っていただければと思います。
あと、More with symfonyもぜひみてください!


名前空間で思い出したのですが、12月15日にモダンPHP勉強会が開催され、そこで名前空間についての発表を行います。当日はUstreamでも配信予定ですので、よろしければ見てください。

Doctrine2.0がアルファリリースされたので使ってみた

こんにちは、小川です。
本日Doctrine2.0のAlphaバージョンがリリースされました。簡単なチュートリアルも公開されているので、本日は実際に動かしてみたいと思います。

今回のリリースについては以下の記事をご覧ください。
Doctrine - Doctrine 2 Preview Release

以前僕が書いた、「Doctrine2.0について」という記事をご覧になった方でDoctrine1.xをご存じの方はわかるかと思うのですが、Doctrine2.0は全くの別物です。
上記の記事にも書いてあるとおり、実に90%以上ものコードが書き直されています。とはいえ現在はAlphaバージョン。実装されているのはまだまだ最低限の機能でしかありません。CLIやビヘイビアといった機能が実装されるのはまだ先のことでしょう。

さて、本題に入っていきましょう。実際に動かすにあたって、PHP5.3が必要になります。データベースはSQLite3を使用します。
チュートリアルはDocument内の以下のページに書いてあります。
Doctrine ORM for PHP - Introduction - Sandbox Quickstart

まずはインストールです。とりあえずSubversionを使います。



$ svn co http://svn.doctrine-project.org/trunk doctrine2
$ cd doctrine2/tools/sandbox


doctrine2という名前でチェックアウトしてきました。このなかのtools/sandboxに設定済みのファイルやディレクトリが配置されています。
内部のファイル構成は以下のようになっています。



sandbox/
  Entities/
    - モデルクラスを格納するためのディレクトリ
  xml/
    - XML形式のマッピングファイルが作成されるディレクトリ 
  yaml/
    - YAML形式のマッピングファイルが作成されるディレクトリ 
  cli-config.php
    - CLI用の設定ファイル
  config.php
    - アプリケーション用の設定ファイル
  doctrine
    - コマンドファイル
  index.php
    - アプリケーションファイル


xmlyamlディレクトリは設定次第で使ったり使わなかったりという感じです。僕はphp5.3をphp-5.3.0という名前の実行ファイルとして作成しているので、doctrineコマンドファイルを以下のように変更しました。



#!/usr/bin/env php-5.3.0
<?php

include('doctrine.php');


試しにコマンドを実行すると以下のように、利用可能なタスクの一覧が表示されます。



$ ./doctrine
Doctrine Command Line Interface
Available Tasks:
run-sql --file=<path> | --sql=<SQL>
schema-tool --create | --drop | --update [--dump-sql] [--classdir=<path>]
version


現在はSQLを実行するrun-sql、データベースの生成などを行うschema-tool、バージョンを確認するversionの3つのタスクが有効になっています。このタスクは後ほど、必要に応じて使用していきます。

ではモデルクラスを作成します。Entitiesディレクトリに以下のクラスファイルを作成して、User.phpという名前で保存します。



<?php

namespace Entities;

/**
 * @Entity
 */
class User
{
  /**
   * @Id
   * @Column(type="integer")
   * @GeneratedValue(strategy="AUTO")
   */
  private $id;

  /**
   * @Column(type="string")
   */
  private $name;

  public function getId()
  {
    return $this->id;
  }

  public function getName()
  {
    return $this->name;
  }

  public function setName($name)
  {
    $this->name = $name;
  }
}


これでモデルクラスの定義完了です。ではこのモデルを元にデータベースを作成します。以下のコマンドを実行しましょう。



$ ./doctrine schema-tool --create
Doctrine Command Line Interface
Creating database schema...
Database schema created successfully.


これでデータベースを見てみました。SQLiteを使用している場合はdatabase.sqliteというファイルが作成されているでしょう。データベースとかの設定はconfig.phpに記述してあります。
データベースを直接みてみます。



$ sqlite3 database.sqlite
sqlite> .tables
User
sqlite> .schema User
CREATE TABLE User (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, name TEXT NOT NULL);
sqlite> .quit


先ほどクラス定義時に特に指定は行っていないので、クラス名と同じUserという名前のテーブルが作成されています。
個人的には小文字の方が好きなので小文字に変えてみます。Entities/User.phpを開き、クラスのアノテーションを少し変更します。



<?php
...
/**
 * @Entity
 * @Table(name="user")
 */
class User ...


@Tableというアノテーションを追加しました。これでもう一度Schemaの作成を行います。



$ ./doctrine schema-tool --drop
$ ./doctrine schema-tool --create


これで先ほどと同じ手順で確認するときちんと小文字になっています。アノテーションの種類はたくさんあり、Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.phpの中にアノテーションが1つひとつクラスとして定義されています。@Columnや@Tableに引数のようなものを渡していますが、これは各アノテーションクラスごとにプロパティとして定義されています。ここらへんがどう展開されているかなどは同じディレクトリ内のAnnotationDriver.phpに記述されています。
DoctrineAnnotations.phpをみるとスキーマやリレーションの情報以外に、PreUpdateやPostRemoveなどのフックメソッドや、Doctrineエクステンション用のDoctrineXなどが用意されているようです。Doctrineエクステンションはいわゆるビヘイビアですが、Doctrine2.0でどう扱われるかはまだわかりません。実際に記述してもパースする処理は見当たりませんでした。

少し話がそれましたが、モデルの定義が完了してデータベースも完成したのでいよいよそれを使ってデータベースを操作していきましょう。
ではindex.phpを以下のように変更して、User情報を作成してみましょう。



<?php

require 'config.php';

$user = new \Entities\User;
$user->setName('fivestar');
$em->persist($user);
$em->flush();

echo "User saved!\n";


先ほどUser.phpを作成したときにnamespace Entity;を指定したので名前空間を指定して作成します。オブジェクトを作る流れは特に変わりませんが、保存の流れが変わっています。
$emにはDoctrine\ORM\EntityManagerクラスのインスタンスが格納されており、config.phpで作成されています。このEntityManagerクラスはDoctrine2.0において非常に重要な役割をもっているクラスです。
他の方法があるかはまだ詳しく調べていないのですが、基本的な保存の方法はこのEntityManagerを経由して行います。

EntityManagerに対してpersistメソッドを実行してUserクラスを保存対象にし、flushメソッドを実行することでデータベースへコミットを行っています。EntityManagerの内部で_flushModeというプロパティを持っており、これがFLUSHMODE_IMMIDIATEになっていればpersistした瞬間にflushが行われるようになりますが、デフォルトではFLUSHMODE_COMMITになっているので明示的にflushを行う必要があります。
トランザクションを発行する場合はEntityManagerに従来と同じくbeginTransaction, commit, rollbackといったメソッドが用意されています。commitメソッドは内部でflushも呼んでいるので、そこの流れは基本的に変わらないと思います。



$ php-5.3.0 index.php
User saved!


「User Saved!」が表示されたら保存完了です。保存されたか見てみましょう。直接データベースをみるのではなく、コマンドラインからSQLを実行することが可能です。



$ ./doctrine run-sql --sql="select * from user"
Doctrine Command Line Interface
array(1) {
  [0]=>
  array(2) {
    ["id"]=>
    string(1) "1"
    ["name"]=>
    string(8) "fivestar"
  }
}


現在は配列をそのまま出しているだけですが、とりあえず正しくインサートできている(らしい)ことが確認できました。
ではこのデータをアプリケーションで取得する方法を書いていきます。index.phpを以下のように上書きします。



<?php

require 'config.php';

$q = $em->createQuery('select u from Entities\User u where u.name = ?1');
$q->setParameter('1', 'fivestar');

$user = $q->getSingleResult();

echo "Hello: " . $user->getName() . "!\n";




$ php-5.3.0 index.php
Hello: fivestar!


EntityManagerにcreateQueryというメソッドを発行してDQLを渡しています。このcreateQueryはDoctrine\ORM\Queryクラスを作成するメソッドです。プレースホルダが?1というふうになっていますが、この?のあとの数字をキーにパラメータを指定するようになっています。現在の実装ではsetParameterかsetParametersメソッドで値を渡してあげる必要があるようです。getSingleResultメソッドはfetchOneメソッドと同じようにクエリを実行し単一のエンティティを返すメソッドです。getResultやgetArrayResult、getSingleScalarResultなどのメソッドがあり、いくつかの方法でクエリの実行内容を取得することが可能なようです。

実装をみていたら今までのDoctrine_QueryのメソッドチェーンでDQLを組み立てる実装はこのQueryクラスには実装されていませんでした。Doctrine_Queryクラスに対応するのはDoctrine\ORM\QueryBuilderクラスになります。先ほどの内容をQueryBuilderを使って行います。




<?php

require 'config.php';

$qb = $em->createQueryBuilder()->select('u')->from('Entities\User u')->where('u.name = ?1');
$qb->setParameter('1', 'fivestar');

$user = $qb->getQuery()->getSingleResult();

echo "Hello: " . $user->getName() . "!\n";




$ php-5.3.0 index.php
Hello: fivestar!


QueryBuilderは正確にはDQLを作成するためのクラスで、getQueryメソッドを実行して内部でビルドしたDQLを元にQueryクラスを作成する、というところまで行ってくれます。Doctrine_Queryと違いwhereなどの単位でパラメータを設定できなかったり、selectが必須だったりしています。他にもQuery\Exprというクラスがあり、それを用いてクエリの構築を行うような仕組みになっていたりしています。このあたりは今後実装されていくのではと思います。


チュートリアルはここまでになります。最近はあまりソースを追っていなかったので深いところまでは書けないのですが、現状ではまだまだ使えるレベルにはなっていないというのが率直な感想です。リリースはまだまだ先の話で、ロードマップでは2010年の3月にリリース予定のようです。まだ半年先のことですのでこれからどんどん機能が実装されていくでしょう。

現在DoctrineのブログでもDoctrine2.0の話が増えてきており、今後も様々な情報が配信されると思います。今はまだまだ評価できないですが、リリースが待ち遠しいです。

Doctrineの継承機能について

こんにちは。小川です。

よくあるオブジェクトリレーショナルマッパーは基本的に1つのテーブルに対して1つのモデルオブジェクトを定義します。このときにモデルオブジェクト間に親子関係を持たせたり、いくつかのモデルに対して抽象クラスを作りたいと思ったことはありませんか?Doctrineには継承に関する3つのパターンが用意されており、今回はそれをご紹介したいと思います。

まずは3つのパターンの概要を説明します。

・Simple inheritance
単純に1つのテーブルに対して複数のモデルを作成する方法です。
どのレコードがどのモデルに対応するかは特に判断は行いません。

・Concrete inheritance
モデル1つひとつに対してテーブルを作成する方法です。
テーブルごと分けるため、カラムもテーブルごとに定義されます。

・Column Aggregation inheritance
単純に1つのテーブルに対して複数のモデルを作成する方法です。
Simple inheritanceと違い、各レコードに対してモデルに対応するキーを付与してレコードの判断を行います。


では実際のコードを見つつ、詳細について説明していきたいと思います。

◆Simple inheritance



# schema.yml
Log:
  columns:
    code: string(255)
    message: string(255)

ErrorLog:
  inheritance:
    extends: Log
    type: simple
  columns:
    exception_class: string(255)

ApplicationLog:
  inheritance:
    extends: Log
    type: simple




<?php
// Log.php
class Log extends sfDoctrineRecord
{
    public function setTableDefinition()
    {
        $this->setTableName('log');
        $this->hasColumn('code', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
        $this->hasColumn('message', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
        $this->hasColumn('exception_class', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
    }

}

// ErrorLog.php
class ErrorLog extends Log
{
}

// ApplicationLog.php
class ApplicationLog extends Log
{
}


一番単純なSimple inheritanceです。上記はログを残すためのLogテーブルと、ログの種類に分けていくつかの子クラスを作成する例になります。
この方法では取得した際にレコードがどのモデルに対応するのかは判断しないため、あまり使わないのではないかと思います。
上記のようなログの場合に、取得は全部まとめてやるけど作成するときに色々操作を変えたいというときなどに使用するとよいかと思います。


◆Concrete inheritance



AbstractUser:
  abstract: true
  columns:
    email: string(255)
    name: string(255)
    password: string(255)

User:
  inheritance:
    extends: AbstractUser
    type: concrete
  columns:
    zip: string(8)
    address1: string(255)
    address2: string(255)

Admin:
  inheritance:
    extends: AbstractUser
    type: concrete




<?php
// AbstractUser.php
abstract class AbstractUser extends sfDoctrineRecord
{
    public function setTableDefinition()
    {
        $this->setTableName('abstract_user');
        $this->hasColumn('email', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
        $this->hasColumn('name', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
        $this->hasColumn('password', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
    }

}

// User.php
class User extends AbstractUser
{
    public function setTableDefinition()
    {
        parent::setTableDefinition();
        $this->setTableName('user');
        $this->hasColumn('zip', 'string', 8, array(
             'type' => 'string',
             'length' => '8',
             ));
        $this->hasColumn('address1', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
        $this->hasColumn('address2', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
    }

}

// Admin.php
class Admin extends AbstractUser
{
    public function setTableDefinition()
    {
        parent::setTableDefinition();
        $this->setTableName('admin');
    }

}


上記はUserの抽象クラスを作成して、それをベースにしたuserとadminテーブルを作成し、それぞれに対応するUserモデルとAdminモデルを作成する例です。
AbstractUserというモデルが抽象クラスとなります。schemaでabstractという属性を定義していますが、これをつけたモデルは抽象クラスとして定義されます。
先ほどとは違いテーブルが複数できるため、上記の用にテーブルは分けたい場合に使用します。なお親クラスに定義してあるカラムはそれぞれのテーブルに作成されます。

上記の例では抽象クラスを使用していますが、もちろん抽象クラスである必要はありません。とにかくテーブルを分けたい場合はConcrete inheritanceを使用すると思ってください。


◆Column Aggregation inheritance



Product:
  columns:
    name: string(255)
    description: string(255)
    price: integer(4)
    stock: integer(4)

Book:
  inheritance:
    extends: Product
    type: column_aggregation
  columns:
    author: string(255)

Wear:
  inheritance:
    extends: Product
    type: column_aggregation
  columns:
    size: string(255)
    brand: string(255)




<?php
// Product.php
class Product extends sfDoctrineRecord
{
    public function setTableDefinition()
    {
        $this->setTableName('product');
        $this->hasColumn('name', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
        $this->hasColumn('description', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
        $this->hasColumn('price', 'integer', 4, array(
             'type' => 'integer',
             'length' => '4',
             ));
        $this->hasColumn('stock', 'integer', 4, array(
             'type' => 'integer',
             'length' => '4',
             ));
        $this->hasColumn('type', 'string', 255, array(
             'type' => 'string',
             'length' => 255,
             ));
        $this->hasColumn('author', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
        $this->hasColumn('size', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));
        $this->hasColumn('brand', 'string', 255, array(
             'type' => 'string',
             'length' => '255',
             ));

        $this->setSubClasses(array(
             'Book' =>
             array(
              'type' => 'Book',
             ),
             'Wear' =>
             array(
              'type' => 'Wear',
             ),
             ));
    }

}

// Book.php
class Book extends Product
{
}

// Wear.php
class Wear extends Product
{
}


Simple inheritanceと同様に単一のテーブルを複数のモデルで共有するColumn Aggregation inheritanceですが、Simple inheritanceとは大きな違いがあります。
Productクラスの定義の中に、typeというカラムが定義されています。これはColumn Aggregation inheritanceがレコードがどのモデルに対応するのかを特定するためのカラムです。setSubClassesというメソッドで実際にどのモデルに対してどの値が入るのかを定義しています。デフォルトでは子クラスのクラス名がtypeカラムに入るようになっているのでBookモデルの場合はBook、Wearモデルの場合はWearという文字が入るようになっています。

またこのtypeに入る値や、typeというカラム名は変更可能です。schema.ymlでは以下のようになります。



Product:
  columns:
    name: string(255)
    description: string(255)
    price: integer(4)
    stock: integer(4)

Book:
  inheritance:
    extends: Product
    type: column_aggregation
    keyField: category
    keyValue: 1
  columns:
    author: string(255)

Wear:
  inheritance:
    extends: Product
    type: column_aggregation
    keyField: category
    keyValue: 2
  columns:
    size: string(255)
    brand: string(255)



inheritanceの項目の中にkeyFieldという属性でフィールド名を、keyValueという属性で値を定義します。上記はcategoryというカラムが作成され、Bookの場合は1が、Wearの場合は2が入るようになります。
このkeyFieldで指定したカラム(もしくはtypeカラム)は親クラスに自分自身で定義しても問題はありません。

Column Aggregation inheritanceを用いた場合、親テーブルクラスから値を取得してきてもきちんとkeyFieldを判断して適切なクラスへキャストしてくれますので、Simple inheritanceよりも使えるシーンは多いかと思います。調べ切れていないのですが、たぶんkeyFieldカラムがkeyValueと一致するといった単純な内容でしか判断はできないと思います。複雑な条件を用いる場合はpreDqlSelectなどのフックメソッドを用いて判断する方法が思いつきますが、できる限りDoctrineにあわせた設計をしていく方が賢明ではないかと思います。


紹介は以上になります。上記のことは大抵Doctrineのドキュメントに書いてありますので、興味のある方は是非見てみてください。
Doctrine ORM for PHP - Inheritance


継承の他にもDoctrineにはTemplate(Behavior)という抽象化のための機能があります。Templateは既に定義されているモデルに対して機能をMix-inを行うための仕組みです。汎用性はありますがどうしてもRecordを外部から操作せざるを得ないため、Templateだけでは中々やりたいことが実現できないのではないかと思います。

継承を用いた場合はRecordはもちろん、Tableも継承されますので、ファインダメソッドの抽象化なども非常に簡単に行うことができます。是非こういったDoctrineの機能を活用してみてください!

symfony DoctrineのTIPS

こんにちは、牧野です。
エアコンの入っている部屋にいることが多いせいか、最近夏バテ気味です。。。

さて、今日はDoctrineについてのTIPSです。symfonyとDoctrineの組み合わせで使う中で、役立ちそうなことをいくつか紹介します。

1.複数のデータベースに接続
config/databases.ymlとconfig/doctrine/schema.ymlを次のように書きます。

databases.yml



all:
  main:
    class:        sfDoctrineDatabase
    param:
      classname:  DoctPDO
      dsn:        mysql:dbname=shop_test;host=localhost
      username:   root
      password:
      encoding:   utf8
      persistent: true
      pooling:    true

  sub:
    class:        sfDoctrineDatabase
    param:
      classname:  DoctPDO
      dsn:        mysql:dbname=news_test;host=localhost
      username:   root
      password:
      encoding:   utf8
      persistent: true
      pooling:    true


ここではコネクションを表す名前を「main」と「sub」にしています。好きな名前でかまいません。
schema.ymlには「connection: (コネクションを表す名前)」を追加します。



News:
  connection: sub
  tableName: news
  columns:
    id:
      type: integer(8)
      unsigned: 1
      primary: true
      autoincrement: true
    title: string(2147483647)
    body: string(2147483647)
  actAs:
    Timestampable: { }



ところで、symfonyのdoctrineコマンドには、データベースからschema.ymlを生成する


symfony doctrine:build-schema

があります。現状ではこれを使うとconnectionの行が消えてしまうので気をつけましょう。


2.sqlの数を減らす
Doctrineは、1つのモデルから関連するモデルを芋づる式に簡単に引っ張ってこれて便利です。
ただ、DQLの書き方を気を付けないと大量のSQLが実行されてしまうことがあります。

アクション内


//書き方その1
$this->result_list = Doctrine_Query::create()
  ->from('PurchaseLog l')
  ->execute();





//書き方その2
$this->result_list = Doctrine_Query::create()
  ->from('PurchaseLog l')
  ->leftJoin('l.Shop s')
  ->leftJoin('l.Items i')
  ->leftJoin('i.Category c')
  ->execute();




テンプレート



<table>

<?php foreach ($result_list as $val): ?>
  <tr>
    <td><?php echo $val->id; ?></td>
    <td><?php echo $val->purchase_date; ?></td>
    <td><?php echo $val->Shop->shop_name; ?></td>
    <td>

  <?php foreach ($val->Items as $val2): ?>
    <?php echo $val2->item_name; ?>(<?php echo $val2->Category->category_name; ?>)<br>
  <?Php endforeach; ?>

    </td>
  </tr>
<?php endforeach; ?>

</table>


デバッグコンソールを表示すると、、、
書き方その1の場合


書き方その2の場合


Joinメソッドを書いておかないと1つのデータを表示する度にSQLが実行されます。(上の結果は、表示するデータが1件の場合です。)
一覧画面のデータ数が多い場合は特に注意です。


3.関連データをまとめて削除
あるデータを消す時に、紐付くデータもまとめて消したい、という時は、schema.ymlに


cascade: [delete]

を追加します。
以下の書き方の場合、Userテーブルのデータを消すと、紐付いているProfileデータも
削除されます。



User:
  tableName: user
  columns:
    id:
      type: integer(8)
      unsigned: 1
      primary: true
      autoincrement: true
    profile_id: integer(8)
...
  relations:
    Profile:
      local: profile_id
      foreign: id
      type: one
      cascade: [delete]
  actAs:
    Timestampable: {  }



以上、DoctrineのTIPSでした。

Flex4とsymfonyを連携させて遊んでみた。

最近仕事で毎日PHP4と格闘している橋本です、こんにちは。

さて、4繋がりということで、今日は先日β版がリリースされたばかりのFlashBuilder4を使って、
Flex4とsymfonyを連携させて遊んでみました。

Asialブログの購読者の方々の中に「symfonyって何??」って方はいらっしゃらないと思いますが、「Flexって何??」って方は中にはいらっしゃるかもしれないので、軽く説明。

Flexとは、Adobe社の提供するRIA(Rich Interface Application)開発のフレームワークです。
Adobeの得意分野であるFlashの技術をベースとしています。
インターフェースの作成には、MXMLというXMLを拡張した言語を用いて行います。
実行時には、MXMLファイルがswfファイルに変換され、クライアントのFlashPlayer上で実行されます。
Flexの開発キットのFlexSDKはAdobe社から配布されており無償でDL可能です。
(最新版:Adobe Flex4 SDK
また、有償ですが、FlashBuilder4(旧称 FlexBuilder)というEclipseベースのIDEを利用することも可能です。
(ちなみに、一個前のバージョンのFlexBuilder3は、学生の方ならば無料で利用することができます。こちら。学生さん、うらやましすぎです。)

今回はこのFlashBuilder4を使ってアプリ作成を進めていきたいと思います。


まず、FlashBuilder4をDL&インストールしましょう。

30日間は無料で使えます。

こちら

ちなみに、FlexBuilder3のライセンスを持っている場合は、β期間中は30日を超えても利用可能みたいです。)

次にsymfonyのインストールです。

と、言いたいところですが、既にsymfonyをお使いになっておられる方が多いかと思いますので、割愛。

今回はsymfony-1.2.8-DEVを使って進めていきます。


始める前に、そもそもどうやってFlashPHPでデータをやりとりするのかという話になるかと思いますので、軽く説明。

FlashPHPでデータのやりとりをする際には、AMF(ActionScript Messaging Format)というファイル形式を利用します。他にもXMLだったり、JSONだったりでやりとりすることも可能ですが、AMFはバイナリ形式でやりとりが可能なため、他の二つの方法よりも高速にデータのやりとりを行うことができます。

(少し古いデータですが、三つのデータ形式を利用した結果が載ってるページがありました。こちら

PHPでAMFを利用するためのライブラリとして主に、amfphp、WebOrb、SabreAMFの三つがありますが、今回はSabreAMFを利用します。

SabreAMFを使うのは、SabreAMFを利用したsymfonyのpluginが公開されていた、という単純な理由からです。

sfAmfPlugin

今回はこのpluginを使ってすすめていきたいと思います。

では、まず、symfonyプロジェクトを作成します。



symfony generate:project amf_sample
symfony generate:app frontend


次に、sfAmfPluginをインストールします。キャッシュのクリアもお忘れなく。



symfony plugin:install sfAmfPlugin
symfony cc


プラグインを使って、AMF-Serviceを作成します。



symfony amf:create-service --package=frontend.user Register
symfony cc


引数としてパッケージ名を渡していますが、これは渡しても渡さなくても大丈夫です。
パッケージ名を渡さなかった場合には、lib/services以下に、パッケージ名を渡した場合は、lib/services/パッケージ名にAMF-Serviceクラスのファイルが作成されます。

上記の例では、lib/services/frontend/user/RegisterServices.class.phpが作成されます。

次に、このサービスの中継役をするモジュールを作成します。

symfonyの場合は、モジュールとアクションを作ってやればOKです。



symfony generate:module amfgateway
symfony cc


apps/frontend/modules/amfgateway/actions/actions.class.php




class amfgatewayActions extends sfActions
{
  public function executeAmf(sfWebRequest $request) {
    $this->setLayout(false);

    $gateway = new sfAmfGateway();
    $response = sfContext::GetInstance()->getResponse();
    $response->setContent($gateway->service());
    return sfView::NONE;
  }
}



次に、サービスの中身を作っていきます。
といっても今日は簡単なことしかしません。
Flex側で取得した値をDoctrineに渡して保存するのみ。

DBはこんな感じの簡単なものを用意しました。



User:
  tableName: user
  columns:
    id:
      type: integer(4)
      primary: true
      autoincrement: true
    name: string(255)
    email: string(255)
    phone: string(255)
    created_at: timestamp(25)
    updated_at: timestamp(25)
  actAs:
    Timestampable: { }


記述の仕方は普通にDoctrineを使用する場合と同じです。

lib/services/frontend/user/RegisterService.class.php



class RegisterService extends sfAmfService {
  // ユーザデータを保存する
  public function createUserData($user_data) {
    $user = new User();
    $user->fromArray($user_data);
    $user->save();

    return 'OK';
  }
}


次にFlexを使ってクライアント側をつくります。
外観はデザインモードにして、適当に作成。
スクリプト部分をソースモードにして、これまた適当に作成します。
デザインモードで適当に作ったため、座標の値が中途半端なのですが、ご愛嬌ってことで見逃してください。(す、すみません。)

ソースはこんな感じ。


<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/halo" minWidth="1024" minHeight="768">
	<fx:Script>
		<![CDATA[
			import mx.collections.ArrayCollection;
			import mx.controls.Alert;
			import mx.events.CloseEvent;
			import mx.rpc.remoting.RemoteObject;
			import mx.rpc.events.FaultEvent;
			import mx.rpc.events.ResultEvent;
				
			private function createUserData():void {
				var userData:Array = new Array();
				
				userData['name'] = userName.text;
				userData['email'] = email.text;
				userData['phone'] = phone.text;

                remote.addEventListener("result", function (e:ResultEvent):void {
                 	Alert.show(e.result.toString(), "Registrated!", null, null, function (e:CloseEvent):void {
						clearInputText();
                 	});
                });
                 
                remote.addEventListener("fault", function(event:FaultEvent):void {
                 	Alert.show(event.fault.toString(), "Error");
                });
                
                remote.createUserData(userData);
			}
			
			private function clearInputText():void {
				userName.text = "";
                email.text = "";
                phone.text = "";
   			}
		]]>
	</fx:Script>
	<fx:Declarations>
		<mx:RemoteObject 
        id="remote"
        endpoint="http://amf_sample/amfgateway/amf"
        source="frontend.user.RegisterService"
        destination="hoge" 
        showBusyCursor="true"
    />	
	</fx:Declarations>
	
	<mx:TabNavigator x="90.7" y="16.35" width="419" height="261" cornerRadius="0">
		<mx:Canvas label="register member" width="100%" height="100%" cornerRadius="0" alpha="1.0" borderStyle="none">
			<mx:Label x="50" y="27" text="Your name:"/>
			<s:TextInput x="149.25" y="25.25" id="userName"/>
			<mx:Label x="79.000015" y="67.65" text="email:"/>
			<s:TextInput x="149.25" y="64.65" id="email"/>
			<mx:Label x="75.000015" y="108.65" text="phone:"/>
			<s:TextInput x="149.25" y="111.65" id="phone"/>
			<s:Button x="231.3" y="170.65" label="clear" id="clear" click="clearInputText()"/>
			<s:Button x="118.3" y="170.65" label="create"  id="create" click="createUserData()"/>
		</mx:Canvas>
		<mx:Canvas label="show members" width="100%" height="100%">
			<mx:DataGrid x="15" y="10" height="265" width="453">
				<mx:columns>
					<mx:DataGridColumn headerText="ID" dataField="userId" width="50"/>
					<mx:DataGridColumn headerText="Name" dataField="userName" />
					<mx:DataGridColumn headerText="Email" dataField="email"/>
					<mx:DataGridColumn headerText="phone" dataField="phone"/>
				</mx:columns>
			</mx:DataGrid>
		</mx:Canvas>
	</mx:TabNavigator>
	
</s:Application>



RemoteObjectがサーバとのやりとりを行うオブジェクトです。
この中でも特に重要なのが、endpointとsourceです。
endpointは、さきほど作った中継役のモジュールのアクションを指定します。
sourceは、サービスをパッケージ名を含んだ形で指定します。

ようやく準備が整ったので、さっそく実行してみましょう。
実行するクライアントは、4つながりでsafari4を使用します。

実行時


登録完了時


FlashBuilderを使ってさくっと作っただけでしたが、一応ちゃんと動くものができました。

今回は登録だけでしたが、次回はDBからデータを持ってきて表示するところまでやってみたいと思います。

(本当は、今回でデータの取得と表示まで出来るようにしたかったのですが、Doctrineを使ったデータの取得部分ではまってしまったため延期です。すみません…。)

Doctrine 2.0について

こんにちは。小川です。

ブログとは全く関係ないですが昨日引っ越しをしました。今まで埼玉に住んでいたのですが、都内に引っ越してきたので会社まで自転車で通えるようになりました。
入居申し込みから申請を急ピッチで行うことなったおかげでしばらくインターネットに繋げないので発狂しそうです。
入社2年目になりますが、今年度は仕事も生活も1人でこなせるよう、心機一転がんばっていきたいです。

本日のブログはDoctrine 2.0について少しお話ししたいと思います。

Doctrineは現在1.1が最新となりますが、昨年の9月頃から(僕が確認したのがその頃だったのでもっと前からかもしれませんが)Subversionのtrunk上でDoctrine 2.0の開発が進められていました。
現在もまだbranchは作られていないですが、先日Doctrineのブログで2.0に関する記事が投稿されていました。

Glimpse of Doctrine 2.0

記事の最初の方でDoctrine2.0に関連することが3つ挙げられています。

◆ Doctrine 2.0はPHP 5.3以上が必要
◆ Doctrine 2.0は速い
◆ Doctrine 1.x系のメンテナンス期間の延長


1つめですが、現在開発中のPHP 5.3以上が必須環境になります。
2.0では1.x系に比べかなり内部構造が変わります。今回はそれと同時に名前空間を使用した実装になっています。他にも__DIR__定数や__callStatis()メソッドなどPHP 5.3で追加される新たな機能も使われています。内部構造については後ほど話します。

2つめはパフォーマンスが改善されるようです。ブログの記事によると、ハイドレーション(DBから取得した値からレコードオブジェクトを作る機能)が大幅に短縮されるようです。
Doctrine 1.1では5,000件のレコードのハイドレーションに約4.34秒かかっていたものが、Doctrine 2.0では5,000件で約1.43秒、10,000件でも約3.46秒と倍以上速くなっていることがわかります。どちらかというと元が遅いという感じですが、改善されるということは非常にありがたいですね。

3つめについてですが、詳しいことは知りませんがメンテナンスが延長されるようです。PHP 5.3の採用もあって中々移行できないと思われますので非常にありがたいですね。

ブログには他にもいくつか簡単な説明がされています。
もっとも違いがわかるのは、モデルの定義方法でしょう。

Doctrine 1.1



<?php

class User extends Doctrine_Record
{
    public function setTableDefinition()
    {
        $this->hasColumn('id', 'integer', null, array(
          'primary' => true,
          'auto_increment' => true
        ));
        $this->hasColumn('username', 'string', 255);
    }
}


Doctrine 2.0



<?php

/**
 * @DoctrineEntity
 * @DoctrineTable(name="user")
 */
class User
{
    /**
     * @DoctrineId
     * @DoctrineColumn(type="integer")
     * @DoctrineGeneratedValue(strategy="auto")
     */
    public $id;
    /**
     * @DoctrineColumn(type="varchar", length=255)
     */
    public $username;
}


Doctrine 1.x系ではDoctrineのモデルはDoctrine_Recordというクラスを継承して、その中でカラムやリレーションなどをメソッドとして実行し、定義を行っています。
Doctrine 2.0では同じものとは思えないくらい変わっており、ドキュメントコメント上に定義を行います。それどころか、親クラスの指定すら不要です。

Doctrine 2.0ではリフレクションを用いてドキュメントコメントのパースし、モデルとのマッピングを行っています。
ちなみにマッピングデータはClassMetadataクラスに、ドキュメントコメントのパースなどはAnnotationクラスなどで行う実装になっています。

また構造が大きく変わったと最初にお伝えしましたが、実際にリポジトリをみるとよくわかると思います。

/branches/1.1/lib/Doctrine
/trunk/lib/Doctrine

Doctrine 1.1まではDoctrineクラスを除く全てのクラスがDoctrineディレクトリ以下に配置されていましたが、Doctrine 2.0からはCommon、DBAL、ORMという3つの層に大きくわけられています。

DBAL / DataBaseAbstractionLayer
 データベースの抽象化に関連する、データベースのドライバーや型の情報、コネクションなどのクラスが含まれています。
ORM / ObjectRelationalMapping
 モデルクラス関連やクエリークラスなどが含まれています。
Common
 名前の通り、共通の例外やイベントなどのクラスが含まれています。

1.1まではごちゃごちゃしてる印象がありましたが、非常にすっきりしていますね。
気になるのは1.1まであったPagerやTemplateなどの機能ですが、現在はリポジトリには含まれていないようです。PagerはともかくTemplateシステムは内部で実装してくれないとと思うのですが、Annotationをみるかぎりでは実装されていないようですし、非常に使い勝手の良かった機能なだけに早めに実装をみてみたいところです。

昨年の9月にみたときは、1.1までのモデルクラスであるDoctrine_RecordがEntityというクラスになるという内容でしたがだいぶ変わりましたね。Entityクラスがリポジトリ上から消えたときは目を疑いましたが、ブログに書いたと言うことはモデルクラスに関してはFixしてきたということでしょうか。

Tracのロードマップでは、最初の頃は今年の9月1日にリリースと書いてありましたが今見たら来年の3月1日に変更されていました。まだPHP 5.3が正式にリリースしていませんし、じっくり時間をかけていいものを作ってもらいたいですね。
今後も継続して、Doctrineを追っていこうと思います。

DoctrineのSELECT句と集計関数の扱い方

こんにちは。小川です。
今回はDoctrineのSELECT句と集計関数の扱い方などについて書いていこうと思います。

始める前に今回使うデータベースのスキーマとサンプルとなるフィクスチャデータを書いておきます。一応symfony 1.2上で使うことを前提としています。



# config/doctrine/schema.yml
User:
  actAs:
    Timestampable:
  columns:
    id:
      type:           integer(4)
      notnull:        true
      primary:        true
      autoincrement:  true
    name:
      type:           string(255)
      notnull:        true

Account:
  actAs:
    Timestampable:
  columns:
    id:
      type:           integer(4)
      notnull:        true
      primary:        true
      autoincrement:  true
    user_id:
      type:           integer(4)
      notnull:        true
    amount:
      type:           integer(4)
      notnull:        true
  relations:
    User:
      foreignAlias:   Accounts




# data/fixtures/fixtures.yml
User:
  katsuhiro:
    name:       Katsuhiro Ogawa
  massie:
    name:       Masahiro Tanaka

Account:
  Acount_1:
    amount:     1500
    User:       katsuhiro
  Acount_2:
    amount:     3000
    User:       katsuhiro
  Acount_3:
    amount:     4000
    User:       massie
  Acount_4:
    amount:     10000
    User:       massie
  Acount_5:
    amount:     1500
    User:       katsuhiro


上記を用いて symfony doctrine:build-all-reload コマンドを実行すればデータは整います。
Userはユーザ、Accountは売上のモデルとなります。今回はこの2つのモデルを使って説明していきます。

では話を進めて参りましょう。集計関数というのはSUMやMAX、AVGといったSQL上で集計を行う関数のことです。



SELECT u.name, SUM(a.amount) FROM user u LEFT JOIN account a ON u.id = a.user_id GROUP BY u.name;


上記はユーザごとの販売金額の合計を集計するSQLの例です。
このSQLを単純にDoctrine_Queryにすると以下のようになります。



<?php
$totalAmountList = Doctrine_Query::create()->select('u.name, SUM(amount)')
  ->from('User u, u.Accounts a')->groupBy('u.name')->execute();


さて、$totalAmountListからSUM(amount)を取得したい場合はどう指定すればよいでしょうか。
実際に上記のDoctrine_Queryが取得する内容をみてみましょう。
$totalAmountListをtoArrayなどしてダンプしてもいいですが、単純なDQLの結果を取得するだけであればコマンドラインで確認するのが楽かと思います。
実はsymfonyにはDQLの結果を取得するコマンドが用意されています。早速使ってみてみましょう。



$ symfony doctrine:dql "SELECT u.name, SUM(a.amount) FROM User u, u.Accounts a GROUP BY u.name"
>> doctrine  executing dql query
DQL: SELECT u.name, SUM(a.amount) FROM User u, u.Accounts a GROUP BY u.name
found 2 results
-
   id: '1'
  name: 'Katsuhiro Ogawa'
  SUM: '6000'
  Accounts:
    -
      SUM: '6000'
-
   id: '2'
  name: 'Masahiro Tanaka'
  SUM: '14000'
  Accounts:
    -
      SUM: '14000'


「SUM」と表示されているのがSUM(a.amount)の部分になります。括弧以降の部分が表示されていない状態です。
実際に$totalAmountListから取得する場合、この状態であれば $totalAmountList->SUM とやることで取得できます。
また、連結している全てのオブジェクトに集計部分がついています。エイリアスで制御できないからでしょうね。

上記のように、集計関数はDoctrineでも特に意識せずに使用することができます。ただ、SUMという名前になってしまうのは正直使いづらいですよね。
そこで登場するのがSQLのAS句です。これをSELECT句内で指定すると、



$ symfony doctrine:dql "SELECT u.name, SUM(a.amount) AS total_amount FROM User u, u.Accounts a GROUP BY u.name"
>> doctrine  executing dql query
DQL: SELECT u.name, SUM(a.amount) total_amount FROM User u, u.Accounts a GROUP BY u.name
found 2 results
-
  id: '1'
  name: 'Katsuhiro Ogawa'
  total_amount: '6000'
  Accounts:
    -
      total_amount: '6000'
-
  id: '2'
  name: 'Masahiro Tanaka'
  total_amount: '14000'
  Accounts:
    -
      total_amount: '14000'



このようにきちんと変換され、 $totalAmountList->total_amount として取得できるようになります。
もちろんAS句は全ての値に使用できます。また、AS自体は省略しても問題なく動作します。
DATE関数でグループ化したいといった場合も、 DATE(created_at) AS create_date のようにしてあげます。
また全ての項目をそのまま取得したい場合は、u.*のようにエイリアスアスタリスクで指定します。

Doctrineはこのように、集計関数などを用いたクエリでも柔軟に対応してくれます。
Doctrineで開発するときは、是非こういった点を活用していきましょう。