类文档

Walker

💡 云策文档标注

概述

Walker 是 WordPress 中用于遍历和渲染树状数据结构的抽象类,自 WordPress 2.1 引入。开发者需扩展此类并实现必要的抽象方法,以处理如导航菜单、页面分类等层级数据。它支持作为回调函数使用(如 wp_nav_menu())或手动调用,是自定义菜单输出等场景的核心工具。

关键要点

  • Walker 是一个抽象类,用于遍历树状结构(如数组或对象),并在每个节点执行抽象方法以生成 HTML 输出。
  • 扩展 Walker 时,必须设置 $db_fields 属性来指定父 ID 和项目 ID 的字段名,其他属性如 $tree_type 和 $max_pages 可选。
  • 常见用法包括作为 wp_nav_menu() 的回调,或通过 walk() 和 paged_walk() 方法手动调用自定义 Walker 类。
  • 抽象方法如 start_el()、end_el()、start_lvl() 和 end_lvl() 需在子类中实现,以控制输出结构;其他方法如 display_element() 和 walk() 提供遍历逻辑。
  • Walker 基于 PHP4 设计,方法为隐式抽象,子类无需实现所有方法,只需覆盖所需部分。

代码示例

class Walker_Quickstart_Menu extends Walker {
    var $db_fields = array(
        'parent' => 'menu_item_parent',
        'id'     => 'db_id'
    );

    function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
        $output .= sprintf( "<li><a href='%s'%s>%s</a></li>n",
            esc_url( $item->url ),
            ( $item->object_id === get_the_ID() ) ? esc_attr( ' class="current"' ) : '',
            esc_html( $item->title )
        );
    }
}

注意事项

  • 设置 $db_fields 时,确保键 'parent' 和 'id' 的值对应数据对象的属性名,通常与数据库字段相关。
  • 对于常见用例(如自定义菜单),可考虑扩展 Walker_Nav_Menu 等现有子类,以避免手动定义 $db_fields。
  • 调用 walk() 方法时,参数 $max_depth 控制遍历深度:-1 表示扁平显示所有元素,0 表示显示所有层级,大于 0 指定层级数。

📄 原文内容

A class for displaying various tree-like structures.

Description

Extend the Walker class to use it, see examples below. Child classes do not need to implement all of the abstract methods in the class. The child only needs to implement the methods that are needed.

More Information

The Walker class was implemented in WordPress 2.1 to provide developers with a means to traverse tree-like data structures for the purpose of rendering HTML.

Tree-Like Structures

In terms of web development, a tree-like structure is an array or object with hierarchical data – such that it can be visually represented with a root element and subtrees of children.

Examples of WordPress objects with data that are structured in a “tree-like” way include navigational menus, page categories, and breadcrumbs.

Role of Walker

Walker is an abstract class. In order to be useful the class must be extended and any necessary abstract methods defined (see “Abstract Methods” below for more).

The class itself simply “walks” through each node in a tree (e.g. an object or associative array) and executes an abstract function at each node. In order to take an action at one of these nodes, a developer must define those abstract methods within a custom child class.

Although the Walker class has many uses, one of the most common usages by developers is outputting HTML for custom menus (usually ones that have been defined using the Appearance Menus screen in the Administration Screens).

Abstraction Note: The Walker class was created prior to PHP5 and so does not make use of PHP5’s explicit abstraction keywords or features. In this case, the class and its methods are implicitly abstract (PHP4 compatible) and not explicitly abstract (PHP5 compatible). Developers are not required to implement any methods of the class, and may use or override only those methods that are needed. If you chose not to extend a specific abstract method, that method will simply do nothing.

Methods & Properties

Please refer source code for the complete lists of methods and properties. Below description may cover some of them.

Properties

Note that the properties of the Walker class are intended to be set by the extending class and probably should not vary over the lifetime of an instance.

$db_fields
Required. Because Walker can take any type of tree object, you need to specify what object properties the Walker class should treat as parent id and item id (usually these are the names of database fields, hence the property name). This property must be an associative array with two keys: 'parent' and 'id'. The value of each key should be the names of the object properties that hold the parent id and item id, respectively.
$tree_type
Optional. The Walker class itself makes no use of this value, although it may be useful to developers. Internally, WordPress’s own extended Walker classes will set this to values like ‘category’ or ‘page’.
$max_pages
Optional. The maximum number of pages walked by the paged walker.

Usage

There are two general use-cases for the Walker class.

Usage as a Callback

Some WordPress APIs and functions ( such as wp_nav_menu() ) allow developers to specify a custom Walker class as a callback. This is the most common usage of the Walker class by developers.

In this scenario, the class is automatically passed a tree of elements. When creating a custom walker for this scenario, you will generally only need to define the abstract methods needed to create the kind of structure you want. Everything else is handled automatically for you.

Custom Usage

It is also possible to call your custom Walker classes manually. This is particularly useful for plugin developers.

In this scenario, you can initiate the walker by calling either the walk() or paged_walk() method of your child class, with the appropriate parameters.

Methods

Name Description
Walker::display_element Traverses elements to create list from elements.
Walker::end_el Ends the element output, if needed.
Walker::end_lvl Ends the list of after the elements are added.
Walker::get_number_of_root_elements Calculates the total number of root elements.
Walker::paged_walk Produces a page of nested elements.
Walker::start_el Starts the element output.
Walker::start_lvl Starts the list before the elements are added.
Walker::unset_children Unsets all the children for a given top level element.
Walker::walk Displays array of elements hierarchically.

Source

class Walker {
	/**
	 * What the class handles.
	 *
	 * @since 2.1.0
	 * @var string
	 */
	public $tree_type;

	/**
	 * DB fields to use.
	 *
	 * @since 2.1.0
	 * @var string[]
	 */
	public $db_fields;

	/**
	 * Max number of pages walked by the paged walker.
	 *
	 * @since 2.7.0
	 * @var int
	 */
	public $max_pages = 1;

	/**
	 * Whether the current element has children or not.
	 *
	 * To be used in start_el().
	 *
	 * @since 4.0.0
	 * @var bool
	 */
	public $has_children;

	/**
	 * Starts the list before the elements are added.
	 *
	 * The $args parameter holds additional values that may be used with the child
	 * class methods. This method is called at the start of the output list.
	 *
	 * @since 2.1.0
	 * @abstract
	 *
	 * @param string $output Used to append additional content (passed by reference).
	 * @param int    $depth  Depth of the item.
	 * @param array  $args   An array of additional arguments.
	 */
	public function start_lvl( &$output, $depth = 0, $args = array() ) {}

	/**
	 * Ends the list of after the elements are added.
	 *
	 * The $args parameter holds additional values that may be used with the child
	 * class methods. This method finishes the list at the end of output of the elements.
	 *
	 * @since 2.1.0
	 * @abstract
	 *
	 * @param string $output Used to append additional content (passed by reference).
	 * @param int    $depth  Depth of the item.
	 * @param array  $args   An array of additional arguments.
	 */
	public function end_lvl( &$output, $depth = 0, $args = array() ) {}

	/**
	 * Starts the element output.
	 *
	 * The $args parameter holds additional values that may be used with the child
	 * class methods. Also includes the element output.
	 *
	 * @since 2.1.0
	 * @since 5.9.0 Renamed `$object` (a PHP reserved keyword) to `$data_object` for PHP 8 named parameter support.
	 * @abstract
	 *
	 * @param string $output            Used to append additional content (passed by reference).
	 * @param object $data_object       The data object.
	 * @param int    $depth             Depth of the item.
	 * @param array  $args              An array of additional arguments.
	 * @param int    $current_object_id Optional. ID of the current item. Default 0.
	 */
	public function start_el( &$output, $data_object, $depth = 0, $args = array(), $current_object_id = 0 ) {}

	/**
	 * Ends the element output, if needed.
	 *
	 * The $args parameter holds additional values that may be used with the child class methods.
	 *
	 * @since 2.1.0
	 * @since 5.9.0 Renamed `$object` (a PHP reserved keyword) to `$data_object` for PHP 8 named parameter support.
	 * @abstract
	 *
	 * @param string $output      Used to append additional content (passed by reference).
	 * @param object $data_object The data object.
	 * @param int    $depth       Depth of the item.
	 * @param array  $args        An array of additional arguments.
	 */
	public function end_el( &$output, $data_object, $depth = 0, $args = array() ) {}

	/**
	 * Traverses elements to create list from elements.
	 *
	 * Display one element if the element doesn't have any children otherwise,
	 * display the element and its children. Will only traverse up to the max
	 * depth and no ignore elements under that depth. It is possible to set the
	 * max depth to include all depths, see walk() method.
	 *
	 * This method should not be called directly, use the walk() method instead.
	 *
	 * @since 2.5.0
	 *
	 * @param object $element           Data object.
	 * @param array  $children_elements List of elements to continue traversing (passed by reference).
	 * @param int    $max_depth         Max depth to traverse.
	 * @param int    $depth             Depth of current element.
	 * @param array  $args              An array of arguments.
	 * @param string $output            Used to append additional content (passed by reference).
	 */
	public function display_element( $element, &$children_elements, $max_depth, $depth, $args, &$output ) {
		if ( ! $element ) {
			return;
		}

		$max_depth = (int) $max_depth;
		$depth     = (int) $depth;

		$id_field = $this->db_fields['id'];
		$id       = $element->$id_field;

		// Display this element.
		$this->has_children = ! empty( $children_elements[ $id ] );
		if ( isset( $args[0] ) && is_array( $args[0] ) ) {
			$args[0]['has_children'] = $this->has_children; // Back-compat.
		}

		$this->start_el( $output, $element, $depth, ...array_values( $args ) );

		// Descend only when the depth is right and there are children for this element.
		if ( ( 0 === $max_depth || $max_depth > $depth + 1 ) && isset( $children_elements[ $id ] ) ) {

			foreach ( $children_elements[ $id ] as $child ) {

				if ( ! isset( $newlevel ) ) {
					$newlevel = true;
					// Start the child delimiter.
					$this->start_lvl( $output, $depth, ...array_values( $args ) );
				}
				$this->display_element( $child, $children_elements, $max_depth, $depth + 1, $args, $output );
			}
			unset( $children_elements[ $id ] );
		}

		if ( isset( $newlevel ) && $newlevel ) {
			// End the child delimiter.
			$this->end_lvl( $output, $depth, ...array_values( $args ) );
		}

		// End this element.
		$this->end_el( $output, $element, $depth, ...array_values( $args ) );
	}

	/**
	 * Displays array of elements hierarchically.
	 *
	 * Does not assume any existing order of elements.
	 *
	 * $max_depth = -1 means flatly display every element.
	 * $max_depth = 0 means display all levels.
	 * $max_depth > 0 specifies the number of display levels.
	 *
	 * @since 2.1.0
	 * @since 5.3.0 Formalized the existing `...$args` parameter by adding it
	 *              to the function signature.
	 *
	 * @param array $elements  An array of elements.
	 * @param int   $max_depth The maximum hierarchical depth.
	 * @param mixed ...$args   Optional additional arguments.
	 * @return string The hierarchical item output.
	 */
	public function walk( $elements, $max_depth, ...$args ) {
		$output = '';

		$max_depth = (int) $max_depth;

		// Invalid parameter or nothing to walk.
		if ( $max_depth < -1 || empty( $elements ) ) {
			return $output;
		}

		$parent_field = $this->db_fields['parent'];

		// Flat display.
		if ( -1 === $max_depth ) {
			$empty_array = array();
			foreach ( $elements as $e ) {
				$this->display_element( $e, $empty_array, 1, 0, $args, $output );
			}
			return $output;
		}

		/*
		 * Need to display in hierarchical order.
		 * Separate elements into two buckets: top level and children elements.
		 * Children_elements is two dimensional array. Example:
		 * Children_elements[10][] contains all sub-elements whose parent is 10.
		 */
		$top_level_elements = array();
		$children_elements  = array();
		foreach ( $elements as $e ) {
			if ( empty( $e->$parent_field ) ) {
				$top_level_elements[] = $e;
			} else {
				$children_elements[ $e->$parent_field ][] = $e;
			}
		}

		/*
		 * When none of the elements is top level.
		 * Assume the first one must be root of the sub elements.
		 */
		if ( empty( $top_level_elements ) ) {

			$first = array_slice( $elements, 0, 1 );
			$root  = $first[0];

			$top_level_elements = array();
			$children_elements  = array();
			foreach ( $elements as $e ) {
				if ( $root->$parent_field === $e->$parent_field ) {
					$top_level_elements[] = $e;
				} else {
					$children_elements[ $e->$parent_field ][] = $e;
				}
			}
		}

		foreach ( $top_level_elements as $e ) {
			$this->display_element( $e, $children_elements, $max_depth, 0, $args, $output );
		}

		/*
		 * If we are displaying all levels, and remaining children_elements is not empty,
		 * then we got orphans, which should be displayed regardless.
		 */
		if ( ( 0 === $max_depth ) && count( $children_elements ) > 0 ) {
			$empty_array = array();
			foreach ( $children_elements as $orphans ) {
				foreach ( $orphans as $op ) {
					$this->display_element( $op, $empty_array, 1, 0, $args, $output );
				}
			}
		}

		return $output;
	}

	/**
	 * Produces a page of nested elements.
	 *
	 * Given an array of hierarchical elements, the maximum depth, a specific page number,
	 * and number of elements per page, this function first determines all top level root elements
	 * belonging to that page, then lists them and all of their children in hierarchical order.
	 *
	 * $max_depth = 0 means display all levels.
	 * $max_depth > 0 specifies the number of display levels.
	 *
	 * @since 2.7.0
	 * @since 5.3.0 Formalized the existing `...$args` parameter by adding it
	 *              to the function signature.
	 *
	 * @param array $elements  An array of elements.
	 * @param int   $max_depth The maximum hierarchical depth.
	 * @param int   $page_num  The specific page number, beginning with 1.
	 * @param int   $per_page  Number of elements per page.
	 * @param mixed ...$args   Optional additional arguments.
	 * @return string XHTML of the specified page of elements.
	 */
	public function paged_walk( $elements, $max_depth, $page_num, $per_page, ...$args ) {
		$output = '';

		$max_depth = (int) $max_depth;

		if ( empty( $elements ) || $max_depth < -1 ) {
			return $output;
		}

		$parent_field = $this->db_fields['parent'];

		$count = -1;
		if ( -1 === $max_depth ) {
			$total_top = count( $elements );
		}
		if ( $page_num < 1 || $per_page < 0 ) {
			// No paging.
			$paging = false;
			$start  = 0;
			if ( -1 === $max_depth ) {
				$end = $total_top;
			}
			$this->max_pages = 1;
		} else {
			$paging = true;
			$start  = ( (int) $page_num - 1 ) * (int) $per_page;
			$end    = $start + $per_page;
			if ( -1 === $max_depth ) {
				$this->max_pages = (int) ceil( $total_top / $per_page );
			}
		}

		// Flat display.
		if ( -1 === $max_depth ) {
			if ( ! empty( $args[0]['reverse_top_level'] ) ) {
				$elements = array_reverse( $elements );
				$oldstart = $start;
				$start    = $total_top - $end;
				$end      = $total_top - $oldstart;
			}

			$empty_array = array();
			foreach ( $elements as $e ) {
				++$count;
				if ( $count < $start ) {
					continue;
				}
				if ( $count >= $end ) {
					break;
				}
				$this->display_element( $e, $empty_array, 1, 0, $args, $output );
			}
			return $output;
		}

		/*
		 * Separate elements into two buckets: top level and children elements.
		 * Children_elements is two dimensional array, e.g.
		 * $children_elements[10][] contains all sub-elements whose parent is 10.
		 */
		$top_level_elements = array();
		$children_elements  = array();
		foreach ( $elements as $e ) {
			if ( empty( $e->$parent_field ) ) {
				$top_level_elements[] = $e;
			} else {
				$children_elements[ $e->$parent_field ][] = $e;
			}
		}

		$total_top = count( $top_level_elements );
		if ( $paging ) {
			$this->max_pages = (int) ceil( $total_top / $per_page );
		} else {
			$end = $total_top;
		}

		if ( ! empty( $args[0]['reverse_top_level'] ) ) {
			$top_level_elements = array_reverse( $top_level_elements );
			$oldstart           = $start;
			$start              = $total_top - $end;
			$end                = $total_top - $oldstart;
		}
		if ( ! empty( $args[0]['reverse_children'] ) ) {
			foreach ( $children_elements as $parent => $children ) {
				$children_elements[ $parent ] = array_reverse( $children );
			}
		}

		foreach ( $top_level_elements as $e ) {
			++$count;

			// For the last page, need to unset earlier children in order to keep track of orphans.
			if ( $end >= $total_top && $count < $start ) {
					$this->unset_children( $e, $children_elements );
			}

			if ( $count < $start ) {
				continue;
			}

			if ( $count >= $end ) {
				break;
			}

			$this->display_element( $e, $children_elements, $max_depth, 0, $args, $output );
		}

		if ( $end >= $total_top && count( $children_elements ) > 0 ) {
			$empty_array = array();
			foreach ( $children_elements as $orphans ) {
				foreach ( $orphans as $op ) {
					$this->display_element( $op, $empty_array, 1, 0, $args, $output );
				}
			}
		}

		return $output;
	}

	/**
	 * Calculates the total number of root elements.
	 *
	 * @since 2.7.0
	 *
	 * @param array $elements Elements to list.
	 * @return int Number of root elements.
	 */
	public function get_number_of_root_elements( $elements ) {
		$num          = 0;
		$parent_field = $this->db_fields['parent'];

		foreach ( $elements as $e ) {
			if ( empty( $e->$parent_field ) ) {
				++$num;
			}
		}
		return $num;
	}

	/**
	 * Unsets all the children for a given top level element.
	 *
	 * @since 2.7.0
	 *
	 * @param object $element           The top level element.
	 * @param array  $children_elements The children elements.
	 */
	public function unset_children( $element, &$children_elements ) {
		if ( ! $element || ! $children_elements ) {
			return;
		}

		$id_field = $this->db_fields['id'];
		$id       = $element->$id_field;

		if ( ! empty( $children_elements[ $id ] ) && is_array( $children_elements[ $id ] ) ) {
			foreach ( (array) $children_elements[ $id ] as $child ) {
				$this->unset_children( $child, $children_elements );
			}
		}

		unset( $children_elements[ $id ] );
	}
}

Changelog

Version Description
2.1.0 Introduced.

User Contributed Notes

  1. Skip to note 5 content

    Tip: In this case, you could choose to extend Walker_Nav_Menu instead of Walker, and then you wouldn’t need to define $db_fields manually.

    In order to utilize this custom walker class, you would call wp_nav_menu() (likely from within a theme file) and pass it a new instance of the custom Walker child class.

    <ul>
         2, //menu id
            'walker'  => new Walker_Quickstart_Menu() //use our custom walker
        ));
        ?>
    </ul>

  2. Skip to note 6 content

    From Codex

    Using Walker Manually

    This example will cover how to initialize a custom Walker manually. In this example, our goal is to render the same menu as in the previous example. We will use the same Walker class as above, but without using the callback feature of wp_nav_menu() .

    walk( $menu_items, -1 ) );

  3. Skip to note 7 content

    This example shows one of the simplest (and most common) implementations of the walker class. In this case, the Walker will be used to generate a custom menu in combination with wp_nav_menu() . The first block shows the example Walker child class, the second block demonstrates how this class is utilized.

    class Walker_Quickstart_Menu extends Walker {
    
        // Tell Walker where to inherit it's parent and id values
        var $db_fields = array(
            'parent' => 'menu_item_parent', 
            'id'     => 'db_id' 
        );
    
        /**
         * At the start of each element, output a <li> and <a> tag structure.
         * 
         * Note: Menu objects include url and title properties, so we will use those.
         */
        function start_el( &$output, $item, $depth = 0, $args = array(), $id = 0 ) {
            $output .= sprintf( "n<li><a href='%1$s'%2$s>%3$s</a></li>n",
                esc_url( $item->url ),
                ( $item->object_id === get_the_ID() ) ? esc_attr( ' class="current"' ) : '',
                esc_html( $item->title )
            );
        }
    
    }

  4. Skip to note 8 content

    This example would shows how you might set up the Walker’s $db_fields property for handling a tree of page objects. Since we know the parent and id properties for all post objects (pages included), we just need to match those up using the Walker’s $db_fields property. Like so…

    class Walker_Page extends Walker {
        var $db_fields = array (
            'parent' => 'post_parent', 
            'id'     => 'ID'
        );
    
        // define abstract methods here
    }