🚀 告别手写注释!Laravel 自定义命令创建模型时自动生成 @property 属性提示

前言

在 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: tinyint
  • created_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 立刻就会列出 usernameis_active,开发效率瞬间提升!

💡 实现细节解析

  1. 正则替换 vs Stub 占位符:

    Laravel 默认的 model.stub 并没有 {{ propertiesDoc }} 占位符。代码中做了一个兼容处理:

    • 如果你发布了 stubs (php artisan stub:publish) 并在里面加了占位符,它会精准替换。
    • 如果没有,它会用正则 preg_replace 强行把注释插入到 class Xxx extends 的上方。
  2. 多态与Pivot支持:

    代码中保留了对 --pivot 和 --morph-pivot 的支持,确保生成中间表模型时也能正常工作。

📦 如何在你的项目中使用

  1. app/Console/Commands 下新建 MakeModelCommand.php
  2. 将上述代码(及必要的 import)粘贴进去。
  3. app/Console/Kernel.php 中注册(Laravel 11 可忽略,会自动发现)。
  4. 确保你的数据库已经建好表(因为它是Database First逻辑,先有表,后生成带注释的模型)。

Happy Coding! 🚀

相关推荐
JaguarJack2 天前
Laravel 乐观锁:高并发场景下的性能优化利器
后端·php·laravel
life码农4 天前
在 Laravel框架 Blade 模板中显示原始的 {{ }} 符号的几种方法
php·laravel
JienDa12 天前
Laravel 11与UniApp实战:构建高性能电商API与移动端交互系统
laravel
catchadmin15 天前
用 LaraDumps 高效调试 PHP 和 Laravel
php·laravel
JaguarJack15 天前
Laravel ObjectId 性能最强体积最小的分布式 UUID 生成扩展
后端·laravel
BingoGo17 天前
深入理解 Laravel Middleware:完整指南
后端·laravel
JaguarJack17 天前
深入理解 Laravel Middleware:完整指南
后端·php·laravel
JaguarJack18 天前
15 个 Eloquent 高级技巧,瞬间提升你的 Laravel 应用性能
后端·php·laravel
JaguarJack19 天前
从零开始打造 Laravel 扩展包:开发、测试到发布完整指南
后端·php·laravel