wikimedia/webidl
A PHP parser for WebIDL, the interface definition language used by web platform specifications. It turns WebIDL definitions into a structured syntax tree, giving PHP tools a foundation for validation, documentation, and code or binding generation.
wikimedia/webidl is a pure-PHP parser for WebIDL, the interface definition language used to describe web APIs and, in MediaWiki, JSON request/response bodies for the REST framework. It is MIT-licensed, has no runtime dependencies beyond PHP, and is maintained upstream on Gerrit (mediawiki/libs/WebIDL) with GitHub acting as a read-only mirror. The single star and zero dependents are misleading: nearly all real usage flows through MediaWiki core, which vendors/requires it.
First three steps
composer require wikimedia/webidl (check composer.json for the PHP floor before pinning).README.md, then src/WebIDL.php — the static entry point — and skim src/AST/ to see the node classes. The tests/ fixtures are the de-facto grammar-coverage documentation.<?php
use Wikimedia\WebIDL\WebIDL;
use Wikimedia\WebIDL\SyntaxError;
$idl = file_get_contents( __DIR__ . '/fixtures/example.webidl' );
try {
$doc = WebIDL::parse( $idl );
} catch ( SyntaxError $e ) {
fwrite( STDERR, "Invalid IDL: {$e->getMessage()}\n" );
exit( 1 );
}
var_dump( get_class( $doc ), array_keys( get_object_vars( $doc ) ) );
The AST is plain PHP objects (public properties, no visitor infrastructure), so var_dump/get_object_vars is the fastest way to learn the shape. Confirm the exact property names against src/AST/*.php rather than trusting blog posts — the API surface is small but not documented in depth.
If you are in MediaWiki, you probably never call this directly. Core wires it into the REST layer: a route definition declares a WebIDL schema for its body, and the framework parses it and validates JSON against the derived schema. Reach for the library itself only when you are building tooling — docs generators, linters, SDK stubs — or need request validation outside MediaWiki.
The canonical first use case: parse a .webidl file and enumerate the definitions (interface, dictionary, enum, typedef, callback) so you can drive something else — a schema, a doc page, a type stub.
This is the dominant pattern and the one most developers will actually meet. A route's JSON file points at an IDL schema; the handler returns a body validator built from it.
{
"path": "/v1/widgets/{id}",
"method": "PUT",
"body": {
"content-type": "application/json",
"schema": "widgets/updateBody.webidl"
}
}
dictionary UpdateWidgetBody {
required DOMString name;
DOMString? description;
unsigned long weight = 1;
};
Your job is to write correct IDL, not to parse it. Use the library directly only when you need to compile that same IDL into something core does not produce (e.g. an OpenAPI component, a Typia/Zod schema, a TS client).
The most valuable pattern. Walk the AST, resolve type references through a symbol table, emit JSON Schema 2020-12. Keep your mapper as a pure function of the type node:
| WebIDL | JSON Schema |
|---|---|
DOMString, USVString, ByteString |
{"type":"string"} |
boolean |
{"type":"boolean"} |
long, short, byte, and unsigned variants |
{"type":"integer","minimum":…,"maximum":…} |
unrestricted float/double, float, double |
{"type":"number"} |
object, any |
{} |
T? |
{"anyOf":[T,{"type":"null"}]} (or type: ["…","null"]) |
sequence<T> / T[] |
{"type":"array","items":T} |
record<K,V> |
{"type":"object","additionalProperties":V} |
(A or B) |
{"anyOf":[A,B]} |
| dictionary | {"type":"object","properties":…,"required":[…],"additionalProperties":false} |
| enum | {"type":"string","enum":[…]} |
typedef |
inline the target (no $ref unless you build a defs map) |
Two details that bite: required comes from the required keyword on the dictionary member, not from the absence of a default; and T? is not the same as omitting the member — a nullable required member must be present and may be null.
The parser does not resolve cross-document references, includes statements, or partial interface merging. That is your job.
$symbols = []; // name => AST node
foreach ( $files as $file ) {
$doc = WebIDL::parse( file_get_contents( $file ) );
foreach ( $doc->definitions as $def ) {
// merge partials rather than overwriting
$symbols[$def->name] = merge( $symbols[$def->name] ?? null, $def );
}
}
// second pass: validate every referenced type name exists or is a builtin
Do the resolution as a second pass. A single-pass walker that throws on the first unknown type will fail on legal forward references and on types defined in a sibling file.
There is no visitor base class, so write a ~40-line recursive walker that dispatches on instanceof:
function walk( $node, callable $fn ) {
$fn( $node );
foreach ( get_object_vars( $node ) as $v ) {
if ( $v instanceof \Wikimedia\WebIDL\AST\Node ) {
walk( $v, $fn );
} elseif ( is_array( $v ) ) {
foreach ( $v as $item ) {
if ( $item instanceof \Wikimedia\WebIDL\AST\Node ) { walk( $item, $fn ); }
}
}
}
}
Good lint rules: every dictionary member type must resolve; enum values unique; no unsigned long used where a DOMString was meant; every operation has at least one argument of a non-nullable type; naming conventions (UpperCamelCase for dictionaries, lowerCamelCase for members).
Dictionaries map cleanly to TypeScript interfaces or PHP DTOs; enums map to enums or PHP backed enums; operations can seed an API client skeleton. Keep a golden-file test suite: check in .webidl inputs and expected generated output, and fail the build on drift. For the parser side, a smoke test that every .webidl file in the repo parses is cheap and catches half the breakage from an upgrade.
Parsing is regex/recursive-descent work — fine at build time or once per request, wasteful per-route.
$cacheKey = 'webidl:' . sha1_file( $path );
$schema = $cache->get( $cacheKey ) ?? $cache->set( $cacheKey, compile( $path ) );
Cache the compiled output (JSON Schema array), not the AST. ASTs are large graphs of objects and will bloat any cache backend even if serialize() works. Wrap the library in a thin WebIdlSchemaProvider service so the rest of your code depends on your interface, not on the parser — this is what makes a future swap realistic.
The GitHub repo is a mirror. Issues and PRs are not triaged there. Contributions go through Gerrit (git review, Change-Id in the commit message, developer access per mediawiki.org). Treat the GitHub tag list as possibly lagging upstream.
It is a parser, not a validator. Unknown type names, duplicate definitions, unresolvable typedefs, and semantically wrong nullable usage all parse fine. Every check above the grammar is yours to write.
Comments are discarded. You cannot extract doc comments from the AST, which kills naive "generate docs from IDL" plans. If you need descriptions, put them in extended attributes ([Description="…"]) and confirm the parser preserves them; otherwise keep docs in a parallel file.
Extended attributes are generic key/value bags. The parser records [Exposed=Window, NewObject] but assigns no meaning. You must interpret the ones you care about — and silently ignoring an unknown one is a common source of "why is this schema wrong".
Nullable vs optional vs union-with-null. DOMString? foo; (nullable, required), DOMString foo; inside a dictionary vs optional DOMString foo; in an operation, and (DOMString or null) are three different things. Get the required mapping wrong and clients send null where the server expects omission.
Default-value parsing is the weakest edge. optional DOMString foo = "bar" and unsigned long x = 0 may or may not round-trip depending on the version. Test your specific defaults before designing around them; if defaults are dropped, don't advertise them in generated OpenAPI.
Grammar coverage lags the W3C spec. Newer constructs — constructor, static, stringifier, async iterables, mixins, includes — are where you should expect gaps. Run the specific construct through WebIDL::parse before committing to a design that needs it; the tests/ tree tells you what is actually exercised.
Error reporting is line-oriented. SyntaxError gives you a message and position, not a pretty caret. Wrap it and print the offending source line for humans:
catch ( SyntaxError $e ) {
$lines = explode( "\n", $idl );
$n = $e->getLine() ?: 0;
fwrite( STDERR, "line {$n}: {$lines[$n - 1]}\n{$e->getMessage()}\n" );
}
(Verify whether the exception carries line info via getLine()/custom property before relying on it.)
Deeply nested unions recurse. (A or (B or (C or …))) is legal and your walker will follow it. Add a depth guard if IDL comes from untrusted sources.
Builtin vs user type names are indistinguishable in the AST. Maintain an explicit list of primitives (DOMString, long, boolean, object, any, ArrayBuffer, typed arrays, …); otherwise a user-defined dictionary Long {…} will shadow correctly or incorrectly depending on your lookup order — pick an order and document it.
Names are case-sensitive. USVString the builtin is not UsvString, and dictionary references must match exactly. Normalizing case anywhere will create phantom bugs.
Pin the major version and vendor if critical. One star, zero dependents, Gerrit-hosted: the API can change between majors without a deprecation cycle. If your product depends on this, either extract the ~2k lines you need or wrap it and add your own golden tests so an upgrade failure is loud.
Don't log or var_dump ASTs in production. A medium IDL file produces thousands of objects and a multi-MB dump; log the file path and line number instead.
String semantics are not enforced. USVString (no lone surrogates, scalar values) and ByteString are just strings to this parser. If those constraints matter, validate after decoding JSON yourself.
Extension point that actually works: since the AST is public-property objects with no final/private barriers, you can subclass or post-process nodes. That is more stable in practice than patching the parser — but it still breaks on upstream AST changes, so keep it behind your own adapter layer.
How can I help you explore Laravel packages today?