Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions src/Illuminate/View/Compilers/Concerns/CompilesJson.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<?php echo json_encode($parts[0], $options, $depth) ?>";
// Ensure we have at least one argument, default to null if empty
$data = $parts[0] ?? 'null';

return "<?php echo json_encode($data, $options, $depth) ?>";
Copy link
Contributor

@shaedrich shaedrich Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can shorten this to

Suggested change
$parts = $this->parseArguments($this->stripParentheses($expression));
$options = isset($parts[1]) ? trim($parts[1]) : $this->encodingOptions;
$depth = isset($parts[2]) ? trim($parts[2]) : 512;
return "<?php echo json_encode($parts[0], $options, $depth) ?>";
// Ensure we have at least one argument, default to null if empty
$data = $parts[0] ?? 'null';
return "<?php echo json_encode($data, $options, $depth) ?>";
[$data, $options, $depth] = $this->parseArguments($this->stripParentheses($expression)) + ['null', $this->encodingOptions, 512];
return '<?php echo json_encode(' . $data . ', ' . trim($options) . ', ' . trim($depth) .') ?>";

or

Suggested change
$parts = $this->parseArguments($this->stripParentheses($expression));
$options = isset($parts[1]) ? trim($parts[1]) : $this->encodingOptions;
$depth = isset($parts[2]) ? trim($parts[2]) : 512;
return "<?php echo json_encode($parts[0], $options, $depth) ?>";
// Ensure we have at least one argument, default to null if empty
$data = $parts[0] ?? 'null';
return "<?php echo json_encode($data, $options, $depth) ?>";
[$data, $options, $depth] = $this->parseArguments($this->stripParentheses($expression)) + ['null', $this->encodingOptions, 512];
return sprintf('<?php echo json_encode(%s, %d, %d); ?>', $data, trim($options), trim($depth));

if you want

}

/**
* 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can narrow this further down:

Suggested change
* @return array
* @return array{0?: string, 1?: string, 2?: string}

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you're welcome!

*/
protected function parseArguments($expression)
{
if ('' === trim($expression)) {
return [];
}

$tokens = @token_get_all('<?php '.$expression);

if (false === $tokens) {
// Fallback to simple explode if tokenization fails
return array_map('trim', explode(',', $expression));
}

$parts = [];
$current = '';
$depth = 0;

foreach ($tokens as $index => $token) {
// Skip the initial <?php token
if (0 === $index && is_array($token) && T_OPEN_TAG === $token[0]) {
continue;
}

if (is_array($token)) {
[$id, $text] = $token;

// Handle strings - preserve them completely
if (in_array($id, [T_CONSTANT_ENCAPSED_STRING, T_ENCAPSED_AND_WHITESPACE])) {
$current .= $text;
continue;
}

// Handle whitespace at top level when $current is empty (can be ignored)
if (T_WHITESPACE === $id && 0 === $depth && '' === $current) {
continue;
}

$current .= $text;
} else {
$char = $token;

// Track nesting depth for parentheses, brackets, and braces
if ('(' === $char || '[' === $char || '{' === $char) {
$depth++;
$current .= $char;
} elseif (')' === $char || ']' === $char || '}' === $char) {
$depth--;
$current .= $char;
} elseif (',' === $char && 0 === $depth) {
// Only split on commas at the top level
$parts[] = trim($current);
$current = '';
} else {
$current .= $char;
}
}
}

// Add the last part
if ('' !== $current) {
$parts[] = trim($current);
}

return $parts;
}
}
49 changes: 49 additions & 0 deletions tests/View/Blade/BladeJsonTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,53 @@ public function testEncodingOptionsCanBeOverwritten()

$this->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 = '<?php echo json_encode([\'items\' => 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 = '<?php echo json_encode([\'a\' => 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 = '<?php echo json_encode($items->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 = '<?php echo json_encode($data, JSON_PRETTY_PRINT, 256) ?>';

$this->assertEquals($expected, $this->compiler->compileString($string));
}

public function testJsonWithEmptyExpressionDefaultsToNull()
{
$string = '@json()';
$expected = '<?php echo json_encode(null, 15, 512) ?>';

$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 = '<?php echo json_encode([\'items\' => $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));
}
}
Loading