Skip to content
LaravelBlog

laravel/framework: [13.x] Fix inherited queue attributes being ignored when the child uses Queueable

LaravelBlogBot
LaravelBlogBot

Contributed by xurshudyan

Part of the framework v13.35.0 release

Category: Fix (3) · laravel/framework

PR #61826 fixes a quiet bug in how Laravel resolves queue attributes on inherited job classes. Ever since attribute-based queue configuration landed in #60369, a public property declared on a child class was treated as an override of an attribute on its parent — and because the Queueable trait declares $queue, $connection, and $delay as null, any child job that used the trait (including every job generated by make:job) silently discarded the attributes declared on its parent. This change makes a null property no longer count as an override, so inherited #[Queue], #[Connection], and #[Delay] attributes are finally applied as intended.

Queue attributes, briefly

Laravel lets you declare queueing behavior with PHP attributes instead of — or alongside — plain properties:

use Illuminate\Queue\Attributes\Connection;
use Illuminate\Queue\Attributes\Delay;
use Illuminate\Queue\Attributes\Queue;

#[Queue('imports'), Connection('redis'), Delay(30)]
abstract class ImportJob implements ShouldQueue {}

Attributes shine on abstract base classes: you define shared queue policy once, and concrete jobs inherit it. To keep that flexible, #60369 introduced a precedence rule — a public property on the child class wins over an attribute on the parent, so public $queue = 'invoices'; in a subclass still beats #[Queue('imports')]. Sensible, until it wasn't.

The bug: null counted as an override

Attribute resolution during dispatch goes through the ReadsClassAttributes trait, whose propertyOverridesAttribute() check asks: is there a public, initialized property on the child whose declaring class is a subclass of where the attribute lives? If so, the property wins and the attribute is skipped.

The Queueable trait ships with defaults that match exactly:

trait Queueable
{
    public $connection = null;
    public $queue = null;
    public $delay = null;
    // ...
}

Because use Queueable; declares three public properties on the child, all set to null, the override check matched all three and threw away the parent's attributes. Nothing failed loudly — the job was simply pushed to the default connection and queue with no delay.

What the fix does

The patch adds a single condition to the override check:

return $property->isPublic()
    && $property->isInitialized($target)
    && ! is_null($target->{$property->getName()})
    && $property->getDeclaringClass()->isSubclassOf($attributeDeclaringClass->getName());

In words: a property now only counts as an override when it actually holds a value. null means "nothing declared here," so the parent's attributes flow through untouched. Everything else from #60369 is preserved — explicit child properties with real values still win, and the property still has to be public and initialized.

A regression test was added to BusDispatcherTest covering exactly this shape: an abstract parent annotated with #[Connection('redis')], #[Queue('foo')], and #[Delay(10)], and a child that uses Queueable. The test asserts the dispatch resolves to the redis connection, the foo queue, and a 10-second delay.

Before and after

#[Queue('imports'), Connection('redis'), Delay(30)]
abstract class ImportJob implements ShouldQueue {}

class ImportUsers extends ImportJob
{
    use Queueable;
}

ImportUsers::dispatch();
  • On v13.34.0: default connection, default queue, dispatched immediately — the attributes were ignored.
  • After this fix: redis connection, imports queue, delayed 30 seconds — the attributes apply.

Intentional overrides on the child keep their precedence, unchanged:

class ImportInvoices extends ImportJob
{
    use Queueable;

    public $queue = 'invoices'; // wins over #[Queue('imports')]
}

Upgrade impact

This is a behavior fix, not a breaking API change — no configuration, method signatures, or interfaces are affected. Still, it can change where your jobs land, so before upgrading, audit any hierarchy where a parent class declares queue attributes:

  • Jobs that previously (unknowingly) fell back to the default connection and queue will now honor the parent's attributes. If an attribute points at a connection you don't run workers for — say redis — jobs will queue up but never execute until a worker listens on it.
  • If a child class intentionally relied on the defaults, either set concrete properties on it or move the attributes down to the children that actually need them.
  • You can no longer "clear" a parent attribute by declaring a null property on the child — that never really worked as an opt-out. Set a concrete value or don't inherit from the annotated base.

Takeaways

  • null properties no longer override inherited queue attributes; only non-null values do.
  • Child jobs using Queueable now correctly inherit #[Queue], #[Connection], and #[Delay] from parent classes.
  • Explicit child properties with values keep winning, so intentional overrides are unaffected.
  • After upgrading, verify your job hierarchies — jobs may start landing on the connection, queue, and delay their attributes actually specify.

Sources

More from this release

Related Articles