521 lines
18 KiB
PHP
521 lines
18 KiB
PHP
<?php
|
|
// $Id: mtree.php,v 1.1 2004/07/23 00:22:13 tomas Exp $
|
|
|
|
/**
|
|
* Mtree class
|
|
*
|
|
* class for tree hierarchy stored in db
|
|
*
|
|
* example config: example/conf.php
|
|
* example minimal config:
|
|
* $config = array(
|
|
* 'dsn' => array( // data source definition
|
|
* 'username' => DBUSER,
|
|
* 'password' => DBPASSWORD,
|
|
* 'hostspec' => 'localhost',
|
|
* 'phptype' => 'pgsql',
|
|
* 'database' => DBNAME
|
|
* ),
|
|
* 'tblNamePrefix' => 'al_',
|
|
* 'RootNode' =>'RootNode',
|
|
* );
|
|
* (mysql phptype is tested too, but psql is recommended)
|
|
**/
|
|
define('ALIBERR_MTREE', 10);
|
|
|
|
class Mtree{
|
|
var $dbc;
|
|
var $config;
|
|
var $treeTable;
|
|
var $rootNodeName;
|
|
/** Mtree - constructor
|
|
*
|
|
* @param dbc object
|
|
* @param config array
|
|
* @return this
|
|
**/
|
|
function Mtree(&$dbc, $config)
|
|
{
|
|
$this->dbc =& $dbc;
|
|
$this->config = $config;
|
|
$this->treeTable = $config['tblNamePrefix'].'tree';
|
|
$this->rootNodeName = $config['RootNode'];
|
|
}
|
|
|
|
/* ========== public methods: ========== */
|
|
|
|
/**
|
|
* addObj
|
|
*
|
|
* @param name string
|
|
* @param type string
|
|
* @param parid int OPT // parent id
|
|
* @param aftid int OPT // after id
|
|
* @param param string OPT
|
|
* @return int/err // new id of inserted object
|
|
**/
|
|
function addObj($name, $type, $parid=1, $aftid=NULL, $param='')
|
|
{
|
|
if($name=='' || $type=='') return PEAR::raiseError('Mtree::addObj: Wrong name or type', ALIBERR_MTREE);
|
|
$this->dbc->query("BEGIN");
|
|
$r = $this->dbc->query("LOCK TABLE {$this->treeTable}"); if(PEAR::isError($r)) return $r;
|
|
// position resolving:
|
|
if(is_null($aftid)){ // add object as last child
|
|
$after = $this->dbc->getOne("
|
|
SELECT max(rgt) FROM {$this->treeTable} WHERE parid='$parid'
|
|
");
|
|
}else{ // use 'aftid'
|
|
$after = $this->dbc->getOne("
|
|
SELECT ".($aftid == $parid ? 'lft' : 'rgt')."
|
|
FROM {$this->treeTable} WHERE id='$aftid'");
|
|
}
|
|
if(PEAR::isError($after)) return $this->_dbRollback($after);
|
|
if(is_null($after)){ // position not specified - add as first child
|
|
$after = $this->dbc->getOne("
|
|
SELECT lft FROM {$this->treeTable} WHERE id='$parid'
|
|
");
|
|
}
|
|
if(PEAR::isError($after)) return $this->_dbRollback($after);
|
|
$after = intval($after);
|
|
// tree level resolving:
|
|
$level = $this->dbc->getOne("SELECT level FROM {$this->treeTable} WHERE id='$parid'");
|
|
if(is_null($level)) return $this->_dbRollback('addObj: parent does not exist');
|
|
if(PEAR::isError($level)) return $this->_dbRollback($level);
|
|
$id = $this->dbc->nextId("{$this->treeTable}_id_seq");
|
|
if(PEAR::isError($id)) return $this->_dbRollback($id);
|
|
// creating space in rgt/lft sequencies:
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt+2 WHERE rgt>$after");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft+2 WHERE lft>$after");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
// inserting object:
|
|
$r = $this->dbc->query("
|
|
INSERT INTO {$this->treeTable} (id, name, type, parid, level, lft, rgt, param)
|
|
VALUES ('$id', '$name', '$type', $parid, ".($level+1).", ".($after+1).", ".($after+2).", '$param')
|
|
");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("COMMIT");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
return $id;
|
|
}
|
|
|
|
/**
|
|
* copyObj
|
|
*
|
|
* @param id int
|
|
* @param newParid int
|
|
* @param after int OPT
|
|
* @return int/err
|
|
**/
|
|
function copyObj($id, $newParid, $after=NULL)
|
|
{
|
|
$o = $this->dbc->getRow("SELECT * FROM {$this->treeTable} WHERE id='$id'");
|
|
if(PEAR::isError($o)) return $o;
|
|
$nid = $this->addObj($o['name'], $o['type'], $newParid, $after, $o['param']);
|
|
return $nid;
|
|
}
|
|
|
|
/**
|
|
* renameObj
|
|
*
|
|
* @param id int
|
|
* @param newName string
|
|
* @return int/err
|
|
**/
|
|
function renameObj($id, $newName)
|
|
{
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET name='$newName' WHERE id='$id'");
|
|
if(PEAR::isError($r)) return $r;
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* removeObj
|
|
*
|
|
* @param id int
|
|
* @return boolean/err
|
|
**/
|
|
function removeObj($id)
|
|
{
|
|
$dirarr = $this->getDir($id); if(PEAR::isError($dirarr)) return $dirarr;
|
|
foreach($dirarr as $k=>$snod)
|
|
{
|
|
$this->removeObj($snod['id']);
|
|
}
|
|
$this->dbc->query("BEGIN");
|
|
$r = $this->dbc->query("LOCK TABLE {$this->treeTable}"); if(PEAR::isError($r)) return $r;
|
|
$rgt = $this->dbc->getOne("SELECT rgt FROM {$this->treeTable} WHERE id='$id'");
|
|
if(is_null($rgt)) return $this->_dbRollback('removeObj: object not exists');
|
|
// deleting object:
|
|
$r = $this->dbc->query("DELETE FROM {$this->treeTable} WHERE id='$id'");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
// closing the space in rgt/lft sequencies:
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt-2 WHERE rgt>$rgt");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft-2 WHERE lft>$rgt");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("COMMIT");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
return TRUE;
|
|
}
|
|
|
|
/* --- info methods: --- */
|
|
|
|
/**
|
|
* getObjId - search dir for object by name
|
|
*
|
|
* @param name string
|
|
* @param parId int OPT
|
|
* @return int/err
|
|
**/
|
|
function getObjId($name, $parId=NULL)
|
|
{
|
|
if($name=='' && is_null($parId)) $name = $this->rootNodeName;
|
|
return $this->dbc->getOne(
|
|
"SELECT id FROM {$this->treeTable} WHERE name='$name' and ".($parId ? "parid='$parId'":"parid is null")
|
|
);
|
|
}
|
|
|
|
/**
|
|
* getObjName - get one value for object (default: get name)
|
|
*
|
|
* @param oid int
|
|
* @param fld string OPT
|
|
* @return string/err
|
|
**/
|
|
function getObjName($oid, $fld='name')
|
|
{
|
|
return $this->dbc->getOne("SELECT $fld FROM {$this->treeTable} WHERE id='$oid'");
|
|
}
|
|
|
|
/**
|
|
* getObjType
|
|
*
|
|
* @param oid int
|
|
* @return string/err
|
|
**/
|
|
function getObjType($oid)
|
|
{
|
|
return $this->getObjName($oid, 'type');
|
|
}
|
|
|
|
/**
|
|
* getParent
|
|
*
|
|
* @param id int
|
|
* @return string/err
|
|
**/
|
|
function getParent($oid)
|
|
{
|
|
return $this->getObjName($oid, 'parid');
|
|
}
|
|
|
|
/**
|
|
* getPath - get array of nodes in object's path
|
|
*
|
|
* @param id int
|
|
* @param flds string OPT
|
|
* @return array/err
|
|
**/
|
|
function getPath($id, $flds='id')
|
|
{
|
|
$this->dbc->query("BEGIN");
|
|
$a = $this->dbc->getRow("SELECT name, lft, rgt FROM {$this->treeTable} WHERE id='$id'");
|
|
$res = $this->dbc->getAll("
|
|
SELECT $flds FROM {$this->treeTable} WHERE lft<={$a['lft']} AND rgt>={$a['rgt']}
|
|
ORDER by lft
|
|
");
|
|
$this->dbc->query("COMMIT");
|
|
return $res;
|
|
}
|
|
|
|
/**
|
|
* getDir - get array of childnodes
|
|
*
|
|
* @param id int
|
|
* @param flds string OPT
|
|
* @param order string OPT
|
|
* @return array/err
|
|
**/
|
|
function getDir($id, $flds='id', $order='lft')
|
|
{
|
|
return $this->dbc->getAll("
|
|
SELECT $flds FROM {$this->treeTable} WHERE parid='$id' ORDER BY $order
|
|
");
|
|
}
|
|
|
|
/**
|
|
* getSubTree
|
|
*
|
|
* @param id int OPT
|
|
* @param withRoot boolean OPT
|
|
* @return array/err
|
|
**/
|
|
function getSubTree($id=NULL, $withRoot=FALSE)
|
|
{
|
|
if(is_null($id)) $id = $this->getRootNode();
|
|
$r = array();
|
|
if($withRoot) $r[] = $re = $this->dbc->getRow("SELECT id, name, level FROM {$this->treeTable} WHERE id='$id'");
|
|
if(PEAR::isError($re)) return $re;
|
|
$dirarr = $this->getDir($id); if(PEAR::isError($dirarr)) return $dirarr;
|
|
foreach($dirarr as $k=>$snod)
|
|
{
|
|
$r[] = $re = $this->dbc->getRow("SELECT id, name, level FROM {$this->treeTable} WHERE id={$snod['id']}");
|
|
if(PEAR::isError($re)) return $re;
|
|
$r = array_merge($r, $this->getSubTree($snod['id']));
|
|
}
|
|
return $r;
|
|
}
|
|
|
|
/**
|
|
* getRootNode - get id of root node
|
|
*
|
|
* @return int/err
|
|
**/
|
|
function getRootNode()
|
|
{
|
|
return $this->getObjId($this->rootNodeName);
|
|
}
|
|
|
|
/**
|
|
* getAllObjects
|
|
*
|
|
* @return array/err
|
|
**/
|
|
function getAllObjects()
|
|
{
|
|
return $this->dbc->getAll("SELECT * FROM {$this->treeTable} ORDER BY lft");
|
|
}
|
|
|
|
/* --- info methods related to application structure: --- */
|
|
/* (this part should be added/rewritten to allow defining/modifying/using application structure) */
|
|
/* (only very simple structure definition - in config - supported now) */
|
|
|
|
/**
|
|
* getAllowedChildTypes
|
|
*
|
|
* @param type string
|
|
* @return array
|
|
**/
|
|
function getAllowedChildTypes($type)
|
|
{
|
|
return $this->config['objtypes'][$type];
|
|
}
|
|
|
|
|
|
/* ========== "private" methods: ========== */
|
|
|
|
|
|
/**
|
|
* _dbRollback
|
|
*
|
|
* @param r object/string
|
|
* @return err
|
|
**/
|
|
function _dbRollback($r)
|
|
{
|
|
$this->dbc->query("ROLLBACK");
|
|
if(PEAR::isError($r)) return $r;
|
|
elseif(is_string($r)) return PEAR::raiseError("ERROR: ".get_class($this).": $r", ALIBERR_MTREE, PEAR_ERROR_RETURN);
|
|
else return PEAR::raiseError("ERROR: ".get_class($this).": unknown error", ALIBERR_MTREE, PEAR_ERROR_RETURN);
|
|
}
|
|
|
|
/**
|
|
* _relocateSubtree - move subtree to another node without removing/adding
|
|
*
|
|
* @param id int
|
|
* @param newParid int
|
|
* @param after int
|
|
* @return boolean/err
|
|
**/
|
|
function _relocateSubtree($id, $newParid, $after=NULL)
|
|
{
|
|
$this->dbc->query("BEGIN");
|
|
$r = $this->dbc->query("LOCK TABLE {$this->treeTable}"); if(PEAR::isError($r)) return $r;
|
|
// obtain values for source node:
|
|
$a1 = $this->dbc->getRow("SELECT lft, rgt, level FROM {$this->treeTable} WHERE id='$id'");
|
|
if(is_null($a1)) return $this->_dbRollback('_relocateSubtree: object not exists');
|
|
extract($a1);
|
|
// values for destination node:
|
|
$a2 = $this->dbc->getRow("SELECT rgt, level FROM {$this->treeTable} WHERE id='$newParid'");
|
|
if(is_null($a2)) return $this->_dbRollback('_relocateSubtree: new parent not exists');
|
|
$nprgt = $a2['rgt']; $newLevel = $a2['level'];
|
|
// calculate differencies:
|
|
if(is_null($after)) $after = $nprgt-1;
|
|
$dif1 = $rgt-$lft+1;
|
|
$dif2 = $after-$lft+1;
|
|
$dif3 = $newLevel-$level+1;
|
|
// relocate the object"
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET parid='$newParid' WHERE id='$id'");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
if($after>$rgt){
|
|
// relocate subtree to the right:
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt+$dif1 WHERE rgt>$after");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft+$dif1 WHERE lft>$after");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable}
|
|
SET lft=lft+$dif2, rgt=rgt+$dif2, level=level+$dif3
|
|
WHERE lft>=$lft AND rgt <=$rgt");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt-$dif1 WHERE rgt>$rgt");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft-$dif1 WHERE lft>$rgt");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
}else{
|
|
// relocate subtree to the left:
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt+$dif1 WHERE rgt>$after");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft+$dif1 WHERE lft>$after");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable}
|
|
SET lft=lft+$dif2-$dif1, rgt=rgt+$dif2-$dif1, level=level+$dif3
|
|
WHERE lft>=$lft+$dif1 AND rgt <=$rgt+$dif1");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET rgt=rgt-$dif1 WHERE rgt>$rgt+$dif1");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
$r = $this->dbc->query("UPDATE {$this->treeTable} SET lft=lft-$dif1 WHERE lft>$rgt+$dif1");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
}
|
|
$r = $this->dbc->query("COMMIT");
|
|
if(PEAR::isError($r)) return $this->_dbRollback($r);
|
|
return TRUE;
|
|
}
|
|
|
|
/**
|
|
* _copySubtree - recursive copyObj
|
|
*
|
|
* @param id int
|
|
* @param newParid int
|
|
* @param after int
|
|
* @return array
|
|
**/
|
|
function _copySubtree($id, $newParid, $after=NULL)
|
|
{
|
|
$nid = $this->copyObj($id, $newParid, $after);
|
|
if(PEAR::isError($nid)) return $nid;
|
|
$dirarr = $this->getDir($id); if(PEAR::isError($dirarr)) return $dirarr;
|
|
foreach($dirarr as $k=>$snod)
|
|
{
|
|
$r = $this->_copySubtree($snod['id'], $nid);
|
|
if(PEAR::isError($r)) return $r;
|
|
}
|
|
}
|
|
|
|
/* ========== test and debug methods: ========== */
|
|
|
|
/**
|
|
* dumpTree
|
|
*
|
|
* @param id int
|
|
* @param indstr string // indentation string
|
|
* @param ind string // aktual indentation
|
|
* @return string
|
|
**/
|
|
function dumpTree($id=NULL, $indstr=' ', $ind='', $format='{name}', $withRoot=TRUE)
|
|
{
|
|
$r='';
|
|
foreach($this->getSubTree($id, $withRoot) as $o)
|
|
$r .= str_repeat($indstr, intval($o['level'])).
|
|
preg_replace(array('|\{name\}|', '|\{id\}|'), array($o['name'], $o['id']), $format).
|
|
"\n";
|
|
return $r;
|
|
}
|
|
|
|
/**
|
|
* deleteData
|
|
*
|
|
**/
|
|
function deleteData()
|
|
{
|
|
$this->dbc->query("DELETE FROM {$this->treeTable} WHERE parid is not null");
|
|
}
|
|
|
|
/**
|
|
* testData
|
|
*
|
|
* @param id int OPT
|
|
* @return array
|
|
**/
|
|
function testData()
|
|
{
|
|
$o[] = $rootId = $this->getRootNode();
|
|
$o[] = $p1 = $this->addObj('Publication A', 'Publication', $rootId); // 1
|
|
$o[] = $p2 = $this->addObj('Publication B', 'Publication', $rootId); // 2
|
|
$o[] = $i1 = $this->addObj('Issue 1', 'Issue', $p1); // 3
|
|
$o[] = $i2 = $this->addObj('Issue 2', 'Issue', $p1); // 4
|
|
$o[] = $s1 = $this->addObj('Section a', 'Section', $i2);
|
|
$o[] = $s2 = $this->addObj('Section b', 'Section', $i2); // 6
|
|
$o[] = $s3 = $this->addObj('Section c', 'Section', $i2);
|
|
$o[] = $t1 = $this->addObj('Title', 'Title', $s2);
|
|
$o[] = $s4 = $this->addObj('Section a', 'Section', $i1);
|
|
$o[] = $s5 = $this->addObj('Section b', 'Section', $i1);
|
|
$this->tdata['tree'] = $o;
|
|
}
|
|
|
|
/**
|
|
* test
|
|
*
|
|
**/
|
|
function test()
|
|
{
|
|
$this->deleteData();
|
|
$this->testData();
|
|
$rootId = $this->getRootNode();
|
|
$this->test_correct ="RootNode\n Publication A\n Issue 1\n Section a\n Section b\n Issue 2\n Section a\n Section b\n Title\n Section c\n Publication B\nRootNode\n";
|
|
$this->test_dump = $this->dumpTree();
|
|
$this->removeObj($this->tdata['tree'][1]);
|
|
$this->removeObj($this->tdata['tree'][2]);
|
|
$this->test_dump .= $this->dumpTree();
|
|
$this->deleteData();
|
|
if($this->test_dump == $this->test_correct){ $this->test_log.="tree: OK\n"; return TRUE; }
|
|
else return PEAR::raiseError('Mtree::test:', 1, PEAR_ERROR_DIE, '%s'.
|
|
"<pre>\ncorrect:\n.{$this->test_correct}.\ndump:\n.{$this->test_dump}.\n</pre>\n");
|
|
}
|
|
|
|
/**
|
|
* install - create tables + initialize
|
|
*
|
|
**/
|
|
function install()
|
|
{
|
|
$this->dbc->query("CREATE TABLE {$this->treeTable} (
|
|
id int not null,
|
|
name varchar(255) not null default'',
|
|
parid int,
|
|
lft int,
|
|
rgt int,
|
|
level int,
|
|
type varchar(255) not null default'',
|
|
param varchar(255)
|
|
)");
|
|
$this->dbc->query("CREATE UNIQUE INDEX {$this->treeTable}_id_idx on {$this->treeTable} (id)");
|
|
$this->dbc->query("CREATE INDEX {$this->treeTable}_name_idx on {$this->treeTable} (name)");
|
|
$this->dbc->createSequence("{$this->treeTable}_id_seq");
|
|
|
|
$id = $this->dbc->nextId("{$this->treeTable}_id_seq");
|
|
$this->dbc->query("INSERT INTO {$this->treeTable} (id, name, parid, level, lft, rgt, type)
|
|
VALUES ($id, '{$this->rootNodeName}', NULL, 0, 1, 2, 'RootNode')");
|
|
}
|
|
|
|
/**
|
|
* uninstall
|
|
*
|
|
**/
|
|
function uninstall()
|
|
{
|
|
$this->dbc->query("DROP TABLE {$this->treeTable}");
|
|
$this->dbc->dropSequence("{$this->treeTable}_id_seq");
|
|
}
|
|
|
|
/**
|
|
* reinstall
|
|
*
|
|
**/
|
|
function reinstall()
|
|
{
|
|
$this->uninstall();
|
|
$this->install();
|
|
}
|
|
}
|
|
?>
|