PageRenderTime 45ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/apps/navigation/lib/Tree.php

https://bitbucket.org/jonphipps/elefant-vocabhub
PHP | 438 lines | 283 code | 35 blank | 120 comment | 65 complexity | 093fc356866303392b7cafaa335d74c8 MD5 | raw file
  1. <?php
  2. /**
  3. * Elefant CMS - http://www.elefantcms.com/
  4. *
  5. * Copyright (c) 2011 Johnny Broadway
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy
  8. * of this software and associated documentation files (the "Software"), to deal
  9. * in the Software without restriction, including without limitation the rights
  10. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. * copies of the Software, and to permit persons to whom the Software is
  12. * furnished to do so, subject to the following conditions:
  13. *
  14. * The above copyright notice and this permission notice shall be included in
  15. * all copies or substantial portions of the Software.
  16. *
  17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. * THE SOFTWARE.
  24. */
  25. /**
  26. * Handles managing a tree of objects as a JSON file. Items in the tree have the
  27. * following structure:
  28. *
  29. * {"data":"Title or name","attr":{"id":"ID value","sort":0}}
  30. *
  31. * Item that have children will have an additional `children`
  32. * property that is an array of other items.
  33. *
  34. * Usage:
  35. *
  36. * $n = new Tree ('conf/categories.json');
  37. * // $n->tree contains data from conf/categories.json
  38. *
  39. * $n->add ('Topics');
  40. * $n->add ('Design', 'Topics');
  41. * $n->add ('Web Design', 'Design');
  42. * $n->add ('Print Design', 'Design');
  43. * // etc.
  44. *
  45. * $node = $n->node ('Web Design');
  46. * // {"data":"Web Design","attr":{"id":"Web Design","sort":0}}
  47. *
  48. * $path = $n->path ('Web Design');
  49. * // array ('Topics', 'Design', 'Web Design')
  50. *
  51. * $n->move ('Web Design', 'Design', 'before');
  52. * $ids = $n->get_all_ids ();
  53. * // array ('Topics', 'Web Design', 'Design', 'Print Design')
  54. *
  55. * $n->remove ('Web Design');
  56. *
  57. * // save out to conf/categories.json
  58. * $n->save ();
  59. */
  60. class Tree {
  61. /**
  62. * The file that contained the JSON tree structure.
  63. */
  64. public $file = null;
  65. /**
  66. * The tree structure.
  67. */
  68. public $tree = array ();
  69. /**
  70. * The error message if an error occurs, or false if no errors.
  71. */
  72. public $error = false;
  73. /**
  74. * Constructor method. Decodes the tree from the specified file in JSON format.
  75. */
  76. public function __construct ($file) {
  77. $this->file = $file;
  78. $this->tree = json_decode (file_exists ($file) ? file_get_contents ($file) : '[]');
  79. if (json_last_error () != JSON_ERROR_NONE || ! is_array ($this->tree)) {
  80. $this->tree = array ();
  81. }
  82. }
  83. /**
  84. * Get all page ids from the tree.
  85. */
  86. public function get_all_ids ($tree = false) {
  87. if (! $tree) {
  88. $tree = $this->tree;
  89. }
  90. $ids = array ();
  91. foreach ($tree as $item) {
  92. $ids[] = $item->attr->id;
  93. if (isset ($item->children)) {
  94. $ids = array_merge ($ids, $this->get_all_ids ($item->children));
  95. }
  96. }
  97. return $ids;
  98. }
  99. /**
  100. * Get the section nodes, meaning all nodes that have sub-nodes.
  101. */
  102. public function sections ($tree = false) {
  103. if (! $tree) {
  104. $tree = $this->tree;
  105. }
  106. $sections = array ();
  107. foreach ($tree as $item) {
  108. if (isset ($item->children)) {
  109. $sections[] = $item->attr->id;
  110. $sections = array_merge ($sections, $this->sections ($item->children));
  111. }
  112. }
  113. return $sections;
  114. }
  115. /**
  116. * Find a specific node in the tree.
  117. */
  118. public function node ($id, $tree = false) {
  119. if (! $tree) {
  120. $tree = $this->tree;
  121. }
  122. foreach ($tree as $item) {
  123. if ($item->attr->id == $id) {
  124. return $item;
  125. }
  126. if (isset ($item->children)) {
  127. $res = $this->node ($id, $item->children);
  128. if ($res) {
  129. return $res;
  130. }
  131. }
  132. }
  133. return null;
  134. }
  135. /**
  136. * Find the parent of a specific node in the tree.
  137. */
  138. public function parent ($id, $tree = false) {
  139. if (! $tree) {
  140. $tree = $this->tree;
  141. }
  142. $parent = null;
  143. foreach ($tree as $item) {
  144. if ($item->attr->id == $id) {
  145. // found item itself, should have found parent by now
  146. return null;
  147. }
  148. if (isset ($item->children)) {
  149. foreach ($item->children as $child) {
  150. if ($child->attr->id == $id) {
  151. // we're on the parent!
  152. return $item;
  153. }
  154. }
  155. $parent = $this->parent ($id, $item->children);
  156. }
  157. }
  158. return $parent;
  159. }
  160. /**
  161. * Find the path to a specific node in the tree, including the node itself.
  162. * If titles is true, it will return an associative array with the keys being
  163. * object IDs and the values being their data attribute (usually, the title or
  164. * name). If `$titles` is false, it will return an array of IDs only.
  165. */
  166. public function path ($id, $titles = false, $tree = false) {
  167. if (! $tree) {
  168. if (is_array ($titles)) {
  169. $tree = $titles;
  170. $titles = false;
  171. } else {
  172. $tree = $this->tree;
  173. }
  174. }
  175. foreach ($tree as $item) {
  176. if ($item->attr->id == $id) {
  177. return $titles ? array ($item->attr->id => $item->data) : array ($id);
  178. } elseif (isset ($item->children)) {
  179. $res = $this->path ($id, $titles, $item->children);
  180. if ($res) {
  181. return $titles ? array_merge (array ($item->attr->id => $item->data), $res) : array_merge (array ($item->attr->id), $res);
  182. }
  183. }
  184. }
  185. return null;
  186. }
  187. /**
  188. * Add an object to the tree under the specified parent node.
  189. * $id can be an ID or a node object.
  190. */
  191. public function add ($obj, $parent = false) {
  192. if (! is_object ($obj)) {
  193. $obj = (object) array (
  194. 'data' => $obj,
  195. 'attr' => (object) array (
  196. 'id' => $obj,
  197. 'sort' => 0
  198. )
  199. );
  200. }
  201. // locate $parent and add child
  202. if ($parent) {
  203. $ref = $this->node ($parent);
  204. if (! isset ($ref->children)) {
  205. $ref->children = array ();
  206. }
  207. $obj->attr->sort = count ($ref->children);
  208. $ref->children[] = $obj;
  209. $ref->state = 'open';
  210. } else {
  211. $obj->attr->sort = count ($this->tree);
  212. $this->tree[] = $obj;
  213. }
  214. return true;
  215. }
  216. /**
  217. * Remove an object from the tree. Removes all children as well
  218. * unless you set $recursive to false.
  219. */
  220. function remove ($id, $recursive = true) {
  221. $ref = $this->parent ($id);
  222. if ($ref) {
  223. foreach ($ref->children as $key => $child) {
  224. if ($child->attr->id == $id) {
  225. if (! $recursive) {
  226. // move all children to parent
  227. if (isset ($child->children)) {
  228. foreach ($child->children as $item) {
  229. $this->add ($item, $ref->attr->id);
  230. }
  231. }
  232. }
  233. unset ($ref->children[$key]);
  234. if (count ($ref->children) == 0) {
  235. unset ($ref->children);
  236. unset ($ref->state);
  237. } else {
  238. // prevent array from becoming associative on save
  239. $ref->children = array_values ($ref->children);
  240. }
  241. return true;
  242. }
  243. }
  244. } else {
  245. foreach ($this->tree as $key => $child) {
  246. if ($child->attr->id == $id) {
  247. if (! $recursive) {
  248. // move all children to root
  249. if (isset ($child->children)) {
  250. foreach ($child->children as $item) {
  251. $this->add ($item);
  252. }
  253. }
  254. }
  255. unset ($this->tree[$key]);
  256. // prevent array from becoming associative on save
  257. $this->tree = array_values ($this->tree);
  258. return true;
  259. }
  260. }
  261. }
  262. return false;
  263. }
  264. /**
  265. * Remove a specific path from the tree recursively. Used
  266. * primarily by move().
  267. */
  268. public function remove_path ($path) {
  269. $id = $path[count ($path) - 1];
  270. if (! $id) {
  271. return false;
  272. }
  273. if (count ($path) > 1) {
  274. $ref = $this->node ($path[count ($path) - 2]);
  275. }
  276. if ($ref) {
  277. foreach ($ref->children as $key => $child) {
  278. if ($child->attr->id == $id) {
  279. unset ($ref->children[$key]);
  280. if (count ($ref->children) == 0) {
  281. unset ($ref->children);
  282. unset ($ref->state);
  283. }
  284. return true;
  285. }
  286. }
  287. } else {
  288. foreach ($this->tree as $key => $child) {
  289. if ($child->attr->id == $id) {
  290. unset ($this->tree[$key]);
  291. return true;
  292. }
  293. }
  294. }
  295. return false;
  296. }
  297. /**
  298. * Move an object to the specified location ($ref) in the tree.
  299. * $pos can be one of: after, before, inside (default is
  300. * inside).
  301. */
  302. public function move ($id, $ref, $pos = 'inside') {
  303. $old_path = $this->path ($id);
  304. $ref_parent = $this->parent ($ref);
  305. $node = $this->node ($id);
  306. switch ($pos) {
  307. case 'before':
  308. $this->remove ($id);
  309. // add sorted
  310. if ($ref_parent) {
  311. $old_children = $ref_parent->children;
  312. $new_children = array ();
  313. $sort = 0;
  314. foreach ($old_children as $child) {
  315. if ($child->attr->id == $ref) {
  316. $node->attr->sort = $sort;
  317. $new_children[] = $node;
  318. $sort++;
  319. }
  320. $child->attr->sort = $sort;
  321. $new_children[] = $child;
  322. $sort++;
  323. }
  324. $ref_parent->children = $new_children;
  325. } else {
  326. $old_children = $this->tree;
  327. $new_children = array ();
  328. $sort = 0;
  329. foreach ($old_children as $child) {
  330. if ($child->attr->id == $ref) {
  331. $node->attr->sort = $sort;
  332. $new_children[] = $node;
  333. $sort++;
  334. }
  335. $child->attr->sort = $sort;
  336. $new_children[] = $child;
  337. $sort++;
  338. }
  339. $this->tree = $new_children;
  340. }
  341. break;
  342. case 'after':
  343. $this->remove ($id);
  344. // add sorted
  345. if ($ref_parent) {
  346. $old_children = $ref_parent->children;
  347. $new_children = array ();
  348. $sort = 0;
  349. foreach ($old_children as $child) {
  350. $child->attr->sort = $sort;
  351. $new_children[] = $child;
  352. $sort++;
  353. if ($child->attr->id == $ref) {
  354. $node->attr->sort = $sort;
  355. $new_children[] = $node;
  356. $sort++;
  357. }
  358. }
  359. $ref_parent->children = $new_children;
  360. } else {
  361. $old_children = $this->tree;
  362. $new_children = array ();
  363. $sort = 0;
  364. foreach ($old_children as $child) {
  365. $child->attr->sort = $sort;
  366. $new_children[] = $child;
  367. $sort++;
  368. if ($child->attr->id == $ref) {
  369. $node->attr->sort = $sort;
  370. $new_children[] = $node;
  371. $sort++;
  372. }
  373. }
  374. $this->tree = $new_children;
  375. }
  376. break;
  377. case 'inside':
  378. case 'last':
  379. $this->remove ($id);
  380. $this->add ($node, $ref);
  381. break;
  382. }
  383. return true;
  384. }
  385. /**
  386. * Update the tree from Json to the file.
  387. */
  388. public function update ($tree) {
  389. $this->tree = $tree;
  390. return true;
  391. }
  392. /**
  393. * Save the tree out to the file.
  394. */
  395. public function save () {
  396. if (! file_put_contents ($this->file, json_encode ($this->tree))) {
  397. $this->error = 'Failed to save file: ' . $this->file;
  398. return false;
  399. }
  400. return true;
  401. }
  402. }
  403. ?>