From fcaf9326b2b4f1465b0891e837c4ba03e86a934b Mon Sep 17 00:00:00 2001 From: Slowlyo Date: Sat, 23 Mar 2024 15:49:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=A8=E6=80=81=E5=85=B3=E8=81=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/admin.php | 16 +- src/AdminServiceProvider.php | 4 +- src/Controllers/AdminRoleController.php | 1 + .../DevTools/RelationshipController.php | 266 +++++++++++++----- src/Models/AdminRelationship.php | 50 +++- src/Models/PersonalAccessToken.php | 22 ++ src/Services/AdminRelationshipService.php | 109 +++++++ src/Support/Cores/Relationships.php | 28 ++ src/Support/Cores/Route.php | 3 + 9 files changed, 416 insertions(+), 83 deletions(-) create mode 100644 src/Support/Cores/Relationships.php diff --git a/config/admin.php b/config/admin.php index 7e94a437..39c32042 100644 --- a/config/admin.php +++ b/config/admin.php @@ -30,25 +30,27 @@ 'auth' => [ // 是否开启验证码 - 'login_captcha' => env('ADMIN_LOGIN_CAPTCHA', true), + 'login_captcha' => env('ADMIN_LOGIN_CAPTCHA', true), // 是否开启认证 - 'enable' => true, + 'enable' => true, // 是否开启鉴权 - 'permission' => true, - 'guard' => 'admin', - 'guards' => [ + 'permission' => true, + // token 有效期 (分钟), 为空则不限时 + 'token_expiration' => null, + 'guard' => 'admin', + 'guards' => [ 'admin' => [ 'driver' => 'sanctum', 'provider' => 'admin', ], ], - 'providers' => [ + 'providers' => [ 'admin' => [ 'driver' => 'eloquent', 'model' => \Slowlyo\OwlAdmin\Models\AdminUser::class, ], ], - 'except' => [ + 'except' => [ ], ], diff --git a/src/AdminServiceProvider.php b/src/AdminServiceProvider.php index eba60e9a..4a3a046d 100644 --- a/src/AdminServiceProvider.php +++ b/src/AdminServiceProvider.php @@ -9,7 +9,7 @@ use Psr\Container\NotFoundExceptionInterface; use Psr\Container\ContainerExceptionInterface; use Slowlyo\OwlAdmin\Models\PersonalAccessToken; -use Slowlyo\OwlAdmin\Support\{Context, Cores\Menu, Cores\Asset, Cores\Module}; +use Slowlyo\OwlAdmin\Support\{Context, Cores\Menu, Cores\Asset, Cores\Module, Cores\Relationships}; class AdminServiceProvider extends ServiceProvider { @@ -78,6 +78,8 @@ public function boot(): void $this->loadRoutes(); $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); $this->usePersonalAccessTokenModel(); + + Relationships::loader(); } protected function loadBaseRoute() diff --git a/src/Controllers/AdminRoleController.php b/src/Controllers/AdminRoleController.php index ee812cfe..558fc639 100644 --- a/src/Controllers/AdminRoleController.php +++ b/src/Controllers/AdminRoleController.php @@ -77,6 +77,7 @@ protected function setPermission() ->name('permissions') ->label() ->multiple() + ->heightAuto() ->options(AdminPermissionService::make()->getTree()) ->searchable() ->cascade() diff --git a/src/Controllers/DevTools/RelationshipController.php b/src/Controllers/DevTools/RelationshipController.php index b16b1c6e..b0545e9f 100644 --- a/src/Controllers/DevTools/RelationshipController.php +++ b/src/Controllers/DevTools/RelationshipController.php @@ -2,15 +2,14 @@ namespace Slowlyo\OwlAdmin\Controllers\DevTools; -use Illuminate\Support\Str; use Illuminate\Support\Facades\Schema; -use Illuminate\Database\Eloquent\Model; -use Slowlyo\OwlAdmin\Support\Cores\Module; use Slowlyo\OwlAdmin\Models\AdminRelationship; use Slowlyo\OwlAdmin\Controllers\AdminController; use Slowlyo\OwlAdmin\Services\AdminRelationshipService; -use Spatie\LaravelIgnition\Support\Composer\ComposerClassMap; +/** + * @property AdminRelationshipService $service + */ class RelationshipController extends AdminController { protected string $serviceName = AdminRelationshipService::class; @@ -22,13 +21,16 @@ public function list() ->headerToolbar([ $this->createButton(true, 'lg'), ...$this->baseHeaderToolBar(), + $this->modelGenerator(), ]) ->columns([ amis()->TableColumn('id', 'ID')->sortable(), + amis()->TableColumn('model', '模型')->searchable(), amis()->TableColumn('title', '名称')->searchable(), - amis()->TableColumn('sign', '标识')->searchable(), + amis()->TableColumn('remark', '备注')->searchable(), $this->rowActions([ - $this->rowEditButton(true), + $this->previewButton(), + $this->rowEditButton(true, 'lg'), $this->rowDeleteButton(), ]), ]); @@ -36,6 +38,60 @@ public function list() return $this->baseList($crud); } + public function modelGenerator() + { + return amis()->DrawerAction()->label('生成模型')->level('success')->drawer( + amis()->Drawer() + ->title('生成模型') + ->size('lg') + ->resizable() + ->closeOnOutside() + ->closeOnEsc() + ->actions([ + amis()->VanillaAction()->label('生成')->actionType('submit')->level('primary'), + ]) + ->body([ + amis()->Form() + ->api('/dev_tools/relation/generate_model') + ->initApi('/dev_tools/relation/all_models') + ->mode('normal') + ->body([ + amis()->TreeControl() + ->name('check_tables') + ->label() + ->multiple() + ->heightAuto() + ->required() + ->source('${all_models}') + ->searchable() + ->joinValues(false) + ->extractValue() + ->size('full') + ->className('h-full b-none') + ->inputClassName('h-full tree-full') + ->set('menuTpl', '${label} ${model}'), + ]), + ]) + ); + } + + public function previewButton() + { + return amis()->DrawerAction()->label('预览')->level('link')->icon('fa fa-eye')->drawer( + amis()->Drawer() + ->position('top') + ->resizable() + ->title('预览') + ->actions([]) + ->showCloseButton(false) + ->closeOnEsc() + ->closeOnOutside() + ->body( + amis()->Code()->value('${preview_code | raw}')->language('php') + ) + ); + } + public function form() { $modelSelect = function ($name, $label) { @@ -47,7 +103,26 @@ public function form() ->searchable(); }; - return $this->baseForm()->body([ + $columnSelect = function ($name, $label, $modelField = "_blank_model", $tableField = '_blank_table') { + return amis() + ->TextControl($name, $label) + ->source('/dev_tools/relation/column_options?model=${' . $modelField . '}&table=${' . $tableField . '}'); + }; + + $args = function ($type, $items) { + return amis() + ->ComboControl('args', '参数') + ->multiLine() + ->strictMode(false) + ->items($items) + ->visibleOn('${type == "' . $type . '"}'); + }; + + return $this->baseForm()->data([ + 'tables' => collect(json_decode(json_encode(Schema::getAllTables()), true)) + ->map(fn($i) => array_shift($i)) + ->toArray(), + ])->body([ amis()->GroupControl()->body([ amis()->GroupControl()->direction('vertical')->body([ $modelSelect('model', '模型'), @@ -60,88 +135,88 @@ public function form() ->options(AdminRelationship::typeOptions()), ]), // 一对一 - amis()->ComboControl('args', '参数')->multiLine()->items([ + $args(AdminRelationship::TYPE_HAS_ONE, [ // $related, $foreignKey = null, $localKey = null $modelSelect('related', '关联模型'), - amis()->TextControl('foreignKey', 'foreignKey'), - amis()->TextControl('localKey', 'localKey'), - ])->visibleOn('${type == "' . AdminRelationship::TYPE_HAS_ONE . '"}'), + $columnSelect('foreignKey', 'foreignKey', 'related'), + $columnSelect('localKey', 'localKey', 'model'), + ]), // 一对多 - amis()->ComboControl('args', '参数')->multiLine()->items([ + $args(AdminRelationship::TYPE_HAS_MANY, [ // $related, $foreignKey = null, $localKey = null $modelSelect('related', '关联模型'), - amis()->TextControl('foreignKey', 'foreignKey'), - amis()->TextControl('localKey', 'localKey'), - ])->visibleOn('${type == "' . AdminRelationship::TYPE_HAS_MANY . '"}'), + $columnSelect('foreignKey', 'foreignKey', 'related'), + $columnSelect('localKey', 'localKey', 'model'), + ]), // 一对多(反向)/属于 - amis()->ComboControl('args', '参数')->multiLine()->items([ + $args(AdminRelationship::TYPE_BELONGS_TO, [ // $related, $foreignKey = null, $ownerKey = null, $relation = null $modelSelect('related', '关联模型'), - amis()->TextControl('foreignKey', 'foreignKey'), - amis()->TextControl('ownerKey', 'ownerKey'), + $columnSelect('foreignKey', 'foreignKey', 'model'), + $columnSelect('ownerKey', 'ownerKey', 'related'), amis()->TextControl('relation', 'relation'), - ])->visibleOn('${type == "' . AdminRelationship::TYPE_BELONGS_TO . '"}'), + ]), // 多对多 - amis()->ComboControl('args', '参数')->multiLine()->items([ + $args(AdminRelationship::TYPE_BELONGS_TO_MANY, [ // $related, $table = null, $foreignPivotKey = null, $relatedPivotKey = null, $parentKey = null, $relatedKey = null, $relation = null $modelSelect('related', '关联模型'), - amis()->TextControl('table', 'table'), - amis()->TextControl('foreignPivotKey', 'foreignPivotKey'), - amis()->TextControl('relatedPivotKey', 'relatedPivotKey'), - amis()->TextControl('parentKey', 'parentKey'), - amis()->TextControl('relatedKey', 'relatedKey'), + amis()->SelectControl('table', 'table')->source('${tables}')->searchable(), + $columnSelect('foreignPivotKey', 'foreignPivotKey', '_blank_model', 'table'), + $columnSelect('relatedPivotKey', 'relatedPivotKey', '_blank_model', 'table'), + $columnSelect('parentKey', 'parentKey', 'model'), + $columnSelect('relatedKey', 'relatedKey', 'related'), amis()->TextControl('relation', 'relation'), - ])->visibleOn('${type == "' . AdminRelationship::TYPE_BELONGS_TO_MANY . '"}'), + ]), // 远程一对一 - amis()->ComboControl('args', '参数')->multiLine()->items([ + $args(AdminRelationship::TYPE_HAS_ONE_THROUGH, [ // $related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null $modelSelect('related', '关联模型'), $modelSelect('through', '中间模型'), - amis()->TextControl('firstKey', 'firstKey'), - amis()->TextControl('secondKey', 'secondKey'), - amis()->TextControl('localKey', 'localKey'), - amis()->TextControl('secondLocalKey', 'secondLocalKey'), - ])->visibleOn('${type == "' . AdminRelationship::TYPE_HAS_ONE_THROUGH . '"}'), + $columnSelect('firstKey', 'firstKey', 'through'), + $columnSelect('secondKey', 'secondKey', 'related'), + $columnSelect('localKey', 'localKey', 'model'), + $columnSelect('secondLocalKey', 'secondLocalKey', 'through'), + ]), // 远程一对多 - amis()->ComboControl('args', '参数')->multiLine()->items([ + $args(AdminRelationship::TYPE_HAS_MANY_THROUGH, [ // $related, $through, $firstKey = null, $secondKey = null, $localKey = null, $secondLocalKey = null $modelSelect('related', '关联模型'), $modelSelect('through', '中间模型'), - amis()->TextControl('firstKey', 'firstKey'), - amis()->TextControl('secondKey', 'secondKey'), - amis()->TextControl('localKey', 'localKey'), - amis()->TextControl('secondLocalKey', 'secondLocalKey'), - ])->visibleOn('${type == "' . AdminRelationship::TYPE_HAS_MANY_THROUGH . '"}'), + $columnSelect('firstKey', 'firstKey', 'through'), + $columnSelect('secondKey', 'secondKey', 'related'), + $columnSelect('localKey', 'localKey', 'model'), + $columnSelect('secondLocalKey', 'secondLocalKey', 'through'), + ]), // 一对一(多态) - amis()->ComboControl('args', '参数')->multiLine()->items([ + $args(AdminRelationship::TYPE_MORPH_ONE, [ // $related, $name, $type = null, $id = null, $localKey = null $modelSelect('related', '关联模型'), amis()->TextControl('name', 'name')->required(), amis()->TextControl('type', 'type'), amis()->TextControl('id', 'id'), - amis()->TextControl('localKey', 'localKey'), - ])->visibleOn('${type == "' . AdminRelationship::TYPE_MORPH_ONE . '"}'), + $columnSelect('localKey', 'localKey', 'model'), + ]), // 一对多(多态) - amis()->ComboControl('args', '参数')->multiLine()->items([ + $args(AdminRelationship::TYPE_MORPH_MANY, [ // $related, $name, $type = null, $id = null, $localKey = null $modelSelect('related', '关联模型'), amis()->TextControl('name', 'name')->required(), amis()->TextControl('type', 'type'), amis()->TextControl('id', 'id'), - amis()->TextControl('localKey', 'localKey'), - ])->visibleOn('${type == "' . AdminRelationship::TYPE_MORPH_MANY . '"}'), + $columnSelect('localKey', 'localKey', 'model'), + ]), // 多对多(多态) - amis()->ComboControl('args', '参数')->multiLine()->items([ + $args(AdminRelationship::TYPE_MORPH_TO_MANY, [ // $related, $name, $table = null, $foreignPivotKey = null, $relatedPivotKey = null, $parentKey = null, $relatedKey = null, $inverse = false $modelSelect('related', '关联模型'), amis()->TextControl('name', 'name')->required(), - amis()->TextControl('table', 'table'), - amis()->TextControl('foreignPivotKey', 'foreignPivotKey'), - amis()->TextControl('relatedPivotKey', 'relatedPivotKey'), - amis()->TextControl('parentKey', 'parentKey'), - amis()->TextControl('relatedKey', 'relatedKey'), + amis()->SelectControl('table', 'table')->source('${tables}')->searchable(), + $columnSelect('foreignPivotKey', 'foreignPivotKey', '_blank_model', 'table'), + $columnSelect('relatedPivotKey', 'relatedPivotKey', '_blank_model', 'table'), + $columnSelect('parentKey', 'parentKey', 'model'), + $columnSelect('relatedKey', 'relatedKey', 'related'), amis()->TextControl('inverse', 'inverse'), - ])->visibleOn('${type == "' . AdminRelationship::TYPE_MORPH_TO_MANY . '"}'), + ]), ]), ]); } @@ -153,31 +228,82 @@ public function detail($id) } /** - * 获取所有已经加载的 model + * 获取所有 model * * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Resources\Json\JsonResource */ public function modelOptions() { - $composer = require base_path('/vendor/autoload.php'); - $classMap = $composer->getClassMap(); - - $tables = collect(json_decode(json_encode(Schema::getAllTables()), true)) - ->map(fn($i) => array_shift($i)) - ->toArray(); - - $models = collect($classMap) - ->keys() - ->filter(fn($item) => str_contains($item, 'Models\\')) - ->filter(fn($item) => (new \ReflectionClass($item))->isSubclassOf(Model::class)) - ->filter(fn($item) => in_array(app($item)->getTable(), $tables)) - ->values() - ->map(fn($item) => [ - 'label' => Str::of($item)->explode('\\')->pop(), - 'table' => app($item)->getTable(), - 'value' => $item, - ]); + $models = $this->service->allModels()['models']; return $this->response()->success($models); } + + /** + * 字段选项 + * + * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Resources\Json\JsonResource + */ + public function columnOptions() + { + $model = request('model'); + $table = request('table'); + + if (blank($model) && blank($table)) { + return $this->response()->success([]); + } + + $table = $table ?: app($model)->getTable(); + + $columns = Schema::getColumnListing($table); + + return $this->response()->success($columns); + } + + /** + * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Resources\Json\JsonResource + */ + public function allModels() + { + $all = $this->service->allModels(); + $tables = $all['tables']; + $models = collect($all['models'])->keyBy('table'); + + $all_models = collect($tables)->map(function ($item) use ($models) { + $model = data_get($models, $item . '.value'); + + return [ + 'value' => $item, + 'label' => $item, + 'model' => $model, + 'disabled' => (bool)$model, + ]; + })->sortBy('disabled')->values(); + + return $this->response()->success(compact('all_models')); + } + + /** + * 生成模型 + * + * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\Resources\Json\JsonResource + */ + public function generateModel() + { + $tables = request('check_tables'); + $existsList = collect($this->service->allModels()['models'])->pluck('table')->toArray(); + $exists = array_intersect($tables, $existsList); + + admin_abort_if(filled($exists), '模型已存在:' . implode(',', $exists)); + + try { + foreach ($tables as $table) { + $this->service->generateModel($table); + } + } catch (\Throwable $e) { + return $this->response()->fail($e->getMessage()); + } + + return $this->response()->successMessage(__('admin.action_success')); + } } diff --git a/src/Models/AdminRelationship.php b/src/Models/AdminRelationship.php index a46c9079..ca31f1f3 100644 --- a/src/Models/AdminRelationship.php +++ b/src/Models/AdminRelationship.php @@ -2,6 +2,8 @@ namespace Slowlyo\OwlAdmin\Models; +use Illuminate\Support\Str; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Concerns\HasTimestamps; class AdminRelationship extends BaseModel @@ -64,11 +66,6 @@ class AdminRelationship extends BaseModel self::TYPE_MORPH_TO_MANY => '多对多(多态)', ]; - public function aaa() - { - // $this->belongsTo(); - } - public static function typeOptions() { return collect(self::TYPE_MAP)->map(function ($item, $index) { @@ -79,4 +76,47 @@ public static function typeOptions() ]; })->values(); } + + public function method(): Attribute + { + return Attribute::get(fn() => self::TYPE_MAP[$this->type]); + } + + public function buildArgs() + { + $reflection = new \ReflectionClass($this->model); + $params = $reflection->getMethod($this->method)->getParameters(); + + $args = []; + + foreach ($params as $item) { + $_value = data_get($this->args, $item->getName()); + $args[] = [ + 'name' => $item->getName(), + 'value' => filled($_value) ? $_value : $item->getDefaultValue(), + ]; + } + + return $args; + } + + public function getPreviewCode() + { + $className = Str::of($this->model)->explode('\\')->pop(); + $args = collect($this->buildArgs()) + ->pluck('value') + ->map(fn($item) => is_null($item) ? 'null' : (is_string($item) ? "'{$item}'" : $item)) + ->implode(', '); + + return <<title() { + return \$this->$this->method($args); + } +} +PHP; + } } diff --git a/src/Models/PersonalAccessToken.php b/src/Models/PersonalAccessToken.php index a53094c0..5a9611ea 100644 --- a/src/Models/PersonalAccessToken.php +++ b/src/Models/PersonalAccessToken.php @@ -13,4 +13,26 @@ public function __construct(array $attributes = []) parent::__construct($attributes); } + + public static function findToken($token) + { + $expiration = config('admin.auth.token_expiration'); + + if (!str_contains($token, '|')) { + return static::where('token', hash('sha256', $token)) + ->when($expiration, fn($q) => $q->where('created_at', '>=', now()->subMinutes($expiration))) + ->first(); + } + + [$id, $token] = explode('|', $token, 2); + + $instance = static::when($expiration, fn($q) => $q->where('created_at', '>=', now()->subMinutes($expiration))) + ->find($id); + + if ($instance) { + return hash_equals($instance->token, hash('sha256', $token)) ? $instance : null; + } + + return null; + } } diff --git a/src/Services/AdminRelationshipService.php b/src/Services/AdminRelationshipService.php index c73d4251..2791ed21 100644 --- a/src/Services/AdminRelationshipService.php +++ b/src/Services/AdminRelationshipService.php @@ -2,6 +2,9 @@ namespace Slowlyo\OwlAdmin\Services; +use Illuminate\Support\Str; +use Illuminate\Support\Facades\Schema; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; use Slowlyo\OwlAdmin\Models\AdminRelationship; @@ -14,4 +17,110 @@ class AdminRelationshipService extends AdminService protected string $modelName = AdminRelationship::class; public string $cacheKey = 'admin_relationships'; + + public function list() + { + $list = parent::list(); + + collect($list['items'])->transform(function ($item) { + $item->setAttribute('preview_code', $item->getPreviewCode()); + }); + + return $list; + } + + public function getAll() + { + return cache()->rememberForever($this->cacheKey, function () { + return self::query()->get(); + }); + } + + public function saving(&$data, $primaryKey = '') + { + $exists = self::query() + ->where('model', $data['model']) + ->where('title', $data['title']) + ->when($primaryKey, fn($q) => $q->where('id', '<>', $primaryKey)) + ->exists(); + + admin_abort_if($exists, '该模型下存在同名关联'); + + $methodExists = method_exists($data['model'], $data['title']); + + admin_abort_if($methodExists, '该模型下存在同名关联'); + } + + public function saved($model, $isEdit = false) + { + cache()->forget($this->cacheKey); + } + + public function deleted($ids) + { + cache()->forget($this->cacheKey); + } + + public function allModels() + { + $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator(app_path('Models'))); + $phpFiles = new \RegexIterator($iterator, '/^.+\.php$/i', \RegexIterator::GET_MATCH); + + foreach ($phpFiles as $phpFile) { + $filePath = $phpFile[0]; + require_once $filePath; + } + + $modelDirClass = collect(get_declared_classes()) + ->filter(fn($i) => Str::startsWith($i, 'App\\Models')) + ->toArray(); + + $composer = require base_path('/vendor/autoload.php'); + $classMap = $composer->getClassMap(); + + $tables = collect(json_decode(json_encode(Schema::getAllTables()), true)) + ->map(fn($i) => array_shift($i)) + ->toArray(); + + $models = collect($classMap) + ->keys() + ->filter(fn($item) => str_contains($item, 'Models\\')) + ->filter(fn($item) => @class_exists($item)) + ->filter(fn($item) => (new \ReflectionClass($item))->isSubclassOf(Model::class)) + ->merge($modelDirClass) + ->unique() + ->filter(fn($item) => in_array(app($item)->getTable(), $tables)) + ->values() + ->map(fn($item) => [ + 'label' => Str::of($item)->explode('\\')->pop(), + 'table' => app($item)->getTable(), + 'value' => $item, + ]); + + return compact('tables', 'models'); + } + + public function generateModel($table) + { + $className = Str::of($table)->studly()->singular()->value(); + + $template = <<put($path, $template); + } } diff --git a/src/Support/Cores/Relationships.php b/src/Support/Cores/Relationships.php new file mode 100644 index 00000000..157f936b --- /dev/null +++ b/src/Support/Cores/Relationships.php @@ -0,0 +1,28 @@ +getAll(); + + if (blank($relationships)) { + return; + } + + foreach ($relationships as $relationship) { + try { + $relationship->model::resolveRelationUsing($relationship->title, function ($model) use ($relationship) { + $method = $relationship->method; + + return $model->$method(...array_column($relationship->buildArgs(), 'value')); + }); + }catch (\Throwable $e) { + } + } + } +} diff --git a/src/Support/Cores/Route.php b/src/Support/Cores/Route.php index ea9006aa..bc5bd2b3 100644 --- a/src/Support/Cores/Route.php +++ b/src/Support/Cores/Route.php @@ -108,6 +108,9 @@ private static function baseRoutes($prefix) $router->resource('relationships', RelationshipController::class); $router->group(['prefix' => 'relation'], function (Router $router) { $router->get('model_options', [RelationshipController::class, 'modelOptions']); + $router->get('column_options', [RelationshipController::class, 'columnOptions']); + $router->get('all_models', [RelationshipController::class, 'allModels']); + $router->post('generate_model', [RelationshipController::class, 'generateModel']); }); }); }