Files
MrleeAdmin/backend/core/generator/GeneratorEngine.php
T
2026-08-31 11:33:59 +08:00

691 lines
29 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace core\generator;
use core\generator\factory\GeneratorFactory;
use core\generator\utils\TemplateRenderer;
use core\generator\utils\PathResolver;
use support\Log;
/**
* 代码生成器引擎
* 负责协调各个生成器,处理整体生成流程
*/
class GeneratorEngine
{
/**
* @var array 配置信息
*/
private array $config;
/**
* @var GeneratorFactory 生成器工厂
*/
private GeneratorFactory $generatorFactory;
/**
* @var TemplateRenderer 模板渲染器
*/
private TemplateRenderer $templateRenderer;
/**
* @var PathResolver 路径解析器
*/
private PathResolver $pathResolver;
/**
* 构造函数
* @param array|object $config 配置信息(支持数组或对象)
*/
public function __construct($config)
{
// 解析配置数据
$this->config = $this->parseConfig($config);
// 合并默认场景配置
$this->mergeDefaultScenes();
$this->generatorFactory = new GeneratorFactory();
$this->templateRenderer = new TemplateRenderer();
$this->pathResolver = new PathResolver();
}
/**
* 解析配置数据
* @param array|object $config 配置信息
* @return array 解析后的配置数组
*/
private function parseConfig($config): array
{
if (is_object($config)) {
// 如果是对象,转换为数组
$parsedConfig = (array) $config;
} elseif (is_array($config)) {
// 如果是数组,直接使用
$parsedConfig = $config;
} else {
// 其他类型,返回空数组
return [];
}
// 如果有 basic 字段,使用其中的值
if (isset($parsedConfig['basic']) && is_array($parsedConfig['basic'])) {
$basic = $parsedConfig['basic'];
if (isset($basic['table_name']) && !empty($basic['table_name'])) {
$parsedConfig['package_name'] = $basic['table_name'];
} elseif (isset($basic['module_name']) && !empty($basic['module_name'])) {
$parsedConfig['package_name'] = $basic['module_name'];
}
if (isset($basic['class_name']) && !empty($basic['class_name'])) {
$parsedConfig['class_name'] = $basic['class_name'];
} elseif (isset($basic['table_name']) && !empty($basic['table_name'])) {
$parsedConfig['class_name'] = $basic['table_name'];
}
if (isset($basic['table_comment']) && !empty($basic['table_comment'])) {
$parsedConfig['table_comment'] = $basic['table_comment'];
}
if (isset($basic['class_name_comment']) && !empty($basic['class_name_comment'])) {
$parsedConfig['class_name_comment'] = $basic['class_name_comment'];
}
if (isset($basic['plugin_name']) && !empty($basic['plugin_name'])) {
$parsedConfig['plugin_name'] = $basic['plugin_name'];
$parsedConfig['namespace'] = $basic['plugin_name'];
}
}
// 如果顶层有 table_name 字段,使用它作为 package_name
if (isset($parsedConfig['table_name']) && !empty($parsedConfig['table_name']) && !isset($parsedConfig['package_name'])) {
$parsedConfig['package_name'] = $parsedConfig['table_name'];
} elseif (isset($parsedConfig['module_name']) && !empty($parsedConfig['module_name']) && !isset($parsedConfig['package_name'])) {
$parsedConfig['package_name'] = $parsedConfig['module_name'];
}
// 如果顶层有 table_name 字段,使用它作为 class_name(如果没有设置 class_name)
if (isset($parsedConfig['table_name']) && !empty($parsedConfig['table_name']) && !isset($parsedConfig['class_name'])) {
$parsedConfig['class_name'] = $parsedConfig['table_name'];
}
// 处理 template 参数:如果没有传入 template,但传入了 plugin_name,则使用 plugin_name 作为 template 值
if (!isset($parsedConfig['template']) && isset($parsedConfig['plugin_name']) && !empty($parsedConfig['plugin_name'])) {
$parsedConfig['template'] = $parsedConfig['plugin_name'];
$parsedConfig['namespace'] = $parsedConfig['plugin_name'];
} elseif (!isset($parsedConfig['template'])) {
// 如果都没有传入,则使用默认值 app
$parsedConfig['template'] = 'app';
} elseif ($parsedConfig['template'] !== 'app') {
// 如果传入了 template 且不是 app,则使用 template 作为 namespace
$parsedConfig['namespace'] = $parsedConfig['template'];
}
// 如果传入了 template,则直接使用它,优先级高于 plugin_name
// 确保 class_name 是大驼峰格式
if (isset($parsedConfig['class_name']) && !empty($parsedConfig['class_name'])) {
$parsedConfig['class_name'] = str_replace(' ', '', ucwords(str_replace('_', ' ', $parsedConfig['class_name'])));
}
// 验证必须参数
$this->validateRequiredParams($parsedConfig);
return $parsedConfig;
}
/**
* 验证必须参数
* @param array $config 配置数组
* @throws Exception 如果缺少必须参数
*/
private function validateRequiredParams(array &$config): void
{
// 验证 package_name(模块名)
if (!isset($config['package_name']) || empty($config['package_name'])) {
throw new \Exception('Missing required parameter: package_name (table_name)');
}
// 验证 class_name(类名)
if (!isset($config['class_name']) || empty($config['class_name'])) {
throw new \Exception('Missing required parameter: class_name');
}
// 验证 template
if (!isset($config['template']) || empty($config['template'])) {
$config['template'] = 'app';
}
}
/**
* 合并默认场景配置
* @return void
*/
private function mergeDefaultScenes(): void
{
// 如果未设置场景类型,默认同时生成前端和后端代码
if (!isset($this->config['scene_types']) && !isset($this->config['scene_type'])) {
$this->config['scene_types'] = ['backend', 'admin'];
}
// 如果未设置文件类型映射,设置默认值
if (!isset($this->config['file_types_map'])) {
$this->config['file_types_map'] = [
'backend' => ['controller', 'model', 'service', 'dao', 'validate', 'request_form', 'request_query', 'response'],
'admin' => ['api', 'api_model', 'view', 'view_schema', 'lang'],
];
}
}
/**
* 预览代码生成
* @return array 预览结果
*/
public function preview(): array
{
$result = [];
$sceneTypes = $this->config['scene_types'] ?? [$this->config['scene_type'] ?? 'backend'];
$fileTypesMap = $this->config['file_types_map'] ?? [
'backend' => ['controller', 'model', 'service', 'dao', 'validate', 'request_form', 'request_query', 'response'],
'admin' => ['api', 'api_model', 'view', 'view_schema', 'lang'],
];
// 生成每种场景类型
foreach ($sceneTypes as $sceneType) {
try {
// 获取场景生成器
$sceneGenerator = $this->generatorFactory->createSceneGenerator($sceneType, $this->config);
// 获取当前场景的文件类型
$fileTypes = $fileTypesMap[$sceneType] ?? $this->config['file_types'] ?? ['controller', 'model', 'service', 'dao', 'validate', 'request_form', 'request_query','response'];
// 生成每种文件类型
foreach ($fileTypes as $fileType) {
try {
// 获取文件类型生成器
$fileGenerator = $this->generatorFactory->createFileGenerator($fileType, $this->config);
// 生成文件内容
$content = $fileGenerator->generateContent();
// 获取文件扩展名
$extension = $fileGenerator->getFileExtension();
// 生成文件路径
$filePath = $sceneGenerator->generateFilePath($fileType, $extension);
// 添加到结果中
$result[] = [
'name' => basename($filePath),
'type' => $this->getFileType($filePath),
'content' => $content,
'file_dir' => $this->getRelativePath(dirname($filePath), $sceneType),
'scene_type' => $sceneType,
];
// 处理 DTO 特殊情况:同时预览 QueryRequest
// if ($fileType === 'request_query' && method_exists($fileGenerator, 'getQueryRequestContent')) {
// try {
// // 获取 QueryRequest 内容
// $queryContent = $fileGenerator->getQueryRequestContent();
//
// if (!empty($queryContent)) {
// // 生成 QueryRequest 文件路径(与 FormRequest 在同一目录)
// $formDir = dirname($filePath);
// $fileName = basename($filePath);
// $queryFileName = str_replace('FormRequest', 'QueryRequest', $fileName);
// $queryFilePath = $formDir . DS . $queryFileName;
//
// // 添加到结果中
// $result[] = [
// 'name' => basename($queryFilePath),
// 'type' => $this->getFileType($queryFilePath),
// 'content' => $queryContent,
// 'file_dir' => $this->getRelativePath(dirname($queryFilePath), $sceneType),
// 'scene_type' => $sceneType,
// ];
// }
// } catch (\Exception $e) {
// // 记录错误但继续执行
// $result[] = [
// 'name' => 'request_query',
// 'type' => 'error',
// 'content' => 'Error generating QueryRequest: ' . $e->getMessage(),
// 'file_dir' => '',
// 'scene_type' => $sceneType,
// ];
// }
// }
} catch (\Exception $e) {
// 记录错误但继续执行
$result[] = [
'name' => $fileType,
'type' => 'error',
'content' => 'Error: ' . $e->getMessage(),
'file_dir' => '',
'scene_type' => $sceneType,
];
}
}
} catch (\Exception $e) {
// 记录场景错误
$result[] = [
'name' => $sceneType,
'type' => 'error',
'content' => 'Scene Error: ' . $e->getMessage(),
'file_dir' => '',
'scene_type' => $sceneType,
];
}
}
return $result;
}
/**
* 部署代码生成
* @return array 部署结果
*/
public function deploy(): array
{
$result = [];
$sceneTypes = $this->config['scene_types'] ?? [$this->config['scene_type'] ?? 'backend'];
$fileTypesMap = $this->config['file_types_map'] ?? [
'backend' => ['controller', 'model', 'service', 'dao', 'validate', 'response', 'request_form','request_query'],
'admin' => ['api', 'api_model', 'view', 'view_schema', 'lang'],
];
// 生成每种场景类型
foreach ($sceneTypes as $sceneType) {
try {
// 获取场景生成器
$sceneGenerator = $this->generatorFactory->createSceneGenerator($sceneType, $this->config);
// 获取当前场景的文件类型
$fileTypes = $fileTypesMap[$sceneType] ?? $this->config['file_types'] ?? ['controller', 'model', 'service', 'dao', 'validate', 'schema', 'dto'];
// 生成每种文件类型
foreach ($fileTypes as $fileType) {
try {
// 获取文件类型生成器
$fileGenerator = $this->generatorFactory->createFileGenerator($fileType, $this->config);
// 生成文件内容
$content = $fileGenerator->generateContent();
// 验证内容是否为空
if (empty($content)) {
throw new \Exception('Generated content is empty for file type: ' . $fileType);
}
// 获取文件扩展名
$extension = $fileGenerator->getFileExtension();
// 生成文件路径
$filePath = $sceneGenerator->generateFilePath($fileType, $extension);
// 确保目录存在
$dirPath = dirname($filePath);
if (!is_dir($dirPath)) {
mkdir($dirPath, 0777, true);
}
// 写入文件
$written = file_put_contents($filePath, $content);
if ($written === false) {
throw new \Exception('Failed to write file: ' . $filePath);
}
// 添加到结果中
$result[] = [
'file_type' => $fileType,
'file_path' => $filePath,
'status' => 'success',
'scene_type' => $sceneType,
];
// 处理 DTO 特殊情况:同时生成 QueryRequest
// if ($fileType === 'dto' && method_exists($fileGenerator, 'getQueryRequestContent')) {
// try {
// // 获取 QueryRequest 内容
// $queryContent = $fileGenerator->getQueryRequestContent();
//
// if (!empty($queryContent)) {
// // 生成 QueryRequest 文件路径(与 FormRequest 在同一目录)
// $formDir = dirname($filePath);
// $fileName = basename($filePath);
// $queryFileName = str_replace('FormRequest', 'QueryRequest', $fileName);
// $queryFilePath = $formDir . DS . $queryFileName;
//
// // 确保目录存在
// if (!is_dir($formDir)) {
// mkdir($formDir, 0777, true);
// }
//
// // 写入文件
// $queryWritten = file_put_contents($queryFilePath, $queryContent);
// if ($queryWritten !== false) {
// $result[] = [
// 'file_type' => 'dto_query',
// 'file_path' => $queryFilePath,
// 'status' => 'success',
// 'scene_type' => $sceneType,
// ];
// }
// }
// } catch (\Exception $e) {
// // 记录错误但继续执行
// $result[] = [
// 'file_type' => 'dto_query',
// 'status' => 'error',
// 'message' => 'Error generating QueryRequest: ' . $e->getMessage(),
// 'scene_type' => $sceneType,
// ];
// }
// }
} catch (\Exception $e) {
// 记录错误但继续执行
$result[] = [
'file_type' => $fileType,
'status' => 'error',
'message' => $e->getMessage(),
'scene_type' => $sceneType,
];
}
}
} catch (\Exception $e) {
// 记录场景错误
$result[] = [
'file_type' => $sceneType,
'status' => 'error',
'message' => 'Scene Error: ' . $e->getMessage(),
'scene_type' => $sceneType,
];
}
}
return $result;
}
/**
* 获取文件类型
* @param string $filePath 文件路径
* @return string 文件类型
*/
private function getFileType(string $filePath): string
{
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
return $extension ?: 'unknown';
}
/**
* 获取相对路径
* @param string $fullPath 完整路径
* @param string $sceneType 场景类型
* @return string 相对路径
*/
private function getRelativePath(string $fullPath, string $sceneType): string
{
if ($sceneType === 'admin') {
// 前端场景
$template = $this->config['template'] ?? 'app';
// 无论是 app 模式还是插件模式,前端代码都统一生成在 frontend/admin 目录下
$frontendRoot = dirname(base_path()) . DS . 'frontend' . DS . 'admin';
$path = 'frontend/admin' . str_replace($frontendRoot, '', $fullPath);
} else {
// 后端场景,使用当前后端根目录名称作为前缀
$backendRootName = basename(base_path());
$path = $backendRootName . str_replace(base_path(), '', $fullPath);
}
// 调整斜杠为 `/`
$path = str_replace('\\', '/', $path);
// 确保路径末尾有 `/`
if (!empty($path) && substr($path, -1) !== '/') {
$path .= '/';
}
return $path;
}
/**
* 生成文件下载包
*
* @return array 下载包信息
* @throws \Exception
*/
public function download(): array
{
// 验证模块命名
$this->validateModuleName($this->config);
$result = [];
$sceneTypes = $this->config['scene_types'] ?? [$this->config['scene_type'] ?? 'backend'];
$fileTypesMap = $this->config['file_types_map'] ?? [
'backend' => ['controller', 'model', 'service', 'dao', 'validate', 'request_form', 'request_query','response'],
'admin' => ['api', 'api_model', 'view', 'view_schema', 'lang'],
];
// 创建临时目录
$tempDir = sys_get_temp_dir() . DS . 'generator_' . uniqid();
if (!mkdir($tempDir, 0777, true)) {
throw new \Exception('Failed to create temporary directory');
}
// 生成每种场景类型的文件
foreach ($sceneTypes as $sceneType) {
try {
// 获取场景生成器
$sceneGenerator = $this->generatorFactory->createSceneGenerator($sceneType, $this->config);
// 获取当前场景的文件类型
$fileTypes = $fileTypesMap[$sceneType] ?? $this->config['file_types'] ?? ['controller', 'model', 'service', 'dao', 'validate', 'schema', 'dto'];
// 生成每种文件类型
foreach ($fileTypes as $fileType) {
try {
// 获取文件类型生成器
$fileGenerator = $this->generatorFactory->createFileGenerator($fileType, $this->config);
// 生成文件内容
$content = $fileGenerator->generateContent();
// 验证内容是否为空
if (empty($content)) {
throw new \Exception('Generated content is empty for file type: ' . $fileType);
}
// 获取文件扩展名
$extension = $fileGenerator->getFileExtension();
// 生成文件路径
$filePath = $sceneGenerator->generateFilePath($fileType, $extension);
// 构建临时文件路径
$relativePath = str_replace(dirname(base_path()), '', $filePath);
$relativePath = ltrim($relativePath, DS);
$tempFilePath = $tempDir . DS . $relativePath;
// 确保目录存在
$tempDirPath = dirname($tempFilePath);
if (!is_dir($tempDirPath)) {
mkdir($tempDirPath, 0777, true);
}
// 写入临时文件
if (file_put_contents($tempFilePath, $content) === false) {
throw new \Exception('Failed to write temporary file: ' . $tempFilePath);
}
// 处理 DTO 特殊情况:同时生成 QueryRequest
if ($fileType === 'dto' && method_exists($fileGenerator, 'getQueryRequestContent')) {
try {
// 获取 QueryRequest 内容
$queryContent = $fileGenerator->getQueryRequestContent();
if (!empty($queryContent)) {
// 生成 QueryRequest 文件路径
$formDir = dirname($filePath);
$fileName = basename($filePath);
$queryFileName = str_replace('FormRequest', 'QueryRequest', $fileName);
$queryFilePath = $formDir . DS . $queryFileName;
// 构建临时文件路径(与 FormRequest 处理方式一致)
$queryRelativePath = str_replace(dirname(base_path()), '', $queryFilePath);
$queryRelativePath = ltrim($queryRelativePath, DS);
$tempQueryFilePath = $tempDir . DS . $queryRelativePath;
// 确保目录存在
$tempQueryDir = dirname($tempQueryFilePath);
if (!is_dir($tempQueryDir)) {
mkdir($tempQueryDir, 0777, true);
}
// 写入临时文件
if (file_put_contents($tempQueryFilePath, $queryContent) === false) {
throw new \Exception('Failed to write temporary QueryRequest file: ' . $tempQueryFilePath);
}
}
} catch (\Exception $e) {
Log::error('Error generating QueryRequest: ' . $e->getMessage(), [
'file_type' => $fileType,
'trace' => $e->getTraceAsString()
]);
}
}
} catch (\Exception $e) {
Log::error('Error generating file: ' . $e->getMessage(), [
'file_type' => $fileType,
'trace' => $e->getTraceAsString()
]);
}
}
} catch (\Exception $e) {
Log::error('Error in scene generation: ' . $e->getMessage(), [
'scene_type' => $sceneType,
'trace' => $e->getTraceAsString()
]);
}
}
// 创建 zip 文件
$zipFileName = 'generator_' . uniqid() . '.zip';
$zipFilePath = sys_get_temp_dir() . DS . $zipFileName;
if ($this->createZip($tempDir, $zipFilePath)) {
// 清理临时目录
$this->cleanupDir($tempDir);
// 确保返回的路径是绝对路径且格式正确
$zipFilePath = realpath($zipFilePath);
if (!$zipFilePath) {
throw new \Exception('Failed to get real path for zip file');
}
// 检查zip文件大小,确保不是空文件
$fileSize = filesize($zipFilePath);
if ($fileSize < 22) { // 空zip文件的最小大小约为22字节
throw new \Exception('Generated zip file is empty');
}
// 确保路径使用Windows兼容的反斜杠格式
$zipFilePath = str_replace('/', '\\', $zipFilePath);
return [
'status' => 'success',
'file_path' => $zipFilePath,
'file_name' => $zipFileName,
'file_size' => $fileSize,
];
} else {
// 清理临时目录
$this->cleanupDir($tempDir);
throw new \Exception('Failed to create zip file');
}
}
/**
* 创建 zip 文件
* @param string $sourceDir 源目录
* @param string $zipFilePath zip 文件路径
* @return bool 是否成功
*/
private function createZip(string $sourceDir, string $zipFilePath): bool
{
$zip = new \ZipArchive();
if ($zip->open($zipFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
return false;
}
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($sourceDir),
\RecursiveIteratorIterator::LEAVES_ONLY
);
foreach ($files as $file) {
if (!$file->isDir()) {
$filePath = $file->getRealPath();
$relativePath = substr($filePath, strlen($sourceDir) + 1);
$zip->addFile($filePath, $relativePath);
}
}
return $zip->close();
}
/**
* 清理目录
* @param string $dir 目录路径
*/
private function cleanupDir(string $dir): void
{
if (is_dir($dir)) {
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . DS . $file;
if (is_dir($path)) {
$this->cleanupDir($path);
} else {
unlink($path);
}
}
rmdir($dir);
}
}
/**
* 验证模块命名
* @param array $config 生成配置
* @throws \Exception 验证失败时抛出异常
*/
private function validateModuleName(array $config): void
{
// 直接读取配置文件
$configFile = __DIR__ . '/config/app.php';
$restrictions = file_exists($configFile) ? include $configFile : [];
// 获取模块命名限制配置
$restrictions = $restrictions['module_name_restrictions'] ?? [];
// 如果未启用验证,直接返回
if (!isset($restrictions['enabled']) || !$restrictions['enabled']) {
return;
}
// 获取模块名
$moduleName = $config['package_name'] ?? null;
if (!$moduleName) {
return;
}
// 检查是否为保留名称
if (isset($restrictions['reserved_names']) && is_array($restrictions['reserved_names'])) {
if (in_array(strtolower($moduleName), array_map('strtolower', $restrictions['reserved_names']))) {
throw new \Exception(sprintf('Module name "%s" is reserved and cannot be used', $moduleName));
}
}
// 检查命名格式
if (isset($restrictions['pattern']) && $restrictions['pattern']) {
if (!preg_match($restrictions['pattern'], $moduleName)) {
throw new \Exception(sprintf('Module name "%s" is invalid. It should match pattern: %s', $moduleName, $restrictions['pattern']));
}
}
}
}