Skip to content

Commit

Permalink
update stringable implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
ibrasho committed Jun 6, 2021
1 parent d13db93 commit dd909db
Show file tree
Hide file tree
Showing 15 changed files with 116 additions and 112 deletions.
24 changes: 0 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 @@ -711,22 +703,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
12 changes: 5 additions & 7 deletions src/Illuminate/View/Compilers/Concerns/CompilesEchos.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,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 +67,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,7 +90,7 @@ 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);
Expand All @@ -102,10 +102,8 @@ protected function compileEscapedEchos($value)
* @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 "is_object($value) ? \$__env->stringifyObject({$value}) : ($value)";
}
}
46 changes: 46 additions & 0 deletions src/Illuminate/View/Concerns/StringifyObjects.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Illuminate\View\Concerns;

use Closure;

trait StringifyObjects
{
/**
* 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;
}

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

return (string) $value;
}
}
5 changes: 4 additions & 1 deletion src/Illuminate/View/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\Traits\ReflectsClosures;
use Illuminate\View\Engines\EngineResolver;
use InvalidArgumentException;

Expand All @@ -20,7 +21,9 @@ class Factory implements FactoryContract
Concerns\ManagesLayouts,
Concerns\ManagesLoops,
Concerns\ManagesStacks,
Concerns\ManagesTranslations;
Concerns\ManagesTranslations,
Concerns\StringifyObjects,
ReflectsClosures;

/**
* The engine implementation.
Expand Down
2 changes: 1 addition & 1 deletion tests/View/Blade/BladeComponentTagCompilerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public function testBasicComponentParsing()
$result = $this->compiler(['alert' => TestAlertComponent::class])->compileTags('<div><x-alert type="foo" limit="5" @click="foo" wire:click="changePlan(\'{{ $plan }}\')" required /><x-alert /></div>');

$this->assertSame("<div>##BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', [])
<?php \$component->withAttributes(['type' => 'foo','limit' => '5','@click' => 'foo','wire:click' => 'changePlan(\''.e(\$plan).'\')','required' => true]); ?>\n".
<?php \$component->withAttributes(['type' => 'foo','limit' => '5','@click' => 'foo','wire:click' => 'changePlan(\''.e(is_object(\$plan) ? \$__env->stringifyObject(\$plan) : (\$plan)).'\')','required' => true]); ?>\n".
"@endComponentClass##END-COMPONENT-CLASS####BEGIN-COMPONENT-CLASS##@component('Illuminate\Tests\View\Blade\TestAlertComponent', 'alert', [])
<?php \$component->withAttributes([]); ?>\n".
'@endComponentClass##END-COMPONENT-CLASS##</div>', trim($result));
Expand Down
2 changes: 1 addition & 1 deletion tests/View/Blade/BladeCustomTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public function testCustomPhpCodeIsCorrectlyHandled()

public function testMixingYieldAndEcho()
{
$this->assertSame('<?php echo $__env->yieldContent(\'title\'); ?> - <?php echo e(Config::get(\'site.title\')); ?>', $this->compiler->compileString("@yield('title') - {{Config::get('site.title')}}"));
$this->assertSame('<?php echo $__env->yieldContent(\'title\'); ?> - <?php echo e(is_object(Config::get(\'site.title\')) ? $__env->stringifyObject(Config::get(\'site.title\')) : (Config::get(\'site.title\'))); ?>', $this->compiler->compileString("@yield('title') - {{Config::get('site.title')}}"));
}

public function testCustomExtensionsAreCompiled()
Expand Down
47 changes: 4 additions & 43 deletions tests/View/Blade/BladeEchoHandlerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,76 +2,37 @@

namespace Illuminate\Tests\View\Blade;

use Exception;
use Illuminate\Support\Fluent;
use Illuminate\Support\Str;

class BladeEchoHandlerTest extends AbstractBladeTestCase
{
protected function setUp(): void
{
parent::setUp();

$this->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(
"<?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 echo e(is_object($exampleObject) ? $__env->stringifyObject($exampleObject) : ($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 echo is_object($exampleObject) ? $__env->stringifyObject($exampleObject) : ($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 echo e(is_object($exampleObject) ? $__env->stringifyObject($exampleObject) : ($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 echo e(is_object(\$exampleObject) ? \$__env->stringifyObject(\$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('<?php')
->beforeLast('?>')
);
}
}
42 changes: 21 additions & 21 deletions tests/View/Blade/BladeEchoTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,45 +6,45 @@ class BladeEchoTest extends AbstractBladeTestCase
{
public function testEchosAreCompiled()
{
$this->assertSame('<?php echo $name; ?>', $this->compiler->compileString('{!!$name!!}'));
$this->assertSame('<?php echo $name; ?>', $this->compiler->compileString('{!! $name !!}'));
$this->assertSame('<?php echo $name; ?>', $this->compiler->compileString('{!!
$this->assertSame('<?php echo is_object($name) ? $__env->stringifyObject($name) : ($name); ?>', $this->compiler->compileString('{!!$name!!}'));
$this->assertSame('<?php echo is_object($name) ? $__env->stringifyObject($name) : ($name); ?>', $this->compiler->compileString('{!! $name !!}'));
$this->assertSame('<?php echo is_object($name) ? $__env->stringifyObject($name) : ($name); ?>', $this->compiler->compileString('{!!
$name
!!}'));

$this->assertSame('<?php echo e($name); ?>', $this->compiler->compileString('{{{$name}}}'));
$this->assertSame('<?php echo e($name); ?>', $this->compiler->compileString('{{$name}}'));
$this->assertSame('<?php echo e($name); ?>', $this->compiler->compileString('{{ $name }}'));
$this->assertSame('<?php echo e($name); ?>', $this->compiler->compileString('{{
$this->assertSame('<?php echo e(is_object($name) ? $__env->stringifyObject($name) : ($name)); ?>', $this->compiler->compileString('{{{$name}}}'));
$this->assertSame('<?php echo e(is_object($name) ? $__env->stringifyObject($name) : ($name)); ?>', $this->compiler->compileString('{{$name}}'));
$this->assertSame('<?php echo e(is_object($name) ? $__env->stringifyObject($name) : ($name)); ?>', $this->compiler->compileString('{{ $name }}'));
$this->assertSame('<?php echo e(is_object($name) ? $__env->stringifyObject($name) : ($name)); ?>', $this->compiler->compileString('{{
$name
}}'));
$this->assertSame("<?php echo e(\$name); ?>\n\n", $this->compiler->compileString("{{ \$name }}\n"));
$this->assertSame("<?php echo e(\$name); ?>\r\n\r\n", $this->compiler->compileString("{{ \$name }}\r\n"));
$this->assertSame("<?php echo e(\$name); ?>\n\n", $this->compiler->compileString("{{ \$name }}\n"));
$this->assertSame("<?php echo e(\$name); ?>\r\n\r\n", $this->compiler->compileString("{{ \$name }}\r\n"));
$this->assertSame("<?php echo e(is_object(\$name) ? \$__env->stringifyObject(\$name) : (\$name)); ?>\n\n", $this->compiler->compileString("{{ \$name }}\n"));
$this->assertSame("<?php echo e(is_object(\$name) ? \$__env->stringifyObject(\$name) : (\$name)); ?>\r\n\r\n", $this->compiler->compileString("{{ \$name }}\r\n"));
$this->assertSame("<?php echo e(is_object(\$name) ? \$__env->stringifyObject(\$name) : (\$name)); ?>\n\n", $this->compiler->compileString("{{ \$name }}\n"));
$this->assertSame("<?php echo e(is_object(\$name) ? \$__env->stringifyObject(\$name) : (\$name)); ?>\r\n\r\n", $this->compiler->compileString("{{ \$name }}\r\n"));

$this->assertSame('<?php echo e("Hello world or foo"); ?>',
$this->assertSame('<?php echo e(is_object("Hello world or foo") ? $__env->stringifyObject("Hello world or foo") : ("Hello world or foo")); ?>',
$this->compiler->compileString('{{ "Hello world or foo" }}'));
$this->assertSame('<?php echo e("Hello world or foo"); ?>',
$this->assertSame('<?php echo e(is_object("Hello world or foo") ? $__env->stringifyObject("Hello world or foo") : ("Hello world or foo")); ?>',
$this->compiler->compileString('{{"Hello world or foo"}}'));
$this->assertSame('<?php echo e($foo + $or + $baz); ?>', $this->compiler->compileString('{{$foo + $or + $baz}}'));
$this->assertSame('<?php echo e("Hello world or foo"); ?>', $this->compiler->compileString('{{
$this->assertSame('<?php echo e(is_object($foo + $or + $baz) ? $__env->stringifyObject($foo + $or + $baz) : ($foo + $or + $baz)); ?>', $this->compiler->compileString('{{$foo + $or + $baz}}'));
$this->assertSame('<?php echo e(is_object("Hello world or foo") ? $__env->stringifyObject("Hello world or foo") : ("Hello world or foo")); ?>', $this->compiler->compileString('{{
"Hello world or foo"
}}'));

$this->assertSame('<?php echo e(\'Hello world or foo\'); ?>',
$this->assertSame('<?php echo e(is_object(\'Hello world or foo\') ? $__env->stringifyObject(\'Hello world or foo\') : (\'Hello world or foo\')); ?>',
$this->compiler->compileString('{{ \'Hello world or foo\' }}'));
$this->assertSame('<?php echo e(\'Hello world or foo\'); ?>',
$this->assertSame('<?php echo e(is_object(\'Hello world or foo\') ? $__env->stringifyObject(\'Hello world or foo\') : (\'Hello world or foo\')); ?>',
$this->compiler->compileString('{{\'Hello world or foo\'}}'));
$this->assertSame('<?php echo e(\'Hello world or foo\'); ?>', $this->compiler->compileString('{{
$this->assertSame('<?php echo e(is_object(\'Hello world or foo\') ? $__env->stringifyObject(\'Hello world or foo\') : (\'Hello world or foo\')); ?>', $this->compiler->compileString('{{
\'Hello world or foo\'
}}'));

$this->assertSame('<?php echo e(myfunc(\'foo or bar\')); ?>',
$this->assertSame('<?php echo e(is_object(myfunc(\'foo or bar\')) ? $__env->stringifyObject(myfunc(\'foo or bar\')) : (myfunc(\'foo or bar\'))); ?>',
$this->compiler->compileString('{{ myfunc(\'foo or bar\') }}'));
$this->assertSame('<?php echo e(myfunc("foo or bar")); ?>',
$this->assertSame('<?php echo e(is_object(myfunc("foo or bar")) ? $__env->stringifyObject(myfunc("foo or bar")) : (myfunc("foo or bar"))); ?>',
$this->compiler->compileString('{{ myfunc("foo or bar") }}'));
$this->assertSame('<?php echo e(myfunc("$name or \'foo\'")); ?>',
$this->assertSame('<?php echo e(is_object(myfunc("$name or \'foo\'")) ? $__env->stringifyObject(myfunc("$name or \'foo\'")) : (myfunc("$name or \'foo\'"))); ?>',
$this->compiler->compileString('{{ myfunc("$name or \'foo\'") }}'));
}

Expand Down
4 changes: 2 additions & 2 deletions tests/View/Blade/BladeErrorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public function testErrorsAreCompiled()
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<span><?php echo e($message); ?></span>
<span><?php echo e(is_object($message) ? $__env->stringifyObject($message) : ($message)); ?></span>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
Expand All @@ -37,7 +37,7 @@ public function testErrorsWithBagsAreCompiled()
if ($__bag->has($__errorArgs[0])) :
if (isset($message)) { $__messageOriginal = $message; }
$message = $__bag->first($__errorArgs[0]); ?>
<span><?php echo e($message); ?></span>
<span><?php echo e(is_object($message) ? $__env->stringifyObject($message) : ($message)); ?></span>
<?php unset($message);
if (isset($__messageOriginal)) { $message = $__messageOriginal; }
endif;
Expand Down
6 changes: 3 additions & 3 deletions tests/View/Blade/BladeExpressionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ public function testExpressionsOnTheSameLine()

public function testExpressionWithinHTML()
{
$this->assertSame('<html <?php echo e($foo); ?>>', $this->compiler->compileString('<html {{ $foo }}>'));
$this->assertSame('<html<?php echo e($foo); ?>>', $this->compiler->compileString('<html{{ $foo }}>'));
$this->assertSame('<html <?php echo e($foo); ?> <?php echo app(\'translator\')->get(\'foo\'); ?>>', $this->compiler->compileString('<html {{ $foo }} @lang(\'foo\')>'));
$this->assertSame('<html <?php echo e(is_object($foo) ? $__env->stringifyObject($foo) : ($foo)); ?>>', $this->compiler->compileString('<html {{ $foo }}>'));
$this->assertSame('<html<?php echo e(is_object($foo) ? $__env->stringifyObject($foo) : ($foo)); ?>>', $this->compiler->compileString('<html{{ $foo }}>'));
$this->assertSame('<html <?php echo e(is_object($foo) ? $__env->stringifyObject($foo) : ($foo)); ?> <?php echo app(\'translator\')->get(\'foo\'); ?>>', $this->compiler->compileString('<html {{ $foo }} @lang(\'foo\')>'));
}
}
2 changes: 1 addition & 1 deletion tests/View/Blade/BladeForeachStatementsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public function testLoopContentHolderIsExtractedFromForeachStatements()
<input {{ \$foo ? 'bar': 'baz' }}>
@endforeach";
$expected = "<?php \$__currentLoopData = resolve('App\\\\DataProviders\\\\'.\$provider)->data(); \$__env->addLoop(\$__currentLoopData); foreach(\$__currentLoopData as \$key => \$value): \$__env->incrementLoopIndices(); \$loop = \$__env->getLastLoop(); ?>
<input <?php echo e(\$foo ? 'bar': 'baz'); ?>>
<input <?php echo e(is_object(\$foo ? 'bar': 'baz') ? \$__env->stringifyObject(\$foo ? 'bar': 'baz') : (\$foo ? 'bar': 'baz')); ?>>
<?php endforeach; \$__env->popLoop(); \$loop = \$__env->getLastLoop(); ?>";

$this->assertEquals($expected, $this->compiler->compileString($string));
Expand Down
4 changes: 2 additions & 2 deletions tests/View/Blade/BladePhpStatementsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public function testPhpStatementsWithoutExpressionAreIgnored()
$this->assertEquals($expected, $this->compiler->compileString($string));

$string = '{{ "Ignore: @php" }}';
$expected = '<?php echo e("Ignore: @php"); ?>';
$expected = '<?php echo e(is_object("Ignore: @php") ? $__env->stringifyObject("Ignore: @php") : ("Ignore: @php")); ?>';
$this->assertEquals($expected, $this->compiler->compileString($string));
}

Expand All @@ -38,7 +38,7 @@ public function testVerbatimAndPhpStatementsDontGetMixedUp()

$expected = " {{ Hello, I'm not blade! }}"
."\n@php echo 'And I'm not PHP!' @endphp"
."\n <?php echo e('I am Blade'); ?>"
."\n <?php echo e(is_object('I am Blade') ? \$__env->stringifyObject('I am Blade') : ('I am Blade')); ?>"
."\n\n<?php echo 'I am PHP {{ not Blade }}' ?>";

$this->assertEquals($expected, $this->compiler->compileString($string));
Expand Down
Loading

0 comments on commit dd909db

Please sign in to comment.