剖析 `get_plugins()` 函数的源码,它是如何扫描并解析插件头信息来获取所有插件列表的?

各位朋友们,晚上好!今天咱们来聊聊WordPress插件的“寻宝之旅”,看看get_plugins()这个函数,是如何像一位经验老道的考古学家一样,把散落在插件目录里的宝藏(插件头信息)给挖掘出来的。

开场白:插件,WordPress的灵魂伴侣

WordPress之所以如此强大,很大程度上要归功于其丰富的插件生态。插件就像乐高积木,可以让你自由组合,搭建出各种功能的网站。而get_plugins()函数,就是负责把这些积木都给你展示出来,让你知道有哪些积木可以用。

一、get_plugins():插件管理的指挥官

首先,咱们要明确一点,get_plugins()函数位于wp-admin/includes/plugin.php文件中。它的主要职责就是扫描插件目录,解析每个插件的头部信息,然后把这些信息整理成一个数组,方便你在后台管理插件。

咱们先看看get_plugins()函数的大致框架(简化版):

function get_plugins( $plugin_folder = '' ) {
    static $all_plugins;

    if ( ! is_array( $all_plugins ) ) {
        $all_plugins = array();
    }

    if ( isset( $all_plugins[ $plugin_folder ] ) ) {
        return $all_plugins[ $plugin_folder ];
    }

    $wp_plugins = array();
    $plugin_root = WP_PLUGIN_DIR;
    if ( ! empty( $plugin_folder ) ) {
        $plugin_root .= '/' . $plugin_folder;
    }

    // 1. 扫描插件目录
    $plugins_dir = @opendir( $plugin_root );
    $plugin_files = array();
    if ( $plugins_dir ) {
        while ( ( $file = readdir( $plugins_dir ) ) !== false ) {
            if ( substr( $file, 0, 1 ) == '.' ) {
                continue;
            }

            if ( is_dir( $plugin_root . '/' . $file ) ) {
                $plugins_subdir = @opendir( $plugin_root . '/' . $file );

                if ( $plugins_subdir ) {
                    while ( ( $subfile = readdir( $plugins_subdir ) ) !== false ) {
                        if ( substr( $subfile, 0, 1 ) == '.' ) {
                            continue;
                        }

                        if ( substr( $subfile, -4 ) == '.php' ) {
                            $plugin_files[] = "$file/$subfile";
                        }
                    }
                    closedir( $plugins_subdir );
                }
            } else {
                if ( substr( $file, -4 ) == '.php' ) {
                    $plugin_files[] = $file;
                }
            }
        }
        closedir( $plugins_dir );
    }

    if ( empty( $plugin_files ) ) {
        return array();
    }

    // 2. 加载插件头部信息
    foreach ( $plugin_files as $plugin_file ) {
        if ( ! is_readable( "$plugin_root/$plugin_file" ) ) {
            continue;
        }

        $plugin_data = get_plugin_data( "$plugin_root/$plugin_file" );

        if ( empty( $plugin_data['Name'] ) ) {
            continue;
        }

        $wp_plugins[ $plugin_file ] = $plugin_data;
    }

    $all_plugins[ $plugin_folder ] = $wp_plugins;

    return $wp_plugins;
}

这个简化版已经包含了get_plugins()的核心逻辑:

  1. 扫描插件目录: 找到所有.php文件(可能是直接在插件目录下,也可能在子目录里)。
  2. 加载插件头部信息: 读取每个.php文件的头部注释,提取插件的名称、描述、版本等信息。

二、深入虎穴:扫描插件目录

这部分的代码主要负责遍历插件目录,找到所有的插件文件。

    $plugins_dir = @opendir( $plugin_root );
    $plugin_files = array();
    if ( $plugins_dir ) {
        while ( ( $file = readdir( $plugins_dir ) ) !== false ) {
            if ( substr( $file, 0, 1 ) == '.' ) {
                continue;
            }

            if ( is_dir( $plugin_root . '/' . $file ) ) {
                $plugins_subdir = @opendir( $plugin_root . '/' . $file );

                if ( $plugins_subdir ) {
                    while ( ( $subfile = readdir( $plugins_subdir ) ) !== false ) {
                        if ( substr( $subfile, 0, 1 ) == '.' ) {
                            continue;
                        }

                        if ( substr( $subfile, -4 ) == '.php' ) {
                            $plugin_files[] = "$file/$subfile";
                        }
                    }
                    closedir( $plugins_subdir );
                }
            } else {
                if ( substr( $file, -4 ) == '.php' ) {
                    $plugin_files[] = $file;
                }
            }
        }
        closedir( $plugins_dir );
    }

这段代码逻辑清晰,但我们还是来拆解一下:

  • opendir():打开插件目录。
  • readdir():读取目录中的文件和子目录。
  • is_dir():判断是否是目录。
  • substr():截取字符串,用来判断文件名是否以.php结尾,或者是否以.开头(隐藏文件)。

这段代码会递归地遍历插件目录,找到所有以.php结尾的文件,并将它们的文件名存储在$plugin_files数组中。 注意,这里的路径是相对于插件目录的。

三、拨云见日:解析插件头部信息

找到了插件文件,下一步就是读取它们的头部信息。 这个任务由get_plugin_data()函数来完成。

function get_plugin_data( $plugin_file, $markup = true, $translate = true ) {
    $default_headers = array(
        'Name'        => 'Plugin Name',
        'PluginURI'   => 'Plugin URI',
        'Version'     => 'Version',
        'Description' => 'Description',
        'Author'      => 'Author',
        'AuthorURI'   => 'Author URI',
        'TextDomain'  => 'Text Domain',
        'DomainPath'  => 'Domain Path',
        'Network'     => 'Network',
        'RequiresWP'  => 'Requires at least',
        'RequiresPHP' => 'Requires PHP',
        'UpdateURI'   => 'Update URI',
    );

    $plugin_data = get_file_data( $plugin_file, $default_headers, 'plugin' );

    if ( $markup ) {
        $plugin_data['Name']        = wptexturize( $plugin_data['Name'] );
        $plugin_data['PluginURI']   = esc_url( $plugin_data['PluginURI'] );
        $plugin_data['Description'] = wptexturize( $plugin_data['Description'] );
        $plugin_data['Author']      = wptexturize( $plugin_data['Author'] );
        $plugin_data['AuthorURI']   = esc_url( $plugin_data['AuthorURI'] );
        $plugin_data['Version']     = wptexturize( $plugin_data['Version'] );
    } else {
        $plugin_data['Name']        = wp_strip_all_tags( $plugin_data['Name'] );
        $plugin_data['Description'] = wp_strip_all_tags( $plugin_data['Description'] );
        $plugin_data['Author']      = wp_strip_all_tags( $plugin_data['Author'] );
    }

    if ( $translate && ! empty( $plugin_data['TextDomain'] ) ) {
        /**
         * Filters the translated plugin header data array.
         *
         * @since 2.7.0
         *
         * @param string[] $plugin_data Plugin header data.
         * @param string   $plugin_file Path to the plugin file.
         * @param bool     $markup      Whether to apply markup to the plugin data. Defaults to true.
         * @param bool     $translate   Whether to translate the plugin data. Defaults to true.
         */
        $plugin_data = apply_filters( 'plugin_data_i18n', $plugin_data, $plugin_file, $markup, $translate );
    }

    $plugin_data = apply_filters( 'extra_plugin_headers', $plugin_data, $plugin_file );

    return $plugin_data;
}

get_plugin_data()函数的核心在于get_file_data()函数,它负责读取文件内容并解析头部信息。我们先来看看get_plugin_data()做了哪些事情:

  1. 定义默认头部信息: $default_headers数组定义了插件头部信息的字段名和对应的标签。
  2. 调用get_file_data() 将插件文件路径和默认头部信息传递给get_file_data()函数。
  3. 处理数据: 对获取到的数据进行一些处理,比如URL转义、文本格式化等。
  4. 应用过滤器: 允许其他插件或主题通过过滤器修改插件数据。

接下来,我们深入get_file_data()函数,看看它是如何读取和解析文件头部信息的。

function get_file_data( $file, $default_headers, $context = '' ) {
    // 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 the extra file headers used to retrieve metadata.
     *
     * The dynamic portion of the hook name, `$context`, refers
     * to the context where the hook is used, such as a plugin or theme.
     *
     * @since 2.9.0
     *
     * @param string[] $default_headers Array of default file headers.
     * @param string   $file            Full path to the file.
     */
    $default_headers = apply_filters( "extra_{$context}_headers", $default_headers, $file );

    foreach ( $default_headers as $field => $regex ) {
        if ( preg_match( '/^[ t/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_data, $match ) && $match[1] ) {
            $all_headers[ $field ] = trim( $match[1] );
        } else {
            $all_headers[ $field ] = '';
        }
    }

    return $all_headers;
}

get_file_data()函数的主要步骤如下:

  1. 打开文件: 使用fopen()函数以只读模式打开文件。
  2. 读取文件内容: 使用fread()函数读取文件的前8KB内容。 为什么只读取前8KB? 因为插件的头部信息通常都在文件的开头部分,所以没有必要读取整个文件。
  3. 关闭文件: 使用fclose()函数关闭文件。
  4. 处理换行符:r替换为n,确保换行符的一致性。
  5. 应用过滤器: 允许通过过滤器修改默认的头部信息。
  6. 正则匹配: 遍历默认头部信息,使用正则表达式从文件内容中提取对应的值。

重点来了:正则表达式

get_file_data()函数使用正则表达式来匹配插件头部信息。让我们来看一下这个正则表达式:

/^[ t/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi
  • ^[ t/*#@]*: 匹配行首的空白字符(空格、制表符)、/*#@等注释符号,允许头部信息出现在注释中。
  • preg_quote( $regex, '/'): 转义$regex中的特殊字符,确保正则表达式的准确性。 $regex就是我们在$default_headers中定义的标签,比如Plugin Name
  • :(.*)$: 匹配标签后面的冒号和所有内容,直到行尾。 $match[1]就是我们想要提取的插件信息。
  • m: 多行模式,允许正则表达式匹配多行文本。
  • i: 忽略大小写模式,允许正则表达式忽略大小写。

举个例子,如果$regexPlugin Name,那么正则表达式就会匹配类似下面这样的字符串:

/*
Plugin Name: My Awesome Plugin
*/

或者

# Plugin Name: My Awesome Plugin

四、整合与返回:插件信息的归宿

回到get_plugins()函数,在解析完所有插件的头部信息后,它会将这些信息存储在一个数组中,并返回这个数组。

    foreach ( $plugin_files as $plugin_file ) {
        if ( ! is_readable( "$plugin_root/$plugin_file" ) ) {
            continue;
        }

        $plugin_data = get_plugin_data( "$plugin_root/$plugin_file" );

        if ( empty( $plugin_data['Name'] ) ) {
            continue;
        }

        $wp_plugins[ $plugin_file ] = $plugin_data;
    }

    $all_plugins[ $plugin_folder ] = $wp_plugins;

    return $wp_plugins;

$wp_plugins数组的键是插件的文件名,值是插件的头部信息。 $all_plugins是一个静态变量,用来缓存已经扫描过的插件目录,避免重复扫描。

五、实战演练:自己动手丰衣足食

为了更好地理解get_plugins()函数的工作原理,我们可以自己动手写一个简单的函数来模拟它的功能。

function my_get_plugins( $plugin_dir ) {
    $plugins = array();
    $files = scandir( $plugin_dir );

    foreach ( $files as $file ) {
        if ( $file === '.' || $file === '..' ) {
            continue;
        }

        $plugin_file = $plugin_dir . '/' . $file;

        if ( is_dir( $plugin_file ) ) {
            // 递归扫描子目录,简化处理,只取第一个php文件
            $sub_files = scandir( $plugin_file );
            foreach($sub_files as $sub_file) {
                if(substr($sub_file, -4) === '.php') {
                    $plugin_file = $plugin_dir . '/' . $file . '/' . $sub_file;
                    break;
                }
            }

        }

        if ( substr( $file, -4 ) === '.php' || (is_dir($plugin_file) && substr($plugin_file, -4) === '.php') ) {
            $plugin_data = my_get_plugin_data( $plugin_file );
            if ( ! empty( $plugin_data['Name'] ) ) {
                $plugins[ $file ] = $plugin_data;
            }
        }
    }

    return $plugins;
}

function my_get_plugin_data( $plugin_file ) {
    $default_headers = array(
        'Name'        => 'Plugin Name',
        'Version'     => 'Version',
        'Description' => 'Description',
        'Author'      => 'Author',
    );

    $file_contents = file_get_contents( $plugin_file );

    $plugin_data = array();
    foreach ( $default_headers as $field => $regex ) {
        if ( preg_match( '/^[ t/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi', $file_contents, $match ) && $match[1] ) {
            $plugin_data[ $field ] = trim( $match[1] );
        } else {
            $plugin_data[ $field ] = '';
        }
    }

    return $plugin_data;
}

// 使用示例
$plugins_dir = WP_PLUGIN_DIR;
$my_plugins = my_get_plugins( $plugins_dir );

echo "<pre>";
print_r( $my_plugins );
echo "</pre>";

这个简单的例子可以让你更直观地了解get_plugins()函数的工作流程。 当然,这个例子只是一个简化版,没有处理所有的情况,比如插件的目录结构、错误处理等。

六、总结:插件管理的艺术

get_plugins()函数是WordPress插件管理的核心函数之一。它通过扫描插件目录,解析插件头部信息,将所有插件的信息整理成一个数组,方便我们在后台管理插件。

函数/代码段 功能描述
get_plugins() 扫描插件目录,解析插件头部信息,返回插件列表。
opendir() 打开目录。
readdir() 读取目录中的文件和子目录。
is_dir() 判断是否是目录。
get_plugin_data() 读取插件文件内容,解析插件头部信息。
get_file_data() 从文件中读取数据,使用正则表达式匹配头部信息。
正则表达式 '/^[ t/*#@]*' . preg_quote( $regex, '/' ) . ':(.*)$/mi',用于匹配插件头部信息。
$default_headers 数组,定义了插件头部信息的字段名和对应的标签。
WP_PLUGIN_DIR 常量,定义了插件目录的路径。

通过对get_plugins()函数的源码剖析,我们不仅了解了它的工作原理,也学习了一些PHP编程技巧,比如目录遍历、文件读取、正则表达式等。 希望今天的分享对大家有所帮助!

各位,下课!

发表回复

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