// +---------------------------------------------------------------------- namespace ywxapp\command; use think\console\command\Make; use think\console\Input; use think\console\input\Argument; use think\console\input\Option; use think\console\Output; /** * Addon 统一脚手架 / 生成器命令 * * 合并原 addon:build / addon:clear / addon:controller / addon:model / * addon:middleware / addon:validate / addon:event / addon:listener / * addon:service / addon:subscribe 十条命令为单入口: * php think addon:make <插件名> -a [--name=<类>] [动作选项] * * @author ywxapp */ class AddonMake extends Make { /** * 当前生成动作对应的类类型(首字母大写),供 getStub/buildClass 使用 * @var string */ protected $currentType = ''; /** * 当前生成动作对应的 stub 关键字 * @var string */ protected $currentStub = ''; /** * 应用基础目录 * @var string */ protected $basePath; /** * {@inheritdoc} */ protected function configure() { $this->setName('addon:make') ->addArgument('addon', Argument::OPTIONAL, 'addon name, e.g. demo (required for build & generators)') ->addOption('action', 'a', Option::VALUE_REQUIRED, 'build|clear|controller|model|middleware|validate|event|listener|service|subscribe') ->addOption('name', 'n', Option::VALUE_OPTIONAL, 'class name for generator actions, e.g. User') ->addOption('api', null, Option::VALUE_NONE, 'controller: generate an api controller class.') ->addOption('plain', null, Option::VALUE_NONE, 'controller: generate an empty controller class.') ->addOption('cache', 'c', Option::VALUE_NONE, 'clear: clear cache file') ->addOption('log', 'l', Option::VALUE_NONE, 'clear: clear log file') ->addOption('dir', 'r', Option::VALUE_NONE, 'clear: clear empty dir') ->setDescription('Unified addon scaffold & class generator (build / clear / controller / model / middleware / validate / event / listener / service / subscribe)'); } protected function execute(Input $input, Output $output): int { $addon = trim((string) $input->getArgument('addon')); $action = strtolower(trim((string) $input->getOption('action'))); if ($action === '') { $output->writeln('Please specify an action via -a/--action, e.g. -a build, -a controller.'); return 1; } // clear 允许插件名为空(清理根 runtime) if ($action === 'clear') { return $this->doClear($addon, $input, $output); } // build 与生成器均要求合法的插件名 if ($addon === '' || ! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $addon)) { $output->writeln('Please provide a valid addon name, e.g. php think addon demo -a ' . $action . ''); return 1; } switch ($action) { case 'build': return $this->doBuild($addon, $output); case 'controller': case 'model': case 'middleware': case 'validate': case 'event': case 'listener': case 'service': case 'subscribe': return $this->doGenerate($action, $addon, $input, $output); default: $output->writeln('Unknown action: ' . $action . ''); return 1; } } /* ----------------------------- 生成器动作 ----------------------------- */ /** * 生成各类插件类文件(controller/model/middleware/validate/event/listener/service/subscribe) * * @return int */ protected function doGenerate(string $action, string $addon, Input $input, Output $output): int { $this->currentType = ucfirst($action); $this->currentStub = $action; $class = trim((string) ($input->getOption('name') ?: '')); if ($class === '' || ! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $class)) { $output->writeln('Please provide a valid class name via --name, e.g. --name=User'); return 1; } $name = 'addon' . '\\' . $addon . '\\' . $action . '\\' . $class; $pathname = $this->app->getRootPath() . ltrim(str_replace('\\', '/', $name), '/') . '.php'; if (is_file($pathname)) { $output->writeln('' . $this->currentType . ':' . $name . ' already exists!'); return 1; } if (! is_dir(dirname($pathname))) { mkdir(dirname($pathname), 0755, true); } file_put_contents($pathname, $this->buildClass($name)); $output->writeln('' . $this->currentType . ':' . $name . ' created successfully.'); return 0; } /** * 根据 stub 构建类内容(覆盖 Make::buildClass 以使用插件命名空间) * * @param string $name 完整类名 addon\\\ * @return string */ protected function buildClass(string $name): string { $stub = file_get_contents($this->getStub()); $namespace = trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\'); $class = str_replace($namespace . '\\', '', $name); return str_replace( ['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'], [ $class, $this->app->config->get('route.action_suffix'), $namespace, $this->app->getNamespace(), ], $stub ); } /** * 根据当前动作与 --api/--plain 选项返回 stub 路径 * * @return string */ protected function getStub(): string { $stubPath = __DIR__ . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR; if ($this->currentStub === 'controller') { if ($this->input->getOption('api')) { return $stubPath . 'controller.api.stub'; } if ($this->input->getOption('plain')) { return $stubPath . 'controller.plain.stub'; } return $stubPath . 'controller.stub'; } return $stubPath . $this->currentStub . '.stub'; } /* ------------------------------- build -------------------------------- */ /** * 脚手架生成插件骨架 * * @return int */ protected function doBuild(string $app, Output $output): int { $this->basePath = $this->app->getRootPath() . 'addon' . DIRECTORY_SEPARATOR; if (is_dir($this->basePath . $app)) { $output->writeln("Addon '{$app}' already exists, ensuring skeleton only."); } $this->buildAddon($app, []); $output->writeln("Addon '{$app}' scaffold ready at addon/{$app}/"); return 0; } /** * 创建应用 * * @param string $app 应用名 * @param array $list 目录结构 * @return void */ protected function buildAddon(string $app, array $list = []): void { $appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : ''); $namespace = 'addon' . ($app ? '\\' . $app : ''); if (! is_dir($appPath)) { mkdir($appPath); } $this->buildAddonFile($app, $namespace); $this->buildInfoFile($app); $this->buildCommonFile($app); $this->buildConfigFile($app); if (! is_dir($appPath . 'controller')) { mkdir($appPath . 'controller'); mkdir($appPath . 'controller/backend'); mkdir($appPath . 'controller/member'); } $this->buildIndexController($app, $namespace); if (! is_dir($appPath . 'lang')) { mkdir($appPath . 'lang'); } if (! is_dir($appPath . 'model')) { mkdir($appPath . 'model'); } if (! is_dir($appPath . 'view')) { mkdir($appPath . 'view'); mkdir($appPath . 'view/frontend'); mkdir($appPath . 'view/backend'); mkdir($appPath . 'view/member'); } if (! is_dir($appPath . 'route')) { mkdir($appPath . 'route'); } $this->buildRouteFile($app, $namespace); $this->buildMenuJson($app); $appPublicPath = public_path() . 'static' . DIRECTORY_SEPARATOR . ($app ? $app . DIRECTORY_SEPARATOR : ''); if (! is_dir($appPublicPath)) { mkdir($appPublicPath, 0755, true); mkdir($appPublicPath . 'img', 0755, true); mkdir($appPublicPath . 'css', 0755, true); mkdir($appPublicPath . 'js', 0755, true); } } /** * 创建插件主类文件 * * @param string $app 应用名 * @param string $namespace 类库命名空间 * @return void */ protected function buildAddonFile(string $app, string $namespace): void { $appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : ''); if (! is_file($appPath . 'Addon.php')) { $content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . 'addon.stub'); $content = str_replace(['{%namespace%}', '{%addon%}', '{%className%}'], [$namespace, $app, 'Addon'], $content); file_put_contents($appPath . 'Addon.php', $content); } } /** * 创建插件信息文件 * * @param string $app 目录 * @return void */ protected function buildInfoFile(string $app): void { $filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'info.php'; if (! is_file($filename)) { $content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . 'info.stub'); $content = str_replace(['{%app%}'], [$app], $content); $this->checkDirBuild(dirname($filename)); file_put_contents($filename, $content); } } /** * 创建菜单 JSON * * @param string $app 应用名 * @return void */ protected function buildMenuJson(string $app): void { $filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'menu.json'; if (! is_file($filename)) { $menu = ["frontend" => [], "member" => [], "backend" => []]; $content = json_encode($menu, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); file_put_contents($filename, $content); } } /** * 创建公共文件 * * @param string $app 目录 * @return void */ public function buildCommonFile(string $app): void { $appPath = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : ''); if (! is_file($appPath . 'common.php')) { file_put_contents($appPath . 'common.php', "basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'config.php'; if (! is_file($filename)) { file_put_contents($filename, "app->config->get('route.controller_suffix') ? 'Controller' : ''; $filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'controller' . DIRECTORY_SEPARATOR . 'Index' . $suffix . '.php'; if (! is_file($filename)) { $content = file_get_contents(__DIR__ . DIRECTORY_SEPARATOR . 'addon' . DIRECTORY_SEPARATOR . 'controller.stub'); $content = str_replace(['{%className%}', '{%actionSuffix%}', '{%namespace%}', '{%app_namespace%}'], ['Index', $suffix, $namespace . '\\controller', $namespace], $content); $this->checkDirBuild(dirname($filename)); file_put_contents($filename, $content); } } /** * 创建路由文件 * * @param string $app 应用名 * @param string $namespace 类库命名空间 * @return void */ public function buildRouteFile(string $app, string $namespace): void { $filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'route' . DIRECTORY_SEPARATOR . 'app.php'; if (! is_file($filename)) { $this->checkDirBuild(dirname($filename)); file_put_contents($filename, "app->getRootPath() . 'runtime' . DIRECTORY_SEPARATOR . ($app ? $app . DIRECTORY_SEPARATOR : ''); if ($input->getOption('cache')) { $path = $runtimePath . 'cache'; } elseif ($input->getOption('log')) { $path = $runtimePath . 'log'; } else { $path = $runtimePath; } $rmdir = $input->getOption('dir') ? true : false; $this->clear(rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, $rmdir); $output->writeln("Clear Successed"); return 0; } /** * 递归清理目录 * * @param string $path 目录 * @param bool $rmdir 是否删除空目录 * @return void */ protected function clear(string $path, bool $rmdir): void { $files = is_dir($path) ? scandir($path) : []; foreach ($files as $file) { if ('.' != $file && '..' != $file && is_dir($path . $file)) { array_map('unlink', glob($path . $file . DIRECTORY_SEPARATOR . '*.*')); if ($rmdir) { rmdir($path . $file); } } elseif ('.gitignore' != $file && is_file($path . $file)) { unlink($path . $file); } } } }