PHP engine implemented in pure Go. Feature-complete for PHP 8.5 language features.
PHP is a nice language but is having trouble keeping up with modern languages. This implementation makes a number of things possible:
- Usage of goroutines, go channels, etc from within PHP
- Better caching of compiled code by allowing sharing of compiled or live objects (classes, objects, etc) between running PHP scripts
- Use Go's memory management within PHP
- Ability to run functions or code sandboxed (including filesystem via
fs.FS) to limit security risks - Easily call the PHP engine from Go to execute pieces of PHP code (user provided or legacy)
go install github.com/KarpelesLab/goro/sapi/php-cli@latestGoro passes ~11,864 of 12,121 tests (~97.9%) from the PHP 8.5.5 test suite (~170 failures, 87 skipped in CI). PHP memory_limit enforcement (128MB default). Includes PCRE2 via gopcre2, IANA timezones via gotz, and 10 extensions (session, xml, curl, gd, sockets, zlib, mysqli, sqlite3, bz2).
| Area | Failures | Notes |
|---|---|---|
| ext/date | 51 | DatePeriod serialization format, date_parse edge cases, DST fallback transitions |
| attributes | 20 | Reflection __toString formatting, delayed target validation, AST printing |
| exceptions | 9 | __toString error location, variance autoload, stream wrappers |
| closures | 9 | Closure const expressions, binding edge cases |
| clone | 6 | AST printing, clone-with edge cases |
| exit | 5 | exit() in custom SAPIs, disabling exit |
| constexpr | 5 | Constant expression edge cases (new in defaults, array unpack) |
| asymmetric_visibility | 5 | Static props, nested variations, indirect modification |
| assert | 5 | assert() callback exceptions, ??= in assert, AST pretty-printer |
| ext/mbstring | 4 | Encoding conversion edge cases |
| constants | 4 | Constant evaluation edge cases |
| ext/hash | 3 | PHP serialization format edge cases |
| ext/gmp | 3 | GMP unserialize with references |
| Other | ~75 | Scattered across ~40 areas (≤3 failures each): reference tracking, object ID ordering, warning ordering, etc. |
| SAPI | Status |
|---|---|
CLI (php-cli) |
Working |
CGI (php-cgi) |
Working |
FPM (php-fpm) |
Working |
HTTP handler (php-httpd) |
Working |
Test runner (php-test) |
Working |
| Extension | Functions | Pass Rate | Notes |
|---|---|---|---|
| standard | 527+ | ~70% | Core functions, arrays, strings, files, math, output buffering, streams |
| ctype | 11 | 100% | Complete |
| json | 5 | 98% | json_encode, json_decode, json_validate, error handling |
| pcre | 11 | 67% | preg_match, preg_replace, preg_split, preg_grep — PCRE2 via gopcre2 (backreferences, lookahead) |
| hash | 14 | 94% | hash, hash_hmac, hash_file, hash_pbkdf2, hash_hkdf, incremental |
| gmp | 49 | 96% | Arithmetic, division, modular, bitwise, primes, GCD/LCM, factorial, operator overloading, import/export |
| mbstring | 49 | 97% | strlen, substr, strpos, strtolower/upper, convert_encoding, detect_encoding, check_encoding |
| date | 48 | 89% | date, time, strtotime, mktime, DateTime, DateTimeImmutable, DateInterval, DatePeriod, DateTimeZone, sunrise/sunset |
| openssl | 16 | — | AES/DES/RSA/ECDSA encryption, signing, key generation via Go crypto |
| bz2 | 2 | — | Compress (gobzip2) and decompress (Go stdlib) |
| zlib | 22 | — | gzcompress/gzencode/gzdeflate, gzip file ops, stream filters, compress.zlib:// |
| session | 19 | — | session_start/id/destroy, file-based storage, $_SESSION superglobal |
| xml | 18 | — | SimpleXMLElement class, xml_parser_create/parse, simplexml_load_string/file |
| curl | 13 | — | CurlHandle class, curl_init/setopt/exec/getinfo via Go net/http |
| sockets | 25 | — | Socket class, socket_create/bind/listen/accept/connect, stream_socket_*, fsockopen |
| mysqli | 25 | — | mysqli/mysqli_result/mysqli_stmt classes, prepared statements, transactions via go-sql-driver/mysql |
| sqlite3 | 20+ | — | SQLite3/SQLite3Result/SQLite3Stmt classes, prepared statements via glebarez/go-sqlite (pure Go) |
| spl | 40+ | 82% | ArrayObject, ArrayIterator, SplFileObject, SplFixedArray, SplHeap, SplObjectStorage, iterators |
| reflection | 8 classes | 75% | ReflectionClass (with __toString), ReflectionMethod, ReflectionProperty, ReflectionFunction, ReflectionParameter, ReflectionAttribute |
| gd | 60+ | — | GdImage class, imagecreate/truecolor, drawing (lines, rectangles, ellipses, polygons, arcs, fill), text (TTF, built-in fonts), PNG/JPEG/GIF/BMP I/O, copy/resize/resample/rotate/crop/flip/scale, filters, convolution — pure Go via gogd |
| getimagesize | — | 100% | 16 image formats (JPEG, PNG, GIF, BMP, WebP, AVIF, HEIF, TIFF, PSD, etc.) |
| Extension | Notes |
|---|---|
| PDO | Planned via database/sql (MySQL + SQLite drivers already available) |
| iconv | Planned via golang.org/x/text/transform |
| intl | Internationalization (ICU) |
| Phar | PHP archive format |
A stack-based bytecode VM runs in parallel to the AST tree-walking executor. It's opt-in and falls back to the AST per-function on any unsupported construct.
Enable it with the GORO_VM=1 environment variable:
GORO_VM=1 php-cli script.phpThe VM emitter currently handles:
- Scalar literals (
int,float,string,bool,null) — including the case-insensitive constantstrue/false/null. - Variable read/write, with a per-frame slot cache so reads skip the
FuncContext hashtable entirely. Functions with no
extract/compact/$$x/global/static/$GLOBALSuse further perform slot-only writes (skip the hashtable mirror) for a sizeable perf win on write-heavy loops. - Arithmetic / bitwise / shift / comparison / concat / unary
(
-,~,!); plain and compound assignment (=,+=, …); pre/post++/--. - Short-circuit
&&/||and??(null coalesce, simple-variable LHS only). if/elseif/else,while,for,foreach(value form, array + object iteration),break/continue(single level),return,throw,try/catch(multi-type union, multi-clause, destructor-during-catch-bind chained correctly),try/finally(delegated to the AST runner so finally runs on every exit path).- String interpolation (
"hello $name","v={$x}") lowered to a chain ofOP_CONCAT. - Array literals (
[…], including keyedk => v);$a[$k]read;$a[$k] = vand$a[] = vwrites (with auto-vivification from null/false and string-offset semantics). - Object instantiation (
new Cls(args…)), property read ($obj->prop), method call ($obj->m(args…)) with full PHP visibility checks (private/protected) and__callfallback.$thisoutside object context throws the correct Error. Class const / static prop /Foo::class(Foo::CONST,self::method,Foo::$bar,Foo::class) are AST-delegated for full CompileDelayed / visibility / LSB semantics. - Builtin and user-defined function calls (positional args). Calls to
by-ref builtins (
end,sort,array_walk,array_push, …) fall back to AST so the by-ref binding works. - Inline closures (
function() { … },fn() => …) with use captures,$thisbinding, and arrow auto-capture. Indirect calls ($f(),[$obj, 'method']()) resolve the callable at runtime viacompiler.ResolveCallableand forward the implicit$this.
Out of scope (falls back to AST per-function via ErrUnsupported):
- By-ref returns and by-ref parameters on user-defined functions
(the VM passes pre-evaluated ZVals; the AST passes Runnables and
binds
Writables). - Generators (
yield). $obj->prop = vand other property writes (deferred until a public WriteValue helper exists).- Nullsafe chains (
$obj?->...). - Spread (
...$arr) and named arguments. - Dynamic names (
$$x,$obj->{$x},new $cls()). - Multi-level
break N/continue N. - Type-hinted return values (the AST coerces; the VM doesn't yet).
- User-defined constants (PHP_INT_MAX, MYAPP_FOO, …).
- List destructure, anonymous classes,
extract/compact/$$xand similar locals-introspecting builtins (those force slot-only off; currently we just compile-time bail when the body uses them).
Functions matching any of the above run as AST as before — the engine silently picks the right backend per-function, with no behaviour change.
Bench wins (vs. AST baseline, per-iter):
| Benchmark | AST | VM | Δ |
|---|---|---|---|
| Arithmetic | 58M ns | 27M ns | -54% |
| ArrayOps | 11M ns | 8M ns | -27% |
| Fibonacci | 26M ns | 21M ns | -19% |
| StringConcat | 13M ns | 11M ns | -18% |
| FunctionCalls | 18M ns | 15M ns | -14% |
Larger gains require either an unboxed value type, slot-only writes (skipping the hashtable mirror) for slot-safe functions, or register-based opcodes. The 64-bit instruction format already has room for the last one.
A process object is typically created once per runtime environment. It caches compiled code and holds global stream wrapper resources, persistent connections, and runtime cache.
When a request is received or script execution is requested, a new Global context is created. It contains runtime state: global variables, declared functions, classes, constants, output buffers, and memory limits.
Context is a local scope (e.g., within a running function). Global has a root context, and each function call creates a new context to separate variable scope.
See development.md for details on writing extensions.
Writing an extension: create a directory in ext/, write functions with magic comment prefixes, run make buildext to generate bindings, and add the extension import to each SAPI's main.go.