WP_List_Table 类:后台列表与分页逻辑深度剖析
大家好,今天我们来深入探讨 WordPress 中 WP_List_Table
类,这个类是构建 WordPress 后台列表页面的核心工具,它负责生成列表的结构、处理分页逻辑,并提供各种增强列表功能的钩子。我们将从基础用法开始,逐步分析其内部机制,并通过实例代码展示如何利用它构建自定义列表。
1. WP_List_Table
类的基本概念
WP_List_Table
类是一个抽象类,位于 wp-admin/includes/class-wp-list-table.php
文件中。要使用它,我们需要创建一个子类,并实现一些关键的抽象方法。该类的主要职责包括:
- 数据展示: 负责从数据库或其他数据源获取数据,并将其格式化为列表的形式。
- 列定义: 定义列表中显示的列,包括列标题、数据提取方式和排序方式。
- 分页处理: 生成分页导航,并处理用户点击分页链接时的请求。
- 批量操作: 允许用户选择多个条目,并对它们执行批量操作,例如删除或更改状态。
- 搜索过滤: 提供搜索框,允许用户根据关键词过滤列表。
2. 创建自定义 WP_List_Table
子类
首先,我们需要创建一个类来继承 WP_List_Table
。假设我们要创建一个显示书籍信息的列表,我们可以这样开始:
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
class Book_List_Table extends WP_List_Table {
/**
* 构造函数,设置列表的一些基本属性
*/
public function __construct() {
parent::__construct( array(
'singular' => 'book', // 单数形式的名称,用于 URL 参数等
'plural' => 'books', // 复数形式的名称
'ajax' => false // 是否启用 AJAX 分页,这里先设为 false
) );
}
// ... 更多方法将在后面实现 ...
}
这段代码首先检查 WP_List_Table
类是否已加载,如果没有,则引入它。然后,我们创建了一个名为 Book_List_Table
的类,并继承了 WP_List_Table
。在构造函数中,我们调用了父类的构造函数,并传递了一个包含 singular
、plural
和 ajax
属性的数组。singular
和 plural
用于生成 URL 参数和显示信息,ajax
用于控制是否使用 AJAX 分页。
3. 定义列:get_columns()
接下来,我们需要定义列表中显示的列。我们需要重写 get_columns()
方法来实现:
/**
* 定义列
*
* @return array 列的数组,键是列的 ID(用于排序等),值是列的标题
*/
public function get_columns() {
$columns = array(
'cb' => '<input type="checkbox" />', // 用于批量操作的选择框
'title' => '书名',
'author' => '作者',
'isbn' => 'ISBN',
'price' => '价格'
);
return $columns;
}
get_columns()
方法返回一个关联数组,键是列的 ID,值是列的标题。cb
列是特殊列,用于显示批量操作的选择框。
4. 准备数据:prepare_items()
prepare_items()
方法是 WP_List_Table
类中最重要的方法之一。它负责从数据库或其他数据源获取数据,并将其格式化为列表的形式。
/**
* 准备列表数据
*/
public function prepare_items() {
$columns = $this->get_columns();
$hidden = array(); // 隐藏的列,默认为空
$sortable = $this->get_sortable_columns(); // 可排序的列
$this->_column_headers = array( $columns, $hidden, $sortable );
// 获取数据
$data = $this->get_book_data();
// 处理排序
usort( $data, array( $this, 'usort_reorder' ) );
// 处理分页
$per_page = $this->get_items_per_page( 'books_per_page', 10 ); // 每页显示 10 条数据
$current_page = $this->get_pagenum();
$total_items = count( $data );
$data = array_slice( $data, ( ( $current_page - 1 ) * $per_page ), $per_page );
$this->items = $data;
// 设置分页参数
$this->set_pagination_args( array(
'total_items' => $total_items, // 总条目数
'per_page' => $per_page, // 每页显示条目数
'total_pages' => ceil( $total_items / $per_page ) // 总页数
) );
}
让我们逐步分析这段代码:
- 获取列信息: 首先,我们获取列的定义、隐藏列和可排序的列。
- 设置列头: 我们将这些信息存储在
$this->_column_headers
属性中,WP_List_Table
类会使用这些信息来生成列头。 - 获取数据: 我们调用
get_book_data()
方法来获取书籍数据。这个方法需要我们自己实现,它应该从数据库或其他数据源获取数据。 - 处理排序: 我们使用
usort()
函数对数据进行排序,排序的依据是用户点击的列头。usort_reorder()
方法将在后面实现。 - 处理分页:
- 我们使用
get_items_per_page()
方法获取每页显示的条目数,默认值为 10。 - 我们使用
get_pagenum()
方法获取当前页码。 - 我们使用
array_slice()
函数截取当前页的数据。 - 我们将截取后的数据存储在
$this->items
属性中,WP_List_Table
类会使用这些数据来生成列表内容。
- 我们使用
- 设置分页参数: 我们使用
set_pagination_args()
方法设置分页参数,包括总条目数、每页显示的条目数和总页数。这些参数会被WP_List_Table
类用来生成分页导航。
5. 实现 get_book_data()
:模拟数据源
get_book_data()
方法需要我们自己实现,它负责从数据库或其他数据源获取数据。为了演示,我们可以创建一个模拟的数据源:
/**
* 获取书籍数据(模拟)
*
* @return array 书籍数据的数组
*/
private function get_book_data() {
$data = array(
array(
'id' => 1,
'title' => '百年孤独',
'author' => '加西亚·马尔克斯',
'isbn' => '978-7-5063-4168-3',
'price' => 29.80
),
array(
'id' => 2,
'title' => '红楼梦',
'author' => '曹雪芹',
'isbn' => '978-7-02-000467-7',
'price' => 39.00
),
array(
'id' => 3,
'title' => '三国演义',
'author' => '罗贯中',
'isbn' => '978-7-02-000468-4',
'price' => 45.00
),
array(
'id' => 4,
'title' => '水浒传',
'author' => '施耐庵',
'isbn' => '978-7-02-000469-1',
'price' => 35.00
),
array(
'id' => 5,
'title' => '西游记',
'author' => '吴承恩',
'isbn' => '978-7-02-000470-7',
'price' => 42.00
),
// ... 更多书籍数据 ...
);
// 增加更多模拟数据,超过 10 条,方便测试分页
for ($i = 6; $i <= 25; $i++) {
$data[] = array(
'id' => $i,
'title' => '书籍 ' . $i,
'author' => '作者 ' . $i,
'isbn' => 'ISBN ' . $i,
'price' => rand(20, 50) + 0.99
);
}
return $data;
}
这个方法返回一个包含书籍数据的数组。每个书籍数据都是一个关联数组,包含 id
、title
、author
、isbn
和 price
属性。
6. 实现列的显示:column_default()
和 column_{column_name}()
我们需要实现 column_default()
方法来处理默认列的显示,并实现 column_{column_name}()
方法来处理特定列的显示。
/**
* 处理默认列的显示
*
* @param array $item 当前行的数据
* @param string $column_name 列的 ID
*
* @return mixed 列的内容
*/
public function column_default( $item, $column_name ) {
switch ( $column_name ) {
case 'isbn':
case 'price':
return $item[ $column_name ];
default:
return '<em>暂无内容</em>'; // 如果没有匹配的列,显示 "暂无内容"
}
}
/**
* 处理 "title" 列的显示
*
* @param array $item 当前行的数据
*
* @return string 列的内容
*/
public function column_title( $item ) {
return '<strong>' . $item['title'] . '</strong>';
}
/**
* 处理 "author" 列的显示
*
* @param array $item 当前行的数据
*
* @return string 列的内容
*/
public function column_author( $item ) {
return '<em>' . $item['author'] . '</em>';
}
column_default()
方法接收两个参数:当前行的数据和列的 ID。它根据列的 ID 返回列的内容。column_{column_name}()
方法是特定列的显示方法,其中 {column_name}
是列的 ID。例如,column_title()
方法用于显示 title
列的内容。
7. 实现批量操作的选择框:column_cb()
我们需要实现 column_cb()
方法来显示批量操作的选择框:
/**
* 处理 "cb" 列的显示 (批量操作选择框)
*
* @param array $item 当前行的数据
*
* @return string 选择框的 HTML 代码
*/
public function column_cb( $item ) {
return sprintf(
'<input type="checkbox" name="book[]" value="%s" />', $item['id']
);
}
/**
* 定义批量操作
*
* @return array 批量操作的数组,键是操作的 ID,值是操作的标题
*/
public function get_bulk_actions() {
$actions = array(
'delete' => '删除'
);
return $actions;
}
column_cb()
方法返回一个 HTML 代码,用于显示选择框。选择框的 name
属性是 book[]
,value
属性是书籍的 ID。get_bulk_actions()
方法返回一个数组,定义了批量操作。键是操作的 ID,值是操作的标题。
8. 实现排序:get_sortable_columns()
和 usort_reorder()
我们需要实现 get_sortable_columns()
方法来定义可排序的列,并实现 usort_reorder()
方法来处理排序:
/**
* 定义可排序的列
*
* @return array 可排序列的数组,键是列的 ID,值是一个包含 'orderby' 和 'order' 元素的数组
*/
public function get_sortable_columns() {
$sortable_columns = array(
'title' => array( 'title', true ), // true 表示默认升序
'author' => array( 'author', false ), // false 表示默认降序
'price' => array( 'price', false )
);
return $sortable_columns;
}
/**
* 处理排序
*
* @param array $a 第一个元素
* @param array $b 第二个元素
*
* @return int 排序结果
*/
public function usort_reorder( $a, $b ) {
// 如果没有设置排序参数,则不排序
$orderby = ( ! empty( $_GET['orderby'] ) ) ? $_GET['orderby'] : 'title';
$order = ( ! empty( $_GET['order'] ) ) ? $_GET['order'] : 'asc';
$result = strcmp( $a[ $orderby ], $b[ $orderby ] );
return ( ( $order === 'asc' ) ? $result : -$result );
}
get_sortable_columns()
方法返回一个数组,定义了可排序的列。键是列的 ID,值是一个包含 orderby
和 order
元素的数组。orderby
元素是用于排序的字段,order
元素表示默认的排序方式(true
表示升序,false
表示降序)。
usort_reorder()
方法接收两个参数:要比较的两个元素。它首先获取排序的字段和排序方式,然后使用 strcmp()
函数比较两个元素的指定字段的值。最后,它根据排序方式返回排序结果。
9. 显示列表:display()
现在,我们已经实现了 WP_List_Table
子类的所有必要方法。我们可以使用 display()
方法来显示列表:
/**
* 显示列表
*/
public function display() {
// 添加一个搜索框
echo '<form method="get">';
echo '<input type="hidden" name="page" value="' . $_REQUEST['page'] . '" />'; // 非常重要,保持页面上下文
$this->search_box( 'search', 'search_id' ); // 'search' 是搜索框的文本,'search_id' 是搜索框的 ID
echo '</form>';
parent::display();
}
在 display()
方法中,我们首先添加了一个搜索框,然后调用了父类的 display()
方法来显示列表。
10. 注册列表页面
最后,我们需要创建一个页面来显示列表。我们可以使用 add_menu_page()
函数来注册一个菜单项,并在该菜单项对应的页面上显示列表:
add_action( 'admin_menu', 'register_book_list_page' );
function register_book_list_page() {
add_menu_page(
'书籍列表', // 页面标题
'书籍管理', // 菜单标题
'manage_options', // 权限
'book-list', // 菜单 slug
'book_list_page_callback' // 回调函数
);
}
function book_list_page_callback() {
$book_list_table = new Book_List_Table();
$book_list_table->prepare_items();
echo '<div class="wrap">';
echo '<h2>书籍列表</h2>';
$book_list_table->display();
echo '</div>';
}
这段代码首先使用 add_action()
函数注册一个 admin_menu
钩子,并在该钩子上注册一个名为 register_book_list_page()
的函数。register_book_list_page()
函数使用 add_menu_page()
函数注册一个菜单项,该菜单项对应的页面会调用 book_list_page_callback()
函数。book_list_page_callback()
函数创建了一个 Book_List_Table
对象,调用其 prepare_items()
方法准备数据,然后调用其 display()
方法显示列表。
11. 处理批量操作:process_bulk_action()
我们需要添加一个方法来处理批量操作。
/**
* 处理批量操作
*/
public function process_bulk_action() {
// 如果用户没有选择任何条目,则直接返回
if ( 'delete' === $this->current_action() ) {
$ids = isset( $_REQUEST['book'] ) ? $_REQUEST['book'] : array();
if ( empty( $ids ) ) {
return;
}
// 确认是数组
if ( ! is_array( $ids ) ) {
$ids = array( $ids );
}
// 这里可以添加删除书籍的逻辑,例如:
// foreach ( $ids as $id ) {
// delete_book( $id );
// }
// 添加一个通知,显示删除成功
echo '<div class="updated"><p>成功删除 ' . count( $ids ) . ' 本书籍!</p></div>';
}
}
在 book_list_page_callback()
函数中,我们需要在调用 $book_list_table->display()
之前调用 $book_list_table->process_bulk_action()
。
function book_list_page_callback() {
$book_list_table = new Book_List_Table();
$book_list_table->process_bulk_action(); // 处理批量操作
$book_list_table->prepare_items();
echo '<div class="wrap">';
echo '<h2>书籍列表</h2>';
$book_list_table->display();
echo '</div>';
}
12. 实现搜索:search_box()
我们已经在display()
方法中添加了搜索框,现在我们需要修改prepare_items()
方法来处理搜索:
/**
* 准备列表数据
*/
public function prepare_items() {
$columns = $this->get_columns();
$hidden = array(); // 隐藏的列,默认为空
$sortable = $this->get_sortable_columns(); // 可排序的列
$this->_column_headers = array( $columns, $hidden, $sortable );
// 获取数据
$data = $this->get_book_data();
// 处理搜索
$search_term = isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '';
if ( ! empty( $search_term ) ) {
$data = array_filter( $data, function ( $item ) use ( $search_term ) {
// 在标题、作者和 ISBN 中搜索
return (
stripos( $item['title'], $search_term ) !== false ||
stripos( $item['author'], $search_term ) !== false ||
stripos( $item['isbn'], $search_term ) !== false
);
} );
}
// 处理排序
usort( $data, array( $this, 'usort_reorder' ) );
// 处理分页
$per_page = $this->get_items_per_page( 'books_per_page', 10 ); // 每页显示 10 条数据
$current_page = $this->get_pagenum();
$total_items = count( $data );
$data = array_slice( $data, ( ( $current_page - 1 ) * $per_page ), $per_page );
$this->items = $data;
// 设置分页参数
$this->set_pagination_args( array(
'total_items' => $total_items, // 总条目数
'per_page' => $per_page, // 每页显示条目数
'total_pages' => ceil( $total_items / $per_page ) // 总页数
) );
}
我们添加了一段代码来处理搜索。首先,我们从 $_REQUEST
数组中获取搜索关键词,并使用 sanitize_text_field()
函数对其进行过滤。然后,我们使用 array_filter()
函数过滤数据,只保留包含搜索关键词的条目。
13. 总结
我们详细介绍了 WP_List_Table
类的使用方法,包括创建子类、定义列、准备数据、处理排序、处理分页、显示列表、处理批量操作和实现搜索。通过这些步骤,我们可以构建自定义的 WordPress 后台列表页面。
14. 进一步探索与优化
以上代码提供了一个 WP_List_Table
的基本实现。在实际应用中,可以进一步优化和扩展,例如:
- AJAX 分页: 使用 AJAX 加载分页数据,提高用户体验。
- 自定义列操作: 添加自定义列操作,例如编辑或删除按钮。
- 更复杂的数据源: 从 WordPress 数据库或其他 API 获取数据。
- 更强大的搜索过滤: 使用更高级的搜索算法和过滤器。
- 使用 Transients 缓存数据: 缓存数据,减少数据库查询。
15. 代码示例
完整的 Book_List_Table
类代码如下:
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
class Book_List_Table extends WP_List_Table {
/**
* 构造函数,设置列表的一些基本属性
*/
public function __construct() {
parent::__construct( array(
'singular' => 'book', // 单数形式的名称,用于 URL 参数等
'plural' => 'books', // 复数形式的名称
'ajax' => false // 是否启用 AJAX 分页,这里先设为 false
) );
}
/**
* 定义列
*
* @return array 列的数组,键是列的 ID(用于排序等),值是列的标题
*/
public function get_columns() {
$columns = array(
'cb' => '<input type="checkbox" />', // 用于批量操作的选择框
'title' => '书名',
'author' => '作者',
'isbn' => 'ISBN',
'price' => '价格'
);
return $columns;
}
/**
* 定义可排序的列
*
* @return array 可排序列的数组,键是列的 ID,值是一个包含 'orderby' 和 'order' 元素的数组
*/
public function get_sortable_columns() {
$sortable_columns = array(
'title' => array( 'title', true ), // true 表示默认升序
'author' => array( 'author', false ), // false 表示默认降序
'price' => array( 'price', false )
);
return $sortable_columns;
}
/**
* 准备列表数据
*/
public function prepare_items() {
$columns = $this->get_columns();
$hidden = array(); // 隐藏的列,默认为空
$sortable = $this->get_sortable_columns(); // 可排序的列
$this->_column_headers = array( $columns, $hidden, $sortable );
// 处理批量操作
$this->process_bulk_action();
// 获取数据
$data = $this->get_book_data();
// 处理搜索
$search_term = isset( $_REQUEST['s'] ) ? sanitize_text_field( $_REQUEST['s'] ) : '';
if ( ! empty( $search_term ) ) {
$data = array_filter( $data, function ( $item ) use ( $search_term ) {
// 在标题、作者和 ISBN 中搜索
return (
stripos( $item['title'], $search_term ) !== false ||
stripos( $item['author'], $search_term ) !== false ||
stripos( $item['isbn'], $search_term ) !== false
);
} );
}
// 处理排序
usort( $data, array( $this, 'usort_reorder' ) );
// 处理分页
$per_page = $this->get_items_per_page( 'books_per_page', 10 ); // 每页显示 10 条数据
$current_page = $this->get_pagenum();
$total_items = count( $data );
$data = array_slice( $data, ( ( $current_page - 1 ) * $per_page ), $per_page );
$this->items = $data;
// 设置分页参数
$this->set_pagination_args( array(
'total_items' => $total_items, // 总条目数
'per_page' => $per_page, // 每页显示条目数
'total_pages' => ceil( $total_items / $per_page ) // 总页数
) );
}
/**
* 获取书籍数据(模拟)
*
* @return array 书籍数据的数组
*/
private function get_book_data() {
$data = array(
array(
'id' => 1,
'title' => '百年孤独',
'author' => '加西亚·马尔克斯',
'isbn' => '978-7-5063-4168-3',
'price' => 29.80
),
array(
'id' => 2,
'title' => '红楼梦',
'author' => '曹雪芹',
'isbn' => '978-7-02-000467-7',
'price' => 39.00
),
array(
'id' => 3,
'title' => '三国演义',
'author' => '罗贯中',
'isbn' => '978-7-02-000468-4',
'price' => 45.00
),
array(
'id' => 4,
'title' => '水浒传',
'author' => '施耐庵',
'isbn' => '978-7-02-000469-1',
'price' => 35.00
),
array(
'id' => 5,
'title' => '西游记',
'author' => '吴承恩',
'isbn' => '978-7-02-000470-7',
'price' => 42.00
),
// ... 更多书籍数据 ...
);
// 增加更多模拟数据,超过 10 条,方便测试分页
for ($i = 6; $i <= 25; $i++) {
$data[] = array(
'id' => $i,
'title' => '书籍 ' . $i,
'author' => '作者 ' . $i,
'isbn' => 'ISBN ' . $i,
'price' => rand(20, 50) + 0.99
);
}
return $data;
}
/**
* 处理默认列的显示
*
* @param array $item 当前行的数据
* @param string $column_name 列的 ID
*
* @return mixed 列的内容
*/
public function column_default( $item, $column_name ) {
switch ( $column_name ) {
case 'isbn':
case 'price':
return $item[ $column_name ];
default:
return '<em>暂无内容</em>'; // 如果没有匹配的列,显示 "暂无内容"
}
}
/**
* 处理 "title" 列的显示
*
* @param array $item 当前行的数据
*
* @return string 列的内容
*/
public function column_title( $item ) {
return '<strong>' . $item['title'] . '</strong>';
}
/**
* 处理 "author" 列的显示
*
* @param array $item 当前行的数据
*
* @return string 列的内容
*/
public function column_author( $item ) {
return '<em>' . $item['author'] . '</em>';
}
/**
* 处理 "cb" 列的显示 (批量操作选择框)
*
* @param array $item 当前行的数据
*
* @return string 选择框的 HTML 代码
*/
public function column_cb( $item ) {
return sprintf(
'<input type="checkbox" name="book[]" value="%s" />', $item['id']
);
}
/**
* 定义批量操作
*
* @return array 批量操作的数组,键是操作的 ID,值是操作的标题
*/
public function get_bulk_actions() {
$actions = array(
'delete' => '删除'
);
return $actions;
}
/**
* 处理批量操作
*/
public function process_bulk_action() {
// 如果用户没有选择任何条目,则直接返回
if ( 'delete' === $this->current_action() ) {
$ids = isset( $_REQUEST['book'] ) ? $_REQUEST['book'] : array();
if ( empty( $ids ) ) {
return;
}
// 确认是数组
if ( ! is_array( $ids ) ) {
$ids = array( $ids );
}
// 这里可以添加删除书籍的逻辑,例如:
// foreach ( $ids as $id ) {
// delete_book( $id );
// }
// 添加一个通知,显示删除成功
echo '<div class="updated"><p>成功删除 ' . count( $ids ) . ' 本书籍!</p></div>';
}
}
/**
* 处理排序
*
* @param array $a 第一个元素
* @param array $b 第二个元素
*
* @return int 排序结果
*/
public function usort_reorder( $a, $b ) {
// 如果没有设置排序参数,则不排序
$orderby = ( ! empty( $_GET['orderby'] ) ) ? $_GET['orderby'] : 'title';
$order = ( ! empty( $_GET['order'] ) ) ? $_GET['order'] : 'asc';
$result = strcmp( $a[ $orderby ], $b[ $orderby ] );
return ( ( $order === 'asc' ) ? $result : -$result );
}
/**
* 显示列表
*/
public function display() {
// 添加一个搜索框
echo '<form method="get">';
echo '<input type="hidden" name="page" value="' . $_REQUEST['page'] . '" />'; // 非常重要,保持页面上下文
$this->search_box( 'search', 'search_id' ); // 'search' 是搜索框的文本,'search_id' 是搜索框的 ID
echo '</form>';
parent::display();
}
}
这段代码提供了一个完整的 Book_List_Table
类的实现,包括定义列、准备数据、处理排序、处理分页、显示列表、处理批量操作和实现搜索。
16. 将代码整合到 WordPress 中
将以上代码保存为一个 PHP 文件(例如 book-list-table.php
),并将其放置在你的 WordPress 主题目录或插件目录中。然后,在你的主题的 functions.php
文件或插件的主文件中引入该文件:
require_once get_template_directory() . '/book-list-table.php'; // 如果在主题目录中
// 或者
require_once plugin_dir_path( __FILE__ ) . 'book-list-table.php'; // 如果在插件目录中
接下来,将注册列表页面的代码添加到你的主题的 functions.php
文件或插件的主文件中。
现在,你应该可以在 WordPress 后台看到一个名为 "书籍管理" 的菜单项,点击该菜单项即可看到书籍列表。
17. 掌握核心逻辑,灵活应对各种场景
WP_List_Table
类提供了强大的列表构建功能,掌握其核心逻辑,我们就能灵活应对各种 WordPress 后台列表的需求,构建功能丰富、用户友好的管理界面。