TimeTrex/classes/modules/core/FastTree.class.php

1077 lines
29 KiB
PHP
Raw Normal View History

2022-12-13 07:10:06 +01:00
<?php
/*********************************************************************************
*
* TimeTrex is a Workforce Management program developed by
* TimeTrex Software Inc. Copyright (C) 2003 - 2021 TimeTrex Software Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY TIMETREX, TIMETREX DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
*
* You should have received a copy of the GNU Affero General Public License along
* with this program; if not, see http://www.gnu.org/licenses or write to the Free
* Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA.
*
*
* You can contact TimeTrex headquarters at Unit 22 - 2475 Dobbin Rd. Suite
* #292 West Kelowna, BC V4T 2E9, Canada or at email address info@timetrex.com.
*
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by TimeTrex" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by TimeTrex".
*
********************************************************************************/
/*
-- id serial NOT NULL,
CREATE TABLE hierarchy_tree (
tree_id integer DEFAULT 0 NOT NULL,
parent_id integer DEFAULT 0 NOT NULL,
object_id integer DEFAULT 0 NOT NULL,
left_id bigint DEFAULT 0 NOT NULL,
right_id bigint DEFAULT 0 NOT NULL
) WITHOUT OIDS;
--Order of the columsn in the below index seem to matter
create index "hierarchy_tree_left_id_right_id" on hierarchy_tree(left_id, right_id);
create index "hierarchy_tree_tree_id_object_id" on hierarchy_tree(tree_id, object_id);
create index "hierarchy_tree_tree_id_parent_id" on hierarchy_tree(tree_id, parent_id);
*/
/**
* @package Core
*/
class FastTree {
var $db = null;
var $table = 'fast_tree';
var $tree_id = 0;
var $spacer = 0;
/**
* FastTree constructor.
* @param null $options
*/
function __construct( $options = null ) {
//Debug::Text(' Contruct... ', __FILE__, __LINE__, __METHOD__, 10);
$this->db = $options['db'];
//Debug::Text(' Setting DB to: '. $options['db'], __FILE__, __LINE__, __METHOD__, 10);
$this->table = $options['table'];
//Debug::Text(' Setting Table to: '. $options['table'], __FILE__, __LINE__, __METHOD__, 10);
if ( isset( $options['tree_id'] ) ) {
$this->setTree( $options['tree_id'] );
//$this->tree_id = $options['tree_id'];
//Debug::Text(' Setting Tree ID to: '. $options['tree_id'], __FILE__, __LINE__, __METHOD__, 10);
}
return true;
}
/**
* @return int
*/
function getTree() {
return $this->tree_id;
}
/**
* @param string $id UUID
* @return bool
*/
function setTree( $id ) {
if ( $id != '' ) {
//Debug::Text(' Setting Tree ID to: '. $id, __FILE__, __LINE__, __METHOD__, 10);
$this->tree_id = $id;
$this->_setupTree();
return true;
}
return false;
}
/**
* @return bool
*/
function _setupTree() {
//Add the root node if its missing.
$node_data = $this->getNode( 0 );
if ( $node_data === false ) {
Debug::Text( ' Initiating Tree with Root object: ', __FILE__, __LINE__, __METHOD__, 10 );
$this->add( 0, -1 );
return true;
}
//Debug::Text(' NOT Initiating Tree with Root object: ', __FILE__, __LINE__, __METHOD__, 10);
return false;
}
/**
* @return mixed
*/
function getRootId() {
$ph = [
'tree_id' => $this->tree_id,
];
// get all children of this node
$query = 'SELECT object_id FROM ' . $this->table . ' WHERE tree_id = ? AND parent_id = -1';
$root_id = $this->db->GetOne( $query, $ph );
return $root_id;
}
/**
* @param string $object_id UUID
* @return bool
*/
function getNode( $object_id ) {
//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);
//Check to make sure object_id doesn't exceed 32bit integer.
if ( !is_numeric( $object_id ) || ( $object_id > 2147483647 || $object_id < -2147483648 ) ) {
Debug::Text( ' aReturning False', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
$ph = [
'tree_id' => (int)$this->getTree(),
'object_id' => (int)$object_id,
];
// get all children of this node
$query = ' SELECT a.object_id, a.parent_id, a.left_id, a.right_id, count(b.object_id)-1 as level
FROM ' . $this->table . ' a
LEFT JOIN ' . $this->table . ' b ON a.tree_id = b.tree_id AND a.left_id BETWEEN b.left_id AND b.right_id
WHERE a.tree_id = ?
AND a.object_id = ?
GROUP BY a.object_id, a.left_id, a.object_id, a.parent_id, a.right_id
';
$data = $this->db->GetRow( $query, $ph );
if ( count( $data ) == 0 ) {
return false;
}
return $data;
}
/**
* @param string $object_id UUID
* @return bool
*/
function getLevel( $object_id ) {
//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);
$data = $this->getNode( $object_id );
if ( $data === false ) {
return false;
}
return $data['level'];
}
/**
* @param string $object_id UUID
* @return bool
*/
function getRightId( $object_id ) {
//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);
$data = $this->getNode( $object_id );
if ( $data === false ) {
return false;
}
return $data['right_id'];
}
/**
* @param string $object_id UUID
* @return bool
*/
function getLeftId( $object_id ) {
//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);
$data = $this->getNode( $object_id );
if ( $data === false ) {
return false;
}
return $data['left_id'];
}
/**
* @param string $object_id UUID
* @return bool
*/
function getParentId( $object_id ) {
//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);
$data = $this->getNode( $object_id );
if ( $data === false ) {
return false;
}
return $data['parent_id'];
}
/**
* @param bool $object_id
* @return bool
*/
function rebuildTree( $object_id = false ) {
Debug::Text( ' Object ID: ' . $object_id, __FILE__, __LINE__, __METHOD__, 10 );
$this->db->BeginTrans();
$this->db->SetTransactionMode( 'SERIALIZABLE' ); //Serialize rebuild tree transactions so concurrency issues don't corrupt the tree.
if ( $object_id === false ) {
Debug::Text( ' Object ID not specified, using root: ', __FILE__, __LINE__, __METHOD__, 10 );
$object_id = $this->getRootId();
$left_id = 1;
} else {
Debug::Text( ' Object ID specified: ', __FILE__, __LINE__, __METHOD__, 10 );
$left_id = $this->getLeftId( $object_id );
}
if ( $left_id === false ) {
Debug::Text( ' Error getting left id: ', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
Debug::Text( ' aObject ID: ' . $object_id . ' - Left ID: ' . $left_id, __FILE__, __LINE__, __METHOD__, 10 );
$rebuilt = $this->_rebuildTree( $object_id, $left_id );
if ( $rebuilt === false ) {
Debug::Text( ' Error rebuilding tree: ', __FILE__, __LINE__, __METHOD__, 10 );
$this->db->RollBackTrans();
return false;
}
//$this->db->RollBackTrans();
$this->db->CommitTrans();
$this->db->SetTransactionMode( '' ); //Restore default transaction mode.
Debug::Text( ' Tree Rebuilt: ', __FILE__, __LINE__, __METHOD__, 10 );
return true;
}
/**
* @param string $object_id UUID
* @param string $left_id UUID
* @return bool
*/
function _rebuildTree( $object_id, $left_id ) {
Debug::Text( ' Object ID: ' . $object_id . ' - Left: ' . $left_id, __FILE__, __LINE__, __METHOD__, 10 );
$ph = [
'tree_id' => (int)$this->getTree(),
'parent_id' => (int)$object_id,
];
// get all children of this node
$query = 'SELECT object_id FROM ' . $this->table . ' WHERE tree_id = ? AND parent_id = ?';
$rs = $this->db->Execute( $query, $ph );
if ( !is_object( $rs ) ) {
Debug::Text( ' Select failed', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
// the right value of this node is the left value + 1 (or more)
$right_id = ( $left_id + 10 );
while ( $row = $rs->FetchRow() ) {
// recursive execution of this function for each
// child of this node
// $right is the current right value, which is
// incremented by the rebuild_tree function
Debug::Text( ' Right ID: ' . $right_id, __FILE__, __LINE__, __METHOD__, 10 );
$right_id = $this->_rebuildTree( $row['object_id'], $right_id );
if ( $right_id === false ) {
Debug::Text( ' Right was false: ', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
}
$ph = [
'left_id' => (int)$left_id,
'right_id' => (int)$right_id,
'tree_id' => (int)$this->getTree(),
'object_id' => (int)$object_id,
];
// we've got the left value, and now that we've processed
// the children of this node we also know the right value
$query = 'UPDATE ' . $this->table . ' SET left_id = ?, right_id = ? WHERE tree_id = ? AND object_id = ?';
$rs = $this->db->Execute( $query, $ph );
//Use this to help debug concurrency issues.
//usleep(100000);
if ( !is_object( $rs ) ) {
Debug::Text( ' Rebuild Failed... ', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
// return the right value of this node + 1
return ( $right_id + 1 );
}
/**
* @param string $object_id UUID
* @return bool
*/
function getAllParents( $object_id ) {
//Debug::Text(' Object ID: '. $object_id, __FILE__, __LINE__, __METHOD__, 10);
if ( $object_id === '' ) {
Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
$ph = [
'tree_id' => (int)$this->getTree(),
'object_id' => (int)$object_id,
'object_id2' => (int)$object_id,
];
$query = '
SELECT b.object_id
FROM ' . $this->table . ' as a
LEFT JOIN ' . $this->table . ' as b ON a.tree_id = b.tree_id AND a.left_id BETWEEN b.left_id AND b.right_id
WHERE a.tree_id = ?
AND a.object_id = ?
AND b.object_id != 0
AND b.object_id != ?
ORDER BY b.left_id desc
';
return $this->db->GetCol( $query, $ph );
}
/**
* @param string $object_id UUID
* @return bool
*/
function getChild( $object_id ) {
if ( !is_numeric( $object_id ) ) {
Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
$ph = [
'tree_id' => (int)$this->getTree(),
'object_id' => (int)$object_id,
];
Debug::Text( ' Getting Last Child of: ' . $object_id, __FILE__, __LINE__, __METHOD__, 10 );
//Order by last child first.
//GetOne() automatically sets LIMIT 1;
$query = 'SELECT object_id FROM ' . $this->table . ' WHERE tree_id = ? AND parent_id = ? ORDER BY left_id desc';
$child_id = $this->db->GetOne( $query, $ph );
//var_dump($child_id);
return $child_id;
}
/**
* @param string|bool $object_id UUID
* @param bool $recurse
* @param int $data_format
* @return array|bool
*/
function getAllChildren( $object_id = null, $recurse = false, $data_format = 0 ) {
$original_object_id = $object_id;
//Debug::Text(' Object ID: '. $object_id .' Recurse: '. $recurse, __FILE__, __LINE__, __METHOD__, 10);
if ( $object_id === '' ) {
Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
if ( $object_id === null || $object_id === false ) {
$object_id = $this->getRootId();
Debug::Text( ' Getting Root ID: ' . $object_id, __FILE__, __LINE__, __METHOD__, 10 );
}
$node_data = $this->getNode( $object_id );
if ( $node_data === false ) {
Debug::Text( ' Getting node data of object id failed.', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
//Debug::Text(' Left ID: '. $node_data['left_id'] .' Level: '. $node_data['level'], __FILE__, __LINE__, __METHOD__, 10);
$query = '
SELECT a.object_id, a.parent_id, count(b.object_id) as level
FROM ' . $this->table . ' a
LEFT JOIN ' . $this->table . ' b ON a.tree_id = b.tree_id AND a.left_id BETWEEN b.left_id AND b.right_id
';
switch ( strtoupper( $recurse ) ) {
case 'RECURSE':
$ph = [
'tree_id' => $this->getTree(),
'left_id' => $node_data['left_id'],
'right_id' => $node_data['right_id'],
];
//Don't use >= <= (use > < ) - instead to not include the parent object.
//Make sure current node is not included in the result as well. Otherwise we are saying the current node
//is a child of itself.
$query .= '
WHERE a.tree_id = ?
AND b.left_id > ?
AND b.right_id <= ?';
//Exclude the parnet, but only when the passed object is forsure NULL!
if ( $original_object_id === null || $original_object_id === false ) {
$ph['object_id'] = $object_id;
$query .= '
AND a.object_id != ?
';
}
break;
default:
$ph = [
'tree_id' => (int)$this->getTree(),
'object_id' => (int)$object_id,
];
$query .= '
WHERE a.tree_id = ?
AND a.parent_id = ?';
}
$query .= '
GROUP BY a.object_id, a.parent_id, a.left_id
ORDER BY a.left_id';
$rs = $this->db->Execute( $query, $ph );
$retarr = [];
while ( $row = $rs->FetchRow() ) {
if ( $data_format == 1 ) {
$retarr[$row['object_id']] = $row;
} else {
$retarr[$row['object_id']] = $row['level'];
}
}
if ( empty( $retarr ) == false ) {
//Debug::Arr( $retarr, ' Children: ', __FILE__, __LINE__, __METHOD__, 10);
return $retarr;
}
return false;
}
/**
* @param string $parent_id UUID
* @return array|bool
*/
function _getLeftAndRightIds( $parent_id ) {
Debug::Text( ' getLeftAndRightIds: ' . $parent_id, __FILE__, __LINE__, __METHOD__, 10 );
$node_data = $this->getNode( $parent_id );
$parent_left = $node_data['left_id'];
$parent_right = $node_data['right_id'];
$child_id = $this->getChild( $parent_id );
if ( $child_id !== false ) {
Debug::Text( ' Child found, getting Child data: ' . $child_id, __FILE__, __LINE__, __METHOD__, 10 );
$child_node_data = $this->getNode( $child_id );
$child_left_id = $child_node_data['left_id'];
$child_right_id = $child_node_data['right_id'];
unset( $child_node_data );
Debug::Text( ' Child Left ID: ' . $child_left_id, __FILE__, __LINE__, __METHOD__, 10 );
Debug::Text( ' Child Right ID: ' . $child_right_id, __FILE__, __LINE__, __METHOD__, 10 );
Debug::Text( ' Parent Right ID: ' . $parent_right, __FILE__, __LINE__, __METHOD__, 10 );
$left_id = ( $child_right_id + 1 );
$right_id = ( $child_right_id + 10 );
if ( $right_id >= $parent_right
|| $left_id >= $parent_right ) {
Debug::Text( ' NO CHILD GAP LEFT: ', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
} else {
//Nothing yet.
//Try to keep a large gap for these.
$left_id = ( $parent_left + 1 );
$right_id = ( $parent_right - 1 );
if ( $right_id >= $parent_right
|| $left_id >= $parent_right ) {
Debug::Text( ' NO PARENT GAP LEFT: ', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
}
Debug::Text( ' Next Left ID: ' . $left_id, __FILE__, __LINE__, __METHOD__, 10 );
Debug::Text( ' Next Right ID: ' . $right_id, __FILE__, __LINE__, __METHOD__, 10 );
return [ 'left_id' => (int)$left_id, 'right_id' => (int)$right_id ];
}
/**
* @param string $parent_id UUID
* @return bool
*/
function insertGaps( $parent_id ) {
$this->spacer++;
Debug::Text( ' Attempting to insert gaps: ' . $this->spacer, __FILE__, __LINE__, __METHOD__, 10 );
$node_data = $this->getNode( $parent_id );
if ( $node_data != false ) {
Debug::Text( ' Inserting gaps: ' . $this->spacer, __FILE__, __LINE__, __METHOD__, 10 );
$ph = [
'tree_id' => $this->getTree(),
'right_id' => $node_data['right_id'],
];
$query = 'UPDATE ' . $this->table . ' SET right_id = right_id + 1000 WHERE tree_id = ? AND right_id >= ?';
$this->db->Execute( $query, $ph );
$query = 'UPDATE ' . $this->table . ' SET left_id = left_id + 1000 WHERE tree_id = ? AND left_id > ?';
$this->db->Execute( $query, $ph );
return true;
}
Debug::Text( ' Node Data Null: ' . $this->spacer, __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
/**
* MPTT + Gap add function.
* @param string $object_id UUID
* @param int $parent_id
* @return bool
*/
function add( $object_id, $parent_id = 0 ) {
Debug::Text( ' Object ID: ' . $object_id . ' Parent ID: ' . $parent_id, __FILE__, __LINE__, __METHOD__, 10 );
if ( !is_numeric( $object_id ) ) {
Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
/*
if ( $object_id == $parent_id ) {
Debug::Text(' bReturning False...', __FILE__, __LINE__, __METHOD__, 10);
return FALSE;
}
*/
//$insert_id = $this->db->GenID( $this->table.'_id_seq', 10);
//Make sure object doesn't exist already
if ( $this->getNode( $object_id ) !== false ) {
Debug::Text( ' cReturning False...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
$this->db->BeginTrans();
if ( $parent_id == -1 ) {
Debug::Text( ' Parent is 0', __FILE__, __LINE__, __METHOD__, 10 );
$ph = [
'tree_id' => $this->getTree(),
];
$query = 'SELECT object_id FROM ' . $this->table . ' WHERE tree_id = ? AND parent_id = -1';
$rs = $this->db->Execute( $query, $ph );
if ( !is_object( $rs ) ) {
Debug::Text( ' Select failed', __FILE__, __LINE__, __METHOD__, 10 );
$this->db->RollBackTrans();
return false;
}
if ( $rs->RowCount() > 0 ) {
Debug::Text( ' A root node already exists', __FILE__, __LINE__, __METHOD__, 10 );
$this->db->RollBackTrans();
return false;
}
$left_id = 0;
//Get max right_id, just incase other nodes exist in the tree.
$ph = [
'tree_id' => $this->getTree(),
];
$query = 'SELECT max(right_id) as right_id FROM ' . $this->table . ' WHERE tree_id = ?';
$right_id = ( $this->db->GetOne( $query, $ph ) + 1000 );
} else {
Debug::Text( ' Parent IS NOT 0', __FILE__, __LINE__, __METHOD__, 10 );
$left_and_right_ids = $this->_getLeftAndRightIds( $parent_id );
if ( $left_and_right_ids === false ) {
$this->insertGaps( $parent_id );
$left_and_right_ids = $this->_getLeftAndRightIds( $parent_id );
}
$left_id = $left_and_right_ids['left_id'];
$right_id = $left_and_right_ids['right_id'];
}
if ( is_numeric( $this->getTree() )
&& is_numeric( $parent_id )
&& is_numeric( $object_id )
&& is_numeric( $left_id )
&& is_numeric( $right_id ) ) {
$ph = [
'tree_id' => (int)$this->getTree(),
'parent_id' => (int)$parent_id,
'object_id' => (int)$object_id,
'left_id' => (int)$left_id,
'right_id' => (int)$right_id,
];
Debug::Text( ' Inserting Node... Left ID: ' . $left_id . ' Right ID: ' . $right_id, __FILE__, __LINE__, __METHOD__, 10 );
$query = 'INSERT INTO ' . $this->table . ' (tree_id, parent_id, object_id, left_id, right_id) VALUES (?, ?, ?, ?, ?)';
$rs = $this->db->Execute( $query, $ph );
if ( !is_object( $rs ) ) {
Debug::Text( ' Error inserting node', __FILE__, __LINE__, __METHOD__, 10 );
$this->db->RollBackTrans();
return false;
}
$this->db->CommitTrans();
Debug::Text( ' Returning True.', __FILE__, __LINE__, __METHOD__, 10 );
return true;
}
return false;
}
/**
* @param $array
* @param null $ph
* @return string
*/
protected function getListSQL( $array, &$ph = null ) {
//Debug::Arr($ph, 'Place Holder BEFORE:', __FILE__, __LINE__, __METHOD__, 10);
//Append $array values to end of $ph, return
//one "?, " for each element in $array.
$ph_arr = [];
$array_count = count( $array );
if ( is_array( $array ) && $array_count > 0 ) {
foreach ( $array as $val ) {
$ph_arr[] = '?';
$ph[] = $val;
}
if ( empty( $ph_arr ) == false ) {
$retval = implode( ',', $ph_arr );
}
} else if ( is_array( $array ) ) {
//Return NULL, because this is an empty array.
//This may have to return -1 instead of NULL
//$ph[] = 'NULL';
$ph[] = -1;
$retval = '?';
} else if ( $array == '' ) {
//$ph[] = 'NULL';
$ph[] = -1;
$retval = '?';
} else {
$ph[] = $array;
$retval = '?';
}
//Debug::Arr($ph, 'Place Holder AFTER:', __FILE__, __LINE__, __METHOD__, 10);
//Just a single ID, return it.
return $retval;
}
/**
* @param string $object_id UUID
* @param bool $recurse
* @return bool
*/
function delete( $object_id, $recurse = false ) {
Debug::Text( ' Deleting Object: ' . $object_id, __FILE__, __LINE__, __METHOD__, 10 );
if ( $object_id == '' ) {
Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
//Find out if this node has children
$this->db->BeginTrans();
//This was the source of a bug that was causing the below recurse delete query
//to delete the root node of the tree. getAllChildren was returning FALSE and array_keys()
//was turning that into array(0 => 0), so we were deleting node 0 and XXX in a single operation.
$children_ids = $this->getAllChildren( $object_id, 'RECURSE' );
if ( $children_ids !== false && is_array( $children_ids ) ) {
$children_ids = array_keys( $children_ids );
} else {
$children_ids = [];
}
if ( count( $children_ids ) == 0 ) {
Debug::Text( ' No Children: ', __FILE__, __LINE__, __METHOD__, 10 );
$ph = [
'tree_id' => (int)$this->getTree(),
'object_id' => (int)$object_id,
];
$query = 'DELETE FROM ' . $this->table . ' WHERE tree_id = ? AND object_id = ?';
$this->db->Execute( $query, $ph );
} else if ( strtolower( $recurse ) == 'recurse' ) {
Debug::Arr( $children_ids, ' Recursing Delete - Current Object: ' . $object_id . ' Child IDs: ', __FILE__, __LINE__, __METHOD__, 10 );
$ph = [
'tree_id' => $this->getTree(),
];
//Add current object_id to children for delete.
$children_ids[] = $object_id;
$query = 'DELETE FROM ' . $this->table . ' WHERE tree_id = ? AND object_id in (' . $this->getListSQL( $children_ids, $ph ) . ')';
$this->db->Execute( $query, $ph );
} else {
Debug::Text( ' Re-parenting children: ', __FILE__, __LINE__, __METHOD__, 10 );
$parent_id = $this->getParentId( $object_id );
$ph = [
'tree_id' => (int)$this->getTree(),
'object_id' => (int)$object_id,
];
$query = 'DELETE FROM ' . $this->table . ' WHERE tree_id = ? AND object_id = ?';
$this->db->Execute( $query, $ph );
$ph = [
'parent_id' => (int)$parent_id,
'tree_id' => (int)$this->getTree(),
'object_id' => (int)$object_id,
];
$query = ' UPDATE ' . $this->table . '
SET parent_id = ?
WHERE tree_id = ?
AND parent_id = ?';
$this->db->Execute( $query, $ph );
}
$this->db->CommitTrans();
return true;
}
/**
* @param string $object_id UUID
* @param string $parent_id UUID
* @return bool
*/
function move( $object_id, $parent_id ) {
Debug::Text( ' Object ID: ' . $object_id . ' Parent ID: ' . $parent_id, __FILE__, __LINE__, __METHOD__, 10 );
if ( $object_id === '' ) {
Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
if ( $parent_id === '' ) {
Debug::Text( ' bReturning False...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
//Make sure we don't reparent to self.
$children_ids = array_keys( (array)$this->getAllChildren( $object_id, 'RECURSE' ) );
if ( $parent_id != TTUUID::getZeroID() && is_array( $children_ids ) && in_array( $parent_id, $children_ids ) == true ) {
Debug::Text( ' Objects cant be re-parented to their own children...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
$this->db->BeginTrans();
$ph = [
'parent_id' => (int)$parent_id,
'tree_id' => (int)$this->getTree(),
'object_id' => (int)$object_id,
];
$query = ' UPDATE ' . $this->table . '
SET parent_id = ?
WHERE tree_id = ?
AND object_id = ?';
$this->db->Execute( $query, $ph );
//FIXME: rebuild tree starting from object_id and parent_id only perhaps?
//Might cut down on some work.
$this->rebuildTree();
$this->db->CommitTrans();
return true;
}
/**
* @param string $object_id UUID
* @param string $new_object_id UUID
* @return bool
*/
function edit( $object_id, $new_object_id ) {
Debug::Text( ' Object ID: ' . $object_id . ' New Object ID: ' . $new_object_id, __FILE__, __LINE__, __METHOD__, 10 );
if ( $object_id == '' ) {
Debug::Text( ' aReturning False...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
if ( $new_object_id == '' ) {
Debug::Text( ' bReturning False...', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
if ( $object_id == $new_object_id ) {
Debug::Text( ' Object is the same ', __FILE__, __LINE__, __METHOD__, 10 );
return true;
}
//Make sure new_object_id isn't already in the tree.
if ( $this->getNode( $new_object_id ) === false ) {
Debug::Text( ' Editing object ', __FILE__, __LINE__, __METHOD__, 10 );
$this->db->BeginTrans();
$ph = [
'new_object_id' => (int)$new_object_id,
'tree_id' => (int)$this->getTree(),
'object_id' => (int)$object_id,
];
//Update parent IDs
$query = ' UPDATE ' . $this->table . '
SET parent_id = ?
WHERE tree_id = ?
AND parent_id = ?';
$this->db->Execute( $query, $ph );
//Update object ID
$query = ' UPDATE ' . $this->table . '
SET object_id = ?
WHERE tree_id = ?
AND object_id = ?';
$this->db->Execute( $query, $ph );
$this->db->CommitTrans();
return true;
} else {
Debug::Text( ' New Object ID is already in the tree', __FILE__, __LINE__, __METHOD__, 10 );
return false;
}
}
/**
* Flex requires that all index keys start at 0, even in the children section,
* So we need to handle that as well so Flex doesn't need any post processing.
* @param $nodes
* @param bool $include_root
* @return array
*/
static function FormatFlexArray( $nodes, $include_root = true ) {
Debug::Text( ' Formatting Flex Array...', __FILE__, __LINE__, __METHOD__, 10 );
$nested = [];
$depths = [];
if ( is_array( $nodes ) ) {
foreach ( $nodes as $node ) {
if ( $node['level'] == 1 ) {
//Using sequential keys
$nested[] = $node; //Each new branch of the tree the key should start at 0 and be a sequence without holes.
end( $nested );
$depths[( $node['level'] + 1 )] = key( $nested );
/*
//Using non-sequential keys:
$nested[$key] = $node;
$depths[$node['level'] + 1] = $key;
*/
} else {
$parent =& $nested;
for ( $i = 2; $i <= $node['level']; $i++ ) {
//In cases where parent nodes were deleted without reparenting, prevent PHP warning.
if ( !isset( $depths[$i] ) ) {
$depths[$i] = 0;
}
if ( $i == 2 ) {
$parent =& $parent[$depths[$i]];
} else {
$parent =& $parent['children'][$depths[$i]];
}
}
//Using sequential keys.
$parent['children'][] = $node; //Each new branch of the tree the key should start at 0 and be a sequence without holes.
end( $parent['children'] );
$depths[( $node['level'] + 1 )] = key( $parent['children'] );
/*
//Using non-sequential keys:
$parent['children'][$key] = $node;
$depths[$node['level'] + 1] = $key;
*/
}
}
}
if ( $include_root == true ) {
return [
0 => [
'id' => 0,
'name' => TTi18n::getText( 'Root' ),
'level' => 0,
'children' => $nested,
],
];
} else {
return $nested;
}
}
/**
* @param $nodes
* @param string $type
* @param bool $include_root
* @return array|bool
*/
static function FormatArray( $nodes, $type = 'HTML', $include_root = false ) {
$type = strtolower( $type );
if ( $include_root === true ) {
if ( !is_array( $nodes ) ) {
$nodes = [];
}
$root_node = [
'id' => 0,
'name' => 'Root',
'level' => 0,
];
array_unshift( $nodes, $root_node );
}
if ( $nodes === false ) {
return false;
}
$retarr = [];
foreach ( $nodes as $node ) {
switch ( $type ) {
case 'no_tree_text':
$spacing = str_repeat( '| &nbsp;', ( $node['level'] * 1 ) );
$text = $node['name'];
break;
case 'text':
$spacing = str_repeat( '| &nbsp;', ( $node['level'] * 1 ) );
$text = $spacing . $node['name'];
break;
case 'plain_text':
$spacing = str_repeat( '| ', ( $node['level'] * 1 ) );
$text = $spacing . $node['name'];
break;
case 'html':
$width = ( ( $node['level'] - 1 ) * 20 );
$spacing = '<img src="' . Environment::getBaseURL() . 'images/s.gif" width="' . $width . '">';
$text = $spacing . ' ' . $node['name'];
break;
case 'array':
break;
}
$node['spacing'] = $spacing;
$node['text'] = $text;
$retarr[] = $node;
unset( $node );
}
return $retarr;
}
}
?>