From 661e52a239167752c0f73f1632fee79e8db5280f Mon Sep 17 00:00:00 2001 From: Josh Salway Date: Thu, 2 Apr 2026 08:51:51 +1000 Subject: [PATCH 1/3] Fix closure in chain not dispatched after batch completes When Bus::chain contains a closure after a Bus::batch, the closure fails with 'Call to undefined method Closure::getClosure()'. This happens because attachRemainderOfChainToEndOfBatch() captures the next chain item (a CallQueuedClosure containing a SerializableClosure) directly in the batch's finally callback. When the batch serializes this callback, the nested SerializableClosure gets unwrapped to a raw Closure during the round-trip. Fix by re-serializing the next chain item to a string before capturing it in the finally closure. The string is deserialized at dispatch time, avoiding the serializable-closure library needing to traverse into the CallQueuedClosure's properties. --- src/Illuminate/Bus/ChainedBatch.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Bus/ChainedBatch.php b/src/Illuminate/Bus/ChainedBatch.php index 7dc1a39ecaab..19e77017a19e 100644 --- a/src/Illuminate/Bus/ChainedBatch.php +++ b/src/Illuminate/Bus/ChainedBatch.php @@ -130,9 +130,11 @@ protected function attachRemainderOfChainToEndOfBatch(PendingBatch $batch) $next->chainQueue = $this->chainQueue; $next->chainCatchCallbacks = $this->chainCatchCallbacks; + $next = serialize($next); + $batch->finally(function (Batch $batch) use ($next) { if (! $batch->cancelled()) { - Container::getInstance()->make(Dispatcher::class)->dispatch($next); + Container::getInstance()->make(Dispatcher::class)->dispatch(unserialize($next)); } }); From 3e7b17424ef73de2893be9f4a546e831c25e6f4c Mon Sep 17 00:00:00 2001 From: Josh Salway Date: Thu, 2 Apr 2026 08:58:27 +1000 Subject: [PATCH 2/3] Add test for closure dispatch after batch in chain Verifies that a closure following a Bus::batch inside Bus::chain is correctly dispatched after the batch completes. This was broken because attachRemainderOfChainToEndOfBatch captured a live CallQueuedClosure in the finally callback, causing the nested SerializableClosure to get unwrapped during serialization. --- tests/Integration/Queue/JobChainingTest.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Integration/Queue/JobChainingTest.php b/tests/Integration/Queue/JobChainingTest.php index 8cf10be0e966..6d142d7fc99a 100644 --- a/tests/Integration/Queue/JobChainingTest.php +++ b/tests/Integration/Queue/JobChainingTest.php @@ -470,6 +470,24 @@ public function testBatchCanBeAddedToChain() $this->assertEquals(['c1', 'c2', 'b1', 'b2', 'b3', 'b4', 'c3'], JobRunRecorder::$results); } + public function testClosureAfterBatchInChainIsDispatched() + { + Bus::chain([ + new JobChainingNamedTestJob('c1'), + Bus::batch([ + new JobChainingTestBatchedJob('b1'), + new JobChainingTestBatchedJob('b2'), + ]), + function () { + JobRunRecorder::record('closure-after-batch'); + }, + ])->dispatch(); + + $this->runQueueWorkerCommand(['--stop-when-empty' => true]); + + $this->assertContains('closure-after-batch', JobRunRecorder::$results); + } + public function testBatchInChainUsesCorrectQueue() { $otherQueue = $this->getQueueDriver() === 'redis' ? '{other}' : 'other'; From ffd4bdfe5241f694a822050502a3c49fadc41458 Mon Sep 17 00:00:00 2001 From: Josh Salway Date: Thu, 2 Apr 2026 09:04:26 +1000 Subject: [PATCH 3/3] retry CI