改进型先根遍历算法

22 篇文章 0 订阅
13 篇文章 0 订阅
<?php
/**
 * 无效的分类Id
 *
 */
class NodeNotFoundException extends Exception {
	
	public $id;
	
	function __construct($id){
		$this->id = $id;
		parent::__construct("指定 [ID = {$id}] 的节点不存在");
	}
}

/**
 * 节点名称已经存在
 */
class NodeNameExistsException extends Exception
{	
	public $name,$parentId;
	
	function __construct($name ,$parentId) {
		$this->name = $name;
		$this->parentId = $parentId;		
        parent::__construct("指定 [NAME = {$name} ,PARENT_ID = {$parentId}] 的分类已经存在");        
    }    
}

/**
 * 节点分类 模型 用“改进型先根遍历算法”在数据库中存储层次化的数据(通常所说的无限分类)
 *
 * 由于“改进型先根遍历算法”要求所有分类都是唯一一个根分类的子分类。
 * 所以 NodeModel 假定一个名为“_#_ROOT_NODE_#_”的分类为唯一的根分类。
 *
 * 应用程序在调用 NodeModel::create() 创建第一个分类时,会自动
 * 判断根分类是否存在,并创建根分类。
 *
 * 对于应用程序来说,“_#_ROOT_NODE_#_”分类是不存在的。所以,应用程序
 * 可以创建多个父分类 ID 为 0 的“顶级分类”。这些顶级分类实际上就是
 * “_#_ROOT_NODE_#_”分类的直接子分类。
 *
 * 添加 deep 这个属性用于设置分类的深度,用于按名称查找分类,比如:
 * '/图书/IT/技术/PHP' 其 deep 就 为 
 *
 * 注意: 分类名称 中不能带有 "/" 符号,不然会出现意向不到的问题
 * <sql>
 CREATE TABLE `nodes` (
 	`node_id` int(11) NOT NULL auto_increment,
 	`parent_id` int(11) NOT NULL,
 	`name` varchar(64) collate utf8_unicode_ci NOT NULL,
 	`left_value` int(11) NOT NULL,
 	`right_value` int(11) NOT NULL,
 	`deep` int(4) NOT NULL,
 	`created_at` datetime default NULL,
 	`updated_at` datetime default NULL,
 	PRIMARY KEY  (`node_id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
 * </sql>
 * 
 */
class NodeModel {	
		
	/**
	 * @var CoreDb
	 */
	protected $_dbo = NULL;
	
    /**
     * 数据表名称,在继承类中可能会被覆盖
     *
     * @var string
     */
    public $tableName = 'nodes';

    /**
     * 主键字段名,在继承类中可能会被覆盖
     *
     * @var string
     */
    public $primaryKey = 'node_id';

    /**
     * 根节点名
     *
     * @var string
     */
    protected $_rootNodeName = '_#_ROOT_NODE_#_';
    
    /**
     * 直系子节点名称是否唯一
     *
     * @var boolean
     */
    protected $_uniqueNameSupport = true;
	
	function __construct(CoreDb $dbo){
		$this->_dbo = $dbo;
		$this->init();
	}
	
	protected function init(){
		$this->tableName = $this->_dbo->getDbUtils()->getTableName($this->tableName);
	}

	function find($cond, $fields='*'){
		$sqlCond = CoreDbSqlHelper::parseConditions($this->_dbo,$cond);
		if ($sqlCond) $sqlCond = "WHERE {$sqlCond}";
		
		$qfields = CoreDbSqlHelper::qfields($fields,$this->tableName);
		
		$sql = "SELECT {$qfields} FROM {$this->tableName} {$sqlCond}" . CoreDbSqlHelper::getLimitSql(1);
		
		return $this->_dbo->getRow($sql);
	}
	
	function findByPkv($nodeId, $fields='*') {
    	return $this->find(array($this->primaryKey=>$nodeId),$fields);
    }
    
	/**
	 * 添加用户 并返回插入的主键编号
	 * 
	 * @var array $user
	 * 
	 * @return int
	 */
	protected function insert(array $node){
		
		if (!isset($node['name'])) return false;
		
		$user['created_at'] = CURRENT_TIMESTAMP;
		
		$sql = CoreDbSqlHelper::getInsertSQL($node,$this->tableName);
		if ($sql){
			if ($this->_dbo->execute($sql,$node,true))
				return $this->_dbo->lastInsertId();
		}
		return false;
	}
	
    /**
     * 添加一个节点,返回该节点的 ID
     *
     * @param array $node
     * @param int $parentId
     *
     * @return int
     */
    function create(array $node, $parentId=0) {
        $parentId = (int)$parentId;
        if ($parentId) {
        	
            $parent = $this->find(array($this->primaryKey=>$parentId));
            if (!$parent) {
                // 指定的父节点不存在
                throw new NodeNotFoundException($parentId);
            }
        } else {
            // 如果 $parentId 为 0 或 null,则创建一个顶级节点
            $parent = $this->find(array('name' => $this->_rootNodeName));
            if (!$parent) {
                // 如果根节点不存在,则自动创建
                $parent = array(
                    'name' => $this->_rootNodeName,
                    'left_value' => 1,
                    'right_value' => 2,
                    'parent_id' => -1,
                    'deep' => 0,
                );
                if (!$this->insert($parent)) {
                    return false;
                }
            }
            // 确保所有 _#_ROOT_NODE_#_ 的直接字节点的 parent_id 都为 0
            $parent[$this->primaryKey] = 0;
        }
		
        //检验直系子分类名称是否唯一
		if ($this->_uniqueNameSupport && $this->nameExists($node['name'],$parent[$this->primaryKey])){
			throw new NodeNameExistsException($node['name'],$parent[$this->primaryKey]);
		}
		
        // 加上事务功能
        $this->_dbo->startTrans();
        
        // 根据父节点的左值和右值更新数据
        $sql = "UPDATE {$this->tableName} SET left_value = left_value + 2 " .
               "WHERE left_value >= {$parent['right_value']}";
        $this->_dbo->execute($sql);
        $sql = "UPDATE {$this->tableName} SET right_value = right_value + 2 " .
               "WHERE right_value >= {$parent['right_value']}";
        $this->_dbo->execute($sql);

        // 插入新节点记录
        $node['parent_id'] = $parent[$this->primaryKey];
        $node['deep'] = $parent['deep'] + 1; //路径深度定义
        $node['left_value'] = $parent['right_value'];
        $node['right_value'] = $parent['right_value'] + 1;
        
        $id = $this->insert($node);
        
        $this->_dbo->completeTrans();
        return $id;
    }
	
	/**
	 * 检验直系子分类名称是否唯一
	 *
	 * @param string $name
	 * @param int $parentId
	 * @param int $exceptId 如果指定此参数,则排除掉对指定Id的校验
	 * 
	 * @return boolean
	 */
	function nameExists($name,$parentId,$exceptId=null){
		$cond = array('parent_id' =>$parentId,'name'=>$name);
		if ($exceptId){
			$cond['id'] = array($exceptId,'!=');
		}
		return $this->_dbo->getDbUtils()->findCount($this->tableName,$cond);
	}
    
	function update(array $node){
		
		// 未指定主键
		if (!isset($node[$this->primaryKey])) return false;
					
		$node['updated_at'] = CURRENT_TIMESTAMP;
		
		$sql = CoreDbSqlHelper::getUpdateSQL($this->_dbo,$node,$this->primaryKey,$this->tableName);
		if ($sql){
			if ($this->_dbo->execute($sql,$node))
				return $this->_dbo->lastQueryAffectedRows();
		}		
		return false;
	}
	
    /**
     * 更新节点信息
     *
     * @param array $node
     *
     * @return boolean
     */
    function save(array $node) {
    				
    	//检验节点名称是否在父节点下是唯一的
    	if (isset($node['name'])) {
    	
			if ($this->_uniqueNameSupport && $this->nameExists($node['name'],$node['parent_id'],$node[$this->primaryKey])){
				throw new NodeNameExistsException($node['name'],$node[$this->primaryKey]);
			}
			
    	}
        unset($node['left_value']);
        unset($node['right_value']);
        unset($node['parent_id']);
        unset($node['deep']);
        return $this->update($node);
    }

    /**
     * 删除一个节点及其子节点树
     *
     * @param array $node
     *
     * @return boolean
     */
    function remove(array $node) {
    	$this->_dbo->startTrans();
    	$rst = false;
    	
    	do {
    		$span = $node['right_value'] - $node['left_value'] + 1;
        	$sql = "DELETE FROM {$this->tableName} " .
               "WHERE left_value >= {$node['left_value']} " .
               "AND right_value <= {$node['right_value']}";
    		
        	$rst = $this->_dbo->execute($sql);
        	if (!$rst) break;
        	
        	$sql = "UPDATE {$this->tableName} " .
               "SET left_value = left_value - {$span} " .
               "WHERE left_value > {$node['right_value']}";

        	$rst = $this->_dbo->execute($sql);
        	if (!$rst) break;
        	
        	
        	$sql = "UPDATE {$this->tableName} " .
               "SET right_value = right_value - {$span} " .
               "WHERE right_value > {$node['right_value']}";
    		
        	$rst = $this->_dbo->execute($sql);
//        	if (!$rst) break;
        	
    	} while(false);
    	
    	$this->_dbo->completeTrans();
    	return $rst;
    }

    /**
     * 删除一个节点及其子节点树
     *
     * @param int $nodeId
     *
     * @return boolean
     */
    function removeByPkv($nodeId) {
    	$node = $this->find(array($this->primaryKey=>$nodeId));        
        if (!$node) {
        	throw new NodeNotFoundException($nodeId);
        }
        return $this->remove($node);
    }
	
	/**
	 * 通过路径查找分类: /图书/IT/技术/PHP
	 *
	 * @param string $name
	 * @return array
	 */
	function getByName($name){
		if (empty($name)) return null ;
		
		$path = trim($name);
			
		if (!preg_match('/^\//',$name)) $name = "/{$name}";
				
		if ($name === '/'){
			$name = $this->_rootNodeName ;
			$deep = 0;
		}else {
			$parts = normalize($path,'/');
			$deep = count($parts);
			$name = array_pop($parts);
		}		
		return $this->find(array('name' => $name ,'deep' => $deep) );		
	}
    
	function findAll($cond, $sort=null, $fields='*'){
		$sqlCond = CoreDbSqlHelper::parseConditions($this->_dbo,$cond);
		if ($sqlCond) $sqlCond = "WHERE {$sqlCond}";
		if ($sort) $sort = "ORDER BY {$sort}";
		
		$qfields = CoreDbSqlHelper::qfields($fields,$this->tableName);
		
		$sql = "SELECT {$qfields} FROM {$this->tableName} {$sqlCond} {$sort}";
		
		return $this->_dbo->getAll($sql);
	}
	
    /**
     * 返回根节点到指定节点路径上的所有节点
     *
     * 返回的结果不包括“_#_ROOT_NODE_#_”根节点各个节点同级别的其他节点。
     * 结果集是一个二维数组,可以用 array_to_tree() 函数转换为层次结构(树型)。
     *
     * @param array $node
     *
     * @return array
     */
    function getPath(array $node, $fields='*') {
    	
    	$inputarr = array($node['left_value'], $node['right_value']);
    	
    	$cond = CoreDbSqlHelper::bind($this->_dbo,'left_value < ? AND right_value > ?',$inputarr);
        $sort = 'left_value ASC';
        $rowset = $this->findAll($cond, $sort, $fields);
        if (is_array($rowset)) {
            array_shift($rowset);
        }
        return $rowset;
    }
        
	/**
     * 取得下层分类到指定的上层分类的完整路径
     * 
     * 返回的结果不包括 $up 节点
    
     * @param array $down
     * @param array $up
     * 
     * @return array
     */
    function getPartPath(array $down, array $up, $fields='*'){
    	
    	//父分类的left_value小于子分类的left_value;父分类的right_value大于子分类的right_value
    	$inputarr = array($up['left_value'], $down['left_value'],$down['right_value']);   
    	 	
    	$cond = CoreDbSqlHelper::bind($this->_dbo,'left_value BETWEEN ? AND ? AND right_value > ?',$inputarr);    	
    	$sort = "left_value ASC";
        $rowset = $this->findAll($cond, $sort, $fields);
    	if (is_array($rowset)) {
            array_shift($rowset);
        }
        return $rowset;
    }
    

    /**
     * 返回指定节点的直接子节点
     *
     * @param array $node
     *
     * @return array
     */
    function getSubNodes(array $node, $fields='*') {
    	$inputarr = array($node[$this->primaryKey]);
    	
        $cond = CoreDbSqlHelper::bind($this->_dbo,'parent_id = ?', $inputarr);
        $sort = 'left_value ASC';
        return $this->findAll($cond, $sort, $fields);
    }

    /**
     * 返回指定节点为根的整个子节点树
     *
     * @param array $node
     *
     * @return array
     */
    function getSubTree(array $node, $fields='*') {
    	$inputarr = array($node['left_value'], $node['right_value']);
    	
        $cond = CoreDbSqlHelper::bind($this->_dbo,'left_value BETWEEN ? AND ?', $inputarr);
        $sort = 'left_value ASC';
        return $this->findAll($cond, $sort, $fields);
    }

    /**
     * 获取指定节点同级别的所有节点
     *
     * @param array $node
     *
     * @return array
     */
    function getCurrentLevelNodes(array $node, $fields='*') {
        $cond = array('parent_id' => $node['parent_id']);
        $sort = 'left_value ASC';
        return $this->findAll($cond, $sort, $fields);
    }

    /**
     * 取得所有节点
     *
     * @return array
     */
    function getAllNodes($fields='*') {
        return parent::findAll('left_value > 1', 'left_value ASC', $fields);
    }

    /**
     * 获取所有顶级节点(既 _#_ROOT_NODE_#_ 的直接子节点)
     *
     * @return array
     */
    function getAllTopNodes($fields='*') {
        $cond = "parent_id = 0";
        $sort = 'left_value ASC';
        return $this->findAll($cond, $sort, $fields);
    }

    /**
     * 计算所有子节点的总数
     *
     * @param array $node
     *
     * @return int
     */
    function calcAllChildCount(array $node) {
        return intval(($node['right_value'] - $node['left_value'] - 1) / 2);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值