410 lines
12 KiB
Plaintext
Raw Normal View History

2022-03-27 19:19:05 +08:00
#!/usr/bin/env php
<?php
/**
* 决定是否忽略该类
*
* @param ReflectionClass $class 准备生成的类的反射类
*/
function should_ignore_class(ReflectionClass $class): bool
{
// 注解类
if (($doc = $class->getDocComment()) && strpos($doc, '@Annotation') !== false) {
return true;
}
if (str_ends_with($class->getName(), 'Exception')) {
return true;
}
if (str_contains($class->getName(), '_')) {
return true;
}
return false;
}
if (PHP_VERSION_ID < 70400) {
echo 'PHP 版本必须为 7.4 或以上';
exit(1);
}
function check_composer_executable(string $composer): bool
{
passthru($composer . ' --version 2>/dev/null', $exit_code);
return $exit_code === 0;
}
function find_valid_root(string $current_dir, int $max_loop): string
{
if ($max_loop <= 0) {
return '';
}
if (!file_exists($current_dir . '/composer.json')) {
return find_valid_root(dirname($current_dir), $max_loop - 1);
}
return $current_dir;
}
$root = find_valid_root(getcwd(), 3);
if (empty($root)) {
echo '找不到有效的根目录';
exit(1);
}
echo '根目录: ' . $root . PHP_EOL;
chdir($root);
if (!is_dir($root . '/src/ZM')) {
echo '源码目录不存在';
exit(1);
}
$phpdoc_package_temp_installed = false;
if (file_exists($root . '/vendor/jasny/phpdoc-parser/composer.json')) {
echo '正在使用现有的 PHPDoc 解析库' . PHP_EOL;
} else {
echo 'PHPDoc 解析库不存在,正在尝试安装...' . PHP_EOL;
echo '本次生成完成后将会自动移除' . PHP_EOL;
$composers = [
'runtime/composer', 'composer', 'php composer.phar',
];
foreach ($composers as $composer) {
if (check_composer_executable($composer)) {
echo '正在使用 ' . $composer . ' 安装 PHPDoc 解析库,请稍候...' . PHP_EOL;
2022-04-01 18:48:48 +08:00
passthru("{$composer} require --quiet jasny/phpdoc-parser", $exit_code);
2022-03-27 19:19:05 +08:00
if ($exit_code === 0) {
$phpdoc_package_temp_installed = true;
break;
}
}
}
if (!$phpdoc_package_temp_installed) {
echo '安装失败请手动安装composer require jasny/phpdoc-parser' . PHP_EOL;
exit(1);
}
}
2022-08-22 13:06:48 +08:00
function remove_temp_phpdoc_package()
{
if ($GLOBALS['phpdoc_package_temp_installed']) {
echo '正在移除 PHPDoc 解析库,请稍候...' . PHP_EOL;
passthru('composer remove --quiet jasny/phpdoc-parser', $exit_code);
if ($exit_code !== 0) {
echo '移除失败请手动移除composer remove jasny/phpdoc-parser' . PHP_EOL;
}
echo '移除完成';
}
}
register_shutdown_function('remove_temp_phpdoc_package');
2022-03-27 19:19:05 +08:00
// 从这里开始是主要生成逻辑
require_once $root . '/vendor/autoload.php';
use Jasny\PhpdocParser\PhpdocParser;
use Jasny\PhpdocParser\Set\PhpDocumentor;
use Jasny\PhpdocParser\Tag\Summery;
2022-08-22 13:06:48 +08:00
use ZM\Logger\ConsoleLogger;
2022-03-27 19:19:05 +08:00
$opts = getopt('', ['show-warnings', 'show-debug']);
2022-03-27 19:19:05 +08:00
if (array_key_exists('show-warnings', $opts)) {
$show_warnings = true;
} else {
$show_warnings = false;
}
2022-08-22 13:06:48 +08:00
ob_logger_register(new ConsoleLogger(array_key_exists('show-debug', $opts) ? 'debug' : 'info'));
2022-03-27 19:19:05 +08:00
$errors = [];
$warnings = [];
// 获取源码目录的文件遍历器
$fs = new \RecursiveDirectoryIterator($root . '/src/ZM', FilesystemIterator::SKIP_DOTS);
// 初始化文档解析器
$parser = new PhpdocParser(PhpDocumentor::tags()->with([
new Summery(),
]));
$metas = [];
$class_count = 0;
$method_count = 0;
function error(string $message)
{
global $errors;
$errors[] = $message;
2022-08-22 13:06:48 +08:00
logger()->error($message);
2022-03-27 19:19:05 +08:00
}
function warning(string $message)
{
global $warnings, $show_warnings;
$warnings[] = $message;
if ($show_warnings) {
2022-08-22 13:06:48 +08:00
logger()->warning($message);
2022-03-27 19:19:05 +08:00
}
}
/**
* 获取类的元数据
*
* 包括类的注释、方法的注释、参数、返回值等
*/
function get_class_metas(string $class_name, PhpdocParser $parser): array
{
// 尝试获取反射类
try {
$class = new \ReflectionClass($class_name);
} catch (\ReflectionException $e) {
error('无法获取类 ' . $class_name . ' 的反射类');
return [];
}
// 判断是否略过该类
if (should_ignore_class($class)) {
return [];
}
$metas = [];
// 遍历类方法
foreach ($class->getMethods() as $method) {
if ($method->getDeclaringClass()->getName() !== $class_name) {
continue;
}
2022-08-22 13:06:48 +08:00
logger()->debug('正在解析方法:' . $class_name . '::' . $method->getName());
2022-03-27 19:19:05 +08:00
// 获取方法的注释并解析
$doc = $method->getDocComment();
if (!$doc) {
warning('找不到文档:' . $class_name . '::' . $method->getName());
continue;
}
try {
2022-08-22 13:06:48 +08:00
// 解析器对于多余空格的解析似乎有问题,这里去除一下多余的空格
$doc = preg_replace('/\s(?=\s)/', '\\1', $doc);
2022-03-27 19:19:05 +08:00
$meta = $parser->parse($doc);
} catch (\Exception $e) {
error('解析失败:' . $class_name . '::' . $method->getName() . '' . $e->getMessage());
continue;
}
// 少数情况解析后会带有 */,需要去除
array_walk_recursive($meta, static function (&$item) {
if (is_string($item)) {
$item = trim(str_replace('*/', '', $item));
}
});
// 对比反射方法获取的参数和注释声明的参数
$parameters = $method->getParameters();
$params_in_doc = $meta['params'] ?? [];
foreach ($parameters as $parameter) {
$parameter_name = $parameter->getName();
// 不存在则添加进参数列表中
if (!isset($params_in_doc[$parameter_name])) {
$params_in_doc[$parameter_name] = [
'type' => $parameter->getType()?->getName(),
'description' => '',
];
}
}
// 确保所有参数都有对应的类型和描述
foreach ($params_in_doc as &$param) {
if (!isset($param['type'])) {
$param['type'] = 'mixed';
}
if (!isset($param['description'])) {
$param['description'] = '';
}
}
// 清除引用
unset($param);
$meta['params'] = $params_in_doc;
// 设定方法默认返回值
if (!isset($meta['return'])) {
$meta['return'] = [
'type' => $method->getReturnType()?->getName() ?: 'mixed',
'description' => '',
];
}
// 设定默认描述
if (!isset($meta['return']['description'])) {
$meta['return']['description'] = '';
}
$metas[$method->getName()] = $meta;
}
return $metas;
}
/**
* 将方法的元数据转换为 Markdown 格式
*
* @param string $method 方法名
2022-08-22 13:06:48 +08:00
* @param array $meta 元数据
2022-03-27 19:19:05 +08:00
*/
function convert_meta_to_markdown(string $method, array $meta): string
{
// 方法名作为标题
$markdown = '## ' . $method . "\n\n";
// 构造方法代码块
$markdown .= '```php' . "\n";
// TODO: 适配 private 等修饰符
$markdown .= 'public function ' . $method . '(';
$params = [];
// 添加参数
foreach ($meta['params'] as $param_name => $param_meta) {
$params[] = sprintf('%s $%s', $param_meta['type'] ?? 'mixed', $param_name);
}
$markdown .= implode(', ', $params) . ')';
// 添加返回值
$markdown .= ': ' . $meta['return']['type'];
$markdown .= "\n```\n\n";
// 方法描述
$markdown .= '### 描述' . "\n\n";
$markdown .= ($meta['description'] ?? '作者很懒,什么也没有说') . "\n\n";
// 参数
if (count($meta['params'])) {
$markdown .= '### 参数' . "\n\n";
$markdown .= '| 名称 | 类型 | 描述 |' . "\n";
$markdown .= '| -------- | ---- | ----------- |' . "\n";
foreach ($meta['params'] as $param_name => $param_meta) {
$markdown .= '| ' . $param_name . ' | ' . $param_meta['type'] . ' | ' . $param_meta['description'] . ' |' . "\n";
}
2022-04-01 18:48:48 +08:00
$markdown .= "\n";
2022-03-27 19:19:05 +08:00
}
// 返回值
$markdown .= '### 返回' . "\n\n";
$markdown .= '| 类型 | 描述 |' . "\n";
$markdown .= '| ---- | ----------- |' . "\n";
$markdown .= '| ' . $meta['return']['type'] . ' | ' . $meta['return']['description'] . ' |' . "\n";
return $markdown;
}
function generate_sidebar_config(string $title, array $items): array
{
return [
'title' => $title,
'collapsable' => true,
'children' => $items,
];
}
2022-08-22 13:06:48 +08:00
logger()->info('开始生成!');
2022-03-27 19:19:05 +08:00
// 遍历类并将元数据添加至数组中
foreach (new \RecursiveIteratorIterator($fs) as $file) {
if (!$file->isFile()) {
continue;
}
$path = $file->getPathname();
// 过滤不包含类的文件
$tokens = token_get_all(file_get_contents($path));
$found = false;
foreach ($tokens as $k => $token) {
2022-03-27 19:19:05 +08:00
if (!is_array($token)) {
continue;
}
if ($token[0] === T_CLASS && $tokens[$k - 2][0] !== T_NEW) {
2022-03-27 19:19:05 +08:00
$found = true;
break;
}
if ($k >= 100) {
break;
2022-03-27 19:19:05 +08:00
}
}
if (!$found) {
continue;
}
// 获取完整类名
2022-08-22 13:06:48 +08:00
$path = str_replace($root . '/', '', $path);
2022-03-27 19:19:05 +08:00
$class = str_replace(['.php', 'src/', '/'], ['', '', '\\'], $path);
2022-08-22 13:06:48 +08:00
logger()->debug('正在解析类:' . $class);
2022-03-27 19:19:05 +08:00
$meta = get_class_metas($class, $parser);
// 忽略不包含任何方法的类
if (empty($meta)) {
continue;
}
$metas[$class] = $meta;
++$class_count;
$method_count += count($meta);
}
// 将元数据转换为 Markdown
$markdown = [];
foreach ($metas as $class => $class_metas) {
$markdown[$class] = [];
// 将类名作为页面大标题
$markdown[$class]['class'] = '# ' . $class;
foreach ($class_metas as $method => $meta) {
$markdown[$class][$method] = convert_meta_to_markdown($method, $meta);
}
}
// 生成文档
$docs = $root . '/docs/api/';
foreach ($markdown as $class => $methods) {
$file = $docs . str_replace('\\', '/', $class) . '.md';
// 确保目录存在
if (!file_exists(dirname($file)) && !mkdir($concurrent_directory = dirname($file), 0777, true) && !is_dir($concurrent_directory)) {
throw new \RuntimeException(sprintf('无法建立目录 %s', $concurrent_directory));
}
2022-08-22 13:06:48 +08:00
logger()->debug('正在生成文档:' . $file);
2022-03-27 19:19:05 +08:00
$text = implode("\n\n", $methods);
file_put_contents($file, $text);
}
// 生成文档目录
$children = array_keys($markdown);
$children = str_replace('\\', '/', $children);
$class_tree = [];
foreach ($children as $child) {
$parent = dirname($child);
$class_tree[$parent][] = $child;
}
ksort($class_tree);
$config = 'module.exports = [';
foreach ($class_tree as $parent => $children) {
$encode = json_encode(generate_sidebar_config($parent, $children));
$encode = str_replace('\/', '/', $encode);
$config .= $encode . ',';
}
$config = rtrim($config, ',');
$config .= ']';
$file = $root . '/docs/.vuepress/api.js';
file_put_contents($file, $config);
if (count($warnings)) {
2022-08-22 13:06:48 +08:00
logger()->warning('生成过程中发现 ' . count($warnings) . ' 次警告');
2022-03-27 19:19:05 +08:00
if (!$show_warnings) {
2022-08-22 13:06:48 +08:00
logger()->info('生成器默认忽略了警告,你可以通过 gendoc --show-warnings 来查看警告');
2022-03-27 19:19:05 +08:00
}
}
if (count($errors)) {
2022-08-22 13:06:48 +08:00
logger()->error('生成过程中发现错误:');
2022-03-27 19:19:05 +08:00
foreach ($errors as $error) {
2022-08-22 13:06:48 +08:00
logger()->error($error);
2022-03-27 19:19:05 +08:00
}
}
2022-08-22 13:06:48 +08:00
logger()->notice('API 文档生成完毕');
logger()->notice(sprintf('共生成 %d 个类,共 %d 个方法', $class_count, $method_count));
2022-03-27 19:19:05 +08:00
// 完成生成,进行善后
exit(0);