From cfac4d06cdf2e7f573a8bdab955727695790324a Mon Sep 17 00:00:00 2001 From: CreareWorks Date: Tue, 25 Nov 2025 08:51:26 +0900 Subject: [PATCH] [12.x] Fix CallQueuedClosure::displayName after batch chain (#57597) This fixes a fatal error (`Call to undefined method Closure::getClosure()`) when a Closure is placed immediately after a Bus::batch call within a Bus::chain. The issue arose because the Closure object was passed as a raw `\Closure` instance to `CallQueuedClosure::displayName`, which incorrectly assumed it was always wrapped in a `SerializableClosure` and tried to call `getClosure()`. This patch adds a check to safely extract the underlying Closure, preventing the crash. Fixes #57597 --- src/Illuminate/Queue/CallQueuedClosure.php | 6 ++- tests/Bus/BusBatchTest.php | 62 ++++++++++++++++++++++ 2 files changed, 67 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/CallQueuedClosure.php b/src/Illuminate/Queue/CallQueuedClosure.php index 34bdf3b85796..bc48fdb2f448 100644 --- a/src/Illuminate/Queue/CallQueuedClosure.php +++ b/src/Illuminate/Queue/CallQueuedClosure.php @@ -110,7 +110,11 @@ public function failed($e) */ public function displayName() { - $reflection = new ReflectionFunction($this->closure->getClosure()); + $closure = $this->closure instanceof SerializableClosure + ? $this->closure->getClosure() + : $this->closure; + + $reflection = new ReflectionFunction($closure); $prefix = is_null($this->name) ? '' : "{$this->name} - "; diff --git a/tests/Bus/BusBatchTest.php b/tests/Bus/BusBatchTest.php index 22545db0ddb2..ba5f1d805571 100644 --- a/tests/Bus/BusBatchTest.php +++ b/tests/Bus/BusBatchTest.php @@ -7,9 +7,11 @@ use Illuminate\Bus\Batchable; use Illuminate\Bus\BatchFactory; use Illuminate\Bus\DatabaseBatchRepository; +use Illuminate\Bus\Dispatcher; use Illuminate\Bus\PendingBatch; use Illuminate\Bus\Queueable; use Illuminate\Container\Container; +use Illuminate\Contracts\Bus\Dispatcher as BusDispatcher; use Illuminate\Contracts\Queue\Factory; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Database\Capsule\Manager as DB; @@ -17,7 +19,11 @@ use Illuminate\Database\PostgresConnection; use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Foundation\Bus\PendingChain; use Illuminate\Queue\CallQueuedClosure; +use Illuminate\Support\Facades\Bus; +use Illuminate\Support\Facades\Facade; +use Illuminate\Support\Facades\Queue; use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; @@ -38,6 +44,35 @@ protected function setUp(): void $db->bootEloquent(); $db->setAsGlobal(); + if (! Facade::getFacadeApplication()) { + $container = new Container; + Facade::setFacadeApplication($container); + + $queue = m::mock(Factory::class); + $container->instance(Factory::class, $queue); + $container->alias(Factory::class, 'queue'); + + $dispatcher = m::mock(Dispatcher::class, [$container]); + + $dispatcher->shouldReceive('batch')->zeroOrMoreTimes()->andReturnUsing(function ($jobs) { + $pendingBatch = m::mock(PendingBatch::class); + $pendingBatch->shouldReceive('name')->andReturnSelf(); + $pendingBatch->shouldReceive('dispatch')->zeroOrMoreTimes()->andReturn(m::mock(Batch::class)); + + return $pendingBatch; + })->byDefault(); + + $dispatcher->shouldReceive('chain')->zeroOrMoreTimes()->andReturnUsing(function ($jobs) { + $pendingChain = m::mock(PendingChain::class, [$jobs, \stdClass::class]); + $pendingChain->shouldReceive('dispatch')->zeroOrMoreTimes()->andReturn(m::mock(Batch::class)); + + return $pendingChain; + })->byDefault(); + + $container->instance(BusDispatcher::class, $dispatcher); + $container->alias(BusDispatcher::class, 'bus'); + } + $this->createSchema(); $_SERVER['__finally.count'] = 0; @@ -74,6 +109,10 @@ public function createSchema() */ protected function tearDown(): void { + if (Facade::getFacadeApplication()) { + Facade::setFacadeApplication(null); + } + unset($_SERVER['__finally.batch'], $_SERVER['__progress.batch'], $_SERVER['__then.batch'], $_SERVER['__catch.batch'], $_SERVER['__catch.exception']); $this->schema()->drop('job_batches'); @@ -456,6 +495,29 @@ public function test_chain_can_be_added_to_batch() $this->assertInstanceOf(CarbonImmutable::class, $batch->createdAt); } + public function test_chained_closure_after_multiple_batches_is_properly_dispatched() + { + Queue::fake(); + + $TestBatchJob = new class + { + use Batchable; + + public function handle() + { + } + }; + + Bus::chain([ + Bus::batch([$TestBatchJob])->name('Batch 1'), + Bus::batch([$TestBatchJob])->name('Batch 2'), + function () { + }, + ])->dispatch(); + + $this->assertTrue(true); + } + public function test_options_serialization_on_postgres() { $pendingBatch = (new PendingBatch(new Container, collect()))