<?php /** * 树形结构操作模型:改进的前序遍历 —— 表必备字段: id,name,lft,rgt,nlevel */ class TreeModel extends Model { protected $_validate = array( array('name', '2,32', '字符长度需在2到32字符之内', Model::MUST_VALIDATE, 'length'), ); /** * 判断表中是否有根节点 * @return int */ function isHasRoot() { return $this->where('lft=1 and nlevel=0')->getField('id'); } /** * 判断名字是否已存在 * @param string $name * @return int */ function isExistName($name) { return $this->where( array('name'=>$name) )->getField('id'); } /** * 判断节点是否为指定节点的子节点 * @param int $childId: 子节点 * @param int $parentId: 父节点 * @return bool */ function isChild($childId, $parentId) { if ( $childId == $parentId ) { return false; } $nodes = $this->whereNode( array($childId, $parentId) )->getField('id,lft,rgt'); $child = $nodes[$childId]; $parent = $nodes[$parentId]; if ( $child && $parent ) { return ($child['lft'] > $parent['lft']) && ($child['rgt'] < $parent['rgt']); } return false; } /** * 获取节点信息 * @param int $nodeId * @return array */ function getNodeInfo($nodeId) { return $this->whereNode($nodeId)->field('id,lft,rgt,nlevel')->find(); } //---------------------------------where条件 代替where()---------------------------------------- /** * 条件:节点 * @param int|array $nodeId * @param array $conditions: 附加条件 * @return this */ function whereNode($nodeId, array $conditions=array()) { $nodeId = (array)$nodeId; $conditions['id'] = array('in', $nodeId); return $this->where( $conditions ); } /** * 条件:获取节点(在指定的层级范围内)的子节点 * @param array $node * @param int $childLevel: 距当前节点的层级, 为0时获取所有 * @param array $conditions: 附加条件 * @param bool $onlyLeaf: 为true时只获取叶子节点 * @return this */ function whereChild(array $node, $childLevel=1, array $conditions=array(), $onlyLeaf=false) { $conditions['lft'] = array('gt', $node['lft']); $conditions['rgt'][] = array('lt', $node['rgt']); if ( $onlyLeaf ) { $conditions['rgt'][] = array('eq', 'lft+1'); } if ( $childLevel>0 ) { $conditions['nlevel'] = array( array('gt', $node['nlevel']), array('elt', $node['nlevel'] + $childLevel) ); } return $this->where($conditions); } /** * 条件:获取节点(在指定的层级范围内)的(直系)祖先节点 * @param array $node: 节点信息 * @param int $parentLevel:距当前节点的层级,0为获取所有 * @param array $conditions: 附加条件 * @return array */ function whereParent(array $node, $parentLevel=1, array $conditions=array()) { //lft<$lft and rgt>$rgt $conditions['lft'] = array('lt', $node['lft']); $conditions['rgt'] = array('gt', $node['rgt']); if ( $parentLevel>0 ) { $conditions['nlevel'] = array( array('lt', $node['nlevel']), array('egt', $node['nlevel'] - $parentLevel) ); } return $this->where($conditions); } /** * 条件:获取节点排除指定节点后的子孙节点(即从node的子孙节点中将out节点及其子节点排除) * @param array $node: * @param array $out: 排除的节点 * @param array $conditions: 附加条件 * @return this */ function whereChildWithout(array $node, array $out, array $conditions=array()) { $conditions['lft'] = array('gt', $node['lft']); $conditions['rgt'] = array('lt', $node['rgt']); $conditions['_string'] = 'lft<'.$out['lft'].' or rgt>'.$out['rgt']; return $this->where($conditions); } //------------------------------------append---------------------------------------- /** * 添加节点 * @param array $data: 要添加的节点信息 * @param int $noteId: 节点id, 为0时添加首节点 * @param bool $insertToRight: 添加在目标节点的最右,为false时添加在最左 * @return int */ function appendNode(array $data, $nodeId, $insertToRight=true) { if ( !$this->create( $data ) ) { return false; } if ( !$nodeId ) { return $this->appendRootNode($data); } $node = $this->getNodeInfo($nodeId); if ( $node ) { $this->startTrans(); if ( $insertToRight ) { $id = $this->appendRightNode($data, $node); } else { $id = $this->appendLeftNode($data, $node); } $id!==false ? $this->commit() : $this->rollback(); return $id; } return false; } /** * 添加根节点 * @param array $data: 要添加的节点信息 * @return int */ protected function appendRootNode(array $data) { if ( !$this->isHasRoot() ) { $data['lft'] = 1; $data['rgt'] = 2; $data['nlevel'] = 0; return $this->add($data); } return false; } /** * 添加左节点: 添加在目标节点的最左 * @param array $data: 要添加的节点信息 * @param array $node: 目标节点信息 * @return int */ protected function appendLeftNode(array $data, $node) { //所有左(右)值大于目标节点的左值的节点其右值+2(包含当前节点的子节点) $upLft = $this->where('lft>'.$node['lft'])->setInc('lft', 2); $upRgt = $this->where('rgt>'.$node['lft'])->setInc('rgt', 2); $data['lft'] = $node['lft'] + 1; //左值加1 $data['rgt'] = $node['lft'] + 2; $data['nlevel'] = $node['nlevel'] + 1; $id = $this->add($data); if ( $upLft !== false && $upRgt && $id ) { //可能无左节点的更新,但肯定有右节点的更新 return $id; } else { return false; } } /** * 添加右节点: 添加在目标节点的最右 * @param array $data: 要添加的节点信息 * @param array $node: 目标节点信息 * @return int */ protected function appendRightNode(array $data, $node) { //所有左值大于目标节点的右值的节点其右值+2(不包含当前节点的子节点) $upLft = $this->where('lft>'.$node['rgt'])->setInc('lft', 2); //左值全部+2 $upRgt = $this->where('rgt>='.$node['rgt'])->setInc('rgt', 2); //含当前节点的右值 $data['lft'] = $node['rgt']; //当前节点右值 $data['rgt'] = $node['rgt'] + 1; $data['nlevel'] = $node['nlevel'] + 1; $id = $this->add($data); if ( $upLft !== false && $upRgt && $id ) { return $id; } else { return false; } } //-----------------------------------remove----------------------------------------- /** * 删除节点 * @param int $nodeId * @param bool $isDelChild: 是否一起删除该节点下的子节点, 为false时节点下的子节点将置于该节点的父节点下 * @return bool */ function removeNode($nodeId, $isDelChilds=true) { $nodeInfo = $this->getNodeInfo($nodeId); if ( $nodeInfo && $nodeInfo['nlevel'] ) { //排除根节点 $this->startTrans(); if ($isDelChilds) { $del = $this->removeChildNodes($nodeInfo); } else { $del = $this->removeSingleNode($nodeInfo); } $del!==false ? $this->commit() : $this->rollback(); return $del; } return false; } /** * 删除单一节点,若此节点含有子节点,则将子节点转到此节点的父节点下 * @param array $nodeInfo:待删除的节点信息 * @return bool */ protected function removeSingleNode(array $nodeInfo) { //删除节点 $del = $this->where('id='.$nodeInfo['id'])->delete(); //更新节点的子节点:左右值都减1,层级减1 $sets = array( 'rgt' => array('exp', 'rgt - 1'), 'lft' => array('exp', 'lft - 1'), 'nlevel' => array('exp', 'nlevel - 1'), ); $upChild = $this->where('lft between '.$nodeInfo['lft'].' and '.$nodeInfo['rgt'])->setField($sets); //更新右值 $upRgt = $this->where('rgt>'.$nodeInfo['rgt'])->setDec('rgt', 2); //更新左值 $upLft = $this->where('lft>'.$nodeInfo['rgt'])->setDec('lft', 2); return $del && $upRgt && $upLft !== false && $upChild !== false; } /** * 删除节点及其子节点 * @param array $nodeInfo:待删除的节点信息 * @param bool */ protected function removeChildNodes(array $nodeInfo) { //删除节点及其子节点 $del = $this->where('lft between '.$nodeInfo['lft'].' and '.$nodeInfo['rgt'])->delete(); $diff = $nodeInfo['rgt'] - $nodeInfo['lft'] + 1; //节点差值 //更新右值 $upRgt = $this->where('rgt>'.$nodeInfo['rgt'])->setDec('rgt', $diff); //更新左值 $upLft = $this->where('lft>'.$nodeInfo['rgt'])->setDec('lft', $diff); return $del && $upRgt && $upLft !== false; } //-----------------------------------move------------------------------------------- /** * 节点移动:移动节点时该节点的子节点也会一起移动(禁止从父节点移动到子节点) * @param int $fromId: 待移动的节点 * @param int $toId: 移向的目的节点 * @return bool */ function moveTo($fromId, $toId) { if ( $fromId == $toId ) { //移到自身 $this->error = '父节点错误'; return false; } $nodes = $this->whereNode( array($fromId, $toId) )->getField('id,lft,rgt,nlevel'); $from = $nodes[$fromId]; $to = $nodes[$toId]; if ( !($from && $to) ) { $this->error = '未知节点'; return false; } else if ( $from['lft'] < $to['lft'] && $from['rgt'] > $to['rgt'] ) { //从父节点移到子节点 $this->error = '禁止从父节点移向子节点'; return false; } else if ( $from['nlevel'] == $to['nlevel']+1 && $to['lft'] < $from['lft'] && $to['rgt'] > $from['rgt'] ) { //本身即为父子节点 return true; } if ( $from['lft'] > $to['rgt'] ) { //左移 $move = $this->moveToLeft($from, $to); } else if ( $from['rgt'] < $to['lft'] ) { //右移 $move = $this->moveToRight($from, $to); } else { $move = $this->moveToParent($from, $to); //移向父节点 } $move!==false ? $this->commit() : $this->rollback(); return $move; } /** * 节点左移 * @param array $src: 要移动的节点信息 * @param array $to: 目标节点信息 * @return bool */ protected function moveToLeft(array $src, array $to) { $nodeStep = $src['rgt'] - $src['lft'] + 1; //-------------置于目标节点的最右-------------------- //更新中间节点的右值,此时中间节点的右值与待移节点的右值有重复 $upRgt = $this->where('rgt<'.$src['lft'].' and rgt>='.$to['rgt'])->setInc('rgt', $nodeStep); //包含目标节点 //更新移动节点的左右值(判断待移节点的左值), 此时与中间节点的左值有重复,右值无重复 $moveStep = $src['lft'] - $to['rgt']; $diffLevel = $to['nlevel'] - $src['nlevel'] + 1; //比目标等级低1 $sets = array( 'lft' => array('exp', 'lft-'.$moveStep), 'rgt' => array('exp', 'rgt-'.$moveStep), 'nlevel' => array('exp', 'nlevel+'.$diffLevel), ); $upNode = $this->where('lft between '.$src['lft'].' and '.$src['rgt'])->setField($sets); //更新中间节点的左值(判断中间节点的右值),$to['rgt']+$nodeStep为移动后的目标节点的右值 $wheres = 'lft<'.$src['lft'].' and lft>'.$to['lft'].' and rgt>'.($to['rgt']+$nodeStep); //左值更新范围(排序移动节点) $upLft = $this->where($wheres)->setInc('lft', $nodeStep); return $upNode && $upRgt && $upLft !== false; } /** * 节点右移 * @param array $from: 要移动的节点信息 * @param array $to: 目标节点信息 * @return bool */ protected function moveToRight(array $from, array $to) { $nodeStep = $from['rgt'] - $from['lft'] + 1; //-----------------置于目标节点的最左----------------- //更新中间节点的左值,此时中间节点的左值与待移节点的左值有重复 $upLft = $this->where('lft>'.$from['rgt'].' and lft<='.$to['lft'])->setDec('lft', $nodeStep); //包含目标节点 //更新移动节点的左右值(判断待移节点的右值), 此时与中间节点的右值有重复,左值无重复 $moveStep = $to['lft'] - $from['rgt']; $diffLevel = $to['nlevel'] - $from['nlevel'] + 1; //比目标等级低1 $sets = array( 'lft' => array('exp', 'lft+'.$moveStep), 'rgt' => array('exp', 'rgt+'.$moveStep), 'nlevel' => array('exp', 'nlevel+'.$diffLevel), ); $upNode = $this->where('rgt between '.$from['lft'].' and '.$from['rgt'])->setField($sets); //更新中间节点的右值(判断中间节点的左值),$to['lft']-$nodeStep为移动后的目标节点的左值 $wheres = 'rgt>'.$from['rgt'].' and rgt<'.$to['rgt'].' and lft<'.($to['lft']-$nodeStep); //右值更新范围(排除移动节点) $upLft = $this->where( $wheres )->setDec('rgt', $nodeStep); return $upNode && $upRgt && $upLft !== false; } /** * 节点上移 * @param array $from: 要移动的节点信息 * @param array $to: 目标节点信息 * @return bool */ protected function moveToParent(array $from, array $to) { $nodeStep = $from['rgt'] - $from['lft'] + 1; //-----------------置于目标节点的最右----------------- //更新中间节点的右值,此时中间节点的右值与待移节点的右值有重复 $upRgt = $this->where('rgt>'.$from['rgt'].' and rgt<'.$to['rgt'])->setDec('rgt', $nodeStep); //不包含目标节点 //更新移动节点的左右值(判断待移节点的右值), 此时与中间节点的左值有重复,右值无重复 $moveStep = $to['rgt'] - $from['rgt'] - 1; //本身就在$to节点下,故再减1 $diffLevel = $to['nlevel'] - $from['nlevel'] + 1; //比目标等级低1 $sets = array( 'lft' => array('exp', 'lft+'.$moveStep), 'rgt' => array('exp', 'rgt+'.$moveStep), 'nlevel' => array('exp', 'nlevel+'.$diffLevel), ); $upNode = $this->where('lft between '.$from['lft'].' and '.$from['rgt'])->setField($sets); //更新中间节点的左值(判断中间节点的右值),$to['rgt']-$nodeStep为排除移动节点 $wheres = 'lft>'.$from['lft'].' and rgt>'.$from['lft'].' and rgt<'.($to['rgt']-$nodeStep); //左值更新范围(排除移动节点) $upRgt = $this->where( $wheres )->setDec('lft', $nodeStep); return $upNode && $upRgt && $upLft !== false; } }