PHP基于命令行的crud生成器示例

一、项目结构

webman-crud-generator/
├── app/
│   └── command/              # 命令行工具
│       └── CrudGenerator.php
├── config/                   # 配置
├── stubs/                    # 模板文件
│   ├── model.stub
│   ├── service.stub
│   ├── request.stub
│   ├── controller.stub
│   └── route.stub
└── ... (Webman 标准结构)

二、命令行工具实现

1. 创建命令文件

<?php
// app/command/CrudGenerator.php
namespace app\command;

use app\base\Service\BaseService;
use support\Db;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Webman\Console\Application;
use Illuminate\Database\Connection;
use support\Log;

class CrudGenerator extends Command
{
    protected static $defaultName = 'make:crud';
    protected static $defaultDescription = 'Generate CRUD files for a database table';
    
    // 配置命令参数
    protected function configure()
    {
        $this->setDescription('Generate complete CRUD files for a database table')
            ->addArgument('table', InputArgument::REQUIRED, 'Database table name')
            ->addOption('model', 'm', InputOption::VALUE_OPTIONAL, 'Model class name (default: StudlyCase of table)')
            ->addOption('module', null, InputOption::VALUE_OPTIONAL, 'Module/namespace (e.g., admin, api)', '')
            ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force overwrite existing files')
            ->addOption('connection', 'c', InputOption::VALUE_OPTIONAL, 'Database connection name', 'default')
            ->addOption('prefix', null, InputOption::VALUE_OPTIONAL, 'Route prefix', '')
            ->addOption('skip-routes', null, InputOption::VALUE_NONE, 'Skip generating routes')
            ->addOption('skip-request', null, InputOption::VALUE_NONE, 'Skip generating request validation')
            ->addOption('only', null, InputOption::VALUE_OPTIONAL, 'Only generate specific files (model,service,controller,request)')
            ->addOption('with-resource', null, InputOption::VALUE_NONE, 'Generate resource controller methods (index, show, store, update, destroy)')
            ->addOption('soft-deletes', null, InputOption::VALUE_NONE, 'Add soft delete support');
    }
    
    // 执行命令
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $table = $input->getArgument('table');
        $modelName = $input->getOption('model') ?: $this->getModelName($table);
        $module = $input->getOption('module');
        $force = $input->getOption('force');
        $connection = $input->getOption('connection');
        $prefix = $input->getOption('prefix');
        $skipRoutes = $input->getOption('skip-routes');
        $skipRequest = $input->getOption('skip-request');
        $only = $input->getOption('only');
        $withResource = $input->getOption('with-resource');
        $softDeletes = $input->getOption('soft-deletes');
        
        $output->writeln("🚀 Generating CRUD for table: <info>{$table}</info>");
        
        try {
            // 1. 获取表结构信息
            $tableInfo = $this->getTableInfo($table, $connection);
            
            if (empty($tableInfo['columns'])) {
                $output->writeln("<error>❌ Table '{$table}' not found or has no columns</error>");
                return Command::FAILURE;
            }
            
            $output->writeln("📊 Found <info>" . count($tableInfo['columns']) . "</info> columns in table");
            
            // 2. 解析要生成的文件类型
            $fileTypes = ['model', 'service', 'controller', 'request'];
            if ($only) {
                $onlyTypes = explode(',', $only);
                $fileTypes = array_intersect($fileTypes, $onlyTypes);
            }
            
            if ($skipRequest) {
                $fileTypes = array_diff($fileTypes, ['request']);
            }
            
            // 3. 生成文件
            $generatedFiles = [];
            
            foreach ($fileTypes as $type) {
                $method = "generate" . ucfirst($type);
                if (method_exists($this, $method)) {
                    $result = $this->$method($table, $modelName, $module, $tableInfo, $force, [
                        'withResource' => $withResource,
                        'softDeletes' => $softDeletes,
                        'connection' => $connection,
                    ]);
                    
                    if ($result) {
                        $generatedFiles[] = $result;
                        $output->writeln("✅ Generated: <info>{$result}</info>");
                    }
                }
            }
            
            // 4. 生成路由(如果未跳过)
            if (!$skipRoutes && !in_array('routes', $only ? explode(',', $only) : [])) {
                $routeFile = $this->generateRoutes($table, $modelName, $module, $prefix, $withResource);
                if ($routeFile) {
                    $generatedFiles[] = $routeFile;
                    $output->writeln("✅ Generated: <info>{$routeFile}</info>");
                }
            }
            
            // 5. 输出成功信息
            $output->writeln("\n🎉 <info>CRUD generation completed!</info>");
            $output->writeln("📁 Generated files:");
            foreach ($generatedFiles as $file) {
                $output->writeln("  - {$file}");
            }
            
            // 6. 后续步骤提示
            if (!$skipRoutes && file_exists($this->getRoutesFilePath())) {
                $output->writeln("\n📋 <comment>Next steps:</comment>");
                $output->writeln("1. Review generated files in <info>app/</info> directory");
                $output->writeln("2. Add the following to your <info>config/route.php</info> if not done automatically:");
                $output->writeln("   <comment>require_once base_path('{$this->getRoutesFilePath()}');</comment>");
                $output->writeln("3. Run database migrations if needed");
                $output->writeln("4. Test the API endpoints");
            }
            
            return Command::SUCCESS;
            
        } catch (\Exception $e) {
            $output->writeln("\n<error>❌ Error: {$e->getMessage()}</error>");
            Log::error("CRUD Generator Error: " . $e->getMessage(), [
                'trace' => $e->getTraceAsString(),
                'table' => $table
            ]);
            return Command::FAILURE;
        }
    }
    
    /**
     * 获取表结构信息
     */
    private function getTableInfo(string $table, string $connection): array
    {
        $schema = Db::connection($connection);
        
        // 获取列信息
        $columns = [];
        $primaryKey = 'id';
        $timestamps = false;
        
        try {
            $schemaColumns = $schema->getSchemaBuilder()->getColumns($table);
            
            foreach ($schemaColumns as $column) {
                $columnInfo = [
                    'name' => $column['name'],
                    'type' => $column['type_name'],
                    'nullable' => $column['nullable'],
                    'default' => $column['default'],
                    'comment' => $column['comment'] ?? '',
                    'auto_increment' => stripos($column['type'], 'auto_increment') !== false,
                ];
                
                // 检查主键
                if ($column['primary']) {
                    $primaryKey = $column['name'];
                }
                
                // 检查时间戳字段
                if (in_array($column['name'], ['created_at', 'updated_at', 'deleted_at'])) {
                    $timestamps = true;
                }
                
                $columns[] = $columnInfo;
            }
            
        } catch (\Exception $e) {
            // 如果获取列信息失败,尝试另一种方式
            $columns = $this->getTableColumnsFallback($table, $connection);
        }
        
        // 获取索引信息
        $indexes = [];
        try {
            $schemaIndexes = $schema->getSchemaBuilder()->getIndexes($table);
            foreach ($schemaIndexes as $index) {
                if ($index['primary']) {
                    $primaryKey = $index['columns'][0] ?? 'id';
                }
                $indexes[] = $index;
            }
        } catch (\Exception $e) {
            // 忽略索引获取错误
        }
        
        return [
            'columns' => $columns,
            'primary_key' => $primaryKey,
            'timestamps' => $timestamps,
            'indexes' => $indexes,
            'connection' => $connection,
        ];
    }
    
    /**
     * 备用方法:获取表列信息
     */
    private function getTableColumnsFallback(string $table, string $connection): array
    {
        $columns = [];
        $schema = Db::connection($connection);
        
        $sql = "SHOW COLUMNS FROM `{$table}`";
        $results = $schema->select($sql);
        
        foreach ($results as $column) {
            $columns[] = [
                'name' => $column->Field,
                'type' => $this->normalizeType($column->Type),
                'nullable' => $column->Null === 'YES',
                'default' => $column->Default,
                'comment' => '',
                'auto_increment' => stripos($column->Extra, 'auto_increment') !== false,
            ];
        }
        
        return $columns;
    }
    
    /**
     * 规范化数据库类型
     */
    private function normalizeType(string $type): string
    {
        $type = strtolower($type);
        
        if (str_contains($type, 'int')) {
            return 'integer';
        } elseif (str_contains($type, 'char') || str_contains($type, 'text')) {
            return 'string';
        } elseif (str_contains($type, 'decimal') || str_contains($type, 'float') || str_contains($type, 'double')) {
            return 'decimal';
        } elseif (str_contains($type, 'bool')) {
            return 'boolean';
        } elseif (str_contains($type, 'date') || str_contains($type, 'time')) {
            return 'datetime';
        } elseif (str_contains($type, 'json')) {
            return 'json';
        }
        
        return 'string';
    }
    
    /**
     * 从表名生成模型名
     */
    private function getModelName(string $table): string
    {
        // 移除表前缀
        $prefixes = ['tb_', 't_', config('database.connections.mysql.prefix', '')];
        foreach ($prefixes as $prefix) {
            if (str_starts_with($table, $prefix)) {
                $table = substr($table, strlen($prefix));
                break;
            }
        }
        
        // 转换为驼峰命名
        $name = str_replace(['_', '-'], ' ', $table);
        $name = ucwords($name);
        $name = str_replace(' ', '', $name);
        
        return $name;
    }
    
    /**
     * 生成命名空间
     */
    private function getNamespace(string $type, string $module = ''): string
    {
        $baseNamespace = "app\\";
        
        if ($module) {
            $baseNamespace .= $module . "\\";
        }
        
        switch ($type) {
            case 'model':
                return $baseNamespace . 'model';
            case 'service':
                return $baseNamespace . 'service';
            case 'controller':
                return $baseNamespace . 'controller';
            case 'request':
                return $baseNamespace . 'request';
            default:
                return $baseNamespace . $type;
        }
    }
    
    /**
     * 生成文件路径
     */
    private function getFilePath(string $type, string $name, string $module = ''): string
    {
        $basePath = base_path() . '/app/';
        
        if ($module) {
            $basePath .= $module . '/';
        }
        
        switch ($type) {
            case 'model':
                return $basePath . 'model/' . $name . '.php';
            case 'service':
                return $basePath . 'service/' . $name . 'Service.php';
            case 'controller':
                return $basePath . 'controller/' . $name . 'Controller.php';
            case 'request':
                return $basePath . 'request/' . $name . 'Request.php';
            case 'routes':
                return $basePath . 'route/' . strtolower($name) . '.php';
            default:
                throw new \InvalidArgumentException("Unknown file type: {$type}");
        }
    }
    
    /**
     * 生成 Model 文件
     */
    private function generateModel(string $table, string $modelName, string $module, array $tableInfo, bool $force, array $options = []): ?string
    {
        $filePath = $this->getFilePath('model', $modelName, $module);
        
        // 检查文件是否存在
        if (!$force && file_exists($filePath)) {
            return null;
        }
        
        // 创建目录
        $dir = dirname($filePath);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        
        // 准备填充数据
        $fillable = [];
        $casts = [];
        $dates = [];
        
        foreach ($tableInfo['columns'] as $column) {
            $columnName = $column['name'];
            
            // 排除不需要的字段
            if (in_array($columnName, ['id', 'created_at', 'updated_at', 'deleted_at'])) {
                continue;
            }
            
            $fillable[] = $columnName;
            
            // 处理字段类型转换
            switch ($column['type']) {
                case 'integer':
                case 'bigint':
                    $casts[$columnName] = 'integer';
                    break;
                case 'boolean':
                case 'bool':
                    $casts[$columnName] = 'boolean';
                    break;
                case 'decimal':
                case 'float':
                case 'double':
                    $casts[$columnName] = 'float';
                    break;
                case 'json':
                    $casts[$columnName] = 'array';
                    break;
                case 'datetime':
                case 'date':
                case 'timestamp':
                    $dates[] = $columnName;
                    break;
            }
        }
        
        // 读取模板
        $template = file_get_contents(base_path() . '/stubs/model.stub');
        
        // 替换模板变量
        $replacements = [
            '{{namespace}}' => $this->getNamespace('model', $module),
            '{{class}}' => $modelName,
            '{{table}}' => $table,
            '{{primaryKey}}' => $tableInfo['primary_key'],
            '{{fillable}}' => $this->formatArray($fillable, 12),
            '{{casts}}' => $this->formatArray($casts, 12, ' => '),
            '{{dates}}' => $this->formatArray($dates, 12),
            '{{incrementing}}' => 'true',
            '{{timestamps}}' => $tableInfo['timestamps'] ? 'true' : 'false',
            '{{softDeletes}}' => $options['softDeletes'] ? "use Illuminate\\Database\\Eloquent\\SoftDeletes;\n" : '',
            '{{softDeletesUse}}' => $options['softDeletes'] ? "use SoftDeletes;\n    " : '',
            '{{connection}}' => $tableInfo['connection'] !== 'default' ? "'" . $tableInfo['connection'] . "'" : 'null',
        ];
        
        $content = str_replace(
            array_keys($replacements),
            array_values($replacements),
            $template
        );
        
        // 写入文件
        file_put_contents($filePath, $content);
        
        return $filePath;
    }
    
    /**
     * 生成 Service 文件
     */
    private function generateService(string $table, string $modelName, string $module, array $tableInfo, bool $force, array $options = []): ?string
    {
        $serviceName = $modelName . 'Service';
        $filePath = $this->getFilePath('service', $modelName, $module);
        
        if (!$force && file_exists($filePath)) {
            return null;
        }
        
        $dir = dirname($filePath);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        
        // 读取模板
        $template = file_get_contents(base_path() . '/stubs/service.stub');
        
        // 生成验证规则
        $rules = $this->generateValidationRules($tableInfo['columns']);
        
        $replacements = [
            '{{namespace}}' => $this->getNamespace('service', $module),
            '{{class}}' => $serviceName,
            '{{modelNamespace}}' => $this->getNamespace('model', $module) . '\\' . $modelName,
            '{{model}}' => $modelName,
            '{{modelVariable}}' => lcfirst($modelName),
            '{{validationRules}}' => $this->formatArray($rules, 8, ' => '),
        ];
        
        $content = str_replace(
            array_keys($replacements),
            array_values($replacements),
            $template
        );
        
        file_put_contents($filePath, $content);
        
        return $filePath;
    }
    
    /**
     * 生成 Request 文件
     */
    private function generateRequest(string $table, string $modelName, string $module, array $tableInfo, bool $force, array $options = []): ?string
    {
        $requestName = $modelName . 'Request';
        $filePath = $this->getFilePath('request', $modelName, $module);
        
        if (!$force && file_exists($filePath)) {
            return null;
        }
        
        $dir = dirname($filePath);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        
        // 读取模板
        $template = file_get_contents(base_path() . '/stubs/request.stub');
        
        // 生成验证规则
        $rules = $this->generateValidationRules($tableInfo['columns']);
        
        $replacements = [
            '{{namespace}}' => $this->getNamespace('request', $module),
            '{{class}}' => $requestName,
            '{{validationRules}}' => $this->formatArray($rules, 8, ' => '),
        ];
        
        $content = str_replace(
            array_keys($replacements),
            array_values($replacements),
            $template
        );
        
        file_put_contents($filePath, $content);
        
        return $filePath;
    }
    
    /**
     * 生成 Controller 文件
     */
    private function generateController(string $table, string $modelName, string $module, array $tableInfo, bool $force, array $options = []): ?string
    {
        $controllerName = $modelName . 'Controller';
        $filePath = $this->getFilePath('controller', $modelName, $module);
        
        if (!$force && file_exists($filePath)) {
            return null;
        }
        
        $dir = dirname($filePath);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        
        // 读取模板
        $template = file_get_contents(base_path() . '/stubs/controller.stub');
        
        $serviceNamespace = $this->getNamespace('service', $module) . '\\' . $modelName . 'Service';
        $requestNamespace = $this->getNamespace('request', $module) . '\\' . $modelName . 'Request';
        
        $replacements = [
            '{{namespace}}' => $this->getNamespace('controller', $module),
            '{{class}}' => $controllerName,
            '{{model}}' => $modelName,
            '{{modelVariable}}' => lcfirst($modelName),
            '{{serviceNamespace}}' => $serviceNamespace,
            '{{service}}' => $modelName . 'Service',
            '{{requestNamespace}}' => $requestNamespace,
            '{{request}}' => $modelName . 'Request',
            '{{resourceMethods}}' => $options['withResource'] ? $this->generateResourceMethods($modelName) : '',
        ];
        
        $content = str_replace(
            array_keys($replacements),
            array_values($replacements),
            $template
        );
        
        file_put_contents($filePath, $content);
        
        return $filePath;
    }
    
    /**
     * 生成路由文件
     */
    private function generateRoutes(string $table, string $modelName, string $module, string $prefix, bool $withResource): ?string
    {
        $routeFileName = strtolower($modelName);
        $filePath = $this->getFilePath('routes', $modelName, $module);
        
        $dir = dirname($filePath);
        if (!is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        
        // 读取模板
        $template = file_get_contents(base_path() . '/stubs/route.stub');
        
        $controllerNamespace = $this->getNamespace('controller', $module) . '\\' . $modelName . 'Controller';
        $routePath = $prefix ?: strtolower(str_replace('_', '-', $table));
        
        $replacements = [
            '{{routePath}}' => $routePath,
            '{{controller}}' => $controllerNamespace,
            '{{resourceRoutes}}' => $withResource ? "->resource();" : '',
        ];
        
        $content = str_replace(
            array_keys($replacements),
            array_values($replacements),
            $template
        );
        
        file_put_contents($filePath, $content);
        
        return $filePath;
    }
    
    /**
     * 获取路由文件路径
     */
    private function getRoutesFilePath(): string
    {
        return 'app/route';
    }
    
    /**
     * 生成验证规则
     */
    private function generateValidationRules(array $columns): array
    {
        $rules = [];
        
        foreach ($columns as $column) {
            $rule = [];
            
            // 处理必填
            if (!$column['nullable'] && $column['default'] === null && $column['name'] !== 'id') {
                $rule[] = 'required';
            } else {
                $rule[] = 'nullable';
            }
            
            // 处理类型
            switch ($column['type']) {
                case 'integer':
                case 'bigint':
                    $rule[] = 'integer';
                    break;
                case 'string':
                case 'varchar':
                case 'text':
                    $rule[] = 'string';
                    if (str_contains($column['type'], 'varchar')) {
                        // 提取长度
                        preg_match('/varchar\((\d+)\)/', $column['type'], $matches);
                        if (isset($matches[1])) {
                            $rule[] = 'max:' . $matches[1];
                        }
                    }
                    break;
                case 'decimal':
                case 'float':
                case 'double':
                    $rule[] = 'numeric';
                    break;
                case 'boolean':
                    $rule[] = 'boolean';
                    break;
                case 'datetime':
                case 'date':
                    $rule[] = 'date';
                    break;
                case 'json':
                    $rule[] = 'array';
                    break;
            }
            
            $rules[$column['name']] = implode('|', $rule);
        }
        
        return $rules;
    }
    
    /**
     * 生成资源控制器方法
     */
    private function generateResourceMethods(string $modelName): string
    {
        $methods = <<<PHP

    /**
     * 列表
     */
    public function index(Request \$request)
    {
        \$page = \$request->input('page', 1);
        \$size = \$request->input('size', 20);
        
        \$list = \$this->service->getList([], \$page, \$size);
        
        return json([
            'code' => 0,
            'message' => 'success',
            'data' => \$list
        ]);
    }
    
    /**
     * 详情
     */
    public function show(\$id)
    {
        \$data = \$this->service->find(\$id);
        
        if (!\$data) {
            return json(['code' => 404, 'message' => 'Not found']);
        }
        
        return json([
            'code' => 0,
            'message' => 'success',
            'data' => \$data
        ]);
    }
    
    /**
     * 创建
     */
    public function store({$modelName}Request \$request)
    {
        \$validated = \$request->validated();
        \$result = \$this->service->create(\$validated);
        
        return json([
            'code' => 0,
            'message' => 'Created successfully',
            'data' => \$result
        ]);
    }
    
    /**
     * 更新
     */
    public function update({$modelName}Request \$request, \$id)
    {
        \$validated = \$request->validated();
        \$result = \$this->service->update(\$id, \$validated);
        
        if (!\$result) {
            return json(['code' => 404, 'message' => 'Not found']);
        }
        
        return json([
            'code' => 0,
            'message' => 'Updated successfully',
            'data' => \$result
        ]);
    }
    
    /**
     * 删除
     */
    public function destroy(\$id)
    {
        \$result = \$this->service->delete(\$id);
        
        if (!\$result) {
            return json(['code' => 404, 'message' => 'Not found']);
        }
        
        return json([
            'code' => 0,
            'message' => 'Deleted successfully'
        ]);
    }
PHP;
        
        return $methods;
    }
    
    /**
     * 格式化数组为字符串
     */
    private function formatArray(array $array, int $indent = 4, string $operator = ' => '): string
    {
        if (empty($array)) {
            return '[]';
        }
        
        $result = "[\n";
        $indentStr = str_repeat(' ', $indent);
        
        foreach ($array as $key => $value) {
            if (is_numeric($key)) {
                $result .= "{$indentStr}'{$value}',\n";
            } else {
                if (is_string($value) && !str_starts_with($value, '[')) {
                    $value = "'{$value}'";
                }
                $result .= "{$indentStr}'{$key}'{$operator}{$value},\n";
            }
        }
        
        $result .= str_repeat(' ', $indent - 4) . ']';
        
        return $result;
    }
}

三、创建模板文件

1. Model 模板

<?php
// stubs/model.stub
namespace {{namespace}};

use Illuminate\Database\Eloquent\Model;
{{softDeletes}}

class {{class}} extends Model
{
    {{softDeletesUse}}
    /**
     * 表名
     */
    protected $table = '{{table}}';
    
    /**
     * 主键
     */
    protected $primaryKey = '{{primaryKey}}';
    
    /**
     * 是否自增
     */
    public $incrementing = {{incrementing}};
    
    /**
     * 是否启用时间戳
     */
    public $timestamps = {{timestamps}};
    
    /**
     * 数据库连接
     */
    protected $connection = {{connection}};
    
    /**
     * 可批量赋值的属性
     */
    protected $fillable = {{fillable}};
    
    /**
     * 类型转换
     */
    protected $casts = {{casts}};
    
    /**
     * 日期字段
     */
    protected $dates = {{dates}};
    
    /**
     * 隐藏字段
     */
    protected $hidden = [];
}

2. Service 模板

<?php
// stubs/service.stub
namespace {{namespace}};

use app\base\Service\BaseService;
use {{modelNamespace}};
use support\Db;
use support\Log;

class {{class}} extends BaseService
{
    /**
     * 获取模型类
     */
    protected function getModel(): string
    {
        return {{model}}::class;
    }
    
    /**
     * 验证规则
     */
    protected function getValidationRules(): array
    {
        return {{validationRules}};
    }
    
    /**
     * 创建前处理
     */
    protected function beforeCreate(array &$data)
    {
        // 在创建前可以处理数据
    }
    
    /**
     * 创建后处理
     */
    protected function afterCreate(array $data, $model)
    {
        // 在创建后可以执行其他操作
    }
    
    /**
     * 更新前处理
     */
    protected function beforeUpdate(int $id, array &$data)
    {
        // 在更新前可以处理数据
    }
    
    /**
     * 更新后处理
     */
    protected function afterUpdate($model, array $data)
    {
        // 在更新后可以执行其他操作
    }
    
    /**
     * 删除前处理
     */
    protected function beforeDelete($model)
    {
        // 在删除前可以执行检查
    }
    
    /**
     * 删除后处理
     */
    protected function afterDelete($model)
    {
        // 在删除后可以清理相关数据
    }
    
    /**
     * 获取列表(可覆盖)
     */
    public function getList(array $conditions = [], int $page = 1, int $size = 20): array
    {
        $query = $this->getModel()::query();
        
        // 添加条件
        foreach ($conditions as $field => $value) {
            if (is_array($value)) {
                $query->whereIn($field, $value);
            } else {
                $query->where($field, $value);
            }
        }
        
        $paginator = $query->orderBy('id', 'desc')->paginate($size, ['*'], 'page', $page);
        
        return [
            'list' => $paginator->items(),
            'pagination' => [
                'total' => $paginator->total(),
                'current_page' => $paginator->currentPage(),
                'per_page' => $paginator->perPage(),
                'has_more' => $paginator->hasMorePages(),
            ]
        ];
    }
}

3. Request 模板

<?php
// stubs/request.stub
namespace {{namespace}};

use app\base\Request\BaseRequest;
use Respect\Validation\Validator as v;

class {{class}} extends BaseRequest
{
    /**
     * 获取验证规则
     */
    public function rules(): array
    {
        return {{validationRules}};
    }
    
    /**
     * 自定义错误消息
     */
    public function messages(): array
    {
        return [
            // 'field.required' => '字段是必填的',
        ];
    }
    
    /**
     * 验证前处理
     */
    public function prepareForValidation()
    {
        // 在验证前可以处理数据
    }
    
    /**
     * 验证后处理
     */
    public function afterValidation()
    {
        // 在验证后可以处理数据
    }
}

4. Controller 模板

<?php
// stubs/controller.stub
namespace {{namespace}};

use support\Request;
use {{serviceNamespace}};
use {{requestNamespace}};
use app\base\Controller\BaseController;

class {{class}} extends BaseController
{
    protected {{service}} $service;
    
    public function __construct({{service}} $service)
    {
        $this->service = $service;
    }
{{resourceMethods}}
    /**
     * 自定义操作示例
     */
    public function customAction(Request $request)
    {
        // 这里可以添加自定义的业务逻辑
        return json(['code' => 0, 'message' => 'Custom action']);
    }
}

5. Route 模板

<?php
// stubs/route.stub
use Webman\Route;

// {{class}} 路由
Route::group('/api/{{routePath}}', function () {
    Route::get('/', [{{controller}}::class, 'index']);
    Route::get('/{id}', [{{controller}}::class, 'show']);
    Route::post('/', [{{controller}}::class, 'store']);
    Route::put('/{id}', [{{controller}}::class, 'update']);
    Route::delete('/{id}', [{{controller}}::class, 'destroy']);
    // 可以添加自定义路由
    // Route::post('/custom', [{{controller}}::class, 'customAction']);
});

四、基础类定义

1. BaseService

<?php
// app/base/Service/BaseService.php
namespace app\base\Service;

use Illuminate\Database\Eloquent\Model;
use support\Db;
use support\Log;
use Respect\Validation\Validator as v;
use Respect\Validation\Exceptions\NestedValidationException;

abstract class BaseService
{
    /**
     * 获取模型类
     */
    abstract protected function getModel(): string;
    
    /**
     * 获取验证规则
     */
    abstract protected function getValidationRules(): array;
    
    /**
     * 验证数据
     */
    protected function validate(array $data, array $rules = null): array
    {
        $rules = $rules ?: $this->getValidationRules();
        $filteredRules = [];
        
        foreach ($rules as $field => $rule) {
            if (array_key_exists($field, $data) || (is_string($rule) && str_contains($rule, 'required'))) {
                $filteredRules[$field] = $rule;
            }
        }
        
        $validator = v::create();
        
        foreach ($filteredRules as $field => $rule) {
            $validator->key($field, $this->createValidator($rule));
        }
        
        try {
            $validator->assert($data);
        } catch (NestedValidationException $e) {
            $errors = [];
            foreach ($e->getMessages() as $messages) {
                foreach ($messages as $message) {
                    $errors[] = $message;
                }
            }
            throw new \InvalidArgumentException(implode(', ', $errors));
        }
        
        return $data;
    }
    
    /**
     * 创建验证器
     */
    private function createValidator(string $rule)
    {
        $validators = explode('|', $rule);
        $validator = v::create();
        
        foreach ($validators as $v) {
            if (empty($v)) continue;
            
            if (str_contains($v, ':')) {
                list($method, $params) = explode(':', $v, 2);
                $params = explode(',', $params);
                $validator = $validator->$method(...$params);
            } else {
                $validator = $validator->$v();
            }
        }
        
        return $validator;
    }
    
    /**
     * 创建记录
     */
    public function create(array $data): Model
    {
        $data = $this->validate($data);
        
        $this->beforeCreate($data);
        
        Db::beginTransaction();
        
        try {
            $modelClass = $this->getModel();
            $model = $modelClass::create($data);
            
            $this->afterCreate($data, $model);
            
            Db::commit();
            
            return $model;
            
        } catch (\Exception $e) {
            Db::rollBack();
            Log::error('创建失败: ' . $e->getMessage(), ['data' => $data]);
            throw $e;
        }
    }
    
    /**
     * 更新记录
     */
    public function update(int $id, array $data): ?Model
    {
        $data = $this->validate($data, false);
        
        $this->beforeUpdate($id, $data);
        
        Db::beginTransaction();
        
        try {
            $model = $this->find($id);
            if (!$model) {
                return null;
            }
            
            $model->update($data);
            
            $this->afterUpdate($model, $data);
            
            Db::commit();
            
            return $model;
            
        } catch (\Exception $e) {
            Db::rollBack();
            Log::error('更新失败: ' . $e->getMessage(), ['id' => $id, 'data' => $data]);
            throw $e;
        }
    }
    
    /**
     * 删除记录
     */
    public function delete(int $id): bool
    {
        $model = $this->find($id);
        if (!$model) {
            return false;
        }
        
        $this->beforeDelete($model);
        
        Db::beginTransaction();
        
        try {
            $result = $model->delete();
            
            $this->afterDelete($model);
            
            Db::commit();
            
            return $result;
            
        } catch (\Exception $e) {
            Db::rollBack();
            Log::error('删除失败: ' . $e->getMessage(), ['id' => $id]);
            throw $e;
        }
    }
    
    /**
     * 查找记录
     */
    public function find(int $id): ?Model
    {
        $modelClass = $this->getModel();
        return $modelClass::find($id);
    }
    
    /**
     * 查找或失败
     */
    public function findOrFail(int $id): Model
    {
        $modelClass = $this->getModel();
        return $modelClass::findOrFail($id);
    }
    
    // 钩子方法(可在子类中覆盖)
    protected function beforeCreate(array &$data) {}
    protected function afterCreate(array $data, Model $model) {}
    protected function beforeUpdate(int $id, array &$data) {}
    protected function afterUpdate(Model $model, array $data) {}
    protected function beforeDelete(Model $model) {}
    protected function afterDelete(Model $model) {}
}

2. BaseController

<?php
// app/base/Controller/BaseController.php
namespace app\base\Controller;

abstract class BaseController
{
    // 基础控制器逻辑
}

3. BaseRequest

<?php
// app/base/Request/BaseRequest.php
namespace app\base\Request;

abstract class BaseRequest
{
    abstract public function rules(): array;
    
    public function messages(): array
    {
        return [];
    }
    
    public function prepareForValidation() {}
    public function afterValidation() {}
}

五、配置命令行

1. 创建配置文件

<?php
// config/command.php
return [
    app\command\CrudGenerator::class,
];

2. 安装依赖

composer require symfony/console

六、使用示例

1. 基本使用

# 生成 users 表的 CRUD
php webman make:crud users

# 指定模型名
php webman make:crud user_profiles --model=UserProfile

# 生成到指定模块
php webman make:crud articles --module=admin

# 强制覆盖已存在的文件
php webman make:crud products --force

# 生成资源路由控制器
php webman make:crud categories --with-resource

# 只生成特定文件
php webman make:crud orders --only=model,service

2. 完整示例

php webman make:crud blog_posts \
  --model=BlogPost \
  --module=blog \
  --with-resource \
  --soft-deletes \
  --prefix=blog/posts

这会生成:

  • app/blog/model/BlogPost.php
  • app/blog/service/BlogPostService.php
  • app/blog/controller/BlogPostController.php
  • app/blog/request/BlogPostRequest.php
  • app/blog/route/blogpost.php

七、高级功能扩展

1. 添加更多模板变量

// 在 CrudGenerator 类中添加
private function getTemplateVariables(string $table, string $modelName, array $tableInfo): array
{
    return [
        'table' => $table,
        'model' => $modelName,
        'columns' => $tableInfo['columns'],
        'hasTimestamps' => $tableInfo['timestamps'],
        'primaryKey' => $tableInfo['primary_key'],
        'connection' => $tableInfo['connection'],
        'fillableFields' => $this->getFillableFields($tableInfo['columns']),
        'validationRules' => $this->generateValidationRules($tableInfo['columns']),
    ];
}

2. 支持更多数据库类型

private function getValidationRuleForType(string $type, array $column): string
{
    $rules = [];
    
    switch ($type) {
        case 'integer':
        case 'bigint':
            $rules[] = 'integer';
            break;
        case 'decimal':
        case 'float':
        case 'double':
            $rules[] = 'numeric';
            break;
        case 'boolean':
            $rules[] = 'boolean';
            break;
        case 'datetime':
        case 'timestamp':
            $rules[] = 'date';
            break;
        case 'json':
            $rules[] = 'array';
            break;
        case 'enum':
            $values = $this->parseEnumValues($column['type']);
            if ($values) {
                $rules[] = 'in:' . implode(',', $values);
            }
            break;
        default:
            $rules[] = 'string';
            break;
    }
    
    return implode('|', $rules);
}

3. 生成 API 文档

private function generateApiDoc(string $modelName, array $tableInfo): string
{
    $doc = <<<YAML
/**
 * @OA\Tag(
 *     name="{$modelName}",
 *     description="{$modelName} Management"
 * )
 */
 
/**
 * @OA\Schema(
 *     schema="{$modelName}",
 *     required={$this->getRequiredFields($tableInfo['columns'])},
 *     @OA\Property(property="id", type="integer", format="int64"),
{$this->generateSchemaProperties($tableInfo['columns'])}
 * )
 */
YAML;
    
    return $doc;
}

八、总结

这个 CRUD 代码生成器提供了以下功能:

  1. 智能表结构分析:自动读取数据库表结构
  2. 完整文件生成:Model、Service、Controller、Request、Routes
  3. 灵活配置:支持模块化、自定义命名、路由前缀等
  4. 代码质量:生成符合 PSR 标准的代码
  5. 扩展性强:模板化设计,易于自定义
  6. 安全考虑:自动生成验证规则
  7. 生产就绪:包含事务处理、错误处理、日志记录

使用建议

  1. 将模板文件放在 stubs/目录下
  2. 根据项目需求自定义模板
  3. 可以在生成后手动调整生成的代码
  4. 为特殊表结构添加自定义生成逻辑
  5. 考虑将常用配置保存为预设(preset)

Comments

No comments yet. Why don’t you start the discussion?

发表回复