Make Console Commands Great Again

This is an excerpt of our upcoming book Laravel Secrets.

Laravel offers this neat feature where you can 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. However, you probably have seen such a progress bar when using npm or yarn.

$ npm install
⸨█████████░░░░░░░░░⸩ ⠧ preinstall:secrets: info lifecycle @~preinstall: @

$ yarn
yarn install v1.17.3
info No lockfile found.
[1/4] 🔍  Resolving packages...
[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:

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

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

class ImportUsers extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var  string
     */
    protected $signature = 'secrets:import-users
    {file : The CSV file with the list of users}';

    /**
     * The console command description.
     *
     * @var  string
     */
    protected $description = 'Import users from a CSV file.';

    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        $lines = $this->parseCsvFile($this->argument('file'));

        $progressBar = $this->output->createProgressBar($lines->count());
        $progressBar->start();

        $lines->each(function ($line) use ($progressBar) {
            $this->convertLineToUser($line);

            $progressBar->advance();
        });

        $progressBar->finish();
    }

    // class specific methods
}

If we dive into the code that's behind our user importer, we can see a few important things here. First, we have a method called parseCsvFile which parses the CSV file to 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 extra methods which makes it more readable.

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

After that we can start looping over our data and perform our action. In this case convertLineToUser is converting the data of a line 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 higher up the steps on the progress bar and output the current state.

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

Override output

In our case we have four users. However, we only see one line of output in the console. This is because the output will be overwritten on each step. Whenever we advance a step the output will be overridden by default. So it basically redraws the output. This was we only see one output.

$ php artisan secrets:import-users users.csv

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

However, 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 first one 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 you want to do this you can simply turn it off by doing the following:

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

The output will then look like this:

$ php artisan secrets:import-users users.csv

0/4 [>                           ]   0%
1/4 [=======>                    ]  25%
2/4 [==============>             ]  50%
3/4 [=====================>      ]  75%
4/4 [============================] 100%

Custom messages

When going over data showing just percentages might not be useful enough. In some cases you want to show how much time is remaining for processing your data. This is only useful whenever you have to import a lot of data or whenever you have a slow processing bit of data. Think about inserting into a lot of tables or using API's when processing your data. Also just displaying additional data can be really useful.

By default the message that is being output from the console looks like this

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

We can override this message and provider our own format with our own 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.

ProgressBar::setFormatDefinition('custom', ' %current%/%max% [%bar%] %message%');

$progressBar = $this->output->createProgressBar($lines->count());
$progressBar->setFormat('custom');

$progressBar->setMessage('Starting...');
$progressBar->start();

$lines->each(function ($line) use ($progressBar) {
    $this->convertLineToUser($line);

    $message = sprintf(
        '%d seconds remaining',
        $this->calculateRemainingTime($progressBar)
    );

    $progressBar->setMessage($message);
    $progressBar->advance();
});

$progressBar->setMessage('Finished!');
$progressBar->finish();

With this custom message we get an output like this

0/4 [░░░░░░░░░░░░░░░░░░░░░░░░░░░░] Starting...
1/4 [▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░] 8 seconds remaining
2/4 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░] 5 seconds remaining
3/4 [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓░░░░░░░] 2 seconds remaining
4/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

We've already seen how the progress bar looks like. In most cases this is perfectly fine. By default the progress bar looks like this:

4/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:

$progressBar = $this->output->createProgressBar($lines->count());

$progressBar->setBarCharacter('=');
$progressBar->setProgressCharacter('>');
$progressBar->setEmptyBarCharacter(' ');

This will then result in the following output

0/2 [>                           ]   0%
1/2 [==============>             ]  50%
2/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 really easy for us. So if we run composer require spatie/emoji we can start using these emoijs in our output:

$progressBar->setBarCharacter('=');
$progressBar->setProgressCharacter('⏳');
$progressBar->setEmptyBarCharacter(' ');

This will then result in the following output

2/4 [==============⏳             ]  50%

Pretty neat huh?!

Why customize the progress bar?

We've been playing around with the console commands and the progress bar that comes by default in Laravel. The default bar is fine in most cases, so why would we optimize this? It mostly comes down to developer experience. If you're anything 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 enjoyable and even gives you more info while waiting!

Finally Convinced?

You can't do anything wrong. It's just a pre registration. We'll keep you updated on the progress. When the book will be published you get notified and you can decide to buy or not. No obligations.

    In order to comply with the GDPR, you consent to receive promotional emails about our products. We use ConvertKit for that. You can find their privacy policy here.

    Imprint and legal disclosure