SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Treenode.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13.  
  14. namespace Sprout\Helpers;
  15.  
  16. use ArrayAccess;
  17. use Exception;
  18.  
  19.  
  20. /**
  21. * @package Tree
  22. **/
  23.  
  24. /**
  25. * Represents a node in the tree
  26. **/
  27. class Treenode implements ArrayAccess
  28. {
  29. protected $data = array();
  30. private $real_children = array();
  31. private $filtered_children = null;
  32.  
  33. public $parent = null;
  34.  
  35.  
  36. /**
  37.   * Creates a node, with the specified data
  38.   *
  39.   * @param array $data Initial data for the node
  40.   **/
  41. public function __construct($data = null)
  42. {
  43. if (! $data) return;
  44. foreach ($data as $key => $value) {
  45. $this->data[$key] = $value;
  46. }
  47. }
  48.  
  49.  
  50. /**
  51.   * Returns true if this node is the root node
  52.   **/
  53. public function isRoot()
  54. {
  55. if ($this->parent) return false;
  56. return true;
  57. }
  58.  
  59.  
  60.  
  61. /* TREE LOADING ----------------------------------------------*/
  62.  
  63. /**
  64.   * Loads a flat table into a tree structure.
  65.   * Requires the table to have an id, parent_id and record_order field.
  66.   *
  67.   * @param string $table The name of the table to load
  68.   * @param array $conditions Query conditions, in the format used by {@see Pdb::buildClause}
  69.   * @param string $order The column to order by. Defaults to 'record_order'
  70.   * @return Treenode The loaded tree
  71.   **/
  72. public static function loadTree($table, array $conditions = ['1'], $order = 'record_order')
  73. {
  74. Pdb::validateIdentifier($table);
  75. Pdb::validateIdentifier($order);
  76.  
  77. $binds = array();
  78. $where = Pdb::buildClause($conditions, $binds);
  79.  
  80. $q = "SELECT *
  81. FROM ~{$table}
  82. WHERE {$where}
  83. ORDER BY parent_id, {$order}";
  84. $res = Pdb::query($q, $binds, 'pdo');
  85.  
  86. $nodes = array();
  87. foreach ($res as $row) {
  88. $nodes[] = new Treenode($row);
  89. }
  90.  
  91. $res->closeCursor();
  92.  
  93. $root_node = new Treenode(array('id' => 0));
  94.  
  95. // Assign nodes to their parents
  96. $orphans = array();
  97. do {
  98. $num_processed = 0;
  99. foreach ($nodes as $index => $node) {
  100. $parent = $root_node->findNodeValue('id', $node['parent_id']);
  101.  
  102. if ($parent) {
  103. $parent->children[] = $node;
  104. $node->parent = $parent;
  105. unset ($nodes[$index]);
  106. unset ($orphans[$node['id']]);
  107. $num_processed++;
  108.  
  109. } else {
  110. $orphans[$node['id']] = $node;
  111. }
  112. }
  113. if ($num_processed == 0) break;
  114.  
  115. } while (count($nodes));
  116.  
  117. return $root_node;
  118. }
  119.  
  120.  
  121. /* TREE SEARCHING --------------------------------------------*/
  122.  
  123. /**
  124.   * Finds a node by looking at this node
  125.   * If that does not match, looking at each of this nodes children in turn.
  126.   * If that does not match, return null
  127.   *
  128.   * @param mixed $key The key to search for in the data
  129.   * @param mixed $value The value to search for in the data
  130.   * @return TreeNode if found, null if not found.
  131.   **/
  132. public function findNodeValue($key, $value)
  133. {
  134. if ($this->data[$key] == $value) {
  135. return $this;
  136. }
  137.  
  138. foreach ($this->children as $child) {
  139. $res = $child->findNodeValue ($key, $value);
  140. if ($res) return $res;
  141. }
  142.  
  143. return null;
  144. }
  145.  
  146.  
  147. /**
  148.   * Finds a node by looking at this node
  149.   * If that does not match, looking at each of this nodes children in turn.
  150.   * If that does not match, return null
  151.   *
  152.   * @param TreenodeMatcher $matcher The matcher to use for finding the node.
  153.   * @return static if found, null if not found.
  154.   **/
  155. public function findNode(TreenodeMatcher $matcher)
  156. {
  157. if ($matcher->match($this)) {
  158. return $this;
  159. }
  160.  
  161. $res = $matcher->descend($this);
  162. if (! $res) return null;
  163.  
  164. foreach ($this->children as $child) {
  165. $res = $child->findNode ($matcher);
  166. if ($res) return $res;
  167. }
  168.  
  169. $matcher->ascend($this);
  170.  
  171. return null;
  172. }
  173.  
  174.  
  175. /**
  176.   * Finds all nodes which match the specified matcher.
  177.   *
  178.   * @param TreenodeMatcher $matcher The matcher to use for finding the nodes.
  179.   * @return static[] The found nodes, or an empty array if no nodes were found.
  180.   **/
  181. public function findAllNodes(TreenodeMatcher $matcher)
  182. {
  183. $nodes = array();
  184.  
  185. if ($matcher->match($this)) {
  186. $nodes[] = $this;
  187. }
  188.  
  189. $res = $matcher->descend($this);
  190. if (! $res) return $nodes;
  191.  
  192. foreach ($this->children as $child) {
  193. $res = $child->findAllNodes ($matcher);
  194. if ($res) {
  195. $nodes = array_merge($nodes, $res);
  196. }
  197. }
  198.  
  199. $matcher->ascend($this);
  200.  
  201. return $nodes;
  202. }
  203.  
  204.  
  205. /**
  206.   * Finds all ancestors of a specific node, including the node, not including the root node of the tree
  207.   * The array is in order from top to bottom, so $ancestors[0] is the top-parent, and $ancestors[len-1] is the node.
  208.   *
  209.   * A
  210.   * B C
  211.   * D E F G
  212.   * H I
  213.   *
  214.   * NODE RESULT ARRAY
  215.   * H B D H
  216.   * D B D
  217.   * B B
  218.   * A (empty)
  219.   *
  220.   * @return static[] The ancestors, as described above.
  221.   **/
  222. public function findAncestors()
  223. {
  224. $ancestors = array();
  225. $node = $this;
  226. while ($node->data['id'] != 0) {
  227. $ancestors[] = $node;
  228. $node = $node->parent;
  229. }
  230.  
  231. $ancestors = array_reverse($ancestors);
  232.  
  233. return $ancestors;
  234. }
  235.  
  236.  
  237. /**
  238.   * Filter the children of this node, removing any children which don't match the specified TreenodeMatcher.
  239.   * The nodes are not actually removed, matching nodes are just added to a filtered children list
  240.   * Which is returned instead of the original children list.
  241.   **/
  242. public function filterChildren(TreenodeMatcher $matcher)
  243. {
  244. $this->filtered_children = array();
  245.  
  246. $res = $matcher->descend($this);
  247. if (! $res) return;
  248.  
  249. foreach ($this->real_children as $node) {
  250. if ($matcher->match($node)) {
  251. $this->filtered_children[] = $node;
  252. $node->filterChildren($matcher);
  253. }
  254. }
  255.  
  256. $matcher->ascend($this);
  257. }
  258.  
  259.  
  260. /**
  261.   * Removes the currently active filter
  262.   **/
  263. public function removeFilter()
  264. {
  265. $this->filtered_children = null;
  266.  
  267. foreach ($this->real_children as $node) {
  268. $node->removeFilter();
  269. }
  270. }
  271.  
  272.  
  273. /**
  274.   * Returns an array of all children nodes, including sub-children
  275.   *
  276.   * The array will be id => name.
  277.   * This function requires the table to have a column named 'name'.
  278.   * The name field will be indented according to the depth.
  279.   * If specified, the exclude_id argument will be used to
  280.   * exclude nodes (and their children) according to id.
  281.   **/
  282. public function getAllChildren($exclude_id = null, $indent_str = ' ', $indent = 0)
  283. {
  284. $output = array();
  285. foreach ($this->children as $node) {
  286. if ($exclude_id == $node->data['id']) continue;
  287.  
  288. $output[$node->data['id']] = str_repeat($indent_str, $indent) . $node->data['name'];
  289.  
  290. $children = $node->getAllChildren($exclude_id, $indent_str, $indent + 1);
  291. foreach ($children as $id => $name) {
  292. $output[$id] = $name;
  293. }
  294. }
  295.  
  296. return $output;
  297. }
  298.  
  299.  
  300. /**
  301.   *
  302.   * @return static[]
  303.   */
  304. public function & getChildren()
  305. {
  306. if ($this->filtered_children !== null) {
  307. return $this->filtered_children;
  308. }
  309. return $this->real_children;
  310. }
  311.  
  312.  
  313. /**
  314.   * Is this node an orphan?
  315.   * Orphans are at the top of the tree, and they don't have any children.
  316.   *
  317.   * @return bool if it's an orphan, false otherwise
  318.   **/
  319. public function isOrphan()
  320. {
  321. if ($this->parent and !$this->parent->isRoot()) return false;
  322. if (count($this->children) != 0) return false;
  323.  
  324. return true;
  325. }
  326.  
  327.  
  328.  
  329. /* NODE DATA -------------------------------------------------*/
  330.  
  331. /**
  332.   * Generic field getter for unknown properties - used for TreeNode->children
  333.   *
  334.   * Be aware that data gotten through this getter is BY REFERENCE
  335.   * For by-value data retrival, use the array-access functions.
  336.   *
  337.   * @param string $field The field to get.
  338.   **/
  339. public function &__get($field) {
  340. if ($field == 'children') {
  341. return $this->getChildren();
  342. }
  343.  
  344. throw new Exception("Invalid usage of \$node->__get() for field '{$field}'; use array methods (\$node['{$field}']) instead.");
  345. }
  346.  
  347. /**
  348.   * Generic field getter for unknown properties - used for TreeNode->children
  349.   *
  350.   * @param string $field The field to set.
  351.   * @param mixed $value The value to set.
  352.   **/
  353. public function __set($field, $value)
  354. {
  355. if ($field == 'children') {
  356. $this->real_children = $value;
  357. return;
  358. }
  359.  
  360. throw new Exception("Invalid usage of \$node->__set() for field '{$field}'; use array methods (\$node['{$field}']) instead.");
  361. }
  362.  
  363. /**
  364.   * ArrayAccess function for checking if a specified key exists
  365.   * @param mixed $offset The offset to check.
  366.   **/
  367. public function offsetExists($offset)
  368. {
  369. return isset($this->data[$offset]);
  370. }
  371.  
  372. /**
  373.   * ArrayAccess function for getting a value by its key
  374.   * @param mixed $offset The offset to get.
  375.   **/
  376. public function offsetGet($offset)
  377. {
  378. return $this->data[$offset];
  379. }
  380.  
  381. /**
  382.   * ArrayAccess function for setting a value by its key
  383.   * @param mixed $offset The offset to set.
  384.   * @param mixed $value The value to set.
  385.   **/
  386. public function offsetSet($offset, $value)
  387. {
  388. $this->data[$offset] = $value;
  389. }
  390.  
  391. /**
  392.   * ArrayAccess function for unsetting a value
  393.   * @param mixed $offset The offset to remove.
  394.   **/
  395. public function offsetUnset($offset)
  396. {
  397. unset ($this->data[$offset]);
  398. }
  399.  
  400. }
  401.  
  402.  
  403.