[2.x] [bug] Fix SerializableClosure as class property being unwrapped during deserialization#135
Merged
taylorotwell merged 1 commit intoApr 3, 2026
Conversation
|
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. |
…apped during deserialization When a class property holds a SerializableClosure or UnsignedSerializableClosure instance, the deserialization in __unserialize() would call getClosure() on it, unwrapping it to a raw Closure. This caused errors like "Call to undefined method Closure::getClosure()" when the property was later used as a SerializableClosure. The fix checks whether the stored object is a SerializableClosure or UnsignedSerializableClosure before deciding whether to unwrap it. Fixes #106 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #106
Also fixes: laravel/framework#57597, laravel/framework#50028
When a class has a
SerializableClosureorUnsignedSerializableClosureinstance as a property value, and that class is captured in another closure'susevariables, deserialization incorrectly unwraps the property to a rawClosureby callinggetClosure()on it. This causes a "Call to undefined method Closure::getClosure()" error when the property is later used as aSerializableClosure.The root cause is in
Native::__unserialize()- theobjectsrestoration loop unconditionally calls$item['object']->getClosure()on every stored object, butSerializableClosureandUnsignedSerializableClosureinstances should be preserved as-is rather than unwrapped.This bug was inherited from opis/closure 3.x (same unconditional
getClosure()call). It went unnoticed because the trigger pattern (SC as a typed class property on a serialized object) only became common with PHP 8.0+ typed properties and features likeChainedBatch.The fix
A simple
instanceofcheck before callinggetClosure():SerializableClosureorUnsignedSerializableClosure, keep it as-isNativeserializer instance), callgetClosure()to unwrap to the rawClosureReal-world issues
This bug has been reported multiple times across different repos and forums since 2021:
Bus::chainclosure afterBus::batchsilently failsBus::batchinsideBus::chainserialization errorAffected packages
Any class that stores a
SerializableClosureas a property and is then captured in another closure's scope during serialization will hit this bug:CallQueuedClosurepublic $closuretyped asSerializableClosurehandle()calls$this->closure->getClosure()which fails after unwraphandle()anddisplayName()patterns on a fresh Laravel 13 appQueueabletraitpublic $deduplicatortyped asSerializableClosure|nullClosureWorkflowStepprivate SerializableClosure $callbackColumn/CanModifyQuerypublic ?SerializableClosuretyped propertiesClosureassigned, violating the type declarationVerification
Tested on a fresh Laravel 13 app against v2.0.10 (before) and this PR (after). Every closure pattern from the Laravel 13.x queue documentation was included to check for regressions. Test code: JoshSalway/sc-135-tests.
Docs pattern tests (18 tests: 6 fixed, 12 already passing)
dispatch(fn() => ...)dispatch(fn())->name(...)dispatch(fn())->catch(fn())Bus::chain([Job, Job, Job])Bus::chain([Job, Job, fn()])Bus::chain([...])->catch(fn())Bus::batch([...])->then()->catch()->finally()Bus::batch([[Job, Job], [Job, Job]])->then()Bus::chain([Job, Bus::batch([...]), Bus::batch([...])])Bus::chain([Job, Bus::batch([...]), fn()])Bus::chain([Bus::batch([...]), fn(), Bus::batch([...])])Bus::chain([Job, Bus::batch([...]), fn()])->catch(fn())*Pass with Bus::fake (no serialization). Fails during actual queue processing.
Package pattern and edge case tests (24 tests: 18 fixed, 6 already passing)
PR #129 regression tests also verified (9/9 pass).
Breakage analysis
$property->getClosure()(framework pattern)Call to undefined method Closure::getClosure()$property instanceof SerializableClosureguardfalse(unwrapped to raw Closure)true(preserved)__invoke/($property)()is_callable($property)No code in the framework or affected packages checks
instanceof \ClosureonSerializableClosuretyped properties, so the only behavior change (property staying asSerializableClosureinstead of being unwrapped to rawClosure) has no downstream impact.Credit
Thanks to @Dry7 for the initial investigation and fix attempt in #86, which identified the same root cause.
Test plan
serializable closure as class property(signed + unsigned serializer datasets)unsigned serializable closure as class property