Skip to content

marko/view-latte

Latte templating driver for the Marko Framework. Implements ViewInterface from marko/view using the Latte engine, with module-namespaced template resolution, automatic caching, and strict types.

Terminal window
composer require marko/view-latte

This automatically installs marko/view. Note: marko/view-latte conflicts with marko/view-twig --- install only one view driver per project.

Configure via the view config key. Settings from marko/view (cache_directory, auto_refresh) apply to all drivers. The following are Latte-specific defaults:

config/view.php
return [
'cache_directory' => 'storage/views', // relative to the project root; created 0700
'auto_refresh' => true, // false in production, with view:cache on deploy
'extension' => '.latte',
'strict_types' => true,
];

Templates are rendered using the module namespace syntax:

use Marko\View\ViewInterface;
$view->render('blog::post/index', ['posts' => $posts]);

The format is module::path/to/template where:

  • module is the module name (e.g., blog, admin)
  • path/to/template is the path within resources/views/

The render() method returns an HTTP Response object. Use renderToString() when you need the raw HTML --- for example, when rendering email bodies:

use Marko\View\ViewInterface;
$html = $view->renderToString('blog::email/comment-verification/html', $data);
$text = $view->renderToString('blog::email/comment-verification/text', $data);

All template includes use the same namespaced syntax:

{include 'blog::post/list/item', post: $post}
{include 'blog::pagination/index', pagination: $posts}

Relative paths (../) are not supported. This ensures:

  • Consistent syntax throughout templates
  • Templates can include from any module
  • No fragile relative path dependencies

Pass variables to included templates as named parameters:

{include 'blog::post/list/item', post: $post, showAuthor: true}

Default values in the included template:

resources/views/post/list/item.latte
{* post/list/item.latte *}
{default $showAuthor = true}
{default $linkAuthor = true}
<li class="post-item">
<h2><a href="/blog/{$post->slug}">{$post->title}</a></h2>
{if $showAuthor}
<span>By {$post->getAuthor()->name}</span>
{/if}
</li>

The route() function builds URLs for named routes through UrlGeneratorInterface, so templates don’t hard-code paths:

resources/views/post/list/item.latte
<a href={route('blog.post.show', [slug: $post->slug])}>{$post->title}</a>
<a href={route('blog.index', [page: 2])}>Next page</a> {* /blog?page=2 *}
<link rel="canonical" href={route('blog.post.show', [slug: $post->slug], absolute: true)}>

The arguments match UrlGeneratorInterface::route(string $name, array $parameters = [], bool $absolute = false). Parameters the path doesn’t use become the query string, and absolute: true uses APP_URL. Output is escaped like any other expression, so & in a query string renders as &amp; inside an attribute. An unknown name or a missing parameter throws UrlGenerationException and fails the render.

signedRoute() builds a signed URL through UrlSignerInterface, for links that leave the app, such as an unsubscribe link in an email template. It is absolute by default. Pass a DateTimeImmutable view variable as expiresAt for a link that expires:

resources/views/mail/newsletter.latte
<a href={signedRoute('newsletter.unsubscribe', [id: $subscriber->id])}>Unsubscribe</a>
<a href={signedRoute('share.show', [token: $token], expiresAt: $expiresAt)}>Open (valid for 24 hours)</a>

route() comes from RouteFunctionProvider and signedRoute() from SignedRouteFunctionProvider, both in marko/view. Every template function provider is registered with Latte by TemplateFunctionExtension: a function declared isSafeHtml has its result wrapped in Latte\Runtime\Html and prints unescaped, every other result is escaped. Shared view data is merged into every render() and renderToString() call.

marko view:cache compiles every template into view.cache_directory ahead of time. This package binds LatteTemplatePrecompiler as the TemplatePrecompilerInterface the command uses:

Terminal window
marko view:cache

It builds an engine with LatteEngineFactory::createWithAutoRefresh(), from the same factory as rendering, and calls Latte\Engine::warmupCache(). The factory gives every engine it builds the module loader, so the compiled file lands under the key rendering looks up: the template’s resolved absolute path. A Latte\CompileException is rethrown as TemplatePrecompileException with the template name, file and line. A broken template fails the command with its file and line:

Template 'blog::post/show' failed to compile: Unexpected tag {notATag} (on line 2 at column 4)
in /var/www/app/blog/resources/views/post/show.latte:2

Only the template that wins resolution is compiled, so an app/ override is compiled and the module template it shadows is skipped. Auto-refresh is always on while precompiling, so a template changed since the last compile is compiled again even when auto_refresh is false.

In production, run view:cache on every deploy, on the server at the path the app runs from, and set auto_refresh to false. Compiled files are keyed by absolute path, so templates compiled during an image build at another path are not reused. See deploying to production.

All templates must live within at least one directory. No top-level template files.

views/
post/
index.latte # Post listing
show.latte # Single post
list/
item.latte # Reusable list item
category/
show.latte
tag/
index.latte
author/
show.latte
search/
index.latte
bar.latte # Search input
comment/
form.latte
thread.latte
pagination/
index.latte

Email templates group HTML and plain text versions together:

views/
email/
comment-verification/
html.latte # HTML version
text.latte # Plain text version
welcome/
html.latte
text.latte

LatteView implements ViewInterface and LatteTemplatePrecompiler implements TemplatePrecompilerInterface, both from marko/view.

MethodDescription
render(string $template, array $data = []): ResponseRender a template and return an HTTP response
renderToString(string $template, array $data = []): stringRender a template and return the raw HTML string
LatteTemplatePrecompiler::precompile(string $template): voidCompile one template into the cache; throws TemplatePrecompileException on a syntax error
LatteEngineFactory::create(): EngineBuild an engine that loads module::path templates and refreshes compiled templates per auto_refresh
LatteEngineFactory::createWithAutoRefresh(): EngineBuild the same engine with auto-refresh always on
FunctionDescription
route(string $name, array $parameters = [], bool $absolute = false): stringURL for a named route (Marko\View\Functions\RouteFunctionProvider in marko/view)
signedRoute(string $name, array $parameters = [], ?DateTimeImmutable $expiresAt = null, bool $absolute = true): stringSigned URL for a named route (Marko\View\Functions\SignedRouteFunctionProvider in marko/view)

Functions from every other discovered #[TemplateFunctions] provider are available the same way.

OptionTypeDescription
cache_directorystringDirectory for compiled template cache (from marko/view). Defaults to storage/views under the project root; the factory refuses a directory that is world-writable or owned by another user --- see compiled-template cache directory
auto_refreshboolRecompile templates when source changes --- set false in production and run view:cache on deploy (from marko/view)
extensionstringTemplate file extension (default .latte)
strict_typesboolEnable strict type checking in templates (default true)