Stop Repeating Yourself in Laravel: Custom Artisan Commands Explained
It all started with a simple annoyance.
I was working on a Laravel project and kept having to run the same set of commands in a tinker session to setup a test subscriber in a specific state. Every time I would need to copy and paste a bunch of commands into tinker to get everything to the correct state. It wasn’t necessarily hard but it was annoying and I just knew there had to be an easier way.
It turns out, there is. You can create a custom artisan command using the make:command
command.
The Problem: Death by Repetition
I don’t mind repetition when it’s meaningful, but this wasn’t. I was typing things like:
$team = \App\Models\Team::create([
"name" => "Team with two podcasts" . now()->toDateTimeString(),
]);
\App\Models\Podcast::create([
"team_id" => $team->id,
"name" => "Podcast 1",
]);
\App\Models\Podcast::create([
"team_id" => $team->id,
"name" => "Podcast 2",
]);
Now imagine doing that 20 times in a day while debugging issues. I wanted a single command that would quickly handle creating a specific test setup and give me feedback with color-coded logs so I could tell when something went wrong.
Laravel makes this surprisingly easy.
The Plan: Create a Custom Artisan Command
Artisan provides a structure that allows us to extend the artisan command by defining specific command classes that will automatically register themselves, so we can quickly use them.
Here’s what I did.
Step 1: Generate the Command
We’re going to start out by using Laravel’s make:command
, you just have to give it a name.
php artisan make:command TestCreateTeamWithTwoPodcasts
This created a file in app/Console/Commands/TestCreateTeamWithTwoPodcasts.php
with a basic structure already filled out. Here’s what it looked like:
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
class TestCreateTeamWithTwoPodcasts extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:test-create-team-with-two-podcasts';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Command description';
/**
* Execute the console command.
*/
public function handle()
{
//
}
}
The signature
is how you call the command (php artisan app:test-create-team-with-two-podcasts
), the description
is what shows up when you run php artisan list
, and the handle()
function is where the actual work is done.
Time to make it do something.
Step 2: Running Our Commands
To get started I changed the handle()
function to include our code from before:
public function handle()
{
$team = \App\Models\Team::create([
"name" => "Team with two podcasts" . now()->toDateTimeString(),
]);
\App\Models\Podcast::create([
"team_id" => $team->id,
"name" => "Podcast 1",
]);
\App\Models\Podcast::create([
"team_id" => $team->id,
"name" => "Podcast 2",
]);
}
And then I ran the command:
php artisan app:test-create-team-with-two-podcasts
And when I ran this I received the following boring output:
scottkeck-warren@scotts-MacBook-Pro-2 podplanner % sail artisan app:test-create-team-with-two-podcasts
scottkeck-warren@scotts-MacBook-Pro-2 podplanner %
We can make this a lot more interesting by using one of the logging functions that is included in the base Illuminate\Console\Command
class.
I’m going to start by displaying the name of the new Team
we’ve created.
public function handle()
{
$team = \App\Models\Team::create([
"name" => "Team with two podcasts" . now()->toDateTimeString(),
]);
$this->info("Team {$team->name} has been created");
// ..snip
}
And when we run this we’ll get our output:
scottkeck-warren@scotts-MacBook-Pro-2 podplanner % sail artisan app:test-create-team-with-two-podcasts
Team Team with two podcasts2025-08-18 21:15:11 has been created
scottkeck-warren@scotts-MacBook-Pro-2 podplanner %
Laravel has several of these helper functions that automatically formats the log with colored output in the terminal.
$this->info()
= green$this->warn()
= yellow$this->error()
= red$this->line()
= plain text (no color)
It makes it really simple and readable.
Step 3: Adding Input
The huge downside to our solution is that we might want to customize the name of our Team
s so we can better segment out different test cases. Again, Laravel has us covered because we can define a parameter to the command in the signature, and Laravel will automatically take care of it for us.
class TestCreateTeamWithTwoPodcasts extends Command
{
protected $signature = 'app:test-create-team-with-two-podcasts {team}';
protected $description = 'Command description';
public function handle()
{
$team = \App\Models\Team::create([
"name" => $this->argument("team"),
]);
// snip...
}
}
Now we can pass it as a parameter:
scottkeck-warren@scotts-MacBook-Pro-2 podplanner % sail artisan app:test-create-team-with-two-podcasts "Scott's"
Team Scott's has been created
scottkeck-warren@scotts-MacBook-Pro-2 podplanner %
To make it dummy proof we have our command implement the PromptsForMissingInput
interface.
class TestCreateTeamWithTwoPodcasts extends Command implements PromptsForMissingInput
Now we’ll get a prompt for the team name.
scottkeck-warren@scotts-MacBook-Pro-2 podplanner % sail artisan app:test-create-team-with-two-podcasts
┌ What is the team? ───────────────────────────────────────────┐
│ │
└──────────────────────────────────────────────────────────────┘
Step 4: Add Error Handling
So after I’ve created sixteen teams named “Scott’s” I realized I should enforce some uniqueness to do that we nee to add some error handling. I’m going to add a basic check to make sure the “name” field is unique.
public function handle()
{
$teamName = $this->argument("team");
$existing = Team::where('name', $teamName)->first();
if ($existing) {
$this->error("Team $teamName already exists");
return COMMAND::FAILURE;
}
// snip...
}
Now I can’t create a duplicate:
scottkeck-warren@scotts-MacBook-Pro-2 podplanner % sail artisan app:test-create-team-with-two-podcasts "Scott's"
Team Scott's already exists
Final Command Class
Here’s what the whole thing looked like when I was done:
<?php
namespace App\Console\Commands;
use App\Models\Team;
use Illuminate\Console\Command;
use Illuminate\Contracts\Console\PromptsForMissingInput;
class TestCreateTeamWithTwoPodcasts extends Command implements PromptsForMissingInput
{
protected $signature = 'app:test-create-team-with-two-podcasts {team}';
protected $description = 'Command description';
public function handle()
{
$teamName = $this->argument("team");
$existing = Team::where('name', $teamName)->first();
if ($existing) {
$this->error("Team $teamName already exists");
return COMMAND::FAILURE;
}
$team = \App\Models\Team::create([
"name" => $teamName,
]);
$this->info("Team {$team->name} has been created");
\App\Models\Podcast::create([
"team_id" => $team->id,
"name" => "Podcast 1",
]);
\App\Models\Podcast::create([
"team_id" => $team->id,
"name" => "Podcast 2",
]);
return COMMAND::SUCCESS;
}
}
What I Learned
This wasn’t rocket science. But it was one of those quality-of-life improvements that made my dev workflow smoother instantly.
Here are a few takeaways:
- Laravel’s command structure is really easy to extend.
- Color-coded logging with
$this->info()
,$this->warn()
, and$this->error()
makes output easy to scan. - You can return
Command::SUCCESS
orCommand::FAILURE
for better CI feedback.
Most importantly: this was satisfying. It felt like Laravel wanted me to build tools like this.
Leave a comment
Use the form below to leave a comment: