已完成#I52KIU屋班兔创建于 2022-04-13 17:17
Dcat Admin 的迁移文件生成类(Dcat\Admin\Scaffold\MigrationCreator)的 populateStub 方法与其父类(Illuminate\Database\Migrations\MigrationCreator)中的该方法函数签名不兼容,导致运行时报错,该错误只在 Laravel 9 及以上版本才会产生,其他 Laravel 版本无需考虑这个问题。错误详情如下:
local.ERROR: Declaration of Dcat\Admin\Scaffold\MigrationCreator::populateStub($name, $stub, $table) must be compatible with Illuminate\Database\Migrations\MigrationCreator::populateStub($stub, $table) {"userId":1,"exception":"[object] (Symfony\\Component\\ErrorHandler\\Error\\FatalError(code: 0): Declaration of Dcat\\Admin\\Scaffold\\MigrationCreator::populateStub($name, $stub, $table) must be compatible with Illuminate\\Database\\Migrations\\MigrationCreator::populateStub($stub, $table) at /var/www/html/vendor/dcat/laravel-admin/src/Scaffold/MigrationCreator.php:61)
vendor/dcat/laravel-admin/src/Scaffold/MigrationCreator.php:
/** * Populate stub. * * @param string $name * @param string $stub * @param string $table * @return mixed */ protected function populateStub($name, $stub, $table) { return str_replace( ['DummyClass', 'DummyTable', 'DummyStructure'], [$this->getClassName($name), $table, $this->bluePrint], $stub ); }
vendor/laravel/framework/src/Illuminate/Database/Migrations/MigrationCreator.php:
/** * Populate the place-holders in the migration stub. * * @param string $stub * @param string|null $table * @return string */ protected function populateStub($stub, $table) { // Here we will replace the table place-holders with the table specified by // the developer, which is useful for quickly creating a tables creation // or update migration from the console instead of typing it manually. if (! is_null($table)) { $stub = str_replace( ['DummyTable', '{{ table }}', '{{table}}'], $table, $stub ); } return $stub; }
Laravel 9 重写了MigrationCreator::populateStub方法,导致该方法的函数参数产生变更,从而触发子类的兼容性问题。由于 PHP 是弱类型语言,没有类似于 java 语言的方法重载功能,因此只能通过重新创建一个兼容类来解决问题。
解决方案其实很简单,重写创建表迁移文件的控制器,将不兼容的代码改写即可:
<?php namespace App\Admin\CustomModules; use Dcat\Admin\Exception\AdminException; use Illuminate\Database\Migrations\MigrationCreator as BaseMigrationCreator; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Arr; use JetBrains\PhpStorm\Pure; class L9MigrationCreator extends BaseMigrationCreator { /** * @var string */ protected $bluePrint = ''; /** * Create a new migration creator instance. * * @param \Illuminate\Filesystem\Filesystem $files * @return void */ #[Pure] public function __construct(Filesystem $files) { $stubs = __DIR__.'stubs/create.stub'; parent::__construct($files,$stubs); } /** * Create a new model. * * @param string $name * @param string $path * @param null $table * @param bool|true $create * @return string */ public function create($name, $path, $table = null, $create = true) { $this->ensureMigrationDoesntAlreadyExist($name); $path = $this->getPath($name, $path); $stub = $this->files->get(__DIR__.'/stubs/create.stub'); $this->files->put($path, $this->populateStub($stub, $table, $name)); $this->files->chmod($path, 0777); $this->firePostCreateHooks($table, $path); return $path; } /** * Populate stub. * * @param string $name * @param string $stub * @param string $table * @return mixed */ protected function populateStub($stub, $table, $name = '') { return str_replace( ['DummyClass', 'DummyTable', 'DummyStructure'], [$this->getClassName($name), $table, $this->bluePrint], $stub ); } /** * Build the table blueprint. * * @param array $fields * @param string $keyName * @param bool|true $useTimestamps * @param bool|false $softDeletes * @return $this * * @throws \Exception */ public function buildBluePrint($fields = [], $keyName = 'id', $useTimestamps = true, $softDeletes = false) { $fields = array_filter($fields, function ($field) { return isset($field['name']) && ! empty($field['name']); }); if (empty($fields)) { throw new AdminException('Table fields can\'t be empty'); } $rows[] = "\$table->increments('$keyName');\n"; foreach ($fields as $field) { $column = "\$table->{$field['type']}('{$field['name']}')"; if ($field['key']) { $column .= "->{$field['key']}()"; } $hasDefault = isset($field['default']) && ! is_null($field['default']) && $field['default'] !== ''; if ($hasDefault) { $column .= "->default('{$field['default']}')"; } if (Arr::get($field, 'nullable') == 'on') { $column .= '->nullable()'; } elseif (! $hasDefault && $field['type'] === 'string') { $column .= "->default('')"; } if (isset($field['comment']) && $field['comment']) { $column .= "->comment('{$field['comment']}')"; } $rows[] = $column.";\n"; } if ($useTimestamps) { $rows[] = "\$table->timestamps();\n"; } if ($softDeletes) { $rows[] = "\$table->softDeletes();\n"; } $this->bluePrint = trim(implode(str_repeat(' ', 12), $rows), "\n"); return $this; } }
<?php namespace App\Admin\Controllers; use Dcat\Admin\Http\Auth\Permission; use Dcat\Admin\Http\Controllers\ScaffoldController; use Dcat\Admin\Scaffold\ControllerCreator; use Dcat\Admin\Scaffold\LangCreator; use Dcat\Admin\Scaffold\MigrationCreator; use Dcat\Admin\Scaffold\ModelCreator; use Dcat\Admin\Scaffold\RepositoryCreator; use Dcat\Admin\Support\Helper; use Illuminate\Http\Request; use Illuminate\Support\Facades\Artisan; use App\Admin\CustomModules\L9MigrationCreator; class CustomScaffoldController extends ScaffoldController { public function store(Request $request) { if (! config('app.debug')) { Permission::error(); } $paths = []; $message = ''; $creates = (array) $request->get('create'); $table = Helper::slug($request->get('table_name'), '_'); $controller = $request->get('controller_name'); $model = $request->get('model_name'); $repository = $request->get('repository_name'); try { // 1. Create model. if (in_array('model', $creates)) { $modelCreator = new ModelCreator($table, $model); $paths['model'] = $modelCreator->create( $request->get('primary_key'), $request->get('timestamps') == 1, $request->get('soft_deletes') == 1 ); } // 2. Create controller. if (in_array('controller', $creates)) { $paths['controller'] = (new ControllerCreator($controller)) ->create(in_array('repository', $creates) ? $repository : $model); } // 3. Create migration. if (in_array('migration', $creates)) { $migrationName = 'create_'.$table.'_table'; $version = (int)substr(app()::VERSION,0,1); if($version > 8){ $paths['migration'] = (new L9MigrationCreator(app('files')))->buildBluePrint( $request->get('fields'), $request->get('primary_key', 'id'), $request->get('timestamps') == 1, $request->get('soft_deletes') == 1 )->create($migrationName, database_path('migrations'), $table); }else{ $paths['migration'] = (new MigrationCreator(app('files')))->buildBluePrint( $request->get('fields'), $request->get('primary_key', 'id'), $request->get('timestamps') == 1, $request->get('soft_deletes') == 1 )->create($migrationName, database_path('migrations'), $table); } } if (in_array('lang', $creates)) { $paths['lang'] = (new LangCreator($request->get('fields'))) ->create($controller, $request->get('translate_title')); } if (in_array('repository', $creates)) { $paths['repository'] = (new RepositoryCreator()) ->create($model, $repository); } // Run migrate. if (in_array('migrate', $creates)) { Artisan::call('migrate'); $message = Artisan::output(); } // Make ide helper file. if (in_array('migrate', $creates) || in_array('controller', $creates)) { try { Artisan::call('admin:ide-helper', ['-c' => $controller]); $paths['ide-helper'] = 'dcat_admin_ide_helper.php'; } catch (\Throwable $e) { } } } catch (\Exception $exception) { // Delete generated files if exception thrown. app('files')->delete($paths); return $this->backWithException($exception); } return $this->backWithSuccess($paths, $message); } }
$router->post('helpers/scaffold', 'CustomScaffoldController@store');
问题得以解决!
同样的思路,建一个新的 MigrationCreator 类,将不兼容的方法替换掉。当然,框架需要兼容 Laravel 9 一下的版本,因此会加入 Laravel 的版本判断
<?php namespace Dcat\Admin\Scaffold; use Dcat\Admin\Exception\AdminException; use Illuminate\Database\Migrations\MigrationCreator as BaseMigrationCreator; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Arr; use JetBrains\PhpStorm\Pure; class L9MigrationCreator extends BaseMigrationCreator { /** * @var string */ protected $bluePrint = ''; /** * Create a new migration creator instance. * * @param \Illuminate\Filesystem\Filesystem $files * @return void */ #[Pure] public function __construct(Filesystem $files) { // 由于父类的构造方法多了一个 $stubs 参数,这里重写了构造方法, $stubs = __DIR__.'stubs/create.stub'; parent::__construct($files,$stubs); } /** * Create a new model. * * @param string $name * @param string $path * @param null $table * @param bool|true $create * @return string */ public function create($name, $path, $table = null, $create = true) { $this->ensureMigrationDoesntAlreadyExist($name); $path = $this->getPath($name, $path); $stub = $this->files->get(__DIR__.'/stubs/create.stub'); // 这里调整了 populateStub 方法的传参顺序: $this->files->put($path, $this->populateStub($stub, $table, $name)); $this->files->chmod($path, 0777); $this->firePostCreateHooks($table, $path); return $path; } /** * Populate stub. * 这里将函数的参数进行了调整,以兼容父类的函数参数 * * @param string $name * @param string $stub * @param string $table * @return mixed */ protected function populateStub($stub, $table, $name = '') { return str_replace( ['DummyClass', 'DummyTable', 'DummyStructure'], [$this->getClassName($name), $table, $this->bluePrint], $stub ); } /** * Build the table blueprint. * * @param array $fields * @param string $keyName * @param bool|true $useTimestamps * @param bool|false $softDeletes * @return $this * * @throws \Exception */ public function buildBluePrint($fields = [], $keyName = 'id', $useTimestamps = true, $softDeletes = false) { $fields = array_filter($fields, function ($field) { return isset($field['name']) && ! empty($field['name']); }); if (empty($fields)) { throw new AdminException('Table fields can\'t be empty'); } $rows[] = "\$table->increments('$keyName');\n"; foreach ($fields as $field) { $column = "\$table->{$field['type']}('{$field['name']}')"; if ($field['key']) { $column .= "->{$field['key']}()"; } $hasDefault = isset($field['default']) && ! is_null($field['default']) && $field['default'] !== ''; if ($hasDefault) { $column .= "->default('{$field['default']}')"; } if (Arr::get($field, 'nullable') == 'on') { $column .= '->nullable()'; } elseif (! $hasDefault && $field['type'] === 'string') { $column .= "->default('')"; } if (isset($field['comment']) && $field['comment']) { $column .= "->comment('{$field['comment']}')"; } $rows[] = $column.";\n"; } if ($useTimestamps) { $rows[] = "\$table->timestamps();\n"; } if ($softDeletes) { $rows[] = "\$table->softDeletes();\n"; } $this->bluePrint = trim(implode(str_repeat(' ', 12), $rows), "\n"); return $this; } }
use Dcat\Admin\Scaffold\L9MigrationCreator; . . . // 3. Create migration. if (in_array('migration', $creates)) { $migrationName = 'create_'.$table.'_table'; // 这里判断 Laravel 的版本 $version = (int)substr(app()::VERSION,0,1); if($version > 8){ // 如果 Laravel 的版本大于8,则使用我们上面创建的 L9MigrationCreator 类 $paths['migration'] = (new L9MigrationCreator(app('files')))->buildBluePrint( $request->get('fields'), $request->get('primary_key', 'id'), $request->get('timestamps') == 1, $request->get('soft_deletes') == 1 )->create($migrationName, database_path('migrations'), $table); }else{ $paths['migration'] = (new MigrationCreator(app('files')))->buildBluePrint( $request->get('fields'), $request->get('primary_key', 'id'), $request->get('timestamps') == 1, $request->get('soft_deletes') == 1 )->create($migrationName, database_path('migrations'), $table); } }