ログ日記

作業ログと日記とメモ

PHP5用のORM作った

O/Rマッピングライブラリを自作した。


ずっと前から自作ORMを作り替えたいと思っていて *1S2JDBCのようなものが作りたいからどうせならJavaやるかと思ってJavaをやり始め、やっぱりJavaよりPHPがいいと思ってPHPに戻ってきて、そうしたらまたORMに不満が出てきた。


作ってみたら三日くらいである程度形になった。ここ数年間作り替えたいと思い続けてきたもやもやは一体何だったんだってくらいにさくっとできたのでアップ。
https://github.com/nishimura/Tsukiyo


最近のPHPフレームワークやORM事情はさっぱり分からない。
http://codezine.jp/article/detail/5858
ここをざっくり読んだけど今ひとつ心惹かれなかったので、何も参考にせずに作りたいように作った。


以下メモ程度に使い方。
まだ作り始めて一週間も経ってないので色々変わるかもしれない。

初期設定

DSNの設定、DBテーブル情報のファイルパス、DBのテーブル解析するかどうか、Voクラス名のprefixを設定する。

<?php
require_once 'Tsukiyo/Db.php';
$db = new Tsukiyo_Db();
$db->setDsn('pgsql:host=localhost dbname=mydb user=myuser password=pass')
    ->setConfigFile('writable/config.ini');
    ->setAutoConfig(true)
    ->setVoPrefix('Vo_');

必須なのは setDsnとsetConfigFile。setConfigFileは書き込み可能なパスを指定する。
実際使うときはSingletonクラスで包んだり共通設定ファイルのようなものにべた書きしたり。

SELECT文

<?php
$item = $db->from('Item')
    ->eq('name' => 'Name 1')
    ->result();
$itemAndSubItems = $db->from('Item')
    ->join('SubItem')
    ->eq('Item.name' => 'Name 1')
    ->result();
$items = $db->from('Item')
    ->like('Item.name', 'a')
    ->order('itemId')
    ->iterator();

こんな感じ。
昔作ったORMだとjoinが出来なかったし条件の指定が微妙だったので、これらをごっそり変えたかった。


foreach ($itemAndSubItems->SubItem as $subItem) で結合先のデータを取ってこれる。
一対多なのか多対一なのかは自動判別。一対一は今後作る(多分)。
eq(他、ne、lt、le、などS2JDBC風)の引数はarray('プロパティ名' => 変数)の形式。テーブルが一つならarray('name' => 'foo')でいい。joinしてカラム名がかぶった場合はSQLを書く場合と同様にarray('Item.name' => 'foo')などと指定する必要がある。同一カラム名がないならテーブル名(Vo名)は不要。


複数取得はiterator()で。
ここで取得するイテレーターはforeach文が進むごとにデータベースにfetchしにいくのでちょっとクセがある。が、詰め替えするより処理は速いはず(測ってない)。


変数名の変換法則はDB側 sub_item.sub_item_id が SubItem->subItemId になるような感じ。
joinで自己結合はできない。同じテーブル名をjoinすることは不可(たぶんSQLエラーになる)。

UPDATE文

<?php
$item->name = 'Item 2';
$db->save($item);

特に説明なし。insertじゃなくてupdateだと明示するならば $db->update($item) でもいい。

INSERT文

<?php
$subItem = $db->generateVo('SubItem');
$subItem->itemId = $item->itemId;
$subItem->name = 'Sub Name 1';
$db->save($subItem);

これも特に説明なし。新しい行のためのクラスはgenerateVoで生成、updateじゃなくてinsertと明示する場合は $db->insert($subItem) でもいい。

DELETE文

<?php
$db->delete($subItem);

これもそのまま。$db->delete('SubItem', $subItem->subItemId) というようにテーブル名+プライマリキーでもいい。

ページャー

<?php
require_once 'Tsukiyo/Pager.php';
$pager = new Tsukiyo_Pager($items, 10);
$pagerHtml = $pager->getHtml();

簡易ページャー
上の$itemsはイテレーター。イテレーターを渡すと、勝手にlimit、offsetを設定してくれる。コンストラクタ引数の10は一ページに表示する行数。ページャーのリンク表示数を変える場合は new Tsukiyo_Pager($items, 10, 5); のようにする。

その他

イテレーターの参照に注意。

<?php
$iterator = $db->from('Goods')->iterator();
$sum = 0;
foreach ($iterator as $goods){
    $sum += $goods->price;
}
echo $sum;

これは想定通り合計が出る。


イテレーターが返すVoは同一のオブジェクトで中に参照を保持しているので

<?php
$iterator = $db->from('Goods')->iterator();
$arr = array();
foreach ($iterator as $goods){
    $arr[] = $goods;
}
$sum = 0;
foreach ($arr as $goods){
    $sum += $goods->price;
}
echo $sum;

これはダメ。配列に入れたオブジェクトは全て同じものを指す(具体的には最後の行)。
すぐに処理せずに詰め替えたい場合は

<?php
$iterator = $db->from('Goods')->iterator()->setCloneVo(true);
$arr = array();
foreach ($iterator as $goods){
    $arr[] = $goods;
}
...

setCloneVo(true)を呼び出す必要がある。


また、joinなどで二重以上のループがあると

<?php
foreach ($items as $item){
  echo $item->itemId;
  foreach ($item->SubItem as $subItem){
  }
  echo $item->itemId;
}

一回目のechoと二回目のechoは別のものになる。中のループが終わった段階でデータベースfetch用のカーソルは先に進んでいるのでVoの中身が変わっている。この場合もsetCloneVoが必要。

毎回指定するのがめんどくさそうなら初期設定に追加 or デフォルトの動作を変更するかもしれない。


また、一対多で二重以上のループにする場合は、一回のSQL文で全ての結合を取得するためにS2JDBCのようにorderでループ順に結果が並ぶようにしないといけない。




600行ほどのOrm.phpと120行ほどのIterator.phpがほとんど全てなので、ここを見ればだいたい書いてある。
今のところDBはPostgreSQLのみ対応。テーブル一覧やカラム一覧、外部結合の情報を取得するSQLを書けば他のDBでも動く、はず。