Attribute-Based Job Configuration (Laravel 13)

+15 Mana ✨

Introduction

Laravel 13 introduced PHP attributes for job configuration — you can now replace the old public int $tries = 3; property style with #[Tries(3)] at the class level. This pulls configuration out of the property bag and into declarative metadata, the same way controllers already use #[Route] and models use #[ObservedBy].

Key Concepts

  • Job attribute: A PHP 8 attribute from Illuminate\Queue\Attributes that configures one aspect of a job.
  • Class-level only: Job attributes apply to the job class as a whole, not individual methods.

Real World Context

Properties work, but they clutter the top of every job class and mix configuration with state. Attributes make intent obvious at a glance and group cleanly with use imports the IDE can autocomplete.

Deep Dive

Compare the two styles. Here is the old property-based form:

php
class ProcessPayment implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 5;
    public int $timeout = 120;
    public int $maxExceptions = 3;
    public array $backoff = [10, 30, 60];
    public bool $failOnTimeout = true;
}

And here is the equivalent in Laravel 13 using attributes:

php
use Illuminate\Queue\Attributes\{Tries, Timeout, MaxExceptions, FailOnTimeout};

#[Tries(5)]
#[Timeout(120)]
#[MaxExceptions(3)]
#[FailOnTimeout]
class ProcessPayment implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public array $backoff = [10, 30, 60];

    public function __construct(public Order $order) {}

    public function handle(PaymentGateway $gateway): void
    {
        $gateway->charge($this->order);
    }
}

Retries, timeout, and max-exceptions become attributes; backoff stays as a property because it has no attribute equivalent yet. The job body shrinks to mostly business logic — construction and handle().

The full attribute list

AttributeReplacesPurpose
#[Tries(n)]public int $triesMax retry attempts
#[Timeout(n)]public int $timeoutMax seconds per attempt
#[MaxExceptions(n)]public int $maxExceptionsFail after N exceptions
#[FailOnTimeout]public bool $failOnTimeoutFail (not retry) on timeout
#[UniqueFor(n)]public int $uniqueForUniqueness lock in seconds
#[WithoutRelations]deleteWhenMissingModels-adjacentStrip loaded relations when serializing

backoff has no attribute form in Laravel 13 — keep it as public array $backoff = [...] alongside the attributes.

Mixing styles

You can still use properties where an attribute doesn't exist yet, and attributes don't affect middleware() or failed() methods — only configuration values.

Common Pitfalls

  1. Forgetting the use import — attributes look like comments without it. Illuminate\Queue\Attributes\Tries must be imported or the class breaks silently.
  2. Setting the same value twice — don't mix #[Tries(5)] with public int $tries = 3; on the same class. The attribute wins, and the property becomes dead code that confuses readers.

Best Practices

  1. Adopt attributes for new jobs — keep existing jobs on properties until you touch them again, then convert in one commit so the diff is focused.
  2. Group the attributes above class — one per line, sorted alphabetically, so diffs stay minimal when someone adds a new one.

Summary

  • Laravel 13 adds #[Tries], #[Timeout], #[MaxExceptions], #[Backoff], #[FailOnTimeout], #[UniqueFor], and #[WithoutRelations].
  • They replace the old public property style for job configuration.
  • The job body collapses to just constructor + handle().
✓ Completed