こんにちは、tetsuです。
今回はCakePHPのBehaviorの中で最初から利用できる「Tree」Behaviorについて、独自カスタマイズと実装をする方法のご紹介です。
「Tree」Behaviorとは
カテゴリーなどのデータを扱う際に、階層や深さといったデータ構造(ツリー構造)のデータ処理を一手に引き受けてくれる素晴らしいBehaviorです。
CakePHPに用意されている「Tree」Behaviorを利用する場合は、利用するモデルで次のようにコードを記述することですぐに利用できます。
例)「Category」モデルに「Tree」Behaviorを適用する場合
class Category extends AppModel {
public $actsAs = array('Tree');//←この行を追加
}
DB設計は以下の参考ページにてサンプルがあります。
これだけでツリー構造のデータ管理をすべて処理してくれます。
開発が楽になる素晴らしいBehaviorです。
カスタマイズするに至った経緯
「Tree」Behaviorをカスタマイズするに至ったのは、「一つのツリー構造となるDBテーブルで、条件を指定できるか」と考えた際にCakePHPにある「Tree」Behaviorでは対応ができなかったからです。
前述した「Category」モデルを例にすると、顧客毎(ここではCustomerモデル)別に情報を管理したい場合、既存の「Tree」Behaviorでは処理をすることができませんでした。
独自Behaviorを用意
今回はCakePHPで用意されている「Tree」Behaviorを基に、要件を満たす独自Behaviorを用意します。
独自Behaviorファイルの作成
「app」→「Model」→「Behavior」に独自Behaviorファイルを作成します。
ここで独自Behaviorファイル名を「ScopeTreeBehavior.php」とし、「ScopeTree」Behaviorを作成していきます。
「ScopeTree」Behaviorの基本ソースコード
「ScopeTree」Behaviorでの初期状態となるソースコードを以下のように記述します。
▼ソースコード
<?php
App::uses('TreeBehavior', 'Model/Behavior');
class ScopeTreeBehavior extends TreeBehavior {
}
「App::uses」で「TreeBehavior」を定義することで、「ScopeTree」Behaviorを「Tree」Behaviorと同じ処理ができる状態にします。
条件指定が可能になるようカスタマイズ
「ScopeTree」Behaviorで「Tree」Behaviorの各メソッドが実行される前に条件を指定できるよう、カスタマイズしていきます。
今回は「ScopeTree」Behaviorを利用する各モデルのプロパティ「scopeTreeConditions」に指定の条件を設定することで、条件に合わせたツリー構造を構築できるように行います。
カスタマイズしたソースコードは以下となります。
▼ソースコード
<?php
App::uses('TreeBehavior', 'Model/Behavior');
class ScopeTreeBehavior extends TreeBehavior {
public $scopeTreeConditionsName='scopeTreeConditions';
public function _resetScope(Model $Model){$this->settings[$Model->alias]['scope']=array();}
public function _beforeModelScope(Model $Model){
$this->settings[$Model->alias]['scope']=$this->_amModelScope($Model,$this->settings[$Model->alias]['scope']);
}
public function _amModelScope(Model $Model,$scope){
if(!empty($Model->{$this->scopeTreeConditionsName})){
if(is_string($scope)&&$scope=='1 = 1'){$scope=array();}
return array_merge_recursive($scope,$Model->{$this->scopeTreeConditionsName});
}else{
return $scope;
}
}
public function beforeFind(Model $Model, $query) {
$this->_beforeModelScope($Model);
return parent::beforeFind($Model, $query);
}
public function beforeDelete(Model $Model, $cascade = true) {
$this->_beforeModelScope($Model);
return parent::beforeDelete($Model, $cascade);
}
public function afterDelete(Model $Model) {
$this->_beforeModelScope($Model);
return parent::afterDelete($Model);
}
public function beforeSave(Model $Model, $options = array()) {
$this->_resetScope($Model);
$this->_beforeModelScope($Model);
return parent::beforeSave($Model, $options);
}
public function childCount(Model $Model, $id = null, $direct = false) {
$this->_beforeModelScope($Model);
return parent::children($Model, $id, $direct);
}
public function children(Model $Model, $id = null, $direct = false, $fields = null, $order = null, $limit = null, $page = 1, $recursive = null) {
$this->_beforeModelScope($Model);
return parent::children($Model, $id, $direct, $fields, $order, $limit, $page, $recursive);
}
public function generateTreeList(Model $Model, $conditions = null, $keyPath = null, $valuePath = null, $spacer = '-', $recursive = null) {
$this->_resetScope($Model);
$this->_beforeModelScope($Model);
return parent::generateTreeList($Model, $conditions, $keyPath, $valuePath, $spacer, $recursive);
}
public function getParentNode(Model $Model, $id = null, $fields = null, $recursive = null) {
$this->_beforeModelScope($Model);
return parent::getParentNode($Model, $id, $fields, $recursive);
}
public function getPath(Model $Model, $id = null, $fields = null, $recursive = null) {
$this->_beforeModelScope($Model);
return parent::getPath($Model, $id, $fields, $recursive);
}
public function moveDown(Model $Model, $id = null, $number = 1) {
$this->_beforeModelScope($Model);
return parent::moveDown($Model, $id, $number);
}
public function moveUp(Model $Model, $id = null, $number = 1) {
$this->_beforeModelScope($Model);
return parent::moveUp($Model, $id, $number);
}
public function recover(Model $Model, $mode = 'parent', $missingParentAction = null) {
$this->_beforeModelScope($Model);
return parent::recover($Model, $mode, $missingParentAction);
}
protected function _recoverByParentId(Model $Model, $counter = 1, $parentId = null) {
$params = array(
'conditions' => array(
$this->settings[$Model->alias]['parent'] => $parentId
),
'fields' => array($Model->primaryKey),
'page' => 1,
'limit' => 100,
'order' => array($Model->primaryKey)
);
$scope = $this->settings[$Model->alias]['scope'];
if ($scope && ($scope !== '1 = 1' && $scope !== true)) {
$params['conditions'][] = $scope;
}
$children = $Model->find('all', $params);
$hasChildren = (bool)$children;
if ($parentId !== null) {
if ($hasChildren) {
$Model->updateAll(
array($this->settings[$Model->alias]['left'] => $counter),
array($Model->escapeField() => $parentId)
);
$counter++;
} else {
$Model->updateAll(
array(
$this->settings[$Model->alias]['left'] => $counter,
$this->settings[$Model->alias]['right'] => $counter + 1
),
array($Model->escapeField() => $parentId)
);
$counter += 2;
}
}
while ($children) {
foreach ($children as $row) {
$counter = $this->_recoverByParentId($Model, $counter, $row[$Model->alias][$Model->primaryKey]);
}
if (count($children) !== $params['limit']) {
break;
}
$params['page']++;
$children = $Model->find('all', $params);
}
if ($parentId !== null && $hasChildren) {
$Model->updateAll(
array($this->settings[$Model->alias]['right'] => $counter),
array($Model->escapeField() => $parentId)
);
$counter++;
}
return $counter;
}
public function reorder(Model $Model, $options = array()) {
return parent::reorder($Model, $options);
}
public function removeFromTree(Model $Model, $id = null, $delete = false) {
$this->_beforeModelScope($Model);
return parent::removeFromTree($Model, $id, $delete);
}
public function verify(Model $Model) {
$this->_beforeModelScope($Model);
return parent::verify($Model);
}
protected function _setParent(Model $Model, $parentId = null, $created = false) {
$this->_beforeModelScope($Model);
return parent::_setParent($Model, $parentId, $created);
}
protected function _getMax(Model $Model, $scope, $right, $recursive = -1, $created = false) {
$scope=$this->_amModelScope($Model,$scope);
return parent::_getMax($Model, $scope, $right, $recursive, $created);
}
protected function _getMin(Model $Model, $scope, $left, $recursive = -1) {
$scope=$this->_amModelScope($Model,$scope);
return parent::_getMax($Model, $scope, $left, $recursive);
}
protected function _sync(Model $Model, $shift, $dir = '+', $conditions = array(), $created = false, $field = 'both') {
$this->_beforeModelScope($Model);
return parent::_sync($Model, $shift, $dir, $conditions, $created, $field);
}
}
「ScopeTree」Behaviorの実装
カスタマイズ完了後、「ScopeTree」Behaviorを実装する例は次の通りです。
例)「Category」モデルに「ScopeTree」Behaviorを適用する場合
class Category extends AppModel {
public $actsAs = array('ScopeTree');
}
「Tree」Behaviorを利用するのと同じになります。
あとはコントローラー側で任意のタイミングを条件指定することで、条件に合わせたツリー構造を構築することが可能になります。
例えば冒頭にあった「CategoryモデルでCustomerモデル別に情報を管理したい」とした場合、コントローラー側で次のように定義することで、参照や保存時に条件に合わせたツリー構造で管理されます。
例)定義例
$this->Category->scopeTreeConditions['AND']['Category.customer_id']='指定顧客ID';
最後に
いかがだったでしょうか。
CakePHPで用意されているものを再利用することで、要件に合わせたBehaviorを作成することができます。
みなさまのエンジニアライフに役に立てれば幸いです。
【CakePHPを利用するあれこれ】
※ AuthComponentで非認証ページと認証ページを共存させる方法
※ CakePHPでBitbucketのAPIを利用する方法
※ 「AppModel」を有効に活用して開発の効率化する方法
※ CakePHPの「AppModel」を有効に活用して開発を効率化する方法 第二弾
※ CakePHP + 更新Ping送信で更新情報サービスを利用する方法
LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。