mirror of
https://github.com/zhamao-robot/zhamao-framework.git
synced 2026-03-17 20:54:52 +08:00
403 lines
11 KiB
PHP
Executable File
403 lines
11 KiB
PHP
Executable File
#!/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;
|
||
passthru("{$composer} require --quiet jasny/phpdoc-parser", $exit_code);
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 从这里开始是主要生成逻辑
|
||
|
||
require_once $root . '/vendor/autoload.php';
|
||
|
||
use Jasny\PhpdocParser\PhpdocParser;
|
||
use Jasny\PhpdocParser\Set\PhpDocumentor;
|
||
use Jasny\PhpdocParser\Tag\Summery;
|
||
use ZM\Console\Console;
|
||
|
||
var_dump('hi');
|
||
|
||
$opts = getopt('', ['show-warnings']);
|
||
if (array_key_exists('show-warnings', $opts)) {
|
||
$show_warnings = true;
|
||
} else {
|
||
$show_warnings = false;
|
||
}
|
||
|
||
Console::init(4);
|
||
|
||
$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;
|
||
Console::error($message);
|
||
}
|
||
|
||
function warning(string $message)
|
||
{
|
||
global $warnings, $show_warnings;
|
||
$warnings[] = $message;
|
||
if ($show_warnings) {
|
||
Console::warning($message);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取类的元数据
|
||
*
|
||
* 包括类的注释、方法的注释、参数、返回值等
|
||
*/
|
||
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;
|
||
}
|
||
|
||
Console::verbose('正在解析方法:' . $class_name . '::' . $method->getName());
|
||
|
||
// 获取方法的注释并解析
|
||
$doc = $method->getDocComment();
|
||
if (!$doc) {
|
||
warning('找不到文档:' . $class_name . '::' . $method->getName());
|
||
continue;
|
||
}
|
||
try {
|
||
$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 方法名
|
||
* @param array $meta 元数据
|
||
*/
|
||
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";
|
||
}
|
||
$markdown .= "\n";
|
||
}
|
||
|
||
// 返回值
|
||
$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,
|
||
];
|
||
}
|
||
|
||
Console::info('开始生成!');
|
||
|
||
// 遍历类并将元数据添加至数组中
|
||
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) {
|
||
if (!is_array($token)) {
|
||
continue;
|
||
}
|
||
if ($token[0] === T_CLASS && $tokens[$k - 2][0] !== T_NEW) {
|
||
$found = true;
|
||
break;
|
||
}
|
||
if ($k >= 100) {
|
||
break;
|
||
}
|
||
}
|
||
if (!$found) {
|
||
continue;
|
||
}
|
||
|
||
// 获取完整类名
|
||
$path = ltrim($path, $root . '/');
|
||
$class = str_replace(['.php', 'src/', '/'], ['', '', '\\'], $path);
|
||
Console::verbose('正在解析类:' . $class);
|
||
$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));
|
||
}
|
||
Console::verbose('正在生成文档:' . $file);
|
||
$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)) {
|
||
Console::warning('生成过程中发现 ' . count($warnings) . ' 次警告');
|
||
if (!$show_warnings) {
|
||
Console::info('生成器默认忽略了警告,你可以通过 gendoc --show-warnings 来查看警告');
|
||
}
|
||
}
|
||
if (count($errors)) {
|
||
Console::error('生成过程中发现错误:');
|
||
foreach ($errors as $error) {
|
||
Console::error($error);
|
||
}
|
||
}
|
||
|
||
Console::success('API 文档生成完毕');
|
||
Console::success(sprintf('共生成 %d 个类,共 %d 个方法', $class_count, $method_count));
|
||
|
||
// 完成生成,进行善后
|
||
|
||
if ($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 '移除完成';
|
||
}
|
||
exit(0);
|