PHP Architect logo

Want to check out an issue? Sign up to receive a special offer.

Stop Repeating Yourself in Laravel: Custom Artisan Commands Explained

Posted by on September 8, 2025

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 Teams 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 or Command::FAILURE for better CI feedback.

Most importantly: this was satisfying. It felt like Laravel wanted me to build tools like this.


Tags:
 

Leave a comment

Use the form below to leave a comment: