深入研究WordPress核心类WP_Widget如何通过钩子与模板交互

WordPress Widget的钩子与模板交互:一场深度剖析

大家好,今天我们来深入探讨WordPress中 WP_Widget 核心类如何通过钩子与模板进行交互。理解这一机制对于开发自定义Widget,以及更好地控制Widget在主题中的呈现方式至关重要。

1. WP_Widget 类的基本结构

WP_Widget 类是所有Widget的基类。它定义了Widget的基本行为和属性。一个自定义Widget必须继承自这个类,并覆盖其中的一些方法来实现特定的功能。

/**
 * Base class for all Widgets.
 *
 * @since 2.8.0
 */
class WP_Widget {

    /**
     * Root id for all widgets of this type.
     *
     * @since 2.8.0
     * @var string
     */
    public $id_base;

    /**
     * Name of Widget displayed in the admin panel.
     *
     * @since 2.8.0
     * @var string
     */
    public $name;

    /**
     * Option array passed to wp_register_sidebar_widget()
     *
     * @since 2.8.0
     * @var array
     */
    public $widget_options = array();

    /**
     * Option array passed to wp_register_widget_control()
     *
     * @since 2.8.0
     * @var array
     */
    public $control_options = array();

    /**
     * Unique ID number of the widget.
     *
     * @since 2.8.0
     * @var int
     */
    public $number = 0;

    /**
     * Unique ID of the widget.
     *
     * @since 2.8.0
     * @var string
     */
    public $id = '';

    /**
     * Constructor.
     *
     * @since 2.8.0
     *
     * @param string $id_base Optional Base ID for the widget, lowercase and should be unique. If left empty,
     *                        a portion of the class name will be used.
     * @param string $name     Name of the widget displayed in the admin panel.
     * @param array  $widget_options Optional passed to wp_register_sidebar_widget().
     * @param array  $control_options Optional passed to wp_register_widget_control().
     */
    public function __construct( $id_base, $name, $widget_options = array(), $control_options = array() ) {
        $this->id_base = $id_base;
        $this->name = $name;
        $this->widget_options = wp_parse_args( $widget_options, array( 'classname' => $id_base ) );
        $this->control_options = wp_parse_args( $control_options, array( 'width' => 250, 'height' => 200 ) );
    }

    /**
     * Echoes the widget content.
     *
     * Subclasses should over-ride this function to generate their widget code.
     *
     * @since 2.8.0
     *
     * @param array $args     Display arguments including 'before_title', 'after_title',
     *                        'before_widget', and 'after_widget'.
     * @param array $instance The settings for the particular instance of the widget.
     */
    public function widget( $args, $instance ) {
        _deprecated_function( __METHOD__, '4.4.0' );
        echo '<!-- Widget output started -->' . "n";
        echo 'Widget class needs to implement the widget() method.';
        echo '<!-- Widget output ended -->' . "n";
    }

    /**
     * Updates a particular instance of a widget.
     *
     * This function should check that $new_instance is set correctly.
     * The newly calculated value of $instance should be returned.
     * If false is returned, the instance won't be saved/updated.
     *
     * @since 2.8.0
     *
     * @param array $new_instance New settings for this instance as input by the user via
     *                            form().
     * @param array $old_instance Old settings for this instance.
     * @return array|false Settings to save or bool false to cancel saving.
     */
    public function update( $new_instance, $old_instance ) {
        return $new_instance;
    }

    /**
     * Outputs the settings update form.
     *
     * @since 2.8.0
     *
     * @param array $instance Current settings.
     * @return string Default return is 'noform.'.
     */
    public function form( $instance ) {
        echo '<p class="no-options-widget">' . __( 'There are no options for this widget.' ) . '</p>';
        return 'noform.';
    }

    /**
     * Registers the widget with WordPress.
     *
     * Every child class must call this method inside its
     * constructor. This method is the one that triggers all the
     * hooks and filters associated with the Widget API.
     *
     * @since 2.8.0
     *
     * @global WP_Widget_Factory $wp_widget_factory
     *
     * @return bool True if the object is registered, false if there was an error.
     */
    public function register() {
        global $wp_widget_factory;

        if ( isset( $wp_widget_factory->widgets[ $this->id_base ] ) ) {
            _doing_it_wrong(
                __METHOD__,
                sprintf(
                    /* translators: 1: Widget class name, 2: Widget ID base. */
                    __( 'Widget class %1$s has already been registered. Use a different ID base, e.g. `%2$s_2`.' ),
                    get_class( $this ),
                    $this->id_base
                ),
                '4.9.0'
            );
            return false;
        }

        $wp_widget_factory->widgets[ $this->id_base ] = $this;

        /**
         * Fires after a new widget is registered.
         *
         * @since 2.8.0
         *
         * @param WP_Widget $this The widget instance, passed by reference.
         */
        do_action_ref_array( 'widgets_init', array( &$this ) );

        return true;
    }

    /**
     * Displays the form for this widget on the Widgets page.
     *
     * @since 2.8.0
     *
     * @param array $instance Current settings.
     */
    public function _register_one( $number ) {
        $this->number = $number;
        $id_base = $this->id_base;
        $id = $this->id = "$id_base-$number";
        $name = $this->name;

        wp_register_sidebar_widget(
            $id,
            $name,
            array( $this, 'widget' ),
            $this->widget_options,
            array( 'classname' => $this->widget_options['classname'], 'description' => $this->widget_options['description'] ?? '' ) // 'description' was introduced in WP 5.8.0
        );

        wp_register_widget_control(
            $id,
            $name,
            array( $this, 'form' ),
            $this->control_options
        );

        /**
         * Fires when a single widget is registered.
         *
         * @since 2.8.0
         *
         * @param WP_Widget $this The widget instance, passed by reference.
         */
        do_action_ref_array( 'wp_widget_register', array( &$this ) );
    }

    /**
     * Handles changed widgets after a theme switch.
     *
     * Use the 'widget_update_callback' filter to modify the settings.
     *
     * @since 2.8.0
     *
     * @param array $instance Old settings.
     * @param array $new_instance New settings.
     * @return array The new settings.
     */
    public function _get_display_callback( $instance ) {
        return array( $this, 'widget' );
    }

    /**
     * Get the settings for all instances of this widget.
     *
     * @since 2.8.0
     *
     * @return array Multiwidget array of settings.
     */
    public function get_settings() {
        $settings = get_option( 'widget_' . $this->id_base );

        if ( empty( $settings ) || ! is_array( $settings ) ) {
            $settings = array();
        }

        return $settings;
    }

    /**
     * Save the settings for all instances of this widget.
     *
     * @since 2.8.0
     *
     * @param array $settings Multiwidget array of settings.
     */
    public function save_settings( $settings ) {
        update_option( 'widget_' . $this->id_base, $settings );
    }

    /**
     * Number the instance.
     *
     * When loading a particular widget instance, the instance number needs to be tracked.
     *
     * @since 2.8.0
     *
     * @param array|int|false $number Optional. If specified, the number to use.
     *                                Default null, which triggers use of the next available number.
     * @return int|false Instance number. False if $number is not null and is already in use.
     */
    public function number( $number = null ) {
        if ( null === $number ) {
            $settings = $this->get_settings();

            $number = isset( $settings['_multiwidget'] ) ? count( $settings ) : 2;

            while ( isset( $settings[ $number ] ) ) {
                $number++;
            }
        }

        if ( is_numeric( $number ) ) {
            $number = (int) $number;

            $settings = $this->get_settings();

            if ( isset( $settings[ $number ] ) ) {
                return false;
            }

            return $number;
        }

        return false;
    }

    /**
     * Load the widget's translated strings.
     *
     * @since 2.8.0
     */
    public function load_textdomain() {
        load_plugin_textdomain( $this->id_base, false, dirname( plugin_basename( __FILE__ ) ) . '/languages' );
    }

    /**
     * Is this really a multi widget?
     *
     * @since 2.8.0
     *
     * @return bool True if this is a multi widget, false otherwise.
     */
    public function is_multi_widget() {
        $settings = $this->get_settings();
        return isset( $settings['_multiwidget'] ) && $settings['_multiwidget'];
    }

    /**
     * Filters a single instance of a widget's settings.
     *
     * @since 2.8.0
     *
     * @param array      $instance The current widget settings.
     * @param WP_Widget  $this     The current widget instance.
     * @param array|bool $return   The array of filters to apply.
     * @return array An array of widget settings.
     */
    public function _filter_pre_instance( $instance, $return = array() ) {
        /**
         * Filters a single instance of a widget's settings.
         *
         * The dynamic portion of the hook name, `$this->id_base`, refers to the widget's ID base.
         *
         * @since 2.8.0
         *
         * @param array     $instance The current widget settings.
         * @param WP_Widget $this     The current widget instance.
         */
        return apply_filters( "widget_{$this->id_base}_pre_instance", $instance, $this );
    }
}

其中,几个关键的方法包括:

  • __construct(): 构造函数,用于初始化Widget的属性,如 ID、名称和选项。
  • widget(): 最重要的方法。 用于生成Widget的输出内容。这个方法会被主题调用,将Widget的内容显示在侧边栏或其他区域。
  • update(): 用于更新Widget的实例设置。当用户在后台保存Widget设置时,这个方法会被调用,对数据进行验证和清理,并返回更新后的设置。
  • form(): 用于生成Widget在后台的设置表单。用户可以通过这个表单来配置Widget的选项。
  • register(): 用于向WordPress注册Widget,使之能够被使用。

2. Widget的注册过程

Widget的注册是Widget能够被WordPress识别和使用的关键步骤。通常,Widget的注册会在主题的 functions.php 文件或者插件中进行。

注册过程大致如下:

  1. 定义Widget类: 创建一个继承自 WP_Widget 的类,并覆盖 widget(), update(), 和 form() 方法。
  2. 注册Widget: 使用 register_widget() 函数注册Widget类。通常在 widgets_init 钩子上注册。
// 定义Widget类
class My_Custom_Widget extends WP_Widget {

  public function __construct() {
    parent::__construct(
      'my_custom_widget', // Widget ID
      'My Custom Widget', // Widget Name
      array( 'description' => 'A simple custom widget.' ) // Widget Description
    );
  }

  public function widget( $args, $instance ) {
    // Widget output logic
    $title = apply_filters( 'widget_title', $instance['title'] );

    echo $args['before_widget'];
    if ( ! empty( $title ) ) {
      echo $args['before_title'] . $title . $args['after_title'];
    }
    echo '<p>This is my custom widget content.</p>';
    echo $args['after_widget'];
  }

  public function form( $instance ) {
    // Widget settings form in the admin panel
    $title = isset( $instance['title'] ) ? esc_attr( $instance['title'] ) : '';
    ?>
    <p>
      <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label>
      <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo $title; ?>" />
    </p>
    <?php
  }

  public function update( $new_instance, $old_instance ) {
    // Processing widget options on save
    $instance = array();
    $instance['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';
    return $instance;
  }
}

// 注册Widget
function register_my_custom_widget() {
    register_widget( 'My_Custom_Widget' );
}
add_action( 'widgets_init', 'register_my_custom_widget' );

3. widget() 方法与模板文件的交互

widget() 方法是Widget与主题模板交互的核心。当WordPress需要显示一个Widget时,它会调用该Widget的 widget() 方法。

widget() 方法接收两个参数:

  • $args: 一个数组,包含在 register_sidebar() 函数中定义的侧边栏参数,以及一些额外的参数,如 before_widget, after_widget, before_title, after_title
  • $instance: 一个数组,包含Widget的实例设置。这些设置是在后台通过 form() 方法创建的表单进行配置的。

widget() 方法的主要职责是:

  1. 获取Widget的实例设置: 从 $instance 数组中获取用户配置的选项。
  2. 生成Widget的输出内容: 使用 $args 数组中的参数来包装Widget的内容,例如添加标题、边框等。
  3. 输出Widget的内容: 使用 echo 函数将Widget的内容输出到页面上。
public function widget( $args, $instance ) {
    $title = apply_filters( 'widget_title', $instance['title'] ); // 获取标题,并应用 'widget_title' 过滤器
    $content = isset( $instance['content'] ) ? $instance['content'] : 'Default Content';

    echo $args['before_widget']; // 输出widget容器的开始标签
    if ( ! empty( $title ) ) {
        echo $args['before_title'] . $title . $args['after_title']; // 输出标题
    }
    echo '<div class="widget-content">' . $content . '</div>'; // 输出widget内容
    echo $args['after_widget']; // 输出widget容器的结束标签
}

4. 关键的钩子 (Hooks)

WP_Widget 类和相关的函数提供了一系列的钩子,允许开发者在不同的阶段修改Widget的行为和输出。这些钩子可以分为以下几类:

  • widget_title 过滤器: 用于修改Widget的标题。
  • widget_{$this->id_base}_pre_instance 过滤器: 用于在保存Widget实例之前修改实例数据。 $this->id_base 是widget的id_base.
  • dynamic_sidebar_params 过滤器: 用于修改传递给 dynamic_sidebar() 函数的参数,这会影响到传递给每个Widget的 $args 参数。
  • widget_types_to_hide_from_legacy_widget_block过滤器: 用于控制widget是否在区块编辑器中显示。

下面将更详细的介绍这些钩子:

4.1 widget_title 过滤器

这个过滤器允许你修改Widget的标题。它接收两个参数:

  • $title: Widget的原始标题。
  • $instance: Widget的实例设置。
  • $id_base: Widget的ID base.
add_filter( 'widget_title', 'my_custom_widget_title', 10, 3 );
function my_custom_widget_title( $title, $instance = array(), $id_base = null ) {
  // 修改标题的逻辑
  if($id_base == 'my_custom_widget') { //只对特定的widget ID生效
    $title = 'Custom: ' . $title;
  }
  return $title;
}

4.2 widget_{$this->id_base}_pre_instance 过滤器

这个过滤器允许你在保存Widget实例之前修改实例数据。 这对于验证和清理用户输入非常有用。 $this->id_base 会被替换为widget的id_base。

add_filter( 'widget_my_custom_widget_pre_instance', 'my_custom_widget_pre_instance', 10, 2 );
function my_custom_widget_pre_instance( $instance, $widget ) {
  // 修改实例数据的逻辑
  $instance['title'] = sanitize_text_field( $instance['title'] ); // 清理标题
  return $instance;
}

4.3 dynamic_sidebar_params 过滤器

dynamic_sidebar_params 过滤器允许你修改传递给 dynamic_sidebar() 函数的参数。 这可以用于全局修改所有Widget的 $args 参数,从而影响所有Widget的输出。

add_filter( 'dynamic_sidebar_params', 'my_custom_sidebar_params' );
function my_custom_sidebar_params( $params ) {
  // 修改侧边栏参数的逻辑
  $params[0]['before_widget'] = '<div class="custom-widget-wrapper">';
  $params[0]['after_widget'] = '</div>';
  return $params;
}

4.4 widget_types_to_hide_from_legacy_widget_block过滤器

此过滤器允许你从旧版widget块中隐藏特定的widget类型。 如果你希望强制用户使用块编辑器中的新widget块,或者widget与块编辑器不兼容,这将非常有用。

add_filter( 'widget_types_to_hide_from_legacy_widget_block', 'my_hide_widget_from_block_editor' );

function my_hide_widget_from_block_editor( $widget_types ) {
  // 将要隐藏的widget的id_base添加到数组中
  $widget_types[] = 'my_custom_widget';
  return $widget_types;
}

5. 自定义模板的使用

虽然 widget() 方法通常直接生成Widget的输出,但在某些情况下,你可能希望使用单独的模板文件来组织Widget的HTML结构。

你可以通过以下步骤来实现:

  1. 创建模板文件: 在你的主题或插件目录中创建一个模板文件,例如 my-custom-widget.php
  2. widget() 方法中包含模板文件: 使用 includerequire 函数在 widget() 方法中包含模板文件,并将 $args$instance 变量传递给模板文件。
public function widget( $args, $instance ) {
    $title = apply_filters( 'widget_title', $instance['title'] );
    $content = isset( $instance['content'] ) ? $instance['content'] : 'Default Content';

    // 将变量传递给模板文件
    $widget_data = array(
        'args' => $args,
        'instance' => $instance,
        'title' => $title,
        'content' => $content,
    );

    // 包含模板文件
    include( get_template_directory() . '/my-custom-widget.php' ); // 假设模板文件在主题根目录下
}

my-custom-widget.php 模板文件中,你可以使用 $args, $instance, $title$content 变量来生成Widget的HTML结构。

<?php
// my-custom-widget.php

echo $args['before_widget'];
if ( ! empty( $title ) ) {
    echo $args['before_title'] . $title . $args['after_title'];
}
?>
<div class="widget-content">
    <?php echo $content; ?>
</div>
<?php
echo $args['after_widget'];
?>

6. 实例:一个显示最近文章的Widget

为了更好地理解Widget的钩子和模板交互,我们来看一个显示最近文章的Widget的例子。

class Recent_Posts_Widget extends WP_Widget {

    public function __construct() {
        parent::__construct(
            'recent_posts_widget',
            'Recent Posts',
            array( 'description' => 'Displays recent posts.' )
        );
    }

    public function widget( $args, $instance ) {
        $title = apply_filters( 'widget_title', $instance['title'] );
        $number = isset( $instance['number'] ) ? intval( $instance['number'] ) : 5;

        $query_args = array(
            'posts_per_page' => $number,
            'orderby' => 'date',
            'order' => 'DESC',
        );

        $recent_posts = new WP_Query( $query_args );

        echo $args['before_widget'];
        if ( ! empty( $title ) ) {
            echo $args['before_title'] . $title . $args['after_title'];
        }

        if ( $recent_posts->have_posts() ) {
            echo '<ul>';
            while ( $recent_posts->have_posts() ) {
                $recent_posts->the_post();
                echo '<li><a href="' . get_permalink() . '">' . get_the_title() . '</a></li>';
            }
            echo '</ul>';
            wp_reset_postdata();
        } else {
            echo '<p>No posts found.</p>';
        }

        echo $args['after_widget'];
    }

    public function form( $instance ) {
        $title = isset( $instance['title'] ) ? esc_attr( $instance['title'] ) : '';
        $number = isset( $instance['number'] ) ? intval( $instance['number'] ) : 5;
        ?>
        <p>
            <label for="<?php echo $this->get_field_id( 'title' ); ?>"><?php _e( 'Title:' ); ?></label>
            <input class="widefat" id="<?php echo $this->get_field_id( 'title' ); ?>" name="<?php echo $this->get_field_name( 'title' ); ?>" type="text" value="<?php echo $title; ?>" />
        </p>
        <p>
            <label for="<?php echo $this->get_field_id( 'number' ); ?>"><?php _e( 'Number of posts:' ); ?></label>
            <input id="<?php echo $this->get_field_id( 'number' ); ?>" name="<?php echo $this->get_field_name( 'number' ); ?>" type="number" value="<?php echo $number; ?>" size="3" />
        </p>
        <?php
    }

    public function update( $new_instance, $old_instance ) {
        $instance = array();
        $instance['title'] = ( ! empty( $new_instance['title'] ) ) ? strip_tags( $new_instance['title'] ) : '';
        $instance['number'] = ( ! empty( $new_instance['number'] ) ) ? intval( $new_instance['number'] ) : 5;
        return $instance;
    }
}

function register_recent_posts_widget() {
    register_widget( 'Recent_Posts_Widget' );
}
add_action( 'widgets_init', 'register_recent_posts_widget' );

在这个例子中,widget() 方法使用 WP_Query 类获取最近的文章,并使用 <ul><li> 标签来显示文章列表。form() 方法允许用户配置Widget的标题和显示的文章数量。

7. 表格总结

方法/钩子 描述 参数 用途
__construct() Widget类的构造函数,用于初始化Widget的属性。 $id_base (ID), $name (名称), $widget_options (Widget选项), $control_options (控制选项) 设置Widget的基本信息,例如ID、名称和一些默认选项。
widget() 核心方法,用于生成Widget的输出内容。主题会调用此方法来显示Widget。 $args (侧边栏参数), $instance (Widget实例设置) 生成Widget的HTML代码,并将其输出到页面上。这是Widget与主题模板交互的主要方式。
update() 用于更新Widget的实例设置。当用户在后台保存Widget设置时,此方法会被调用。 $new_instance (新设置), $old_instance (旧设置) 验证和清理用户输入,并返回更新后的设置。
form() 用于生成Widget在后台的设置表单。用户可以通过此表单来配置Widget的选项。 $instance (当前设置) 创建一个HTML表单,允许用户配置Widget的选项。
register_widget() 用于向WordPress注册Widget类。 Widget类名 将Widget类注册到WordPress,使其能够在后台的Widget管理界面中使用。
widgets_init 钩子 在所有Widget注册后触发的钩子。通常用于注册Widget类。 注册自定义Widget。
widget_title 过滤器 用于修改Widget的标题。 $title (原始标题), $instance (Widget实例设置), $id_base (Widget ID base) 修改Widget的标题,例如添加前缀或后缀。
widget_{$this->id_base}_pre_instance 过滤器 在保存Widget实例之前修改实例数据。 $instance (原始实例数据), $widget (Widget对象) 验证和清理用户输入,并修改Widget的实例设置。
dynamic_sidebar_params 过滤器 修改传递给 dynamic_sidebar() 函数的参数,从而影响每个widget的 $args 参数。 $params (侧边栏参数) 全局修改所有Widget的 $args 参数,例如修改widget的容器标签。
widget_types_to_hide_from_legacy_widget_block过滤器 从旧版widget块中隐藏特定的widget类型。 $widget_types (要隐藏的widget类型数组) 控制widget是否在区块编辑器中显示。

8. 总结

我们深入了解了 WP_Widget 类的结构、Widget的注册过程、widget() 方法与模板文件的交互,以及关键的钩子。 希望通过本文,你能更好地理解WordPress Widget的工作原理,并能够开发出更强大、更灵活的自定义Widget。

Widget的交互核心在于widget()方法,通过钩子可以灵活地修改Widget的行为和输出,而自定义模板则提供了更好的组织和控制HTML结构的方式。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注