一、项目结构
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.phpapp/blog/service/BlogPostService.phpapp/blog/controller/BlogPostController.phpapp/blog/request/BlogPostRequest.phpapp/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 代码生成器提供了以下功能:
- 智能表结构分析:自动读取数据库表结构
- 完整文件生成:Model、Service、Controller、Request、Routes
- 灵活配置:支持模块化、自定义命名、路由前缀等
- 代码质量:生成符合 PSR 标准的代码
- 扩展性强:模板化设计,易于自定义
- 安全考虑:自动生成验证规则
- 生产就绪:包含事务处理、错误处理、日志记录
使用建议:
- 将模板文件放在
stubs/目录下 - 根据项目需求自定义模板
- 可以在生成后手动调整生成的代码
- 为特殊表结构添加自定义生成逻辑
- 考虑将常用配置保存为预设(preset)