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.