PageRenderTime 47ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/hierarchical.php

https://github.com/dreamhackcrew/API
PHP | 523 lines | 333 code | 113 blank | 77 comment | 134 complexity | f5cfba141816b718cae406eadd275bf2 MD5 | raw file
  1. <?php
  2. class hierarchical {
  3. const version = '1.0.0';
  4. private $info = array();
  5. function __construct($table,$id=NULL,$child=NULL) {
  6. $this->table = $table;
  7. $this->id = $id;
  8. $this->child = $child;
  9. }
  10. function init( $do = NULL, $basic = false ) {
  11. if ( $do == NULL )
  12. $do = $this->table;
  13. if ( isset( $this->info[$do]['basic'] ) && $basic )
  14. return true;
  15. if ( isset( $this->info[$do]['ready'] ) )
  16. return true;
  17. if ( trim($do) == '' )
  18. return !trigger_error('No table is defined, failed hierarchical initiation',E_USER_ERROR);
  19. if ( !$tables = db()->fetchAll("DESCRIBE `$do`"))
  20. return !trigger_error('Table "'.$do.'" don´t exists!',E_USER_ERROR);
  21. $this->info[$do] = array();
  22. foreach($tables as $line) {
  23. if ($line['Key'] == 'PRI')
  24. $this->info[$do]['id'] = $line['Field'];
  25. if ($line['Field'] == 'lft')
  26. $lft_found = true;
  27. if ($line['Field'] == 'rgt')
  28. $rgt_found = true;
  29. if ($line['Field'] == 'prio')
  30. $this->info[$do]['prio'] = true;
  31. }
  32. if( !isset($this->info[$do]['id']) || trim($this->info[$do]['id']) == '' )
  33. return !trigger_error('Table "'.$do.'" don´t have a primary key!',E_USER_ERROR);
  34. if ( !isset($lft_found) || !isset($rgt_found) ) {
  35. db()->installTables( array(
  36. $do => array(
  37. array(
  38. 'Field' => 'lft',
  39. 'Type' => 'int(11)'
  40. ),
  41. array(
  42. 'Field' => 'rgt',
  43. 'Type' => 'int(11)'
  44. ),
  45. array(
  46. 'Field' => 'parent',
  47. 'Type' => 'int(11)'
  48. )
  49. )
  50. ));
  51. db()->query("ALTER TABLE `%s` ADD INDEX ( `lft` , `rgt` )",$do);
  52. if ( !isset($this->id) || trim($this->id) == '' )
  53. $this->id = $this->info[$do]['id'];
  54. $this->$this->info[$do]['basic'] = true;
  55. if ( !$this->rebuild() )
  56. return false;
  57. }
  58. $this->info[$do]['basic'] = true;
  59. if ( $do == $this->table ) {
  60. if ( !isset($this->id) || trim($this->id) == '' )
  61. $this->id = $this->info[$do]['id'];
  62. if ( ( !isset($this->prio) || trim($this->prio) == '' ) && isset($this->info[$do]['prio']) )
  63. $this->prio = $this->info[$do]['prio'];
  64. // integrity check... only on the main table
  65. if (!$integrity = db()->fetchSingle("SELECT count(*) as rows, max(rgt) as max FROM `{$do}`") )
  66. return !trigger_error('Table "'.$do.'" failed integrity check',E_USER_ERROR);
  67. if ( ($integrity['rows'] * 2) != $integrity['max'] && !$basic ) {
  68. trigger_error('Table "'.$do.'" failed integrity check, trying to repair tree!',E_USER_ERROR);
  69. if ( !$this->rebuild() )
  70. return false;
  71. }
  72. } else {
  73. if ( !isset($this->childId) || trim($this->childId) == '' )
  74. $this->childId = $this->info[$do]['id'];
  75. }
  76. $this->info[$do]['ready'] = true;
  77. if ( isset($this->child) && $this->child )
  78. return $this->init($this->child);
  79. return true;
  80. }
  81. function setId( $id ) {
  82. if ( is_numeric($id) || strlen($id) < 2 )
  83. return !trigger_error('Id field must be a string and at least 2 chars long');
  84. $this->id = $id;
  85. }
  86. function setChild( $table ) {
  87. if ( trim($table) > '' )
  88. $this->child = $table;
  89. }
  90. function setPrio( $prio ) {
  91. if ( is_numeric($prio) || strlen($prio) < 2 )
  92. return !trigger_error('Priority field must be a string and at least 2 chars long');
  93. $this->info[$this->table]['prio'] = $prio;
  94. }
  95. function tree($root,$where='',$what='*') {
  96. $data = $this->do_tree($root,$where,$what);
  97. return $data['childs'];
  98. }
  99. function get_tree($root,$where = '',$what='*') {
  100. if (!$this->init())
  101. return !trigger_error('Hierarchical isn´t initiated!',E_USER_WARNING);
  102. $where = trim($where);
  103. if ( $where != '' )
  104. $where = '('.$where.') AND ';
  105. $what = explode( ',', $what );
  106. if ( !in_array('*',$what) ) {
  107. $what[] = "`{$this->id}`";
  108. $what[] = "`lft`";
  109. $what[] = "`rgt`";
  110. }
  111. $what = array_unique($what);
  112. $what = implode($what, ', ');
  113. if (is_array($root)) {
  114. if (!$row = db()->fetchSingle("SELECT min(lft) as lft, max(rgt) as rgt FROM {$this->table} WHERE $where parent IN (".implode($root,',').')'))
  115. return false;
  116. }
  117. else {
  118. if ( $root ) {
  119. if (!$row = db()->fetchSingle("SELECT min(lft) as lft, max(rgt) as rgt FROM {$this->table} WHERE $where parent=$root"))
  120. return false;
  121. } else {
  122. if (!$row = db()->fetchSingle("SELECT min(lft) as lft, max(rgt) as rgt FROM {$this->table} WHERE $where 1=1"))
  123. return false;
  124. }
  125. }
  126. if (!($row['lft'] > 0 && $row['rgt'] > 0))
  127. return false;
  128. return db()->fetchAll("SELECT $what FROM {$this->table} WHERE $where lft BETWEEN %d AND %d ORDER BY lft ASC;",$row['lft'],$row['rgt']);
  129. }
  130. function do_tree($root,$where='',$what='*') {
  131. if(!$result = $this->get_tree($root,$where,$what))
  132. return false;
  133. $right = array();
  134. $ret = array();
  135. $level = array();
  136. foreach ($result as $row) {
  137. while (count($right)>0 && ($right[count($right)-1] < $row['rgt'])) {
  138. array_pop($right);
  139. array_pop($level);
  140. }
  141. $l = '';
  142. foreach($level as $line)
  143. $l .= "['childs'][$line]";
  144. eval('$ret'.$l.'[\'childs\']['.$row[$this->id].'] = $row;');
  145. $right[] = $row['rgt'];
  146. $level[] = $row[$this->id];
  147. }
  148. return $ret;
  149. }
  150. function selectOptions($root,$where='',$head = 'head') {
  151. if(!$result = $this->get_tree($root,$where))
  152. return array();
  153. $right = array();
  154. $options = array();
  155. foreach ($result as $row) {
  156. while (count($right)>0 && ($right[count($right)-1] < $row['rgt'])) {
  157. array_pop($right);
  158. }
  159. $options[] = array(
  160. 'text' => str_repeat(' -',count($right)) . ' ' . $row[$head],
  161. 'val' => $row[$this->id]
  162. );
  163. $right[] = $row['rgt'];
  164. }
  165. return $options;
  166. }
  167. function do_level($root,$where = '') {
  168. if(!$result = $this->get_tree($root,$where))
  169. return false;
  170. foreach ($result as $row) {
  171. if ($row['parent'] == $root)
  172. $ret[] = $row;
  173. }
  174. return $ret;
  175. }
  176. function do_list( $root,$fields = '' ) {
  177. if( !$result = $this->get_tree($root) ){
  178. //@trigger_error('No childs to "'.$root.'" was found!');
  179. return array();
  180. }
  181. $ret = array();
  182. foreach ( $result as $row )
  183. if ( $fields != '' )
  184. if( is_array($fields) )
  185. $ret[] = array_intersect_key($row,$fields);
  186. else
  187. if ( isset($row[$fields]) )
  188. $ret[] = $row[$fields];
  189. else
  190. trigger_error('Field "'.$fields.'" not found in source',E_USER_NOTICE);
  191. else
  192. $ret[] = $row;
  193. return $ret;
  194. }
  195. function do_path($id,$field = '',$separator = '') {
  196. if (!$this->init())
  197. return !trigger_error('Hierarchical isn´t initiated!',E_USER_WARNING);
  198. if ( !$data = db()->fetchSingle("SELECT lft,rgt FROM `{$this->table}` WHERE `{$this->id}`='$id'"))
  199. return !trigger_error('Main post not found in '.$this->table.'!',E_USER_WARNING);
  200. if( trim($field) != '' ) {
  201. if( $path = db()->fetchAllOne("SELECT $field FROM {$this->table} WHERE lft <= {$data['lft']} AND rgt >= {$data['rgt']} ORDER BY lft ASC"))
  202. if (trim($separator) != '')
  203. return implode($path,$separator);
  204. else
  205. return $path;
  206. } else {
  207. if( $path = db()->fetchAll("SELECT * FROM {$this->table} WHERE lft <= {$data['lft']} AND rgt >= {$data['rgt']} ORDER BY lft ASC"))
  208. return $path;
  209. }
  210. return false;
  211. }
  212. function descendants($lft,$rgt) {
  213. return ($rgt - $lft - 1) / 2;
  214. }
  215. function rebuild($parent = 0, $left = 0) {
  216. db()->query("UPDATE {$this->table} SET lft=-1, rgt=-1");
  217. if ( !$this->do_rebuild($parent, $left) )
  218. return !trigger_error('rebuild tree failed',E_USER_ERROR);
  219. return true;
  220. }
  221. function do_rebuild($parent, $left) {
  222. if ( !$this->init( NULL, true ) )
  223. return !trigger_error('Hierarchical isn´t initiated, can not save!',E_USER_WARNING);
  224. if (!isset($this->table))
  225. return !trigger_error('Table isn´t defined',E_USER_WARNING);
  226. if (!is_numeric($parent))
  227. return !trigger_error('$parent isn´t a numeric value',E_USER_WARNING);
  228. $right = $left+1;
  229. if ( isset($this->prio) ) {
  230. if($result = db()->fetchAllOne("SELECT {$this->id} FROM {$this->table} WHERE parent=$parent ORDER BY prio;"))
  231. foreach($result as $row)
  232. $right = self::do_rebuild($row, $right);
  233. } else {
  234. if($result = db()->fetchAllOne("SELECT {$this->id} FROM {$this->table} WHERE parent=$parent;"))
  235. foreach($result as $row)
  236. $right = self::do_rebuild($row, $right);
  237. }
  238. db()->query("UPDATE {$this->table} SET lft=$left, rgt=$right WHERE {$this->id}=$parent;");
  239. if ( isset($this->child) && $this->child )
  240. db()->query("UPDATE {$this->child} SET lft=$left, rgt=$right WHERE {$this->id}=$parent;");
  241. return $right+1;
  242. }
  243. function add( $data, $parent = -1 ) {
  244. if (!$this->init())
  245. return !trigger_error('Hierarchical isn´t initiated, can not save!',E_USER_WARNING);
  246. if ( $parent != -1 )
  247. $data['parent'] = $parent;
  248. if ( !isset($data['parent']) )
  249. return !trigger_error('Input parent id isn´t set, can not save!',E_USER_WARNING);
  250. if ( !is_numeric($data['parent']) )
  251. return !trigger_error('Input parent isn´t a numeric value, can not save!',E_USER_WARNING);
  252. if ( isset($this->prio) && isset($data['prio']) && is_numeric($data['prio']) ) {
  253. if ( ($rgt = db()->fetchOne("SELECT rgt FROM {$this->table} WHERE parent={$data['parent']} AND prio<={$data['prio']} ORDER BY prio DESC,lft DESC LIMIT 1")) === false)
  254. @trigger_error('Failed to select the insert point of the post, trying parent!');
  255. } else {
  256. if( ($rgt = db()->fetchOne("SELECT rgt FROM {$this->table} WHERE parent={$data['parent']} ORDER BY lft DESC LIMIT 1")) === false )
  257. @trigger_error('Failed to select the insert point of the post, trying parent!');
  258. }
  259. if ( !isset($rgt) || isset($rgt) && !is_numeric($rgt) )
  260. if ( ($rgt = db()->fetchOne("SELECT lft FROM {$this->table} WHERE {$this->id}={$data['parent']}")) === false )
  261. if ( db()->fetchAll("SELECT * FROM {$this->table}") === false )
  262. $rgt = 0;
  263. else
  264. if ( ($rgt = db()->fetchOne("SELECT lft FROM {$this->table} WHERE parent={$data['parent']} ORDER BY prio LIMIT 1")) === false )
  265. return !trigger_error('Failed to select the insert point of the post!',E_USER_ERROR);
  266. $data['lft'] = $rgt + 1;
  267. $data['rgt'] = $rgt + 2;
  268. $time = microtime(true);
  269. // Skapa ett hål
  270. if ( db()->query("UPDATE {$this->table} SET rgt=rgt+2 WHERE lft < %d AND rgt >= %d ",$data['lft'],$data['lft']) &&
  271. db()->query("UPDATE {$this->table} SET rgt=rgt+2,lft=lft+2 WHERE lft >= %d",$data['rgt']) ) {
  272. // Uppdatera i spegel trädet
  273. if ( isset($this->child) && $this->child ) {
  274. db()->query("UPDATE {$this->child} SET rgt=rgt+2 WHERE lft < %d AND rgt >= %d ",$data['lft'],$data['lft']);
  275. db()->query("UPDATE {$this->child} SET rgt=rgt+2,lft=lft+2 WHERE lft >= %d",$data['rgt']);
  276. }
  277. // Lägg till datan
  278. if ( !db()->insert( $data,$this->table ) )
  279. return !trigger_error('Failed to save data in tree!',E_USER_ERROR);
  280. $insert = mysql_insert_id();
  281. // Kontrollera så att det inte blivit något fel
  282. /*
  283. if ( $failed = db()->fetchAll("SELECT * FROM {$this->table} AS a, {$this->table} AS b
  284. WHERE (a.lft = b.lft OR a.lft = b.rgt OR a.rgt = b.rgt) AND a.{$this->id} <> b.{$this->id}") ) {
  285. trigger_error('Faild to do a cleean insert, rebuilding tree!',E_USER_ERROR);
  286. $this->rebuild();
  287. }
  288. */
  289. // Returnera idt datan fick
  290. return $insert;
  291. }
  292. return !trigger_error('Failed to save data in tree!',E_USER_ERROR);
  293. }
  294. function remove ( $id, $force = false ) {
  295. if (!$this->init())
  296. return !trigger_error('Hierarchical isn´t initiated!',E_USER_WARNING);
  297. if ( !is_numeric($id) )
  298. return !trigger_error('Row identifiaction isn´t a numeric value!');
  299. if ( !$post = db()->fetchSingle("SELECT lft,rgt FROM {$this->table} WHERE {$this->id}=%d",$id) )
  300. return !trigger_error('The selected row could not be found in the database!',E_USER_ERROR);
  301. $posts = ($post['rgt']-$post['lft']+1 * 2)-1;
  302. if ( $posts > 2 )
  303. if ( $force )
  304. $childs = $this->do_list( $id, $this->id );
  305. else
  306. return !trigger_error('The selected post have childs! Remove aborted');
  307. if ( !db()->query("DELETE FROM {$this->table} WHERE lft BETWEEN {$post['lft']} AND {$post['rgt']}") )
  308. return !trigger_error("Failed to remove the selected post and its childs!");
  309. if ( db()->query("UPDATE {$this->table} SET rgt=rgt-$posts,lft=lft-$posts WHERE lft>{$post['rgt']}") &&
  310. db()->query("UPDATE {$this->table} SET rgt=rgt-$posts WHERE rgt > {$post['rgt']} AND lft < {$post['lft']} ")
  311. ) {
  312. if ( isset($this->child) && $this->child ) {
  313. db()->query("UPDATE {$this->child} SET rgt=rgt-$posts,lft=lft-$posts WHERE lft>{$post['rgt']}");
  314. db()->query("UPDATE {$this->child} SET rgt=rgt-$posts WHERE rgt > {$post['rgt']} AND lft < {$post['lft']} ");
  315. }
  316. return true;
  317. }
  318. $this->rebuild();
  319. return !trigger_error('Failed to remove data in tree, reversing and rebuilding tree!',E_USER_ERROR);
  320. }
  321. function move ( $id, $parent, $prio = 0 ) {
  322. if (!$this->init())
  323. return !trigger_error('Hierarchical isn´t initiated!',E_USER_WARNING);
  324. if ( !is_numeric($id) )
  325. return !trigger_error('Row identifiaction isn´t a numeric value!');
  326. if ( !is_numeric($parent) )
  327. return !trigger_error('Row parent identifiaction isn´t a numeric value!',E_USER_WARNING);
  328. $row = db()->fetchSingle("SELECT * FROM {$this->table} WHERE {$this->id}=%d",$id);
  329. $this->remove( $id );
  330. $row['parent'] = $parent;
  331. return $this->add($row);
  332. /*
  333. // ---------------------------------------------------------------------------------------------------------
  334. if ( !$post = db()->fetchSingle("SELECT lft,rgt FROM {$this->table} WHERE {$this->id}=%d",$id) )
  335. return !trigger_error('The selected post could not be found in the database!',E_USER_ERROR);
  336. if ( isset($this->prio) && $prio && is_numeric($prio) ) {
  337. if ( ($rgt = db()->fetchOne("SELECT rgt FROM {$this->table} WHERE parent=$parent AND prio<=$prio ORDER BY prio DESC,lft DESC LIMIT 1")) === false)
  338. @trigger_error('Failed to select the insert point of the post, trying parent!');
  339. } else {
  340. if( ($rgt = db()->fetchOne("SELECT rgt FROM {$this->table} WHERE parent=$parent ORDER BY lft DESC,{$this->id} LIMIT 1")) === false )
  341. @trigger_error('Failed to select the insert point of the post, trying parent!');
  342. }
  343. if ( !isset($rgt) || isset($rgt) && !is_numeric($rgt) )
  344. if ( ($rgt = db()->fetchOne("SELECT lft FROM {$this->table} WHERE {$this->id}=$parent")) === false )
  345. if ( db()->fetchAll("SELECT * FROM {$this->table}") === false )
  346. $rgt = 0;
  347. else
  348. if ( ($rgt = db()->fetchOne("SELECT lft FROM {$this->table} WHERE parent=$parent ORDER BY prio LIMIT 1")) === false )
  349. return !trigger_error('Failed to select the insert point of the post!',E_USER_ERROR);
  350. $shift = $post['lft'] - ($rgt + 1);
  351. $posts = $post['rgt'] - $post['lft'] + 1;
  352. $move = db()->fetchOne("SELECT count(*) FROM {$this->table}") * 2;
  353. //echo $move;
  354. // flytta posten till slutet..
  355. db()->query("UPDATE {$this->table} SET lft=lft+$move, rgt=rgt+$move, parent=$parent WHERE {$this->id}=$id");
  356. if ( $shift > 0 ) { // flytta upp en post
  357. // shifta alla poster emellan
  358. db()->query("UPDATE {$this->table} SET rgt=rgt-$posts, lft=lft-$posts WHERE rgt BETWEEN {$post['rgt']} AND $rgt ");
  359. } else { // flytta ner
  360. // shifta alla poster emellan
  361. db()->query("UPDATE {$this->table} SET rgt=rgt-$posts, lft=lft-$posts WHERE rgt > {$post['rgt']} AND lft < $rgt AND lft > {$post['rgt']} ");
  362. $shift += $posts;
  363. }
  364. // minska alla parents med antalet poster som flyttat
  365. db()->query("UPDATE {$this->table} SET rgt=rgt-$posts WHERE lft < {$post['lft']} AND rgt > {$post['rgt']}");
  366. // öka alla parents med antalet poster som flyttat till
  367. db()->query("UPDATE {$this->table} SET rgt=rgt+$posts WHERE lft < $rgt AND rgt > $rgt");
  368. // flytta in på rätt pos
  369. db()->query("UPDATE {$this->table} SET lft=lft-$move-$shift, rgt=rgt-$move-$shift WHERE lft >$move");
  370. // innan
  371. //db()->query("UPDATE {$this->table} SET rgt=rgt+$shift+1 WHERE rgt > $rgt AND lft < $rgt");
  372. // efter
  373. */
  374. }
  375. function _check( $parent = 0 ) {
  376. if (!$this->init())
  377. return !trigger_error('Hierarchical isn´t initiated!',E_USER_WARNING);
  378. if ( $childs = db()->fetchAll("SELECT {$this->id},name,lft,rgt FROM {$this->table} WHERE parent=%d",$parent) )
  379. foreach ( $childs as $line ) {
  380. $count = db()->fetchOne("SELECT count(*) FROM {$this->table} WHERE lft BETWEEN %d AND %d",$line['lft'],$line['rgt']);
  381. if ( ($count*2) != ($line['rgt'] - $line['lft']+1) ) {
  382. if ( $this->_check( $line[$this->id] ) )
  383. return !trigger_error('Failed integrity check count in ID#'.$line[$this->id].' count is <b>'.($count*2).'</b> and shuld be <b>'.($line['rgt'] - $line['lft']+1).'</b>');
  384. }
  385. }
  386. return true;
  387. }
  388. }
  389. ?>