From 99474d693d9c9656953f1321c2a8ddf3cccc18c1 Mon Sep 17 00:00:00 2001 From: Elliot Anderson Date: Wed, 10 Dec 2025 16:11:29 -0500 Subject: [PATCH 1/5] Fix @json Blade directive parsing with nested structures Replaces `explode()` with tokenizer-based parser that respects nested arrays, closures, and function calls. Only splits on commas at the top level, preserving commas inside nested structure. Example of previously broken case: @json([$items->map(fn($x) => ['a' => $x, 'b' => $x]), 'key' => 'value']) This fix is backwards compatible - all existing @json usage continues to work as before. The change only fixes previously broken cases with complex nested structures. Fixes #56331 --- .../View/Compilers/Concerns/CompilesJson.php | 82 ++++++++++++++++++- tests/View/Blade/BladeJsonTest.php | 49 +++++++++++ 2 files changed, 129 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesJson.php b/src/Illuminate/View/Compilers/Concerns/CompilesJson.php index cf343e972c10..d1c3e60d2944 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesJson.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesJson.php @@ -19,12 +19,90 @@ trait CompilesJson */ protected function compileJson($expression) { - $parts = explode(',', $this->stripParentheses($expression)); + $parts = $this->parseArguments($this->stripParentheses($expression)); $options = isset($parts[1]) ? trim($parts[1]) : $this->encodingOptions; $depth = isset($parts[2]) ? trim($parts[2]) : 512; - return ""; + // Ensure we have at least one argument, default to null if empty + $data = $parts[0] ?? 'null'; + + return ""; + } + + /** + * Parse arguments from an expression, respecting nested structures. + * + * This method properly handles commas inside arrays, closures, function calls, + * and other nested structures by using PHP's tokenizer. + * + * @param string $expression + * @return array + */ + protected function parseArguments($expression) + { + if (trim($expression) === '') { + return []; + } + + $tokens = @token_get_all(' $token) { + // Skip the initial assertEquals($expected, $this->compiler->compileString($string)); } + + public function testJsonWithComplexArrayContainingNestedStructures() + { + $string = '@json([\'items\' => collect([1, 2, 3])->map(fn($x) => [\'id\' => $x, \'name\' => "test"]), \'translation\' => \'%:booking.benefits%\'])'; + $expected = ' collect([1, 2, 3])->map(fn($x) => [\'id\' => $x, \'name\' => "test"]), \'translation\' => \'%:booking.benefits%\'], 15, 512) ?>'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testJsonWithArrayContainingMultipleCommas() + { + $string = '@json([\'a\' => 1, \'b\' => 2, \'c\' => 3], JSON_PRETTY_PRINT)'; + $expected = ' 1, \'b\' => 2, \'c\' => 3], JSON_PRETTY_PRINT, 512) ?>'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testJsonWithClosureContainingCommas() + { + $string = '@json($items->map(fn($item) => [\'icon\' => $item->icon, \'title\' => (string)$item->title, \'description\' => (string)$item->description]))'; + $expected = 'map(fn($item) => [\'icon\' => $item->icon, \'title\' => (string)$item->title, \'description\' => (string)$item->description]), 15, 512) ?>'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testJsonWithAllThreeArguments() + { + $string = '@json($data, JSON_PRETTY_PRINT, 256)'; + $expected = ''; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testJsonWithEmptyExpressionDefaultsToNull() + { + $string = '@json()'; + $expected = ''; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testJsonWithIssue56331ExactCase() + { + // This is the exact case from GitHub issue #56331 + $string = '@json([\'items\' => $helpers[\'benefit\'][\'getAll\']()->map(fn($item) => [\'icon\' => $item->icon, \'title\' => (string)$item->title, \'description\' => (string)$item->description]), \'translation\' => \'%:booking.benefits%\'])'; + $expected = ' $helpers[\'benefit\'][\'getAll\']()->map(fn($item) => [\'icon\' => $item->icon, \'title\' => (string)$item->title, \'description\' => (string)$item->description]), \'translation\' => \'%:booking.benefits%\'], 15, 512) ?>'; + + $this->assertEquals($expected, $this->compiler->compileString($string)); + } } From 57156a19b16bf63f3bebc65e49625ab7ffe97307 Mon Sep 17 00:00:00 2001 From: Elliot Anderson Date: Wed, 10 Dec 2025 16:31:19 -0500 Subject: [PATCH 2/5] styleci/formatting --- .../View/Compilers/Concerns/CompilesJson.php | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesJson.php b/src/Illuminate/View/Compilers/Concerns/CompilesJson.php index d1c3e60d2944..c90875578683 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesJson.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesJson.php @@ -15,6 +15,7 @@ trait CompilesJson * Compile the JSON statement into valid PHP. * * @param string $expression + * * @return string */ protected function compileJson($expression) @@ -38,17 +39,18 @@ protected function compileJson($expression) * and other nested structures by using PHP's tokenizer. * * @param string $expression + * * @return array */ protected function parseArguments($expression) { - if (trim($expression) === '') { + if ('' === trim($expression)) { return []; } - $tokens = @token_get_all(' $token) { // Skip the initial Date: Wed, 10 Dec 2025 17:37:45 -0500 Subject: [PATCH 3/5] style-ci --- src/Illuminate/View/Compilers/Concerns/CompilesJson.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesJson.php b/src/Illuminate/View/Compilers/Concerns/CompilesJson.php index c90875578683..16e95ebcff89 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesJson.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesJson.php @@ -15,7 +15,6 @@ trait CompilesJson * Compile the JSON statement into valid PHP. * * @param string $expression - * * @return string */ protected function compileJson($expression) @@ -39,7 +38,6 @@ protected function compileJson($expression) * and other nested structures by using PHP's tokenizer. * * @param string $expression - * * @return array */ protected function parseArguments($expression) @@ -48,7 +46,7 @@ protected function parseArguments($expression) return []; } - $tokens = @token_get_all(' Date: Fri, 12 Dec 2025 10:02:01 -0500 Subject: [PATCH 4/5] refactoring `compileJson()` - simplifying `compileJson()` - adds more in depth docblock to `parseArguments()`'s return value --- .../View/Compilers/Concerns/CompilesJson.php | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesJson.php b/src/Illuminate/View/Compilers/Concerns/CompilesJson.php index 16e95ebcff89..92f3cb4f8a24 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesJson.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesJson.php @@ -19,14 +19,7 @@ trait CompilesJson */ protected function compileJson($expression) { - $parts = $this->parseArguments($this->stripParentheses($expression)); - - $options = isset($parts[1]) ? trim($parts[1]) : $this->encodingOptions; - - $depth = isset($parts[2]) ? trim($parts[2]) : 512; - - // Ensure we have at least one argument, default to null if empty - $data = $parts[0] ?? 'null'; + [$data, $options, $depth] = $this->parseArguments($this->stripParenthesis($expression)) + ['null', $this->encodingOptions, 512]; return ""; } @@ -38,7 +31,7 @@ protected function compileJson($expression) * and other nested structures by using PHP's tokenizer. * * @param string $expression - * @return array + * @return array{0?: string, 1?: string, 2?: string} */ protected function parseArguments($expression) { From b70578177f8375c618a51905a26e011b302e3bc7 Mon Sep 17 00:00:00 2001 From: Elliot Anderson Date: Fri, 12 Dec 2025 12:34:13 -0500 Subject: [PATCH 5/5] typo in method name --- src/Illuminate/View/Compilers/Concerns/CompilesJson.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesJson.php b/src/Illuminate/View/Compilers/Concerns/CompilesJson.php index 92f3cb4f8a24..e4c34bc68d4c 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesJson.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesJson.php @@ -19,7 +19,7 @@ trait CompilesJson */ protected function compileJson($expression) { - [$data, $options, $depth] = $this->parseArguments($this->stripParenthesis($expression)) + ['null', $this->encodingOptions, 512]; + [$data, $options, $depth] = $this->parseArguments($this->stripParentheses($expression)) + ['null', $this->encodingOptions, 512]; return ""; }