Laravel 9以上的兼容性问题must be compatible with MigrationCreator::populateStub - 小众知识

Laravel 9以上的兼容性问题must be compatible with MigrationCreator::populateStub

2024-03-25 08:47:22 苏内容
  标签:
阅读:301

在 Laravel 9 中使用 Dcat Admin ,当使用代码生成器创建表迁移文件时,由于 Laravel 的更新出现的兼容性问题及解决办法

 已完成#I52KIU屋班兔创建于  2022-04-13 17:17

触发问题的运行环境:

  • PHP版本:8.1
  • Laravel版本:9.6.0

问题描述

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 语言的方法重载功能,因此只能通过重新创建一个兼容类来解决问题。

普通用户解决方案

解决方案其实很简单,重写创建表迁移文件的控制器,将不兼容的代码改写即可:

  1. 在 app/Admin 目录下创建 CustomModules 目录,在 CustomModules 目录下创建 stubs 目录,将 vendor/dcat/laravel-admin/src/Scaffold/stubs/create.stub 文件拷贝到该目录下。
  2. 在 CustomModules 目录下创建 L9MigrationCreator.php 类文件,该类基本内容跟 Dcat Admin 提供的 MigrationCreator 类基本相同,只是将不兼容的内容更改了,该类文件的代码如下所示,我已经将不兼容的代码改写了:
<?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;     } } 
  1. 然后我们使用上述兼容的生成器类来替换不兼容的类,首先在 app\Admin\Controllers 目录下创建 CustomScaffold 控制器,该控制扩展自 Dcat\Admin\Http\Controllers\ScaffoldController,我们要重写 store 方法,新建的控制器代码如下:
<?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);     } } 
  1. 最后编辑 app\Admin\routes.php 文件,加入下面的路由:
$router->post('helpers/scaffold', 'CustomScaffoldController@store'); 

问题得以解决!

框架解决方案

同样的思路,建一个新的 MigrationCreator 类,将不兼容的方法替换掉。当然,框架需要兼容 Laravel 9 一下的版本,因此会加入 Laravel 的版本判断

  1. 在vendor/dcat/laravel-admin/src/Scaffold目录下创建 L9MigrationCreator.php 类文件,文件内容如下所示,该文件内容与同目录下的 MigrationCreator.php 大体相似,我只是重写了 __construct、create与populateStub 方法,具体修改的地方看注释:
<?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;     } } 
  1. 修改 vendor/dcat/laravel-admin/src/Http/Controllers/ScaffoldController.php 的 store 方法,主要修改了生成数据迁移文件的部分,修改后的代码如下:
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);     } }


扩展阅读
相关阅读
© CopyRight 2010-2021, PREDREAM.ORG, Inc.All Rights Reserved. 京ICP备13045924号-1