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

Laravel Latte Laravel Package

miko/laravel-latte

Brings the Latte templating engine to Laravel, letting you use Latte's intuitive syntax, automatic escaping, and template inheritance alongside Blade for safer, cleaner views.

View on GitHub
Deep Wiki
Context7

Getting Started

miko/laravel-latte wires the Latte templating engine into Laravel's container and config system. It is a thin adapter, not a framework — treat it as "Laravel gets a Latte\Engine singleton and a config file," and you'll be productive within an hour.

First five minutes:

composer require miko/laravel-latte
php artisan vendor:publish --provider="Miko\LaravelLatte\LatteServiceProvider"
  1. Read the README before the source. With 4 stars and no dependents, the README is the only real usage contract. Confirm three things: (a) the config key (likely config/latte.php), (b) how the engine is exposed — facade, view engine, or container-bound Latte\Engine, and (c) where the compiled-template cache lives.
  2. Check bootstrap/providers.php / config/app.php — if the provider isn't auto-discovered, register it manually. This is the most common reason the package appears "broken."
  3. Set the cache path explicitly and verify it's writable:
// config/latte.php (names may differ slightly — match the published file)
'cache' => storage_path('framework/latte'),
'debug' => env('APP_DEBUG'),
  1. Create your first template at resources/views/welcome.latte:
{* resources/views/welcome.latte *}
<h1 n:block="title">Hello, {$name}</h1>
{if $users}
    <ul>
        <li n:foreach="$users as $user">{$user->name}</li>
    </ul>
{else}
    <p>No users yet.</p>
{/if}
  1. Render it. Depending on how the package registers, this is either view('welcome', [...]) (if it installs a view-engine driver keyed on .latte) or a direct engine call from a controller or the Latte facade:
Route::get('/', fn () => view('welcome', ['name' => 'Taylor', 'users' => []]));

First real use case: a marketing/landing page or an admin dashboard where you want Latte's n:attributes, structural inheritance, and context-aware escaping — not a full app rewrite. Keep mailables, error pages, and package views on Blade; introduce Latte page-by-page.


Implementation Patterns

Controller rendering

Wrap rendering behind a single helper so you can swap implementations later (see gotchas):

final class LatteRenderer
{
    public function __construct(private \Latte\Engine $engine) {}

    public function render(string $view, array $params = []): string
    {
        return $this->engine->renderToString(
            view_path($view . '.latte'), // or resource_path("views/{$view}.latte")
            $params
        );
    }
}

Then from controllers you return response($this->renderer->render('dashboard.index', $data)). If the package already installs a view() driver, use view('dashboard.index', $data) directly — less code, but you're coupled to the package's driver.

Registering Latte extensions

This is the pattern you'll use daily. Every Laravel convenience Blade gives you needs a small Latte extension: route(), asset(), csrf_field(), @auth/@guest checks, old().

// app/Latte/LaravelFunctionsExtension.php
final class LaravelFunctionsExtension extends \Latte\Extension
{
    public function getFunctions(): array
    {
        return [
            'route'  => fn (string $name, array $params = []) => route($name, $params),
            'asset'  => fn (string $path) => asset($path),
            'old'    => fn (string $key, $default = null) => old($key, $default),
            'auth'   => fn () => auth()->check(),
            'can'    => fn (string $ability, $subject = null) => auth()->user()?->can($ability, $subject) ?? false,
        ];
    }

    public function getFilters(): array
    {
        return [
            'markdown' => fn (string $s) => Str::markdown($s),
            'money'    => fn (float $n) => number_format($n, 2),
        ];
    }
}

Register it once and — this is the payoff — every template gets Laravel semantics without a Blade bridge.

// AppServiceProvider::boot()
app(\Latte\Engine::class)->addExtension(new \App\Latte\LaravelFunctionsExtension());

Layouts with Latte blocks, not Blade sections

Do not try to mimic @extends/@yield. Latte's native inheritance is cleaner:

{* resources/views/layout.latte *}
<!doctype html>
<html>
<head><title>{block title}App{/block}</title></head>
<body>
    {include nav}
    <main>{block content}{/block}</main>
</body>
</html>
{* resources/views/users/index.latte *}
{layout 'layout.latte'}

{block title}Users{/block}

{block content}
    <a href="{route('users.create')}">New user</a>
    <table>
        <tr n:foreach="$users as $user">
            <td>{$user->name}</td>
            <td><a href="{route('users.edit', ['user' => $user])}">Edit</a></td>
        </tr>
    </table>
{/block}

n:attributes (n:if, n:foreach, n:href) compile to plain PHP and compose well with plain {if} — mix them freely.

CSRF and forms

There is no @csrf tag. Add one to your extension set:

'csrf' => fn () => new \Latte\Runtime\Html(
    '<input type="hidden" name="_token" value="' . csrf_token() . '">'
),

Then <form method="post">{csrf()|noescape}</form> — or, since your extension returns Html, just {csrf()}. Returning Latte\Runtime\Html from extension functions is the correct way to mark something as pre-escaped.

Coexistence with Blade

Run both engines side-by-side. A pragmatic split:

Blade Latte
resources/views/mail/** New application pages
errors/*.blade.php Admin dashboards
vendor/* publishes Long-lived marketing pages

Route by view path, not by feature — a developer should never guess which engine a template uses.

Testing

Latte's biggest test win: templates compile to plain PHP, so renderToString is fast and assertable.

it('renders user list', function () {
    $engine = app(\Latte\Engine::class);
    $engine->setTempDirectory(storage_path('framework/testing/latte'));

    $html = $engine->renderToString(resource_path('views/users/index.latte'), [
        'users' => collect([(object) ['name' => 'Ada']]),
    ]);

    expect($html)->toContain('Ada');
});

Clear the testing temp directory in setUp() so a stale compiled template doesn't mask a test failure.


Gotchas and Tips

The compiled cache will bite you in dev

Latte compiles templates to PHP files and only recompiles when the source mtime changes. That means:

  • Config changes (extensions, filters) require clearing the cache directory — rm -rf storage/framework/latte/*.
  • Docker bind mounts are the classic trap: mtimes get rewritten by the host, or the container sees a future timestamp and never invalidates. Set debug mode (which disables caching via $engine->setAutoRefresh(true)) explicitly in local, and never rely on file mtimes in CI.
  • Ensure the cache directory exists and is writable; a missing directory surfaces as a confusing Latte\CompileException rather than a permission error.

Make this a pre-flight step in your local setup script:

if (app()->environment('local')) {
    app(\Latte\Engine::class)->setAutoRefresh(true);
}

There is no Laravel helper surface

route(), asset(), csrf_field(), old(), @auth, pagination links, @vite — none of it exists out of the box. Budget a half-day to write one LaravelFunctionsExtension before you start converting pages, otherwise every template becomes a scavenger hunt.

Escaping is context-aware — don't fight it

Latte escapes based on surrounding HTML, JS, or CSS context. This is stronger than Blade. The rules:

  • Raw output needs |noescape — use it only for \Latte\Runtime\Html values you control.
  • {$user->bio|noescape} on user input is an XSS hole; if you're storing HTML, render it with |noescape after sanitizing, not before.
  • {$value} inside a <script> block is JS-escaped, not HTML-escaped. Don't |noescape there "to make JSON work" — use {$data|json} or |dataStream.

Syntax collisions with JS and CSS

{ is a control character. Inline <script> and <style> blocks containing object literals or CSS rules will be parsed as Latte tags. Fixes, in order of preference:

  1. Use {literal}...{/literal} around the block.
  2. Extract JS/CSS to files served through Vite (recommended — this is the only clean answer for anything non-trivial).
  3. Change delimiters globally if you must.

Register extensions in one place only

The engine is typically a singleton. If you add extensions in both a service provider and a middleware, or twice in boot(), you'll get duplicate-function CompileExceptions. Centralize registration in a single LatteServiceProvider of your own.

Error pages and debugging

Compile-time errors arrive as Latte\CompileException with file, line, and the offending source snippet — genuinely good. Runtime errors, though, are plain PHP exceptions thrown from a compiled file under storage/framework/latte/, so stack traces point at generated code. When debugging:

  • Open the compiled file. It's readable PHP and tells you exactly what Latte generated.
  • Latte has a Tracy bridge, but pairing it with Laravel's handler is fiddly — rely on whoops/Ignition plus opening the compiled file instead.

Be deliberate about adoption

With 4 stars and zero dependents, this is a single-maintainer integration (license MIT, recent release, but a small blast radius). Mitigations that cost nothing:

  • Pin miko/laravel-latte to ^X.Y in composer.json and commit the lock file.
  • Wrap rendering behind your own LatteRenderer (see above) so a future swap to raw Latte\Engine or back to Blade is a one-file change, not a find-and-replace.
  • Do not convert the whole app. Pick page-level views, keep Blade for mailables, error pages, and anything a package publishes.

Extension points worth knowing

If the package's config is limiting you, the real extension surface is Latte itself, not the package:

  • $engine->addExtension() — functions, filters, tags, providers.
  • $engine->setLoader(new \Latte\Loaders\FileLoader($baseDir)) — control template resolution; useful for namespaced views.
  • $engine->setSandboxMode() — mandatory if any template source is user-editable.
  • Custom tags via Latte\Extension::getTags() — the escape hatch when you need something like {vite ...} without a function call.
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.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
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
cgv/localized-resource