phug/compiler
Phug Compiler is the core compiler for the Phug (Pug/Jade) template engine in PHP. It parses Pug's concise, indentation-based syntax and compiles templates into fast, cacheable PHP code, with support for custom dialects, extensions, and Laravel integration.
phug/compiler is the Pug → PHP stage of the Phug toolchain. It takes a Pug source string (or file), lexes/parses it with phug/lexer + phug/parser, and emits PHP source code that, when included, produces HTML. It does not execute anything — that is the job of phug/renderer (or your own include wrapper).
Install and smoke-test:
composer require phug/compiler
use Phug\Compiler;
$compiler = new Compiler([
'paths' => [__DIR__.'/views'], // where include:/extends: look for files
'basedir' => __DIR__.'/views',
'extensions' => ['pug', 'jade'], // tried when a path has no extension
]);
$php = $compiler->compile('p Hello #{name}', __DIR__.'/views/hello.pug');
// $php now looks roughly like:
// <?php echo 'Hello ', htmlspecialchars($name, ENT_QUOTES, 'UTF-8'); ?>
file_put_contents(app('config')['view.compiled'].'/hello.php', $php);
Where to look first
| Path | Why you care |
|---|---|
src/Phug/Compiler.php |
Public API: compile(), compileFile(), compileDocument(), options, events |
src/Phug/Compiler/NodeCompiler/ |
One class per node type (TagNodeCompiler, ConditionalNodeCompiler, …) — the real behavior lives here |
src/Phug/Compiler/Locator/ |
FileLocator / ChainLocator — how include, extends, block paths resolve |
src/Phug/Compiler/Exception/ |
The exact exceptions you'll catch (CompilerException, PathNotFoundException, …) |
First real use case: precompiling a Pug view to a PHP file inside a Laravel view engine, so the hot path is a plain include with zero parsing cost.
class PugEngine implements \Illuminate\Contracts\View\Engine
{
public function __construct(
protected Compiler $compiler,
protected Filesystem $files,
protected string $cachePath,
) {}
public function get($path, array $data = []): string
{
$cached = $this->cachePath.'/'.sha1($path).'.php';
if (! is_file($cached) || filemtime($cached) < filemtime($path)) {
$this->files->put($cached, $this->compiler->compileFile($path));
}
return (function () use ($cached, $data) {
extract($data, EXTR_SKIP);
ob_start();
include $cached;
return ob_get_clean();
})();
}
}
Register it in a service provider with $resolver->addEngineResolver('pug', fn () => new PugEngine(...)) plus View::addExtension('pug', 'pug').
This is the pattern that makes phug/compiler worth using directly. The compiled output is a normal PHP file: echo, if, foreach, htmlspecialchars. It can be opcache'd, profiled, and included with extract($data).
$compiler = new Compiler(['paths' => [$theme, $vendorViews]]);
$php = $compiler->compileFile($absolutePath);
$this->files->put($compiledPath, $php);
Guard with filemtime on both the source and every file it extends/includes — the compiler does not know about partial dependencies, so either record them (see §5) or bust the whole cache on deploy.
paths is ordered. A ChainLocator lets you plug a database- or S3-backed locator in front of the filesystem:
$compiler->setOption('paths', [
base_path('themes/'.$theme), // wins
base_path('vendor/views'), // fallback
]);
Because include foo without an extension is resolved through extensions, keeping 'pug' first avoids accidentally including a stray .php partial.
If you need runtime rendering (no cache dir, dynamic templates in the DB), skip phug/compiler's manual include and use phug/renderer, which composes this compiler. Use phug/compiler directly only when you need control over artifact creation — precompilation, linting, static analysis, CI checks.
A useful CI job: compile every .pug file in the repo and fail the build on CompilerException. You get template syntax errors at build time instead of at 2 a.m.
foreach (File::glob(resource_path('views/**/*.pug')) as $file) {
try {
(new Compiler(['paths' => [dirname($file)]]))->compileFile($file);
} catch (\Phug\Compiler\Exception\CompilerException $e) {
throw new \RuntimeException("Broken template: {$file}\n".$e->getMessage(), 0, $e);
}
}
Extending Phug means creating a parser node + a node compiler. A node compiler receives the node and the compiler and returns a formatted string (or a Phug\Formatter\ElementInterface). The compiler resolves a node class to its compiler by naming convention (FooNode → FooNodeCompiler), so keep names aligned or register an explicit mapping in the options. The node compiler's output is spliced into the generated PHP — always run user-facing strings through the formatter's escaping helpers rather than hand-rolling htmlspecialchars.
Compiler exposes an event manager. Hook the compile/output events to collect provenance — this is how you build a dependency graph for cache invalidation:
$compiler->on('compiler.output', function ($event) use (&$dependencies) {
// record the node/file being emitted into $dependencies
});
Also useful for timing instrumentation: log compile duration per file and alert when a single template exceeds a few hundred ms — usually a sign of a runaway loop in a mixin.
PugEngine as a resolved engine, not as a Blade extension. Blade's @include should stay Blade.storage/framework/views so php artisan view:clear does the right thing for free.View::composer, pass data through the normal Laravel pipeline — PugEngine::get() receives the already-merged $data, so $__env, $__data, etc. are present. Use EXTR_SKIP (as above) so a Pug variable named $data can't clobber your locals.views directory (Vite/Sail file watcher) or simply disable the mtime check when app()->isLocal().compile() returns PHP, not HTML. This is the single most common surprise. echo $compiler->compile('p hi') prints <?php echo 'hi' ?>. If you want HTML, you need the second step (include, phug/renderer, or eval).
Always pass the path as the second argument when compiling a string: compile($source, '/abs/path/file.pug'). Without it, relative include/extends cannot be resolved and you get PathNotFoundException (or a confusing "unknown path" style exception).
exit_on_error is dangerous in web context. Several Phug components will exit on error unless you pass 'exit_on_error' => false (and typically 'debug' => true outside production). Verify your options array before shipping — a malformed template silently returning a blank 200 is a bad time.
No built-in cache. phug/compiler has no cache_dir; that option belongs to phug/renderer. If you use the compiler standalone you own mtime checks, atomic writes, and dependency tracking. Write to a temp file and rename() — two concurrent requests compiling the same template can otherwise serve half a PHP file.
Dependency invalidation is on you. If layout.pug changes, every template that extends layout must be recompiled. Either clear the compiled directory on deploy, or track dependencies via the output event. Checking mtimes recursively at render time is usually slower than just compiling.
pretty costs real time. The compiled PHP is formatted by phug/formatter. 'pretty' => false yields denser generated code, faster compiles, and identical output. Set it to false in production and keep it true locally if you ever debug the compiled output.
Custom syntax requires both halves. Adding a FooNode to the parser without a matching FooNodeCompiler produces an UnknownNodeException at compile time, not parse time — the error message names the node class, which makes it easy to miss the first time.
Compiled files must never be web-reachable. They contain echo statements with your application's interpolations. Keep them outside public/ and named .php (opcache) with a hash, not the original template name.
Escaping differences between #{} and !{} and attributes. #{} escapes, !{} does not, and attribute values are escaped by default. !{} combined with user input is an XSS hole — grep for !{ during review.
Extension points worth knowing: custom LocatorInterface (DB/asset-store templates), custom node compilers (domain tags like card or price), and modules passed in the options array ('modules' => [...]) for bundling a set of node compilers plus their parser nodes. Modules are the cleanest way to distribute a Pug extension between projects.
Versioning: the package is actively released (latest 2026-03-15) but has no dependents on Packagist — you are near the bleeding edge of its user base. Pin a version in composer.json and read the changelog on upgrades; the AST node classes it consumes are owned by phug/parser, so a parser minor bump can require a compiler bump in lockstep.
Fallback ladder: if you find yourself reimplementing cache_dir, paths, error formatting, and an include wrapper, stop — you are rewriting phug/renderer. Drop down to phug/renderer (or pug-php/pug for the Laravel-idiomatic facade) and reach for phug/compiler only when you need the raw compiled artifact.
How can I help you explore Laravel packages today?