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, + ]); +});