diff --git a/src/Illuminate/Support/Facades/Blade.php b/src/Illuminate/Support/Facades/Blade.php index c2aa7fe8aada..537ad2bbc043 100755 --- a/src/Illuminate/Support/Facades/Blade.php +++ b/src/Illuminate/Support/Facades/Blade.php @@ -26,6 +26,7 @@ * @method static void withDoubleEncoding() * @method static void withoutComponentTags() * @method static void withoutDoubleEncoding() + * @method static void stringable(string|callable $class, callable|null $handler) * * @see \Illuminate\View\Compilers\BladeCompiler */ diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index 14f8aefb4f49..d6d7436b79e3 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -2,8 +2,10 @@ namespace Illuminate\View\Compilers; +use Closure; use Illuminate\Support\Arr; use Illuminate\Support\Str; +use Illuminate\Support\Traits\ReflectsClosures; use InvalidArgumentException; class BladeCompiler extends Compiler implements CompilerInterface @@ -22,7 +24,8 @@ class BladeCompiler extends Compiler implements CompilerInterface Concerns\CompilesLoops, Concerns\CompilesRawPhp, Concerns\CompilesStacks, - Concerns\CompilesTranslations; + Concerns\CompilesTranslations, + ReflectsClosures; /** * All of the registered extensions. @@ -99,6 +102,13 @@ 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. * @@ -701,6 +711,22 @@ 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. * diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesEchos.php b/src/Illuminate/View/Compilers/Concerns/CompilesEchos.php index 00612ed868ee..4da995d08cc3 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesEchos.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesEchos.php @@ -46,7 +46,9 @@ protected function compileRawEchos($value) $callback = function ($matches) { $whitespace = empty($matches[3]) ? '' : $matches[3].$matches[3]; - return $matches[1] ? substr($matches[0], 1) : "{$whitespace}"; + return $matches[1] + ? substr($matches[0], 1) + : "applyEchoHandlerFor($matches[2])}; ?>{$whitespace}"; }; return preg_replace_callback($pattern, $callback, $value); @@ -65,7 +67,7 @@ protected function compileRegularEchos($value) $callback = function ($matches) { $whitespace = empty($matches[3]) ? '' : $matches[3].$matches[3]; - $wrapped = sprintf($this->echoFormat, $matches[2]); + $wrapped = sprintf($this->echoFormat, $this->applyEchoHandlerFor($matches[2])); return $matches[1] ? substr($matches[0], 1) : "{$whitespace}"; }; @@ -86,9 +88,24 @@ protected function compileEscapedEchos($value) $callback = function ($matches) { $whitespace = empty($matches[3]) ? '' : $matches[3].$matches[3]; - return $matches[1] ? $matches[0] : "{$whitespace}"; + return $matches[1] + ? $matches[0] + : "applyEchoHandlerFor($matches[2])}); ?>{$whitespace}"; }; return preg_replace_callback($pattern, $callback, $value); } + + /** + * Wrap the echoable value in an echo handler if applicable. + * + * @param string $value + * @return string + */ + protected function applyEchoHandlerFor($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"; + } } diff --git a/tests/View/Blade/BladeEchoHandlerTest.php b/tests/View/Blade/BladeEchoHandlerTest.php new file mode 100644 index 000000000000..75f0b297511b --- /dev/null +++ b/tests/View/Blade/BladeEchoHandlerTest.php @@ -0,0 +1,77 @@ +compiler->stringable(function (Fluent $object) { + return 'Hello World'; + }); + } + + public function testBladeHandlersCanBeAddedForAGivenClass() + { + $this->assertSame('Hello World', $this->compiler->echoHandlers[Fluent::class](new Fluent())); + } + + public function testBladeHandlerCanInterceptRegularEchos() + { + $this->assertSame( + "echoHandlers[get_class(\$exampleObject)]) ? call_user_func_array(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)], [\$exampleObject]) : \$exampleObject); ?>", + $this->compiler->compileString('{{$exampleObject}}') + ); + } + + public function testBladeHandlerCanInterceptRawEchos() + { + $this->assertSame( + "echoHandlers[get_class(\$exampleObject)]) ? call_user_func_array(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)], [\$exampleObject]) : \$exampleObject; ?>", + $this->compiler->compileString('{!!$exampleObject!!}') + ); + } + + public function testBladeHandlerCanInterceptEscapedEchos() + { + $this->assertSame( + "echoHandlers[get_class(\$exampleObject)]) ? call_user_func_array(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)], [\$exampleObject]) : \$exampleObject); ?>", + $this->compiler->compileString('{{{$exampleObject}}}') + ); + } + + public function testWhitespaceIsPreservedCorrectly() + { + $this->assertSame( + "echoHandlers[get_class(\$exampleObject)]) ? call_user_func_array(app('blade.compiler')->echoHandlers[get_class(\$exampleObject)], [\$exampleObject]) : \$exampleObject); ?>\n\n", + $this->compiler->compileString("{{\$exampleObject}}\n") + ); + } + + public function testHandlerLogicWorksCorrectly() + { + $this->expectExceptionMessage('The fluent object has been successfully handled!'); + + $this->compiler->stringable(Fluent::class, function ($object) { + throw new Exception('The fluent object has been successfully handled!'); + }); + + app()->singleton('blade.compiler', function () { + return $this->compiler; + }); + + $exampleObject = new Fluent(); + + eval( + Str::of($this->compiler->compileString('{{$exampleObject}}')) + ->after('beforeLast('?>') + ); + } +}