---
title: "Tree-sitter and PHP: Two Directions, Endless Possibilities"
url: https://www.exakat.io/tree-sitter-and-php-two-directions-endless-possibilities/
date: 2026-09-03
modified: 2026-09-03
author: "dams"
description: "Tree-sitter and PHP: Two Directions, Endless Possibilities Tree-sitter is a very popular library in the computer ecosystem at large. It powers syntax highlighting in Neovim, structural search in VS Code,..."
categories:
  - "Technology"
tags:
  - "parse"
  - "php"
  - "tree-sitter"
image: https://www.exakat.io/wp-content/uploads/2026/09/tree-sitter.320.png
word_count: 1659
---

# Tree-sitter and PHP: Two Directions, Endless Possibilities

# Tree-sitter and PHP: Two Directions, Endless Possibilities

[Tree-sitter](https://tree-sitter.github.io/tree-sitter/) is a very popular library in the computer ecosystem at large. It powers syntax highlighting in Neovim, structural search in VS Code, and a growing wave of static analysis tools, including in PHP environment. What makes it compelling is a combination of properties rarely found together: a full concrete syntax tree or CST, incremental re-parsing on every update, and first-class bindings for languages as different as Rust and PHP.

This post explores two distinct ways PHP and tree-sitter interact. Both pull in opposite directions.

- **From Rust, parsing PHP**: building fast PHP developer tools in Rust, with tree-sitter doing the heavy lifting of understanding PHP source code.
- **From PHP, parsing everything else**: using tree-sitter as a library inside PHP to parse other languages such as JavaScript, Python, JSON, or any of the 100+ supported languages.

## Building PHP Tools in Rust with Tree-sitter

### Why Rust?

PHP's ecosystem of static analysis, linting, and code formatting tools is mature, but it has a structural problem: they run on the PHP runtime itself. A large scan can take tens of seconds. Sometimes, developers reach for `--parallel` flags just to get bearable CI times.

Rust sidesteps the issue entirely. A native binary has no interpreter startup, no garbage collector pauses, and can spread work across all available cores without coordinating through a VM. The only thing still needed is a way to understand PHP code: that is exactly what the `tree-sitter-php`crate provides.

### The tree-sitter-php crate

The official grammar lives at [tree-sitter/tree-sitter-php](https://github.com/tree-sitter/tree-sitter-php). It ships bindings for Node.js, Python, Go, Swift and Rust via [crates.io/crates/tree-sitter-php](https://crates.io/crates/tree-sitter-php).

In Rust, integrating it is minimal:

use tree_sitter::Parser;fn main() {
let mut parser = Parser::new();
parser.set_language(&tree_sitter_php::LANGUAGE_PHP.into()).unwrap();let source = r#""#;
let tree = parser.parse(source, None).unwrap();
let root = tree.root_node();println!("{}", root.to_sexp()); // prints the S-expression CST
}

The resulting tree contains every token, including whitespace and comments: useful for a formatter that must be lossless.

### Mago: The Flagship Example

The most ambitious PHP tool currently built on this stack is [**Mago**](https://github.com/carthage-software/mago), by Carthage Software.

Mago is a full PHP toolchain in a single Rust binary: linter, formatter, static analyzer, and architectural guard. Its feature surface overlaps with PHPStan, Psalm, PHP-CS-Fixer, exakat and PHPCS: all at once, without having to run four separate PHP processes, and in a fraction of the execution time. Just make sure your favorite rule is actually included.

The performance numbers are striking. On a 500 files project, `mago check` completes in under a second. PHPCS on the same project takes 8–12 seconds. On a 2400 files project the gap widens to 30 or 40 times.

Mago does not use tree-sitter directly for its deepest analysis: it ships its own handwritten fault-tolerant parser for more control over error recovery. This demonstrates exactly the category of tool that tree-sitter enables: fast, multi-pass PHP analysis written entirely outside the PHP runtime.

Interestingly, Mago is now in its version 1.4 (August 2026), with versions coming out every other week. Quite impressive for a new tool.

### ast-grep: Structural Search Across 100+ Languages

[**ast-grep**](https://github.com/ast-grep/ast-grep), or `sg` for the initiated, is a Rust CLI that uses tree-sitter to power structural code search and rewriting. Think `grep`, but pattern-matched against the AST rather than raw text.

# Find every call to shell_exec() in a PHP codebase
sg --lang php -p 'shell_exec($CMD)'

Because the pattern matches the tree, it finds the call regardless of whitespace, line breaks, or irrelevant surrounding tokens. It supports over 26 languages out of the box, PHP included, making it a practical replacement for fragile regex-based code searches in CI pipelines.

### php-rust-tools/parser

Worth mentioning: [php-rust-tools/parser](https://github.com/php-rust-tools/parser) is a handwritten, fault-tolerant, recursive-descent PHP parser in Rust. It is not a tree-sitter dependency. It is the foundation for several emerging Rust-based PHP tools and offers finer-grained control over error recovery than a grammar-generated parser. Not tree-sitter, but part of the same wave of Rust-native PHP tooling.

## Using Tree-sitter from Within PHP

The reverse direction is less obvious but equally interesting: bring tree-sitter into PHP as an extension, and use it to parse other languages from PHP code.

### Why Would You Want This?

- Build a custom linter for a domain-specific language without leaving PHP
- Analyze JavaScript, TypeScript, or Python files from a PHP application
- Extract function signatures, imports, or dependency graphs from a polyglot codebase
- Write automated codemods that operate on non-PHP source files
- Write a transpiler that convert languages to PHP source code

The most useful feature is to write a custom linter: tree-sitter can work from manual written grammar, that you can tailor to your format needs. From the grammar, the engine gets the tree for you.

PHP's own tokenizer `token_get_all` only speaks PHP. Tree-sitter speaks everything. Why not build static analysis tools for other languages in PHP? After all, static analysis tools for PHP in other languages do exist.

### tree-sitter-language-pack: Batteries-Included, Polyglot

The most complete option is [xberg-io/tree-sitter-language-pack](https://github.com/xberg-io/tree-sitter-language-pack), a native PHP extension, built in Rust via [`ext-php-rs`](https://github.com/extphprs/ext-php-rs), not hand-written C, that ships pre-compiled grammars for 371 languages (PHP, HTML, CSS, JavaScript/JSX, TypeScript/TSX, JSON, Python, F#, R, Fortran, ...) behind an API shared with the project's Rust, Python, Node.js, Go, Java, C#, Ruby, and Elixir bindings. Parsers are downloaded on first use and cached locally, so installing the package doesn't pull all 371 grammars onto disk at once. It ships pre-packaged binaries and is PIE-compatible, so no local Rust toolchain is required. Note: the project was previously published as `kreuzberg-dev/tree-sitter-language-pack`; the org renamed to xberg-io in 2026 and the old GitHub URL now redirects: it's the same package, not a competing one.

composer require xberg-io/tree-sitter-language-pack

The API is clean and OOP:

This gives the following result:

>program
2
(program (php_tag) (echo_statement (binary_expression left: (encapsed_string (string_content)) right: (encapsed_string (string_content)))))

Tree-sitter never throws on malformed input: it parses through the error and marks the offending region as an `ERROR` node instead. Check for it explicitly, so you can easily validate code in any of the supported languages:

This shows the following result:

--- error detection ---
Syntax error detected
--- positional data ---
start: row 0, column 0
end: row 0, column 30

Every node exposes positional data: `startPosition()` and `endPosition()` return a `Point`with `row` and `column`. This makes it straightforward to produce diagnostic messages with precise line numbers.

Side note: on Mac OSX, I had to install it with PHP 8.4, not 8.5.

### Traversing the Tree: A Practical Example

Suppose you want to extract every `import` statement from a TypeScript file. With tree-sitter inside PHP, the walk is explicit but mechanical:

No regex, no fragile string manipulation. And the tree is structurally sound.

### Other Binding Options

If you prefer not to install a native extension, one alternative exists:

[talbergs/php-tree-sitter](https://github.com/talbergs/php-tree-sitter): FFI-based bindings, valid with PHP 8.0+ and `ext-ffi`. Pure PHP installation via Composer, loads the tree-sitter shared library at runtime. Lower performance ceiling than a native extension, but zero build step.

## The Performance Caveat: Tree-sitter vs token_get_all for PHP

Tree-sitter is impressively fast: it was designed to re-parse large files on every keystroke in an editor. But when your target language is PHP, and all you need is a flat token stream, PHP's native `token_get_all()`, or its object-oriented successor `PhpToken::getAll()` introduced in PHP 8.0, will beat tree-sitter consistently.

The reason is structural: `token_get_all` is a thin wrapper around the same C lexer PHP uses to execute your code. It runs in-process with zero FFI overhead, produces a flat array rather than a tree, and does no grammar inference. For straightforward tokenization: counting strings, scanning for function names, detecting encoding declarations. This is unbeatable.

Tree-sitter's value for PHP analysis lies in the tree, not the tokens. If you need to understand scope, resolve variable references, detect unreachable branches, or navigate the structural relationship between a method call and its arguments, the CST pays for itself. For a flat scan, it does not.

A rough rule of thumb from the field: tree-sitter PHP parsing runs about 2–5× slower than `token_get_all` on the same source. The gap narrows on incremental re-parses (tree-sitter's speciality), but for a single-pass bulk scan of a codebase, `token_get_all` / `PhpToken::getAll()` is still the PHP-native winner.

## Summary

| Direction | Approach | Key Tools |
| --------- | -------- | --------- |
| Rust to PHP | Parse PHP in Rust via tree-sitter | `tree-sitter-php` crate, Mago, ast-grep |
| PHP to Everything | Parse any language inside PHP | xberg-io/tree-sitter-language-pack, talbergs/php-tree-sitter, ext-treesitter |

Tree-sitter sits at an unusual intersection: it is fast enough for editors, expressive enough for static analysis, and portable enough to be embedded almost anywhere. The PHP ecosystem is discovering this from both ends simultaneously. Rust tools are consuming PHP grammars to build the next generation of blazing-fast developer tooling, while PHP itself is gaining the ability to understand every other language in the stack.

Whether you are writing a PHP quality tool in Rust or analysing your JavaScript bundle from a PHP script, tree-sitter is the parsing layer worth knowing.

*Sources and further reading:*

- [carthage-software/mago](https://github.com/carthage-software/mago): Mago PHP toolchain
- [tree-sitter/tree-sitter-php](https://github.com/tree-sitter/tree-sitter-php): official PHP grammar
- [soulseekah/ext-treesitter](https://github.com/soulseekah/ext-treesitter): PHP C extension
- [talbergs/php-tree-sitter](https://github.com/talbergs/php-tree-sitter): FFI bindings
- [xberg-io/tree-sitter-language-pack](https://github.com/xberg-io/tree-sitter-language-pack): 371 grammars for PHP (formerly published as kreuzberg-dev/tree-sitter-language-pack)
- [ast-grep/ast-grep](https://github.com/ast-grep/ast-grep): structural code search in Rust
- [php-rust-tools/parser](https://github.com/php-rust-tools/parser): handwritten Rust PHP parser