[Laravel] migrate:freshコマンドで複数のデータベースのテーブルを全てdropする

# ぶつかった問題

先日ローカル開発環境を整備している際にマイグレーションが通らないパターンがあることに気づきました。
具体的には開発用データベース全体を一度作り直すコマンドの実行時に既にテーブルが存在すると怒られる。

```
 SQLSTATE[42S01]: Base table or view already exists: 1050 Table 'users' already exists ... 
```

# migrate:freshコマンド

LaravelではデータベースをDROPし、マイグレーションファイルから作り直すコマンドとして`migrate:fresh`コマンドが用意されています。

https://laravel.com/docs/10.x/migrations#drop-all-tables-migrate

今回の開発用データベース全体を作り直すコマンドでは、この`migrate:fresh`コマンド使用していました。
**マイグレーション実行前にすべてのテーブルをDROPしているはずなのに何故・・・というのが今回躓いたポイントです。**

# migrate:freshコマンドで達成できないこと

素直に`migrate:fresh`コマンドの実装を確認することで原因は判明しました。

## 1. \Illuminate\Database\Console\Migrations\FreshCommand

`php artisan migrate:fresh`を実行した際に呼ばれる処理の実装は`\Illuminate\Database\Console\Migrations\FreshCommand`にあります。

中身を確認してみるとすべてのテーブルをDROPしていそうな箇所が存在します。
どうやら`db:wipe`コマンドを利用してDROPしているようです。

```php
/**
 * Execute the console command.
 *
 * @return int
 */
public function handle()
{
......(略)......

    $database = $this->input->getOption('database');

    $this->newLine();

    $this->components->task('Dropping all tables', fn () => $this->callSilent('db:wipe', array_filter([
        '--database' => $database,
        '--drop-views' => $this->option('drop-views'),
        '--drop-types' => $this->option('drop-types'),
        '--force' => true,
    ])) == 0);

......(略)......
}
```

## 2. \Illuminate\Database\Console\WipeCommand

`db:wipe`コマンドの実装も確認してみます。

`$database`という変数を渡しつつ`dropAllTables`というメソッドを呼び出していますね。
こちらを確認してみるとどうやら`$database`の文字列を使ってテーブルをDROPするコネクションを指定しているようです。

```php
/**
 * Execute the console command.
 *
 * @return int
 */
public function handle()
{
......(略)......

    $database = $this->input->getOption('database');


......(略)......

    $this->dropAllTables($database);


......(略)......
}

/**
 * Drop all of the database tables.
 *
 * @param  string  $database
 * @return void
 */
protected function dropAllTables($database)
{
    $this->laravel['db']->connection($database)
                ->getSchemaBuilder()
                ->dropAllTables();
}
```

では、この`$database`を指定しなかったらどうなるのか。

## 3. \Illuminate\Foundation\Application

 `\Illuminate\Database\Console\WipeCommand::$laravel`の実態である`\Illuminate\Foundation\Application`クラスには下記のようなマッピングが存在し、 `\Illuminate\Database\Console\WipeCommand::$laravel`のインデックスである`db`には`\Illuminate\Database\DatabaseManager`クラスが格納されているようです。

 ```php
    /**
     * Register the core class aliases in the container.
     *
     * @return void
     */
    public function registerCoreContainerAliases()
    {
        foreach ([
...略...
            'db' => [\Illuminate\Database\DatabaseManager::class, \Illuminate\Database\ConnectionResolverInterface::class],
...略...
        ] as $key => $aliases) {
            foreach ($aliases as $alias) {
                $this->alias($key, $alias);
            }
        }
    }
 ```

 ## 4. \Illuminate\Database\DatabaseManager

 このクラスをのぞいてみると、`WipeCommand`で呼び出されていた`connection()`メソッドを見つけることができました。
 実際に中身を確認してみると`parseConnectionName()`というメソッドの中で、データベース名がnullで来たときはデフォルトのコネクション名を取得していることがわかります。

```php
    /**
     * Get a database connection instance.
     *
     * @param  string|null  $name
     * @return \Illuminate\Database\Connection
     */
    public function connection($name = null)
    {
        [$database, $type] = $this->parseConnectionName($name);
...略...
        return $this->connections[$name];
    }

    /**
     * Parse the connection into an array of the name and read / write type.
     *
     * @param  string  $name
     * @return array
     */
    protected function parseConnectionName($name)
    {
        $name = $name ?: $this->getDefaultConnection();

        return Str::endsWith($name, ['::read', '::write'])
                            ? explode('::', $name, 2) : [$name, null];
    }
```

# 原因まとめ

つまり、`migrate:fresh`コマンドはオプションをつけずに実行した場合、デフォルトのコネクションのデータベースのみをDROPするようです。

【誤】
`migrate:fresh`コマンドを発行すると全てのデータベースのテーブルがDROPされる

【正】
`migrate:fresh`コマンドを発行するとデフォルトコネクションのデータベースのテーブルがDROPされる

# 解決策

結論から言うと、`migrate:fresh`コマンドの実装部分のうち、実際にDROPを発行している部分をループさせて、全てのコネクションに対してDROPを発行してしまえば良さそうです。

### 1. Laravelの提供する`migrate:fresh`コマンドの実装クラスを継承したカスタムコマンドクラスを作成する

```php
<?php

namespace App\Console\Commands\Migrations;

// Laravel提供のmigrate:freshコマンド
use Illuminate\Database\Console\Migrations\FreshCommand as BaseCommand;

// 継承する
class FreshCommand extends BaseCommand
```

### 2. 親クラスから持ってきた`handle()`メソッドの当該部分をループする

```php
/**
 * Execute the console command.
 *
 * @return int
 */
public function handle()
{
......(略)......

    $database = $this->input->getOption('database');

    $this->newLine();

    foreach (config('database.connections') as $key => $values) {
        $this->components->task('Dropping all tables in connection : ' . $key, fn () => $this->callSilent('db:wipe', array_filter([
            '--database' => $key,
            '--drop-views' => $this->option('drop-views'),
            '--drop-types' => $this->option('drop-types'),
            '--force' => true,
        ])) == 0);
    }

......(略)......
}
```

### 結果
無事すべてのコネクションのテーブルをDROPすることができました！

```sh
$ php artisan migrate:fresh

# これは元々DROPされていた
  Dropping all tables in connection : default .......................................................................................... 234ms DONE
# デフォルトでない他のスキーマもDROPされている
  Dropping all tables in connection : schema2 ............................................................................................. 76ms DONE
  Dropping all tables in connection : schema3 ............................................................................................ 73ms DONE
```

# 不満な点
今回はこのように対応しましたが、不満な点もあります。

1. 親クラスのhandle()メソッドにDROP以外の処理も含まれているので、それらも一緒にhandle()メソッドとしてコピーしてくる必要があり、万が一それらを上書きしてしまった際に望ましくない挙動をする可能性があります。
2. Laravelが内部的に`migrate:fresh`コマンドを利用しており、かつデフォルトコネクションのみを操作する目的でこれが使われている場合。

ご使用の際はご自身のユースケースを吟味したうえでご使用ください。

# 他の達成方法
`migrate:fresh`コマンドには`--database`オプションが存在するため、コマンド発行側で`php artisan migrate:fresh --database xxxxx`とコネクションを指定しつつ複数回発行する方法でも達成可能です。

今回この方法を取らなかったのは、現在`migrate:fresh`コマンドを使用している箇所や今後追加される箇所において、いずれも全てのコネクションに対してDROPを行いたかったこと、およびデフォルトのコネクションのみをDROPの対象とするユースケースを想定していないためです。

# まとめ
長くなりましたが、困ったときは実装を確認するのが遠回りなようで一番の近道。
聞いたりググったりすることに加えて、自身が利用しているフレームワークの中身を確認する癖をつけたいものです。