基於 Laravel 命令列開發 API 程式碼生成器

JeffreyBool發表於2019-11-18

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文章參考
程式碼參考文章

by JeffreyBool blog :point_right: link

相關文章