Skip navigation

Creating Jobs

Jobs are the building blocks of your background processing system. Each job is a PHP class that encapsulates a specific task, from sending emails to processing uploaded files. This guide will walk you through creating your first job and explore advanced features for building robust background tasks.

Your First Job

Let's create a simple job that demonstrates the basic structure:

<?php

namespace App\Jobs;

use tobimori\Queues\Job;

class ProcessDataJob extends Job
{
    public function handle(): void
    {
        // Get data from payload
        $itemId = $this->payload()['itemId'];
        $action = $this->payload()['action'];
        
        $this->log('info', 'Starting processing', [
            'itemId' => $itemId,
            'action' => $action
        ]);
        
        // Simulate some work
        sleep(2);
        
        // Do your actual work here
        // $this->processItem($itemId, $action);
        
        $this->log('info', 'Processing completed');
    }
    
    public function type(): string
    {
        return 'process-data';
    }
    
    public function name(): string
    {
        return 'Process Data';
    }
}

Registering Jobs

Before you can use a job, you need to register it with the queue system. Add this to your plugin's index.php or in a system.loadPlugins:after hook:

use tobimori\Queues\Queues;
use App\Jobs\ProcessDataJob;

Queues::register(ProcessDataJob::class);

You can also register multiple jobs at once:

Queues::register([
    ProcessDataJob::class,
    SendNewsletter::class,
    GenerateReport::class
]);

Dispatching Jobs

Once registered, you can push jobs to the queue:

// Simple dispatch
queue(ProcessDataJob::class, [
    'itemId' => 'abc123',
    'action' => 'process'
]);

// Dispatch with delay (process in 5 minutes)
queueLater(300, ProcessDataJob::class, [
    'itemId' => 'abc123',
    'action' => 'process'
]);

// Dispatch to specific queue
queue(ProcessDataJob::class, ['itemId' => 'abc123'], 'high');

Job Lifecycle

Required Methods

Every job must implement three methods:

public function handle(): void
{
    // Your job logic here
}

public function type(): string
{
    // Unique identifier for this job type
    return 'my-job-type';
}

public function name(): string
{
    // Human-readable name for the Panel
    return 'My Job';
}

Optional Methods

Override these methods to customize job behavior:

public function failed(\Exception $exception): void
{
    // Called when job fails after all retries
    // Send notification, log error, cleanup, etc.
    kirby()->email([
        'to' => 'admin@example.com',
        'subject' => 'Job failed: ' . $this->name(),
        'body' => $exception->getMessage()
    ]);
}

public function timeout(): int
{
    // Override default timeout (in seconds)
    return 300; // 5 minutes
}

public function maxAttempts(): int
{
    // Override default retry attempts
    return 5;
}

public function retryBackoff(): int
{
    // Custom retry delay calculation
    return $this->attempts() * 60; // Linear backoff
}

Working with Payload

The payload is the data passed to your job. Access it using the payload() method:

public function handle(): void
{
    $userId = $this->payload()['userId'];
    $action = $this->payload()['action'];
    $options = $this->payload()['options'] ?? [];
    
    // Always validate your payload
    if (!$userId) {
        throw new \InvalidArgumentException('User ID is required');
    }
}

Logging

Jobs can log messages at different levels. Available log levels are:

  • info - General information messages
  • warning - Warning messages that don't stop execution
  • error - Error messages for failures
  • debug - Detailed debugging information
public function handle(): void
{
    $this->log('info', 'Starting process');
    $this->log('debug', 'Payload received', $this->payload());
    
    try {
        // Do something risky
        $result = $this->performAction();
        $this->log('info', 'Action completed', ['result' => $result]);
    } catch (\Exception $e) {
        $this->log('error', 'Action failed', [
            'error' => $e->getMessage(),
            'trace' => $e->getTraceAsString()
        ]);
        throw $e; // Re-throw to mark job as failed
    }
}

Error Handling

Jobs should handle errors gracefully and provide meaningful error messages:

public function handle(): void
{
    $pageId = $this->payload()['pageId'];
    
    $page = kirby()->page($pageId);
    if (!$page) {
        throw new \RuntimeException("Page not found: {$pageId}");
    }
    
    try {
        $this->processPage($page);
    } catch (\Exception $e) {
        // Log the error with context
        $this->log('error', 'Failed to process page', [
            'pageId' => $pageId,
            'error' => $e->getMessage()
        ]);
        
        // Decide whether to retry
        if ($e instanceof \RuntimeException) {
            throw $e; // Will retry
        }
        
        // Don't retry for logic errors
        $this->failed($e);
        return;
    }
}

Best Practices

Keep Jobs Focused

Each job should do one thing well. Instead of a massive ProcessOrder job, break it down:

// Good: Focused jobs
queue(ValidateOrderData::class, ['orderId' => $id]);
queue(ChargePayment::class, ['orderId' => $id]);
queue(SendOrderConfirmation::class, ['orderId' => $id]);
queue(NotifyWarehouse::class, ['orderId' => $id]);

// Bad: Monolithic job
queue(ProcessEverythingAboutOrder::class, ['orderId' => $id]);

Make Jobs Idempotent

Jobs should be safe to run multiple times with the same payload:

public function handle(): void
{
    $importId = $this->payload()['importId'];
    
    // Check if already processed
    if (kirby()->cache('imports')->get($importId)) {
        $this->log('info', 'Import already processed, skipping');
        return;
    }
    
    // Process import...
    
    // Mark as processed
    kirby()->cache('imports')->set($importId, true, 60 * 24 * 7);
}

Handle State Changes Carefully

When modifying content, always verify the current state:

public function handle(): void
{
    $page = kirby()->page($this->payload()['pageId']);
    
    // Don't assume the page still exists
    if (!$page) {
        $this->log('warning', 'Page no longer exists');
        return;
    }
    
    // Don't assume status hasn't changed
    if ($page->status() !== 'draft') {
        $this->log('info', 'Page already published');
        return;
    }
    
    kirby()->impersonate('kirby', fn() => $page->changeStatus('listed'));
}

Next Steps

Now that you understand how to create powerful background jobs, learn how to schedule recurring tasks to automate routine operations in your Kirby application.