#development #laravel #php #terminal

Laravel offers this neat feature to show a progress bar when running a command on the console. The progress bar in Laravel is based on the Symfony Progress Bar Component. Laravel itself is currently not using this feature in the framework. You probably have seen such a progress bar when using npm or yarn.

1$ npm install
2(█████████░░░░░░░░░) preinstall:secrets: info lifecycle @~preinstall: @
3$ yarn
4yarn install v1.17.3
5info No lockfile found.
6[1/4] Resolving packages...
7[2/4] Fetching packages... [#########################################################------] 771/923

In the next part, we will be building a simple CSV importer for users. This small tool will display the numbers of users we have imported and show us the current progress. Eventually, we will end up with output like this:

1$ php artisan secrets:import-users users.csv
2 4/4 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] 100%

Below you can find the code of the command-line tool.

 1class ImportUsers extends Command
 2{
 3    protected $signature = 'secrets:import-users
 4                            {file : A CSV with the list of users}';
 5
 6    protected $description = 'Import users from a CSV file.';
 7
 8        public function handle()
 9    {
10        $lines = $this->parseCsvFile($this->argument('file'));
11
12        $progressBar = $this->output->createProgressBar($lines->count());
13        $progressBar->start();
14
15        $lines->each(function ($line) use ($progressBar) {
16            $this->convertLineToUser($line);
17            $progressBar->advance();
18        });
19
20        $progressBar->finish();
21    }
22
23    // class specific methods
24}

If we dive into the code behind the user importer, we can see a few important things here. First, we have a parseCsvFile method, which transforms the CSV file into a collection. This way, we can easily loop over the results but also get the current count. We could have used a simple array here as well. However, the collection gives us different methods, which makes it more readable.

Next, we create a new progress bar in the output of our command. This means that we create a new ProgressBar instance and pass the number of items we expect to process. Then we need to start the progress bar itself. It will set the current time on the progress bar, and it will set the steps to 0 as well.

After that, we can start looping over your data and perform your actions. In this case, convertLineToUser is converting a line's data to a new user in the database. The final step in our loop is to advance the progress bar. Whenever we call $progressBar->advance(), we will increase the progress bar's steps and output the current state.

Finally, we call finish on the progress bar to make sure it shows that we are 100% there and close the progress bar.

Override Output

In our case, we have four users. However, you only see one line of output in the console. This means the output will be overwritten on each step. Whenever we advance a step, the output will be replaced, so it redraws the output on the same line.

1$ php artisan secrets:import-users users.csv
2 4/4 [============================] 100%

We can turn this off. This way, we will be drawing 5 times to the console. We have the start method first to show that we have zero progress. Then, whenever we complete the next item, we add 25% to the bar and display that. So it will only add something to the output whenever it's finished in the loop. If we want to do this, we can simply turn it off by doing the following:

1$progressBar = $this->output->createProgressBar($lines->count());
2$progressBar->setOverwrite(false);

The output will then look like this::

1$ php artisan secrets:import-users users.csv
2 0/4 [>
3 1/4 [=======>
4 2/4 [==============>
5 3/4 [=====================>
6 4/4 [============================] 100%

When overwrite is enabled you would only see one line in the above example. With overwrite disabled you get an output line for each item.

Custom Messages

In some cases, you want to show how much time is remaining for processing your data. This is useful whenever you have to import a lot of data, or a slow processing bit of data. Think about inserting into a lot of tables or using APIs when processing your data. Also, just displaying additional data can be useful.

By default, the message written to the console looks like this:

1' %current%/%max% [%bar%] %percent:3s%%'

The %current% variable displays the current step, or the current count of items we have already processed. The %max% variable means the maximum number of items we will be processing. You can override the max number by calling setMaxSteps manually on the progress bar. In our case, this number is set by using $lines->count() when creating the ProgressBar. Next up is the %bar% variable, which displays the actual loading bar. The final parameter indicates a percentage based on the max number of steps divided by the current step. Because :3s is appended to the %percent% variable, the result will always be shown as three characters. This makes sure the percentage aligns to the right.

We can override this message and provide our own format with our custom data. To do that, we will first need to set the custom message and assign it to the progress bar. After that, it's just business as usual, and we loop over the data and set a message per action.

 1ProgressBar::setFormatDefinition('custom', ' %current%/%max% [%bar%] %message%');
 2
 3$progressBar = $this->output->createProgressBar($lines->count());
 4$progressBar->setFormat('custom');
 5
 6$progressBar->setMessage('Starting...');
 7$progressBar->start();
 8
 9$lines->each(function ($line) use ($progressBar) {
10    $this->convertLineToUser($line);
11
12    $message = sprintf(
13        '%d seconds remaining',
14        $this->calculateRemainingTime($progressBar)
15    );
16
17    $progressBar->setMessage($message);
18    $progressBar->advance();
19});
20
21$progressBar->setMessage('Finished!');
22$progressBar->finish();

With this custom message, we get an output like this:

10/4 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░] Starting...
21/4 [▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░] 8 seconds remaining
32/4 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░] 5 seconds remaining
43/4 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░] 2 seconds remaining
54/4 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] Finished!

As you can see, there is already a progress bar that indicates how much percentage is completed. However, that doesn't say anything about the remaining time. The messages provide extra value here by showing an estimation of the remaining time together with the progress bar.

Custom Progress Bar

You have already seen what the progress bar looks like. In most cases this is perfectly fine. By default, the progress bar looks like this:

14/4 [============================] 100%

To enjoy looking at the progress bar when waiting, we can spice it up a bit. In this case, we don't have to change much to make this work. We only need to set the characters we want to see:

1$progressBar = $this->output->createProgressBar($lines->count());
2$progressBar->setBarCharacter('=');
3$progressBar->setProgressCharacter('>');
4$progressBar->setEmptyBarCharacter(' ');

This will then result in the following output:

10/2 [>                           ]   0%
21/2 [==============>             ]  50%
32/2 [============================] 100%

We can even take this a step further and use emojis in here. To do this, we need a Spatie package to make this easy for us. So if we run composer require spatie/emoji, we can start using these emojis in our output:

1use Spatie\Emoji\Emoji;
2
3$progressBar->setBarCharacter('=');
4$progressBar->setProgressCharacter(Emoji::hourglassNotDone());
5$progressBar->setEmptyBarCharacter(' ');

This will then result in the following output:

12/4 [==============] 50%

Pretty neat, right?!

withProgressBar

Since Laravel 8, there is a convenient method to generate a progress bar called withProgressBar. This method performs the same action as creating a progress bar yourself and advancing it manually. You now have one callback that you can use to perform your action on each item in the array. The method will make sure the progress bar advances to the next step and calls finish after the last one.

1public function handle()
2{
3    $lines = $this->parseCsvFile($this->argument('file'));
4
5    $this->withProgressBar($lines, function ($line) {
6        $this->convertLineToUser($line);
7    });
8}

The withProgressBar method is handy and fast but doesn't allow for any customizations in the output.

Why Customize the Progress Bar?

You have played around with the console commands, and the default progress bar in Laravel. The default bar is sufficient in most cases, so why would you customize this? It mostly comes down to developer experience. If you are like us, you will spend an insane amount of time on the command line. It's also nice to see some colors and better readable output than always the default output. Especially when you have a long-running process. A customized progress bar can make this even more enjoyable and gives you more info while waiting!