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
檔案的內容,只是所有的佔位符均被替換了。
同樣的事情還發生在controller
和validation
方法裡。因此,我將這兩個方法的內容貼上在這裡。
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);
}
}
至此本篇文章完結。後續開啟基於 laravel
和 react
開發一套全新的cms系統,到時候會將很多程式碼封裝成 sdk
需要值得一提的是我生成的 verification
檔案啥都沒有,可以根據資料庫的表欄位型別生成驗證規則,是不是節省了很多編碼時間呢? 哈哈哈哈
verification文章參考
程式碼參考文章