Files
YwxAppThink/ywxapp/command/AddonMake.php
T

436 lines
15 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
// +----------------------------------------------------------------------
// | YwxApp [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Author: ywxapp<admin@ywxapp.cn>
// +----------------------------------------------------------------------
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 <action> [--name=<类>] [动作选项]
*
* @author ywxapp <admin@ywxapp.cn>
*/
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('<error>Please specify an action via -a/--action, e.g. -a build, -a controller.</error>');
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('<error>Please provide a valid addon name, e.g. php think addon demo -a ' . $action . '</error>');
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('<error>Unknown action: ' . $action . '</error>');
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('<error>Please provide a valid class name via --name, e.g. --name=User</error>');
return 1;
}
$name = 'addon' . '\\' . $addon . '\\' . $action . '\\' . $class;
$pathname = $this->app->getRootPath() . ltrim(str_replace('\\', '/', $name), '/') . '.php';
if (is_file($pathname)) {
$output->writeln('<error>' . $this->currentType . ':' . $name . ' already exists!</error>');
return 1;
}
if (! is_dir(dirname($pathname))) {
mkdir(dirname($pathname), 0755, true);
}
file_put_contents($pathname, $this->buildClass($name));
$output->writeln('<info>' . $this->currentType . ':' . $name . ' created successfully.</info>');
return 0;
}
/**
* 根据 stub 构建类内容(覆盖 Make::buildClass 以使用插件命名空间)
*
* @param string $name 完整类名 addon\<addon>\<type>\<Class>
* @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("<comment>Addon '{$app}' already exists, ensuring skeleton only.</comment>");
}
$this->buildAddon($app, []);
$output->writeln("<info>Addon '{$app}' scaffold ready at addon/{$app}/</info>");
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', "<?php" . PHP_EOL . "// 这是系统自动生成的公共文件" . PHP_EOL);
}
}
/**
* 创建插件配置文件(后台「配置」表单数据源)
*
* @param string $app 应用名
* @return void
*/
protected function buildConfigFile(string $app): void
{
$filename = $this->basePath . ($app ? $app . DIRECTORY_SEPARATOR : '') . 'config.php';
if (! is_file($filename)) {
file_put_contents($filename, "<?php" . PHP_EOL
. "// 插件配置项(后台「配置」表单数据源)" . PHP_EOL
. "return [];" . PHP_EOL);
}
}
/**
* 创建首页控制器
*
* @param string $app 应用名
* @param string $namespace 类库命名空间
* @return void
*/
public function buildIndexController(string $app, string $namespace): void
{
$suffix = $this->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, "<?php" . PHP_EOL . "use think\\facade\\Route;" . PHP_EOL . PHP_EOL);
}
}
/**
* 创建目录
*
* @param string $dirname 目录名称
* @return void
*/
public function checkDirBuild(string $dirname): void
{
if (! is_dir($dirname)) {
mkdir($dirname, 0755, true);
}
}
/* ------------------------------- clear -------------------------------- */
/**
* 清理 runtime 文件
*
* @return int
*/
protected function doClear(string $app, Input $input, Output $output): int
{
$runtimePath = $this->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("<info>Clear Successed</info>");
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);
}
}
}
}