Preventing Destructive Commands from Running in Laravel
Photo by Moritz Mentges on Unsplash
Introduction
I recently stumbled upon a cool Laravel feature I hadn't come across before: the ability to prevent destructive commands from running in production.
So, for those of you like me who haven't heard of this feature before, I thought I'd write a quick article to share it with you. I think it's something you'll really like! I'll be adding to all my Laravel projects from now on.
Preventing Destructive Commands from Running in Laravel
In PR #51376 to Laravel in May 2024, Jason McCreary and Joel Clermont added the Illuminate\Console\Prohibitable
trait to the framework, which was then included in the Laravel 11.9 release.
Adding this trait to an Artisan command provides a prohibit
method that you can use to determine whether the command should be prevented from running.
Example Use Case
I can already think of a perfect use case for this. When I first started as a junior developer, I typed the command php artisan migrate:fresh --seed
in my terminal, thinking I'd be refreshing my local database. Little did I know that my terminal was still connected to the production server. Thankfully, I realised my mistake before hitting ENTER, and I would have only wiped my own website's database, which had no users at the time. But it could have been a disaster - especially if it had been a client's site or app!
P.S. - Don't worry, I've learned my lesson since then! My terminal etiquette has improved significantly.
If I had hit ENTER, Laravel would have asked me to confirm that I wanted to run the command. But it wouldn't have stopped me from running it. So, if I had been in a rush and had not read the confirmation message correctly, I could have still wiped the production database.
That's where the Illuminate\Console\Prohibitable
trait comes in. You can use it to prevent the command from running, even if you try to force it.
Prohibiting Laravel's Destructive Database Commands
Laravel ships with the following commands that have the Illuminate\Console\Prohibitable
trait applied:
migrate:fresh
-Illuminate\Database\Console\Migrations\FreshCommand
migrate:refresh
-Illuminate\Database\Console\Migrations\RefreshCommand
migrate:reset
-Illuminate\Database\Console\Migrations\ResetCommand
migrate:rollback
-Illuminate\Database\Console\Migrations\RollbackCommand
migrate:wipe
-Illuminate\Database\Console\WipeCommand
Let's take a look at how we could prevent these commands from running in a production environment:
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Database\Console\Migrations\FreshCommand;
use Illuminate\Database\Console\Migrations\RefreshCommand;
use Illuminate\Database\Console\Migrations\ResetCommand;
use Illuminate\Database\Console\Migrations\RollbackCommand;
use Illuminate\Database\Console\WipeCommand;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
// ...
/**
* Bootstrap any application services.
*/
public function boot(): void
{
WipeCommand::prohibit($this->app->isProduction());
FreshCommand::prohibit($this->app->isProduction());
ResetCommand::prohibit($this->app->isProduction());
RefreshCommand::prohibit($this->app->isProduction());
RollbackCommand::prohibit($this->app->isProduction());
}
}
As we can see in the code example above, in our App\Providers\AppServiceProvider
class, we've used the boot
method to prohibit the destructive commands from running in a production environment.
The prohibit
method accepts a boolean parameter. If the parameter is true
, the command will be prohibited from running. If the parameter is false
, the command will be allowed to run.
If you were to try running one of these commands outside of a production environment, the commands would run as expected. But if you tried running one of these commands in a production environment, you'd see the following message:
WARN This command is prohibited from running in this environment.
Laravel also ships with a handy Illuminate\Support\Facades\DB::prohibitDestructiveCommands
helper method so you can prevent the five commands from running in a production environment with a single line of code rather than needing to prohibit each one individually:
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
// ...
/**
* Bootstrap any application services.
*/
public function boot(): void
{
DB::prohibitDestructiveCommands($this->app->isProduction());
}
}
As we can see in the code example, we've used the Illuminate\Support\Facades\DB::prohibitDestructiveCommands
helper method and passed it a boolean value. This is equivalent to the previous example but is more concise. I'll be adding this line of code to all my Laravel projects from now on.
Prohibiting Your Own Artisan Commands
You may want to use the Illuminate\Console\Prohibitable
trait in your own Artisan commands. For example, you might have a command that deletes all data from a third-party service, such as Stripe, so you'll want to prevent that from running in a production environment. Or, you may want to prevent a command from running until a particular feature flag is enabled.
Let's look at how you could use this trait in your own commands.
Imagine you have created a command which makes API calls to Stripe to clear all your data. We'll assume we don't want this command to run in a production environment and is only intended for local development purposes. Let's take a look at what the command might look like:
declare(strict_types=1);
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Console\Prohibitable;
final class ClearStripeData extends Command
{
use Prohibitable;
protected $signature = 'app:clear-stripe-data';
// ...
public function handle(): int
{
if ($this->isProhibited()) {
return self::FAILURE;
}
// Delete Stripe data...
}
}
In the command above, we can see that we're using the Illuminate\Console\Prohibitable
trait in our App\Console\Commands\ClearStripeData
command.
We've also added a check at the beginning of the handle
method to see if the command is prohibited from running. If it is, we return self::FAILURE
, preventing the rest of the command from running. If it's not prohibited, the rest of the command will continue running as expected.
Even though we've added the Illuminate\Console\Prohibitable
trait to our command, the command will still run as expected because we've not actually prohibited it from running yet.
So we'll head over to our App\Providers\AppServiceProvider
class and add the following line of code to prohibit the ClearStripeData
command from running in a production environment:
declare(strict_types=1);
namespace App\Providers;
use App\Console\Commands\ClearStripeData;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
ClearStripeData::prohibit();
}
}
Now, if we were to try running the app:clear-stripe-data
command in a production environment, we'd see the following message:
WARN This command is prohibited from running in this environment.
Pretty cool, right?
Of course, you might be looking at this and thinking, "Why wouldn't I just replace the check at the top of the command with if ($this->app->isProduction())
?" And you'd be right; that's a totally valid approach and might be more suitable for you.
But I think the Illuminate\Console\Prohibitable
trait is a nice way to encapsulate the logic for prohibiting a command from running in a single place. It's particularly nice if you want to reuse the same logic across multiple commands.
Conclusion
In this article, we've looked at how to prevent destructive commands from running in Laravel applications. We've also seen how we can use the Illuminate\Console\Prohibitable
trait to prevent our own commands from running in a production environment.
If you enjoyed reading this post, you might be interested in my 220+ page ebook, "Battle Ready Laravel", which covers similar topics in more depth.
Or, you might want to check out my other 440+ page ebook, "Consuming APIs in Laravel", which teaches you how to use Laravel to consume APIs from other services.
If you'd like to be updated each time I publish a new post, feel free to sign up for my newsletter.
Keep on building awesome stuff! 🚀
driesvints liked this article
Other articles you might like
Serve a Laravel project on Web, Desktop and Mobile with Tauri
How to display a Laravel project simultaneously on the web, your operating system, and your mobile d...
How to add WebAuthn Passkeys To Backpack Admin Panel
Want to make your Laravel Backpack admin panel more secure with a unique login experience for your a...
Quickest way to setup PHP Environment (Laravel Herd + MySql)
Setting up a local development environment can be a time taking hassle—whether it's using Docker or...
The Laravel portal for problem solving, knowledge sharing and community building.
The community