详解 WordPress `WP_Theme` 类源码:解析 `style.css` 文件获取主题信息。

咳咳,各位观众老爷们,晚上好!欢迎来到今天的“WordPress主题解剖”特别节目。今天咱们不聊八卦,不谈情怀,就来硬核地啃啃 WordPress 主题背后的“骨头”—— WP_Theme 类,特别是它如何从 style.css 这个小小的文件中,挖出主题的“身世”和“性格”。

准备好了吗?咱们这就开整!

一、 WP_Theme 类:主题的“户口本”

在 WordPress 的世界里,WP_Theme 类就像是每个主题的“户口本”,它记录了主题的各种基本信息,比如:

  • 主题名称 (Theme Name)
  • 主题版本 (Version)
  • 作者 (Author)
  • 描述 (Description)
  • 授权方式 (License)
  • 等等…

这些信息都藏在哪里呢?没错,就在主题根目录下的 style.css 文件里!

二、 style.css:主题的“身份证”

style.css 文件可不只是用来写 CSS 样式那么简单,它还承担着“身份证”的重任,负责告诉 WordPress 这个主题是谁,从哪里来,要到哪里去(误)。

style.css 文件的头部注释部分,就是用来存放主题信息的关键区域。 WordPress 会读取这些注释,并将它们解析成 WP_Theme 对象的属性。

一个典型的 style.css 文件头部注释可能长这样:

/*
Theme Name:   My Awesome Theme
Theme URI:    https://example.com/my-awesome-theme/
Author:       John Doe
Author URI:   https://example.com/
Description:  A super awesome theme for super awesome people.
Version:      1.0.0
License:      GNU General Public License v2 or later
License URI:  http://www.gnu.org/licenses/gpl-2.0.html
Text Domain:  my-awesome-theme
Tags:         blog, one-column, custom-menu, featured-images
*/

注意,这些信息必须按照特定的格式写在注释里,而且必须放在文件的最开头,否则 WordPress 就识别不出来。

三、 WP_Theme 类的“寻宝之旅”:源码解析

好了,理论知识铺垫完毕,现在咱们深入 WP_Theme 类的源码,看看它是如何从 style.css 文件中“寻宝”的。

WP_Theme 类的核心方法之一是 get_stylesheet()get_template()。 它们分别返回主题的样式表目录(stylesheet directory)和模板目录(template directory)。 对于普通主题,这两个目录通常是相同的。 对于子主题,样式表目录是子主题的目录,而模板目录是父主题的目录。

<?php
/**
 * Core class used to implement the Theme object.
 *
 * @since 3.4.0
 *
 * @property string $name        Theme name.
 * @property string $version     Theme version.
 * @property string $stylesheet  Stylesheet name.
 * @property string $template    Template name.
 */
class WP_Theme {
    /**
     * Headers within the style sheet.
     *
     * @since 3.4.0
     * @access private
     * @var array
     */
    private $headers = array();

    /**
     * Root directory of the theme.
     *
     * @since 3.4.0
     * @access private
     * @var string
     */
    private $theme_root;

    /**
     * Root URI of the theme.
     *
     * @since 3.4.0
     * @access private
     * @var string
     */
    private $theme_root_uri;

    /**
     * True, if the theme is installed in a subdirectory off of
     * WP_CONTENT_DIR and WP_CONTENT_URL. Otherwise, false.
     *
     * @since 3.4.0
     * @access private
     * @var bool
     */
    private $stylesheet_dir_name;

    /**
     * True, if the template is installed in a subdirectory off of
     * WP_CONTENT_DIR and WP_CONTENT_URL. Otherwise, false.
     *
     * @since 3.4.0
     * @access private
     * @var bool
     */
    private $template_dir_name;

    /**
     * The text domain of the theme.
     *
     * @since 3.4.0
     * @access private
     * @var string
     */
    private $textdomain;

    /**
     * Constructor.
     *
     * @since 3.4.0
     *
     * @param string $stylesheet Stylesheet name.
     * @param string $theme_root Theme root.
     */
    public function __construct( $stylesheet, $theme_root ) {
        $this->stylesheet = $stylesheet;
        $this->theme_root = $theme_root;
        $this->theme_root_uri = '';
        $this->stylesheet_dir_name = '';
        $this->template_dir_name = '';
        $this->textdomain = '';

        $this->headers = $this->get_theme_data();
    }

    /**
     * Gets the data from a theme's style.css file.
     *
     * @since 3.4.0
     * @access private
     *
     * @return array The contents of the style.css file.
     */
    private function get_theme_data() {
        $stylesheet = $this->get_stylesheet();
        $theme_root = $this->get_theme_root();
        $style_path = $theme_root . '/' . $stylesheet . '/style.css';

        if ( ! is_readable( $style_path ) ) {
            return array();
        }

        $theme_data = get_file_data( $style_path, $this->get_stylesheet_headers() );

        if ( empty( $theme_data['TextDomain'] ) ) {
            $theme_data['TextDomain'] = sanitize_title( $this->get( 'Name' ) );
        }

        return $theme_data;
    }

    /**
     * Gets the headers from a theme's style.css file.
     *
     * @since 3.4.0
     * @access private
     *
     * @return array The headers from the style.css file.
     */
    private function get_stylesheet_headers() {
        return array(
            'Name'        => 'Theme Name',
            'ThemeURI'    => 'Theme URI',
            'Description' => 'Description',
            'Author'      => 'Author',
            'AuthorURI'   => 'Author URI',
            'Version'     => 'Version',
            'Template'    => 'Template',
            'Status'      => 'Status',
            'Tags'        => 'Tags',
            'TextDomain'  => 'Text Domain',
            'License'     => 'License',
            'LicenseURI'  => 'License URI',
        );
    }

    /**
     * Gets a theme's data.
     *
     * @since 3.4.0
     *
     * @param string $header Theme header.
     * @return string|false The value of the header. False if not found.
     */
    public function get( $header ) {
        if ( isset( $this->headers[ $header ] ) ) {
            return $this->headers[ $header ];
        }

        return false;
    }

    /**
     * Gets the theme's stylesheet directory.
     *
     * @since 3.4.0
     *
     * @return string Stylesheet directory.
     */
    public function get_stylesheet() {
        return $this->stylesheet;
    }

    /**
     * Gets the theme's template directory.
     *
     * @since 3.4.0
     *
     * @return string Template directory.
     */
    public function get_template() {
        if ( $this->has_parent() ) {
            return $this->headers['Template'];
        }

        return $this->stylesheet;
    }

    /**
     * Whether the theme has a parent.
     *
     * @since 3.4.0
     *
     * @return bool True, if the theme has a parent. False, if not.
     */
    public function has_parent() {
        return ! empty( $this->headers['Template'] );
    }

    /**
     * Gets the name of the theme's text domain.
     *
     * @since 3.4.0
     *
     * @return string Text domain.
     */
    public function get_template_directory() {
        return $this->get_theme_root() . '/' . $this->get_template();
    }

    /**
     * Gets the URI of the theme's template directory.
     *
     * @since 3.4.0
     *
     * @return string Template directory URI.
     */
    public function get_template_directory_uri() {
        return $this->get_theme_root_uri() . '/' . $this->get_template();
    }

    /**
     * Gets the theme's stylesheet directory.
     *
     * @since 3.4.0
     *
     * @return string Stylesheet directory.
     */
    public function get_stylesheet_directory() {
        return $this->get_theme_root() . '/' . $this->get_stylesheet();
    }

    /**
     * Gets the URI of the theme's stylesheet directory.
     *
     * @since 3.4.0
     *
     * @return string Stylesheet directory URI.
     */
    public function get_stylesheet_directory_uri() {
        return $this->get_theme_root_uri() . '/' . $this->get_stylesheet();
    }

    /**
     * Gets the root directory of the theme.
     *
     * @since 3.4.0
     *
     * @return string Theme root.
     */
    public function get_theme_root() {
        return $this->theme_root;
    }

    /**
     * Gets the URI of the root directory of the theme.
     *
     * @since 3.4.0
     *
     * @return string Theme root URI.
     */
    public function get_theme_root_uri() {
        if ( ! empty( $this->theme_root_uri ) ) {
            return $this->theme_root_uri;
        }

        $theme_root = $this->get_theme_root();

        /**
         * Filters the URI of a theme's root directory.
         *
         * @since 3.4.0
         *
         * @param string   $theme_root_uri The URI of the theme root directory.
         * @param string   $theme_root     The absolute path to the theme root directory.
         * @param string   $stylesheet     The theme's stylesheet.
         */
        $this->theme_root_uri = apply_filters( 'theme_root_uri', get_theme_root_uri( $this->get_stylesheet(), $theme_root ), $theme_root, $this->get_stylesheet() );
        return $this->theme_root_uri;
    }

    /**
     * Gets the name of the theme's text domain.
     *
     * @since 3.4.0
     *
     * @return string Text domain.
     */
    public function get_textdomain() {
        return $this->textdomain;
    }

    /**
     * Whether the theme is a child theme.
     *
     * @since 3.4.0
     *
     * @return bool True, if the theme is a child theme. False, if not.
     */
    public function is_child_theme() {
        return $this->has_parent();
    }

    /**
     * Displays a contextual link to the theme.
     *
     * @since 3.4.0
     *
     * @param string $context Optional. Context of the link. Possible values are
     *                        'author', 'theme', and 'parent'. Default 'theme'.
     */
    public function display( $context = 'theme' ) {
        $stylesheet = $this->get_stylesheet();

        $args = array(
            'theme' => $stylesheet,
        );

        switch ( $context ) {
            case 'author':
                $author = $this->get( 'Author' );
                if ( empty( $author ) ) {
                    break;
                }

                $author_uri = $this->get( 'AuthorURI' );
                if ( empty( $author_uri ) ) {
                    echo esc_html( $author );
                    break;
                }

                echo '<a href="' . esc_url( $author_uri ) . '">' . esc_html( $author ) . '</a>';
                break;

            case 'parent':
                $parent = $this->parent();
                if ( ! $parent ) {
                    break;
                }

                $args['theme'] = $parent->get_stylesheet();
                $name = $parent->get( 'Name' );
                $name = translate( $name, $parent->get( 'TextDomain' ) );
                echo '<a href="' . esc_url( wp_nonce_url( add_query_arg( $args, 'themes.php' ), 'switch-theme_' . $parent->get_stylesheet() ) ) . '">' . esc_html( $name ) . '</a>';
                break;

            case 'theme':
            default:
                $name = $this->get( 'Name' );
                $name = translate( $name, $this->get( 'TextDomain' ) );
                echo '<a href="' . esc_url( wp_nonce_url( add_query_arg( $args, 'themes.php' ), 'switch-theme_' . $stylesheet ) ) . '">' . esc_html( $name ) . '</a>';
                break;
        }
    }

    /**
     * Returns the parent of the current theme.
     *
     * @since 3.4.0
     *
     * @return WP_Theme|false The parent of the current theme, or false if the
     *                        theme does not have a parent.
     */
    public function parent() {
        if ( ! $this->has_parent() ) {
            return false;
        }

        return wp_get_theme( $this->get( 'Template' ) );
    }
}
?>

接下来,我们重点关注 __construct() 构造函数和 get_theme_data() 方法:

  1. __construct( $stylesheet, $theme_root ):

    • 这个构造函数接收两个参数:$stylesheet (样式表名称) 和 $theme_root (主题根目录)。
    • 它会设置 $this->stylesheet$this->theme_root 属性。
    • 最关键的是,它会调用 $this->get_theme_data() 方法,将从 style.css 中提取的信息存储到 $this->headers 属性中。
  2. get_theme_data():

    • 这个方法负责从 style.css 文件中读取主题信息。
    • 它首先构建 style.css 文件的完整路径:$style_path = $theme_root . '/' . $stylesheet . '/style.css';
    • 然后,它会调用 get_file_data() 函数,这个函数是 WordPress 内置的,专门用来读取文件头部注释中的数据。
    • get_file_data() 函数需要两个参数:文件路径和要读取的头部信息列表。
    • $this->get_stylesheet_headers() 方法就是用来定义要读取哪些头部信息的,比如 ‘Theme Name’, ‘Version’, ‘Author’ 等等。
    • 最后,get_theme_data() 方法会将读取到的数据返回。
  3. get_stylesheet_headers():

    返回一个数组,定义了需要从 style.css 文件中读取的头部信息。每个键值对的键表示要获取的属性名,值表示在 style.css 文件中对应的头部名称。

    private function get_stylesheet_headers() {
        return array(
            'Name'        => 'Theme Name',
            'ThemeURI'    => 'Theme URI',
            'Description' => 'Description',
            'Author'      => 'Author',
            'AuthorURI'   => 'Author URI',
            'Version'     => 'Version',
            'Template'    => 'Template',
            'Status'      => 'Status',
            'Tags'        => 'Tags',
            'TextDomain'  => 'Text Domain',
            'License'     => 'License',
            'LicenseURI'  => 'License URI',
        );
    }
  4. get( $header ):

    这个方法用于获取特定头部信息的值。它接收一个 $header 参数,表示要获取的头部信息的名称。如果找到了对应的头部信息,则返回其值;否则返回 false

    public function get( $header ) {
        if ( isset( $this->headers[ $header ] ) ) {
            return $this->headers[ $header ];
        }
    
        return false;
    }

四、 get_file_data():幕后英雄

get_file_data() 函数是 WordPress 提供的核心函数,位于 wp-includes/functions.php 文件中。 它的作用是从文件中读取头部注释中的数据,并将其解析成一个数组。

<?php
/**
 * Gets file data from a specific file.
 *
 * @since 2.9.0
 *
 * @param string $file  Path to the file to open for reading.
 * @param array  $default_headers List of headers, in the format array('HeaderKey' => 'Header Name').
 * @param string $context Optional. If specified adds filter hook {@see 'extra_theme_headers'} or
 *                        {@see 'extra_plugin_headers'}. Use 'theme' for Theme files, 'plugin' for plugin files.
 *                        Default null.
 * @return array Array of file data.
 */
function get_file_data( $file, $default_headers, $context = null ) {
    // We don't need to write to the file, so just use r.
    $fp = fopen( $file, 'r' );

    // Pull out the first 8kiB of the file.
    $file_data = fread( $fp, 8192 );

    // PHP will close file handle, but we are good citizens.
    fclose( $fp );

    // Make sure we catch CR-only line endings.
    $file_data = str_replace( "r", "n", $file_data );

    /**
     * Filters extra file headers when parsing a file.
     *
     * The dynamic portion of the hook name, `$context`, refers to the context
     * passed to `get_file_data()`.
     *
     * @since 2.9.0
     *
     * @param array  $default_headers Array of default headers.
     * @param string $file            Full path to the file.
     */
    if ( ! empty( $context ) ) {
        $extra_headers = apply_filters( "extra_{$context}_headers", array(), $file );
        $default_headers = array_merge( $extra_headers, $default_headers );
    }

    foreach ( $default_headers as $field => $regex ) {
        // Pre-match to avoid perf loss.
        if ( preg_match( '/^[ t/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) {
            $all_matches = preg_split( '/[rn]+/', $match[1] );
            $value = trim( $all_matches[0] );
        } else {
            $value = '';
        }
        $all_headers[ $field ] = $value;
    }

    return $all_headers;
}
?>

简单来说,get_file_data() 函数会:

  1. 打开文件。
  2. 读取文件的前 8KB 内容(理论上主题信息应该都在这个范围内)。
  3. 遍历传入的头部信息列表(比如 ‘Theme Name’, ‘Version’ 等等)。
  4. 使用正则表达式从文件内容中匹配对应的头部信息。
  5. 将匹配到的信息存储到一个数组中并返回。

五、 实战演练:如何使用 WP_Theme 类获取主题信息

说了这么多,咱们来点实际的。 假设我们想要获取当前主题的名称和版本,可以这样做:

<?php
// 获取当前主题的 WP_Theme 对象
$theme = wp_get_theme();

// 获取主题名称
$theme_name = $theme->get( 'Name' );

// 获取主题版本
$theme_version = $theme->get( 'Version' );

// 输出主题信息
echo '主题名称:' . esc_html( $theme_name ) . '<br>';
echo '主题版本:' . esc_html( $theme_version ) . '<br>';
?>

这段代码首先使用 wp_get_theme() 函数获取当前主题的 WP_Theme 对象。 然后,使用 get() 方法分别获取主题的名称和版本。 最后,将主题信息输出到页面上。

六、 子主题的“特殊待遇”

对于子主题来说,WP_Theme 类的处理方式略有不同。 子主题的 style.css 文件中通常会包含一个 Template 头部,用来指定父主题的目录名。

/*
Theme Name:   My Awesome Child Theme
Description:  A child theme for My Awesome Theme.
Author:       John Doe
Template:     my-awesome-theme  // 注意这里!
Version:      1.0.0
*/

WP_Theme 类检测到 Template 头部时,它会知道这是一个子主题,并会加载父主题的 WP_Theme 对象。 这样,子主题就可以继承父主题的信息,并且可以覆盖父主题的样式和功能。

WP_Theme 类中与子主题相关的关键方法是 has_parent()parent()

  • has_parent():判断当前主题是否有父主题。
  • parent():返回父主题的 WP_Theme 对象。

七、 总结与展望

今天咱们一起深入剖析了 WordPress WP_Theme 类的源码,了解了它如何从 style.css 文件中提取主题信息。 掌握了这些知识,你就可以更加灵活地使用 WordPress 主题,并且可以开发出更加强大的主题相关功能。

方法/属性 作用
__construct() 构造函数,初始化 WP_Theme 对象,读取 style.css 信息。
get_theme_data() style.css 文件中读取主题数据。
get() 获取指定头部信息的值。
get_stylesheet() 获取主题的样式表目录名。
get_template() 获取主题的模板目录名(对于子主题,返回父主题的目录名)。
has_parent() 判断主题是否是子主题。
parent() 获取父主题的 WP_Theme 对象。

当然,WP_Theme 类还有很多其他的细节和功能,比如主题的本地化、主题的截图等等,这些就留给各位观众老爷们自己去探索了。

希望今天的节目对大家有所帮助。 如果你觉得还不错,记得点赞、评论、转发三连哦!咱们下期再见!

发表回复

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