<?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);
}
}