* @package projBop.Ethna * @version $Id: projBop_AppObject.php 27 2006-12-05 10:06:16Z segawa $ */ // 定義等取得に使うプロトタイプキャッシュ配列キー define('PROJBOP_APPOBJECT_PROTOCACHE', "__ethna_projBop_appObject_proto_cache"); // アソシエーションが用いるサーチ専用マネージャの格納キー define('PROJBOP_APPOBJECT_SEARCH_MANAGER_CACHE', "__ethha_projBop_appObject_searchManager_cache"); /** * Project-Bop Ethna AppManager Class * * @author BoBpp < bobpp@users.sourceforge.jp > * @package projBop.Ethna * @version 0.1 */ class projBop_AppManager extends Ethna_AppManager { /** * 論理削除に対応した getObjectList() * * @access public * @param string $object 取得するオブジェクト名 * @param array $filter 検索条件 * @param array $order 並び替え条件 * @param int $offset 取得オフセット数 * @param int $num 取得件数 * @return mixed (array: [0]=>$filterにヒットした件数, [1]=>$offset, $countにより指定された件数のオブジェクトの配列. Ethna_Error:エラー) */ function getObjectList($object, $filter=null, $order=null, $offset=null, $num=null) { $filter = $this->_getNotDeletedFilter($object, $filter); return parent::getObjectList($object, $filter, $order, $offset, $num); } /** * 論理削除に対応した getObjectPropList() * * @access public * @param string $object 取得するオブジェクト名 * @param mixed $keys (array:取得キー名, null:すべてのカラムを取得) * @param array $filter 検索条件 * @param array $order 並び替え条件 * @param int $offset 取得オフセット数 * @param int $num 取得件数 * @return mixed (array: [0]=>$filterにヒットした件数, [1]=>$offset, $countにより指定された件数のオブジェクトの配列. Ethna_Error:エラー) */ function getObjectPropList($object, $keys = null, $filter=null, $order=null, $offset=null, $num=null) { $filter = $this->_getNotDeletedFilter($object, $filter); return parent::getObjectPropList($object, $keys, $filter, $order, $offset, $num); } /** * 論理削除に対応した getObjectList() * * @access public * @param string $object 取得するオブジェクト名 * @param mixed $keys (array:取得キー名, null:すべてのカラムを取得) * @param array $filter 検索条件 * @return mixed (array:プロパティ一覧 null:エントリなし Ethna_Error:エラー ) */ function getObjectProp($object, $keys=null, $filter=null) { $filter = $this->_getNotDeletedFilter($object, $filter); return parent::getObjectProp($object, $keys, $filter); } /** * 論理削除済みレコードを検索しないようにするフィルタを生成する * * @access protected * @param array $filter 検索条件 * @return array 論理削除に対応した検索条件 */ function _getNotDeletedFilter($object, $filter) { $object =& $this->backend->getObject($object); if (isset($object->prop_def['deleted_at']) === true) { $deletedFilter = array( 'deleted_at' => new Ethna_AppSearchObject(NULL, OBJECT_CONDITION_NE), ); $filter = array_merge($filter, $deletedFilter); } return $filter; } /** * id => toStringな連想配列を作成する * * @access public * @param string $class 対象のクラス名 * @param array $filter 検索条件 * @param array $order 並び替え条件 * @param int $offset オフセット * @param int $num 件数 * @return mixed (array: 0=>取得件数 1=>必要な配列, Ethna_Error: エラー発生) */ function getObjectNameList($class, $filter=null, $order=null, $offset=null, $num=null) { list($num, $objects) = $this->getObjectList($class, $filter, $order, $offset, $num); if (Ethna::isError($num)) { return $num; } $names = array(); for ($i=0, $n=count($objects); $i<$n; $i++) { $names[$objects[$i]->getId()] = $objects[$i]->toString(); } return array($num, $names); } /** * SQL文からオブジェクトを取得する * * @access public * @param string $model モデル名(Arrayの場合は複数テーブルのJOIN) * @param string $sql SQL文 * @return mixed (Array:オブジェクト配列, Ethna_Error:エラー) */ function getObjectListBySQL($model, $sql) { // SELECT文でなければ・・・Return if(strpos(strtolower($sql), "select") !== 0) { return Ethna::raiseError(sprintf( "Not Invalid Query [SQL: %s]", $sql )); } // 形式をそろえる if (is_scalar($model) === true) { $model = array($model); } // 追加テーブル分の定義を追加する if (count($model) > 1) { $parentProto =& $this->backend->getObject($model[0]); for ($i=1,$n=count($model); $i<$n; $i++) { $subProto =& $this->backend->getObject($model[$i]); $define = $subProto->getDef(); $table = $subProto->_getPrimaryTable(); foreach ($define as $column => $value) { $newColumnName = sprintf("%s_%s", $table, $column); if (isset($parentProto->prop_def[$newColumnName]) === false) { $parentProto->prop_def[$newColumnName] = $value; } } } $prop_def = $parentProto->getDef(); } $db =& $this->_getReadOnlyDB(); $r =& $db->query($sql); if (Ethna::isError($r)) { return $r; } $ret = array(); while(($data = $db->fetchRow($r, DB_FETCHMODE_ASSOC)) !== null) { $object =& $this->backend->getObject($model[0]); if (isset($prop_def) === true) { $object->prop_def = $prop_def; } foreach($data as $k=>$v) { $object->set($k, $v); } $ret[] =& $object; } return array(count($ret), $ret); } /** * 読み込み専用のDBを取得する * * 複数ある場合はランダムに選択 * * @access protected * @return DB */ function &_getReadOnlyDB() { $list = $this->backend->getDBList(); if (Ethna::isError($list)) { return $list; } $readOnly = array(); $write = array(); for ($i=0, $n=count($list); $i<$n; $i++) { if ($list[$i]['type'] == DB_TYPE_RO) { $readOnly[] =& $list[$i]['db']; } else if ($list[$i]['type'] == DB_TYPE_RW) { $write[] =& $list[$i]['db']; } } $readOnlyNum = count($readOnly); if ($readOnlyNum) { return $readOnly[rand(0, $readOnlyNum-1)]; } $writeNum = count($write); if ($writeNum) { return $write[rand(0, $writeNum-1)]; } // 結局DBが無かった場合 return Ethna::raiseError("Not found DB object.", E_DB_NODSN); } } /** * Project-Bop Ethna AppObject Class * * @author BoBpp < bobpp@users.sourceforge.jp > * @package projBop.Ethna * @version 0.3 * * - CHANGELOG - * version 0.3 - at 2006.11.14 - by BoBpp < bobpp@users.sourceforge.jp > * - リレーション周りがすごいことになってきたのでちょっと簡略実装化 * * version 0.2 - at 2006.10.30 - by BoBpp < bobpp@users.sourceforge.jp > * - リレーションの面倒を見るオブジェクトを作ってそっちに委譲を計画して * そっちがよさそうなので採用 * * version 0.1 - at 2006.10.27 - by BoBpp < bobpp@users.sourceforge.jp > * - とりあえず作成 */ class projBop_AppObject extends Ethna_AppObject { /** @var array 所属する関係にあるテーブル情報 */ var $belongs_to = null; /* 例 * (classroom Classだとすれば) * var $belongs_to = array( * 'student' => array( * 'key' => 'classroom_id', // 外部キーは student_id * ), * ); */ /** @var array 1:1関係にあるテーブル情報 */ var $has_one = null; /* 例 * (これがstudent Classだとすれば) * var $has_one = array( * // 学生はプロフィールを持っている (テーブル分割) * 'profile' => array( * 'key' => 'user_id', * ), * ); */ /** @var array 1:n関係にあるテーブル情報 */ var $has_many = null; /* 例 * (これがstudent Classだとすれば) * var $has_many = array( * // 生徒はあるクラスに所属する * 'classroom' => array( * 'key' => 'classroom_id', // 外部キーは classroom_id * ), * // 生徒はある県に住んでいる * 'pref' => array( * 'key' => 'pref_id', // 外部キーは pref_id * ), * ); */ /** @var array n:m関係にあるテーブル情報 */ var $has_many_through = null; /* 例 * (これがstudent Classだとすれば) * var $has_many_through = array( * // 生徒は複数のクラブに参加している * 'club' => array( * 'relation' => 'membership', * 'from_key' => 'student_id', * 'dest_key' => 'club_id', * ), * ); */ /** @var array n:m関係にあるテーブル情報(エンティティ表情報を含む) */ var $has_and_belongs_to_many = null; /* 例 * (これがstudent Classだとすれば) * var $has_and_belongs_to_many = array( * // 生徒は複数のクラブに参加している * 'club' => array( * 'relation' => 'student_club', // エンティティ表は student_club で * 'from_key' => 'student_id', // student側の外部キーに当たるのは student_id * 'dest_key' => 'club_id', // club側の外部キーに当たるのは club_id である * ), * ); */ /** @var array ActAsなリスト */ var $act_as = null; /* 例 * var $act_as = array( * 'list', 'tagging', * ); */ /** * コンストラクタ * * @access public */ function projBop_AppObject(&$backend, $key_type = null, $key = null, $prop = null) { // 親コンストラクタの呼び出し parent::Ethna_AppObject($backend, $key_type, $key, $prop); // この時点でテーブルの定義は完成している // Associationの設定 $associations = array( 'belongs_to' => 'BelongsTo', 'has_one' => 'HasOne', 'has_many' => 'HasMany', 'has_many_through' => 'HasManyThrough', 'has_and_belongs_to_many' => 'HasAndBelongsToMany', ); foreach ($associations as $prop=>$class) { if ($this->$prop !== null) { foreach ($this->$prop as $name=>$value) { // リレーション名の別名対応(自分に関連とか) $objectName = isset($value['object']) ? $value['object'] : $name; if (isset($this->$name) === false) { $className = "projBop_Relation_" . $class; $this->$name =& new $className($this, $objectName, $value); } } } } } /** * オブジェクトを命名化する * * @abstract * @access public * @return string オブジェクト名? */ function toString() { return $this->getObjectName() . " #" . $this->getId(); } /** * php5用 * * @access public * @return string オブジェクト名? */ function __toString() { return $this->toString(); } /** * 自身が有効なオブジェクトか返す * * @access public * @return boolean 有効かどうか */ function isValid() { // もとのisValid if (parent::isValid() === false) { return false; } // 論理削除済み? if (isset($this->prop_def['deleted_at']) && $this->get('deleted_at') != null) { return false; } return true; } /** * 自分自身のオブジェクト名を返す * * @access protected * @return string 自分のオブジェクト名(Ethna_Backend::getObject() で呼べる名前)を返す */ function getObjectName() { $name = get_class($this); return strtolower(substr($name, strpos($name, '_')+1)); } /** * 連携オブジェクトを取得する * * @access public * @param string $name オブジェクト名 * @return mixed (array:オブジェクト配列, Ethna_Error:エラー発生, null:該当オブジェクトなし) */ function getRelationObjects ($name) { if (isset($this->$name) === false) { Ethna::raiseError(sprintf('Not found Association [%s]', $name)); } return $this->$name->getsAll(); } /** * 連携オブジェクトを追加する * * @access public * @param projBop_AppObject &$obj 連携するオブジェクト * @return mixed (Ethna::Error: エラー) */ function appendObject($name, &$obj) { if (isset($this->$name) === false) { Ethna::raiseError(sprintf('Not found Association [%s]', $name)); } return $this->$name->set($obj); } /** * バックアップと比較してプロパティが変更されているか調べる * * @access public * @return boolean バックアップとの比較結果 等しければTRUE */ function isChanged() { return $this->prop === $this->prop_backup; } /** * レコードを追加する * * @access public * @return mixed (0: 正常終了, Ethna_Error: エラー発生) */ function add() { $date = date('Y-m-d H:i:s'); // 追加日時の設定 if (isset($this->prop_def['created_at'])) { $this->set('created_at', $date); } // 最終更新日時の設定 if (isset($this->prop_def['updated_at'])) { $this->set('updated_at', $date); } return parent::add(); } /** * レコードを更新する * * @access public * @return mixed (0: 正常終了, Ethna_Error: エラー発生) */ function update() { // 最終更新日時の設定 if (isset($this->prop_def['updated_at'])) { $this->set('updated_at', date('Y-m-d H:i:s')); } return parent::update(); } /** * レコードおよびプロパティを削除する * * @access public * @return mixed (0: 正常終了, Ethna_Error: エラー発生) */ function remove() { // 削除日時の設定 if (isset($this->prop_def['deleted_at'])) { $r = $this->destroy(); if (Ethna::isError($r)) { return $r; } // プロパティ/バックアップ/キャッシュクリア $this->id = $this->prop = $this->prop_backup = null; $this->_clearPropCache(); return 0; } else { return parent::remove(); } } /** * レコードのみを削除する プロパティのクリアは行わない * * @access public * @return mixed (0: 正常終了, Ethna_Error: エラー発生) */ function destroy() { if (isset($this->prop_def['deleted_at'])) { $this->set('deleted_at', date('Y-m-d H:i:s')); $r = parent::update(); if (Ethna::isError($r)) { return $r; } } else { // コードは parent::remove()より $sql = $this->_getSQL_Remove(); $r =& $this->my_db_rw->query($sql); if (Ethna::isError($r)) { return $r; } } return 0; } } /** * リレーションシップオブジェクト * * @author BoBpp < bobpp@users.sourceforge.jp > * @package projBop.Ethna.AppObject */ class projBop_Relation { /** @var projBop_AppObject 元のオブジェクト */ var $parent = null; /** @var Ethna_Backend バックエンド */ var $backend = null; /** @var array リレーションの情報 */ var $relation = null; /** @var string 自分自身のモデル名 */ var $parentModelName = null; /** @var string リレーションするオブジェクト名*/ var $relationModelName = null; /** * コンストラクタ * * @access public * @param projBop_AppObject &$object 元のオブジェクト * @param string $objName リレーションするオブジェクトの名前 * @param array $rel リレーション情報 */ function projBop_Relation(&$object, $name, $relation=null) { $this->parent =& $object; $this->backend =& $object->backend; $this->parentModelName = $object->getObjectName(); $this->relationModelName = $name; $this->relation = $relation; $this->_setAutoRelation(); } /** * 関連オブジェクトを主キーで指定して設定する * * @access public * @param int $id 関連オブジェクトの主キー * @return mixed (0: 成功, Ethna_Error: エラー) */ function setById ($id) { $id = $this->_getPK($id); if (Ethna::isError($id)) { return $id; } $object =& $this->_getObject($id); if ($object === null) return; return $this->set($object); } /** * 関連オブジェクトをハッシュから生成して保存する * * @access public * @param array $prop_hash プロパティ値のハッシュ * @return mixed (0: 成功, Ethna_Error: エラー) */ function create($prop_hash = null) { $object =& $this->backend->getObject($this->relationModelName, null, null, $prop_hash); return $this->set($object); } /** * プロトオブジェクトを取得する * * @access public * @param string $name オブジェクト名 * @return プロトオブジェクトのコピー(値の変更等されては困るため) */ function &_getProtoObject($name) { $name = strtolower($name); if (isset($GLOBALS[PROJBOP_APPOBJECT_PROTOCACHE][$name]) === false) { $GLOBALS[PROJBOP_APPOBJECT_PROTOCACHE][$name] =& $this->backend->getObject($name); } return $GLOBALS[PROJBOP_APPOBJECT_PROTOCACHE][$name]; } /** * リレーションを自動的に設定する * * @abstract * @access protected */ function _setAutoRelation() { // Abstruct Method die("It's Abstruct Method."); } /** * サーチマネージャを取得する * * @access protected * @return Ethna_AppManager サーチマネージャ */ function &_getSearchManager() { if(isset($GLOBALS[PROJBOP_APPOBJECT_SEARCH_MANAGER_CACHE]) === false) { $GLOBALS[PROJBOP_APPOBJECT_SEARCH_MANAGER_CACHE] =& new projBop_AppManager($this->backend); } return $GLOBALS[PROJBOP_APPOBJECT_SEARCH_MANAGER_CACHE]; } /** * オブジェクトを取得する * * @access public * @param int $id=null 主キー NULLの場合は空のオブジェクト * @param boolean isDestObject=true 親オブジェクトに類するオブジェクトの際はFALSEへ * @return projBop_AppObject */ function &_getObject($id=null) { $targetObject =& $this->_getProtoObject($this->relationModelName); $name = $targetObject->getObjectName(); if ($id !== null) { $idDef = $targetObject->getIdDef(); $object =& $this->backend->getObject($name, $idDef, $id); // 論理削除・レコード不在チェック if ($object->isValid() === true) { return $object; } else { $null = null; return $null; } } else { return $this->backend->getObject($name); } } /** * このリレーションしてよいオブジェクトか判定する * * @access protected * @param projBop_AppObject &$object 判定したいオブジェクト * @return mixed (TRUE: OK, Ethna_Error: 問題あり) */ function _checkRelation(&$object) { // 型のチェック if (is_subclass_of($object, 'projBop_AppObject') === false) { return Ethna::raiseError(sprintf('Wrong ObjectType. [%s]', get_class($object))); } // クラス名のチェック $objectName = $object->getObjectName(); $requireObjectName = $this->objectName; if ($requireObjectName !== $objectName) { return Ethna::raiseError(sprintf('Wrong Object Inserted! Inserted: [%s], Required: [%s], Relation-Parent: [%s]', $objectName, $requireObjectName, $this->parent->getObjectName())); } return true; } /** * オブジェクトから主キーを抜き出す * * @access public * @param mixed $id (int: 主キー, projBop_AppObject:関連オブジェクト) * @return mixed (int: 主キー, Ethna_Error: エラー) */ function _getPK($id) { if (is_object($id) && $this->_checkRelation($id)) { $id = $id->getId(); } else if (is_numeric($id) === false) { return Ethna::raiseError('Arg is not Vaild!'); } return (int)$id; } } /** * オブジェクトを一つだけ持つリレーション * (belongs_to, has_one) * * @author BoBpp < bobpp@users.sourceforge.jp > * @package projBop.Ethna.AppObject.Relation */ class projBop_Relation_OneObject extends projBop_Relation { /** * 関連するオブジェクトを配列で取得する * * @access public * @return array [0]=>projBop_AppObject */ function getsAll() { return array($this->get()); } } /** * オブジェクトをたくさんもつリレーション * * @author BoBpp < bobpp@users.sourceforge.jp > * @package projBop.Ethna.AppObject.Relatioon */ class projBop_Relation_ManyObjects extends projBop_Relation { /** * 関連するオブジェクトを一件取得する * * @abstract * @access public * @param int $id 関連するモデルの主キー * @return mixed (projBop_AppObject: 成功, Ethna_Error: エラー) * @todo 現在はAbstruct ObjectContainer復活後はそちらにロードしてから返す形にする IDも一発で取れてよさそう */ function &get ($id) { // Abstruct Method die('Its Abstruct Method (projBop_ManyObjects::get())'); } /** * 関連オブジェクトをすべて取得する * * @abstract * @access public * @return array オブジェクト * @todo 現在はAbstruct. 将来ObjectContainerが復活すればこのメソッドで処理するように書き換える */ function getsAll() { // Abstruct Method die('Its Abstruct Method (projBop_ManyObjects::getsAll())'); } /** * 指定した関連オブジェクトがコンテナに存在するか返す * * @access public * @param mixed $id (int: 主キー, Object(projBop_AppObject): 関連オブジェクト) * @return boolean (true: 存在する, false: 存在しない) */ function isExist($id) { $id = $this->_getPK($id); if (Ethna::isError($id)) { return $id; } $target =& $this->get($id); return Ethna::isError($target) === false && $target !== null; } /** * 関連オブジェクトコンテナが空であるかを返す * * @access public * @return boolean (TRUE: 空, FALSE: 空でない) */ function isEmpty() { return $this->count() === 0; } } /** * Belongs_Toなリレーション * * @author BoBpp < bobpp@users.sourceforge.jp > * @package projBop.Ethna.AppObject.Relation */ class projBop_Relation_BelongsTo extends projBop_Relation_OneObject { /** * リレーションを自動設定する * * @access protected */ function _setAutoRelation() { if ($this->relation === null) { $this->relation = array(); } if (isset($this->relation['key']) === false) { $this->relation['key'] = $this->relationModelName . '_id'; } } /** * ひもづいているオブジェクトを取得する * * @access public * @return mixed (projBop_AppObject:ひもづいているオブジェクト Ethna_Error:エラー発生) */ function &get() { $object =& $this->_getObject($this->parent->get($this->relation['key'])); if ($object === null) { return Ethna::raiseError(sprintf( "Not found PrimaryObject. [obj:%s id:%s]", $this->relationModelName, $this->parent->get($this->relation['key']))); } return $object; } /** * オブジェクトを設定する * * @access public * @param projBop_AppObject &$object オブジェクト * @param boolean $autoSave 自動的に保存するかどうか * @return mixed (null: 設定成功でセーブされなかった, 0:設定成功セーブも成功, Ethna_Error: エラー発生) */ function set(&$object, $autoSave=true) { $r = $this->_checkRelation($object); if (Ethna::isError($r)) { return $r; } // 自動的にリレーションを設定する $this->parent->set($this->relation['key'], $object->getId()); // 自動セーブを利用する場合 if ($autoSave) { return $this->parent->isValid() ? $this->parent->update() : $this->parent->add(); } } } /** * HasOneなリレーション * * @author BoBpp < bobpp@users.sourceforge.jp > * @package projBop.Ethna.AppObject.Relation */ class projBop_Relation_HasOne extends projBop_Relation { /** * リレーションを自動設定する * * @access protected */ function _setAutoRelation() { if ($this->relation === null) { $this->relation = array(); } if (isset($this->relation['key']) === false) { $this->relation['key'] = $this->relationModelName . '_id'; } } /** * リレーションに必要なフィルタを返す * * @access protected * @return array リレーションするためのフィルタ */ function _getRelationFilter() { return array( $this->relation['key'] => $this->parent->getId(), ); } /** * リレーションされたオブジェクトを取得する * * @access public * @return mixed projBop_AppObject: オブジェクト * NULL: リレーションするものがない * Ethna_Error: エラー発生 */ function &get() { $sm =& $this->_getSearchManager(); list($num, $arr) = $sm->getObjectList( $this->relationModelName, $this->_getRelationFilter() ); if (Ethna::isError($num)) { return $num; } if ($num == 1) { return $arr[0]; } else if ($num == 0) { $null = null; return $null; } else { Ethna::raiseError(sprintf('HasOne Relation is not vaild!')); } } /** * オブジェクトを設定する * * @access public * @param projBop_AppObject &$object オブジェクト * @param boolean $autoSave 自動的オブジェクトを保存するかどうか * @return mixed (0:正常終了, Ethna_Error:エラー) */ function set(&$object, $autoSave=true) { // 対象のオブジェクトなのか調査 $r = $this->_checkRelation($object); if (Ethna::isError($r)) { return $r; } // 旧来のオブジェクトは外部キーをNULL化します $oldObject =& $this->get(); if (Ethna::isError($oldObject) === false && $oldObject !== null) { $oldObject->set($this->relation['key'], NULL); $r = $oldObject->update(); if (Ethna::isError($r)) { return $r; } } // 外部キーの設定 $object->set($this->relation['key'], $this->parent->getId()); // 自動セーブを利用する場合 if ($autoSave) { return $object->isValid() ? $object->update() : $object->add(); } } /** * 空であるか確認する * * @access public * @return boolean (true: 空, false: オブジェクトが入っている) */ function isEmpty() { return $this->get() === null; } /** * 該当のオブジェクトがセットされているのか確認する * * なお、主キーで比較します * * @access public * @param mixed $obj (int: オブジェクトの主キー, object: オブジェクト) * @return boolean (true: このオブジェクトが関連オブジェクト, false: そうでない) */ function exist($obj) { $id = $this->_getPK($id); if (Ethna::isError($id)) { return $id; } $obj =& $this->get(); return $id == $obj->getId(); } } /** * HasManyなリレーション * * @author BoBpp < bobpp@users.sourceforge.jp > * @package projBop.Ethna.AppObject.Relation */ class projBop_Relation_HasMany extends projBop_Relation_ManyObjects { /** * リレーションを自動設定する * * @access protected */ function _setAutoRelation() { if ($this->relation === null) { $this->relation = array(); } if (isset($this->relation['key']) === false) { $this->relation['key'] = $this->parentModelName . '_id'; } } /** * リレーションをするためのフィルタを取得する * * @access protected * @return array フィルタ */ function _getRelationFilter() { return array( $this->relation['key'] => new Ethna_AppSearchObject($this->parent->getId(), OBJECT_CONDITION_EQ), ); } /** * 関連するオブジェクトを一件取得する * * @access public * @param int $id 関連するオブジェクトの主キー * @return mixed projBop_AppObject: 関連するオブジェクト * NULL: 該当の関連オブジェクトがない * Ethna_Error: エラー発生 * @todo ObjectContainerの利用に向けてロードの切り出し等 */ function &get($id) { $proto =& $this->_getProtoObject($this->relationModelName); $data = $this->find( array( $proto->getIdDef() => $id, $this->relation['key'] => $this->parent->getId(), ), null, 0, 1 ); if (Ethna::isError($data)) { return $data; } return $data[0]; } /** * 関連するオブジェクトをすべて取得する * * @access public * @return mixed array: オブジェクトの配列 * Ethna_Error: エラー発生 * @todo ObjectContainer利用に向けたロード部分切り出しとその利用 */ function getsAll() { return $this->find(); } /** * 関連するオブジェクトを条件指定して取得する * * @access public * @param array $filter 検索条件 * @param array $order 並び替え条件 * @param num $offset 取得オフセット * @param num $count 取得個数 * @return mixed (Array: 0=>filterでかかった件数, 1=>order, offset, countされた個数のオブジェクト配列) * (Ethna_Error: エラー発生) */ function find($filter=null, $order=null, $offset=null, $count=null) { $sm =& $this->_getSearchManager(); $filter = array_merge($filter, $this->_getRelationFilter()); list($num, $data) = $sm->getObjectList( $this->relationModelName, $filter, $order, $offset, $count ); if (Ethna::isError($num)) { return $num; } return $data; } /** * 関連オブジェクトの個数を調べる * * @access public * @return mixed int: 関連オブジェクトの個数 * Ethna_Error: エラー */ function count() { $proto = $this->_getProtoObject($this->relationModelName); $filter = $this->_getRelationFilter(); $sql = $proto->_getSQL_SearchLength($filter); $db =& $this->backend->getDb(); $r =& $db->query($sql); if (Ethna::isError($r)) { return $r; } $temp = $db->fetchRow($r, DB_FETCHMODE_ASSOC); return $temp['id_count']; } /** * オブジェクトを追加する * * @access public * @param projBop_AppObject &$object リレーションさせるオブジェクト * @param boolean $autoSave 自動セーブ機能を使うかどうか * @return mixed (0: 成功, Ethna_Error: エラー発生) */ function set(&$object, $autoSave=true) { $r = $this->_checkRelation($object); if (Ethna::isError($r)) { return $r; } // 自動的にリレーションを設定する $object->set($this->relation['key'], $this->parent->getId()); // 自動セーブを利用する場合 if ($autoSave) { return $object->isValid() ? $object->update() : $object->add(); } } } /** * HasAndBelongsToManyなリレーション * * @author BoBpp < bobpp@users.sourceforge.jp > * @package projBop.Ethna.AppObject.Relation */ class projBop_Relation_HasAndBelongsToMany extends projBop_Relation_ManyObjects { /** * リレーションを自動設定する * * @access protected */ function _setAutoRelation() { if ($this->relation === null) { $this->relation = array(); } $myTable = $this->parentModelName; $destTable = $this->relationModelName; if (isset($this->relation['relation']) === false) { $this->relation['relation'] = $myTable < $destTable ? $myTable . '_' . $destTable : $destTable . '_' . $myTable; } if (isset($this->relation['from_key']) === false) { $this->relation['from_key'] = $myTable . '_id'; } if (isset($this->relation['dest_key']) === false) { $this->relation['dest_key'] = $destTable . '_id'; } } /** * リレーションされたオブジェクトを一件取得する * * @access public * @param int $id 関連オブジェクトの主キー * @return mixed projBop_AppObject: 関連オブジェクト * NULL: 関連オブジェクトが存在しない * Ethna_Error: エラー発生 * @todo ObjectContainerへの対応・ロード等 */ function &get($id) { // 主キーを正確に取得 $id = $this->_getPK($id); if (Ethna::isError($id)) { return $id; } // 外部キー読み込み $destKey = $this->relation['dest_key']; // 一件に絞るためのフィルタ $filter = array( $destKey => $id, ); // クエリ実行 $db =& $this->backend->getDb(); $r =& $db->query($this->_getRelationSelectSQL($filter)); if (Ethna::isError($r)) { return $r; } if ($r->numRows() === 1) { return $this->_getObject($id); } else { return Ethna::raiseError(sprintf( "Invalid RelationObject Requested. [RequesObject:%s #%d] [Parent:%s #%d]", $this->relationModelName, $id, $this->parent->getObjectName(), $this->parent->getId() )); } } /** * リレーションされたオブジェクトをすべて取得する * * @access public * @return mixed array: リレーションされたオブジェクトの配列 * Ethna_Error: エラー発生 */ function getsAll() { $db =& $this->backend->getDb(); $sm =& $this->_getSearchManager(); $r =& $db->query($this->_getRelationSelectSQL()); if (Ethna::isError($r)) { return $r; } // フェッチしながらオブジェクト取得 $proto = $this->_getProtoObject($this->relationModelName); $name = $this->relationModelName; $idDef = $proto->getIdDef(); $destKey = $this->relation['dest_key']; $objects = array(); for ($i=0, $n=$r->numRows(); $i<$n; $i++) { $temp = $r->fetchRow(DB_FETCHMODE_ASSOC); $objects[] =& $this->backend->getObject($name, $idDef, $temp[$destKey]); } return $objects; } /** * オブジェクトにリレーションを設定して保存する * * @access public * @param projBop_AppObject &$object リレーション元のオブジェクト */ function set(&$object) { // リレーションできるオブジェクトであるかチェック $r = $this->_checkRelation($object); if (Ethna::isError($r)) { return $r; } // 未保存のオブジェクトに対しては主キー不明のため連携できない if ($object->isValid() === false) { return Ethna::raiseError('Cant create relation to Not saved object.'); } // 未保存の親オブジェクトに対しても主キー不明の状態ではエラーを返す if ($this->parent->isValid() === false) { return Ethna::raiseError('Cant create relation to Not saved object.'); } // がんばってクエリ書いて実装 $db =& $this->backend->getDb(); $r =& $db->query($this->_getRelationInsertSQL($object)); if (Ethna::isError($r)) { return $r; } return 0; } /** * 連携表を検索するSQLを返す * MySQL専用なので他のRDBMSを使う場合は書き換えてください * * @access private * @return string SQL文 */ function _getRelationSelectSQL($otherFilter=null) { $db =& $this->backend->getDb(); $id = $this->parent->getId(); $mainFilter = array( $this->relation['from_key'] => $id, ); if ($otherFilter === null) { $filter = $mainFilter; } else { $filter = array_merge($mainFilter, $otherFilter); } $values = array_values($filter); $keys = array_keys($filter); Ethna_AppSQL::escapeSQL($values); $conditions = array(); for($i=0, $n=count($values); $i<$n; $i++) { $conditions[] = Ethna_AppSQL::getCondition( $db->quoteIdentifier($keys[$i]), $values[$i] ); } return sprintf('SELECT %s FROM %s WHERE %s', $db->quoteIdentifier($this->relation['dest_key']), $db->quoteIdentifier($this->relation['relation']), implode(' AND ', $conditions) ); } /** * 連携表に追加するSQLを返す * MySQL専用なので他のRDBMSを使う場合は書き換えてください * * @access private * @param projBop_AppObject &$object オブジェクト * @return string SQL文 */ function _getRelationInsertSQL(&$object) { $db =& $this->backend->getDb(); $table = $db->quoteIdentifier($this->relation['relation']); $props = array( $this->relation['from_key'] => $this->parent->getId(), $this->relation['dest_key'] => $object->getId(), ); // SET句構築 // from Ethha_AppObject $key_list = array(); $set_list = array(); Ethna_AppSQL::escapeSQL($props); foreach ($props as $k => $v) { $keys_list[] = $db->quoteIdentifier($k); $set_list[] = $v; } $key_list = implode(', ', $key_list); $set_list = implode(', ', $set_list); $sql = "INSERT INTO $table ($key_list) VALUES ($set_list)"; return $sql; } } /** * has_many (through)なリレーションのAssociation * * @author BoBpp < bobpp@users.sourceforge.jp > * @package projBop.Ethna.AppObject.Relation */ class projBop_Relation_HasManyThrough extends projBop_Relation_ManyObjects { /** @var string リレーションを取り持つオブジェクトをinjectionする際のプロパティ名 */ var $RelationObjectPropName = "RelationObject"; /** * リレーションを自動設定する * * @access private */ function _setAutoRelation() { if ($this->relation === null || isset($this->relation['relation']) === false) { return Ethna::raiseError('Not set Relation Setting'); } if (isset($this->relation['from_key']) === false) { $this->relation['from_key'] = $this->parentModelName . '_id'; } if (isset($this->relation['dest_key']) === false) { $this->relation['dest_key'] = $this->relationModelName . '_id'; } } /** * リレーションするためのフィルタを取得する * * @access protected * @return array フィルタ */ function _getRelationFilter() { return array( $this->relation['from_key'] => new Ethna_AppSearchObject( $this->parent->getId(), OBJECT_CONDITION_EQ), ); } /** * リレーションされたオブジェクトを取得する * * @access public * @param int $id オブジェクトの主キー * @param boolean $useRelationObject 関連を取り持つテーブルのオブジェクトを必要とする場合はTRUE * @return mixed projBop_AppObject: リレーションされたオブジェクト(RelationObjectプロパティに必要な場合の関連オブジェクト) * NULL: リレーションされたオブジェクトが存在しない * Ethna_Error: エラー発生 */ function &get($id, $useRelationObject=false) { $id = $this->_getPK($id); if (Ethna::isError($id)) { return $id; } $relation = $this->relation; $mainFilter = $this->_getRelationFilter(); $idFilter = array( $relation['from_key'] => $id, ); $filter = array_merge($mainFilter, $idFilter); $sm =& $this->_getSearchManager(); if ($useRelationObject === true) { list($num, $relationObject) = $sm->getObjectList( $relation['relation'], $filter, null, 0, 1 ); if (Ethna::isError($num)) { return $num; } else if ($num == 0) { return null; } $object =& $this->_getObject($relationObject->get($relation['dest_key'])); $object->{$this->RelationObjectPropName} =& $relationObject; return $object; } else { $prop = $sm->getObjectProp( $relation['relation'], null, $filter ); if (Ethna::isError($prop) || $prop === null) { return $prop; } return $this->_getObject($prop[$relation['dest_key']]); } } /** * リレーションされたオブジェクトをすべて取得する * * @access public * @param boolean $useRelationObject 関連を取り持つオブジェクトのインスタンスが必要な場合はTRUE * @return mixed Array: オブジェクト配列() * Ethna_Error: エラー */ function getsAll($useRelationObject=false) { $destKey = $this->relation['dest_key']; $sm =& $this->_getSearchManager(); $objects = array(); if ($useRelationObject === true) { list($num, $relationObjects) = $sm->getObjectList( $this->relation['relation'], $this->_getRelationFilter() ); if (Ethna::isError($num)) { return $num; } for ($i=0; $i<$num; $i++) { $object =& $this->_getObject($relationObjects[$i]->get($destKey)); if ($object !== null) { $object->{$this->RelationObjectPropName} =& $relationObjects[$i]; $objects[] =& $object; } } } else { list($num, $data) = $sm->getObjectPropList( $this->relation['relation'], $destKey, $this->_getRelationFilter() ); if (Ethna::isError($num)) { return $num; } foreach ($data as $relationProp) { $object =& $this->_getObject($relationProp[$destKey]); if ($object !== null) $objects[] =& $object; } } return $objects; } /** * 関連オブジェクトを検索する * * @access public * @param boolean $useRelationObject 関連を取り持つオブジェクトを取得するか否か * @return mixed (Array:関連オブジェクト配列, Ethna_Error:エラー発生) */ function find($useRelationObject=false) { } /** * 関連オブジェクトの個数を返す * * @access public * @return int 関連オブジェクトの個数 */ function count() { $proto = $this->_getProtoObject($this->relation['relation']); $filter = $this->_getRelationFilter(); $sql = $proto->_getSQL_SearchLength($filter); $db =& $this->backend->getDb(); $r =& $db->query($sql); if (Ethna::isError($r)) { return $r; } $temp = $db->fetchRow($r, DB_FETCHMODE_ASSOC); return $temp['id_count']; } /** * オブジェクトにリレーションを設定して保存する * * @access public * @param projBop_AppObject &$object リレーション先のオブジェクト * @param mixed $rel_attr (projBop_AppObject: 中間オブジェクト, Array: 中間オブジェクトのプロパティ配列) * @return mixed int=0: 成功 * Ethna_Error: エラー発生 */ function set(&$object, $rel_attr=null) { // リレーションできるオブジェクトかチェックする $r = $this->_checkRelation($object); if (Ethna::isError($r)) { return $r; } // 未保存のオブジェクトに対しては主キー不明のため連携できない if ($object->isValid() === false) { return Ethna::raiseError('Cant create relation to Not saved object.'); } // 未保存の親オブジェクトに対しても主キー不明の状態ではエラーを返す if ($this->parent->isValid() === false) { return Ethna::raiseError('Cant create relation to Not saved object.'); } // 中間オブジェクトの設定 $relation = $this->relation; if (is_object($rel_attr) && is_subclass_of($rel_attr, 'projBop_AppObject')) { $relObj =& $rel_attr; } else { $relObj =& $this->backend->getObject($relation['relation']); if (is_array($rel_attr) === true) { foreach ($rel_attr as $k=>$v) { $relObj->set($k, $v); } } } $relObj->set($relation['from_key'], $this->parent->getId()); $relObj->set($relation['dest_key'], $object->getId()); // 中間オブジェクトの保存 return $relObj->add(); } } ?>