Skip to Content
Course content

240: Building CLI Tools with Laravel Artisan Commands

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've noticed a recurring habit among developers moving into larger Laravel projects: when they need to automate a task—like cleaning up old logs or syncing data from an external API—they create a folder called /scripts in the root directory and throw a standalone sync_data.php file in there. They think they're being "efficient" by avoiding the overhead of the framework for a "simple" script.

"I can just run a standalone PHP script for background tasks"

Here is why that approach is a trap. If you write a standalone script, you lose everything that makes Laravel powerful. You don't have access to your Eloquent models, your Service Container, your configured Mailers, or your environment variables without manually bootstrapping the entire framework kernel inside that script. It's a messy, brittle process that usually leads to a Fatal error: Class 'App\Models\User' not found because the autoloader wasn't handled correctly.

If you're already inside a Laravel project, your CLI tool should be an Artisan command. By doing this, your script lives inside the application ecosystem. You can inject dependencies directly into the handle() method, and you get a standardized way to handle input and output.

Leveraging the Service Container via Artisan Commands

Let's look at a real scenario. Imagine you need to build a tool that fetches current product prices from a supplier's API and updates your local database. Instead of a raw script, we use php artisan make:command SyncProductPrices.

In the resulting class, the handle() method is where the magic happens. Because it's an Artisan command, Laravel's container handles the dependencies for you. I personally love this because it makes testing a breeze.


public function handle(ProductPriceService $priceService)
{
    $this->info('Fetching latest prices from supplier...');

    try {
        $count = $priceService->updateAllPrices();
        $this->info("Successfully updated {$count} products.");
    } catch (\Exception $e) {
        $this->error('Sync failed: ' . $e->getMessage());
        return 1; // Return a non-zero exit code on failure
    }
}

Notice how I injected ProductPriceService? I didn't have to instantiate it or worry about its own dependencies. I also used $this->info() and $this->error(). These aren't just echo statements; they provide formatted, color-coded output that makes your CLI tool feel professional.

Defining Signatures and Handling User Input

One of the most powerful parts of Artisan commands is the $signature property. This is where you define how the command is called and what arguments or options it accepts. You don't have to parse $argv manually like you would in a plain PHP script.

Let's say we want to allow the user to sync only a specific category of products, or perhaps run a "dry run" to see what would happen without actually changing the database. Your signature would look like this:


protected $signature = 'app:sync-prices {category?} {--dry-run}';

The {category?} is an optional argument, and {--dry-run} is a boolean flag (an option). Inside the handle() method, you can retrieve these easily:


$category = $this->argument('category');
$isDryRun = $this->option('dry-run');

if ($isDryRun) {
    $this->comment('Dry run enabled. No changes will be saved.');
}

Moving from Manual Execution to the Scheduler

The final piece of the puzzle is that once your logic is wrapped in an Artisan command, you've unlocked the Laravel Scheduler. You no longer have to manually add a cron job for every single script you write.

Instead of adding * * * * * php /path/to/scripts/sync_data.php to your server's crontab, you just add one single entry for the Laravel Scheduler. Then, in your routes/console.php (or app/Console/Kernel.php in older versions), you can define the frequency in plain English:


use Illuminate\Support\Facades\Schedule;

Schedule::command('app:sync-prices')->dailyAt('03:00');

This keeps your automation logic in version control where it belongs, rather than hidden away in a server's crontab file. It's cleaner, safer, and significantly easier for the next developer on your team to discover.




📋 Practical Task

Build a "Stale User Session Cleanup" Tool

Your goal is to create a custom Artisan command that identifies and removes "stale" records from a hypothetical user_sessions table. This is a common task for maintaining database performance in high-traffic apps.

Requirements:

  • Generate a command named app:cleanup-sessions.
  • The command must accept a required argument {days} (an integer) to determine how old a session must be to be considered "stale".
  • The command should include an optional flag {--force}. If this flag is NOT present, the command should simply output how many records would be deleted (a dry run). If the flag IS present, it should actually perform the deletion.
  • Use $this->info() to report the number of records affected and $this->warn() if the command is running in dry-run mode.
  • Ensure the command returns a proper exit code (0 for success, 1 for failure).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.