Skip to content

[13.x] [bug] Fix closure in chain not dispatched after batch completes#59491

Draft
JoshSalway wants to merge 3 commits intolaravel:13.xfrom
JoshSalway:fix/chained-batch-closure-serialization
Draft

[13.x] [bug] Fix closure in chain not dispatched after batch completes#59491
JoshSalway wants to merge 3 commits intolaravel:13.xfrom
JoshSalway:fix/chained-batch-closure-serialization

Conversation

@JoshSalway
Copy link
Copy Markdown
Contributor

@JoshSalway JoshSalway commented Apr 1, 2026

Summary

Fixes a bug where a closure in Bus::chain is not dispatched when it follows a Bus::batch, throwing Call to undefined method Closure::getClosure().

Relates to laravel/serializable-closure#106

Root cause

The root cause is in serializable-closure's Native::__unserialize(). When deserializing a closure that captures an object with a SerializableClosure property, the objects restoration loop unconditionally calls getClosure() on all stored objects, including SerializableClosure instances that should be preserved as-is. This unwraps them to raw Closure objects, breaking any code that later calls getClosure() on the property.

PR laravel/serializable-closure#135 fixes this at the library level with an instanceof check before calling getClosure().

This PR (framework-level alternative)

If the serializable-closure fix is not accepted, this PR provides a framework-level workaround specific to ChainedBatch. It re-serializes $next to a string before capturing it in the finally closure, so the serializable-closure library never traverses into the CallQueuedClosure's properties:

$next = serialize($next);

$batch->finally(function (Batch $batch) use ($next) {
    if (! $batch->cancelled()) {
        Container::getInstance()->make(Dispatcher::class)->dispatch(unserialize($next));
    }
});

Reproduction

Bus::chain([
    new SomeJob,
    Bus::batch([new AnotherJob]),
    fn() => doSomething(), // This closure never runs
])->dispatch();
// Call to undefined method Closure::getClosure()

Test plan

  • Integration test: testClosureAfterBatchInChainIsDispatched in JobChainingTest.php

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.
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 1, 2026

Thanks for submitting a PR!

Note that draft PRs are not reviewed. If you would like a review, please mark your pull request as ready for review in the GitHub user interface.

Pull requests that are abandoned in draft may be closed due to inactivity.

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.
@JoshSalway JoshSalway changed the title [13.x] Fix closure in chain not dispatched after batch completes [13.x] [bug] Fix closure in chain not dispatched after batch completes Apr 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant