新米エンジニアがLaravelを使って、[業務効率化]自動migrationファイル作成ツールをつくってみた話

##作成するにあたっての動機
今まで、Laravelを使ったプロジェクトにかかわらせていただくことがあり、DB設計を終えた後に、sqlファイルやddlファイルを自動で作ってくれるものはあるのに、それを元にマイグレーションファイルを自動で作ってくれるものがないので、大変だぁと思い、取り組んでみました。

####新米のため、こうしたほうがいいなどの意見をいただけるとありがたいです。

##最終目的
すでに作成されているテーブルからマイグレーションファイルを自動で作れるようにします。

##準備

###プロジェクト作成

```:コマンド
composer create-project --prefer-dist laravel/laravel autoCreateMigrationFileSystem
```

###テーブル作成
今回は、laravelのmigrationファイルを作成・実行し、テーブルを作り、そこから同じファイルが作成されるかをテストします。

####実際は複数テーブルがあるため、2つのテーブルを用意する。
####・applesテーブル
####・lemonsテーブル

```:コマンド
/var/www# php artisan make:migration create_apples_table
Created Migration: 2019_07_15_133234_create_apples_table
/var/www# php artisan make:migration create_lemons_table
Created Migration: 2019_07_15_133511_create_lemons_table
```

僕が今回参加するプロジェクトで使うカラムタイプが補えればいいので、それだけ洗い出す(新たに必要なカラムタイプは後から加えていく)

###今回使うカラムタイプ
- bigint(20)のauto_increment
- bigint(20)
- bigint(20) nullable
- bigint(20) default(0)
- varchar(255)
- timestamp  (created_atとupdated_atで使用する)
- int(11)
- tinyint(4)
- text
- date
- datetime

これらを二つのマイグレーションファイルに記述します。

```:CreateApplesTable.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateApplesTable extends Migration
{
    public function up()
    {
        Schema::create('apples', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('a');
            $table->bigInteger('b')->nullable();
            $table->bigInteger('c')->default(0);
            $table->string('d', 255);
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('apples');
    }
}
```

```:CreateLemonsTable.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateLemonsTable extends Migration
{
    public function up()
    {
        Schema::create('lemons', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->integer('e');
            $table->tinyInteger('f');
            $table->text('g');
            $table->date('h');
            $table->dateTime('i');
            $table->timestamps();
            $table->softDeletes();
        });
    }

    public function down()
    {
        Schema::dropIfExists('lemons');
    }
}

```

```:コマンド
/var/www# php artisan migrate
Migration table created successfully.
Migrating: 2019_07_20_183234_create_apples_table
Migrated:  2019_07_20_183234_create_apples_table
Migrating: 2019_07_20_183511_create_lemons_table
Migrated:  2019_07_20_183511_create_lemons_table
```

これで準備が完了しました。

#実装

今回作るファイル(4ファイル)
- app/Repositories/Repository.php
- views/create_table_stub.blade.php
- config/column_type.php
- app/Console/Commands/MakeMigrationFile.php

##app/Repositories/Repository.php(カラム情報を取得するよう)

```:Repository.php
<?php
namespace App\Repositories;

use Illuminate\Support\Facades\DB;


class Repository
{
	//それぞれのテーブルごとのカラム情報を取得する
    public static function getTableColumnsList()
    {
        $tableColumnsList = [];

        $tableNameList = self::getTableNameList();

        foreach ($tableNameList as $tableName){
            $tableColumnsList[$tableName] = DB::select('show columns from '. $tableName);
        }

        return $tableColumnsList;
    }

	//テーブルの名前を一覧取得する(migrationsテーブルは必要ないので、取得したものからはずす)
    private static function getTableNameList()
    {
        return collect(DB::select('show tables'))
            ->keyBy('Tables_in_'. env('DB_DATABASE'))
            ->forget('migrations')
            ->keys();
    }
}
```

####getTableNameListの実行結果
<img width="260" alt="Screenshot_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325600/d4604d90-8f93-51e3-f02f-d0f947e45365.png">


####getTableColumnsListの実行結果(applesテーブルの中身の写真だけ(長いので・・・・)
<img width="270" alt="Screenshot_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325600/9847b36c-1653-9b18-2877-385947788581.png">
<img width="174" alt="Screenshot_3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325600/a497e640-231d-d562-e598-c8d317df81c8.png">


##ファイルを作る用のフォーマットを作成する(views/create_table_stub.blade.php)
create_table_stub.blade.phpをview配下に作成する。

```:create_table_stub.blade.php
@php $phpTag = '<?php' @endphp
{!! $phpTag !!}
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class {{$className}} extends Migration
{
    public function up()
    {
        Schema::create('{{$tableName}}', function (Blueprint $table) {
@foreach($defineColumnList as $defineColumn)
@if(!isset($defineColumn))@continue @endif
            $table->{!! $defineColumn !!};
@endforeach
        });
    }

    public function down()
    {
        Schema::dropIfExists('{{$tableName}}');
    }
}

```

＠foreachを一番左にしているのは、ファイルを作成したときにインデントを整えるためです。
{{}}ではなく、{!! !!}を使っているところは、エスケープされると都合がよくないため、こうしています。


##どのカラムタイプならどのマイグレーション用のメソッドを使うか決めておく(config/column_type.php)

今回使用する必要があるのだけ、定義しました。(後々必要なものは足して、完成度を上げていく)

```:column_type.php
<?php
return [

    'auto_increment' => [
        'bigint(20) unsigned' => 'bigIncrements'
    ],

    //デフォルト
    'default' => [
        'bigint(20)' => 'bigInteger',
        'int(11)'    => 'integer',
        'tinyint(4)' => 'tinyInteger',
        'date'       => 'date',
        'datetime'   => 'dateTime',
        'text'       => 'text',
    ],

    //サイズ考慮する必要のあるタイプ
    'length' => [
        'varchar' => 'string'
    ]
];

```

auto_incrementは特殊パターンとして、またサイズを考量すべきものは、length配列の中に入れています。
今後、min maxや from toを使用するメソッドを使いたい場合はその時に強化します。
<b>created_atとupdated_atのtimestampsメソッドは後々処理で追加します。softDeletesも同様</b>
[ちなみに、公式に使用できるカラムタイプが書いてあるので、そこを参考にしました](https://readouble.com/laravel/5.8/ja/migrations.html)

```:コマンド
/var/www# php artisan config:clear
```

コマンドを打ち、設定を反映させました。

##コマンドで自動作成が動くようにする。(app/Console/Commands/MakeMigrationFile.php)

コマンドを打ち、ファイルを作成します。

```:コマンド
/var/www# php artisan make:command MakeMigrationFile
Console command created successfully.
```

```:MakeMigrationFile.php
<?php

namespace App\Console\Commands;

use App\Repositories\Repository;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;

class MakeMigrationFile extends Command
{
    protected $signature   = 'make:migration-file';
    protected $description = 'マイグレーション用ファイルを自動作成';

    const WORD_AUTO_INCREMENT = 'auto_increment';
    const WORD_DEFAULT        = 'default';
    const WORD_LENGTH         = 'length';
    const WORD_CREATED_AT     = 'created_at';
    const WORD_UPDATED_AT     = 'updated_at';
    const WORD_DELETED_AT     = 'deleted_at';

    const METHOD_TIMESTAMPS   = 'timestamps()';
    const METHOD_SOFTDELETES  = 'softDeletes()';
    const METHOD_NULLABLE     = 'nullable()';
    const METHOD_DEFAULT      = 'default()';

    private $configColumnType = [];

    public function __construct()
    {
        $this->configColumnType = config('column_type');

        parent::__construct();
    }

    public function handle()
    {
        $tableColumnsList = Repository::getTableColumnsList();
        foreach ($tableColumnsList as $tableName => $columnList) {

            $tempDefineColumnList = [];
            foreach ($columnList as $index => $column) {

                $tempDefineColumnList[$index] = $this->addAutoIncrementColumnDefine($column);
                if (isset($tempDefineColumnList[$index]))continue;

                $tempDefineColumnList[$index] = $this->addDefaultColumnDefine($column);
                if (isset($tempDefineColumnList[$index]))continue;

                $tempColumnType = preg_replace('/(\(|\)|[0-9])/', '', $column->Type);
                $tempDefineColumnList[$index] = $this->addLengthColumnDefine($column, $tempColumnType);
                if (isset($tempDefineColumnList[$index]))continue;

                $tempDefineColumnList[$index] = $this->addTimestamps($column, $index, $columnList);
                if (isset($tempDefineColumnList[$index]))continue;

                $tempDefineColumnList[$index] = $this->addSoftDeletes($column);
                if (isset($tempDefineColumnList[$index]))continue;
            }

            $tempClassName  = 'Create'. Str::studly($tableName). 'Table';

            $tempContent = view('create_table_stub', [
                'className'        => $tempClassName,
                'tableName'        => $tableName,
                'defineColumnList' => $tempDefineColumnList
            ])->render();

            $fileNamePrefix = Carbon::now()->format('Y_m_d_His');
            $tempFilePath   = database_path("migrations/{$fileNamePrefix}_create_{$tableName}_table.php");

            File::put($tempFilePath, $tempContent);
        }
    }

    private function addAutoIncrementColumnDefine($column)
    {
        $columnDefine = null;
        if ($column->Extra !== self::WORD_AUTO_INCREMENT)return $columnDefine;

        $columnDefine = "{$this->configColumnType[self::WORD_AUTO_INCREMENT][$column->Type]}('{$column->Field}')";

        return $columnDefine;
    }

    private function addDefaultColumnDefine($column)
    {
        $columnDefine = null;
        if (!isset($this->configColumnType[self::WORD_DEFAULT][$column->Type]))return $columnDefine;

        $columnDefine = "{$this->configColumnType[self::WORD_DEFAULT][$column->Type]}('{$column->Field}')";
        $columnDefine .= $this->addNullableAttribute($column);
        $columnDefine .= $this->addDefaultValue($column);

        return $columnDefine;
    }

    private function addLengthColumnDefine($column, $columnType)
    {
        $columnDefine = null;
        if (!isset($this->configColumnType[self::WORD_LENGTH][$columnType]))return $columnDefine;

        $length = preg_replace('/[^0-9]/', '', $column->Type);
        $columnDefine = "{$this->configColumnType[self::WORD_LENGTH][$columnType]}('{$column->Field}', {$length})";
        $columnDefine .= $this->addNullableAttribute($column);
        $columnDefine .= $this->addDefaultValue($column);

        return $columnDefine;
    }

    private function addTimestamps($column, $index, $columnList)
    {
        $columnDefine = null;
        if ($column->Field !== self::WORD_CREATED_AT
            || !isset($columnList[$index + 1]) || $columnList[$index + 1]->Field !== self::WORD_UPDATED_AT) {
            return $columnDefine;
        }

        $columnDefine = self::METHOD_TIMESTAMPS;
        return $columnDefine;
    }

    private function addSoftDeletes($column)
    {
        $columnDefine = null;
        if ($column->Field !== self::WORD_DELETED_AT)return $columnDefine;

        $columnDefine = self::METHOD_SOFTDELETES;
        return $columnDefine;
    }

    private function addNullableAttribute($column)
    {
        $columnAttribute = null;
        if ($column->Null === 'NO')return $columnAttribute;

        $columnAttribute = '->'. self::METHOD_NULLABLE;
        return $columnAttribute;
    }

    private function addDefaultValue($column)
    {
        $columnDefault = null;
        if (strlen($column->Default) === 0)return $columnDefault;

        $columnDefault = '->'. preg_replace('/(\(\))/', "({$column->Default})", self::METHOD_DEFAULT);
        return $columnDefault;
    }
}
```

とりあえず、要件は満たしたのでこれで、実行する。(足りない処理はまた今度修正)

```:コマンド
php artisan make:migration-file
```

中身が同じマイグレーションファイルができました。いったん完了。

##問題点

- lengthのほうのnullableとdefaultが考慮されていないため、その処理を加える。(その場合stringはデフォルト値をクォテーションで囲む)
- 命名ももっといいのがある
- コマンド書くファイルが肥大化するのでファイル分けるなどなど

##まとめ
意外と要件を満たすだけならさくっとできました。よかったです。
新米のため、こうしたほうがいいなどの意見をいただけるとありがたいです。