topthink/think-view
Official ThinkPHP view component for rendering templates in MVC applications. Assign variables, load view files, and output content through a simple API, integrating smoothly with ThinkPHP projects and compatible template engines.
topthink/think-view is not a template engine itself — it is the ThinkPHP view adapter that wires think-template (the engine) into the framework’s View contract. You almost never configure it in isolation; it arrives with topthink/framework and exists to expose $this->fetch() / view() inside controllers.
Minimal setup:
composer require topthink/think-view (usually already pulled in by topthink/framework). Verify topthink/think-template is also present — they version-lock together.config/view.php first. That single file controls view_suffix, view_dir_name, view_path, cache_path, tpl_cache, tpl_begin / tpl_end, taglib_begin / taglib_end, layout_on, layout_name, and the driver type.app/view/index/index.html.// app/controller/Index.php
public function index()
{
return view('index', ['name' => 'Alice', 'items' => [1,2,3]]);
}
<!-- app/view/index/index.html -->
<h1>Hello, {$name}</h1>
{volist name="items" id="item"}
<li>{$item}</li>
{/volist}
First use case to validate the driver works end-to-end: a single action, one template, one variable, then a {volist} loop. If that renders, the driver and engine are wired correctly.
Controller-driven rendering. The default workflow is return view('template', $vars) (which returns a Response) or $this->fetch('template', $vars) (which returns a string). Prefer view() in HTTP controllers so the response pipeline, status code, and headers stay intact.
Global variables. Use View::share($key, $value) in a middleware or service provider for things like $siteName, $user, or CSRF tokens, rather than repeating assign() in every action.
View::share('siteName', config('app.name'));
Layouts. Enable layout_on => true in config/view.php, create app/view/layout.html, and put {__CONTENT__} where the page body should inject. Layouts are a blunt global switch — if you need per-controller layout overrides, do it via View::assign('view_layout', ...) patterns or switch to inheritance.
Template inheritance and includes. For anything beyond a handful of pages, {extend name="base"} + {block name="content"} + {include file="partials/header"} is cleaner than the layout system because it is per-template and does not fight global config.
Custom taglibraries. Extend think\template\TagLib, name the class with a TagLib suffix, and register it via taglib_pre_load (auto-loaded per render) or taglib_build_in (globally available). This is the natural extension point for reusable UI components, e.g. an {avatar} tag.
Replacing the driver. Implement think\contract\View (or extend think\View), then set 'type' => \app\common\MyView::class in config/view.php. This is how you swap in Twig, Blade, or a preprocessor without touching controller code.
Rendering outside HTTP. View::fetch('report', $data) returns a string — good for HTML-to-PDF, email bodies, or queue jobs. Note that view() (the helper) returns a Response, so it is less useful in CLI consumers.
Testing. Because fetch() returns a string, you can assert on rendered output without booting a full HTTP kernel — instantiate the view with a test config pointing at a fixture template directory.
{ } delimiter collisions. The default tpl_begin/tpl_end delimiters are { and }, which clash with Vue, Alpine, Angular, and plain JS object literals. Wrap JS blocks in {literal}...{/literal}, or globally change the delimiters in config/view.php. Changing delimiters is a breaking change for every existing template — do it at project start, not mid-project.
Template cache is on by default in production. With tpl_cache => true, edits to .html files are not picked up until the compiled file in runtime/temp is invalidated (touch the source file or clear runtime/temp). Set tpl_cache => false locally and remember to clear runtime/temp on deploy.
view_suffix must match reality. The driver appends view_suffix (default html) when resolving a template name. If your templates are .tpl, you will get a silent “template not found” unless you set 'view_suffix' => 'tpl'.
No auto-escaping. Think-template outputs variables raw by default. {$user.bio} will happily emit a <script> tag. Escape manually ({$bio|htmlspecialchars}) or enforce a project convention. This is the single most common security foot-gun in this stack.
Layout placeholder is exact. {__CONTENT__} must appear literally in the layout file. A typo, a renamed block, or an include that shadows content will render an empty page with no error — the failure is silent.
Path resolution. {include file="..."} resolves relative to the view root, not the current module or template directory. In multi-app layouts this is a frequent surprise; prefer explicit paths from the view root.
Composer version pinning. think-view constrains think-template; manually bumping think-template can break tag parsing (e.g. taglib_begin being ignored, or {volist} silently unparsed). Let the constraints resolve together, and re-run composer update only with both in the same step.
Compiled cache is keyed on file mtime + config. Changing taglib_begin or tpl_begin in config does not invalidate previously compiled templates in every scenario — clear runtime/temp after any delimiter or taglib change, or you will chase ghosts.
fetch() needs an app instance. In pure CLI scripts (cron, standalone test bootstrap) View::fetch() may fail resolving view_path because the driver leans on app()->getAppPath(). Bootstrap the app or pass an explicit view_path in a scoped config.
Debugging aid. Dump View::engine()->getConfig() to confirm the effective config, and inspect runtime/temp to see the compiled PHP for the exact template — the compiled file reveals the resolved path, cache key, and any tag parse results. When a tag “doesn’t work”, reading the compiled file is faster than reading the docs.
Extension points worth knowing. taglib_pre_load (auto-register taglibs per render), taglib_build_in (global taglibs), view_filter hook/event (post-process the HTML string — useful for HTML minification or asset URL rewriting), and type (swap the entire driver). Anything more invasive (custom Template subclass) is usually a sign you should be changing the engine, not patching the adapter.
How can I help you explore Laravel packages today?