NTTドコモ様_dカーシェア
NTTドコモ様_dカーシェア
2015.01.20

CakePHPの「Tree」Behaviorをカスタマイズして実装する方法

tetsu

こんにちは、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送信で更新情報サービスを利用する方法