一度に大量のデータを更新する必要がある時などはArtisanコンソールコマンドでバッチを作成して実行します。その際に更新に時間が掛かって途中で何も出力が無いと処理が正常に進んでいるのか不安になってしまいます。そんな時はプログレスバーを表示すれば進捗が確認できて便利です。

演習コマンド

Laravelにデフォルトで用意されているusersテーブルを用いて演習用のコマンドを作成し、使い方を確認していきます。作成するのは登録されているemailからユーザー名部分を抜き出し、nameを上書きするコマンドです。コマンドの実行には更新対象となるusersのレコードが必要となるので先にダミーレコードを準備しましょう。DatabaseSeeder.phprun()を以下の様に編集してください。


namespace Database\Seeders;

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        \App\Models\User::factory(1000)->create();
    }
}

そして以下を実行するとusersに1000件のダミーレコードが作成されます。

$ php artisan db:seed

続いて演習用のUserNameUpdateコマンドを作成します。

$ php artisan make:command UserNameUpdate

UserNameUpdateコマンドは以下の様にしました。まだプログレスバーを実装する前の状態です。


namespace App\Console\Commands;

use App\Models\User;
use Illuminate\Support\Str;
use Illuminate\Console\Command;

class UserNameUpdate extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:user-name-update';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'nameをemailのユーザー名に更新する';

    /**
     * Execute the console command.
     */
    public function handle()
    {
        $this->info('ユーザ名の更新を開始します。');

        $users = User::all();

        $users->each(function ($user) use (&$updated) {
            $user->name = Str::before($user->email, '@');
            $user->save();
            $updated++;
        });

        $this->info('ユーザ名の更新が完了しました。(更新件数:' . $updated . ')');
    }
}

以下を実行するとemailの’@’より前の部分をnameに上書きします。

$ php artisan app:user-name-update
ユーザ名の更新を開始します。
ユーザ名の更新が完了しました。(更新件数:1000)

コマンド実行後にtinkerで以下を実行してみて下さい。ランダムにピックアップした20件ですが、意図通りに更新されているのが確認できると思います。

> User::take(20)->inRandomOrder()->pluck('email', 'name');
= Illuminate\Support\Collection {#7081
    all: [
      "marjorie.hagenes" => "marjorie.hagenes@example.com",
      "owen.sipes" => "owen.sipes@example.org",
      "leann10" => "leann10@example.net",
      "amy21" => "amy21@example.net",
      "brooks.leffler" => "brooks.leffler@example.org",
      "arianna89" => "arianna89@example.com",
      "ledner.teagan" => "ledner.teagan@example.net",
      "axel17" => "axel17@example.org",
      "ykoepp" => "ykoepp@example.com",
      "rita19" => "rita19@example.net",
      "genevieve98" => "genevieve98@example.com",
      "nathen.goldner" => "nathen.goldner@example.net",
      "xjones" => "xjones@example.org",
      "assunta57" => "assunta57@example.com",
      "swift.vena" => "swift.vena@example.net",
      "senger.maybell" => "senger.maybell@example.org",
      "jace.willms" => "jace.willms@example.com",
      "stephanie.berge" => "stephanie.berge@example.org",
      "acremin" => "acremin@example.org",
      "yaufderhar" => "yaufderhar@example.org",
    ],
  }

一度コマンドを実行してnameが更新されてしまったので、以下を実行してダミーデータを作り直しておきましょう。

$ php artisan migrate:fresh --seed

withProgressBar()

それでは先ほどのコマンドを修正してプログレスバーを表示するようにしてみましょう。プログレスバーを表示するにはwithProgressBar()を使用します。withProgressBar()は第一引数にループ処理可能な値(iterable)を、第二引数にループ実行する処理をクロージャで指定します。UserNameUpdateコマンドに実装した例が以下になります。

...
    public function handle()
    {
        $this->info('ユーザ名の更新を開始します。');

        $users = $this->withProgressBar(User::all(), function (User $user) {
            $user->name = Str::before($user->email, '@');
            $user->save();
            usleep(10000);
        });

        $this->info('');    // 改行用
        $this->info('ユーザ名の更新が完了しました。(更新件数:' . $users->count() . ')');
    }
...

コマンドを実行するとバーが表示され、User::all()の総数である1000件の処理が完了すると100%となるようにループ毎にゲージが溜まっていきます。

$ php artisan app:user-name-update
ユーザ名の更新を開始します。
1000/1000 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%
ユーザ名の更新が完了しました。(更新件数:1000)

今回ダミーデータを1000件用意しましたが、私の環境では処理が一瞬で終了してしまい、プログレスバーのゲージが蓄積していく様子が確認できなかったのでusleep()を挟みループ毎に0.01秒スリープするようにしました。また、プログレスバー表示後に改行コードが含まれておらず、続く「ユーザ名の更新が完了しました。・・・」の表示がプログレスバーと同じ行に表示されてしまう為、$this->info('')で改行を挟みました。

createProgressBar()

プログレスバーを表示するもう1つの方法としてcreateProgressBar()を使ってProgressBarインスタンスを生成する方法があります。こちらは前項のwithProgressBar()よりもバーを表示するタイミングやゲージを進めるタイミングを細かくコントロールできます。以下、UserNameUpdateコマンドに実装した例です。

...
    public function handle()
    {
        $this->info('ユーザ名の更新を開始します。');

        $users = User::all();

        $bar = $this->output->createProgressBar($users->count());
        $bar->start();

        foreach ($users as $user) {
            $user->name = Str::before($user->email, '@');
            $user->save();
            usleep(10000);
            $bar->advance();
        }

        $bar->finish();

        $this->info('');
        $this->info('ユーザ名の更新が完了しました。(更新件数:' . $users->count() . ')');
    }
...

今回のUserNameUpdateコマンドの様に取得したデータに対してループでシンプルな処理を実行するパターンでは前項で紹介したwithProgressBar()の方がコードが簡潔ですね。ループ内で実行する処理が複雑でクロージャに渡す変数が多くなってしまう様なケースではcreateProgressBar()を使用するのが良いかもしれません。因みにですが、withProgressBar()Laravel8から追加された関数の様です。Laravel7以前はcreateProgressBar()のみでした。

By hikaru