diff --git a/src/Illuminate/Bus/ChainedBatch.php b/src/Illuminate/Bus/ChainedBatch.php index 6d4f92107400..903c6684599f 100644 --- a/src/Illuminate/Bus/ChainedBatch.php +++ b/src/Illuminate/Bus/ChainedBatch.php @@ -72,7 +72,19 @@ public static function prepareNestedBatches(Collection $jobs): Collection */ public function handle() { - $batch = new PendingBatch(Container::getInstance(), $this->jobs); + $this->attachRemainderOfChainToEndOfBatch( + $this->toPendingBatch() + )->dispatch(); + } + + /** + * Convert the chained batch instance into a pending batch. + * + * @return \Illuminate\Bus\PendingBatch + */ + public function toPendingBatch() + { + $batch = Container::getInstance()->make(Dispatcher::class)->batch($this->jobs); $batch->name = $this->name; $batch->options = $this->options; @@ -85,8 +97,6 @@ public function handle() $batch->onConnection($this->connection); } - $this->dispatchRemainderOfChainAfterBatch($batch); - foreach ($this->chainCatchCallbacks ?? [] as $callback) { $batch->catch(function (Batch $batch, ?Throwable $exception) use ($callback) { if (! $batch->allowsFailures()) { @@ -95,16 +105,16 @@ public function handle() }); } - $batch->dispatch(); + return $batch; } /** * Move the remainder of the chain to a "finally" batch callback. * * @param \Illuminate\Bus\PendingBatch $batch - * @return + * @return \Illuminate\Bus\PendingBatch */ - protected function dispatchRemainderOfChainAfterBatch(PendingBatch $batch) + protected function attachRemainderOfChainToEndOfBatch(PendingBatch $batch) { if (! empty($this->chained)) { $next = unserialize(array_shift($this->chained)); @@ -126,5 +136,7 @@ protected function dispatchRemainderOfChainAfterBatch(PendingBatch $batch) $this->chained = []; } + + return $batch; } } diff --git a/src/Illuminate/Support/Testing/Fakes/BusFake.php b/src/Illuminate/Support/Testing/Fakes/BusFake.php index fb6209f01fdc..3d717eae6231 100644 --- a/src/Illuminate/Support/Testing/Fakes/BusFake.php +++ b/src/Illuminate/Support/Testing/Fakes/BusFake.php @@ -334,6 +334,12 @@ public function assertChained(array $expectedChain) if ($command instanceof Closure) { [$command, $callback] = [$this->firstClosureParameterType($command), $command]; + } elseif ($command instanceof ChainedBatchTruthTest) { + $instance = $command; + + $command = ChainedBatch::class; + + $callback = fn ($job) => $instance($job->toPendingBatch()); } elseif (! is_string($command)) { $instance = $command; @@ -349,9 +355,7 @@ public function assertChained(array $expectedChain) "The expected [{$command}] job was not dispatched." ); - $this->isChainOfObjects($expectedChain) - ? $this->assertDispatchedWithChainOfObjects($command, $expectedChain, $callback) - : $this->assertDispatchedWithChainOfClasses($command, $expectedChain, $callback); + $this->assertDispatchedWithChainOfObjects($command, $expectedChain, $callback); } /** @@ -388,7 +392,7 @@ public function assertDispatchedWithoutChain($command, $callback = null) "The expected [{$command}] job was not dispatched." ); - $this->assertDispatchedWithChainOfClasses($command, [], $callback); + $this->assertDispatchedWithChainOfObjects($command, [], $callback); } /** @@ -401,48 +405,46 @@ public function assertDispatchedWithoutChain($command, $callback = null) */ protected function assertDispatchedWithChainOfObjects($command, $expectedChain, $callback) { - $chain = collect($expectedChain)->map(fn ($job) => serialize($job))->all(); + $chain = $expectedChain; PHPUnit::assertTrue( - $this->dispatched($command, $callback)->filter( - fn ($job) => $job->chained == $chain - )->isNotEmpty(), + $this->dispatched($command, $callback)->filter(function ($job) use ($chain) { + if (count($chain) !== count($job->chained)) { + return false; + } + + foreach ($job->chained as $index => $serializedChainedJob) { + if ($chain[$index] instanceof ChainedBatchTruthTest) { + $chainedBatch = unserialize($serializedChainedJob); + + if (! $chainedBatch instanceof ChainedBatch || + ! $chain[$index]($chainedBatch->toPendingBatch())) { + return false; + } + } elseif (is_string($chain[$index])) { + if ($chain[$index] != get_class(unserialize($serializedChainedJob))) { + return false; + } + } elseif (serialize($chain[$index]) != $serializedChainedJob) { + return false; + } + } + + return true; + })->isNotEmpty(), 'The expected chain was not dispatched.' ); } /** - * Assert if a job was dispatched with chained jobs based on a truth-test callback. + * Create a new assertion about a chained batch. * - * @param string $command - * @param array $expectedChain - * @param callable|null $callback - * @return void - */ - protected function assertDispatchedWithChainOfClasses($command, $expectedChain, $callback) - { - $matching = $this->dispatched($command, $callback)->map->chained->map(function ($chain) { - return collect($chain)->map( - fn ($job) => get_class(unserialize($job)) - ); - })->filter( - fn ($chain) => $chain->all() === $expectedChain - ); - - PHPUnit::assertTrue( - $matching->isNotEmpty(), 'The expected chain was not dispatched.' - ); - } - - /** - * Determine if the given chain is entirely composed of objects. - * - * @param array $chain - * @return bool + * @param \Closure $callback + * @return \Illuminate\Support\Testing\Fakes\ChainedBatchTruthTest */ - protected function isChainOfObjects($chain) + public function chainedBatch(Closure $callback) { - return ! collect($chain)->contains(fn ($job) => ! is_object($job)); + return new ChainedBatchTruthTest($callback); } /** diff --git a/src/Illuminate/Support/Testing/Fakes/ChainedBatchTruthTest.php b/src/Illuminate/Support/Testing/Fakes/ChainedBatchTruthTest.php new file mode 100644 index 000000000000..4d1cec732ed3 --- /dev/null +++ b/src/Illuminate/Support/Testing/Fakes/ChainedBatchTruthTest.php @@ -0,0 +1,37 @@ +callback = $callback; + } + + /** + * Invoke the truth test with the given pending batch. + * + * @param \Illuminate\Bus\PendingBatch + * @return bool + */ + public function __invoke($pendingBatch) + { + return call_user_func($this->callback, $pendingBatch); + } +} diff --git a/tests/Support/SupportTestingBusFakeTest.php b/tests/Support/SupportTestingBusFakeTest.php index 9377070f0212..b1d6d8bfd952 100644 --- a/tests/Support/SupportTestingBusFakeTest.php +++ b/tests/Support/SupportTestingBusFakeTest.php @@ -4,6 +4,8 @@ use Illuminate\Bus\Batch; use Illuminate\Bus\Queueable; +use Illuminate\Container\Container; +use Illuminate\Contracts\Bus\Dispatcher; use Illuminate\Contracts\Bus\QueueingDispatcher; use Illuminate\Support\Testing\Fakes\BatchRepositoryFake; use Illuminate\Support\Testing\Fakes\BusFake; @@ -468,6 +470,10 @@ public function testAssertNothingDispatched() public function testAssertChained() { + Container::setInstance($container = new Container); + + $container->instance(Dispatcher::class, $this->fake); + $this->fake->chain([ new ChainedJobStub, ])->dispatch(); @@ -485,6 +491,50 @@ public function testAssertChained() ChainedJobStub::class, OtherBusJobStub::class, ]); + + $this->fake->chain([ + new ChainedJobStub, + $this->fake->batch([ + new OtherBusJobStub, + new OtherBusJobStub, + ]), + new ChainedJobStub, + ])->dispatch(); + + $this->fake->assertChained([ + ChainedJobStub::class, + $this->fake->chainedBatch(function ($pendingBatch) { + return $pendingBatch->jobs->count() === 2; + }), + ChainedJobStub::class, + ]); + + $this->fake->assertChained([ + new ChainedJobStub, + $this->fake->chainedBatch(function ($pendingBatch) { + return $pendingBatch->jobs->count() === 2; + }), + new ChainedJobStub, + ]); + + $this->fake->chain([ + $this->fake->batch([ + new OtherBusJobStub, + new OtherBusJobStub, + ]), + new ChainedJobStub, + new ChainedJobStub, + ])->dispatch(); + + $this->fake->assertChained([ + $this->fake->chainedBatch(function ($pendingBatch) { + return $pendingBatch->jobs->count() === 2; + }), + ChainedJobStub::class, + ChainedJobStub::class, + ]); + + Container::setInstance(null); } public function testAssertDispatchedWithIgnoreClass()