laravel/framework: [13.x] Fix inherited queue attributes being ignored when the child uses Queueable
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:
redisconnection,importsqueue, 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
nullproperty on the child — that never really worked as an opt-out. Set a concrete value or don't inherit from the annotated base.
Takeaways
nullproperties no longer override inherited queue attributes; only non-null values do.- Child jobs using
Queueablenow 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.