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'])); } } } }