基于 Laravel 命令行开发 API 代码生成器

1. 命令行文件生成

$ php artisan make:command ApiGenerator

2. 编写代码模板

就像你看到的,我使用了 php 的 heredoc 方式,不太优雅。开始用的文件方式,但是不支持替换数组,就放弃了;有好的建议欢迎提。

App\Traits\GeneratorTemplate

<?php
/**
 * Created by PhpStorm.
 * User: JeffreyBool
 * Date: 2019/11/18
 * Time: 01:20
 */

namespace App\Traits;

trait GeneratorTemplate
{
    /**
     * 创建验证模板.
     * @param $dummyNamespace
     * @param $modelName
     * @param $storeRules
     * @param $updateRules
     * @param $storeMessages
     * @param $updateMessages
     * @return string
     */
    public function genValidationTemplate(
        $dummyNamespace,
        $modelName,
        $storeRules,
        $updateRules,
        $storeMessages,
        $updateMessages
    ) {
        $template = <<<EOF
<?php
namespace {$dummyNamespace};

class {$modelName}
{
    /**
     * @return array
     */
    public function store()
    {
        /**
         * 新增验证规则
         */
        return [
            'rules'=> $storeRules,

            'messgaes'=> $storeMessages
        ];
     }

    /**
     * 编辑验证规则
     */
    public function update()
    {
        return [
           'rules'=> $updateRules,

           'messgaes'=> $updateMessages
        ];
    }
}
EOF;
        return $template;
    }

    /**
     * 创建资源返回模板.
     * @param $dummyNamespace
     * @param $modelName
     * @return string
     */
    public function genResourceTemplate($dummyNamespace, $modelName)
    {
        $template = <<<EOF
<?php
namespace {$dummyNamespace};

use Illuminate\Http\Resources\Json\JsonResource;

class {$modelName}Resource extends JsonResource
{
    /**
     * Transform the resource into an array.
     *
     * @param  \Illuminate\Http\Request  \$request
     * @return array
     */
    public function toArray(\$request)
    {
        return parent::toArray(\$request);
    }
}
EOF;
        return $template;
    }

    /**
     * 创建控制器模板.
     * @param $dummyNamespace
     * @param $modelName
     * @param $letterModelName
     * @param $modelNamePluralLowerCase
     * @return string
     */
    public function genControllerTemplate($dummyNamespace, $modelName, $letterModelName, $modelNamePluralLowerCase)
    {
        $template = <<<EOF
<?php
namespace {$dummyNamespace};

use Illuminate\Http\Request;
use App\Models\\{$modelName};
use App\Http\Resources\\{$modelName}Resource;

class {$modelName}Controller extends Controller
{
    /**
     * Get {$modelName} Paginate.
     * @param {$modelName} \${$letterModelName}
     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
     */
    public function index({$modelName} \${$letterModelName})
    {
        \${$modelNamePluralLowerCase} = \${$letterModelName}->paginate();
        return {$modelName}Resource::collection(\${$modelNamePluralLowerCase});
    }

    /**
     * Create {$modelName}.
     * @param Request         \$request
     * @param {$modelName} \${$letterModelName}
     * @return \Illuminate\Http\Response
     */
    public function store(Request \$request, {$modelName} \${$letterModelName})
    {
        \$this->validateRequest(\$request);
        \${$letterModelName}->fill(\$request->all());
        \${$letterModelName}->save();

        return \$this->created(\${$letterModelName});
    }

    /**
     * All {$modelName}.
     * @param Request         \$request
     * @param {$modelName} \${$letterModelName}
     * @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
     */
    public function all(Request \$request, {$modelName} \${$letterModelName})
    {
       \${$modelNamePluralLowerCase} = \${$letterModelName}->get();

       return {$modelName}Resource::collection(\${$modelNamePluralLowerCase});
    }

    /**
     * Show {$modelName}.
     * @param {$modelName} \${$letterModelName}
     * @return {$modelName}Resource
     */
    public function show({$modelName} \${$letterModelName})
    {
        return new {$modelName}Resource(\${$letterModelName});
    }

    /**
     * Update {$modelName}.
     * @param Request         \$request
     * @param {$modelName} \${$letterModelName}
     * @return \Illuminate\Http\Response
     */
    public function update(Request \$request, {$modelName} \${$letterModelName})
    {
        \$this->validateRequest(\$request);
        \${$letterModelName}->fill(\$request->all());
        \${$letterModelName}->save();

        return \$this->noContent();
    }

    /**
     * Delete {$modelName}.
     * @param {$modelName} \${$letterModelName}
     * @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response
     * @throws \Exception
     */
    public function destroy({$modelName} \${$letterModelName})
    {
        \${$letterModelName}->delete();
        return \$this->noContent();
    }
}
EOF;
        return $template;
    }

    /**
     * 创建模型模板.
     * @param $dummyNamespace
     * @param $modelName
     * @param $fields
     * @return string
     */
    public function genModelTemplate($dummyNamespace, $modelName, $fields)
    {
        $template = <<<EOF
<?php
namespace {$dummyNamespace};

class {$modelName} extends Model
{
    protected \$fillable = {$fields};
}
EOF;
        return $template;
    }
}

 3. 实现代码生成器

现在让我们来实现第 1 步所创建的控制台命令。
app/Console/Commands 文件夹找到 ApiGenerator.php

当然,该命令还没有设置,这就是为什么你看到一个默认的名称和说明。

修改命令标志和描述,如下:

    /**
     * The name and signature of the console command.
     * @var string
     */
    protected $signature = 'api:generator
    {name : Class (singular) for example User}';

    /**
     * The console command description.
     * @var string
     */
    protected $description = 'Create Api operations';

描述要简洁、明了。

至于命令标志,可以根据个人喜好命名,就是后面我们要调用的 artisan 命令,如下:

$ php artisan api:generator RoleMenu

接下来实现数据库表结构读取

App\Traits\MysqlStructure.php

<?php
namespace App\Traits;

use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Console\Exception\RuntimeException;

trait MysqlStructure
{

    private $db;

    private $database;

    private $doctrineTypeMapping = [
        'tinyint'    => 'boolean',
        'smallint'   => 'smallint',
        'mediumint'  => 'integer',
        'int'        => 'integer',
        'integer'    => 'integer',
        'bigint'     => 'bigint',
        'tinytext'   => 'text',
        'mediumtext' => 'text',
        'longtext'   => 'text',
        'text'       => 'text',
        'varchar'    => 'string',
        'string'     => 'string',
        'char'       => 'string',
        'date'       => 'date',
        'datetime'   => 'datetime',
        'timestamp'  => 'datetime',
        'time'       => 'time',
        'float'      => 'float',
        'double'     => 'float',
        'real'       => 'float',
        'decimal'    => 'decimal',
        'numeric'    => 'decimal',
        'year'       => 'date',
        'longblob'   => 'blob',
        'blob'       => 'blob',
        'mediumblob' => 'blob',
        'tinyblob'   => 'blob',
        'binary'     => 'binary',
        'varbinary'  => 'binary',
        'set'        => 'simple_array',
        'json'       => 'json',
    ];

    /**
     * 表字段类型替换成laravel字段类型
     * @param string $table
     * @return Collection
     */
    public function tableFieldsReplaceModelFields(string $table): Collection
    {
        $sql = sprintf('SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = \'%s\' AND TABLE_NAME = \'%s\' ',
            $this->getDatabase(), $table);
        $columns = collect(DB::select($sql));
        if($columns->isEmpty()) {
            throw new RuntimeException(sprintf('Not Found Table, got "%s".', $table));
        }
        $columns = $columns->map(function($column) {
            if($column && $column->DATA_TYPE) {
                if(array_key_exists($column->DATA_TYPE,$this->doctrineTypeMapping)) {
                    $column->DATA_TYPE = $this->doctrineTypeMapping[$column->DATA_TYPE];
                }
            }
            return $column;
        });
        return $columns;
    }

    /**
     * 获取数据库所有表
     * @return array
     */
    protected function getAllTables()
    {
        $tables = DB::select('show tables');
        $box = [];
        $key = 'Tables_in_' . $this->db;
        foreach($tables as $tableName) {
            $tableName = $tableName->$key;
            $box[] = $tableName;
        }
        return $box;
    }

    /**
     * 输出表信息
     * @param $tableName
     */
    protected function outTableAction($tableName)
    {
        $columns = $this->getTableColumns($tableName);
        $rows = [];
        foreach($columns as $column) {
            $rows[] = [
                $column->COLUMN_NAME,
                $column->COLUMN_TYPE,
                $column->COLUMN_DEFAULT,
                $column->IS_NULLABLE,
                $column->EXTRA,
                $column->COLUMN_COMMENT,
            ];
        }
        $header = ['COLUMN', 'TYPE', 'DEFAULT', 'NULLABLE', 'EXTRA', 'COMMENT'];
        $this->table($header, $rows);
    }

    /**
     * 输出某个表所有字段
     * @param $tableName
     * @return mixed
     */
    public function getTableFields($tableName)
    {
        $columns = collect($this->getTableColumns($tableName));
        $columns = $columns->pluck('COLUMN_NAME');
        $columns = $columns->map(function($value) {
            return "'{$value}'";
        });
        return $columns->toArray();
    }

    /**
     * 获取数据库的表名
     * @param $table
     * @return array
     */
    public function getTableColumns($table)
    {
        $sql = sprintf('SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = \'%s\' AND TABLE_NAME = \'%s\' ',
            $this->getDatabase(), $table);
        $columns = DB::select($sql);
        if(!$columns) {
            throw new RuntimeException(sprintf('Not Found Table, got "%s".', $table));
        }
        return $columns;
    }

    /**
     * 获取表注释
     * @param $table
     * @return string
     */
    public function getTableComment($table)
    {
        $sql = sprintf('SELECT TABLE_COMMENT FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = \'%s\' AND TABLE_SCHEMA = \'%s\'',
            $table, $this->getDatabase());
        $tableComment = DB::selectOne($sql);
        if(!$tableComment) {
            return '';
        }
        return $tableComment->TABLE_COMMENT;
    }

    public function getDatabase()
    {
        return env('DB_DATABASE');
    }
}

上面是我封装的数据库表信息查询 sql 的文件。

生成代码实现

下面,我们来看看怎样使用App\Traits\GeneratorTemplate 文件夹下的 model 模板创建模型。

/**
 * 创建模型
 * @param $name
 */
protected function model($name)
{
    $namespace = $this->getDefaultNamespace('Models');
    $table = Str::snake(Str::pluralStudly(class_basename($this->argument('name'))));
    $columns = $this->getTableFields($table);
    $fields = "[";
    for($i = 0; $i < count($columns); $i++) {
        $column = $columns[$i];
        if(in_array($column, ["'id'", "'created_at'", "'updated_at'", "'status'"])) {
            continue;
        }
        $fields .= sprintf("%s,", $column);
    }
    $fields .= "]";
    $fields = str_replace(",]", "]", $fields);
    $modelTemplate = $this->genModelTemplate($namespace, $name, $fields);
    $class = $namespace . '\\' . $name;
    if(class_exists($class)) {
        throw new RuntimeException(sprintf('class %s exist', $class));
    }
    file_put_contents(app_path("/Models/{$name}.php"), $modelTemplate);
    $this->info($name . ' created model successfully.');
}

从代码可以看到,model方法需要一个 name 参数,它由我们在 artisan 命令里传入。
看看 $modelTemplate 属性。我们使用变量把model模板文件里的占位符替换为我们期望的值。

基本上,在App\Traits\GeneratorTemplate文件里,我们用$name替换了{{modelName}}。请记住,在我们的例子中,$name的值是 RoleMenu。

你可以打开App\Traits\GeneratorTemplate文件检查一下,所有的{{modelName}}都被替换为了 RoleMenu。

file_put_contents函数再次使用了$name创建了一个新文件,因此它被命名为RoleMenu.php。并且,我们给这个文件传入内容,这些内容是从$modelTemplate属性获取的。$modelTemplate属性值是App\Traits\GeneratorTemplate文件的内容,只是所有的占位符均被替换了。

同样的事情还发生在controllervalidation方法里。因此,我将这两个方法的内容粘贴在这里。

App\Console\Commands\ApiGenerator.php

<?php

namespace App\Console\Commands;

use Illuminate\Support\Str;
use App\Traits\MysqlStructure;
use Illuminate\Console\Command;
use App\Traits\GeneratorTemplate;
use Symfony\Component\Console\Exception\RuntimeException;

class ApiGenerator extends Command
{
    use MysqlStructure, GeneratorTemplate;

    private $db;

    /**
     * The name and signature of the console command.
     * @var string
     */
    protected $signature = 'api:generator
    {name : Class (singular) for example User}';

    /**
     * The console command description.
     * @var string
     */
    protected $description = 'Create Api operations';

    public function __construct()
    {
        parent::__construct();
        $this->db = env('DB_DATABASE');
    }

    /**
     * Get the root namespace for the class.
     * @return string
     */
    protected function rootNamespace()
    {
        return $this->laravel->getNamespace();
    }

    /**
     * Get the default namespace for the class.
     * @param $name
     * @return string
     */
    protected function getDefaultNamespace($name)
    {
        $namespace = trim($this->rootNamespace(), '\\') . '\\' . $name;
        return $namespace;
    }

    /**
     * 获取规则文件
     * @param $type
     * @return bool|string
     */
    protected function getStub($type)
    {
        return file_get_contents(resource_path("stubs/$type.stub"));
    }

    /**
     * 创建规则文件
     * @param $name
     */
    protected function validation($name)
    {
        $namespace = $this->getDefaultNamespace('Http\Validations\Api');
        $table = Str::snake(Str::pluralStudly(class_basename($this->argument('name'))));
        $columns = $this->tableFieldsReplaceModelFields($table);
        $rules = "[\n";
        $messgaes = '[]';
        foreach($columns as $column) {
            if(in_array($column->COLUMN_NAME, ['id', 'created_at', 'updated_at', 'status'])) {
                continue;
            }
            $rule = '';
            if($column->IS_NULLABLE == "YES") {
                $rule .= 'required';
            } else {
                $rule .= 'nullable';
            }
            if($column->CHARACTER_MAXIMUM_LENGTH) {
                $rule .= '|max:' . $column->CHARACTER_MAXIMUM_LENGTH;
            }
            $rules .= sprintf("                '%s' => '%s',\n", $column->COLUMN_NAME, $rule);
        }
        $rules .= "            ]";
        $templateContent = $this->genValidationTemplate($namespace, $name, $rules, $rules, $messgaes, $messgaes);
        $class = $namespace . '\\' . $name;
        if(class_exists($class)) {
            throw new RuntimeException(sprintf('class %s exist', $class));
        }
        file_put_contents(app_path("/Http/Validations/Api/{$name}.php"), $templateContent);
        $this->info($name . ' created validation successfully.');
    }

    /**
     * 创建资源文件
     * @param $name
     */
    protected function resource($name)
    {
        $namespace = $this->getDefaultNamespace('Http\Resources');
        $resourceTemplate = $this->genResourceTemplate($namespace, $name);
        $class = $namespace . '\\' . $name;
        if(class_exists($class)) {
            throw new RuntimeException(sprintf('class %s exist', $class));
        }
        file_put_contents(app_path("/Http/Resources/{$name}Resource.php"), $resourceTemplate);
        $this->info($name . ' created resource successfully.');
    }

    /**
     * 创建控制器
     * @param $name
     */
    protected function controller($name)
    {
        $namespace = $this->getDefaultNamespace('Http\Controllers\Api');
        $controllerTemplate = $this->genControllerTemplate($namespace, $name, Str::camel($name),
            Str::pluralStudly(Str::camel($name)));
        $class = $namespace . '\\' . $name;
        if(class_exists($class)) {
            throw new RuntimeException(sprintf('class %s exist', $class));
        }
        file_put_contents(app_path("/Http/Controllers/Api/{$name}Controller.php"), $controllerTemplate);
        $this->info($name . ' created controller successfully.');
    }

    /**
     * 创建模型
     * @param $name
     */
    protected function model($name)
    {
        $namespace = $this->getDefaultNamespace('Models');
        $table = Str::snake(Str::pluralStudly(class_basename($this->argument('name'))));
        $columns = $this->getTableFields($table);
        $fields = "[";
        for($i = 0; $i < count($columns); $i++) {
            $column = $columns[$i];
            if(in_array($column, ["'id'", "'created_at'", "'updated_at'", "'status'"])) {
                continue;
            }
            $fields .= sprintf("%s,", $column);
        }
        $fields .= "]";
        $fields = str_replace(",]", "]", $fields);
        $modelTemplate = $this->genModelTemplate($namespace, $name, $fields);
        $class = $namespace . '\\' . $name;
        if(class_exists($class)) {
            throw new RuntimeException(sprintf('class %s exist', $class));
        }
        file_put_contents(app_path("/Models/{$name}.php"), $modelTemplate);
        $this->info($name . ' created model successfully.');
    }

    /**
     * Execute the console command.
     * @return mixed
     */
    public function handle()
    {
        $name = Str::ucfirst($this->argument('name'));
        $this->validation($name);
        $this->resource($name);
        $this->controller($name);
        $this->model($name);
    }
}

至此本篇文章完结。后续打开基于 laravelreact 开发一套全新的cms系统,到时候会将很多代码封装成 sdk

需要值得一提的是我生成的 verification 文件啥都没有,可以根据数据库的表字段类型生成验证规则,是不是节省了很多编码时间呢? 哈哈哈哈
verification文章参考
代码参考文章

本作品采用《CC 协议》,转载必须注明作者和本文链接
by JeffreyBool blog :point_right: link
JeffreyBool
《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
讨论数量: 1

特意回来评论
还不错
改改就能用

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!