Processes and Artisan commands in Laravel

Read about how to create and test commands to interact with your Laravel application and server. Discover new tips and tricks for writing your own Artisan commands, and how to use Laravel 10's Process facade.

The command-line interface (CLI) can be a powerful tool for developers. You can use it as part of your development workflow to add new features to your application and to perform tasks in a production environment. Laravel allows you to create "Artisan commands" to add bespoke functionality to your application. It also provides a Process facade that you can use to run OS (operating system) processes from your Laravel application to perform tasks such as running custom shell scripts.

In this article, we'll explore what Artisan commands are and provide tips and tricks for creating and testing them. We'll also look at how to run operating system (OS) processes from your Laravel application and test they are called correctly.

What are Artisan commands?

Artisan commands can be run from the CLI to perform many tasks on a Laravel app. They can allow a more streamlined development process and be used to perform tasks in a production environment.

As a Laravel developer, you'll have likely already used some built-in Artisan commands, such as php artisan make:model, php artisan migrate, and php artisan down.

For example, some Artisan commands can be used as part of the development process, such as php artisan make:model and php artisan make:controller. Typically, these wouldn't be run in a production environment and are used purely to speed up the development process by creating boilerplate files.

Some Artisan commands can be used to perform tasks in a production environment, such as php artisan migrate and php artisan down. These are used to perform tasks, such as running database migrations and taking your application offline while you perform maintenance or roll out an update.

Thus, Artisan commands can be used to perform a variety of tasks and can be used in both development and production environments.

Creating your own Artisan commands

Now that we have a better understanding of what Artisan commands are, let's look at how we can create our own.

Getting input from users

To give some examples of what we can do with Artisan commands, let's take a look at a common use-case for them that you may across in your own projects: creating a new super admin in the database. In this example, we need the following pieces of information to create a new super admin:

  • Name
  • Email address
  • Password

Let's create the command to do this. We'll call the command CreateSuperAdmin and create it by running the following command:

php artisan make:command CreateSuperAdmin

This command will create a new app/Console/Commands/CreateSuperAdmin.php file. We'll assume that we have access to a createSuperAdmin method in a UserService class. For the purposes of this article, we don't need to know what it does or how it works since we're focused on how the commands work.

The command class created by the make:command command will look something like this:

namespace App\Console\Commands;

use Illuminate\Console\Command;

class CreateSuperAdmin extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:create-super-admin';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        //
    }
}

Now we want to add our arguments to the command so that we can accept the name, email, and password for each new user. We can do this by updating the signature property of the command class. The signature property is used to define the name of the command, the arguments, and the options that the command accepts.

The signature property should look something like this:

protected $signature = 'app:create-super-admin {--email=} {--password=} {--name=}';

We may also want to add an option to the command to allow the user to specify whether an email should be sent to confirm the account. By default, we'll assume the user shouldn't be sent the email. To add this option to the code, we can update the signature property to look like this:

protected $signature = 'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}';

It's important to also update the command's description property to describe what it does. This will be displayed when the user runs the php artisan list or php artisan help commands.

Now that we have our options configured to accept input, we can pass these options to our createSuperAdmin method. Let's take a look at what our command class will look like now:

namespace App\Console\Commands;

use App\Services\UserService;
use Illuminate\Console\Command;

class CreateSuperAdmin extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Store a new super admin in the database';

    /**
     * Execute the console command.
     */
    public function handle(UserService $userService): int
    {
        $userService->createSuperAdmin(
            email: $this->option('email'),
            password: $this->option('password'),
            name: $this->option('name'),
            sendEmail: $this->option('send-email'),
        );

        $this->components->info('Super admin created successfully!');

        return self::SUCCESS;
    }
}

Our command should now be ready to run. We can run it using the following command:

php artisan app:create-super-admin --email="hello@example.com" --name="John Doe" --password="password" --send-email

If we wanted to take this a step further, we may also want to add questions to the command so that the user can be prompted to enter the information if they don't provide it as an argument or option. This can provide a friendly user experience for developers, especially if they are new to using the command or if it has several arguments and options.

If we wanted to update our command to use questions, our command may now look something like this:

namespace App\Console\Commands;

use App\Services\UserService;
use Illuminate\Console\Command;

class CreateSuperAdmin extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Store a new super admin in the database';

    /**
     * Execute the console command.
     */
    public function handle(UserService $userService): int
    {
        $userService->createSuperAdmin(
            email: $this->getInput('email'),
            password: $this->getInput('password'),
            name: $this->getInput('name'),
            sendEmail: $this->getInput('send-email'),
        );

        $this->components->info('Super admin created successfully!');

        return self::SUCCESS;
    }

    public function getInput(string $inputKey): string
    {
        return match($inputKey) {
            'email' => $this->option('email') ?? $this->ask('Email'),
            'password' => $this->option('password') ?? $this->secret('Password'),
            'name' => $this->option('name') ?? $this->ask('Name'),
            'send-email' => $this->option('send-email') === true
                ? $this->option('send-email')
                : $this->confirm('Send email?'),
            default => null,
        };
    }
}

As you can see, we've added a new getInput method to the command class. Within this method, we're checking whether an argument has been passed to the command. If it hasn't, we prompt the user for input. You may have also noticed that we've used the secret method for getting the new password. This method is used so that we can hide the password from the terminal output. If we didn't use the secret method, the password would be displayed in the terminal output. Similarly, we've also used the confirm method for determining whether to send an email to the new user. This will prompt the user with a yes or no question, so it's a good way to get a Boolean value from the user rather than using the ask method.

Running the command with only the email argument would result in the following output:

❯ php artisan app:create-super-admin --email="hello@example.com"

Password:
>

Name:
> Joe Smith

Send email? (yes/no) [no]:
> yes

Anticipating input

If you're building a command for the user and providing multiple options to choose from a known data set (e.g., rows in the database), you may sometimes want to anticipate the user's input. By doing this, it can suggest autocompleting options for the user to choose from.

For example, let's imagine that you have a command that can be used to create a new user and allows you to specify the user's role. You may want to anticipate the possible roles from which the user may choose. You can do this by using the anticipate method:

$roles = [
    'super-admin',
    'admin',
    'manager',
    'user',
];

$role = $this->anticipate('What is the role of the new user?', $roles);

When the user is prompted to choose the role, the command will try to autocomplete the field. For instance, if the user started typing sup, the command would suggest er-admin as the autocomplete.

Running the command would result in the following output:

What is the role of the new user?:
> super-admin

It's important to remember that the anticipate method is only providing a hint to the user, so they can still enter any value they'd like. Therefore, all input from the method must still be validated to ensure it can be used.

Multiple arguments and choice inputs

There may be times when you're building an Artisan command and want to enable users to enter multiple options from a list. If you've used Laravel Sail, you'll have already used this feature when you run the php artisan sail:install command and are asked to choose the various services you want to install.

You can allow multiple inputs to be passed as arguments to the command. For example, if we wanted to create a command that can be used to install some services for our local development environment, we could allow the user to pass multiple services as arguments to the command:

protected $signature = 'app:install-services {services?*}';

We could now call this command like so to install the mysql and redis services:

php artisan app:install-services mysql redis

Within our Artisan command, $this->argument('services') would return an array containing two items: mysql and redis.

We could also add this functionality to our command to display the options to the user if they don't provide any arguments by using the choice method:

$installableServices = [
    'mysql',
    'redis',
    'mailpit',
];

$services = $this->choice(
    question: 'Which services do you want to install?',
    choices: $installableServices,
    multiple: true,
);

Running this command without any arguments would now display the following output:

Which services do you want to install?:
 [0] mysql
 [1] redis
 [2] mailpit
> mysql,redis

Using the choice method, we can allow the user to select multiple options from a list. The method provides auto-completion for the user, similar to the anticipate method. It also comes in handy because it will only allow the user to select options from the list. If the user tries to enter an option that isn't on the list, they'll be prompted to try again. Hence, it can act as a form of validation for your users' input.

Input validation

Similar to how you would validate the input for an HTTP request, you may also want to validate the input of your Artisan commands. By doing this, you can ensure that the input is correct and can be passed to other parts of your application's code.

Let's take a look at a possible way to validate the input. We'll start by reviewing the code, and then I'll break it down afterwards.

To validate your input, you could do something like so:

namespace App\Console\Commands;

use App\Services\UserService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\MessageBag;
use InvalidArgumentException;

class CreateSuperAdmin extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:create-super-admin {--email=} {--password=} {--name=} {--send-email}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Store a new super admin in the database';

    private bool $inputIsValid = true;

    /**
     * Execute the console command.
     */
    public function handle(UserService $userService): int
    {
        $input = $this->validateInput();

        if (!$this->inputIsValid) {
            return self::FAILURE;
        }

        $userService->createSuperAdmin(
            email: $input['email'],
            password: $input['password'],
            name: $input['name'],
            sendEmail: $input['send-email'],
        );

        $this->components->info('Super admin created successfully!');

        return self::SUCCESS;
    }

    /**
     * Validate and return all the input from the command. If any of the input 
     * was invalid, an InvalidArgumentException will be thrown. We catch this
     * and report it so it's still logged or sent to a bug-tracking system.
     * But we don't display it to the console. Only the validation error
     * messages will be displayed in the console.
     *
     * @return array
     */
    private function validateInput(): array
    {
        $input = [];

        try {
            foreach (array_keys($this->rules()) as $inputKey) {
                $input[$inputKey] = $this->validated($inputKey);
            }
        } catch (InvalidArgumentException $e) {
            $this->inputIsValid = false;

            report($e);
        }

        return $input;
    }

    /**
     * Validate the input and then return it. If the input is invalid, we will
     * display the validation messages and then throw an exception.
     *
     * @param string $inputKey
     * @return string
     */
    private function validated(string $inputKey): string
    {
        $input = $this->getInput($inputKey);

        $validator = Validator::make(
            data: [$inputKey => $input],
            rules: [$inputKey => $this->rules()[$inputKey]]
        );

        if ($validator->passes()) {
            return $input;
        }

        $this->handleInvalidData($validator->errors());
    }

    /**
     * Loop through each of the error messages and output them to the console.
     * Then throw an exception so we can prevent the rest of the command
     * from running. We will catch this in the "validateInput" method.
     *
     * @param MessageBag $errors
     * @return void
     */
    private function handleInvalidData(MessageBag $errors): void
    {
        foreach ($errors->all() as $error) {
            $this->components->error($error);
        }

        throw new InvalidArgumentException();
    }

    /**
     * Define the rules used to validate the input.
     *
     * @return array<string,string>
     */
    private function rules(): array
    {
        return [
            'email' => 'required|email',
            'password' => 'required|min:8',
            'name' => 'required',
            'send-email' => 'boolean',
        ];
    }

    /**
     * Attempt to get the input from the command options. If the input wasn't passed
     * to the command, ask the user for the input.
     *
     * @param string $inputKey
     * @return string|null
     */
    private function getInput(string $inputKey): ?string
    {
        return match($inputKey) {
            'email' => $this->option('email') ?? $this->ask('Email'),
            'password' => $this->option('password') ?? $this->secret('Password'),
            'name' => $this->option('name') ?? $this->ask('Name'),
            'send-email' => $this->option('send-email') === true
                ? $this->option('send-email')
                : $this->confirm('Send email?'),
            default => null,
        };
    }
}

Let's break down the above code.

First, we're calling a validateInput method before we try to do anything with the input. This method loops through every input key that we've specified in our rules method and calls the validated method. The array returned from the validateInput method will only contain validated data.

If any of the input is invalid, the necessary error messages will be displayed on the page. You may have noticed that we're also catching any InvalidArgumentException exceptions thrown. This is done so that we can still log or send the exception to a bug-tracking system, but without displaying the exception message in the console. Thus, we can keep the console output neat by only showing the validation error messages.

Running the above code and providing an invalid email address will result in the following output:

❯ php artisan app:create-super-admin                            

Email:
> INVALID

ERROR  The email field must be a valid email address.

Hiding commands from the list

Depending on the type of command you're building, you may want to hide it from the php artisan list command that displays all your app's available commands. This is useful if you're building a command that is only intended to run once, such as an installation command for a package.

To hide a command from the list, you can use the setHidden property to set the value the command's hidden property to true. For example, if you're building an installation command as part of a package that publishes some assets, you may want to check whether those assets already exist in the filesystem. If they do, you can probably assume that the command has already been run once and doesn't need to be displayed in the list command output.

Let's take a look at how we can do this. We'll imagine that our package publishes a my-new-package.php config file at the time of installation. If this file exists, we'll hide the command from the list. Our command's code may look something like this:

namespace App\Console\Commands;

use Illuminate\Console\Command;

class PackageInstall extends Command
{
    protected $signature = 'app:package-install';

    protected $description = 'Install the package and publish the assets';

    public function __construct()
    {
        parent::__construct();

        if (file_exists(config_path('my-new-package.php'))) {
            $this->setHidden();
        }
    }

    public function handle()
    {
        // Run the command here as usual...
    }
}

It's worth noting that hiding a command doesn't stop the command from being able to be run. If someone knows the name of the command, they can still run it.

Command help

When building your commands, it's crucial to use obvious names for your arguments and options. This will make it easier for other developers to understand what the command does and how to use it.

However, there may be times when you want to provide additional help information so they can be displayed in the console by running the php artisan help command.

For instance, if we wanted to take our CreateSuperAdmin example from earlier in this guide, we could update the signature to the following:

protected $signature = 'app:create-super-admin
                        {--email= : The email address of the new user}
                        {--password= : The password of the new user}
                        {--name= : The name of the new user}
                        {--send-email : Send a welcome email after creating the user}';

Now if we were to run php artisan help app:create-super-admin, we would see the following output:

❯ php artisan help app:create-super-admin
Description:
  Store a new super admin in the database

Usage:
  app:create-super-admin [options]

Options:
      --email[=EMAIL]        The email address of the new user
      --password[=PASSWORD]  The password of the new user
      --name[=NAME]          The name of the new user
      --send-email           Send a welcome email after creating the user
  -h, --help                 Display help for the given command. When no command is given display help for the list command
  -q, --quiet                Do not output any message
  -V, --version              Display this application version
      --ansi|--no-ansi       Force (or disable --no-ansi) ANSI output
  -n, --no-interaction       Do not ask any interactive question
      --env[=ENV]            The environment the command should run under
  -v|vv|vvv, --verbose       Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Automate tasks using the scheduler

Artisan commands also provide a way to automate tasks in your application.

For example, let's say that on the first day of every month, you want to build a PDF report for your users' activity from the previous month and email it to them. To automate this process, you could create a custom Artisan command and add it to your app's "scheduler". Assuming you had a scheduler set up on your server, this command would run automatically on the first day of each month and send the report to your users.

For the purposes of this article, we won't be covering how to set up a scheduler on your server. However, if you're interested in learning more about how to do this, you can check out the Laravel documentation. In its most basic terms, though, the scheduler is a cron job that runs on your server and is called once a minute. The cron job runs the following command:

php /path/to/artisan schedule:run >> /dev/null 2>&1

It's essentially just calling the php artisan schedule:run command for your project, which allows Laravel to handle whether the scheduled commands are ready to be run. The >> /dev/null part of the command is redirecting the output of the command to /dev/null which discards it, so it's not displayed. The 2>&1 part of the command is redirecting the error output of the command to the standard output. This is used so that any errors occurring when the command is run are also discarded.

Now, let's take a look at how to add a command to the scheduler in Laravel.

Let's imagine we want to implement our example from above and email our users on the first day of each month. To do this, we'll need to create a SendMonthlyReport Artisan command:

namespace App\Console\Commands;

use Illuminate\Console\Command;

class SendMonthlyReport extends Command
{
    protected $signature = 'app:send-monthly-report';

    protected $description = 'Send monthly report to each user';

    public function handle(): void
    {
        // Send the monthly reports here...
    }
}

After creating your command, we can then add it to your app's scheduler. To do this, we'll need to register it in the schedule method of the app/Console/Kernel.php file. Your Kernel class may look something like this:

namespace App\Console;

use App\Console\Commands\SendMonthlyReport;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;

class Kernel extends ConsoleKernel
{
    /**
     * Define the application's command schedule.
     */
    protected function schedule(Schedule $schedule): void
    {
        $schedule->command('app:send-monthly-report')->monthly();
    }

    // ...

}

If your scheduler is set up correctly on your server, this command should now automatically run at the beginning of each month.

A top tip when building automated commands like this is to add the functionality to pass arguments and options to the command. Sometimes, the command may fail when it's run automatically. For example, a user might not receive their monthly report, so by adding the options and arguments, you could manually rerun the command just for one user rather than everyone in the system. Although you may feel you don't need this functionality, it is something that has always come in handy with projects I've worked on.

Testing your Artisan commands

Like any other piece of code you write, it's important you test your Artisan commands. This is particularly true if you're going to be using them in a production environment or to automate tasks via the scheduler.

Asserting output from commands

Typically, the easiest parts of your command to test are that they were run successfully and that they output the expected text.

Let's imagine that we have the following example command we want to test:

namespace App\Console\Commands;

use App\Services\UserService;
use Illuminate\Console\Command;

class CreateUser extends Command
{
    protected $signature = 'app:create-user {email} {--role=}';

    protected $description = 'Create a new user';

    public function handle(UserService $userService): int
    {
        $userService->createUser(
            email: $this->argument('email'),
            role: $this->option('role'),
        );

        $this->info('User created successfully!');

        return self::SUCCESS;
    }
}

If we wanted to test that it was run successfully and that it outputs the expected text, we could do the following:

namespace Tests\Feature\Console\Commands;

use Tests\TestCase;

class CreateUserTest extends TestCase
{
    /** @test */
    public function user_can_be_created(): void
    {
        $this->artisan('app:create-user', [
            'email' => 'hello@example.com',
            '--role' => 'super-admin'
        ])
            ->expectsOutput('User created successfully!')
            ->assertSuccessful();

        // Run extra assertions to check the user was created with the correct role...
    }
}

Asserting questions are asked

If your application asks the user a question, you will need to specify the answer to the question in your test. This can be done by using the expectsQuestion method.

For example, let's imagine we want to test the following command:

protected $signature = 'app:create-user {email}';

protected $description = 'Create a new user';

public function handle(UserService $userService): int
{
    $roles = [
        'super-admin',
        'admin',
        'manager',
        'user',
    ];

    $role = $this->choice('What is the role of the new user?', $roles);

    if (!in_array($role, $roles, true)) {
        $this->error('The role is invalid!');

        return self::FAILURE;
    }

    $userService->createUser(
        email: $this->argument('email'),
        role: $role,
    );

    $this->info('User created successfully!');

    return self::SUCCESS;
}

To test this command, we could do the following:

/** @test */
public function user_can_be_created_with_choice_for_role(): void
{
    $this->artisan('app:create-user', [
        'email' => 'hello@example.com',
    ])
        ->expectsQuestion('What is the role of the new user?', 'super-admin')
        ->expectsOutput('User created successfully!')
        ->assertSuccessful();

    // Run extra assertions to check the user was created with the correct role...
}

/** @test */
public function error_is_returned_if_the_role_is_invalid(): void
{
    $this->artisan('app:create-user', [
        'email' => 'hello@example.com',
    ])
        ->expectsQuestion('What is the role of the new user?', 'INVALID')
        ->expectsOutput('The role is invalid!')
        ->assertFailed();

    // Run extra assertions to check the user wasn't created...
}

As you can see in the code above, we have one test to ensure the user is created successfully and another test to ensure that an error is returned if the role is invalid.

Running OS processes in Laravel

So far, we've looked at how you can run interact with your Laravel application by running commands from the operating system. However, there may be times when you want to do the opposite and run commands on the operating system from your Laravel application.

This can be useful, for example, when running custom shell scripts, antivirus scans, or file converters.

Let's take a look at how we can add this functionality to our application by using the Process facade that was added in Laravel 10. We'll cover several of the features that you'd be most likely to use in your application. However, if you want to see all the available methods, you can check out the Laravel documentation.

Running processes

To run a command on the operating system, you can use the run method on the Process facade. Let's imagine that we have a shell script called install.sh in the root directory of our project. We can run this script from our code:

use Illuminate\Support\Facades\Process;

$process = Process::path(base_path())->run('./install.sh');
$output = $process->output();

To provide additional context, we may want to run this script from the handle method of an Artisan command that does several things (e.g., reading from the database or making API calls). We could call the shell script from our command:

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Process;

class RunInstallShellScript extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'app:run-install-shell-script';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Install the package and publish the assets';

    /**
     * Execute the console command.
     */
    public function handle(): void
    {
        $this->info('Starting installation...');

        $process = Process::path(base_path())->run('./install.sh');

        $this->info($process->output());

        // Make calls to API here...

        // Publish assets here...

        // Add new rows to the database here...

        $this->info('Installation complete!');
    }
}

You may also notice that we've grabbed the output using the output method so that we can handle it in our Laravel application. For example, if the process was being run via an HTTP request, we may want to display the output on the page.

The commands typically have two types of output: standard output and error output. If we wanted to get any error output from the process, we'd need to use the errorOutput method instead.

Running the process from another directory

By default, the Process facade will run the command in the same working directory as the PHP file that's being run. Typically, this means if you are running the process using an Artisan command, the process will be run in the root directory of your project because that's where the artisan file is located. However, if the process is being run from an HTTP controller, the process will be run in the public directory of your project because that's the entry point for the web server.

Therefore, if you can run the process from both the console and the web, not explicitly specifying the working directory may result in unexpected behavior and errors. One way to tackle this problem is to ensure you always use the path method where possible. This will ensure that the process is always run in the expected directory.

For example, let's imagine that we have a shell script called custom-script.sh in a scripts/custom directory in our project. To run this script, we could do the following:

use Illuminate\Support\Facades\Process;

$process = Process::path(base_path('/scripts/custom'))->run('./install.sh');
$output = $process->output();

Specifying the process timeout

By default, the Process facade will allow processes to be run for a maximum of 60 seconds before it times out. This is to prevent processes from running indefinitely (e.g., if they get stuck in an infinite loop).

However, if you want to run a process that may take longer than the default timeout, you can specify a custom timeout in seconds by using the timeout method. For example, let's imagine that we want to run a process that may take up to 5 minutes (300 seconds) to complete:

use Illuminate\Support\Facades\Process;

$process = Process::timeout(300)->run('./install.sh');
$output = $process->output();

Getting the output in real time

Depending on the process you're executing, you may prefer to output the text from the process as it's running rather than waiting for the process to finish. You might want to do this if you have a long-running script (e.g., an installation script) where you want to see the steps the script is going through.

To do this, you can pass a closure as the second parameter of the run method to determine what to do with the output as it's received. For example, let's imagine that we have a shell script called install.sh and want to see the output in real-time rather than waiting until it's finished. We could run the script like so:

use Illuminate\Support\Facades\Process;

$process = Process::run('./install.sh', function (string $type, string $output): void {
    $type === 'out' ? $this->line($output) : $this->error($output);
});

Running processes asynchronously

There may be times when you want to run a process and carry on running other code while it's running rather than waiting until it's complete. For example, you may want to run an installation script and write to the database or make some API calls while the installation is ongoing.

To do this, you can use the start method. Let's take a look at an example of how we might do this. Let's imagine that we have a shell script called install.sh and want to run some other installation steps while we wait for the shell script to finish running:

use Illuminate\Support\Facades\Process;

public function handle(): void
{
    $this->info('Starting installation...');

    $extraCodeHasBeenRun = false;

    $process = Process::start('./install.sh');

    while ($process->running()) {
        if (!$extraCodeHasBeenRun) {
            $extraCodeHasBeenRun = true;

            $this->runExtraCode();
        }
    }

    $result = $process->wait();

    $this->info($process->output());

    $this->info('Installation complete!');
}

private function runExtraCode(): void
{
    // Make calls to API here...

    // Publish assets here...

    // Add new rows to the database here...
}

As you may have noticed, we're checking in the while loop that we haven't already started running the extra code. We do this because we don't want to run the extra code multiple times. We only want to run it once while the process is running.

After the process has finished running, we can get the result of the process (using the output method) and output it to the console.

Running processes concurrently

There may be times when you want to run multiple commands concurrently. For example, you may want to do this if you have a script that converts a file from one format to another. If you want to convert multiple files at once in bulk (and the script doesn't support multiple files), you may want to run the script for each file concurrently.

This is beneficial because you don't need to run the script for each file sequentially. For instance, if a script took five seconds to run for each file, and you had 3 files to convert, it would take fifteen seconds to run sequentially. However, if you ran the script concurrently, it would only take five seconds to run.

To do this you can use the concurrently method. Let's take a look at an example of how to use it.

We'll imagine that we want to run a convert.sh shell script for three files in a directory. For the purpose of this example, we'll hard code these filenames. However, in a real-world scenario, you'd likely get these filenames dynamically from the filesystem, database, or request.

If we run the scripts sequentially, our code may look something like this:

// 15 seconds to run...

$firstOutput = Process::path(base_path())->run('./convert.sh file-1.png');
$secondOutput = Process::path(base_path())->run('./convert.sh file-1.png');
$thirdOutput = Process::path(base_path())->run('./convert.sh file-1.png');

However, if we run the scripts concurrently, our code may look something like this:

// 5 seconds to run...

$commands = Process::concurrently(function (Pool $pool) {
    $pool->path(base_path())->command('./convert.sh file-1.png');
    $pool->path(base_path())->command('./convert.sh file-2.png');
    $pool->path(base_path())->command('./convert.sh file-3.png');
});

foreach ($commands->collect() as $command) {
    $this->info($command->output());
}

Testing OS processes

Like Artisan commands, you can also test that your OS processes are executed as expected. Let's take a look at some common things you may want to test.

To get started with testing our processes, let's imagine that we have a web route in our application that accepts a file and converts it to a different format. If the file passed is an image, we'll convert the file using a convert-image.sh script. If the file is a video, we'll convert it using convert-video.sh. We'll assume that we have an isImage method in our controller that will return true if the file is an image and false if it's a video.

For the purposes of this example, we're not going to worry about any validation or HTTP testing. We'll focus solely on testing the process. However, in a real-life project, you would want to ensure that your test covers all the possible scenarios.

We'll imagine that our controller can be reached via a POST request to /file/convert (with the route named file.convert) and that it looks something this:

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

class ConvertFileController extends Controller
{
    /**
     * Handle the incoming request.
     */
    public function __invoke(Request $request)
    {
        // Temporarily the file so it can be converted.
        $tempFilePath = 'tmp/'.Str::random();

        Storage::put(
            $tempFilePath,
            $request->file('uploaded_file')
        );

        // Determine which command to run.
        $command = $this->isImage($request->file('uploaded_file'))
            ? 'convert-image.sh'
            : 'convert-video.sh';

        $command .= ' '.$tempFilePath;

        // The conversion command.
        $process = Process::timeout(30)
            ->path(base_path())
            ->run($command);

        // If the process fails, report the error.
        if ($process->failed()) {
            // Report the error here to the logs or bug tracking system...

            return response()->json([
                'message' => 'Something went wrong!',
            ], 500);
        }

        return response()->json([
            'message' => 'File converted successfully!',
            'path' => trim($process->output()),
        ]);
    }

    // ...
}

To test that the processes are correctly dispatched, we may want to write the following tests:

namespace Tests\Feature\Http\Controllers;

use Illuminate\Http\UploadedFile;
use Illuminate\Process\PendingProcess;
use Illuminate\Support\Facades\Process;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Tests\TestCase;

class ConvertFileControllerTest extends TestCase
{
    public function setUp(): void
    {
        parent::setUp();

        Storage::fake();

        Process::preventStrayProcesses();

        // Determine how the random strings should be built.
        // We do this so we can assert the correct command
        // is run.
        Str::createRandomStringsUsing(static fn (): string => 'random');
    }

    /** @test */
    public function image_can_be_converted(): void
    {
        Process::fake([
            'convert-image.sh tmp/random' => Process::result(
                output: 'tmp/converted.webp',
            )
        ]);

        $this->post(route('file.convert'), [
            'uploaded_file' => UploadedFile::fake()->image('dummy.png'),
        ])
            ->assertOk()
            ->assertExactJson([
                'message' => 'File converted successfully!',
                'path' => 'tmp/converted.webp',
            ]);

        Process::assertRan(function (PendingProcess $process): bool {
            return $process->command === 'convert-image.sh tmp/random'
                && $process->path === base_path()
                && $process->timeout === 30;
        });
    }

    /** @test */
    public function video_can_be_converted(): void
    {
        Process::fake([
            'convert-video.sh tmp/random' => Process::result(
                output: 'tmp/converted.mp4',
            )
        ]);

        $this->post(route('file.convert'), [
            'uploaded_file' => UploadedFile::fake()->create('dummy.mp4'),
        ])
            ->assertOk()
            ->assertExactJson([
                'message' => 'File converted successfully!',
                'path' => 'tmp/converted.mp4',
            ]);

        Process::assertRan(function (PendingProcess $process): bool {
            return $process->command === 'convert-video.sh tmp/random'
                && $process->path === base_path()
                && $process->timeout === 30;
        });
    }

    /** @test */
    public function error_is_returned_if_the_file_cannot_be_converted(): void
    {
        Process::fake([
            'convert-video.sh tmp/random' => Process::result(
                errorOutput: 'Something went wrong!',
                exitCode: 1 // Error exit code
            )
        ]);

        $this->post(route('file.convert'), [
            'uploaded_file' => UploadedFile::fake()->create('dummy.mp4'),
        ])
            ->assertStatus(500)
            ->assertExactJson([
                'message' => 'Something went wrong!',
            ]);

        Process::assertRan(function (PendingProcess $process): bool {
            return $process->command === 'convert-video.sh tmp/random'
                && $process->path === base_path()
                && $process->timeout === 30;
        });
    }
}

The above tests will ensure the following:

  • If a file is an image, only the convert-image.sh script is run.
  • If a file is a video, only the convert-video.sh script is run.
  • If the conversion script fails, an error is returned.

You may have noticed that we've used the preventStrayProcesses method. This ensures that only the commands we have specified are run. If any other commands are run, the test will fail. This is handy for giving you confidence that the scripts aren't accidentally running any external processes on your system.

We've also explicitly tested that the commands are run with the expected path and timeout. This is to ensure that the commands are run with the correct settings.

Conclusion

Hopefully, this article has shown you how to create and test your own Artisan commands. It should have also shown you how to run OS processes from your Laravel application and test them. You should now be able to implement both of these features in your projects to add extra functionality to your applications.

What to do next:
  1. Try Honeybadger for FREE
    Honeybadger helps you find and fix errors before your users can even report them. Get set up in minutes and check monitoring off your to-do list.
    Start free trial
    Easy 5-minute setup — No credit card required
  2. Get the Honeybadger newsletter
    Each month we share news, best practices, and stories from the DevOps & monitoring community—exclusively for developers like you.
    author photo

    Ashley Allen

    Ashley is a freelance Laravel web developer who loves contributing to open-source projects and building exciting systems to help businesses succeed.

    More articles by Ashley Allen
    Stop wasting time manually checking logs for errors!

    Try the only application health monitoring tool that allows you to track application errors, uptime, and cron jobs in one simple platform.

    • Know when critical errors occur, and which customers are affected.
    • Respond instantly when your systems go down.
    • Improve the health of your systems over time.
    • Fix problems before your customers can report them!

    As developers ourselves, we hated wasting time tracking down errors—so we built the system we always wanted.

    Honeybadger tracks everything you need and nothing you don't, creating one simple solution to keep your application running and error free so you can do what you do best—release new code. Try it free and see for yourself.

    Start free trial
    Simple 5-minute setup — No credit card required

    Learn more

    "We've looked at a lot of error management systems. Honeybadger is head and shoulders above the rest and somehow gets better with every new release."
    — Michael Smith, Cofounder & CTO of YvesBlue

    Honeybadger is trusted by top companies like:

    “Everyone is in love with Honeybadger ... the UI is spot on.”
    Molly Struve, Sr. Site Reliability Engineer, Netflix
    Start free trial
    Are you using Sentry, Rollbar, Bugsnag, or Airbrake for your monitoring? Honeybadger includes error tracking with a whole suite of amazing monitoring tools — all for probably less than you're paying now. Discover why so many companies are switching to Honeybadger here.
    Start free trial
    Stop digging through chat logs to find the bug-fix someone mentioned last month. Honeybadger's built-in issue tracker keeps discussion central to each error, so that if it pops up again you'll be able to pick up right where you left off.
    Start free trial
    “Wow — Customers are blown away that I email them so quickly after an error.”
    Chris Patton, Founder of Punchpass.com
    Start free trial