Files
swiftadmin/app/admin/controller/developer/Curd.php
2024-07-13 12:53:20 +08:00

805 lines
27 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
declare(strict_types=1);
// +----------------------------------------------------------------------
// | swiftAdmin 极速开发框架 [基于WebMan开发]
// +----------------------------------------------------------------------
// | Copyright (c) 2020-2030 http://www.swiftadmin.net
// +----------------------------------------------------------------------
// | swiftAdmin.net High Speed Development Framework
// +----------------------------------------------------------------------
// | Author: meystack <coolsec@foxmail.com> Apache 2.0 License
// +----------------------------------------------------------------------
namespace app\admin\controller\developer;
use app\admin\model\developer\Generate;
use app\AdminController;
use app\common\model\system\AdminRules;
use system\Form;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
use think\facade\Db;
use think\helper\Str;
/**
* 一键CURD管理
* <!--Developer-->
* Class Curd
* @package app\admin\controller\developer
*/
class Curd extends AdminController
{
/**
* 数据表前缀
*
* @var mixed
*/
public mixed $prefix = 'sa_';
/**
* 获取菜单
*
* @var array
*/
public array $menus = [];
/**
* 关联表信息
*
* @var array
*/
public array $relation = [];
/**
* 函数体
*
* @var array
*/
public array $methods = [];
/**
* 模板路径
*
* @var string
*/
public string $templatePath = '';
/**
* 模板文件
*
* @var array
*/
public array $templateFiles = [];
/**
* 添加时间字段
* @var string
*/
protected string $createTimeField = 'create_time';
/**
* 更新时间字段
* @var string
*/
protected string $updateTimeField = 'update_time';
/**
* 软删除时间字段
* @var string
*/
protected string $deleteTimeField = 'delete_time';
/**
* 过滤默认模板
*
* @var array
*/
public array $filterMethod = ['index', 'add', 'edit', 'del', 'status'];
/**
* 限定特定组件
*
* @var array
*/
public array $mustbeComponent = ['set', 'text', 'json'];
/**
* 修改器字段
*
* @var array
*/
public array $modifyFieldAttr = ['set', 'text', 'json'];
/**
* 保留字段
*
* @var string
*/
public string $keepField = 'status';
/**
* 查询字段[SELECT]
*
* @var array
*/
public array $dropdown = ['radio', 'checkbox', 'select'];
/**
* COLS换行符
*
* @var string
*/
public string $commaEol = ',' . PHP_EOL;
/**
* 受保护的表
* 禁止CURD操作
* @var array
*/
protected array $protectTable = [
"admin", "admin_access", "admin_group", "admin_rules", "company", "department",
"dictionary", "generate", "jobs", "user", "user_group", "user_third", "user_validate"
];
/**
* 类保留关键字
*
* @var array
*/
protected array $internalKeywords = [
'abstract', 'and', 'array', 'as', 'break', 'callable', 'case', 'catch',
'class', 'clone', 'const', 'continue', 'declare', 'default', 'die', 'do', 'echo', 'else',
'elseif', 'empty', 'enddeclare', 'endfor', 'endforeach', 'endif', 'endswitch', 'endwhile',
'eval', 'exit', 'extends', 'final', 'for', 'foreach', 'function', 'global', 'goto', 'if', 'implements',
'include', 'include_once', 'instanceof', 'insteadof', 'interface', 'isset', 'list', 'namespace', 'new',
'or', 'print', 'private', 'protected', 'public', 'require', 'require_once', 'return', 'static', 'switch',
'throw', 'trait', 'try', 'unset', 'use', 'var', 'while', 'xor', 'yield', 'readonly', 'match', 'fn'
];
// 初始化操作
public function __construct()
{
parent::__construct();
$this->model = new Generate();
$this->prefix = function_exists('get_env') ? get_env('DATABASE_PREFIX') : getenv('DATABASE_PREFIX');
}
/**
* 生成CURD代码
* @return \support\Response
* @throws \think\db\exception\DataNotFoundException
* @throws \think\db\exception\DbException
* @throws \think\db\exception\ModelNotFoundException
*/
public function build(): \support\Response
{
$id = input('id');
$data = $this->model->find($id);
if ($data['status'] && !$data['force']) {
return $this->error('该表已经生成过了');
}
$table = str_replace($this->prefix, '', $data['table']);
if ($this->filterSystemTable($table)) {
return $this->error('禁止操作系统表');
}
// 命名空间
$replaces = [];
$controller = $data['controller'];
$module = $data['global'] ? 'common' : 'admin';
$element = ['controller', 'model', 'validate'];
try {
foreach ($element as $key => $item) {
$result = $this->parseNameData($item == 'controller' ? 'admin' : $module, $controller, $key ? $table : '', $item);
list($replaces[$item . 'Name'], $replaces[$item . 'Namespace'], $replaces[$item . 'File']) = $result;
}
$this->getTemplatePath($controller);
list($this->menus, $this->methods, $this->templateFiles) = $this->getMenuMethods($data->toArray());
// 获取字段
$adviceField = [];
$adviceSearch = [];
$everySearch = [];
// 字段属性值
$colsFields = [];
$fieldAttrArr = [];
// 表单设计
$formDesign = [];
$formItem = [];
$formType = $data['formType'];
if (!empty($data['formDesign'])) {
$formDesign = json_decode($data['formDesign'], true);
}
$this->tableFields = Db::name($table)->getFields();
$listFields = explode(',', $data['listField']);
foreach ($this->tableFields as $key => $value) {
$field = $value['name'];
$comment = str_replace(':', ';', $value['comment']);
if (empty($comment)) {
return $this->error($field . " 字段注释不能为空");
}
$this->tableFields[$key]['title'] = explode(';', $comment)[0];
// 是否存在状态字段
if ($field == $this->keepField) {
$adviceSearch[] = $field;
}
// 获取字段类型
$everySearch[] = $field;
$type = explode('(', $value['type'])[0];
// 限定组件类型
if (in_array($type, $this->mustbeComponent)) {
$this->validComponent($field, $type, $formDesign);
}
if (in_array($type, $this->modifyFieldAttr)) {
$fieldAttrArr[] = $this->getFieldAttrArr($field, $type);
}
if (empty($adviceField)
|| ($adviceField['type'] != 'varchar' && $type == 'varchar')) {
$adviceField = [
'field' => $field,
'type' => $type,
];
}
if (in_array($field, $listFields)) {
$colsFields[] = [
'field' => $field,
'title' => '{:__("' . $this->tableFields[$key]['title'] . '")}',
];
}
}
// 推荐搜索片段
$adviceSearch[] = $adviceField['field'];
$adviceSearchHtml = $this->getAdviceSearch($adviceSearch, $formDesign);
// 获取全部搜索字段
$everySearch = array_diff($everySearch, $adviceSearch);
$everySearchHtml = $this->getAdviceSearch($everySearch, $formDesign);
$controller = substr($controller, 0, (strrpos($controller, '/') + 1));
$colsListArr = $this->getColsListFields($colsFields, $formDesign);
$replaces['table'] = $table;
$replaces['title'] = $data['title'];
$replaces['pluginClass'] = $data['plugin'];
$replaces['controller'] = $controller;
$replaces['controllerDiy'] = $this->getMethodString($this->methods);
$replaces['colsListArr'] = $colsListArr;
$replaces['fieldAttrArr'] = implode(PHP_EOL . PHP_EOL, $fieldAttrArr);
$replaces['adviceSearchHtml'] = $adviceSearchHtml;
$replaces['everySearchHtml'] = $everySearchHtml;
$replaces['relationMethodList'] = $this->getRelationMethodList($data['relation']);
$replaces['FormArea'] = $data['width'] . ',' . $data['height'];
$replaces['softDelete'] = array_key_exists($this->deleteTimeField, $this->tableFields) ? "use SoftDelete;" : '';
$replaces['softDeleteClassPath'] = array_key_exists($this->deleteTimeField, $this->tableFields) ? "use think\model\concern\SoftDelete;" : '';
$replaces['createTime'] = array_key_exists($this->createTimeField, $this->tableFields) ? "'$this->createTimeField'" : 'false';
$replaces['updateTime'] = array_key_exists($this->updateTimeField, $this->tableFields) ? "'$this->updateTimeField'" : 'false';
$replaces['deleteTime'] = array_key_exists($this->deleteTimeField, $this->tableFields) ? "'$this->deleteTimeField'" : 'false';
// 生成控制器/模型/验证器规则
foreach ($element as $index => $item) {
if ($index == 0
&& (!$data['create'] || !$data['listField'])) {
continue;
}
$code = read_file($this->getStubTpl($item));
foreach ($replaces as $key => $value) {
$code = str_replace("{%$key%}", $value, $code);
}
write_file($replaces[$item . 'File'], $code);
}
// 生成表单元素
$template = $formType ? 'add' : 'inside';
$formHtml = read_file($this->getStubTpl($template));
if (!empty($formDesign) && $data['listField']) {
foreach ($formDesign as $key => $value) {
$formItem[$key] = Form::itemElem($value, $formType);
}
$formItem = implode(PHP_EOL, $formItem);
$formHtml = str_replace(['{formItems}', '{pluginClass}'], [$formItem, $replaces['pluginClass']], $formHtml);
$formType && write_file($this->templatePath . 'add.html', $formHtml);
}
// 生成首页模板
$indexHtml = read_file($this->getStubTpl($formType ? 'index' : 'index_inside'));
if (!empty($data['listField'])) {
$replaces['editforms'] = $formType ? '' : $formHtml;
foreach ($replaces as $key => $value) {
$indexHtml = str_replace("{%$key%}", $value, $indexHtml);
}
if (empty($formDesign)) {
$indexHtml = preg_replace('/<!--formBegin-->(.*)<!--formEnd-->/isU', '', $indexHtml);
}
write_file($this->templatePath . 'index.html', str_replace('{pluginClass}', $replaces['pluginClass'], $indexHtml));
}
// 生成扩展模板
$extendHtml = read_file($this->getStubTpl('extend'));
$extendHtml = str_replace('{pluginClass}', $replaces['pluginClass'], $extendHtml);
foreach ($this->methods as $method) {
write_file($this->templatePath . Str::snake($method) . '.html', $extendHtml);
}
// 生成CURD菜单
if ($data['create'] && !empty($data['listField'])) {
AdminRules::createMenu([$this->menus], $replaces['pluginClass'] ?: $table, $data['pid']);
}
// 更新生成状态
$data->save(['status' => 1]);
} catch (\Throwable $e) {
return $this->error($e->getMessage());
}
return $this->success('生成成功');
}
/**
* 清理内容
* @return mixed|void
* @throws DataNotFoundException
* @throws DbException
* @throws ModelNotFoundException
*/
public function clear()
{
$id = input('id');
if (request()->isAjax()) {
$data = $this->model->find($id);
$table = str_replace($this->prefix, '', $data['table']);
$controller = $data['controller'];
try {
$module = $data['global'] ? 'common' : 'admin';
$element = ['controller', 'model', 'validate'];
foreach ($element as $key => $item) {
$result = $this->parseNameData($item == 'controller' ? 'admin' : $module, $controller, $key ? $table : '', $item);
$file = end($result);
if (file_exists($file)) {
unlink($file);
}
// 删除空文件夹
remove_empty_dir(dirname($file));
}
list($this->menus, $this->methods, $this->templateFiles) = $this->getMenuMethods($data->toArray());
recursive_delete($this->getTemplatePath($controller));
AdminRules::disabled($table, true);
$data->save(['status' => 0]);
} catch (\Throwable $th) {
return $this->error($th->getMessage());
}
return $this->success('删除成功');
}
}
/**
* 获取列表字段
* @param array $colsFields
* @param array $formDesign
* @return string
*/
public function getColsListFields(array $colsFields = [], array $formDesign = []): string
{
$colsListArr = [];
foreach ($colsFields as $key => $value) {
// 过滤删除字段
$colsLine = [];
$colsField = $value['field'];
$colsTitle = $value['title'];
if ($colsField == $this->deleteTimeField) {
continue;
}
// 获取每一列参数合集
$colsLine[] = "field:'$colsField'";
if ($colsField == $this->keepField) {
$colsLine[] = "templet: '#columnStatus'";
}
$item = $this->recursiveComponent($colsField, $formDesign);
if (!empty($item) && is_array($item)) {
$colsArr = '';
$colsTag = $item['tag'];
if (in_array($colsTag, $this->dropdown)) {
$colsArr = $item['options'];
foreach ($colsArr as $index => $elem) {
$colsArr[$index]['title'] = "{:__('" . $elem['title'] . "')}";
}
$colsArr = json_encode($colsArr, JSON_UNESCAPED_UNICODE);
$colsTpl = read_file($this->getStubTpl('list/' . $colsTag));
} else if ($colsTag == 'upload') {
$colsTpl = read_file($this->getStubTpl('list/' . $item['uploadtype']));
} else {
$colsTpl = read_file($this->getStubTpl('list/' . $colsTag));
}
if (!empty($colsTpl)) {
$colsLine[] = str_replace(['{colsArr}', '{field}'], [$colsArr, $colsField], $colsTpl);
}
}
$colsLine[] = "title:'$colsTitle'";
$colsListArr[$key] = '{' . implode(',', $colsLine) . '}';
}
$colsListArr = implode($this->commaEol, $colsListArr);
return $colsListArr ? $colsListArr . ',' : $colsListArr;
}
/**
* 获取修改器
* @param string|null $field
* @param string|null $type
* @param string $subTpl
* @return array|false|string|string[]
*/
public function getFieldAttrArr(string $field = null, string $type = null, string $subTpl = 'change')
{
$tplPath = $subTpl . '/' . $type;
$methods = read_file($this->getStubTpl($tplPath));
if (!empty($methods)) {
$methods = str_replace('{%field%}', ucfirst($field), $methods);
}
return $methods;
}
/**
* 验证组件
* @param string|null $field
* @param string|null $type
* @param array $data
* @return mixed
*/
public function validComponent(string $field, string $type, array $data = [])
{
if (!$field || !$data) {
return false;
}
$result = $this->recursiveComponent($field, $data);
if (!empty($result)) {
$tag = strtolower($result['tag']);
switch ($type) {
case 'set':
if ($tag != 'checkbox') {
return $this->error($field . ' 组件类型限定为checkbox');
}
break;
case 'json':
if ($tag != 'json') {
return $this->error($field . ' 组件类型限定为json');
}
break;
case 'text': // 限定TEXT字段类型必须为多文件上传
if ($tag != 'upload' || $result['uploadtype'] != 'multiple') {
return $this->error($field . ' 字段类型为text时组件类型限定为多文件上传');
}
break;
default:
break;
}
}
return false;
}
/**
* 查找组件
* @param string $field
* @param array $data
* @return mixed
*/
public function recursiveComponent(string $field = '', array $data = [])
{
foreach ($data as $value) {
if ($field == $value['name']) {
return $value;
}
if (isset($value['children']) && $value['children']) {
$subElem = $value['children'];
foreach ($subElem as $child) {
$item = $this->recursiveComponent($field, $child['children']);
if (!empty($item)) {
return $item;
}
}
}
}
}
/**
* 搜索模板
* @param array $searchArr
* @param array $formArr
* @return false|string
*/
public function getAdviceSearch(array $searchArr = [], array $formArr = [])
{
if (!$searchArr) {
return false;
}
$varData = '';
$searchHtml = [];
foreach ($searchArr as $searchField) {
if ($searchField == $this->deleteTimeField) {
continue;
}
if ($searchField == $this->keepField) {
$rhtml = read_file($this->getStubTpl('search/status'));
} else if (in_array($searchField, [$this->createTimeField, $this->updateTimeField])) {
$rhtml = read_file($this->getStubTpl('search/datetime'));
} else {
$result = $this->recursiveComponent($searchField, $formArr);
if ($result && in_array($result['tag'], $this->dropdown)) {
$varData = Form::validOptions($result['options']);
$rhtml = read_file($this->getStubTpl('search/select'));
} else if ($result && in_array($result['tag'], ['slider'])) {
$rhtml = read_file($this->getStubTpl('search/slider'));
$rhtml = str_replace(
['{default}', '{theme}', '{step}', '{max}', '{min}'],
[$result['data_default'], $result['data_theme'], $result['data_step'], $result['data_max'], $result['data_min']],
$rhtml
);
} else if ($result && $result['tag'] == 'cascader') {
$rhtml = read_file($this->getStubTpl('search/cascader'));
} else if ($result && $result['tag'] == 'date') {
$rhtml = read_file($this->getStubTpl('search/datetime'));
} else if ($result && $result['tag'] == 'rate') {
$rhtml = read_file($this->getStubTpl('search/rate'));
$rhtml = str_replace(['{theme}', '{length}'], [$result['data_theme'], $result['data_length']], $rhtml);
} else {
$rhtml = read_file($this->getStubTpl('search/input'));
}
}
$replace = [
'field' => $searchField,
'title' => $this->tableFields[$searchField]['title'],
'varlist' => ucfirst($searchField) . '_list',
'vardata' => $varData,
];
foreach ($replace as $key => $value) {
$rhtml = str_replace("{%$key%}", $value, $rhtml);
}
$searchHtml[] = $rhtml;
}
return implode(PHP_EOL . PHP_EOL, $searchHtml);
}
/**
* 获取菜单函数
* @param array $data
* @return array
* @throws \Exception
*/
protected function getMenuMethods(array $data = []): array
{
if (empty($data) || !is_array($data)) {
throw new \Exception("Error Params Request", 1);
}
if (!is_array($data['menus'])) {
$data['menus'] = unserialize($data['menus']);
}
$MenuRules = [
'title' => $data['title'],
'router' => $data['controller'],
'icon' => $data['icon'] ?: '',
'pid' => $data['pid'],
'auth' => $data['auth'],
];
foreach ($data['menus'] as $key => $value) {
$MenuRules['children'][$key] = [
'title' => $value['title'],
'router' => $value['router'],
'auth' => $value['auth'],
'type' => $value['type'],
];
$parse = explode(':', $value['route']);
$parse = end($parse);
if (!in_array($parse, $this->filterMethod)) {
$this->methods[$key] = $parse;
$this->templateFiles[$key] = Str::snake($parse);
}
}
return [$MenuRules, $this->methods, $this->templateFiles];
}
/**
* 获取其他函数
* @param array $methods
* @return string
*/
protected function getMethodString(array $methods = []): string
{
$outsMethod = PHP_EOL;
foreach ($methods as $method) {
if (!in_array($method, $this->filterMethod)) {
$outsMethod .= str_replace('method', $method, read_file($this->getStubTpl('method')));
}
}
return $outsMethod;
}
/**
* 获取关联表信息
* id style KEY
* @param $relation
* @return string
* @throws \Exception
*/
protected function getRelationMethodList($relation): string
{
$relationString = PHP_EOL;
if (!empty($relation) && !is_array($relation)) {
$relation = unserialize($relation);
foreach ($relation as $value) {
if (!$value) {
continue;
}
$table = str_replace($this->prefix, '', $value['table']);
$schema = Db::query("SHOW TABLE STATUS LIKE '$table'");
$studly = Str::studly($table);
// 直接判断是否存在
// 可提交遍历命名空间PR
if (in_array($table,$this->protectTable)) {
$studly = '\\app\\common\\model\\system\\' . $studly;
} else {
$namespace = '\\app\\admin\\model\\'.$studly;
if (class_exists($namespace)) {
$studly = $namespace;
}
}
// 拼接关联语句
$localKey = $value['localKey'];
$foreignKey = $value['foreignKey'];
$str_relation = '$this->' . $value['style'] . '(' . $studly . '::Class,' . "'$foreignKey','$localKey')";
$bindField = [];
if ($value['relationField']) {
$bindField = explode(',', $value['relationField']);
$bindField = array_unique(array_filter($bindField));
$str_relation .= '->bind(' . str_replace('"', '\'', json_encode($bindField)) . ')';
}
try {
$Comment = $schema[0]['Comment'] ?? $value['table'];
$table = Str::camel($table);
$relationString .= ' /**';
$relationString .= PHP_EOL . ' * 定义 ' . $Comment . ' 关联模型';
$relationString .= PHP_EOL . ' * @localKey ' . $localKey;
$relationString .= PHP_EOL . ' * @bind ' . implode(',', $bindField);
$relationString .= PHP_EOL . ' */';
$relationString .= PHP_EOL . ' public function ' . $table . '()';
$relationString .= PHP_EOL . ' {';
$relationString .= PHP_EOL . ' return ' . $str_relation . ';';
$relationString .= PHP_EOL . ' }';
$relationString .= PHP_EOL;
} catch (\Throwable $th) {
throw new \Exception($th->getMessage());
}
}
}
return $relationString;
}
/**
* 获取文件信息
* @param string $module
* @param string $name
* @param string $table
* @param string $type
* @return array
* @throws \Exception
*/
protected function parseNameData(string $module, string $name, string $table = '', string $type = 'controller'): array
{
$array = str_replace(['.', '/', '\\'], '/', $name);
$array = array_filter(explode('/', $array));
if (substr($name, 0 - strlen('/')) != '/') {
array_pop($array);
}
$parseName = $type == 'controller' ? ucfirst(end($array)) : Str::studly($table);
if (in_array(strtolower($parseName), $this->internalKeywords)) {
throw new \Exception('类名称不能使用内置关键字' . $parseName);
}
array_pop($array);
$appNamespace = "app\\{$module}\\$type" . ($array ? "\\" . implode("\\", $array) : "");
$parseFile = root_path() . $appNamespace . DIRECTORY_SEPARATOR . $parseName . '.php';
$parseFile = str_replace('\\', '/', $parseFile);
return [$parseName, $appNamespace, $parseFile];
}
/**
* @param $table
* @return bool
*/
protected function filterSystemTable($table): bool
{
if (in_array($table, $this->protectTable)) {
return true;
}
return false;
}
/**
* 获取模板文件
* @param [type] $name
* @return string
*/
protected function getStubTpl($name): string
{
return __DIR__ . DIRECTORY_SEPARATOR . 'stubs' . DIRECTORY_SEPARATOR . $name . '.stub';
}
/**
* 获取代码模板
* @param [type] $name
* @return string
*/
protected function getTemplatePath($name): string
{
$this->templatePath = root_path('app/admin/view');
$array = str_replace(['.', '/', '\\'], '/', $name);
$array = array_filter(explode('/', $array));
if (substr($name, 0 - strlen('/')) != '/') {
array_pop($array);
}
foreach ($array as $value) {
$value = Str::snake($value);
$this->templatePath .= $value . DIRECTORY_SEPARATOR;
}
return $this->templatePath;
}
}