Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message
Twig
symkit/error-bundle
cursor
twig
templating
arrow-functions
intl-formatting
fragment-caching
html-cva
twigstan

Twig v3: arrow functions, CVA, enum(), cache/types tags, Intl filters, null-safe, embed.

Install
php artisan boost:add-skill symkit/error-bundle

Save this content to: .cursor/rules/twig.mdc

# Twig (v3)

For Stimulus/Turbo/Live Components/AssetMapper, see `frontend.mdc`. For translation filters (`|trans`, `t()`), see `i18n.mdc`. For `{% cache %}` pool configuration and tag invalidation, see `caching.mdc`.

## Core Principles

1. **Arrow functions over loops** — Use `|map`, `|filter`, `|find`, `|reduce`, `|sort` with arrow functions instead of verbose `{% for %}` + `{% if %}` + `{% set %}` combos.
2. **Intl filters over manual formatting** — Use `|format_currency`, `|format_datetime`, `|format_number` for locale-aware output. Never format with `|date` + string concatenation.
3. **`html_cva()` for variant styling** — Define component class variants with `html_cva()`. Never build CSS classes with string concatenation or nested `{% if %}`.
4. **`{% cache %}` for fragment caching** — Cache expensive template fragments with key, TTL, and tags. Requires `twig/cache-extra`.
5. **`{% types %}` for template contracts** — Declare expected variables and their types. Enables TwigStan static analysis and self-documents templates.
6. **`enum()` for PHP enums** — Access enum cases directly in templates. Never pass enum values as magic strings from controllers.
7. **Null-safe `?.` over ternary guards** — Use `user?.address?.city` instead of multi-line `{% if defined %}` chains.
8. **`{% embed %}` for micro-layouts** — Combine include + extends for reusable structural skeletons with overridable blocks.
9. **`|u` for Unicode string ops** — Use `|u.truncate()`, `|u.wordwrap()` for string manipulation. Never truncate with `|slice` (breaks multi-byte).
10. **Named arguments with `:`** — Prefer `range(low: 1, high: 10)` over positional args. Available for filters, functions, tests, macros.
11. **`twig/extra-bundle` required** — Most v3 features (`|u`, `|slug`, `|format_*`, `html_cva`, `{% cache %}`) need `twig/extra-bundle` + specific extras.

---

## Conventions

### Arrow Functions — Functional Collection Processing

**Do:**

```twig
{# Pipeline: filter → map → join #}
{{ users|filter(u => u.active)|map(u => u.name)|join(', ') }}

{# Find first matching element (v3.11) #}
{% set admin = users|find(u => u.role == 'admin') %}

{# Sort by property with spaceship operator #}
{% for p in products|sort((a, b) => a.price <=> b.price) %}
    {{ p.name }}: {{ p.price|format_currency('EUR') }}
{% endfor %}

{# Reduce to total #}
{{ cart.items|reduce((total, item) => total + item.price * item.qty, 0)|format_currency('EUR') }}

{# Extract single column from collection #}
{{ users|column('email')|join(', ') }}

{# has some / has every (iterable operators) #}
{% if items has every v => v.stock > 0 %}All in stock{% endif %}
{% if items has some v => v.featured %}Has featured{% endif %}
```

**Don't:**

```twig
{% set names = [] %}
{% for u in users %}
    {% if u.active %}
        {% set names = names|merge([u.name]) %}
    {% endif %}
{% endfor %}
{{ names|join(', ') }}
```

### Intl Filters — Locale-Aware Formatting

**Do:**

```twig
{{ product.price|format_currency('EUR', locale: app.request.locale) }}

{{ order.createdAt|format_datetime('long', 'short', locale: 'fr') }}

{{ 0.15|format_number(style: 'percent') }}

{{ 42|format_number(style: 'ordinal', locale: 'en') }}

{{ 'FR'|country_name }}
{{ 'EUR'|currency_name('fr') }}
{{ 'en'|language_name('fr') }}
```

**Don't:**

```twig
{{ product.price|number_format(2, ',', ' ') }} €
{{ order.createdAt|date('d/m/Y H:i') }}
```

### html_cva() — Class Variant Authority

**Do:**

```twig
{% set btn = html_cva(
    base: 'btn',
    variants: {
        color: { primary: 'btn-primary', danger: 'btn-danger' },
        size: { sm: 'btn-sm', lg: 'btn-lg' },
    },
    default_variant: { color: 'primary', size: 'sm' },
    compound_variants: [{
        color: ['danger'],
        size: ['lg'],
        class: 'font-bold uppercase',
    }]
) %}

<button class="{{ btn.apply({color, size}) }}">{{ label }}</button>
```

**Don't:**

```twig
<button class="btn {% if color == 'primary' %}btn-primary{% elseif color == 'danger' %}btn-danger{% endif %} {% if size == 'lg' %}btn-lg{% endif %}">
    {{ label }}
</button>
```

### html_classes() — Conditional CSS Classes

**Do:**

```twig
<div class="{{ html_classes('card', {
    'card--featured': product.featured,
    'card--sold-out': product.stock == 0,
}) }}">
```

**Don't:**

```twig
<div class="card {{ product.featured ? 'card--featured' : '' }} {{ product.stock == 0 ? 'card--sold-out' : '' }}">
```

### enum() and enum_cases() — PHP Enum Access

**Do:**

```twig
{% if order.status == enum('App\\Enum\\OrderStatus').Paid %}
    <span class="badge-success">{{ order.status.value }}</span>
{% endif %}

{% for status in enum('App\\Enum\\OrderStatus').cases %}
    <option value="{{ status.value }}" {{ order.status == status ? 'selected' }}>
        {{ status.name|trans }}
    </option>
{% endfor %}

{# Dynamic case access #}
{% set case_name = 'Active' %}
{{ enum('App\\Enum\\Status').(case_name).value }}
```

**Don't:**

```twig
{% if order.status.value == 'paid' %}
```

### {% cache %} — Fragment Caching

**Do:**

```twig
{% cache "product_card;v1;" ~ product.id ~ ";" ~ product.updatedAt.timestamp
    ttl(3600) tags(['product-' ~ product.id]) %}
    <div class="product-card">
        {{ product.name }} — {{ product.price|format_currency('EUR') }}
    </div>
{% endcache %}
```

### {% types %} — Variable Type Declarations

**Do:**

```twig
{% types {
    product: 'App\\Entity\\Product',
    is_preview?: 'boolean',
    tags?: 'string[]',
} %}

<h1>{{ product.name }}</h1>
```

### Null-Safe Operator and Null Coalescing

**Do:**

```twig
{{ user?.address?.city ?? 'N/A' }}
{{ order?.customer?.company?.name }}

{# Dynamic attribute access #}
{{ user.('first-name') }}
{{ object.(dynamicMethod)() }}
```

**Don't:**

```twig
{% if user is defined and user.address is not null %}
    {{ user.address.city }}
{% else %}
    N/A
{% endif %}
```

### {% embed %} — Micro-Layout Skeletons

**Do:**

```twig
{# templates/_card.html.twig #}
<div class="card">
    <div class="card-header">{% block header %}{% endblock %}</div>
    <div class="card-body">{% block body %}{% endblock %}</div>
</div>

{# Usage — override blocks in-place #}
{% embed '_card.html.twig' %}
    {% block header %}Order #{{ order.id }}{% endblock %}
    {% block body %}
        {{ order.total|format_currency('EUR') }}
    {% endblock %}
{% endembed %}
```

**Don't:** Create separate include templates for each card variation when they share the same structure.

### |u and |slug — Unicode String Operations

**Do:**

```twig
{{ title|u.truncate(50, '…') }}
{{ title|u.truncate(100, '…', false) }}
{{ article.body|u.wordwrap(80) }}
{{ 'UserProfile'|u.snake }}
{{ 'some_method'|u.camel.title }}
{{ title|u.truncate(30).upper }}
{{ 'Héllo Wörld'|slug }}
```

**Don't:**

```twig
{{ title|slice(0, 50) }}…
```

### String Interpolation and Shorthand Mappings

**Do:**

```twig
{{ "Hello #{user.name}, you have #{count} items" }}

{# Shorthand mapping keys (v3.12) #}
{% set data = {name, email, role} %}

{# Spread operator #}
{% set merged = {class: 'btn', ...attrs} %}
{{ 'Hello %s %s!'|format(...names) }}
```

**Don't:**

```twig
{{ 'Hello ' ~ user.name ~ ', you have ' ~ count ~ ' items' }}
```

### {% guard %} — Conditional Compilation

**Do:**

```twig
{% guard function importmap %}
    {{ importmap('app') }}
{% else %}
    <script src="{{ asset('build/app.js') }}"></script>
{% endguard %}
```

### {% apply %} — Block-Level Filters

**Do:**

```twig
{% apply spaceless %}
    <nav>
        <ul>
            {% for item in menu %}
                <li><a href="{{ item.url }}">{{ item.label }}</a></li>
            {% endfor %}
        </ul>
    </nav>
{% endapply %}
```

### Macros — Named Arguments and Self-Reference

**Do:**

```twig
{% macro icon(name, size = 16, class = '') %}
    <svg class="icon {{ class }}" width="{{ size }}" height="{{ size }}">
        <use href="#icon-{{ name }}"/>
    </svg>
{% endmacro %}

{{ _self.icon(name: 'check', size: 24, class: 'text-green') }}
```

### Destructuring (v3.23)

**Do:**

```twig
{% do {name, email} = user %}
<p>{{ name }} ({{ email }})</p>

{% do [first, , third] = items %}

{# Rename during destructuring #}
{% do {name: userName, email: userEmail} = user %}
```

### Inline Comments (v3.15)

**Do:**

```twig
{{
    product.price # raw price before tax
    |format_currency('EUR')
}}
```

### |sanitize_html — Safe User HTML

**Do:**

```twig
{{ user_content|sanitize_html }}
```

**Don't:**

```twig
{{ user_content|raw }}
```

### {% deprecated %} — Template Deprecation

**Do:**

```twig
{% deprecated 'Use "sidebar.html.twig" instead.' package='app' version='2.0' %}
{% include 'sidebar.html.twig' %}
```

---

## Pitfalls

| Pitfall | Fix |
|---------|-----|
| `|date` for user-facing output | `|format_datetime` — locale-aware via ICU |
| `|number_format` for currency | `|format_currency('EUR')` — symbol, grouping, locale |
| `|slice` on multi-byte strings | `|u.truncate()` — Unicode-safe |
| String concat for CSS classes | `html_classes()` or `html_cva()` |
| Magic strings for enum values | `enum('App\\Enum\\Status').Active` — compile-time validated |
| Missing `twig/extra-bundle` | Required for `|u`, `|slug`, `|format_*`, `html_cva`, `{% cache %}` |
| `attribute()` for dynamic access | Deprecated v3.15 — use `object.(method)` syntax |
| `{% filter %}` tag | Removed — use `{% apply %}` instead |
| Verbose null checks | `?.` null-safe (v3.23) + `??` null coalescing |
| Copy-pasting card structures | `{% embed %}` for micro-layouts with overridable blocks |
| `|raw` on user content | `|sanitize_html` — allows safe subset of HTML |
| Positional args in complex calls | Named arguments with `:` — self-documenting, order-independent |

Twig (v3)

For Stimulus/Turbo/Live Components/AssetMapper, see frontend.mdc. For translation filters (|trans, t()), see i18n.mdc. For {% cache %} pool configuration and tag invalidation, see caching.mdc.

Core Principles

  1. Arrow functions over loops — Use |map, |filter, |find, |reduce, |sort with arrow functions instead of verbose {% for %} + {% if %} + {% set %} combos.
  2. Intl filters over manual formatting — Use |format_currency, |format_datetime, |format_number for locale-aware output. Never format with |date + string concatenation.
  3. html_cva() for variant styling — Define component class variants with html_cva(). Never build CSS classes with string concatenation or nested {% if %}.
  4. {% cache %} for fragment caching — Cache expensive template fragments with key, TTL, and tags. Requires twig/cache-extra.
  5. {% types %} for template contracts — Declare expected variables and their types. Enables TwigStan static analysis and self-documents templates.
  6. enum() for PHP enums — Access enum cases directly in templates. Never pass enum values as magic strings from controllers.
  7. Null-safe ?. over ternary guards — Use user?.address?.city instead of multi-line {% if defined %} chains.
  8. {% embed %} for micro-layouts — Combine include + extends for reusable structural skeletons with overridable blocks.
  9. |u for Unicode string ops — Use |u.truncate(), |u.wordwrap() for string manipulation. Never truncate with |slice (breaks multi-byte).
  10. Named arguments with : — Prefer range(low: 1, high: 10) over positional args. Available for filters, functions, tests, macros.
  11. twig/extra-bundle required — Most v3 features (|u, |slug, |format_*, html_cva, {% cache %}) need twig/extra-bundle + specific extras.

Conventions

Arrow Functions — Functional Collection Processing

Do:

{# Pipeline: filter → map → join #}
{{ users|filter(u => u.active)|map(u => u.name)|join(', ') }}

{# Find first matching element (v3.11) #}
{% set admin = users|find(u => u.role == 'admin') %}

{# Sort by property with spaceship operator #}
{% for p in products|sort((a, b) => a.price <=> b.price) %}
    {{ p.name }}: {{ p.price|format_currency('EUR') }}
{% endfor %}

{# Reduce to total #}
{{ cart.items|reduce((total, item) => total + item.price * item.qty, 0)|format_currency('EUR') }}

{# Extract single column from collection #}
{{ users|column('email')|join(', ') }}

{# has some / has every (iterable operators) #}
{% if items has every v => v.stock > 0 %}All in stock{% endif %}
{% if items has some v => v.featured %}Has featured{% endif %}

Don't:

{% set names = [] %}
{% for u in users %}
    {% if u.active %}
        {% set names = names|merge([u.name]) %}
    {% endif %}
{% endfor %}
{{ names|join(', ') }}

Intl Filters — Locale-Aware Formatting

Do:

{{ product.price|format_currency('EUR', locale: app.request.locale) }}

{{ order.createdAt|format_datetime('long', 'short', locale: 'fr') }}

{{ 0.15|format_number(style: 'percent') }}

{{ 42|format_number(style: 'ordinal', locale: 'en') }}

{{ 'FR'|country_name }}
{{ 'EUR'|currency_name('fr') }}
{{ 'en'|language_name('fr') }}

Don't:

{{ product.price|number_format(2, ',', ' ') }} €
{{ order.createdAt|date('d/m/Y H:i') }}

html_cva() — Class Variant Authority

Do:

{% set btn = html_cva(
    base: 'btn',
    variants: {
        color: { primary: 'btn-primary', danger: 'btn-danger' },
        size: { sm: 'btn-sm', lg: 'btn-lg' },
    },
    default_variant: { color: 'primary', size: 'sm' },
    compound_variants: [{
        color: ['danger'],
        size: ['lg'],
        class: 'font-bold uppercase',
    }]
) %}

<button class="{{ btn.apply({color, size}) }}">{{ label }}</button>

Don't:

<button class="btn {% if color == 'primary' %}btn-primary{% elseif color == 'danger' %}btn-danger{% endif %} {% if size == 'lg' %}btn-lg{% endif %}">
    {{ label }}
</button>

html_classes() — Conditional CSS Classes

Do:

<div class="{{ html_classes('card', {
    'card--featured': product.featured,
    'card--sold-out': product.stock == 0,
}) }}">

Don't:

<div class="card {{ product.featured ? 'card--featured' : '' }} {{ product.stock == 0 ? 'card--sold-out' : '' }}">

enum() and enum_cases() — PHP Enum Access

Do:

{% if order.status == enum('App\\Enum\\OrderStatus').Paid %}
    <span class="badge-success">{{ order.status.value }}</span>
{% endif %}

{% for status in enum('App\\Enum\\OrderStatus').cases %}
    <option value="{{ status.value }}" {{ order.status == status ? 'selected' }}>
        {{ status.name|trans }}
    </option>
{% endfor %}

{# Dynamic case access #}
{% set case_name = 'Active' %}
{{ enum('App\\Enum\\Status').(case_name).value }}

Don't:

{% if order.status.value == 'paid' %}

{% cache %} — Fragment Caching

Do:

{% cache "product_card;v1;" ~ product.id ~ ";" ~ product.updatedAt.timestamp
    ttl(3600) tags(['product-' ~ product.id]) %}
    <div class="product-card">
        {{ product.name }} — {{ product.price|format_currency('EUR') }}
    </div>
{% endcache %}

{% types %} — Variable Type Declarations

Do:

{% types {
    product: 'App\\Entity\\Product',
    is_preview?: 'boolean',
    tags?: 'string[]',
} %}

<h1>{{ product.name }}</h1>

Null-Safe Operator and Null Coalescing

Do:

{{ user?.address?.city ?? 'N/A' }}
{{ order?.customer?.company?.name }}

{# Dynamic attribute access #}
{{ user.('first-name') }}
{{ object.(dynamicMethod)() }}

Don't:

{% if user is defined and user.address is not null %}
    {{ user.address.city }}
{% else %}
    N/A
{% endif %}

{% embed %} — Micro-Layout Skeletons

Do:

{# templates/_card.html.twig #}
<div class="card">
    <div class="card-header">{% block header %}{% endblock %}</div>
    <div class="card-body">{% block body %}{% endblock %}</div>
</div>

{# Usage — override blocks in-place #}
{% embed '_card.html.twig' %}
    {% block header %}Order #{{ order.id }}{% endblock %}
    {% block body %}
        {{ order.total|format_currency('EUR') }}
    {% endblock %}
{% endembed %}

Don't: Create separate include templates for each card variation when they share the same structure.

|u and |slug — Unicode String Operations

Do:

{{ title|u.truncate(50, '…') }}
{{ title|u.truncate(100, '…', false) }}
{{ article.body|u.wordwrap(80) }}
{{ 'UserProfile'|u.snake }}
{{ 'some_method'|u.camel.title }}
{{ title|u.truncate(30).upper }}
{{ 'Héllo Wörld'|slug }}

Don't:

{{ title|slice(0, 50) }}…

String Interpolation and Shorthand Mappings

Do:

{{ "Hello #{user.name}, you have #{count} items" }}

{# Shorthand mapping keys (v3.12) #}
{% set data = {name, email, role} %}

{# Spread operator #}
{% set merged = {class: 'btn', ...attrs} %}
{{ 'Hello %s %s!'|format(...names) }}

Don't:

{{ 'Hello ' ~ user.name ~ ', you have ' ~ count ~ ' items' }}

{% guard %} — Conditional Compilation

Do:

{% guard function importmap %}
    {{ importmap('app') }}
{% else %}
    <script src="{{ asset('build/app.js') }}"></script>
{% endguard %}

{% apply %} — Block-Level Filters

Do:

{% apply spaceless %}
    <nav>
        <ul>
            {% for item in menu %}
                <li><a href="{{ item.url }}">{{ item.label }}</a></li>
            {% endfor %}
        </ul>
    </nav>
{% endapply %}

Macros — Named Arguments and Self-Reference

Do:

{% macro icon(name, size = 16, class = '') %}
    <svg class="icon {{ class }}" width="{{ size }}" height="{{ size }}">
        <use href="#icon-{{ name }}"/>
    </svg>
{% endmacro %}

{{ _self.icon(name: 'check', size: 24, class: 'text-green') }}

Destructuring (v3.23)

Do:

{% do {name, email} = user %}
<p>{{ name }} ({{ email }})</p>

{% do [first, , third] = items %}

{# Rename during destructuring #}
{% do {name: userName, email: userEmail} = user %}

Inline Comments (v3.15)

Do:

{{
    product.price # raw price before tax
    |format_currency('EUR')
}}

|sanitize_html — Safe User HTML

Do:

{{ user_content|sanitize_html }}

Don't:

{{ user_content|raw }}

{% deprecated %} — Template Deprecation

Do:

{% deprecated 'Use "sidebar.html.twig" instead.' package='app' version='2.0' %}
{% include 'sidebar.html.twig' %}

Pitfalls

Pitfall Fix
` date` for user-facing output
` number_format` for currency
` slice` on multi-byte strings
String concat for CSS classes html_classes() or html_cva()
Magic strings for enum values enum('App\\Enum\\Status').Active — compile-time validated
Missing twig/extra-bundle Required for `
attribute() for dynamic access Deprecated v3.15 — use object.(method) syntax
{% filter %} tag Removed — use {% apply %} instead
Verbose null checks ?. null-safe (v3.23) + ?? null coalescing
Copy-pasting card structures {% embed %} for micro-layouts with overridable blocks
` raw` on user content
Positional args in complex calls Named arguments with : — self-documenting, order-independent
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource