Skip to content

Runner Laravel

A powerful Laravel package for managing and executing one-time or recurring tasks with scheduling capabilities, execution tracking, and comprehensive logging.

Overview

Runner Laravel provides a flexible system for creating and managing executable tasks (runners) in your Laravel application. It's perfect for data migrations, seeding operations, maintenance tasks, and any code that needs to run once or on a schedule.

Features

  • Flexible Execution Types: Run tasks once or repeatedly
  • Cron Scheduling: Built-in scheduling with cron expressions
  • Tag-Based Filtering: Organize and filter runners by tags
  • Priority System: Control execution order with priorities
  • Execution Tracking: Automatic database tracking of runs
  • Comprehensive Logging: Detailed logs with execution time and output
  • Force Mode: Re-execute runners regardless of history
  • Artisan Commands: Full CLI support for creating, listing, and running tasks

Installation

Install the package via Composer:

bash
composer require obelaw/runner-laravel

Run the migrations to create the necessary database tables:

bash
php artisan migrate

Configuration

By default, runners are stored in the base_path('runners') directory. You can add additional runner paths using the RunnerPool:

php
use Obelaw\Runner\RunnerPool;

// In a service provider
RunnerPool::addPath(app_path('Runners'));
RunnerPool::addPath(database_path('runners'));

Creating Runners

Using the Artisan Command

The easiest way to create a runner is using the runner:make command:

bash
php artisan runner:make

This interactive command will prompt you for:

  • Runner name
  • Tag (optional)
  • Description (optional)
  • Type (once/always)
  • Priority (optional)
  • Schedule (optional, cron expression)

Manual Creation

Create a new PHP file in your runners directory with a timestamp prefix:

php
<?php
// File: runners/2024_11_01_120000_create_categories.php

use Obelaw\Runner\Runner;

return new class extends Runner
{
    /**
     * The tag of the runner for filtering execution.
     */
    public ?string $tag = 'setup';

    /**
     * The priority of the runner (lower numbers run first).
     */
    public int $priority = 0;

    /**
     * The description of what this runner does.
     */
    public ?string $description = 'Create default product categories';

    /**
     * The type of runner execution: 'once' or 'always'.
     */
    protected string $type = Runner::TYPE_ONCE;

    /**
     * Execute the runner logic.
     */
    public function handle(): void
    {
        // Your implementation here
        $this->info('Creating categories...');
    }

    /**
     * Hook that runs before the main handle method.
     */
    public function before(): void
    {
        // Optional: Add pre-execution logic
    }

    /**
     * Hook that runs after the main handle method.
     */
    public function after(): void
    {
        // Optional: Add post-execution logic
    }

    private function info(string $message): void
    {
        echo "[INFO] " . $message . PHP_EOL;
    }
};

Runner Types

TYPE_ONCE (Default)

Runs only once. After execution, it won't run again unless forced:

php
protected string $type = Runner::TYPE_ONCE;

TYPE_ALWAYS

Runs every time the runner command is executed:

php
protected string $type = Runner::TYPE_ALWAYS;

Scheduling

Runners support cron-based scheduling using the Schedulable trait. You can set schedules directly or use helper methods:

Direct Cron Expression

php
protected ?string $schedule = '0 0 * * *'; // Daily at midnight

Using Helper Methods

php
public function __construct()
{
    $this->everyMinute();      // Every minute: * * * * *
    $this->everyMinutes(5);     // Every 5 minutes: */5 * * * *
    $this->hourly();            // Hourly: 0 * * * *
    $this->everyHours(3);       // Every 3 hours: 0 */3 * * *
    $this->daily();             // Daily at midnight: 0 0 * * *
    $this->dailyAt('14:30');    // Daily at 2:30 PM: 30 14 * * *
    $this->weekly();            // Weekly on Sunday: 0 0 * * 0
    $this->weeklyOn(1);         // Weekly on Monday: 0 0 * * 1
    $this->monthly();           // Monthly on 1st: 0 0 1 * *
    $this->monthlyOn(15);       // Monthly on 15th: 0 0 15 * *
    $this->cron('0 8-23,0-3 * * *'); // Custom cron
}

Schedule Information

Get schedule information:

php
$runner->getSchedule();           // Get cron expression
$runner->isValidSchedule();       // Validate cron expression
$runner->getNextRunTime();        // Get next run time
$runner->getPreviousRunTime();    // Get previous run time
$runner->getNextRunDates(5);      // Get next 5 run dates

Setting Up Cron Job

To automatically execute scheduled runners, add this entry to your server's crontab:

bash
* * * * * cd /path-to-project && php artisan runner:run --scheduled >> /dev/null 2>&1

This will run every minute and execute any runners that are due according to their schedule. Replace /path-to-project with the absolute path to your Laravel project.

To edit your crontab:

bash
crontab -e

Running Runners

Run All Runners

bash
php artisan runner:run

Run by Tag

bash
php artisan runner:run --tag=setup

Run Specific Runner

bash
php artisan runner:run 2024_11_01_120000_create_categories.php

Force Re-execution

Re-run all runners, including those already executed (TYPE_ONCE):

bash
php artisan runner:run --force

Run Scheduled Runners Only

Execute only runners with defined schedules:

bash
php artisan runner:run --scheduled

Combined Options

bash
php artisan runner:run --tag=maintenance --scheduled --force

Listing Runners

Display all available runners:

bash
php artisan runner:list

Filter by Tag

bash
php artisan runner:list --tag=setup

Filter by Type

bash
php artisan runner:list --type=once
php artisan runner:list --type=always

Filter by Status

bash
php artisan runner:list --status=executed
php artisan runner:list --status=pending

Programmatic Usage

Use the RunnerService class for programmatic execution:

php
use Obelaw\Runner\Services\RunnerService;
use Obelaw\Runner\RunnerPool;

$service = new RunnerService(RunnerPool::getPaths());

// Run all runners
$summary = $service->run();

// Run with tag filter
$summary = $service->run('setup');

// Force execution
$service->force(true)->run();

// Run only scheduled runners
$service->scheduledOnly(true)->run();

// Run specific runner
$summary = $service->runByName('2024_11_01_120000_create_categories.php');

// Disable execution tracking
$service->trackExecutions(false)->run();

Execution Summary

The service returns an execution summary:

php
[
    'executed_count' => 5,
    'skipped_count' => 2,
    'error_count' => 0,
    'executed_files' => ['file1.php', 'file2.php', ...],
    'skipped_files' => ['file3.php', ...],
    'errors' => [],
    'success' => true
]

Execution Tracking

Database Models

RunnerModel

Tracks runner executions:

php
use Obelaw\Runner\Models\RunnerModel;

// Check if executed
RunnerModel::hasBeenExecuted('runner_name.php');

// Mark as executed
RunnerModel::markAsExecuted('runner_name.php', [
    'tag' => 'setup',
    'type' => 'once',
    'description' => 'Create categories'
]);

// Get by tag
RunnerModel::getByTag('setup');

// Get by type
RunnerModel::getOnceRunners();
RunnerModel::getAlwaysRunners();

// Get logs
$runner = RunnerModel::where('name', 'runner.php')->first();
$runner->lastLog();
$runner->failedLogs();

RunnerLog

Detailed execution logs:

php
use Obelaw\Runner\Models\RunnerLog;

// Get recent logs
RunnerLog::getRecentLogs('runner_name.php', 10);

// Get failed logs
RunnerLog::getFailedLogs(50);

// Query by status
RunnerLog::status('completed')->get();
RunnerLog::failed()->get();
RunnerLog::completed()->get();

// Query by runner
RunnerLog::forRunner('runner_name.php')->get();

// Query by tag
RunnerLog::withTag('setup')->get();

// Get execution time
$log = RunnerLog::find(1);
$log->getExecutionTimeInSeconds(); // In seconds
$log->execution_time; // In milliseconds

Database Schema

runners Table

php
Schema::create('runners', function (Blueprint $table) {
    $table->id();
    $table->string('name')->unique();
    $table->string('tag')->nullable()->index();
    $table->text('description')->nullable();
    $table->integer('priority')->default(0);
    $table->enum('type', ['once', 'always'])->default('once');
    $table->timestamp('executed_at');
    $table->timestamps();
});

runner_logs Table

php
Schema::create('runner_logs', function (Blueprint $table) {
    $table->id();
    $table->string('runner_name')->index();
    $table->string('tag')->nullable()->index();
    $table->enum('type', ['once', 'always'])->default('once');
    $table->enum('status', ['started', 'completed', 'failed'])->index();
    $table->text('output')->nullable();
    $table->text('error')->nullable();
    $table->integer('execution_time')->nullable()->comment('In milliseconds');
    $table->timestamp('started_at')->nullable();
    $table->timestamp('completed_at')->nullable();
    $table->timestamps();
});

Advanced Features

Conditional Execution

Override the shouldRun() method:

php
public function shouldRun(): bool
{
    // Only run in production
    if (!app()->environment('production')) {
        return false;
    }
    
    // Only run if condition is met
    if (!SomeModel::exists()) {
        return false;
    }
    
    return parent::shouldRun();
}

Custom Execution Logic

Use hooks for complex workflows:

php
public function before(): void
{
    Log::info('Starting runner execution');
    DB::beginTransaction();
}

public function handle(): void
{
    // Main logic
}

public function after(): void
{
    DB::commit();
    Log::info('Runner completed successfully');
}

Priority-Based Execution

Lower priority numbers run first:

php
// This runs first
public int $priority = 0;

// This runs second
public int $priority = 10;

// This runs last
public int $priority = 100;

Tagging Strategy

Organize runners with tags:

php
// Setup runners
public ?string $tag = 'setup';

// Maintenance runners
public ?string $tag = 'maintenance';

// Data migration runners
public ?string $tag = 'migration';

// Installation runners
public ?string $tag = 'install';

Best Practices

  1. Use Timestamps: Always prefix runner files with timestamps (format: YYYY_MM_DD_HHMMSS_)
  2. Descriptive Names: Use clear, descriptive names for runners
  3. Set Descriptions: Always add descriptions to explain what the runner does
  4. Use Tags: Organize runners with consistent tagging
  5. Set Priorities: Use priorities when execution order matters
  6. Error Handling: Wrap critical operations in try-catch blocks
  7. Logging: Use the info() method or Laravel's Log facade for output
  8. Schedule Wisely: Use appropriate schedules to avoid resource contention
  9. Test Locally: Test runners in development before production deployment
  10. Document Dependencies: Note any dependencies or prerequisites in descriptions

Example Use Cases

One-Time Data Migration

php
public ?string $tag = 'migration';
public ?string $description = 'Migrate legacy user data to new schema';
protected string $type = Runner::TYPE_ONCE;

public function handle(): void
{
    User::chunk(100, function ($users) {
        foreach ($users as $user) {
            // Migration logic
        }
    });
}

Recurring Cleanup Task

php
public ?string $tag = 'maintenance';
public ?string $description = 'Clean up expired sessions daily';
protected string $type = Runner::TYPE_ALWAYS;
protected ?string $schedule = '0 2 * * *'; // 2 AM daily

public function handle(): void
{
    Session::where('last_activity', '<', now()->subDays(30))->delete();
}

Initial Setup Runner

php
public ?string $tag = 'install';
public ?string $description = 'Create default application settings';
protected string $type = Runner::TYPE_ONCE;
public int $priority = 1;

public function handle(): void
{
    Setting::create(['key' => 'app_initialized', 'value' => true]);
}

Troubleshooting

Runners Not Executing

  • Check if the runner file exists in a configured path
  • Verify the file returns a runner instance
  • Check execution history (TYPE_ONCE runners won't re-run)
  • Use --force flag to bypass execution history

Schedule Not Working

  • Verify cron expression is valid
  • Check if shouldRunBySchedule() returns true
  • Ensure the schedule period has elapsed
  • Use --scheduled flag to test scheduled runners

Database Tracking Issues

  • Run migrations: php artisan migrate
  • Check database connection
  • Verify trackExecutions is not disabled

License

This package is open-sourced software licensed under the MIT license.

Repository

https://github.com/obelaw/runner-laravel

Released under the MIT License.