// +---------------------------------------------------------------------- namespace qiniu\command; use think\console\Command; use think\console\Input; use think\console\input\Option; use think\console\Output; use think\Exception; use think\exception\ErrorException; use think\facade\Db; use think\facade\Config; /** * Class Business * @package crmeb\command */ class MakeAdmin extends Command { protected $stubList = []; private $after = [ 'model' => '', 'services' => 'Services', 'validate' => 'Validate', 'controller' => '', ]; private $deleteTimeField = 'delete_time'; private $addTimeField = 'add_time'; private $sortField = 'sort'; /** * Int类型识别为日期时间的结尾字符,默认会识别为日期文本框 */ protected $intDateSuffix = ['time']; /** * Int类型识别为日期时间的结尾字符,默认会识别为日期文本框 */ protected $intListSuffix = ['images', 'ids', 'list']; protected function configure() { parent::configure(); $this->setName('make') ->addOption('table', 't', Option::VALUE_REQUIRED, 'The name of the table to create') ->addOption('path', 'p', Option::VALUE_OPTIONAL, 'path', null) ->addOption('hidden', 'x', Option::VALUE_OPTIONAL, 'hidden fields', null) ->addOption('force', 'f', Option::VALUE_OPTIONAL, 'force replace file', null) ->addOption('delete', 'd', Option::VALUE_OPTIONAL, 'delete all files', null) ->addOption('sort', 's', Option::VALUE_OPTIONAL, 'sort field', null) ->setDescription('Create a new table class'); } public function execute(Input $input, Output $output) { $table = $input->getOption('table') ?: ''; if (!$table) { throw new Exception('Please enter the table name'); } //自定义控制器 $path = $input->getOption('path'); $force = $input->getOption('force'); $hidden = $input->getOption('hidden'); $delete = $input->getOption('delete'); $sortField = $input->getOption('sort'); $hidden = str_replace(',', ',', $hidden); $hidden = explode(',', $hidden); array_push($hidden, $this->deleteTimeField); $connect = Db::connect('mysql'); $dbname = Config::get('database.connections.mysql.database'); $prefix = Config::get('database.connections.mysql.prefix'); //检查主表 $modelName = $table = stripos($table, $prefix) === 0 ? substr($table, strlen($prefix)) : $table; $modelTableType = 'table'; $modelTableTypeName = $modelTableName = $modelName; $modelTableInfo = $connect->query("SHOW TABLE STATUS LIKE '{$modelTableName}'", [], true); if (!$modelTableInfo) { $modelTableType = 'name'; $modelTableName = $prefix . $modelName; $modelTableInfo = $connect->query("SHOW TABLE STATUS LIKE '{$modelTableName}'", [], true); if (!$modelTableInfo) { throw new Exception("table not found"); } } $modelTableInfo = $modelTableInfo[0]; //模型 list($modelNamespace, $modelName, $modelFile, $modelArr) = $this->getModelData($path, $table); list($serviceNamespace, $serviceName, $serviceFile, $serviceArr) = $this->getServicesData($path, $table); list($controllerNamespace, $controllerName, $controllerFile, $controllerArr, $controllerPath, $resourceName) = $this->getControllerData($path, $table); list($validateNamespace, $validateName, $validateFile, $validateArr) = $this->getValidateData($path, $table); if ($delete) { $readyFiles = [$controllerFile, $modelFile, $validateFile, $serviceFile]; foreach ($readyFiles as $k => $v) { $output->warning($v); } if (!$force) { $output->info("Are you sure you want to delete all those files? Type 'yes' to continue: "); $line = fgets(defined('STDIN') ? STDIN : fopen('php://stdin', 'r')); if (trim($line) != 'yes') { throw new Exception("Operation is aborted!"); } } foreach ($readyFiles as $k => $v) { if (file_exists($v)) { unlink($v); } //删除空文件夹 switch ($v) { case $modelFile: $this->removeEmptyBaseDir($v, $modelArr); break; case $validateFile: $this->removeEmptyBaseDir($v, $validateArr); break; case $serviceArr: $this->removeEmptyBaseDir($v, $serviceArr); break; default: $this->removeEmptyBaseDir($v, $controllerArr); } } $output->info("Delete Successed"); return; } //非覆盖模式时如果存在控制器文件则报错 if (is_file($controllerFile) && !$force) { throw new Exception("controller already exists!\nIf you need to rebuild again, use the parameter --force=true "); } //非覆盖模式时如果存在模型文件则报错 if (is_file($modelFile) && !$force) { throw new Exception("model already exists!\nIf you need to rebuild again, use the parameter --force=true "); } //非覆盖模式时如果存在验证文件则报错 if (is_file($serviceFile) && !$force) { throw new Exception("services already exists!\nIf you need to rebuild again, use the parameter --force=true "); } //非覆盖模式时如果存在验证文件则报错 if (is_file($validateFile) && !$force) { throw new Exception("validate already exists!\nIf you need to rebuild again, use the parameter --force=true "); } //从数据库中获取表字段信息 $sql = "SELECT * FROM `information_schema`.`columns` " . "WHERE TABLE_SCHEMA = ? AND table_name = ? " . "ORDER BY ORDINAL_POSITION"; //加载主表的列 $columnList = $connect->query($sql, [$dbname, $modelTableName]); $fieldArr = []; $priKeyArr = []; foreach ($columnList as $k => $v) { $fieldArr[] = $v['COLUMN_NAME']; if ($v['COLUMN_KEY'] == 'PRI') { $priKeyArr[] = $v['COLUMN_NAME']; } } if (!$priKeyArr) { throw new Exception('Primary key not found!'); } if (count($priKeyArr) > 1) { throw new Exception('Multiple primary key not support!'); } $priKey = reset($priKeyArr); try { $setAttrArr = []; $getAttrArr = []; $getEnumArr = []; $hiddenArr = []; $appendAttrList = []; $exportAttr = []; $validateRules = []; $searchAttr = []; $searchFieldAttr = []; $validateMessage = []; $createParams = []; $order = ''; //循环所有字段,开始构造视图的HTML和JS信息 foreach ($columnList as $k => $v) { $field = $v['COLUMN_NAME']; $itemArr = []; // 这里构建Enum和Set类型的列表数据 if (in_array($v['DATA_TYPE'], ['enum', 'set', 'tinyint'])) { if ($v['DATA_TYPE'] !== 'tinyint') { $itemArr = substr($v['COLUMN_TYPE'], strlen($v['DATA_TYPE']) + 1, -1); $itemArr = explode(',', str_replace("'", '', $itemArr)); } $itemArr = $this->getItemArray($itemArr, $field, $v['COLUMN_COMMENT']); //如果类型为tinyint且有使用备注数据 if ($itemArr && !in_array($v['DATA_TYPE'], ['enum', 'set'])) { $v['DATA_TYPE'] = 'enum'; } } // 语言列表 $langList = []; if ($v['COLUMN_COMMENT'] != '') { $langList = $this->getLangItem($field, $v['COLUMN_COMMENT']); } if (in_array($field, $hidden)) { $this->hiddenAttr($hiddenArr, $field); } if ($v['COLUMN_KEY'] == 'PRI') $exportAttr[$field] = $langList[$field] ?? parseName($field, 1); if ($v['COLUMN_KEY'] != 'PRI' && !in_array($field, $hidden)) { if ($field == $this->addTimeField) { if ($v['DATA_TYPE'] == 'int') { $this->getAttr($getAttrArr, $field, 'add_time'); } else { $this->getAttr($getAttrArr, $field, 'add_time_datetime'); } $this->searchAttr($searchFieldAttr, 'time'); $exportAttr[$field] = $langList[$field] ?? '添加时间'; } else if ($sortField && $field == $sortField || !$sortField && $field == $this->sortField) { $exportAttr[$field] = $langList[$field] ?? '排序'; $order = 'protected $order = ' . "'{$field} desc,{$priKey} desc';"; } else { $inputType = $this->getFieldType($v); // 如果默认值非null,则是一个必选项 if ($inputType == 'select') { $this->getEnum($getEnumArr, $field, $itemArr, $langList); //添加一个获取器 $this->getAttr($getAttrArr, $field, $v['DATA_TYPE'] == 'set' ? 'multiple' : 'select'); if ($v['DATA_TYPE'] == 'set') { $this->setAttr($setAttrArr, $field, $inputType); } $this->setCreate($createParams, $v); $this->appendAttr($appendAttrList, $field); $exportAttr[$field . '_chs'] = $langList[$field] ?? parseName($field, 1); } else if ($inputType == 'list') { $this->getAttr($getAttrArr, $field, $inputType); $this->setAttr($setAttrArr, $field, $inputType); $this->setCreate($createParams, $v); $this->appendAttr($appendAttrList, $field); $exportAttr[$field . '_chs'] = $langList[$field] ?? parseName($field, 1); } elseif ($inputType == 'datetime') { $this->getAttr($getAttrArr, $field, $inputType); $this->setAttr($setAttrArr, $field, $inputType); $this->setCreate($createParams, $v); $this->appendAttr($appendAttrList, $field); $exportAttr[$field . '_chs'] = $langList[$field] ?? parseName($field, 1); } else { $this->setCreate($createParams, $v); $exportAttr[$field] = $langList[$field] ?? parseName($field, 1); } $this->checkValidate($validateRules, $validateMessage, $v, $field, $langList[$field] ?? parseName($field, 1), $itemArr); $this->searchAttrHandel($searchAttr, $searchFieldAttr, $field, $langList[$field] ?? parseName($field, 1), $inputType); } } } //表注释 $tableComment = $modelTableInfo ? $modelTableInfo['Comment'] : ''; $tableComment = mb_substr($tableComment, -1) == '表' ? mb_substr($tableComment, 0, -1) : $tableComment; $data = [ 'table' => $table, 'tableComment' => $tableComment, 'tableName' => $modelTableName, 'resourceName' => $resourceName, 'path' => $controllerPath, 'pk' => $priKey, 'controllerNamespace' => $controllerNamespace, 'controllerName' => $controllerName, 'modelNamespace' => $modelNamespace, 'modelName' => $modelName, 'validateNamespace' => $validateNamespace, 'validateName' => $validateName, 'servicesNamespace' => $serviceNamespace, 'servicesName' => $serviceName, 'modelTableName' => $modelTableName, 'modelTableType' => $modelTableType, 'modelTableTypeName' => $modelTableTypeName, 'softDeleteClassPath' => in_array($this->deleteTimeField, $fieldArr) ? "use think\model\concern\SoftDelete;" : '', 'softDelete' => in_array($this->deleteTimeField, $fieldArr) ? "use SoftDelete;" : '', 'hiddenArrList' => implode(",\n\t", $hiddenArr), 'appendAttrList' => implode(",\n\t", $appendAttrList), 'getEnumList' => implode("\n\n", $getEnumArr), 'getAttrList' => implode("\n\n", $getAttrArr), 'setAttrList' => implode("\n\n", $setAttrArr), 'validateRules' => implode(",\n", $this->handelArrayDate($validateRules)), 'validateMessage' => implode(",\n", $this->handelArrayDate($validateMessage)), 'exportAttrList' => implode(",\n", $this->handelArrayDate($exportAttr)), 'createParams' => implode(",\n\t", $createParams), 'searchFieldAttr' => implode(",\n\t", $searchFieldAttr), 'searchAttrList' => implode("\n\n", $searchAttr), 'order' => $order ]; // 生成控制器文件 $this->writeToFile('controller', $data, $controllerFile); // 生成服务文件 $this->writeToFile('services', $data, $serviceFile); // 生成模型文件 $this->writeToFile('model', $data, $modelFile); // 生成验证文件 $this->writeToFile('validate', $data, $validateFile); } catch (ErrorException $e) { throw new Exception("Code: " . $e->getCode() . "\nLine: " . $e->getLine() . "\nMessage: " . $e->getMessage() . "\nFile: " . $e->getFile()); } } /** * 移除相对的空目录 * @param $parseFile * @param $parseArr * @return bool */ protected function removeEmptyBaseDir($parseFile, $parseArr) { if (count($parseArr) > 1) { $parentDir = dirname($parseFile); for ($i = 0; $i < count($parseArr); $i++) { try { $iterator = new \FilesystemIterator($parentDir); $isDirEmpty = !$iterator->valid(); if ($isDirEmpty) { rmdir($parentDir); $parentDir = dirname($parentDir); } else { return true; } } catch (\UnexpectedValueException $e) { return false; } } } return true; } protected function setCreate(&$createParams, $item) { $field = $item['COLUMN_NAME']; $type = $item['DATA_TYPE']; if ($type == 'set' || $this->isMatchSuffix($field, $this->intListSuffix)) { $createParams[] = <<isMatchSuffix($field, $this->intDateSuffix)) { $default = !($item['COLUMN_DEFAULT'] == '' || $item['COLUMN_DEFAULT'] == 'null') ? $item['COLUMN_DEFAULT'] : 0; $createParams[] = << $v) { $list[] = $k; } $validateArr[$field][] = 'in:' . implode(',', $list); $validateMessage[$field . '.in'] = '请选择正确的' . $fieldName; } if ($item['DATA_TYPE'] == 'set' || $this->isMatchSuffix($field, $this->intListSuffix)) { $validateArr[$field][] = 'array'; $validateMessage[$field . '.array'] = $fieldName . '必须是数组'; } if (in_array($item['DATA_TYPE'], ['bigint', 'int', 'mediumint', 'smallint', 'tinyint']) && !$this->isMatchSuffix($field, $this->intDateSuffix)) { $validateArr[$field][] = 'number'; $validateMessage[$field . '.number'] = $fieldName . '必须是整数'; } if (in_array($item['DATA_TYPE'], ['decimal', 'double', 'float'])) { $validateArr[$field][] = 'float'; $validateMessage[$field . '.float'] = $fieldName . '必须是数字'; } if (stripos($item['COLUMN_TYPE'], 'unsigned') !== false) { $validateArr[$field][] = 'egt:0'; $validateMessage[$field . '.egt'] = $fieldName . '大于等于0'; } if ($this->isMatchSuffix($field, ['phone', 'mobile'])) { $validateArr[$field][] = 'mobile'; $validateMessage[$field . '.mobile'] = $fieldName . '不是有效的手机号码'; } if ($this->isMatchSuffix($field, ['email'])) { $validateArr[$field][] = 'email'; $validateMessage[$field . '.email'] = $fieldName . '不是有效的邮箱地址'; } if ($this->isMatchSuffix($field, ['url', 'link'])) { $validateArr[$field][] = 'url'; $validateMessage[$field . '.url'] = $fieldName . '不是有效的url'; } } protected function handelArrayDate($validate) { $res = []; foreach ($validate as $k => $v) { if (!empty($v)) { if (is_array($v)) $v = implode('|', $v); $res[] = << '{$v}' EOD; } } return $res; } /** * 写入到文件 * @param string $name * @param array $data * @param string $pathname * @return mixed */ protected function writeToFile($name, $data, $pathname) { foreach ($data as $index => &$datum) { $datum = is_array($datum) ? '' : $datum; } unset($datum); $content = $this->getReplacedStub($name, $data); if (!is_dir(dirname($pathname))) { mkdir(dirname($pathname), 0755, true); } return file_put_contents($pathname, $content); } protected function getLangItem($field, $content) { $content = str_replace(',', ',', $content); if (stripos($content, ':') !== false && stripos($content, '=') !== false) { list($fieldLang, $item) = explode(':', $content); $itemArr = [$field => $fieldLang]; foreach (explode(',', $item) as $k => $v) { $valArr = explode('=', $v); if (count($valArr) == 2) { list($key, $value) = $valArr; $itemArr[$field . ' ' . $key] = $value; } } } else { $itemArr = [$field => $content]; } return $itemArr; } protected function getEnum(&$getEnum, $field, $itemArr = [], $langList = []) { $fieldList = $this->getFieldListName($field); $methodName = 'get' . ucfirst($fieldList); foreach ($itemArr as $k => &$v) { $v = $langList[$v] ?? '未知'; } unset($v); $itemString = $this->getArrayString($itemArr); $getEnum[] = << $v) { $is_var = in_array(substr($v, 0, 1), ['$', '_']); if (!$is_var) { $v = str_replace("'", "\'", $v); $k = str_replace("'", "\'", $k); } $stringArr[] = "'" . $k . "' => " . ($is_var ? $v : "'{$v}'"); } return implode(", ", $stringArr); } protected function setAttr(&$setAttr, $field, $inputType = '') { if (!in_array($inputType, ['datetime', 'select', 'list'])) { return; } $return = <<getCamelizeName($field)); if ($inputType == 'datetime') { $return = <<getCamelizeName($field) . 'List'; } protected function getCamelizeName($uncamelized_words, $separator = '_') { $uncamelized_words = $separator . str_replace($separator, " ", strtolower($uncamelized_words)); return ltrim(str_replace(" ", "", ucwords($uncamelized_words)), $separator); } protected function getAttr(&$getAttr, $field, $inputType = '') { if (!in_array($inputType, ['datetime', 'select', 'multiple', 'add_time', 'add_time_datetime', 'list'])) { return; } $attrField = ucfirst($this->getCamelizeName($field)); $getAttr[] = $this->getReplacedStub("mixins" . DS . $inputType, ['field' => $field, 'methodName' => "get{$attrField}ChsAttr", 'listMethodName' => "get{$attrField}List"]); } protected function searchAttrHandel(&$searchAttr, &$searchFieldAttr, $field, $fieldName, $inputType = '') { $type = 'default'; if ($this->isMatchSuffix($field, ['id'])) { $type = 'id'; } else if ($this->isMatchSuffix($field, ['name', 'title', 'info'])) { $type = 'like'; } else if (in_array($inputType, ['datetime'])) { $type = 'time'; } else if (in_array($inputType, ['multiple', 'list'])) { $type = 'find_in_set'; } else if (in_array($inputType, ['select'])) { $type = 'default'; } else { return; } $attrField = ucfirst($this->getCamelizeName($field)); $searchAttr[] = $this->getReplacedStub("search" . DS . $type, ['field' => $field, 'fieldName' => $fieldName, 'methodName' => "search{$attrField}Attr"]); $this->searchAttr($searchFieldAttr, $field); } /** * 获取替换后的数据 * @param string $name * @param array $data * @return string */ protected function getReplacedStub($name, $data) { foreach ($data as $index => &$datum) { $datum = is_array($datum) ? '' : $datum; } unset($datum); $search = $replace = []; foreach ($data as $k => $v) { $search[] = "{%{$k}%}"; $replace[] = $v; } $stubname = $this->getStub($name); if (isset($this->stubList[$stubname])) { $stub = $this->stubList[$stubname]; } else { $this->stubList[$stubname] = $stub = file_get_contents($stubname); } $content = str_replace($search, $replace, $stub); return $content; } /** * 获取基础模板 * @param string $name * @return string */ protected function getStub($name) { return __DIR__ . DS . 'stubs' . DS . $name . '.stub'; } protected function getFieldType(&$v) { $inputType = 'text'; switch ($v['DATA_TYPE']) { case 'enum': case 'set': $inputType = 'select'; break; case 'year': case 'date': case 'time': case 'datetime': case 'timestamp': $inputType = 'datetime'; break; default: break; } $fieldsName = $v['COLUMN_NAME']; // 指定后缀说明也是个时间字段 if ($this->isMatchSuffix($fieldsName, $this->intDateSuffix)) { $inputType = 'datetime'; } // 指定后缀结尾且类型为enum,说明是个单选框 if ($this->isMatchSuffix($fieldsName, $this->intListSuffix)) { $inputType = "list"; } return $inputType; } /** * 判断是否符合指定后缀 * @param string $field 字段名称 * @param mixed $suffixArr 后缀 * @return boolean */ protected function isMatchSuffix($field, $suffixArr) { $suffixArr = is_array($suffixArr) ? $suffixArr : explode(',', $suffixArr); foreach ($suffixArr as $k => $v) { if (preg_match("/{$v}$/i", $field)) { return true; } } return false; } /** * 获取模型相关信息 * @param $module * @param $model * @param $table * @return array */ protected function getModelData($path, $table) { return $this->getParseNameData($path, $table, 'model'); } /** * 获取模型相关信息 * @param $module * @param $model * @param $table * @return array */ protected function getServicesData($path, $table) { return $this->getParseNameData($path, $table, 'services'); } /** * 获取模型相关信息 * @param $module * @param $model * @param $table * @return array */ protected function getValidateData($path, $table) { return $this->getParseNameData($path, $table, 'validate'); } protected function getControllerData($path, $table) { return $this->getParseNameData($path, $table, 'controller'); } /** * 获取已解析相关信息 * @param string $name 自定义名称 * @param string $table 数据表名 * @param string $type 解析类型,本例中为controller、model、validate * @return array */ protected function getParseNameData($path, $table, $type) { $parseName = parseName($table, 1); $parseName = $parseName . $this->after[$type]; if (!$path) { $path = str_replace('_', '/', $table); $path = str_replace(['.', '/', '\\'], '/', $path); $arr = explode('/', $path); array_pop($arr); } else { $path = str_replace(['.', '/', '\\'], '/', $path); $arr = explode('/', $path); } $resourceName = $table; foreach ($arr as $v) { if (strpos($resourceName, $v . '_') === 0) { $resourceName = substr($resourceName, strlen($v . '_')); } } $parseArr = $arr; array_push($parseArr, $parseName); $appNamespace = Config::get('app.app_namespace'); $parseNamespace = "{$appNamespace}\\{$type}" . (in_array($type, ['controller', 'validate']) ? '\\admin' : '') . ($arr ? "\\" . implode("\\", $arr) : ""); $moduleDir = $this->app->getRootPath() . 'app' . DS . $type . DS . ((in_array($type, ['controller', 'validate']) ? 'admin' : '') . DS); $parseFile = $moduleDir . ($arr ? implode(DS, $arr) . DS : '') . $parseName . '.php'; return [$parseNamespace, $parseName, $parseFile, $parseArr, ($arr ? implode('.', $arr) . '.' : ''), $resourceName]; } protected function getItemArray($item, $field, $comment) { $itemArr = []; $comment = str_replace(',', ',', $comment); if (stripos($comment, ':') !== false && stripos($comment, '=') !== false) { list($fieldLang, $item) = explode(':', $comment); $itemArr = []; foreach (explode(',', $item) as $k => $v) { $valArr = explode('=', $v); if (count($valArr) == 2) { list($key, $value) = $valArr; $itemArr[$key] = $field . ' ' . $key; } } } else { foreach ($item as $k => $v) { $itemArr[$v] = is_numeric($v) ? $field . ' ' . $v : $v; } } return $itemArr; } }