phug/reader
Phug Reader is a PHP library for reading Pug templates and turning them into a structured syntax tree. It serves as the parsing foundation for the Phug template engine, enabling developers to inspect, transform, and compile Pug source reliably in PHP applications.
Install it as a normal Composer dependency — the reader has no service provider or framework integration:
composer require phug/reader
You now have the two-stage front end of the Phug pipeline (phug/lexer → phug/parser) behind one call. The typical entry point is a single parse call that turns a Pug source string into a node tree:
use Phug\Reader;
$document = (new Reader())->parse(
"div.container\n h1.title Hello\n p= \$name",
__DIR__ . '/views/page.pug' // pass the real path, see Gotchas
);
var_dump($document->getName()); // "document"
var_dump($document->getChildren()); // block/nodes…
Where to look first, in order:
phug/lexer and phug/parser — this package is a thin convenience layer over them. Any behaviour you cannot explain lives in those two repos, not here.Phug\Parser\Node\* classes — that's your working vocabulary (DocumentNode, ElementNode, TextNode, AttributeNode, ConditionalNode, MixinCallNode, IncludeNode, …).The first thing most people actually want this for is not rendering HTML (that's phug/renderer), but inspecting a template: building a small script that parses every .pug file in resources/views and prints the tag tree. That single script proves the package works in your environment and is immediately extendable into a class/ID inventory, a lint pass, or a migration report.
Parse once, walk many times. Lexing + parsing is the expensive half of the Phug pipeline. In any tool that inspects the same template repeatedly (linters, generators, watch loops), parse into an AST up front, keep it in memory keyed by realpath, and run all your analyses against the tree. Only reach for phug/compiler or phug/renderer when you actually need output.
Recursive visitor as the default shape. Node traversal is uniform: getChildren() plus a getName() switch. A generic collector covers most day-to-day needs:
function walk(NodeInterface $node, callable $fn): void
{
$fn($node);
foreach ($node->getChildren() as $child) {
walk($child, $fn);
}
}
$classes = [];
walk($document, function ($node) use (&$classes) {
if ($node->getName() === 'element' && $node->hasAttribute('class')) {
$classes[] = (string) $node->getAttribute('class')->getValue();
}
});
Use it as a gatekeeper, not a renderer. A very common production workflow: parse the template in CI to fail fast on syntax errors with file/line context, then let the normal renderer handle the happy path. Parsing is cheaper than rendering and produces much more precise diagnostics than "the page 500'd".
Combine with phug/compiler for targeted compilation. If you only need to render a fragment (say, a mixin extracted from a large layout), parse to find the node, then hand that subtree — not the whole file — to the compiler. This is the clean way to build "render one component" endpoints.
Dependency/inventory pass before deploys. Walk the tree for include, extends, and mixin-call nodes to produce an asset manifest (which partials does this view touch?) for cache warming, cache busting, or precompiling. Note that resolution itself is not the reader's job (see Gotchas).
Wrap it in a small domain service. Bind a TemplateInspector class into your container rather than calling new Reader() in controllers or commands. It gives you one place to inject options, memoise ASTs, and swap in a custom parser subclass later.
Options pass-through is your configuration surface. Whatever you pass as the third argument is forwarded down the lexer/parser chain — this is how you turn on/off features (pretty-print handling, custom keywords, etc.) without forking. Keep a single options array in config and spread it everywhere, so a Pug dialect change is a one-line edit.
Testing: because the tree is a plain object graph, assert on getName()/attribute values rather than on rendered strings. Fixture .pug files plus snapshot-style assertions on a flattened node list are far more stable than HTML diffs.
include, extends, and mixin definitions appear as nodes with unresolved paths/names. If you need the real file graph, use phug/renderer's dependency handling or write your own resolver against your view root.__FILE__-style absolute paths, not 'template.pug'.str_replace("\r\n", "\n"), strip BOM) before parsing when reading from disk, uploads, or a DB.getName() returns strings, and the vocabulary is version-sensitive. instanceof against Phug\Parser\Node\* classes is more refactor-safe than string matching. Snapshot your node-name usage when bumping versions — Phug's minor releases have historically renamed/tuned node internals.class="a #{b}" you need to walk the attribute's children. If you only need the raw source, the token/attribute helpers are friendlier than reconstructing from the tree.Phug\Parser/Phug\Lexer (e.g. addKeyword, addTokenHandler) and wiring those classes in through the options array. Treat any such customisation as fork-level code: it is not covered by this package's tests.parse() call is self-contained and independent — good for correctness (no cross-request bleed), but a watch loop that re-parses 500 templates will feel it. Add your own AST cache keyed on file mtime.error_reporting(E_ALL) once on a staging branch before rolling into production.How can I help you explore Laravel packages today?