| // | Jason Rust | // +----------------------------------------------------------------------+ // $Id$ // CREDITS: // -------- // - Thanks to Kristian Koehntopp for publishing an explanation of the Nested Set // technique and for the great work he did and does for the php community // - Thanks to Daniel T. Gorski for his great tutorial on www.develnet.org // - Thanks to my parents for ... just kidding :] require_once 'PEAR.php'; // {{{ constants // Error and message codes define('NESE_ERROR_RECURSION', 'E100'); define('NESE_ERROR_NODRIVER', 'E200'); define('NESE_ERROR_NOHANDLER', 'E300'); define('NESE_ERROR_TBLOCKED', 'E010'); define('NESE_MESSAGE_UNKNOWN', 'E0'); define('NESE_ERROR_NOTSUPPORTED', 'E1'); define('NESE_ERROR_PARAM_MISSING', 'E400'); define('NESE_ERROR_NOT_FOUND', 'E500'); define('NESE_ERROR_WRONG_MPARAM', 'E2'); // for moving a node before another define('NESE_MOVE_BEFORE', 'BE'); // for moving a node after another define('NESE_MOVE_AFTER', 'AF'); // for moving a node below another define('NESE_MOVE_BELOW', 'SUB'); // Sortorders define('NESE_SORT_LEVEL', 'SLV'); define('NESE_SORT_PREORDER', 'SPO'); // }}} // {{{ DB_NestedSet:: class /** * DB_NestedSet is a class for handling nested sets * * @author Daniel Khan * @package DB_NestedSet * @version $Revision: 1.86 $ * @access public */ // }}} class DB_NestedSet { // {{{ properties /** * * @var array The field parameters of the table with the nested set. Format: 'realFieldName' => 'fieldId' * @access public */ var $params = array('STRID' => 'id', 'ROOTID' => 'rootid', 'l' => 'l', 'r' => 'r', 'STREH' => 'norder', 'LEVEL' => 'level', // 'parent'=>'parent', // Optional but very useful 'STRNA' => 'name' ); // To be used with 2.0 - would be an api break atm // var $quotedParams = array('name'); /** * * @var string The table with the actual tree data * @access public */ var $node_table = 'tb_nodes'; /** * * @var string The table to handle locking * @access public */ var $lock_table = 'tb_locks'; /** * * @var string The table used for sequences * @access public */ var $sequence_table; /** * Secondary order field. Normally this is the order field, but can be changed to * something else (i.e. the name field so that the tree can be shown alphabetically) * * @var string * @access public */ var $secondarySort; /** * Used to store the secondary sort method set by the user while doing manipulative queries * * @var string * @access private */ var $_userSecondarySort = false; /** * The default sorting field - will be set to the table column inside the constructor * * @var string * @access private */ var $_defaultSecondarySort = 'norder'; /** * * @var int The time to live of the lock * @access public */ var $lockTTL = 1; /** * * @var bool Enable debugging statements? * @access public */ var $debug = 0; /** * * @var bool Lock the structure of the table? * @access private */ var $_structureTableLock = false; /** * * @var bool Don't allow unlocking (used inside of moves) * @access private */ var $_lockExclusive = false; /** * * @var object cache Optional PEAR::Cache object * @access public */ var $cache = false; /** * Specify the sortMode of the query methods * NESE_SORT_LEVEL is the 'old' sorting method and sorts a tree by level * all nodes of level 1, all nodes of level 2,... * NESE_SORT_PREORDER will sort doing a preorder walk. * So all children of node x will come right after it * Note that moving a node within it's siblings will obviously not change the output * in this mode * * @var constant Order method (NESE_SORT_LEVEL|NESE_SORT_PREORDER) * @access private */ var $_sortMode = NESE_SORT_LEVEL; /** * * @var array Available sortModes * @access private */ var $_sortModes = array(NESE_SORT_LEVEL, NESE_SORT_PREORDER); /** * * @var array An array of field ids that must exist in the table * @access private */ var $_requiredParams = array('id', 'rootid', 'l', 'r', 'norder', 'level'); /** * * @var bool Skip the callback events? * @access private */ var $_skipCallbacks = false; /** * * @var bool Do we want to use caching * @access private */ var $_caching = false; /** * * @var array The above parameters flipped for easy access * @access private */ var $flparams = array(); /** * * @var bool Temporary switch for cache * @access private */ var $_restcache = false; /** * Used to determine the presence of listeners for an event in triggerEvent() * * If any event listeners are registered for an event, the event name will * have a key set in this array, otherwise, it will not be set. * * @see triggerEvent * @var arrayg * @access private */ var $_hasListeners = array(); /** * * @var string packagename * @access private */ var $_packagename = 'DB_NestedSet'; /** * * @var int Majorversion * @access private */ var $_majorversion = 1; /** * * @var string Minorversion * @access private */ var $_minorversion = '3'; /** * * @var array Used for mapping a cloned tree to the real tree for move_* operations * @access private */ var $_relations = array(); /** * Used for _internal_ tree conversion * * @var bool Turn off user param verification and id generation * @access private */ var $_dumbmode = false; /** * * @var array Map of error messages to their descriptions */ var $messages = array( NESE_ERROR_RECURSION => '%s: This operation would lead to a recursion', NESE_ERROR_TBLOCKED => 'The structure Table is locked for another database operation, please retry.', NESE_ERROR_NODRIVER => 'The selected database driver %s wasn\'t found', NESE_ERROR_NOTSUPPORTED => 'Method not supported yet', NESE_ERROR_NOHANDLER => 'Event handler not found', NESE_ERROR_PARAM_MISSING => 'Parameter missing', NESE_MESSAGE_UNKNOWN => 'Unknown error or message', NESE_ERROR_NOT_FOUND => '%s: Node %s not found', NESE_ERROR_WRONG_MPARAM => '%s: %s' ); /** * * @var array The array of event listeners * @access private */ var $eventListeners = array(); // }}} // +---------------------------------------+ // | Base methods | // +---------------------------------------+ // {{{ constructor /** * Constructor * * @param array $params Database column fields which should be returned * @access private * @return void */ function DB_NestedSet($params) { if ($this->debug) { $this->_debugMessage('DB_NestedSet()'); } if (is_array($params) && count($params) > 0) { $this->params = $params; } $this->flparams = array_flip($this->params); $this->sequence_table = $this->node_table . '_' . $this->flparams['id']; $this->secondarySort = $this->flparams[$this->_defaultSecondarySort]; register_shutdown_function(array(& $this, '_DB_NestedSet')); } // }}} // {{{ destructor /** * PEAR Destructor * Releases all locks * Closes open database connections * * @access private * @return void */ function _DB_NestedSet() { if ($this->debug) { $this->_debugMessage('_DB_NestedSet()'); } $this->_releaseLock(true); } // }}} // {{{ factory /** * Handles the returning of a concrete instance of DB_NestedSet based on the driver. * If the class given by $driver allready exists it will be used. * If not the driver will be searched inside the default path ./NestedSet/ * * @param string $driver The driver, such as DB or MDB * @param string $dsn The dsn for connecting to the database * @param array $params The field name params for the node table * @static * @access public * @return object The DB_NestedSet object */ function & factory($driver, $dsn, $params = array()) { $classname = 'DB_NestedSet_' . $driver; if (!class_exists($classname)) { $driverpath = dirname(__FILE__) . '/NestedSet/' . $driver . '.php'; if (!file_exists($driverpath) || !$driver) { return PEAR::raiseError("factory(): The database driver '$driver' wasn't found", NESE_ERROR_NODRIVER, PEAR_ERROR_TRIGGER, E_USER_ERROR); } include_once($driverpath); } $c = & new $classname($dsn, $params); return $c; } // }}} // +----------------------------------------------+ // | NestedSet manipulation and query methods | // |----------------------------------------------+ // | Querying the tree | // +----------------------------------------------+ // {{{ getAllNodes() /** * Fetch the whole NestedSet * * @param bool $keepAsArray (optional) Keep the result as an array or transform it into * a set of DB_NestedSet_Node objects? * @param bool $aliasFields (optional) Should we alias the fields so they are the names * of the parameter keys, or leave them as is? * @param array $addSQL (optional) Array of additional params to pass to the query. * @access public * @return mixed False on error, or an array of nodes */ function getAllNodes($keepAsArray = false, $aliasFields = true, $addSQL = array()) { if ($this->debug) { $this->_debugMessage('getAllNodes()'); } if ($this->_sortMode == NESE_SORT_LEVEL) { $sql = sprintf('SELECT %s %s FROM %s %s %s %s ORDER BY %s.%s, %s.%s ASC', $this->_getSelectFields($aliasFields), $this->_addSQL($addSQL, 'cols'), $this->node_table, $this->_addSQL($addSQL, 'join'), $this->_addSQL($addSQL, 'where', 'WHERE'), $this->_addSQL($addSQL, 'append'), $this->node_table, $this->flparams['level'], $this->node_table, $this->secondarySort); } elseif ($this->_sortMode == NESE_SORT_PREORDER) { $nodeSet = array(); $rootnodes = $this->getRootNodes(true); foreach($rootnodes AS $rid => $rootnode) { $nodeSet = $nodeSet + $this->getBranch($rootnode, $keepAsArray, $aliasFields, $addSQL); } return $nodeSet; } if (!$this->_caching) { $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields); } else { $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields); } if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) { // EVENT (nodeLoad) foreach (array_keys($nodeSet) as $key) { $this->triggerEvent('nodeLoad', $nodeSet[$key]); } } return $nodeSet; } // }}} // {{{ getRootNodes() /** * Fetches the first level (the rootnodes) of the NestedSet * * @param bool $keepAsArray (optional) Keep the result as an array or transform it into * a set of DB_NestedSet_Node objects? * @param bool $aliasFields (optional) Should we alias the fields so they are the names * of the parameter keys, or leave them as is? * @param array $addSQL (optional) Array of additional params to pass to the query. * @see _addSQL * @access public * @return mixed False on error, or an array of nodes */ function getRootNodes($keepAsArray = false, $aliasFields = true, $addSQL = array()) { if ($this->debug) { $this->_debugMessage('getRootNodes()'); } $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s.%s %s %s ORDER BY %s.%s ASC', $this->_getSelectFields($aliasFields), $this->_addSQL($addSQL, 'cols'), $this->node_table, $this->_addSQL($addSQL, 'join'), $this->node_table, $this->flparams['id'], $this->node_table, $this->flparams['rootid'], $this->_addSQL($addSQL, 'where', 'AND'), $this->_addSQL($addSQL, 'append'), $this->node_table, $this->secondarySort); if (!$this->_caching) { $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields); } else { $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields); } if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) { // EVENT (nodeLoad) foreach (array_keys($nodeSet) as $key) { $this->triggerEvent('nodeLoad', $nodeSet[$key]); } } return $nodeSet; } // }}} // {{{ getBranch() /** * Fetch the whole branch where a given node id is in * * @param int $id The node ID * @param bool $keepAsArray (optional) Keep the result as an array or transform it into * a set of DB_NestedSet_Node objects? * @param bool $aliasFields (optional) Should we alias the fields so they are the names * of the parameter keys, or leave them as is? * @param array $addSQL (optional) Array of additional params to pass to the query. * @see _addSQL * @access public * @return mixed False on error, or an array of nodes */ function getBranch($id, $keepAsArray = false, $aliasFields = true, $addSQL = array()) { if ($this->debug) { $this->_debugMessage('getBranch($id)'); } if (!($thisnode = $this->pickNode($id, true))) { $epr = array('getBranch()', $id); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_NOTICE, $epr); } if ($this->_sortMode == NESE_SORT_LEVEL || $this->params[$this->secondarySort] != $this->_defaultSecondarySort) { $firstsort = $this->flparams['level']; $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s %s %s ORDER BY %s.%s, %s.%s ASC', $this->_getSelectFields($aliasFields), $this->_addSQL($addSQL, 'cols'), $this->node_table, $this->_addSQL($addSQL, 'join'), $this->node_table, $this->flparams['rootid'], $thisnode['rootid'], $this->_addSQL($addSQL, 'where', 'AND'), $this->_addSQL($addSQL, 'append'), $this->node_table, $firstsort, $this->node_table, $this->secondarySort); } elseif ($this->_sortMode == NESE_SORT_PREORDER) { $firstsort = $this->flparams['l']; $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s %s %s ORDER BY %s.%s ASC', $this->_getSelectFields($aliasFields), $this->_addSQL($addSQL, 'cols'), $this->node_table, $this->_addSQL($addSQL, 'join'), $this->node_table, $this->flparams['rootid'], $thisnode['rootid'], $this->_addSQL($addSQL, 'where', 'AND'), $this->_addSQL($addSQL, 'append'), $this->node_table, $firstsort, $this->node_table); } if (!$this->_caching) { $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields); } else { $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields); } if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) { // EVENT (nodeLoad) foreach (array_keys($nodeSet) as $key) { $this->triggerEvent('nodeLoad', $nodeSet[$key]); } } if ($this->_sortMode == NESE_SORT_PREORDER && ($this->params[$this->secondarySort] != $this->_defaultSecondarySort)) { $nodeSet = $this->_secSort($nodeSet); } return $nodeSet; } // }}} // {{{ getParents() /** * Fetch the parents of a node given by id * * @param int $id The node ID * @param bool $keepAsArray (optional) Keep the result as an array or transform it into * a set of DB_NestedSet_Node objects? * @param bool $aliasFields (optional) Should we alias the fields so they are the names * of the parameter keys, or leave them as is? * @param array $addSQL (optional) Array of additional params to pass to the query. * @see _addSQL * @access public * @return mixed False on error, or an array of nodes */ function getParents($id, $keepAsArray = false, $aliasFields = true, $addSQL = array()) { if ($this->debug) { $this->_debugMessage('getParents($id)'); } if (!($child = $this->pickNode($id, true))) { $epr = array('getParents()', $id); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_NOTICE, $epr); } $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s AND %s.%s<%s AND %s.%s<%s AND %s.%s>%s %s %s ORDER BY %s.%s ASC', $this->_getSelectFields($aliasFields), $this->_addSQL($addSQL, 'cols'), $this->node_table, $this->_addSQL($addSQL, 'join'), $this->node_table, $this->flparams['rootid'], $child['rootid'], $this->node_table, $this->flparams['level'], $child['level'], $this->node_table, $this->flparams['l'], $child['l'], $this->node_table, $this->flparams['r'], $child['r'], $this->_addSQL($addSQL, 'where', 'AND'), $this->_addSQL($addSQL, 'append'), $this->node_table, $this->flparams['level']); if (!$this->_caching) { $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields); } else { $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields); } if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) { // EVENT (nodeLoad) foreach (array_keys($nodeSet) as $key) { $this->triggerEvent('nodeLoad', $nodeSet[$key]); } } return $nodeSet; } // }}} // {{{ getParent() /** * Fetch the immediate parent of a node given by id * * @param int $id The node ID * @param bool $keepAsArray (optional) Keep the result as an array or transform it into * a set of DB_NestedSet_Node objects? * @param bool $aliasFields (optional) Should we alias the fields so they are the names * of the parameter keys, or leave them as is? * @param array $addSQL (optional) Array of additional params to pass to the query. * @see _addSQL * @access public * @return mixed False on error, or the parent node */ function getParent($id, $keepAsArray = false, $aliasFields = true, $addSQL = array(), $useDB = true) { if ($this->debug) { $this->_debugMessage('getParent($id)'); } if (!($child = $this->pickNode($id, true))) { $epr = array('getParent()', $id); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_NOTICE, $epr); } if ($child['id'] == $child['rootid']) { return false; } // If parent node is set inside the db simply return it if (isset($child['parent']) && !empty($child['parent']) && ($useDB == true)) { return $this->pickNode($child['parent'], $keepAsArray, $aliasFields, 'id', $addSQL); } $addSQL['where'] = sprintf('%s.%s = %s', $this->node_table, $this->flparams['level'], $child['level']-1); $nodeSet = $this->getParents($id, $keepAsArray, $aliasFields, $addSQL); if (!empty($nodeSet)) { $keys = array_keys($nodeSet); return $nodeSet[$keys[0]]; } else { return false; } } // }}} // {{{ getSiblings() /** * Fetch all siblings of the node given by id * Important: The node given by ID will also be returned * Do a unset($array[$id]) on the result if you don't want that * * @param int $id The node ID * @param bool $keepAsArray (optional) Keep the result as an array or transform it into * a set of DB_NestedSet_Node objects? * @param bool $aliasFields (optional) Should we alias the fields so they are the names * of the parameter keys, or leave them as is? * @param array $addSQL (optional) Array of additional params to pass to the query. * @see _addSQL * @access public * @return mixed False on error, or the parent node */ function getSiblings($id, $keepAsArray = false, $aliasFields = true, $addSQL = array()) { if ($this->debug) { $this->_debugMessage('getSiblings($id)'); } if (!($sibling = $this->pickNode($id, true))) { $epr = array('getSibling()', $id); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_NOTICE, $epr); } $parent = $this->getParent($sibling, true); return $this->getChildren($parent, $keepAsArray, $aliasFields, false, $addSQL); } // }}} // {{{ getChildren() /** * Fetch the children _one level_ after of a node given by id * * @param int $id The node ID * @param bool $keepAsArray (optional) Keep the result as an array or transform it into * a set of DB_NestedSet_Node objects? * @param bool $aliasFields (optional) Should we alias the fields so they are the names * of the parameter keys, or leave them as is? * @param bool $forceNorder (optional) Force the result to be ordered by the norder * param (as opposed to the value of secondary sort). Used by the move and * add methods. * @param array $addSQL (optional) Array of additional params to pass to the query. * @see _addSQL * @access public * @return mixed False on error, or an array of nodes */ function getChildren($id, $keepAsArray = false, $aliasFields = true, $forceNorder = false, $addSQL = array()) { if ($this->debug) { $this->_debugMessage('getChildren($id)'); } if (!($parent = $this->pickNode($id, true))) { $epr = array('getChildren()', $id); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_NOTICE, $epr); } if (!$parent || $parent['l'] == ($parent['r'] - 1)) { return false; } $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s=%s AND %s.%s=%s+1 AND %s.%s BETWEEN %s AND %s %s %s ORDER BY %s.%s ASC', $this->_getSelectFields($aliasFields), $this->_addSQL($addSQL, 'cols'), $this->node_table, $this->_addSQL($addSQL, 'join'), $this->node_table, $this->flparams['rootid'], $parent['rootid'], $this->node_table, $this->flparams['level'], $parent['level'], $this->node_table, $this->flparams['l'], $parent['l'], $parent['r'], $this->_addSQL($addSQL, 'where', 'AND'), $this->_addSQL($addSQL, 'append'), $this->node_table, $this->secondarySort); if (!$this->_caching) { $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields); } else { $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields); } if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) { // EVENT (nodeLoad) foreach (array_keys($nodeSet) as $key) { $this->triggerEvent('nodeLoad', $nodeSet[$key]); } } return $nodeSet; } // }}} // {{{ getSubBranch() /** * Fetch all the children of a node given by id * * getChildren only queries the immediate children * getSubBranch returns all nodes below the given node * * @param string $id The node ID * @param bool $keepAsArray (optional) Keep the result as an array or transform it into * a set of DB_NestedSet_Node objects? * @param bool $aliasFields (optional) Should we alias the fields so they are the names * of the parameter keys, or leave them as is? * @param array $addSQL (optional) Array of additional params to pass to the query. * @see _addSQL * @access public * @return mixed False on error, or an array of nodes */ function getSubBranch($id, $keepAsArray = false, $aliasFields = true, $addSQL = array()) { if ($this->debug) { $this->_debugMessage('getSubBranch($id)'); } if (!($parent = $this->pickNode($id, true))) { $epr = array('getSubBranch()', $id); return $this->_raiseError(NESE_ERROR_NOT_FOUND, E_USER_NOTICE, $epr); } if ($this->_sortMode == NESE_SORT_LEVEL || $this->params[$this->secondarySort] != $this->_defaultSecondarySort) { $firstsort = $this->flparams['level']; $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s BETWEEN %s AND %s AND %s.%s=%s AND %s.%s!=%s %s %s ORDER BY %s.%s, %s.%s ASC', $this->_getSelectFields($aliasFields), $this->_addSQL($addSQL, 'cols'), $this->node_table, $this->_addSQL($addSQL, 'join'), $this->node_table, $this->flparams['l'], $parent['l'], $parent['r'], $this->node_table, $this->flparams['rootid'], $parent['rootid'], $this->node_table, $this->flparams['id'], $id, $this->_addSQL($addSQL, 'where', 'AND'), $this->_addSQL($addSQL, 'append'), $this->node_table, $firstsort, $this->node_table, $this->secondarySort); } elseif ($this->_sortMode == NESE_SORT_PREORDER) { $firstsort = $this->flparams['l']; $sql = sprintf('SELECT %s %s FROM %s %s WHERE %s.%s BETWEEN %s AND %s AND %s.%s=%s AND %s.%s!=%s %s %s ORDER BY %s.%s ASC', $this->_getSelectFields($aliasFields), $this->_addSQL($addSQL, 'cols'), $this->node_table, $this->_addSQL($addSQL, 'join'), $this->node_table, $this->flparams['l'], $parent['l'], $parent['r'], $this->node_table, $this->flparams['rootid'], $parent['rootid'], $this->node_table, $this->flparams['id'], $id, $this->_addSQL($addSQL, 'where', 'AND'), $this->_addSQL($addSQL, 'append'), $this->node_table, $firstsort); } if (!$this->_caching) { $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields); } else { $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields); } if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) { // EVENT (nodeLoad) foreach (array_keys($nodeSet) as $key) { $this->triggerEvent('nodeLoad', $nodeSet[$key]); } } if ($this->_sortMode == NESE_SORT_PREORDER && ($this->params[$this->secondarySort] != $this->_defaultSecondarySort)) { $nodeSet = $this->_secSort($nodeSet); } return $nodeSet; } // }}} // {{{ pickNode() /** * Fetch the data of a node with the given id * * @param int $id The node id of the node to fetch * @param bool $keepAsArray (optional) Keep the result as an array or transform it into * a set of DB_NestedSet_Node objects? * @param bool $aliasFields (optional) Should we alias the fields so they are the names * of the parameter keys, or leave them as is? * @param string $idfield (optional) Which field has to be compared with $id? * This is can be used to pick a node by other values (e.g. it's name). * @param array $addSQL (optional) Array of additional params to pass to the query. * @see _addSQL * @access public * @return mixed False on error, or an array of nodes */ function pickNode($id, $keepAsArray = false, $aliasFields = true, $idfield = 'id', $addSQL = array()) { if ($this->debug) { $this->_debugMessage('pickNode($id)'); } if (is_object($id) && $id->id) { return $id; } elseif (is_array($id) && isset($id['id'])) { return $id; } if (!$id) { return false; } $sql = sprintf("SELECT %s %s FROM %s %s WHERE %s.%s=%s %s %s", $this->_getSelectFields($aliasFields), $this->_addSQL($addSQL, 'cols'), $this->node_table, $this->_addSQL($addSQL, 'join'), $this->node_table, $this->flparams[$idfield], $this->_quote($id), $this->_addSQL($addSQL, 'where', 'AND'), $this->_addSQL($addSQL, 'append')); if (!$this->_caching) { $nodeSet = $this->_processResultSet($sql, $keepAsArray, $aliasFields); } else { $nodeSet = $this->cache->call('DB_NestedSet->_processResultSet', $sql, $keepAsArray, $aliasFields); } $nsKey = false; if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeLoad'])) { // EVENT (nodeLoad) foreach (array_keys($nodeSet) as $key) { $this->triggerEvent('nodeLoad', $nodeSet[$key]); $nsKey = $key; } } else { foreach (array_keys($nodeSet) as $key) { $nsKey = $key; } } if (is_array($nodeSet) && $idfield != 'id') { $id = $nsKey; } return isset($nodeSet[$id]) ? $nodeSet[$id] : false; } // }}} // {{{ isParent() /** * See if a given node is a parent of another given node * * A node is considered to be a parent if it resides above the child * So it doesn't mean that the node has to be an immediate parent. * To get this information simply compare the levels of the two nodes * after you know that you have a parent relation. * * @param mixed $parent The parent node as array or object * @param mixed $child The child node as array or object * @access public * @return bool True if it's a parent */ function isParent($parent, $child) { if ($this->debug) { $this->_debugMessage('isParent($parent, $child)'); } if (!isset($parent) || !isset($child)) { return false; } if (is_array($parent)) { $p_rootid = $parent['rootid']; $p_l = $parent['l']; $p_r = $parent['r']; } elseif (is_object($parent)) { $p_rootid = $parent->rootid; $p_l = $parent->l; $p_r = $parent->r; } if (is_array($child)) { $c_rootid = $child['rootid']; $c_l = $child['l']; $c_r = $child['r']; } elseif (is_object($child)) { $c_rootid = $child->rootid; $c_l = $child->l; $c_r = $child->r; } if (($p_rootid == $c_rootid) && ($p_l < $c_l && $p_r > $c_r)) { return true; } return false; } // }}} // +----------------------------------------------+ // | NestedSet manipulation and query methods | // |----------------------------------------------+ // | insert / delete / update of nodes | // +----------------------------------------------+ // | [PUBLIC] | // +----------------------------------------------+ // {{{ createRootNode() /** * Creates a new root node. If no id is specified then it is either * added to the beginning/end of the tree based on the $pos. * Optionally it deletes the whole tree and creates one initial rootnode * *
    * +-- root1 [target]
    * |
    * +-- root2 [new]
    * |
    * +-- root3
    * 
* * @param array $values Hash with param => value pairs of the node (see $this->params) * @param integer $id ID of target node (the rootnode after which the node should be inserted) * @param bool $first Danger: Deletes and (re)init's the hole tree - sequences are reset * @param string $pos The position in which to insert the new node. * @access public * @return mixed The node id or false on error */ function createRootNode($values, $id = false, $first = false, $pos = NESE_MOVE_AFTER) { if ($this->debug) { $this->_debugMessage('createRootNode($values, $id = false, $first = false, $pos = \'AF\')'); } // Try to aquire a table lock if (PEAR::isError($lock = $this->_setLock())) { return $lock; } $this->_verifyUserValues('createRootNode()', $values); // If they specified an id, see if the parent is valid if (!$first && ($id && !$parent = $this->pickNode($id, true))) { $epr = array('createRootNode()', $id); $this->_releaseLock(); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr); } elseif ($first && $id) { // No notice for now. But these 2 params don't make sense together $epr = array('createRootNode()', '[id] AND [first] were passed - that doesn\'t make sense'); // $this->_raiseError(NESE_ERROR_WRONG_MPARAM, E_USER_WARNING, $epr); } elseif (!$first && !$id) { // If no id was specified, then determine order $parent = array(); if ($pos == NESE_MOVE_BEFORE) { $parent['norder'] = 1; } elseif ($pos == NESE_MOVE_AFTER) { // Put it at the end of the tree $qry = sprintf('SELECT MAX(%s) FROM %s WHERE %s=1', $this->flparams['norder'], $this->node_table, $this->flparams['l']); $tmp_order = $this->_getOne($qry); // If null, then it's the first one $parent['norder'] = is_null($tmp_order) ? 0 : $tmp_order; } } $sql = array(); $addval = array(); $addval[$this->flparams['level']] = 1; // Shall we delete the existing tree (reinit) if ($first) { $dsql = sprintf('DELETE FROM %s', $this->node_table); $this->_query($dsql); $this->_dropSequence($this->sequence_table); // New order of the new node will be 1 $addval[$this->flparams['norder']] = 1; } else { // Let's open a gap for the new node if ($pos == NESE_MOVE_AFTER) { $addval[$this->flparams['norder']] = $parent['norder'] + 1; $sql[] = sprintf('UPDATE %s SET %s=%s+1 WHERE %s=1 AND %s > %s', $this->node_table, $this->flparams['norder'], $this->flparams['norder'], $this->flparams['l'], $this->flparams['norder'], $parent['norder']); } elseif ($pos == NESE_MOVE_BEFORE) { $addval[$this->flparams['norder']] = $parent['norder']; $sql[] = sprintf('UPDATE %s SET %s=%s+1 WHERE %s=1 AND %s >= %s', $this->node_table, $this->flparams['norder'], $this->flparams['norder'], $this->flparams['l'], $this->flparams['norder'], $parent['norder']); } } if (isset($this->flparams['parent'])) { $addval[$this->flparams['parent']] = 0; } // Sequence of node id (equals to root id in this case if (!$this->_dumbmode || !$node_id = isset($values[$this->flparams['id']]) || !isset($values[$this->flparams['rootid']])) { $addval[$this->flparams['rootid']] = $node_id = $addval[$this->flparams['id']] = $this->_nextID($this->sequence_table); } else { $node_id = $values[$this->flparams['id']]; } // Left/Right values for rootnodes $addval[$this->flparams['l']] = 1; $addval[$this->flparams['r']] = 2; // Transform the node data hash to a query if (!$qr = $this->_values2InsertQuery($values, $addval)) { $this->_releaseLock(); return false; } // Insert the new node $sql[] = sprintf('INSERT INTO %s (%s) VALUES (%s)', $this->node_table, implode(', ', array_keys($qr)), implode(', ', $qr)); foreach ($sql as $qry) { $res = $this->_query($qry); $this->_testFatalAbort($res, __FILE__, __LINE__); } // EVENT (nodeCreate) if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeCreate'])) { $thisnode = $this->pickNode($node_id); $this->triggerEvent('nodeCreate', $thisnode); } $this->_releaseLock(); return $node_id; } // }}} // {{{ createSubNode() /** * Creates a subnode * *
    * +-- root1
    * |
    * +-\ root2 [target]
    * | |
    * | |-- subnode1 [new]
    * |
    * +-- root3
    * 
* * @param integer $id Parent node ID * @param array $values Hash with param => value pairs of the node (see $this->params) * @access public * @return mixed The node id or false on error */ function createSubNode($id, $values) { if ($this->debug) { $this->_debugMessage('createSubNode($id, $values)'); } // Try to aquire a table lock if (PEAR::isError($lock = $this->_setLock())) { return $lock; } // invalid parent id, bail out if (!($thisnode = $this->pickNode($id, true))) { $epr = array('createSubNode()', $id); $this->_releaseLock(); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr); } $this->_verifyUserValues('createRootNode()', $values); // Get the children of the target node $children = $this->getChildren($id, true); // We have children here if ($thisnode['r']-1 != $thisnode['l']) { // Get the last child $last = array_pop($children); // What we have to do is virtually an insert of a node after the last child // So we don't have to proceed creating a subnode $newNode = $this->createRightNode($last['id'], $values); $this->_releaseLock(); return $newNode; } $sql = array(); $sql[] = sprintf('UPDATE %s SET %s=CASE WHEN %s>%s THEN %s+2 ELSE %s END, %s=CASE WHEN (%s>%s OR %s>=%s) THEN %s+2 ELSE %s END WHERE %s=%s', $this->node_table, $this->flparams['l'], $this->flparams['l'], $thisnode['l'], $this->flparams['l'], $this->flparams['l'], $this->flparams['r'], $this->flparams['l'], $thisnode['l'], $this->flparams['r'], $thisnode['r'], $this->flparams['r'], $this->flparams['r'], $this->flparams['rootid'], $thisnode['rootid']); $addval = array(); if (isset($this->flparams['parent'])) { $addval[$this->flparams['parent']] = $thisnode['id']; } $addval[$this->flparams['l']] = $thisnode['r']; $addval[$this->flparams['r']] = $thisnode['r'] + 1; $addval[$this->flparams['rootid']] = $thisnode['rootid']; $addval[$this->flparams['norder']] = 1; $addval[$this->flparams['level']] = $thisnode['level'] + 1; if (!$this->_dumbmode || !$node_id = isset($values[$this->flparams['id']])) { $node_id = $addval[$this->flparams['id']] = $this->_nextID($this->sequence_table); } else { $node_id = $values[$this->flparams['id']]; } if (!$qr = $this->_values2InsertQuery($values, $addval)) { $this->_releaseLock(); return false; } $sql[] = sprintf('INSERT INTO %s (%s) VALUES (%s)', $this->node_table, implode(', ', array_keys($qr)), implode(', ', $qr)); foreach ($sql as $qry) { $res = $this->_query($qry); $this->_testFatalAbort($res, __FILE__, __LINE__); } // EVENT (NodeCreate) if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeCreate'])) { $thisnode = $this->pickNode($node_id); $this->triggerEvent('nodeCreate', $thisnode); } $this->_releaseLock(); return $node_id; } // }}} // {{{ createLeftNode() /** * Creates a node before a given node *
    * +-- root1
    * |
    * +-\ root2
    * | |
    * | |-- subnode2 [new]
    * | |-- subnode1 [target]
    * | |-- subnode3
    * |
    * +-- root3
    * 
* * @param int $id Target node ID * @param array $values Hash with param => value pairs of the node (see $this->params) * @param bool $returnID Tell the method to return a node id instead of an object. * ATTENTION: That the method defaults to return an object instead of the node id * has been overseen and is basically a bug. We have to keep this to maintain BC. * You will have to set $returnID to true to make it behave like the other creation methods. * This flaw will get fixed with the next major version. * @access public * @return mixed The node id or false on error */ function createLeftNode($id, $values) { if ($this->debug) { $this->_debugMessage('createLeftNode($target, $values)'); } if (PEAR::isError($lock = $this->_setLock())) { return $lock; } $this->_verifyUserValues('createLeftode()', $values); // invalid target node, bail out if (!($thisnode = $this->pickNode($id, true))) { $epr = array('createLeftNode()', $id); $this->_releaseLock(); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr); } // If the target node is a rootnode we virtually want to create a new root node if ($thisnode['rootid'] == $thisnode['id']) { $this->_releaseLock(); return $this->createRootNode($values, $id, false, NESE_MOVE_BEFORE); } $addval = array(); $parent = $this->getParent($id, true); if (isset($this->flparams['parent'])) { $addval[$this->flparams['parent']] = $parent['id']; } $sql = array(); $sql[] = sprintf('UPDATE %s SET %s=%s+1 WHERE %s=%s AND %s>=%s AND %s=%s AND %s BETWEEN %s AND %s', $this->node_table, $this->flparams['norder'], $this->flparams['norder'], $this->flparams['rootid'], $thisnode['rootid'], $this->flparams['norder'], $thisnode['norder'], $this->flparams['level'], $thisnode['level'], $this->flparams['l'], $parent['l'], $parent['r']); // Update all nodes which have dependent left and right values $sql[] = sprintf('UPDATE %s SET %s=CASE WHEN %s>=%s THEN %s+2 ELSE %s END, %s=CASE WHEN (%s>=%s OR %s>=%s) THEN %s+2 ELSE %s END WHERE %s=%s', $this->node_table, $this->flparams['l'], $this->flparams['l'], $thisnode['l'], $this->flparams['l'], $this->flparams['l'], $this->flparams['r'], $this->flparams['r'], $thisnode['r'], $this->flparams['l'], $thisnode['l'], $this->flparams['r'], $this->flparams['r'], $this->flparams['rootid'], $thisnode['rootid']); $addval[$this->flparams['norder']] = $thisnode['norder']; $addval[$this->flparams['l']] = $thisnode['l']; $addval[$this->flparams['r']] = $thisnode['l'] + 1; $addval[$this->flparams['rootid']] = $thisnode['rootid']; $addval[$this->flparams['level']] = $thisnode['level']; if (!$this->_dumbmode || !$node_id = isset($values[$this->flparams['id']])) { $node_id = $addval[$this->flparams['id']] = $this->_nextID($this->sequence_table); } else { $node_id = $values[$this->flparams['id']]; } if (!$qr = $this->_values2InsertQuery($values, $addval)) { $this->_releaseLock(); return false; } // Insert the new node $sql[] = sprintf('INSERT INTO %s (%s) VALUES (%s)', $this->node_table, implode(', ', array_keys($qr)), implode(', ', $qr)); foreach ($sql as $qry) { $res = $this->_query($qry); $this->_testFatalAbort($res, __FILE__, __LINE__); } // EVENT (NodeCreate) if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeCreate'])) { $thisnode = $this->pickNode($node_id); $this->triggerEvent('nodeCreate', $thisnode); } $this->_releaseLock(); return $node_id; } // }}} // {{{ createRightNode() /** * Creates a node after a given node *
    * +-- root1
    * |
    * +-\ root2
    * | |
    * | |-- subnode1 [target]
    * | |-- subnode2 [new]
    * | |-- subnode3
    * |
    * +-- root3
    * 
* * @param int $id Target node ID * @param array $values Hash with param => value pairs of the node (see $this->params) * @param bool $returnID Tell the method to return a node id instead of an object. * ATTENTION: That the method defaults to return an object instead of the node id * has been overseen and is basically a bug. We have to keep this to maintain BC. * You will have to set $returnID to true to make it behave like the other creation methods. * This flaw will get fixed with the next major version. * @access public * @return mixed The node id or false on error */ function createRightNode($id, $values) { if ($this->debug) { $this->_debugMessage('createRightNode($target, $values)'); } if (PEAR::isError($lock = $this->_setLock())) { return $lock; } $this->_verifyUserValues('createRootNode()', $values); // invalid target node, bail out if (!($thisnode = $this->pickNode($id, true))) { $epr = array('createRightNode()', $id); $this->_releaseLock(); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr); } // If the target node is a rootnode we virtually want to create a new root node if ($thisnode['rootid'] == $thisnode['id']) { $nid = $this->createRootNode($values, $id); $this->_releaseLock(); return $nid; } $addval = array(); $parent = $this->getParent($id, true); if (isset($this->flparams['parent'])) { $addval[$this->flparams['parent']] = $parent['id']; } $sql = array(); $sql[] = sprintf('UPDATE %s SET %s=%s+1 WHERE %s=%s AND %s>%s AND %s=%s AND %s BETWEEN %s AND %s', $this->node_table, $this->flparams['norder'], $this->flparams['norder'], $this->flparams['rootid'], $thisnode['rootid'], $this->flparams['norder'], $thisnode['norder'], $this->flparams['level'], $thisnode['level'], $this->flparams['l'], $parent['l'], $parent['r']); // Update all nodes which have dependent left and right values $sql[] = sprintf('UPDATE %s SET %s=CASE WHEN (%s>%s AND %s>%s) THEN %s+2 ELSE %s END, %s=CASE WHEN %s>%s THEN %s+2 ELSE %s END WHERE %s=%s', $this->node_table, $this->flparams['l'], $this->flparams['l'], $thisnode['l'], $this->flparams['r'], $thisnode['r'], $this->flparams['l'], $this->flparams['l'], $this->flparams['r'], $this->flparams['r'], $thisnode['r'], $this->flparams['r'], $this->flparams['r'], $this->flparams['rootid'], $thisnode['rootid']); $addval[$this->flparams['norder']] = $thisnode['norder'] + 1; $addval[$this->flparams['l']] = $thisnode['r'] + 1; $addval[$this->flparams['r']] = $thisnode['r'] + 2; $addval[$this->flparams['rootid']] = $thisnode['rootid']; $addval[$this->flparams['level']] = $thisnode['level']; if (!$this->_dumbmode || !isset($values[$this->flparams['id']])) { $node_id = $addval[$this->flparams['id']] = $this->_nextID($this->sequence_table); } else { $node_id = $values[$this->flparams['id']]; } if (!$qr = $this->_values2InsertQuery($values, $addval)) { $this->_releaseLock(); return false; } // Insert the new node $sql[] = sprintf('INSERT INTO %s (%s) VALUES (%s)', $this->node_table, implode(', ', array_keys($qr)), implode(', ', $qr)); foreach ($sql as $qry) { $res = $this->_query($qry); $this->_testFatalAbort($res, __FILE__, __LINE__); } // EVENT (NodeCreate) if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeCreate'])) { $thisnode = $this->pickNode($node_id); $this->triggerEvent('nodeCreate', $thisnode); } $this->_releaseLock(); return $node_id; } // }}} // {{{ deleteNode() /** * Deletes a node * * @param int $id ID of the node to be deleted * @access public * @return bool True if the delete succeeds */ function deleteNode($id) { if ($this->debug) { $this->_debugMessage("deleteNode($id)"); } if (PEAR::isError($lock = $this->_setLock())) { return $lock; } // invalid target node, bail out if (!($thisnode = $this->pickNode($id, true))) { $epr = array('deleteNode()', $id); $this->_releaseLock(); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr); } if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeDelete'])) { // EVENT (NodeDelete) $this->triggerEvent('nodeDelete', $this->pickNode($id)); } $parent = $this->getParent($id, true); $len = $thisnode['r'] - $thisnode['l'] + 1; $sql = array(); // Delete the node $sql[] = sprintf('DELETE FROM %s WHERE %s BETWEEN %s AND %s AND %s=%s', $this->node_table, $this->flparams['l'], $thisnode['l'], $thisnode['r'], $this->flparams['rootid'], $thisnode['rootid']); if ($thisnode['id'] != $thisnode['rootid']) { // The node isn't a rootnode so close the gap $sql[] = sprintf('UPDATE %s SET %s=CASE WHEN %s>%s THEN %s-%s ELSE %s END, %s=CASE WHEN %s>%s THEN %s-%s ELSE %s END WHERE %s=%s AND (%s>%s OR %s>%s)', $this->node_table, $this->flparams['l'], $this->flparams['l'], $thisnode['l'], $this->flparams['l'], $len, $this->flparams['l'], $this->flparams['r'], $this->flparams['r'], $thisnode['l'], $this->flparams['r'], $len, $this->flparams['r'], $this->flparams['rootid'], $thisnode['rootid'], $this->flparams['l'], $thisnode['l'], $this->flparams['r'], $thisnode['r']); // Re-order $sql[] = sprintf('UPDATE %s SET %s=%s-1 WHERE %s=%s AND %s=%s AND %s>%s AND %s BETWEEN %s AND %s', $this->node_table, $this->flparams['norder'], $this->flparams['norder'], $this->flparams['rootid'], $thisnode['rootid'], $this->flparams['level'], $thisnode['level'], $this->flparams['norder'], $thisnode['norder'], $this->flparams['l'], $parent['l'], $parent['r']); } else { // A rootnode was deleted and we only have to close the gap inside the order $sql[] = sprintf('UPDATE %s SET %s=%s-1 WHERE %s=%s AND %s > %s', $this->node_table, $this->flparams['norder'], $this->flparams['norder'], $this->flparams['rootid'], $this->flparams['id'], $this->flparams['norder'], $thisnode['norder']); } foreach ($sql as $qry) { $res = $this->_query($qry); $this->_testFatalAbort($res, __FILE__, __LINE__); } $this->_releaseLock(); return true; } // }}} // {{{ updateNode() /** * Changes the payload of a node * * @param int $id Node ID * @param array $values Hash with param => value pairs of the node (see $this->params) * @param bool $_intermal Internal use only. Used to skip value validation. Leave this as it is. * @access public * @return bool True if the update is successful */ function updateNode($id, $values, $_internal = false) { if ($this->debug) { $this->_debugMessage('updateNode($id, $values)'); } if (PEAR::isError($lock = $this->_setLock())) { return $lock; } if (!$_internal) { $this->_verifyUserValues('createRootNode()', $values); } $eparams = array('values' => $values); if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeUpdate'])) { // EVENT (NodeUpdate) $this->triggerEvent('nodeUpdate', $this->pickNode($id), $eparams); } $addvalues = array(); if (!$qr = $this->_values2UpdateQuery($values, $addvalues)) { $this->_releaseLock(); return false; } $sql = sprintf('UPDATE %s SET %s WHERE %s=%s', $this->node_table, $qr, $this->flparams['id'], $id); $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); $this->_releaseLock(); return true; } // }}} // +----------------------------------------------+ // | Moving and copying | // |----------------------------------------------+ // | [PUBLIC] | // +----------------------------------------------+ // {{{ moveTree() /** * Wrapper for node moving and copying * * @param int $id Source ID * @param int $target Target ID * @param constant $pos Position (use one of the NESE_MOVE_* constants) * @param bool $copy Shall we create a copy * @see _moveInsideLevel * @see _moveAcross * @see _moveRoot2Root * @access public * @return int ID of the moved node or false on error */ function moveTree($id, $targetid, $pos, $copy = false) { if ($this->debug) { $this->_debugMessage('moveTree($id, $target, $pos, $copy = false)'); } if (PEAR::isError($lock = $this->_setLock(true))) { return $lock; } if ($id == $targetid && !$copy) { $epr = array('moveTree()'); $this->_releaseLock(); return $this->_raiseError(NESE_ERROR_RECURSION, PEAR_ERROR_RETURN, E_USER_NOTICE, $epr); } // Get information about source and target if (!($source = $this->pickNode($id, true))) { $epr = array('moveTree()', $id); $this->_releaseLock(); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr); } if (!($target = $this->pickNode($targetid, true))) { $epr = array('moveTree()', $targetid); $this->_releaseLock(); return $this->_raiseError(NESE_ERROR_NOT_FOUND, PEAR_ERROR_TRIGGER, E_USER_ERROR, $epr); } $this->_relations = array(); // This operations don't need callbacks except the copy handler // which ignores this setting $this->_skipCallbacks = true; if (!$copy) { // We have a recursion - let's stop if (($target['rootid'] == $source['rootid']) && (($source['l'] <= $target['l']) && ($source['r'] >= $target['r']))) { $this->_releaseLock(true); $epr = array('moveTree()'); return $this->_raiseError(NESE_ERROR_RECURSION, PEAR_ERROR_RETURN, E_USER_NOTICE, $epr); } // Insert/move before or after if (($source['rootid'] == $source['id']) && ($target['rootid'] == $target['id']) && ($pos != NESE_MOVE_BELOW)) { // We have to move a rootnode which is different from moving inside a tree $nid = $this->_moveRoot2Root($source, $target, $pos); $this->_releaseLock(true); return $nid; } } elseif (($target['rootid'] == $source['rootid']) && (($source['l'] < $target['l']) && ($source['r'] > $target['r']))) { $this->_releaseLock(true); $epr = array('moveTree()'); return $this->_raiseError(NESE_ERROR_RECURSION, PEAR_ERROR_RETURN, E_USER_NOTICE, $epr); } // We have to move between different levels and maybe subtrees - let's rock ;) $moveID = $this->_moveAcross($source, $target, $pos, true); $this->_moveCleanup($copy); $this->_releaseLock(true); if (!$copy) { return $id; } else { return $moveID; } } // }}} // {{{ _moveAcross() /** * Moves nodes and trees to other subtrees or levels * *
    * [+] <--------------------------------+
    * +-[\] root1 [target]                 |
    *        <-------------------------+      |p
    * +-\ root2                     |      |
    * | |                           |      |
    * | |-- subnode1 [target]       |      |B
    * | |-- subnode2 [new]          |S     |E
    * | |-- subnode3                |U     |F
    * |                             |B     |O
    * +-\ root3                     |      |R
    *      |-- subnode 3.1             |      |E
    *      |-\ subnode 3.2 [source] >--+------+
    *        |-- subnode 3.2.1
    * 
* * @param object $ NodeCT $source Source node * @param object $ NodeCT $target Target node * @param string $pos Position [SUBnode/BEfore] * @param bool $copy Shall we create a copy * @access private * @see moveTree * @see _r_moveAcross * @see _moveCleanup */ function _moveAcross($source, $target, $pos, $first = false) { if ($this->debug) { $this->_debugMessage('_moveAcross($source, $target, $pos, $copy = false)'); } // Get the current data from a node and exclude the id params which will be changed // because of the node move $values = array(); foreach($this->params as $key => $val) { if ($source[$val] && $val != 'parent' && !in_array($val, $this->_requiredParams)) { $values[$key] = trim($source[$val]); } } switch ($pos) { case NESE_MOVE_BEFORE: $clone_id = $this->createLeftNode($target['id'], $values); break; case NESE_MOVE_AFTER: $clone_id = $this->createRightNode($target['id'], $values); break; case NESE_MOVE_BELOW: $clone_id = $this->createSubNode($target['id'], $values); break; } if ($first && isset($this->flparams['parent'])) { $t_parent = $this->getParent($clone_id, true, true, array(), false); $t_parent_id = $t_parent['id']; } elseif (isset($this->flparams['parent'])) { $t_parent_id = $source['parent']; } else { $t_parent_id = false; } $children = $this->getChildren($source['id'], true, true, true); if ($children) { $pos = NESE_MOVE_BELOW; $sclone_id = $clone_id; // Recurse through the child nodes foreach($children AS $cid => $child) { $sclone = $this->pickNode($sclone_id, true); $sclone_id = $this->_moveAcross($child, $sclone, $pos); $pos = NESE_MOVE_AFTER; } } $this->_relations[$source['id']]['clone'] = $clone_id; $this->_relations[$source['id']]['parent'] = $t_parent_id; return $clone_id; } // }}} // {{{ _moveCleanup() /** * Deletes the old subtree (node) and writes the node id's into the cloned tree * * @param array $relations Hash in der Form $h[alteid]=neueid * @param array $copy Are we in copy mode? * @access private */ function _moveCleanup($copy = false) { $relations = $this->_relations; if ($this->debug) { $this->_debugMessage('_moveCleanup($relations, $copy = false)'); } $deletes = array(); $updates = array(); $pupdates = array(); $tb = $this->node_table; $fid = $this->flparams['id']; $froot = $this->flparams['rootid']; foreach($relations AS $key => $val) { $cloneid = $val['clone']; $parentID = $val['parent']; $clone = $this->pickNode($cloneid); if ($copy) { // EVENT (NodeCopy) $eparams = array('clone' => $clone); if (!$this->_skipCallbacks && isset($this->_hasListeners['nodeCopy'])) { $this->triggerEvent('nodeCopy', $this->pickNode($key), $eparams); } continue; } // No callbacks here because the node itself doesn't get changed // Only it's position // If one needs a callback here please let me know if (!empty($parentID)) { $sql = sprintf('UPDATE %s SET %s=%s WHERE %s=%s', $this->node_table, $this->flparams['parent'], $parentID, $fid, $key); $pupdates[] = $sql; } $deletes[] = $key; // It's isn't a rootnode if ($clone->id != $clone->rootid) { $sql = sprintf('UPDATE %s SET %s=%s WHERE %s=%s', $this->node_table, $fid, $key, $fid, $cloneid); $updates[] = $sql; } else { $sql = sprintf('UPDATE %s SET %s=%s, %s=%s WHERE %s=%s', $this->node_table, $fid, $key, $froot, $cloneid, $fid, $cloneid); $updates[] = $sql; $orootid = $clone->rootid; $sql = sprintf('UPDATE %s SET %s=%s WHERE %s=%s', $tb, $froot, $key, $froot, $orootid); $updates[] = $sql; } $this->_skipCallbacks = false; } if (!empty($deletes)) { foreach ($deletes as $delete) { $this->deleteNode($delete); } } if (!empty($updates)) { for($i = 0;$i < count($updates);$i++) { $res = $this->_query($updates[$i]); $this->_testFatalAbort($res, __FILE__, __LINE__); } } if (!empty($pupdates)) { for($i = 0;$i < count($pupdates);$i++) { $res = $this->_query($pupdates[$i]); $this->_testFatalAbort($res, __FILE__, __LINE__); } } return true; } // }}} // {{{ _moveRoot2Root() /** * Moves rootnodes * *
    * +-- root1
    * |
    * +-\ root2
    * | |
    * | |-- subnode1 [target]
    * | |-- subnode2 [new]
    * | |-- subnode3
    * |
    * +-\ root3
    *     [|]  <-----------------------+
    *      |-- subnode 3.1 [target]    |
    *      |-\ subnode 3.2 [source] >--+
    *        |-- subnode 3.2.1
    * 
* * @param object $ NodeCT $source Source * @param object $ NodeCT $target Target * @param string $pos BEfore | AFter * @access private * @see moveTree */ function _moveRoot2Root($source, $target, $pos) { if ($this->debug) { $this->_debugMessage('_moveRoot2Root($source, $target, $pos, $copy)'); } if (PEAR::isError($lock = $this->_setLock())) { return $lock; } $tb = $this->node_table; $fid = $this->flparams['id']; $froot = $this->flparams['rootid']; $freh = $this->flparams['norder']; $s_order = $source['norder']; $t_order = $target['norder']; $s_id = $source['id']; $t_id = $target['id']; if ($s_order < $t_order) { if ($pos == NESE_MOVE_BEFORE) { $sql = "UPDATE $tb SET $freh=$freh-1 WHERE $freh BETWEEN $s_order AND $t_order AND $fid!=$t_id AND $fid!=$s_id AND $froot=$fid"; $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); $sql = "UPDATE $tb SET $freh=$t_order -1 WHERE $fid=$s_id"; $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); } elseif ($pos == NESE_MOVE_AFTER) { $sql = "UPDATE $tb SET $freh=$freh-1 WHERE $freh BETWEEN $s_order AND $t_order AND $fid!=$s_id AND $froot=$fid"; $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); $sql = "UPDATE $tb SET $freh=$t_order WHERE $fid=$s_id"; $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); } } if ($s_order > $t_order) { if ($pos == NESE_MOVE_BEFORE) { $sql = "UPDATE $tb SET $freh=$freh+1 WHERE $freh BETWEEN $t_order AND $s_order AND $fid != $s_id AND $froot=$fid"; $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); $sql = "UPDATE $tb SET $freh=$t_order WHERE $fid=$s_id"; $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); } elseif ($pos == NESE_MOVE_AFTER) { $sql = "UPDATE $tb SET $freh=$freh+1 WHERE $freh BETWEEN $t_order AND $s_order AND $fid!=$t_id AND $fid!=$s_id AND $froot=$fid"; $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); $sql = "UPDATE $tb SET $freh=$t_order+1 WHERE $fid = $s_id"; $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); } } $this->_releaseLock(); return $s_id; } // }}} // +-----------------------+ // | Helper methods | // +-----------------------+ // {{{ _secSort() /** * OMG ... This is nasty * * @access private */ function _secSort($nodeSet) { // Nothing to do - empty tree if(empty($nodeSet)) { return $nodeSet; } $retArray = array(); foreach($nodeSet AS $nodeID=>$node) { $deepArray[$node['parent']][$nodeID] = $node; } $reset = true; foreach($deepArray AS $parentID=>$children) { $retArray = $this->_secSortCollect($children, $deepArray, $reset); $reset = false; } return $retArray; } function _secSortCollect($segment, $deepArray, $reset = false) { static $retArray; if(!$retArray || $reset) { $retArray = array(); } foreach($segment AS $nodeID=>$node) { if(isset($retArray[$nodeID])) { continue; } $retArray[$nodeID] = $node; if(isset($deepArray[$nodeID])) { $this->_secSortCollect($deepArray[$nodeID], $deepArray); } } return $retArray; } // }}} // {{{ _addSQL() /** * Adds a specific type of SQL to a query string * * @param array $addSQL The array of SQL strings to add. Example value: * $addSQL = array( * 'cols' => 'tb2.col2, tb2.col3', // Additional tables/columns * 'join' => 'LEFT JOIN tb1 USING(STRID)', // Join statement * 'where' => 'A='B' AND C='D', // Where statement without 'WHERE' OR 'AND' in front * 'append' => 'GROUP by tb1.STRID'); // Group condition * @param string $type The type of SQL. Can be 'cols', 'join', or 'append'. * @access private * @return string The SQL, properly formatted */ function _addSQL($addSQL, $type, $prefix = false) { if (!isset($addSQL[$type])) { return ''; } switch ($type) { case 'cols': return ', ' . $addSQL[$type]; case 'where': return $prefix . ' (' . $addSQL[$type] . ')'; default: return $addSQL[$type]; } } // }}} // {{{ _getSelectFields() /** * Gets the select fields based on the params * * @param bool $aliasFields Should we alias the fields so they are the names of the * parameter keys, or leave them as is? * @access private * @return string A string of query fields to select */ function _getSelectFields($aliasFields) { $queryFields = array(); foreach ($this->params as $key => $val) { $tmp_field = $this->node_table . '.' . $key; if ($aliasFields) { $tmp_field .= ' AS ' . $this->_quoteIdentifier($val); } $queryFields[] = $tmp_field; } $fields = implode(', ', $queryFields); return $fields; } // }}} // {{{ _processResultSet() /** * Processes a DB result set by checking for a DB error and then transforming the result * into a set of DB_NestedSet_Node objects or leaving it as an array. * * @param string $sql The sql query to be done * @param bool $keepAsArray Keep the result as an array or transform it into a set of * DB_NestedSet_Node objects? * @param bool $fieldsAreAliased Are the fields aliased? * @access private * @return mixed False on error or the transformed node set. */ function _processResultSet($sql, $keepAsArray, $fieldsAreAliased) { $result = $this->_getAll($sql); if ($this->_testFatalAbort($result, __FILE__, __LINE__)) { return false; } $nodes = array(); $idKey = $fieldsAreAliased ? 'id' : $this->flparams['id']; foreach ($result as $row) { $node_id = $row[$idKey]; if ($keepAsArray) { $nodes[$node_id] = $row; } else { // Create an instance of the node container $nodes[$node_id] = & new DB_NestedSet_Node($row); } } return $nodes; } // }}} // {{{ _testFatalAbort() /** * Error Handler * * Tests if a given ressource is a PEAR error object * ans raises a fatal error in case of an error object * * @param object $ PEAR::Error $errobj The object to test * @param string $file The filename wher the error occured * @param int $line The line number of the error * @return void * @access private */ function _testFatalAbort($errobj, $file, $line) { if (!$this->_isDBError($errobj)) { return false; } if ($this->debug) { $this->_debugMessage('_testFatalAbort($errobj, $file, $line)'); } if ($this->debug) { $message = $errobj->getUserInfo(); $code = $errobj->getCode(); $msg = "$message ($code) in file $file at line $line"; } else { $msg = $errobj->getMessage(); $code = $errobj->getCode(); } PEAR::raiseError($msg, $code, PEAR_ERROR_TRIGGER, E_USER_ERROR); } // }}} // {{{ __raiseError() /** * * @access private */ function _raiseError($code, $mode, $option, $epr = array()) { $message = vsprintf($this->_getMessage($code), $epr); return PEAR::raiseError($message, $code, $mode, $option); } // }}} // {{{ addListener() /** * Add an event listener * * Adds an event listener and returns an ID for it *
Known events are
    * nodeCreate
    * nodeDelete
    * nodeUpdate
    * nodeCopy
    * nodeLoad
    * 
* @param string $event The event name * @param string $listener The listener object * @return string * @access public */ function addListener($event, & $listener) { $listenerID = uniqid('el'); $this->eventListeners[$event][$listenerID] = & $listener; $this->_hasListeners[$event] = true; return $listenerID; } // }}} // {{{ removeListener() /** * Removes an event listener * * Removes the event listener with the given ID * * @param string $event The ivent name * @param string $listenerID The listener's ID * @return bool * @access public */ function removeListener($event, $listenerID) { unset($this->eventListeners[$event][$listenerID]); if (!isset($this->eventListeners[$event]) || !is_array($this->eventListeners[$event]) || count($this->eventListeners[$event]) == 0) { unset($this->_hasListeners[$event]); } return true; } // }}} // {{{ triggerEvent() /** * Triggers and event an calls the event listeners * * @param string $event The Event that occured * @param object $ node $node A Reference to the node object which was subject to changes * @param array $eparams A associative array of params which may be needed by the handler * @return bool * @access public */ function triggerEvent($event, & $node, $eparams = false) { if ($this->_skipCallbacks || !isset($this->_hasListeners[$event])) { return false; } foreach($this->eventListeners[$event] as $key => $val) { if (!method_exists($val, 'callEvent')) { return new PEAR_Error($this->_getMessage(NESE_ERROR_NOHANDLER), NESE_ERROR_NOHANDLER); } $val->callEvent($event, $node, $eparams); } return true; } // }}} // {{{ apiVersion() function apiVersion() { return array('package:' => $this->_packagename, 'majorversion' => $this->_majorversion, 'minorversion' => $this->_minorversion, 'version' => sprintf('%s.%s', $this->_majorversion, $this->_minorversion), 'revision' => str_replace('$', '', '$Revision: 1.86 $') ); } // }}} // {{{ setAttr() /** * Sets an object attribute * * @param array $attr An associative array with attributes * @return bool * @access public */ function setAttr($attr) { static $hasSetSequence; if (!isset($hasSetSequence)) { $hasSetSequence = false; } if (!is_array($attr) || count($attr) == 0) { return false; } foreach ($attr as $key => $val) { $this->$key = $val; if ($key == 'sequence_table') { $hasSetSequence = true; } // only update sequence to reflect new table if they haven't set it manually if (!$hasSetSequence && $key == 'node_table') { $this->sequence_table = $this->node_table . '_' . $this->flparams['id']; } if ($key == 'cache' && is_object($val)) { $this->_caching = true; $GLOBALS['DB_NestedSet'] = & $this; } } return true; } // }}} // {{{ setsortMode() /** * This enables you to set specific options for each output method * * @param constant $sortMode * @access public * @return Current sortMode */ function setsortMode($sortMode = false) { if ($sortMode && in_array($sortMode, $this->_sortModes)) { $this->_sortMode = $sortMode; } else { return $this->_sortMode; } return $this->_sortMode; } // }}} // {{{ setDbOption() /** * Sets a db option. Example, setting the sequence table format * * @var string $option The option to set * @var string $val The value of the option * @access public * @return void */ function setDbOption($option, $val) { $this->db->setOption($option, $val); } // }}} // {{{ testLock() /** * Tests if a database lock is set * * @access public */ function testLock() { if ($this->debug) { $this->_debugMessage('testLock()'); } if ($lockID = $this->_structureTableLock) { return $lockID; } $this->_lockGC(); $sql = sprintf('SELECT lockID FROM %s WHERE lockTable=%s', $this->lock_table, $this->_quote($this->node_table)) ; $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); if ($this->_numRows($res)) { return new PEAR_Error($this->_getMessage(NESE_ERROR_TBLOCKED), NESE_ERROR_TBLOCKED); } return false; } // }}} // {{{ _setLock() /** * * @access private */ function _setLock($exclusive = false) { $lock = $this->testLock(); if (PEAR::isError($lock)) { return $lock; } if ($this->debug) { $this->_debugMessage('_setLock()'); } if ($this->_caching) { @$this->cache->flush('function_cache'); $this->_caching = false; $this->_restcache = true; } if (!$lockID = $this->_structureTableLock) { $lockID = $this->_structureTableLock = uniqid('lck-'); $sql = sprintf('INSERT INTO %s (lockID, lockTable, lockStamp) VALUES (%s, %s, %s)', $this->lock_table, $this->_quote($lockID), $this->_quote($this->node_table), time()); } else { $sql = sprintf('UPDATE %s SET lockStamp=%s WHERE lockID=%s AND lockTable=%s', $this->lock_table, time(), $this->_quote($lockID), $this->_quote($this->node_table)); } if ($exclusive) { $this->_lockExclusive = true; } $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); return $lockID; } // }}} // {{{ _releaseLock() /** * * @access private */ function _releaseLock($exclusive = false) { if ($this->debug) { $this->_debugMessage('_releaseLock()'); } if ($exclusive) { $this->_lockExclusive = false; } if ((!$lockID = $this->_structureTableLock) || $this->_lockExclusive) { return false; } $tb = $this->lock_table; $stb = $this->node_table; $sql = "DELETE FROM $tb WHERE lockTable=" . $this->_quote($stb) . " AND lockID=" . $this->_quote($lockID); $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); $this->_structureTableLock = false; if ($this->_restcache) { $this->_caching = true; $this->_restcache = false; } return true; } // }}} // {{{ _lockGC() /** * * @access private */ function _lockGC() { if ($this->debug) { $this->_debugMessage('_lockGC()'); } $tb = $this->lock_table; $stb = $this->node_table; $lockTTL = time() - $this->lockTTL; $sql = "DELETE FROM $tb WHERE lockTable=" . $this->_quote($stb) . " AND lockStamp < $lockTTL"; $res = $this->_query($sql); $this->_testFatalAbort($res, __FILE__, __LINE__); } // }}} // {{{ _values2UpdateQuery() /** * * @access private */ function _values2UpdateQuery($values, $addval = false) { if ($this->debug) { $this->_debugMessage('_values2UpdateQuery($values, $addval = false)'); } if (is_array($addval)) { $values = $values + $addval; } $arq = array(); foreach($values AS $key => $val) { $k = $this->_quoteIdentifier(trim($key)); // To be used with the next major version // $iv = in_array($this->params[$k], $this->_quotedParams) ? $this->_quote($v) : $v; $iv = $this->_quote(trim($val)); $arq[] = "$k=$iv"; } if (!is_array($arq) || count($arq) == 0) { return false; } $query = implode(', ', $arq); return $query; } // }}} // {{{ _values2UpdateQuery() /** * * @access private */ function _values2InsertQuery($values, $addval = false) { if ($this->debug) { $this->_debugMessage('_values2InsertQuery($values, $addval = false)'); } if (is_array($addval)) { $values = $values + $addval; } $arq = array(); foreach($values AS $key => $val) { $k = $this->_quoteIdentifier(trim($key)); // To be used with the next major version // $iv = in_array($this->params[$k], $this->_quotedParams) ? $this->_quote($v) : $v; $iv = $this->_quote(trim($val)); $arq[$k] = $iv; } if (!is_array($arq) || count($arq) == 0) { return false; } return $arq; } // }}} // {{{ _verifyUserValues() /** * Clean values from protected or unknown columns * * @var string $caller The calling method * @var string $values The values array * @access private * @return void */ function _verifyUserValues($caller, & $values) { if ($this->_dumbmode) { return true; } foreach($values as $field => $value) { if (!isset($this->params[$field])) { $epr = array($caller, sprintf('Unknown column/param \'%s\'', $field)); $this->_raiseError(NESE_ERROR_WRONG_MPARAM, PEAR_ERROR_RETURN, E_USER_NOTICE, $epr); unset($values[$field]); } else { $flip = $this->params[$field]; if (in_array($flip, $this->_requiredParams)) { $epr = array($caller, sprintf('\'%s\' is autogenerated and can\'t be passed - it will be ignored', $field)); $this->_raiseError(NESE_ERROR_WRONG_MPARAM, PEAR_ERROR_RETURN, E_USER_NOTICE, $epr); unset($values[$field]); } } } } // }}} // {{{ _debugMessage() /** * * @access private */ function _debugMessage($msg) { if ($this->debug) { list($usec, $sec) = explode(' ', microtime()); $time = ((float)$usec + (float)$sec); echo "$time::Debug:: $msg
\n"; } } // }}} // {{{ _getMessage() /** * * @access private */ function _getMessage($code) { if ($this->debug) { $this->_debugMessage('_getMessage($code)'); } return isset($this->messages[$code]) ? $this->messages[$code] : $this->messages[NESE_MESSAGE_UNKNOWN]; } // }}} // {{{ convertTreeModel() /** * Convert a <1.3 tree into a 1.3 tree format * * This will convert the tree into a format needed for some new features in * 1.3. Your <1.3 tree will still work without converting but some new features * like preorder sorting won't work as expected. * *
    * Usage:
    * - Create a new node table (tb_nodes2) from the current node table (tb_nodes1) (only copy the structure).
    * - Create a nested set instance of the 'old' set (NeSe1) and one of the new set (NeSe2)
    * - Now you have 2 identical objects where only node_table differs
    * - Call DB_NestedSet::convertTreeModel(&$orig, &$copy);
    * - After that you have a cleaned up copy of tb_nodes1 inside tb_nodes2
    * 
* * @param object $ DB_NestedSet $orig Nested set we want to copy * @param object $ DB_NestedSet $copy Object where the new tree is copied to * @param integer $_parent ID of the parent node (private) * @static * @access public * @return bool True uns success */ function convertTreeModel(& $orig, & $copy, $_parent = false) { static $firstSet; $isRoot = false; if (!$_parent) { if (!is_object($orig) || !is_object($copy)) { return false; } if ($orig->node_table == $copy->node_table) { return false; } $copy->_dumbmode = true; $orig->sortMode = NESE_SORT_LEVEL; $copy->sortMode = NESE_SORT_LEVEL; $sibl = $orig->getRootNodes(true); $isRoot = true; } else { $sibl = $orig->getChildren($_parent, true); } if (empty($sibl)) { return false; } foreach($sibl AS $sid => $sibling) { unset($sibling['l']); unset($sibling['r']); unset($sibling['norder']); $values = array(); foreach($sibling AS $key => $val) { if (!isset($copy->flparams[$key])) { continue; } $values[$copy->flparams[$key]] = $val; } if (!$firstSet) { $psid = $copy->createRootNode($values, false, true); $firstSet = true; } elseif ($isRoot) { $psid = $copy->createRightNode($psid, $values); } else { $copy->createSubNode($_parent, $values); } DB_NestedSet::convertTreeModel($orig, $copy, $sid); } return true; } // }}} } // {{{ DB_NestedSet_Node:: class /** * Generic class for node objects * * @autor Daniel Khan ; * @version $Revision: 1.86 $ * @package DB_NestedSet * @access private */ class DB_NestedSet_Node { // {{{ constructor /** * Constructor */ function DB_NestedSet_Node($data) { if (!is_array($data) || count($data) == 0) { return new PEAR_ERROR($data, NESE_ERROR_PARAM_MISSING); } $this->setAttr($data); return true; } // }}} // {{{ setAttr() function setAttr($data) { if (!is_array($data) || count($data) == 0) { return false; } foreach ($data as $key => $val) { $this->$key = $val; } } // }}} } // }}} ?>