Porting Svelte's new snippet feature to Laravel
Svelte 5 has a neat new feature called "snippets" which allows you to extract parts of a template into a snippet, in the same file, and reuse that later in the file. That way you can avoid using another blade file, which in certain cases might be preferable.
See https://svelte-5-preview.vercel.app/docs/snippet
You can take those Svelte examples and translate them to blade, from this:
@foreach($images as $image)
@if($image['href'])
<a href="{{ $image['href'] }}">
<figure>
<img
src="{{ $image['src'] }}"
alt="{{ $image['caption'] }}"
width="{{ $image['width'] }}"
height="{{ $image['height'] }}"
/>
<figcaption>{{ $image['caption'] }}</figcaption>
</figure>
</a>
@else
<figure>
<img
src="{{ $image['src'] }}"
alt="{{ $image['caption'] }}"
width="{{ $image['width'] }}"
height="{{ $image['height'] }}"
/>
<figcaption>{{ $image['caption'] }}</figcaption>
</figure>
@endif
@endforeach
To this:
@snippet ('image', $img)
<figure>
<img
src="{{ $img['src'] }}"
alt="{{ $img['caption'] }}"
width="{{ $img['width'] }}"
height="{{ $img['height'] }}"
/>
<figcaption>{{ $img['caption'] }}</figcaption>
</figure>
@endsnippet
@foreach ($images as $image)
@if ($image['href'])
<a href="{{ $image['href'] }}">
@renderSnippet ('image', $image)
</a>
@else
@renderSnippet ('image', $image)
@endif
@endforeach
Blade directives
My Laravel Core PR was denied, but you can still use this feature with these blade directives, which you put into your AppServiceProvider.php file:
\Blade::directive('snippet', static function ($expression) {
$functionParts = explode(',', $expression);
$function = trim(array_shift($functionParts), "'\" ");
if (empty($function)) {
$function = 'function';
}
$function = '__snippet_' . Str::camel($function);
$args = trim(implode(',', $functionParts));
return implode("\n", [
"<?php if (! isset(\${$function})):",
'$'.$function.' = static function('.$args.') use($__env) {',
'?>',
]);
});
\Blade::directive('endsnippet', static function ($expression) {
return implode("\n", [
'<?php } ?>',
'<?php endif; ?>',
]);
});
\Blade::directive('renderSnippet', static function ($expression) {
$functionParts = explode(',', $expression);
$function = trim(array_shift($functionParts), "'\" ");
if (empty($function)) {
$function = 'function';
}
$function = '__snippet_' . Str::camel($function);
$args = trim(implode(',', $functionParts));
return '<?php echo $'.$function.'('.$args.'); ?>';
});
Snippet scope
Snippets can be declared anywhere inside a blade file. As of now, snippet functions they cannot reference variables outside themselves. Maybe one day PHP will have multiline arrow functions. Nuno tried that already (php/php-src#6246).
Declared snippets are only usable in the same blade file with the exception of included bladed files using @include directive, which makes sense since you are literally including the content of another file.
And like function declarations, snippets can have an arbitrary number of parameters, which can have default values.
Snippet declaration
Snippets can be declared with or without a specific function name, and with or without parameters:
@snippet
will be declared as main snippet of this blade file
@endsnippet
@snippet ("foo")
will be declared as 'foo' snippet
@endsnippet
@snippet ('foo', $bar)
will be declared as 'foo' snippet with $bar as parameter
@endsnippet
@snippet (foobar, string $barfoo)
will be declared as 'foobar' snippet with an explicit string as $barfoo parameter
@endsnippet
@snippet ("foo-bar", string $barfoo)
will be declared as 'foo-bar' snippet with an explicit string as $barfoo parameter
@endsnippet
Snippet rendering
Taking the previous examples, snippets can be rendered like this:
Render the default snippet of the current blade file
@renderSnippet
Render the "foo" snippet, with double quotes
@renderSnippet ("foo")
Render the "foo" snippet with single quotes and $bar as parameter
@renderSnippet ('foo', $bar)
Render the "foobar" snippet without quotes and $barfoo as parameter
@renderSnippet (foobar, $bar)
Render the sluggy "foo-bar" snippet without quotes and $barfoo as parameter
@renderSnippet ("foo-bar", $bar)
Benchmarks
I've done some benchmarking, with PHP 8.3, on a Macbook Pro M2 Max, analog to this (#51141 (comment)), with the following process:
Route::get('/test', function () {
$a = array_map(
fn () => Benchmark::measure(fn() => view('simple-component-test')->render()),
range(0, 10)
);
$b = array_map(
fn () => Benchmark::measure(fn() => view('snippet-test')->render()),
range(0, 10)
);
return 'OPCache Enabled: ' . (is_array(opcache_get_status()) ? 'Enabled' : 'Disabled') .
'<br>With blade component call: ' . (array_sum($a) / count($a)) .
'<br>With snippet call: ' . (array_sum($b) / count($b)) .
'<br>Performance improvement: ' . ((array_sum($a) / count($a)) / (array_sum($b) / count($b)));
});
avatar.blade.php:
@props(['name'])
<div {{ $attributes }}>
Name: {{ $name }}
</div>
simple-component-test.blade.php:
@foreach (range(1, 20000) as $id)
<x-avatar :name="'Taylor'" class="mt-4" />
@endforeach
snippet-test.blade.php:
@snippet('avatar', string $name)
<div class="mt-4">
Name: {{ $name }}
</div>
@endsnippet
@foreach (range(1, 20000) as $id)
@renderSnippet('avatar', 'Taylor')
@endforeach
Results
Compared to simply using another blade file for some snippet code, there is around a 13x speed improvement.