Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Fixes for Stringable (updated) #37613

Merged
merged 5 commits into from
Jun 7, 2021
Merged
Show file tree
Hide file tree
Changes from all 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
31 changes: 7 additions & 24 deletions src/Illuminate/View/Compilers/BladeCompiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Illuminate\View\Compilers;

use Closure;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\ReflectsClosures;
Expand Down Expand Up @@ -102,13 +101,6 @@ class BladeCompiler extends Compiler implements CompilerInterface
*/
protected $echoFormat = 'e(%s)';

/**
* Custom rendering callbacks for stringable objects.
*
* @var array
*/
public $echoHandlers = [];

/**
* Array of footer lines to be added to the template.
*
Expand Down Expand Up @@ -263,6 +255,13 @@ public function compileString($value)
$result = $this->addFooters($result);
}

// If there are blade echo handlers defined, we will prepend the file
// with a resolved instance of the blade compiler, stored inside a
// variable, so that it only has to be resolved a single time.
if (! empty($this->echoHandlers)) {
$result = $this->addBladeCompilerVariable($result);
}

return str_replace(
['##BEGIN-COMPONENT-CLASS##', '##END-COMPONENT-CLASS##'],
'',
Expand Down Expand Up @@ -711,22 +710,6 @@ public function getCustomDirectives()
return $this->customDirectives;
}

/**
* Add a handler to be executed before echoing a given class.
*
* @param string|callable $class
* @param callable|null $handler
* @return void
*/
public function stringable($class, $handler = null)
{
if ($class instanceof Closure) {
[$class, $handler] = [$this->firstClosureParameterType($class), $class];
}

$this->echoHandlers[$class] = $handler;
}

/**
* Register a new precompiler.
*
Expand Down
64 changes: 57 additions & 7 deletions src/Illuminate/View/Compilers/Concerns/CompilesEchos.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,34 @@

namespace Illuminate\View\Compilers\Concerns;

use Closure;
use Illuminate\Support\Str;

trait CompilesEchos
{
/**
* Custom rendering callbacks for stringable objects.
*
* @var array
*/
protected $echoHandlers = [];

/**
* Add a handler to be executed before echoing a given class.
*
* @param string|callable $class
* @param callable|null $handler
* @return void
*/
public function stringable($class, $handler = null)
{
if ($class instanceof Closure) {
[$class, $handler] = [$this->firstClosureParameterType($class), $class];
}

$this->echoHandlers[$class] = $handler;
}

/**
* Compile Blade echos into valid PHP.
*
Expand Down Expand Up @@ -48,7 +74,7 @@ protected function compileRawEchos($value)

return $matches[1]
? substr($matches[0], 1)
: "<?php echo {$this->applyEchoHandlerFor($matches[2])}; ?>{$whitespace}";
: "<?php echo {$this->wrapInEchoHandler($matches[2])}; ?>{$whitespace}";
};

return preg_replace_callback($pattern, $callback, $value);
Expand All @@ -67,7 +93,7 @@ protected function compileRegularEchos($value)
$callback = function ($matches) {
$whitespace = empty($matches[3]) ? '' : $matches[3].$matches[3];

$wrapped = sprintf($this->echoFormat, $this->applyEchoHandlerFor($matches[2]));
$wrapped = sprintf($this->echoFormat, $this->wrapInEchoHandler($matches[2]));

return $matches[1] ? substr($matches[0], 1) : "<?php echo {$wrapped}; ?>{$whitespace}";
};
Expand All @@ -90,22 +116,46 @@ protected function compileEscapedEchos($value)

return $matches[1]
? $matches[0]
: "<?php echo e({$this->applyEchoHandlerFor($matches[2])}); ?>{$whitespace}";
: "<?php echo e({$this->wrapInEchoHandler($matches[2])}); ?>{$whitespace}";
};

return preg_replace_callback($pattern, $callback, $value);
}

/**
* Add an instance of the blade echo handler to the start of the compiled string.
*
* @param string $result
* @return string
*/
protected function addBladeCompilerVariable($result)
{
return "<?php \$__bladeCompiler = app('blade.compiler'); ?>".$result;
}

/**
* Wrap the echoable value in an echo handler if applicable.
*
* @param string $value
* @return string
*/
protected function applyEchoHandlerFor($value)
protected function wrapInEchoHandler($value)
{
return empty($this->echoHandlers)
? $value
: "is_object($value) && isset(app('blade.compiler')->echoHandlers[get_class($value)]) ? call_user_func_array(app('blade.compiler')->echoHandlers[get_class($value)], [$value]) : $value";
return empty($this->echoHandlers) ? $value : '$__bladeCompiler->applyEchoHandler('.Str::beforeLast($value, ';').')';
}

/**
* Apply the echo handler for the value if it exists.
*
* @param $value string
* @return string
*/
public function applyEchoHandler($value)
{
if (is_object($value) && isset($this->echoHandlers[get_class($value)])) {
return call_user_func($this->echoHandlers[get_class($value)], $value);
}

return $value;
}
}
60 changes: 45 additions & 15 deletions tests/View/Blade/BladeEchoHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,42 @@ protected function setUp(): void
});
}

public function testBladeHandlersCanBeAddedForAGivenClass()
{
$this->assertSame('Hello World', $this->compiler->echoHandlers[Fluent::class](new Fluent()));
}

public function testBladeHandlerCanInterceptRegularEchos()
{
$this->assertSame(
"<?php echo e(is_object(\$exampleObject) && isset(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)]) ? call_user_func_array(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)], [\$exampleObject]) : \$exampleObject); ?>",
"<?php \$__bladeCompiler = app('blade.compiler'); ?><?php echo e(\$__bladeCompiler->applyEchoHandler(\$exampleObject)); ?>",
$this->compiler->compileString('{{$exampleObject}}')
);
}

public function testBladeHandlerCanInterceptRawEchos()
{
$this->assertSame(
"<?php echo is_object(\$exampleObject) && isset(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)]) ? call_user_func_array(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)], [\$exampleObject]) : \$exampleObject; ?>",
"<?php \$__bladeCompiler = app('blade.compiler'); ?><?php echo \$__bladeCompiler->applyEchoHandler(\$exampleObject); ?>",
$this->compiler->compileString('{!!$exampleObject!!}')
);
}

public function testBladeHandlerCanInterceptEscapedEchos()
{
$this->assertSame(
"<?php echo e(is_object(\$exampleObject) && isset(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)]) ? call_user_func_array(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)], [\$exampleObject]) : \$exampleObject); ?>",
"<?php \$__bladeCompiler = app('blade.compiler'); ?><?php echo e(\$__bladeCompiler->applyEchoHandler(\$exampleObject)); ?>",
$this->compiler->compileString('{{{$exampleObject}}}')
);
}

public function testWhitespaceIsPreservedCorrectly()
{
$this->assertSame(
"<?php echo e(is_object(\$exampleObject) && isset(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)]) ? call_user_func_array(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)], [\$exampleObject]) : \$exampleObject); ?>\n\n",
"<?php \$__bladeCompiler = app('blade.compiler'); ?><?php echo e(\$__bladeCompiler->applyEchoHandler(\$exampleObject)); ?>\n\n",
$this->compiler->compileString("{{\$exampleObject}}\n")
);
}

public function testHandlerLogicWorksCorrectly()
/**
* @dataProvider handlerLogicDataProvider
*/
public function testHandlerLogicWorksCorrectly($blade)
{
$this->expectExceptionMessage('The fluent object has been successfully handled!');

Expand All @@ -68,10 +66,42 @@ public function testHandlerLogicWorksCorrectly()

$exampleObject = new Fluent();

eval(
Str::of($this->compiler->compileString('{{$exampleObject}}'))
->after('<?php')
->beforeLast('?>')
);
eval(Str::of($this->compiler->compileString($blade))->remove(['<?php', '?>']));
}

public function handlerLogicDataProvider()
{
return [
['{{$exampleObject}}'],
['{{$exampleObject;}}'],
['{{{$exampleObject;}}}'],
['{!!$exampleObject;!!}'],
];
}

/**
* @dataProvider nonStringableDataProvider
*/
public function testHandlerWorksWithNonStringables($blade, $expectedOutput)
{
app()->singleton('blade.compiler', function () {
return $this->compiler;
});

ob_start();
eval(Str::of($this->compiler->compileString($blade))->remove(['<?php', '?>']));
$output = ob_get_contents();
ob_end_clean();

$this->assertSame($expectedOutput, $output);
}

public function nonStringableDataProvider()
{
return [
['{{"foo" . "bar"}}', 'foobar'],
['{{ 1 + 2 }}{{ "test"; }}', '3test'],
['@php($test = "hi"){{ $test }}', 'hi'],
];
}
}