Skip to content

[11.x] Added "snippet" blade feature with >13x speed improvement #51268

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

Closed
wants to merge 1 commit into from

Conversation

dimitri-koenig
Copy link
Contributor

@dimitri-koenig dimitri-koenig commented May 2, 2024

Svelte 5 has a neat new feature called "snippets". See https://svelte-5-preview.vercel.app/docs/snippets

Basically it 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.

To take those Svelte examples and translate them to blade:

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

Becomes 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

Like function declarations, snippets can have an arbitrary number of parameters, which can have default values.

Snippet scope

Snippets can be declared anywhere inside a blade file. But since they are 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.

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 function name declaration could also be declared as @snippet (foo). The implementation can handle this case too.

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

With opcache, cached views:
Screenshot-2024-05-02-11 29 25

Without opcache, but cached views:
Screenshot-2024-05-02-11 33 14

I know, the example falls a bit short, but it's still nice to see some numbers :-)

Backport to older Laravel versions

In case someone wants to use this feature on older installations, this would be the code:

AppServiceProvider.php:

\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.'); ?>';
});

@donnysim
Copy link
Contributor

donnysim commented May 2, 2024

Would this also allow passing those snippets into inner views as a variable (can see some use cases for it for myself 🤔)? also didn't see it handle invalid names if someone named it as @snippet('my-label') though maybe it would just be a limitation of giving a safe php variable name.

@dimitri-koenig
Copy link
Contributor Author

Would this also allow passing those snippets into inner views as a variable (can see some use cases for it for myself 🤔)?

Can you show an example of your use case?

also didn't see it handle invalid names if someone named it as @snippet('my-label') though maybe it would just be a limitation of giving a safe php variable name.

Fixed that

@donnysim
Copy link
Contributor

donnysim commented May 2, 2024

There are a lot of cases like blade component slots where you cannot receive data up where the slot is provided, this would allow passing the state up to the parent from the component/child view with additional state of where the parent can additionally add or render based on it. Probably bad example, but example nonetheless:

// fields/input.blade.php
@php($id = Uuid::generate())

@if(isset($labelSnippet))
    {!! $labelSnippet($id) !!}
@endif

<input id="{{ $id }}">

// form.blade.php
@snippet('labelSnippet', $id)
    <label for="{{ $id }}">This is my <strong>rich</strong> label</label>
@endsnippet

@foreach($fields as $field)
    @include('fields.input', ['name' => $field->name, 'labelSnippet' => $labelSnippet])
@endforeach

@dimitri-koenig
Copy link
Contributor Author

dimitri-koenig commented May 2, 2024

Got it. Right now a snippet name"labelSnippet" will turned to $__snippet_labelSnippet. So if you are passing that one to your included blade file, that should work.

One could make it more strict and say: each snippet name MUST be a proper php compatible variable name, like $labelSnippet so that it is easily reusable for such include calls.

@dimitri-koenig
Copy link
Contributor Author

I wonder wether this "mechanism" could work for blade components as well, somehow. That would speed them up by a lot. Will check that out.

@driesvints driesvints changed the title Added "snippet" blade feature with >13x speed improvement [11.x] Added "snippet" blade feature with >13x speed improvement May 3, 2024
@taylorotwell
Copy link
Member

I think we specifically want to make Blade components much faster. 👍

@dimitri-koenig
Copy link
Contributor Author

@taylorotwell, I get your point about speed improvements for blade components. But this PR is not aimed at that at all. The main point of this PR is to add a simple feature which makes it possible to avoid some blade components at all, for better readability. The example with the image tags is a real one. In cases like this it does not make sense to create another blade component, for readability sake. I kindly ask you to reconsider.

@dimitri-koenig dimitri-koenig deleted the 11.x branch May 16, 2024 14:52
@dimitri-koenig
Copy link
Contributor Author

@donnysim FYI: Ryan Chandler published something similar which could suit your usecase even better: https://github.com/ryangjchandler/blade-capture-directive/tree/main

@donnysim
Copy link
Contributor

@dimitri-koenig Oh cool, definitely will play around with it, thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants