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:
app/Providers/AppServiceProvider.php
1declare(strict_types=1);23namespace App\Providers;45use Illuminate\Database\Console\Migrations\FreshCommand;6use Illuminate\Database\Console\Migrations\RefreshCommand;7use Illuminate\Database\Console\Migrations\ResetCommand;8use Illuminate\Database\Console\Migrations\RollbackCommand;9use Illuminate\Database\Console\WipeCommand;10use Illuminate\Support\ServiceProvider;1112final class AppServiceProvider extends ServiceProvider13{14 // ...1516 /**17 * Bootstrap any application services.18 */19 public function boot(): void20 {21 WipeCommand::prohibit($this->app->isProduction());22 FreshCommand::prohibit($this->app->isProduction());23 ResetCommand::prohibit($this->app->isProduction());24 RefreshCommand::prohibit($this->app->isProduction());25 RollbackCommand::prohibit($this->app->isProduction());26 }27}
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:
1WARN 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:
app/Providers/AppServiceProvider.php
1declare(strict_types=1);23namespace App\Providers;45use Illuminate\Support\Facades\DB;6use Illuminate\Support\ServiceProvider;78final class AppServiceProvider extends ServiceProvider9{10 // ...1112 /**13 * Bootstrap any application services.14 */15 public function boot(): void16 {17 DB::prohibitDestructiveCommands($this->app->isProduction());18 }19}
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:
app/Console/Commands/ClearStripeData.php
1declare(strict_types=1);23namespace App\Console\Commands;45use Illuminate\Console\Command;6use Illuminate\Console\Prohibitable;78final class ClearStripeData extends Command9{10 use Prohibitable;1112 protected $signature = 'app:clear-stripe-data';1314 // ...1516 public function handle(): int17 {18 if ($this->isProhibited()) {19 return self::FAILURE;20 }2122 // Delete Stripe data...23 }24}
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:
app/Providers/AppServiceProvider.php
1declare(strict_types=1);23namespace App\Providers;45use App\Console\Commands\ClearStripeData;6use Illuminate\Support\ServiceProvider;78final class AppServiceProvider extends ServiceProvider9{10 /**11 * Bootstrap any application services.12 */13 public function boot(): void14 {15 ClearStripeData::prohibit();16 }17}
Now, if we were to try running the app:clear-stripe-data
command in a production environment, we'd see the following message:
1WARN 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 below.
Keep on building awesome stuff! ๐