How To Use Command Events In Laravel

How To Use Command Events In Laravel

·

3 min read

Have you ever wanted to create a global command parameter in your Laravel application? Have you ever wanted to do something whenever an artisan command is called? If you have answered yes to any of the above questions, then this is the article for you.

Example

Let's say you have wrote a very simple multi-tenanted application (single database). You might want to write some commands which import data to specific tenants or you might want to write some other commands that are tenant specific. You could prompt the user inside every command you would like to support multi-tenancy with a question or add in a mandetory argument within the command like php artisan import:data --tenant=1. While both of the aformentioned solutions will work they may, however, not comply with the DRY (Don't Repeat Yourself) rule. How could you achieve this in one place?

The best way I have found to achieve our goal is to utilise some of the existing Command Events Laravel fires by default. If we look at this snippet here from Illuminate/Console/Application.php, we can see the events fired whenever a command is run (click here to view on github).

public function run(InputInterface $input = null, OutputInterface $output = null)
{
    $commandName = $this->getCommandName(
        $input = $input ?: new ArgvInput
    );

    $this->events->dispatch(
        new CommandStarting(
            $commandName, $input, $output = $output ?: new BufferedConsoleOutput
        )
    );

    $exitCode = parent::run($input, $output);

    $this->events->dispatch(
        new CommandFinished($commandName, $input, $output, $exitCode)
    );

    return $exitCode;
}

As you can see, there is a CommandStarting event which is fired before a command is executed and a CommandFinished event fired after the command is executed.

Implementation

Using our above example, let's say we want to specify a tenant argument for all multi-tenanted commands php artisan import:data --tenant=1.

We can start by creating an Event Listener to listen for the CommandStarting event. Let's call it TenantCommandParameterListener.

php artisan make:listener TenantCommandParameterListener

Now let's register our listener and bind it to the CommandStarting event. To do so, open up EventServiceProvider and add in the below to our $listen class variable array.

/**
 * The event listener mappings for the application.
 *
 * @var array
 */
protected $listen = [
    Registered::class => [
        SendEmailVerificationNotification::class,
    ],
    CommandStarting::class => [
        TenantCommandParameterListener::class
    ]
];

With the Event Listener now registered, let's go back to our TenantCommandParameterListener class. Within our handle function, let's pass in the CommandStarting class as a function parameter.

/**
 * Handle the event.
 *
 * @param  CommandStarting  $event
 * @return void
 */
public function handle(CommandStarting $event)
{
}

So you might be wondering how we now access the command parameters from within this. If you would like to try work it out, don't read on and put in dd($event) within your handle function and run any artisan command with an argument (php artisan migrate --tenant=1).

As you may have seen, we can gain access to a variable called input on the $event variable. Using the above command as an example, to get the tenant parameter, we can actually just call the following within our handle function.

$event->input->getParameterOption('--tenant');

If you are wanting to see an example of how this could be used for our use case of multi-tenany, here is a real example from a live project.

/**
 * Handle the event.
 *
 * @param  CommandStarting  $event
 * @return void
 */
public function handle(CommandStarting $event)
{
    if ($event->input->hasParameterOption('--tenant')) {
        app(TenantService::class)->setTenant(Tenant::find($event->input->getParameterOption('--tenant')));
    }
}

This example checks if the parameter --tenant exists and if it does, it sets the current Tenant. This will run for any command that has the --tenant parameter set.

Thank You

I appricate this is one of my first blog posts so it may not be 100% gramatically correct or written very well. Any feedback would be greatly appriciated.

Also, if any of you would like to know how I go about testing the TenantCommandParameterListener class or testing any Event Listeners, do let me know!