From 63366df48a9e0ac171c9e613ced2f3a6ae0f6a1a Mon Sep 17 00:00:00 2001 From: Caleb Porzio Date: Sat, 4 Jul 2026 08:07:13 -0400 Subject: [PATCH] Exclude Blaze scope variables from Volt fragment arguments Volt compiles @volt fragments (used in Folio pages) into a Livewire mount call that captures the surrounding template scope via get_defined_vars(). Blaze introduces variables into that scope (the $__blaze runtime plus call-site temporaries like $__attrs{hash} and $__slots{hash}) that Volt then forwards to Livewire as component arguments. Livewire stores the unrecognized arguments in the snapshot memo, and the serialized objects don't survive the JSON round trip on the next request, corrupting the snapshot checksum and throwing CorruptComponentPayloadException on any interaction. Fixes #187 Co-Authored-By: Claude Fable 5 --- src/BlazeManager.php | 24 ++++++++++++++++++++++++ src/BlazeServiceProvider.php | 2 ++ src/Support/Utils.php | 12 ++++++++++++ tests/BlazeManagerTest.php | 18 ++++++++++++++++++ tests/Support/UtilsTest.php | 23 +++++++++++++++++++++++ 5 files changed, 79 insertions(+) create mode 100644 tests/Support/UtilsTest.php diff --git a/src/BlazeManager.php b/src/BlazeManager.php index eef12d80..b77ebca9 100644 --- a/src/BlazeManager.php +++ b/src/BlazeManager.php @@ -165,6 +165,30 @@ public function compileForUnblaze(string $template): string return $output; } + /** + * Exclude Blaze's internal variables from Volt fragment component arguments. + * + * Volt compiles @volt fragments (used in Laravel Folio pages) into a Livewire + * mount call that captures the surrounding template scope via get_defined_vars(). + * Blaze introduces variables into that scope ($__blaze and call-site temporaries) + * that would otherwise be forwarded to Livewire, serialized into the component + * snapshot, and corrupt its checksum on subsequent requests. + */ + public function excludeBlazeVariablesFromVoltFragments(string $template): string + { + $needle = 'ExtractFragments::componentArguments([...get_defined_vars()'; + + if (! str_contains($template, $needle)) { + return $template; + } + + return str_replace( + $needle, + 'ExtractFragments::componentArguments([...\Livewire\Blaze\Support\Utils::exceptBlazeVariables(get_defined_vars())', + $template, + ); + } + /** * Compile a template for debug-only mode (Blaze disabled). * diff --git a/src/BlazeServiceProvider.php b/src/BlazeServiceProvider.php index 10b8d9a2..4c109a85 100644 --- a/src/BlazeServiceProvider.php +++ b/src/BlazeServiceProvider.php @@ -156,6 +156,8 @@ protected function interceptBladeCompilation(): void return $input; } + $input = $blaze->excludeBlazeVariablesFromVoltFragments($input); + if ($blaze->isDisabled()) { if ($blaze->isDebugging()) { return $blaze->compileForDebug($input, $path); diff --git a/src/Support/Utils.php b/src/Support/Utils.php index 266eab92..36d2bd38 100644 --- a/src/Support/Utils.php +++ b/src/Support/Utils.php @@ -35,4 +35,16 @@ public static function hash(string $componentPath): string { return hash('xxh128', 'v2' . $componentPath); } + + /** + * Remove Blaze's internal template-scope variables (the shared runtime and + * compiled call-site temporaries) from a get_defined_vars() capture. + */ + public static function exceptBlazeVariables(array $variables): array + { + return array_filter($variables, function ($key) { + return ! str_starts_with($key, '__blaze') + && ! preg_match('/^__(?:attrs|slots)(?:Stack)?[0-9a-f]{32}$/', $key); + }, ARRAY_FILTER_USE_KEY); + } } diff --git a/tests/BlazeManagerTest.php b/tests/BlazeManagerTest.php index 52a5947d..0cb2c682 100644 --- a/tests/BlazeManagerTest.php +++ b/tests/BlazeManagerTest.php @@ -54,3 +54,21 @@ expect($manager->viewContainsExpiredFrontMatter($view))->toBeFalse(); }); + +test('blaze variables are excluded from volt fragment component arguments', function () { + // Volt compiles @volt fragments into a Livewire mount call that captures the + // entire template scope via get_defined_vars(). Blaze's runtime and call-site + // temporaries must be filtered out of that capture, otherwise they end up + // serialized into the Livewire snapshot and corrupt its checksum... + $input = '@livewire("volt-anonymous-fragment-abc123", Livewire\Volt\Precompilers\ExtractFragments::componentArguments([...get_defined_vars(), ...array()]))'; + + $compiled = app('blade.compiler')->compileString($input); + + expect($compiled)->toContain('ExtractFragments::componentArguments([...\Livewire\Blaze\Support\Utils::exceptBlazeVariables(get_defined_vars()), ...array()])'); +}); + +test('get_defined_vars is left untouched outside volt fragment component arguments', function () { + $input = ''; + + expect(Blaze::excludeBlazeVariablesFromVoltFragments($input))->toBe($input); +}); diff --git a/tests/Support/UtilsTest.php b/tests/Support/UtilsTest.php new file mode 100644 index 00000000..bbe7d39b --- /dev/null +++ b/tests/Support/UtilsTest.php @@ -0,0 +1,23 @@ + 'Hello', + 'count' => 1, + '__blaze' => new stdClass, + '__blazeViewName' => 'layout', + '__attrs'.$hash => ['title' => 'Test'], + '__slots'.$hash => [], + '__attrsStack'.$hash => [], + '__slotsStack'.$hash => [], + ]; + + expect(Utils::exceptBlazeVariables($variables))->toBe([ + 'message' => 'Hello', + 'count' => 1, + ]); +});