Parse Smarty templates into a typed AST with source positions and recoverable diagnostics.
composer require 3n9/smarty-ast<?php
require __DIR__ . '/vendor/autoload.php';
use SmartyAst\Parser\SmartyParser;
$parser = new SmartyParser();
$result = $parser->parseString("{if \$user.active}{\$user.name}{else}Guest{/if}");
$ast = $result->ast; // DocumentNode
$diagnostics = $result->diagnostics; // Diagnostic[]SmartyAst\Parser\SmartyParser::parseString(string $source, ?ParseOptions $options = null): ParseResultSmartyAst\Parser\SmartyParser::parseFile(string $path, ?ParseOptions $options = null): ParseResult
SmartyAstParser is a stateful facade over SmartyParser that accepts an optional custom parser instance — useful for dependency injection:
use SmartyAst\SmartyAstParser;
$parser = new SmartyAstParser(); // wraps a default SmartyParser internally
$result = $parser->parseString('{$foo}');ParseResult:
ast—DocumentNodediagnostics—Diagnostic[]tokens— optional, whencollectTokens=true
ParseResult also exposes convenience serialisation:
$result->toArray(); // ['ast' => [...], 'diagnostics' => [...], 'tokens' => [...]]
$result->toJson(JSON_PRETTY_PRINT); // JSON stringEvery Node likewise supports:
$node->toArray(); // plain PHP array
$node->toJson(); // JSON string (passes optional $flags to json_encode)use SmartyAst\Comments\PhpDocTemplateAnnotationParser;
use SmartyAst\ParseOptions;
$options = new ParseOptions(
leftDelimiter: '{',
rightDelimiter: '}',
recoverErrors: true,
collectTokens: false,
commentParsers: [new PhpDocTemplateAnnotationParser()], // default; pass [] to disable
phpVersion: '8.1', // gates PHP 8+ named-argument syntax (e.g. func(name: $val))
);Parser errors and warnings are returned as Diagnostic objects alongside the AST:
foreach ($result->diagnostics as $d) {
printf(
"[%s] %s at %d:%d\n",
$d->code,
$d->message,
$d->span->start->line,
$d->span->start->column
);
}Each diagnostic has:
code— unique error code (e.g.PARSE001,EXPR001)message— human-readable descriptionseverity—Severity::Error,Severity::Warning, orSeverity::Infospan— source locationrecoverable— whether the parser produced a partial AST node
Every AST node carries a full source span:
span->start->offset,span->start->line,span->start->columnspan->end->offset,span->end->line,span->end->column
Suitable for precise linter ranges, IDE highlights, and CI annotations.
All nodes implement children(): list<Node> for recursive traversal:
use SmartyAst\Ast\Node;
use SmartyAst\Ast\TagLike;
function walk(Node $node): void {
if ($node instanceof TagLike) {
$tag = $node->resolveTag();
echo "{$tag->name} at line {$tag->span->start->line}\n";
}
foreach ($node->children() as $child) {
walk($child);
}
}
walk($result->ast);TagLike is implemented by both TagNode (inline tags) and BlockTagNode (block tags with children/else branches). resolveTag() returns the underlying TagNode in both cases.
For expression-level traversal, ExpressionNode subclasses implement childExpressions(): list<ExpressionNode>.
For structured depth-first traversal implement SmartyAst\Visitor\NodeVisitorInterface and call Node::walk():
use SmartyAst\Ast\Node;
use SmartyAst\Ast\TagNode;
use SmartyAst\Visitor\NodeVisitorInterface;
final class TagCollector implements NodeVisitorInterface
{
/** @var TagNode[] */
public array $tags = [];
public function enterNode(Node $node): void
{
if ($node instanceof TagNode) {
$this->tags[] = $node;
}
}
public function leaveNode(Node $node): void {}
}
$visitor = new TagCollector();
$result->ast->walk($visitor);
// $visitor->tags — all TagNodes in document orderenterNode is called before a node's children are visited; leaveNode is called after.
Finds a named argument case-insensitively. When the tag uses shorthand syntax ($isShorthand === true) the first positional argument is returned as a fallback:
// {include file='header.tpl'} or {include 'header.tpl'}
$fileArg = $tag->findArgument('file');Safely extract a typed value from a literal node (returns null when the literal is a different type):
$node->asString(); // ?string — only for string literals
$node->asInt(); // ?int
$node->asFloat(); // ?float
$node->asBool(); // ?bool// All variable names (without $) referenced in an expression, including property paths
$names = $expr->collectVariableNames(); // list<string>
// Number of leaf operands in a binary-expression tree (parenthesised groups are unwrapped)
$count = $expr->countBinaryOperands(); // intThe parser supports both named and positional arguments:
{include file='header.tpl'} {* named *}
{include 'header.tpl'} {* shorthand *}TagNode::$isShorthand is true when positional shorthand syntax is used.
Comment parsing is pluggable via CommentParserInterface.
Built-in: SmartyAst\Comments\PhpDocTemplateAnnotationParser — parses phpDoc-style
annotations (@var, @param, custom tags) inside Smarty comments ({* ... *}).
Custom plugin:
use SmartyAst\Ast\CommentNode;
use SmartyAst\Comments\CommentParseContext;
use SmartyAst\Comments\CommentParseResult;
use SmartyAst\Comments\CommentParserInterface;
final class MyCommentParser implements CommentParserInterface
{
public function parse(CommentNode $comment, CommentParseContext $context): CommentParseResult
{
// Inspect $comment->text and return parsed annotations/diagnostics.
return new CommentParseResult();
}
}Register it via ParseOptions:
use SmartyAst\ParseOptions;
use SmartyAst\Parser\SmartyParser;
$options = new ParseOptions(commentParsers: [new MyCommentParser()]);
$result = (new SmartyParser())->parseFile('path/to/template.tpl', $options);- Parse each
.tplfile withparseFile() - Emit
$result->diagnosticsfor parser errors - Walk the AST with custom rules
- Exit non-zero if any issues exist
- Run the parser over changed templates
- Convert
Diagnosticspans to CI annotations (GitHub, GitLab, etc.)
- Parse on save or with debounce while typing
- Use
recoverErrors: truefor a partial AST during invalid intermediate states - Use node spans for squiggles, hover ranges, and quick fixes
composer testThe parser handles Smarty/PHP-style expressions in tags and print statements:
- variable / property / array access
{$foo},{$foo[4]},{$foo.bar},{$foo.$bar}{$foo->bar()},{$object->method1($x)->method2($y)}- static access:
{Cls::method()},{Cls::$prop},{Cls::CONST}
- assignment
{$foo=$bar+2},{$foo.bar=1},{$foo[]=1}
- arrays (including multiline)
[1, 2, 3],['k' => 'v', 'b' => 'c']
- spread operator (
...)- in calls:
{func(...$args)} - in array literals:
[...$a, ...$b]
- in calls:
- modifiers
{$foo|upper},{$foo|truncate:80:'...'}, chained:{$foo|escape:'html'|nl2br}- represented as
ModifierChainExpressionNodewrapping the base expression and a list ofModifierNodes
- string interpolation
- double-quoted:
"hello $name",`$foo` - embedded blocks:
"status is {if $ok}ok{/if}" - single-quoted strings are not interpolated
- double-quoted:
- bitshift operators —
>>and<< - PHP 8 named arguments (when
phpVersion >= '8.0'){func(name: $val, other: 42)}; each argument becomes aNamedArgumentExpressionNodewithname: stringandvalue: ExpressionNode
- operators
- ternary:
a ? b : c, elvis:a ?: c, null coalescing:a ?? c - symbolic:
&&,||,!==,===,!=,==,<,>,<=,>= - word aliases:
and,or,eq,ne/neq,gt,lt,gte/ge,lte/le,mod matches—$subject matches $patternlowers topreg_match($pattern, $subject); produces aCallExpressionNode(not a binary expression) with calleepreg_matchand arguments[$pattern, $subject]
- ternary:
- Smarty predicates
is [not] div by,is [not] even [by],is [not] odd [by],is [not] in
Leading and trailing whitespace around a tag can be trimmed by placing - inside the delimiter:
{- $foo -} {* trims whitespace before and after *}
{- include file='x.tpl'} {* trims only leading whitespace *}
{if $x -}content{- /if} {* trim after open-tag and before close-tag *}TagNode and PrintNode gain trimLeft: bool / trimRight: bool.
BlockTagNode gains closeTrimLeft: bool / closeTrimRight: bool for the close tag.
{literal}…{/literal} and {php}…{/php} are raw-content block tags — their inner content is captured as TextNode children without any template parsing. Useful for JavaScript snippets or legacy inline PHP.
{literal}
var x = {a: 1}; {* braces not parsed *}
{/literal}
{php}
echo "hello"; {* inner content is raw text *}
{/php}{#foo#}— equivalent to{$smarty.config.foo}