Skip to content

Fix serialization of multiple closures on the same line#120

Merged
taylorotwell merged 4 commits intolaravel:2.xfrom
michaelruelas:fix-same-line-closures
Jan 8, 2026
Merged

Fix serialization of multiple closures on the same line#120
taylorotwell merged 4 commits intolaravel:2.xfrom
michaelruelas:fix-same-line-closures

Conversation

@michaelruelas
Copy link
Copy Markdown
Contributor

@michaelruelas michaelruelas commented Jan 7, 2026

Closes #119

Description

This pull request addresses an issue where SerializableClosure would incorrectly serialize the first closure found on a line when multiple closures were defined on that same line (e.g., arrow functions in an array or passed as arguments).

Previously, the ReflectionClosure::getCode() logic would greedily return the first fn or function definition it encountered on the source line. This PR introduces a robust candidate selection mechanism:

  1. It collects all possible closure definitions found on the matching lines.
  2. It validates each candidate against the actual ReflectionFunction properties (parent class of ReflectionClosure) such as:
    • Static status (static fn vs fn).
    • Parameter count.
    • Captured variables (uses a sub-tokenization pass on the candidate code to ensure local variables and captured variables match the reflection data).
  3. It selects the first candidate that matches all these criteria.

Benefit to End Users

This fix allows users to more reliably use arrow functions with Laravel's concurrency and background processing features (like Concurrency::run()), where short closures are often defined inline on a single line. It removes a subtle data corruption bug where the wrong closure logic would be executed after deserialization.

Why it doesn't break existing features

The implementation purely adds disambiguation logic to the code extraction phase. It follows existing parser patterns and adds a secondary validation step using official Reflection metadata. Existing unit tests (331 total) all pass, and the new disambiguation logic only triggers when the reflection data confirms a match.

Tests

New tests have been added in tests/MultipleClosuresOnSameLineTest.php covering scenarios with different arguments, different static variables, and static/non-static mixtures.

@taylorotwell taylorotwell merged commit 48328ed into laravel:2.x Jan 8, 2026
11 of 12 checks passed
@michaelruelas michaelruelas deleted the fix-same-line-closures branch January 8, 2026 17:03
@Niush
Copy link
Copy Markdown

Niush commented Jan 8, 2026

Hi @michaelruelas,
I had opened the original issue, and just tested it locally.

These work nicely:

$a = "a";
$b = "b";
$correct = Concurrency::driver('process')->run([fn () => $a, fn() => $b]);
$correct = Concurrency::driver('process')->run([fn () => "a", static fn() => "b"]);

// ["a", "b"]

But these are still not working:

$incorrect = Concurrency::driver('process')->run([fn () => "a", fn() => "b"]);
$incorrect = Concurrency::driver('process')->run([function () { return "a"; }, function () { return "b"; }]);
$incorrect = Concurrency::driver('process')->run([fn () => "a", function () { return "b"; }]);

// ["a", "a"]

Is this a limitation of this PR?

By the way, if I make one normal fn() and another static fn() it does work.

d8y added a commit to d8y/serializable-closure that referenced this pull request Apr 4, 2026
When a closure (e.g. `static function()`) is returned from an arrow
function on the same line (`fn() => static function() { ... }`), the
token parser incorrectly captures the outer arrow function instead of
the inner closure. After deserialization the closure becomes a silent
no-op — returning a Closure object instead of executing the intended
logic, with no errors or failed jobs.

This happens because the inner closure is absorbed into the arrow
function's body during parsing and never collected as a separate
candidate. The existing candidate selection (PR laravel#120) only handles
sibling closures on the same line, not nested closures.

The fix adds an `extractNestedClosure()` method that, for short closure
candidates, extracts the inner function/closure from after the `=>`
operator and verifies it against the reflected closure's signature.
d8y added a commit to d8y/serializable-closure that referenced this pull request Apr 4, 2026
When a closure (e.g. `static function()`) is returned from an arrow
function on the same line (`fn() => static function() { ... }`), the
token parser incorrectly captures the outer arrow function instead of
the inner closure. After deserialization the closure becomes a silent
no-op — returning a Closure object instead of executing the intended
logic, with no errors or failed jobs.

This happens because the inner closure is absorbed into the arrow
function's body during parsing and never collected as a separate
candidate. The existing candidate selection (PR laravel#120) only handles
sibling closures on the same line, not nested closures.

The fix adds an `extractNestedClosure()` method that, for short closure
candidates, extracts the inner function/closure from after the `=>`
operator and verifies it against the reflected closure's signature.
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.

Concurrency, Arrow Function and SerializableClosure Issues

3 participants