前言
在 Laravel 开发中,Eloquent Model 是我们最常用的组件。通过
php artisan make:model创建模型后,最头疼的往往是 IDE(如 PhpStorm 或 VSCode)无法识别数据库字段,导致没有代码提示。虽然有
laravel-ide-helper这样的神器,但每次新建模型后都要运行一遍ide-helper:models总感觉不够"丝滑"。如果能在执行
make:model的那一瞬间,直接连接数据库,把表结构读取出来,自动生成@property注释,岂不美哉?今天就分享一个实战中的自定义 Command,实现"模型创建即自动拥有补全"。
🤔 痛点分析
通常我们创建一个 User 模型,生成的文件是空的:
PHP
scala
class User extends Model
{
use HasFactory;
}
我们在写代码时 $user->... 后面一片空白。为了有提示,我们需要手动去数据库看字段,然后写成这样:
PHP
scala
/**
* @property int $id
* @property string $name
* @property string $email
* @property \Illuminate\Support\Carbon $created_at
*/
class User extends Model { ... }
这个过程繁琐且容易出错。下面的代码将把这个过程自动化。
💻 核心代码实现
新建一个命令文件 app/Console/Commands/MakeModelWithProperties.php (类名可自定),代码如下:
PHP
php
<?php
namespace App\Console\Commands;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputOption;
class MakeModelWithProperties extends GeneratorCommand
{
// 覆盖原生命令,或者取个别名如 make:model:plus
protected $name = 'make:model';
protected $description = '创建包含 @property 注释的 Eloquent 模型类';
protected $type = 'Model';
public function handle(): void
{
// 1. 处理数据库连接
$conn = $this->normalizeConnectionOption($this->option('connection'));
if (!$conn) {
$conn = $this->promptForConnection();
}
$this->input->setOption('connection', $conn);
// 2. 调用父类逻辑生成文件
if (parent::handle() === false && !$this->option('force')) {
return;
}
// 3. 处理可选参数 (Migration, Factory 等)
$this->handleOptionalGenerators();
}
/**
* 核心逻辑:在构建类时注入注释
*/
protected function buildClass($name)
{
$stub = parent::buildClass($name);
$table = $this->inferTableName($name);
$connection = (string)$this->option('connection');
// 生成 PHPDoc
$propertiesDoc = $this->generatePropertiesDoc($table, $connection);
// 替换 Stub 中的占位符,或者通过正则插入到 class 定义之前
if (Str::contains($stub, '{{ propertiesDoc }}')) {
$stub = str_replace('{{ propertiesDoc }}', $propertiesDoc, $stub);
} else {
// 默认 stub 没有占位符,使用正则强插
$stub = preg_replace('/(\n\s*class\s+[^\s]+\s+extends)/', "\n/**\n{$propertiesDoc}\n */$1", $stub, 1);
}
// 自动注入表名和主键
$primaryKey = $this->getPrimaryKeyColumn($table, $connection) ?? 'id';
// ... (此处省略部分替换逻辑,视具体 Stub 而定)
return $stub;
}
/**
* 生成属性注释文档
*/
protected function generatePropertiesDoc(string $table, string $connection)
{
try {
$schema = Schema::connection($connection);
if ($schema->hasTable($table)) {
$columns = $this->getColumnsMeta($table, $connection);
// 获取表注释
$prefix = config("database.connections.$connection.prefix") ?? '';
$tableStatus = DB::connection($connection)->select("SHOW TABLE STATUS WHERE NAME = ?", [$prefix . $table]);
$tableComment = $tableStatus[0]->Comment ?? '';
$lines = [];
if ($tableComment) $lines[] = ' * ' . $tableComment;
foreach ($columns as $col) {
$phpType = $this->mapDbTypeToPhpType($col['type'], $col['name']);
$nullable = $col['nullable'] ? '|null' : '';
$comment = $col['comment'] ? ' ' . $col['comment'] : '';
$lines[] = " * @property {$phpType}{$nullable} ${$col['name']}{$comment}";
}
return implode("\n", $lines);
}
return ' *';
} catch (\Throwable $e) {
$this->warn('获取表信息失败: ' . $e->getMessage());
return ' *';
}
}
/**
* 获取列元数据 (直接查询 information_schema 以获取更详细信息)
*/
protected function getColumnsMeta(string $table, string $connection): array
{
$conn = DB::connection($connection);
$prefix = config("database.connections.$connection.prefix") ?? '';
$rows = $conn->select(
'select COLUMN_NAME as name, DATA_TYPE as type, IS_NULLABLE as nullable, COLUMN_COMMENT as comment
from information_schema.columns
where table_schema = database() and table_name = ? order by ORDINAL_POSITION',
[$prefix . $table]
);
return array_map(function ($r) {
return [
'name' => $r->name,
'type' => strtolower($r->type),
'nullable' => strtoupper($r->nullable ?? 'YES') === 'YES',
'comment' => $r->comment ?? '',
];
}, $rows);
}
/**
* 数据库类型映射到 PHP 类型
*/
protected function mapDbTypeToPhpType(?string $dbType, string $column): string
{
if (in_array($column, ['created_at', 'updated_at', 'deleted_at'])) {
return '\Illuminate\Support\Carbon';
}
return match (true) {
str_contains($dbType, 'int') => 'int',
in_array($dbType, ['decimal', 'float', 'double']) => 'float',
in_array($dbType, ['json']) => 'array',
in_array($dbType, ['date', 'datetime', 'timestamp']) => '\Illuminate\Support\Carbon',
default => 'string',
};
}
// ... 其他辅助函数如 inferTableName, promptForConnection 等请参考完整源码
protected function getStub()
{
return $this->resolveStubPath('/stubs/model.stub');
}
}
🎯 效果演示
假设我们有一个 users 表,结构如下:
id: bigint (PK)username: varchar, comment '用户名'is_active: tinyintcreated_at: timestamp
在命令行执行:
Bash
ini
php artisan make:model User --connection=mysql
生成的代码将自动变为:
PHP
php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
/**
* 用户表
* @property int $id
* @property string $username 用户名
* @property int $is_active
* @property \Illuminate\Support\Carbon|null $created_at
* @property \Illuminate\Support\Carbon|null $updated_at
*/
class User extends Model
{
// ...
}
现在,你在 Controller 里输入 $user->,IDE 立刻就会列出 username 和 is_active,开发效率瞬间提升!
💡 实现细节解析
-
正则替换 vs Stub 占位符:
Laravel 默认的 model.stub 并没有 {{ propertiesDoc }} 占位符。代码中做了一个兼容处理:
- 如果你发布了 stubs (
php artisan stub:publish) 并在里面加了占位符,它会精准替换。 - 如果没有,它会用正则
preg_replace强行把注释插入到class Xxx extends的上方。
- 如果你发布了 stubs (
-
多态与Pivot支持:
代码中保留了对 --pivot 和 --morph-pivot 的支持,确保生成中间表模型时也能正常工作。
📦 如何在你的项目中使用
- 在
app/Console/Commands下新建MakeModelCommand.php。 - 将上述代码(及必要的 import)粘贴进去。
- 在
app/Console/Kernel.php中注册(Laravel 11 可忽略,会自动发现)。 - 确保你的数据库已经建好表(因为它是Database First逻辑,先有表,后生成带注释的模型)。
Happy Coding! 🚀