class-wp-walker.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. <?php
  2. /**
  3. * A class for displaying various tree-like structures.
  4. *
  5. * Extend the Walker class to use it, see examples below. Child classes
  6. * do not need to implement all of the abstract methods in the class. The child
  7. * only needs to implement the methods that are needed.
  8. *
  9. * @since 2.1.0
  10. *
  11. * @package WordPress
  12. * @abstract
  13. */
  14. #[AllowDynamicProperties]
  15. class Walker {
  16. /**
  17. * What the class handles.
  18. *
  19. * @since 2.1.0
  20. * @var string
  21. */
  22. public $tree_type;
  23. /**
  24. * DB fields to use.
  25. *
  26. * @since 2.1.0
  27. * @var string[]
  28. */
  29. public $db_fields;
  30. /**
  31. * Max number of pages walked by the paged walker.
  32. *
  33. * @since 2.7.0
  34. * @var int
  35. */
  36. public $max_pages = 1;
  37. /**
  38. * Whether the current element has children or not.
  39. *
  40. * To be used in start_el().
  41. *
  42. * @since 4.0.0
  43. * @var bool
  44. */
  45. public $has_children;
  46. /**
  47. * Starts the list before the elements are added.
  48. *
  49. * The $args parameter holds additional values that may be used with the child
  50. * class methods. This method is called at the start of the output list.
  51. *
  52. * @since 2.1.0
  53. * @abstract
  54. *
  55. * @param string $output Used to append additional content (passed by reference).
  56. * @param int $depth Depth of the item.
  57. * @param array $args An array of additional arguments.
  58. */
  59. public function start_lvl( &$output, $depth = 0, $args = array() ) {}
  60. /**
  61. * Ends the list of after the elements are added.
  62. *
  63. * The $args parameter holds additional values that may be used with the child
  64. * class methods. This method finishes the list at the end of output of the elements.
  65. *
  66. * @since 2.1.0
  67. * @abstract
  68. *
  69. * @param string $output Used to append additional content (passed by reference).
  70. * @param int $depth Depth of the item.
  71. * @param array $args An array of additional arguments.
  72. */
  73. public function end_lvl( &$output, $depth = 0, $args = array() ) {}
  74. /**
  75. * Starts the element output.
  76. *
  77. * The $args parameter holds additional values that may be used with the child
  78. * class methods. Also includes the element output.
  79. *
  80. * @since 2.1.0
  81. * @since 5.9.0 Renamed `$object` (a PHP reserved keyword) to `$data_object` for PHP 8 named parameter support.
  82. * @abstract
  83. *
  84. * @param string $output Used to append additional content (passed by reference).
  85. * @param object $data_object The data object.
  86. * @param int $depth Depth of the item.
  87. * @param array $args An array of additional arguments.
  88. * @param int $current_object_id Optional. ID of the current item. Default 0.
  89. */
  90. public function start_el( &$output, $data_object, $depth = 0, $args = array(), $current_object_id = 0 ) {}
  91. /**
  92. * Ends the element output, if needed.
  93. *
  94. * The $args parameter holds additional values that may be used with the child class methods.
  95. *
  96. * @since 2.1.0
  97. * @since 5.9.0 Renamed `$object` (a PHP reserved keyword) to `$data_object` for PHP 8 named parameter support.
  98. * @abstract
  99. *
  100. * @param string $output Used to append additional content (passed by reference).
  101. * @param object $data_object The data object.
  102. * @param int $depth Depth of the item.
  103. * @param array $args An array of additional arguments.
  104. */
  105. public function end_el( &$output, $data_object, $depth = 0, $args = array() ) {}
  106. /**
  107. * Traverses elements to create list from elements.
  108. *
  109. * Display one element if the element doesn't have any children otherwise,
  110. * display the element and its children. Will only traverse up to the max
  111. * depth and no ignore elements under that depth. It is possible to set the
  112. * max depth to include all depths, see walk() method.
  113. *
  114. * This method should not be called directly, use the walk() method instead.
  115. *
  116. * @since 2.5.0
  117. *
  118. * @param object $element Data object.
  119. * @param array $children_elements List of elements to continue traversing (passed by reference).
  120. * @param int $max_depth Max depth to traverse.
  121. * @param int $depth Depth of current element.
  122. * @param array $args An array of arguments.
  123. * @param string $output Used to append additional content (passed by reference).
  124. */
  125. public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
  126. if ( ! $element ) {
  127. return;
  128. }
  129. $id_field = $this->db_fields['id'];
  130. $id = $element->$id_field;
  131. // Display this element.
  132. $this->has_children = ! empty( $children_elements[ $id ] );
  133. if ( isset( $args[0] ) && is_array( $args[0] ) ) {
  134. $args[0]['has_children'] = $this->has_children; // Back-compat.
  135. }
  136. $this->start_el( $output, $element, $depth, ...array_values( $args ) );
  137. // Descend only when the depth is right and there are children for this element.
  138. if ( ( 0 == $max_depth || $max_depth > $depth + 1 ) && isset( $children_elements[ $id ] ) ) {
  139. foreach ( $children_elements[ $id ] as $child ) {
  140. if ( ! isset( $newlevel ) ) {
  141. $newlevel = true;
  142. // Start the child delimiter.
  143. $this->start_lvl( $output, $depth, ...array_values( $args ) );
  144. }
  145. $this->display_element( $child, $children_elements, $max_depth, $depth + 1, $args, $output );
  146. }
  147. unset( $children_elements[ $id ] );
  148. }
  149. if ( isset( $newlevel ) && $newlevel ) {
  150. // End the child delimiter.
  151. $this->end_lvl( $output, $depth, ...array_values( $args ) );
  152. }
  153. // End this element.
  154. $this->end_el( $output, $element, $depth, ...array_values( $args ) );
  155. }
  156. /**
  157. * Displays array of elements hierarchically.
  158. *
  159. * Does not assume any existing order of elements.
  160. *
  161. * $max_depth = -1 means flatly display every element.
  162. * $max_depth = 0 means display all levels.
  163. * $max_depth > 0 specifies the number of display levels.
  164. *
  165. * @since 2.1.0
  166. * @since 5.3.0 Formalized the existing `...$args` parameter by adding it
  167. * to the function signature.
  168. *
  169. * @param array $elements An array of elements.
  170. * @param int $max_depth The maximum hierarchical depth.
  171. * @param mixed ...$args Optional additional arguments.
  172. * @return string The hierarchical item output.
  173. */
  174. public function walk( $elements, $max_depth, ...$args ) {
  175. $output = '';
  176. // Invalid parameter or nothing to walk.
  177. if ( $max_depth < -1 || empty( $elements ) ) {
  178. return $output;
  179. }
  180. $parent_field = $this->db_fields['parent'];
  181. // Flat display.
  182. if ( -1 == $max_depth ) {
  183. $empty_array = array();
  184. foreach ( $elements as $e ) {
  185. $this->display_element( $e, $empty_array, 1, 0, $args, $output );
  186. }
  187. return $output;
  188. }
  189. /*
  190. * Need to display in hierarchical order.
  191. * Separate elements into two buckets: top level and children elements.
  192. * Children_elements is two dimensional array. Example:
  193. * Children_elements[10][] contains all sub-elements whose parent is 10.
  194. */
  195. $top_level_elements = array();
  196. $children_elements = array();
  197. foreach ( $elements as $e ) {
  198. if ( empty( $e->$parent_field ) ) {
  199. $top_level_elements[] = $e;
  200. } else {
  201. $children_elements[ $e->$parent_field ][] = $e;
  202. }
  203. }
  204. /*
  205. * When none of the elements is top level.
  206. * Assume the first one must be root of the sub elements.
  207. */
  208. if ( empty( $top_level_elements ) ) {
  209. $first = array_slice( $elements, 0, 1 );
  210. $root = $first[0];
  211. $top_level_elements = array();
  212. $children_elements = array();
  213. foreach ( $elements as $e ) {
  214. if ( $root->$parent_field == $e->$parent_field ) {
  215. $top_level_elements[] = $e;
  216. } else {
  217. $children_elements[ $e->$parent_field ][] = $e;
  218. }
  219. }
  220. }
  221. foreach ( $top_level_elements as $e ) {
  222. $this->display_element( $e, $children_elements, $max_depth, 0, $args, $output );
  223. }
  224. /*
  225. * If we are displaying all levels, and remaining children_elements is not empty,
  226. * then we got orphans, which should be displayed regardless.
  227. */
  228. if ( ( 0 == $max_depth ) && count( $children_elements ) > 0 ) {
  229. $empty_array = array();
  230. foreach ( $children_elements as $orphans ) {
  231. foreach ( $orphans as $op ) {
  232. $this->display_element( $op, $empty_array, 1, 0, $args, $output );
  233. }
  234. }
  235. }
  236. return $output;
  237. }
  238. /**
  239. * Produces a page of nested elements.
  240. *
  241. * Given an array of hierarchical elements, the maximum depth, a specific page number,
  242. * and number of elements per page, this function first determines all top level root elements
  243. * belonging to that page, then lists them and all of their children in hierarchical order.
  244. *
  245. * $max_depth = 0 means display all levels.
  246. * $max_depth > 0 specifies the number of display levels.
  247. *
  248. * @since 2.7.0
  249. * @since 5.3.0 Formalized the existing `...$args` parameter by adding it
  250. * to the function signature.
  251. *
  252. * @param array $elements An array of elements.
  253. * @param int $max_depth The maximum hierarchical depth.
  254. * @param int $page_num The specific page number, beginning with 1.
  255. * @param int $per_page Number of elements per page.
  256. * @param mixed ...$args Optional additional arguments.
  257. * @return string XHTML of the specified page of elements.
  258. */
  259. public function paged_walk( $elements, $max_depth, $page_num, $per_page, ...$args ) {
  260. if ( empty( $elements ) || $max_depth < -1 ) {
  261. return '';
  262. }
  263. $output = '';
  264. $parent_field = $this->db_fields['parent'];
  265. $count = -1;
  266. if ( -1 == $max_depth ) {
  267. $total_top = count( $elements );
  268. }
  269. if ( $page_num < 1 || $per_page < 0 ) {
  270. // No paging.
  271. $paging = false;
  272. $start = 0;
  273. if ( -1 == $max_depth ) {
  274. $end = $total_top;
  275. }
  276. $this->max_pages = 1;
  277. } else {
  278. $paging = true;
  279. $start = ( (int) $page_num - 1 ) * (int) $per_page;
  280. $end = $start + $per_page;
  281. if ( -1 == $max_depth ) {
  282. $this->max_pages = ceil( $total_top / $per_page );
  283. }
  284. }
  285. // Flat display.
  286. if ( -1 == $max_depth ) {
  287. if ( ! empty( $args[0]['reverse_top_level'] ) ) {
  288. $elements = array_reverse( $elements );
  289. $oldstart = $start;
  290. $start = $total_top - $end;
  291. $end = $total_top - $oldstart;
  292. }
  293. $empty_array = array();
  294. foreach ( $elements as $e ) {
  295. $count++;
  296. if ( $count < $start ) {
  297. continue;
  298. }
  299. if ( $count >= $end ) {
  300. break;
  301. }
  302. $this->display_element( $e, $empty_array, 1, 0, $args, $output );
  303. }
  304. return $output;
  305. }
  306. /*
  307. * Separate elements into two buckets: top level and children elements.
  308. * Children_elements is two dimensional array, e.g.
  309. * $children_elements[10][] contains all sub-elements whose parent is 10.
  310. */
  311. $top_level_elements = array();
  312. $children_elements = array();
  313. foreach ( $elements as $e ) {
  314. if ( empty( $e->$parent_field ) ) {
  315. $top_level_elements[] = $e;
  316. } else {
  317. $children_elements[ $e->$parent_field ][] = $e;
  318. }
  319. }
  320. $total_top = count( $top_level_elements );
  321. if ( $paging ) {
  322. $this->max_pages = ceil( $total_top / $per_page );
  323. } else {
  324. $end = $total_top;
  325. }
  326. if ( ! empty( $args[0]['reverse_top_level'] ) ) {
  327. $top_level_elements = array_reverse( $top_level_elements );
  328. $oldstart = $start;
  329. $start = $total_top - $end;
  330. $end = $total_top - $oldstart;
  331. }
  332. if ( ! empty( $args[0]['reverse_children'] ) ) {
  333. foreach ( $children_elements as $parent => $children ) {
  334. $children_elements[ $parent ] = array_reverse( $children );
  335. }
  336. }
  337. foreach ( $top_level_elements as $e ) {
  338. $count++;
  339. // For the last page, need to unset earlier children in order to keep track of orphans.
  340. if ( $end >= $total_top && $count < $start ) {
  341. $this->unset_children( $e, $children_elements );
  342. }
  343. if ( $count < $start ) {
  344. continue;
  345. }
  346. if ( $count >= $end ) {
  347. break;
  348. }
  349. $this->display_element( $e, $children_elements, $max_depth, 0, $args, $output );
  350. }
  351. if ( $end >= $total_top && count( $children_elements ) > 0 ) {
  352. $empty_array = array();
  353. foreach ( $children_elements as $orphans ) {
  354. foreach ( $orphans as $op ) {
  355. $this->display_element( $op, $empty_array, 1, 0, $args, $output );
  356. }
  357. }
  358. }
  359. return $output;
  360. }
  361. /**
  362. * Calculates the total number of root elements.
  363. *
  364. * @since 2.7.0
  365. *
  366. * @param array $elements Elements to list.
  367. * @return int Number of root elements.
  368. */
  369. public function get_number_of_root_elements( $elements ) {
  370. $num = 0;
  371. $parent_field = $this->db_fields['parent'];
  372. foreach ( $elements as $e ) {
  373. if ( empty( $e->$parent_field ) ) {
  374. $num++;
  375. }
  376. }
  377. return $num;
  378. }
  379. /**
  380. * Unsets all the children for a given top level element.
  381. *
  382. * @since 2.7.0
  383. *
  384. * @param object $element The top level element.
  385. * @param array $children_elements The children elements.
  386. */
  387. public function unset_children( $element, &$children_elements ) {
  388. if ( ! $element || ! $children_elements ) {
  389. return;
  390. }
  391. $id_field = $this->db_fields['id'];
  392. $id = $element->$id_field;
  393. if ( ! empty( $children_elements[ $id ] ) && is_array( $children_elements[ $id ] ) ) {
  394. foreach ( (array) $children_elements[ $id ] as $child ) {
  395. $this->unset_children( $child, $children_elements );
  396. }
  397. }
  398. unset( $children_elements[ $id ] );
  399. }
  400. }