深入理解 `wp_die()` 函数的源码,以及它如何通过 `wp_die_handler` 过滤器允许开发者自定义错误页面。

各位码友们,晚上好!欢迎来到“WordPress底层探秘”小课堂。今晚咱们要聊聊WordPress里一个“一言不合就罢工”的函数——wp_die(),以及它背后的“背锅侠”——wp_die_handler 过滤器。

如果你写WordPress插件或者主题的时候遇到过“啊!页面白屏了,只显示一堆错误信息!”的场景,那么你肯定跟 wp_die() 打过交道。这玩意儿就像个紧急刹车,一旦触发,WordPress就会立刻停止执行,并显示错误信息。但别怕,它不是个蛮不讲理的家伙,它给了我们一个机会,通过 wp_die_handler 过滤器,来定制我们自己的错误页面,让用户体验更上一层楼。

废话不多说,咱们直接上代码,深入了解一下 wp_die() 的源码:

/**
 * Kills WordPress execution and displays an HTML page with an error message.
 *
 * If `$title` is empty, a generic title is used.
 *
 * The error message is HTML-encoded for safe usage in HTML page templates.
 *
 * This function should be called when WordPress is unable to continue running
 * due to an error.
 *
 * @since 2.0.0
 *
 * @global WP_Error $wp_error WordPress error object.
 *
 * @param string|WP_Error $message Error message. May contain HTML.
 * @param string          $title   Optional. Error title. Default empty.
 * @param array|string    $args    Optional. Array or string of arguments. See wp_die().
 * @return void
 */
function wp_die( $message, $title = '', $args = array() ) {
    global $wp_error;

    $defaults = array(
        'response'  => 500,
        'code'      => '',
        'heading'   => '',
        'exit'      => true,
        'back_link' => false,
    );

    $args = wp_parse_args( $args, $defaults );

    $response = $args['response'];

    // Use WP_Error, if possible.
    if ( is_wp_error( $message ) ) {
        if ( empty( $title ) ) {
            $errors = $message->get_error_messages();
            if ( empty( $errors ) ) {
                $title = __( 'WordPress › Error' );
            } else {
                $title = sprintf( __( 'WordPress › Error: %s' ), $message->get_error_code() );
            }
        }

        $errors = $message->get_error_messages();
        if ( ! empty( $errors ) ) {
            $message = '<ul><li>' . implode( '</li><li>', $errors ) . '</li></ul>';
        }
    } elseif ( is_string( $message ) ) {
        $message = sprintf( '<p>%s</p>', $message );
    }

    if ( defined( 'XMLRPC_REQUEST' ) || defined( 'REST_REQUEST' ) ) {
        wp_die( $message, $title, array_merge( $args, array( 'exit' => false ) ) ); // Recursive call.
    }

    if ( function_exists( 'is_admin' ) ) {
        if ( is_admin() ) {
            if ( did_action( 'admin_enqueue_scripts' ) ) {
                wp_enqueue_style( 'wp-admin-css' );
                wp_enqueue_style( 'colors-fresh' );
            } else {
                add_action(
                    'admin_enqueue_scripts',
                    function() {
                        wp_enqueue_style( 'wp-admin-css' );
                        wp_enqueue_style( 'colors-fresh' );
                    }
                );
            }
        }
    }

    $have_gettext = function_exists( '__' );

    if ( ! empty( $title ) ) {
        $title = strip_tags( $title );
        if ( $have_gettext ) {
            $title = __( $title );
        }
    }

    $heading = $args['heading'];
    if ( empty( $heading ) ) {
        $heading = $title;
    }

    if ( ! did_action( 'wp_die_handler' ) ) {
        if ( WP_DEBUG && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) && apply_filters( 'wp_die_ajax_backcompat', true ) ) {
            /**
             * Fires before outputting an error message.
             *
             * @since 2.0.0
             */
            do_action( 'admin_head' );

            if ( 'E_ALL' === error_reporting() ) {
                $error_reporting_string = __( 'E_ALL' );
            } else {
                $error_reporting_string = error_reporting();
            }

            $message = sprintf(
                '<div class="error"><p>%s</p><p>%s</p><p>%s</p></div>',
                __( 'WordPress has encountered a problem.' ),
                sprintf(
                    /* translators: 1: Documentation URL, 2: support forums URL */
                    __( 'For more information, visit the <a href="%1$s">Debugging in WordPress</a> page or the <a href="%2$s">WordPress Support Forums</a>.' ),
                    __( 'https://wordpress.org/documentation/article/debugging-in-wordpress/' ),
                    __( 'https://wordpress.org/support/forums/' )
                ),
                sprintf(
                    /* translators: %s: error reporting value */
                    __( 'Error reporting has been turned on. %s' ),
                    sprintf( '<code>%s</code>', $error_reporting_string )
                )
            ) . $message;
        }

        /**
         * Filters the callback used to handle calls to wp_die().
         *
         * @since 2.0.0
         *
         * @param callable $callback The callback function used to handle calls to wp_die().
         *                           Default 'wp_default_die_handler'.
         */
        $handler = apply_filters( 'wp_die_handler', '_default_wp_die_handler' );
        call_user_func( $handler, $message, $title, $args );
    }

    if ( $args['exit'] ) {
        die();
    }
}

咱们来逐行分析一下这个“刹车”是怎么工作的:

  1. 函数签名: function wp_die( $message, $title = '', $args = array() )

    • $message: 错误信息,可以是字符串或者 WP_Error 对象。 这是必填的,告诉用户哪里出错了。
    • $title: 错误页面的标题,可选。 如果为空,会使用默认标题。
    • $args: 一个数组,包含一些额外的参数,可选。 稍后会详细介绍。
  2. 默认参数:

    $defaults = array(
        'response'  => 500,
        'code'      => '',
        'heading'   => '',
        'exit'      => true,
        'back_link' => false,
    );

    wp_die() 函数允许我们通过 $args 数组传递一些参数来控制错误页面的行为。默认参数如下:

    参数名 默认值 说明
    response 500 HTTP 响应码。 500 表示服务器内部错误。你可以改成其他合适的响应码,例如 403 表示禁止访问,404 表示未找到。
    code '' 错误代码,字符串类型,用于标识错误的类型。例如 ‘invalid_username’。这个参数并没有被WordPress核心直接使用,但是可以在自定义的错误处理函数中使用。
    heading '' 错误页面的标题。 如果为空,会使用 $title 参数的值。
    exit true 是否立即停止执行脚本。 如果设置为 falsewp_die() 会执行错误处理逻辑,但不会调用 die() 函数停止脚本。这在某些特殊情况下很有用。
    back_link false 是否显示一个返回链接。 如果设置为 true,会显示一个返回到前一页的链接。
  3. 处理 WP_Error 对象:

    if ( is_wp_error( $message ) ) {
        if ( empty( $title ) ) {
            $errors = $message->get_error_messages();
            if ( empty( $errors ) ) {
                $title = __( 'WordPress &rsaquo; Error' );
            } else {
                $title = sprintf( __( 'WordPress &rsaquo; Error: %s' ), $message->get_error_code() );
            }
        }
    
        $errors = $message->get_error_messages();
        if ( ! empty( $errors ) ) {
            $message = '<ul><li>' . implode( '</li><li>', $errors ) . '</li></ul>';
        }
    } elseif ( is_string( $message ) ) {
        $message = sprintf( '<p>%s</p>', $message );
    }

    如果 $message 是一个 WP_Error 对象,代码会尝试从 WP_Error 对象中提取错误信息和错误代码,并将其格式化为 HTML。 如果 $message 是一个字符串,会将其包装在一个 <p> 标签中。

  4. 处理 XMLRPC 和 REST 请求:

    if ( defined( 'XMLRPC_REQUEST' ) || defined( 'REST_REQUEST' ) ) {
        wp_die( $message, $title, array_merge( $args, array( 'exit' => false ) ) ); // Recursive call.
    }

    如果当前是一个 XMLRPC 或者 REST 请求,wp_die() 会被递归调用,并将 exit 参数设置为 false。 这是为了防止在 API 请求中直接调用 die() 函数,而是将错误信息返回给客户端。

  5. 加载 Admin CSS:

    if ( function_exists( 'is_admin' ) ) {
        if ( is_admin() ) {
            if ( did_action( 'admin_enqueue_scripts' ) ) {
                wp_enqueue_style( 'wp-admin-css' );
                wp_enqueue_style( 'colors-fresh' );
            } else {
                add_action(
                    'admin_enqueue_scripts',
                    function() {
                        wp_enqueue_style( 'wp-admin-css' );
                        wp_enqueue_style( 'colors-fresh' );
                    }
                );
            }
        }
    }

    如果在后台管理界面,代码会尝试加载 WordPress 的后台 CSS 样式,以保证错误页面看起来更美观。

  6. 处理标题:

    if ( ! empty( $title ) ) {
        $title = strip_tags( $title );
        if ( $have_gettext ) {
            $title = __( $title );
        }
    }
    
    $heading = $args['heading'];
    if ( empty( $heading ) ) {
        $heading = $title;
    }

    代码会对标题进行一些处理,例如去除 HTML 标签,进行本地化等等。 如果 $args['heading'] 参数为空,会使用 $title 参数的值作为标题。

  7. 关键:wp_die_handler 过滤器:

    if ( ! did_action( 'wp_die_handler' ) ) {
        if ( WP_DEBUG && ( ! defined( 'DOING_AJAX' ) || ! DOING_AJAX ) && apply_filters( 'wp_die_ajax_backcompat', true ) ) {
            /**
             * Fires before outputting an error message.
             *
             * @since 2.0.0
             */
            do_action( 'admin_head' );
    
            if ( 'E_ALL' === error_reporting() ) {
                $error_reporting_string = __( 'E_ALL' );
            } else {
                $error_reporting_string = error_reporting();
            }
    
            $message = sprintf(
                '<div class="error"><p>%s</p><p>%s</p><p>%s</p></div>',
                __( 'WordPress has encountered a problem.' ),
                sprintf(
                    /* translators: 1: Documentation URL, 2: support forums URL */
                    __( 'For more information, visit the <a href="%1$s">Debugging in WordPress</a> page or the <a href="%2$s">WordPress Support Forums</a>.' ),
                    __( 'https://wordpress.org/documentation/article/debugging-in-wordpress/' ),
                    __( 'https://wordpress.org/support/forums/' )
                ),
                sprintf(
                    /* translators: %s: error reporting value */
                    __( 'Error reporting has been turned on. %s' ),
                    sprintf( '<code>%s</code>', $error_reporting_string )
                )
            ) . $message;
        }
    
        /**
         * Filters the callback used to handle calls to wp_die().
         *
         * @since 2.0.0
         *
         * @param callable $callback The callback function used to handle calls to wp_die().
         *                           Default 'wp_default_die_handler'.
         */
        $handler = apply_filters( 'wp_die_handler', '_default_wp_die_handler' );
        call_user_func( $handler, $message, $title, $args );
    }

    这部分代码是 wp_die() 函数的核心。它使用 apply_filters( 'wp_die_handler', '_default_wp_die_handler' ) 来获取一个用于处理错误信息的函数。 默认情况下,这个函数是 _default_wp_die_handler。但是,我们可以使用 wp_die_handler 过滤器来替换这个函数,从而自定义错误页面的显示方式。

    call_user_func( $handler, $message, $title, $args ) 这行代码会调用我们指定的错误处理函数,并将错误信息、标题和参数传递给它。

  8. 停止执行:

    if ( $args['exit'] ) {
        die();
    }

    如果 $args['exit'] 参数为 true(默认值),代码会调用 die() 函数来立即停止执行脚本。

_default_wp_die_handler() 函数:

默认的错误处理函数是 _default_wp_die_handler(),它的代码如下:

/**
 * Default handler for wp_die().
 *
 * It exits with a status code of 500.
 *
 * @since 4.7.0
 * @access private
 *
 * @param string $message Error message.
 * @param string $title   Optional. Error title. Default empty.
 * @param array  $args    Optional. Array of arguments.
 */
function _default_wp_die_handler( $message, $title = '', $args = array() ) {
    $defaults = array( 'response' => 500 );
    $args     = wp_parse_args( $args, $defaults );

    $have_gettext = function_exists( '__' );

    if ( function_exists( 'is_admin' ) ) {
        if ( is_admin() ) {
            /**
             * Fires before the administration header is written to output.
             *
             * @since 3.0.0
             */
            do_action( 'admin_head' );
            ?>
            <div class="wrap">
                <h1><?php echo esc_html( $title ); ?></h1>
                <?php echo $message; ?>
            </div>
            <?php
        } else {
            if ( did_action( 'login_head' ) ) {
                $login_header = 'login_header';
            } else {
                $login_header = 'wp_login_header';
            }

            /**
             * Fires in the login page header for displaying login form header elements.
             *
             * @since 3.5.0
             */
            do_action( 'login_enqueue_scripts' );

            /**
             * Fires in the login page header for displaying login form header elements.
             *
             * @since 2.1.0
             *
             * @param string $title   Login page title to display.
             * @param string $message Message to display in the header.
             */
            do_action( $login_header, $title, $message );
            ?>
            <p><?php echo $message; ?></p>
            <?php
        }
    } else {
        if ( ! headers_sent() ) {
            header( 'Content-Type: text/html; charset=utf-8' );
            header( sprintf( 'HTTP/1.1 %d %s', $args['response'], get_status_header_desc( $args['response'] ) ) );
        }
        ?>
        <!DOCTYPE html>
        <html xmlns="http://www.w3.org/1999/xhtml" <?php language_attributes(); ?>>
        <head>
            <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
            <meta name="viewport" content="width=device-width">
            <title><?php echo esc_html( $title ); ?></title>
            <style type="text/css">
                html {
                    background: #f1f1f1;
                }

                body {
                    background: #fff;
                    color: #444;
                    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
                    margin: 2em auto;
                    padding: 1em 2em;
                    max-width: 700px;
                    -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, .13);
                    box-shadow: 0 1px 3px rgba(0, 0, 0, .13);
                }

                h1 {
                    border-bottom: 1px solid #dadada;
                    clear: both;
                    color: #666;
                    font-size: 2em;
                    margin: 30px 0 0 0;
                    padding: 0;
                    padding-bottom: 7px;
                }

                #error-page {
                    margin-top: 50px;
                }

                #error-page p {
                    font-size: 14px;
                    line-height: 1.5;
                    margin: 25px 0 20px;
                }

                #error-page code {
                    font-family: Consolas, Monaco, monospace;
                }

                ul li {
                    margin-bottom: 10px;
                    font-size: 14px;
                }

                a {
                    color: #0073aa;
                }

                a:hover {
                    color: #006799;
                }

                .button {
                    background: #f7f7f7;
                    border: 1px solid #cccccc;
                    color: #555;
                    display: inline-block;
                    text-decoration: none;
                    font-size: 13px;
                    line-height: 2;
                    height: 28px;
                    margin: 0;
                    padding: 0 10px 1px;
                    cursor: pointer;
                    -webkit-border-radius: 3px;
                    border-radius: 3px;
                    -webkit-box-sizing: border-box;
                    -moz-box-sizing: border-box;
                    box-sizing: border-box;

                    -webkit-box-shadow: 0 1px 0 #fff inset, 0 0 0 1px rgba(0, 0, 0, .08);
                    box-shadow: 0 1px 0 #fff inset, 0 0 0 1px rgba(0, 0, 0, .08);
                    vertical-align: top;
                }

                .button:hover {
                    background: #fafafa;
                    border-color: #999;
                    color: #222;
                }

                .button:focus {
                    background: #fff;
                    border-color: #999;
                    color: #333;
                    -webkit-box-shadow: 0 1px 0 #fff inset, 0 0 0 1px rgba(0, 0, 0, .08), 0 0 5px rgba(0, 0, 0, .08);
                    box-shadow: 0 1px 0 #fff inset, 0 0 0 1px rgba(0, 0, 0, .08), 0 0 5px rgba(0, 0, 0, .08);
                    outline: none;
                }

                .button:active {
                    background: #eee;
                    border-color: #999;
                    color: #333;
                    -webkit-box-shadow: 0 2px 5px -3px rgba(0, 0, 0, .5) inset;
                    box-shadow: 0 2px 5px -3px rgba(0, 0, 0, .5) inset;
                }

                <?php if ( is_rtl() ) : ?>

                    body {
                        font-family: Tahoma, Arial, sans-serif;
                    }

                <?php endif; ?>
            </style>
        </head>
        <body id="error-page">
            <div class="container">
                <h1 class="headline"><?php echo esc_html( $title ); ?></h1>
                <p class="message"><?php echo $message; ?></p>
            </div>
        </body>
        </html>
        <?php
    }

    if ( $args['exit'] ) {
        die( 1 );
    }
}

这个函数会根据当前环境(后台、前台、登录页面)显示不同的错误页面。 它会设置 HTTP 响应码,输出 HTML 头部信息,以及显示错误信息和标题。

自定义错误页面:

现在,我们来看看如何使用 wp_die_handler 过滤器来定制错误页面。 假设我们需要创建一个自定义的错误页面,显示一个友好的错误信息,并提供一个返回首页的链接。

首先,我们需要创建一个自定义的错误处理函数:

function my_custom_die_handler( $message, $title = '', $args = array() ) {
    $defaults = array(
        'response'  => 500,
        'back_link' => home_url(), // 添加返回首页链接
    );
    $args = wp_parse_args( $args, $defaults );

    $response = $args['response'];

    // 输出 HTTP 响应码
    status_header( $response );

    // 设置 Content-Type
    header( 'Content-Type: text/html; charset=utf-8' );

    // 输出 HTML 头部
    ?>
    <!DOCTYPE html>
    <html <?php language_attributes(); ?>>
    <head>
        <meta charset="<?php bloginfo( 'charset' ); ?>">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title><?php echo esc_html( $title ); ?></title>
        <style>
            body {
                font-family: sans-serif;
                text-align: center;
                padding: 50px;
            }
            h1 {
                font-size: 2em;
                margin-bottom: 20px;
            }
            p {
                font-size: 1.2em;
                margin-bottom: 30px;
            }
            a {
                color: #0073aa;
                text-decoration: none;
            }
            a:hover {
                text-decoration: underline;
            }
        </style>
    </head>
    <body>
        <h1><?php echo esc_html( $title ); ?></h1>
        <p><?php echo $message; ?></p>
        <?php if ( $args['back_link'] ) : ?>
            <a href="<?php echo esc_url( $args['back_link'] ); ?>">返回首页</a>
        <?php endif; ?>
    </body>
    </html>
    <?php

    // 停止执行
    die();
}

然后,我们需要使用 wp_die_handler 过滤器来替换默认的错误处理函数:

add_filter( 'wp_die_handler', 'my_custom_die_handler' );

这段代码会将 my_custom_die_handler 函数注册为 wp_die_handler 过滤器的回调函数。 这样,当 wp_die() 函数被调用时,就会调用 my_custom_die_handler 函数来处理错误信息。

示例:在插件中使用 wp_die()wp_die_handler

<?php
/**
 * Plugin Name: Custom WP_Die Handler
 * Description: Demonstrates how to customize the wp_die() output.
 * Version: 1.0.0
 */

// 自定义错误处理函数
function my_custom_die_handler( $message, $title = '', $args = array() ) {
    $defaults = array(
        'response'  => 500,
        'back_link' => home_url(), // 添加返回首页链接
    );
    $args = wp_parse_args( $args, $defaults );

    $response = $args['response'];

    // 输出 HTTP 响应码
    status_header( $response );

    // 设置 Content-Type
    header( 'Content-Type: text/html; charset=utf-8' );

    // 输出 HTML 头部
    ?>
    <!DOCTYPE html>
    <html <?php language_attributes(); ?>>
    <head>
        <meta charset="<?php bloginfo( 'charset' ); ?>">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title><?php echo esc_html( $title ); ?></title>
        <style>
            body {
                font-family: sans-serif;
                text-align: center;
                padding: 50px;
            }
            h1 {
                font-size: 2em;
                margin-bottom: 20px;
            }
            p {
                font-size: 1.2em;
                margin-bottom: 30px;
            }
            a {
                color: #0073aa;
                text-decoration: none;
            }
            a:hover {
                text-decoration: underline;
            }
        </style>
    </head>
    <body>
        <h1><?php echo esc_html( $title ); ?></h1>
        <p><?php echo $message; ?></p>
        <?php if ( $args['back_link'] ) : ?>
            <a href="<?php echo esc_url( $args['back_link'] ); ?>">返回首页</a>
        <?php endif; ?>
    </body>
    </html>
    <?php

    // 停止执行
    die();
}

// 注册 wp_die_handler 过滤器
add_filter( 'wp_die_handler', 'my_custom_die_handler' );

// 示例:在插件激活时触发 wp_die()
function my_plugin_activation() {
    // 检查 PHP 版本
    if ( version_compare( PHP_VERSION, '7.0', '<' ) ) {
        wp_die( 'This plugin requires PHP version 7.0 or higher.', 'Plugin Activation Error', array( 'back_link' => false ) );
    }
}
register_activation_hook( __FILE__, 'my_plugin_activation' );

在这个示例中,我们创建了一个名为 "Custom WP_Die Handler" 的插件。

  • 插件激活时,会检查 PHP 版本。 如果 PHP 版本低于 7.0,会调用 wp_die() 函数显示一个错误信息,并停止插件的激活过程。
  • 我们使用 wp_die_handler 过滤器来替换默认的错误处理函数,使用我们自定义的 my_custom_die_handler 函数。

现在,当你尝试激活这个插件时,如果你的 PHP 版本低于 7.0,你将会看到我们自定义的错误页面,而不是 WordPress 默认的错误页面。

总结:

wp_die() 函数是 WordPress 中一个非常重要的函数,用于处理错误和异常情况。 通过 wp_die_handler 过滤器,我们可以自定义错误页面的显示方式,从而提高用户体验。

希望今天的讲解能够帮助大家更好地理解 wp_die() 函数和 wp_die_handler 过滤器。 记住,错误并不可怕,可怕的是我们不知道如何优雅地处理它们。

下次再见!

发表回复

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