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

Think Template Laravel Package

topthink/think-template

A lightweight PHP template engine for ThinkPHP that compiles templates into plain PHP. It supports template inheritance, includes, variable output, custom tags and caching, making view rendering fast, simple and easy to integrate into existing applications.

View on GitHub
Deep Wiki
Context7

Getting Started

think-template is the standalone extraction of ThinkPHP's view engine. If you have ever used {$var}, {volist}, or {extend} in a ThinkPHP app, this is the code that compiles it. It has no framework dependency, so it drops into small apps, CLI renderers, and email builders.

composer require topthink/think-template

Minimum viable setup — point it at a template directory and a writable compile directory, then render:

require __DIR__ . '/vendor/autoload.php';

$tpl = new \think\Template([
    'view_path'   => __DIR__ . '/view/',
    'cache_path'  => __DIR__ . '/runtime/tpl/',
    'view_suffix' => 'html',
]);

$tpl->assign('title', 'Invoices');
$tpl->assign('user', ['name' => 'Ada']);
$tpl->assign('items', [
    ['id' => 1, 'total' => 99.5],
    ['id' => 2, 'total' => 12.0],
]);

$tpl->display('invoice/index');   // echoes
// $html = $tpl->fetch('invoice/index'); // returns a string instead

view/invoice/index.html:

<h1>{$title}</h1>
<p>Hello, {$user.name}</p>

{volist name="items" id="item" empty="No line items"}
  <tr>
    <td>{$item.id}</td>
    <td>{$item.total|number_format=2}</td>
  </tr>
{/volist}

Three things to look at first:

  1. think\Template — the public surface (assign, fetch, display, config array). Everything else is compilation.
  2. think\template\taglib\Cx — the built-in tag library. This file is the documentation for the exact attributes each tag accepts (offset, length, mod, empty, key, etc.).
  3. The compiled file — after the first render, look in cache_path. It is plain PHP named after md5() of the template path. Reading it is the fastest way to understand what your template actually does.

First realistic use case: a controller-ish function that assigns data and renders one layout-based page, then reuses the same engine instance to fetch() an email body. That covers 90% of day-to-day usage.

Implementation Patterns

Render vs. return

display() echoes; fetch() returns a string. For anything that is not a direct HTTP response — emails, PDF bodies, caching, test assertions — always use fetch(). Passing vars directly as the second argument skips the assign dance:

$html = $tpl->fetch('mail/welcome', ['user' => $user, 'link' => $url]);

assign() merges into a persistent variable bag on the instance, so long-lived processes (queue workers, Swoole) must be careful: variables leak between renders. Either re-instantiate per render or overwrite every key you depend on.

Layouts, two ways

Global layout — applies to every render, controlled by config:

'layout_on'   => true,
'layout_name' => 'layout',
'layout_item' => '{__CONTENT__}',

Per-template inheritance — more explicit and easier to reason about:

<!-- view/page/about.html -->
{extend name="layout/base" /}

{block name="title"}About us{/block}

{block name="content"}
  <p>Body goes here.</p>
{/block}
<!-- view/layout/base.html -->
<html>
<head><title>{block name="title"}Default{/block}</title></head>
<body>{block name="content"}{/block}</body>
</html>

Use one mechanism per template tree. Mixing global layout_on with {extend} produces confusing nested output.

Partials and includes

{include file="public/header" /} (note the self-closing slash) pulls in another template at compile time — the file must exist when the template compiles, not when it renders. Include files inherit the current variable scope, which is convenient but makes it easy to accidentally depend on parent state. For partials rendered with different data, call fetch() from PHP and inject the result as a variable instead.

Loops and conditionals

{volist name="orders" id="order" key="i"}
  <tr class="{if $i % 2 == 0}even{else}odd{/if}">
    <td>{$i}</td>
    <td>{$order.customer}</td>
  </tr>
{/volist}

{foreach name="tags" item="tag" key="k"}
  <span>{$k}:{$tag}</span>
{/foreach}

{volist} is the workhorse (offset, length, mod, empty). {foreach} is thinner and closer to PHP semantics. Nested {volist} blocks must use distinct id values, or the inner loop overwrites the outer variable.

Comparison tags keep complex conditions out of attribute strings:

{notempty name="user.avatar"}
  <img src="{$user.avatar}">
{else/}
  <img src="/img/placeholder.png">
{/notempty}

{eq name="order.status" value="paid"}Paid{else/}Pending{/eq}

Available shorthands include eq, neq, gt, lt, egt, elt, heq, nheq, in, notin, between, notbetween, empty, notempty, defined, notdefined, even, odd, present, notpresent.

Filters and output shaping

Filters pipe the value through a PHP callable, left to right:

{$user.name|default='Guest'|htmlspecialchars}
{$price|number_format=2}
{$body|nl2br}
{$created_at|substr=0,10}

String arguments need quotes; arguments are comma-separated with no spaces. {:func($arg)} echoes an arbitrary function result — useful for {:csrf_field()}-style helpers, but the function must exist at runtime or you get a fatal error, not a template error.

Static asset placeholders

Rather than hardcoding CDN paths, define them once:

'tpl_replace_string' => [
    '__STATIC__' => '/assets',
    '__CDN__'    => 'https://cdn.example.com',
],

Then {load href="__STATIC__/app.css" /}, {js href="__STATIC__/app.js" /}, {css href="..." /}, or plain __STATIC__/logo.png in attributes.

Custom tags and extension points

  • Custom tag library: subclass think\template\Taglib, mirror Cx, and register it via the taglib_pre_load / taglib_build_in config keys.
  • Override a single tag: Template::extend('tagname', callable) maps a tag to your own compiler function.
  • Delimiters: tpl_begin/tpl_end and taglib_begin/taglib_end are config, not hardcoded. Change them if { clashes with your front-end stack.
  • Runtime config: the config array can be adjusted through the constructor; keep environment-specific values (paths, CDN hosts) out of the template files.

Testing rendered output

Because fetch() is pure, template tests are trivial: assign fixtures, assert str_contains($html, 'Ada'), and assert the escaping behaviour explicitly. Add a test that renders a malicious string (<script>) and proves it is escaped — this catches accidental removal of the default filter during refactors.

Gotchas and Tips

  • There is no automatic escaping. {$user.name} compiles to a raw echo. If the value contains user input, it is an XSS hole. Set 'default_filter' => 'htmlentities' globally and use {$html|raw} for the rare trusted fragment. Do not rely on reviewers catching every unescaped variable.
  • { collides with JavaScript and CSS. {literal}...{/literal} protects a block, but changing tpl_begin/tpl_end to something like {{/}} is usually less painful in modern projects. Also beware inline <script> with object literals.
  • Dot notation is array access. {$user.name} compiles to $user['name']. Objects need {$user->name} or a conversion to array first. On PHP 8, accessing a key on null is a warning, so guard optional data with {$x|default=''} or {notempty}.
  • {php} is disabled by default. tpl_deny_php defaults to true, and tpl_deny_func_list blocks dangerous callables. If a {php} block silently vanishes, this is why. Do not enable it because a template "needs logic" — move the logic into the data you assign.
  • Compiled templates are cached by file mtime. In development, set 'tpl_cache' => false or delete cache_path after edits. The classic symptom of a stale cache is editing a template and seeing nothing change. If mtimes are unreliable (NFS, containers, git checkouts preserving times), set tpl_cache => false or touch the file.
  • cache_path must exist and be writable. A permission failure surfaces as a confusing include error rather than a clear "cache not writable" message. Create the directory with the correct owner at deploy time.
  • Missing templates throw TemplateNotFoundException, not a PHP warning. The name is resolved relative to view_path, dots become directory separators, and view_suffix is appended — so pass 'invoice/index', not 'invoice/index.html'. Verify the resolved path before assuming a parser bug.
  • Debug from the compiled file. When output looks wrong, open the compiled PHP in cache_path and read the generated code. Ninety percent of "the template engine is broken" reports are a filter argument or a condition string the parser interpreted differently than expected.
  • condition attributes use ThinkPHP operators. {if condition="$a eq 1"} works, {if condition="$a === 1"} may not survive parsing intact. Prefer the comparison tags or plain equality for anything non-trivial.
  • Framework helpers are not available. {$Think.get.id}, {:url()}, {:lang()}, and the {load}/{js}/{css} URL resolution all expect ThinkPHP's helpers or __STATIC__ replacements. In standalone use, define these as plain functions or via tpl_replace_string, otherwise you get a fatal "call to undefined function".
  • strip_space is not free. It reduces payload size but will happily collapse whitespace inside <pre>, inline-block layouts, and text between inline elements. Test rendered output before enabling it.
  • Never treat templates as user-editable content. They compile to executable PHP. Keep view_path outside the document root, and never build template names from request parameters without an allow-list.
  • This is a low-level component (52 stars, no direct dependents). Almost everyone consumes it through think\View / the View facade in a full ThinkPHP app. If you are inside that framework, use the facade and let it handle paths and caching; reach for think\Template directly only when you need rendering without the framework — and pin the version in composer.json, since the API is stable but not versioned for third-party extension.
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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib