FrankenPHP Extension for NATS
A high-performance NATS client for PHP, designed to work with FrankenPHP.
It leverages the official NATS Go client.
This extension enables the creation of global, shared NATS connections that persist across requests and worker script instances,
giving PHP applications first-class access to NATS messaging without the per-request reconnect cost of pure-PHP clients.
[!NOTE]
frankenphp-nats follows the pattern established by dunglas/frankenphp-etcd
and is built using FrankenPHP's Extension Generator.
The v0.1.0 scope is core pub/sub (publish, subscribe, request/reply, headers, basic auth, TLS).
JetStream, Key-Value, Object Store, push subscriptions and the NATS Micro framework are planned for follow-up releases —
see Roadmap.
Status
| Feature |
Version |
| Core pub/sub, request/reply, headers |
v0.1.0 |
| JetStream (streams, consumers, JS publish) |
v0.2.0 |
| Key-Value & Object Store |
v0.3.0 |
| Push subscriptions, Caddyfile-declared conns |
v0.4.0 |
| NATS Micro / Services framework |
v0.5.0 |
Installation
First, if not already done, follow the instructions to install a ZTS version of libphp and xcaddy.
Then, use xcaddy to build FrankenPHP with the frankenphp-nats module:
CGO_ENABLED=1 \
CGO_CFLAGS="-D_GNU_SOURCE $(php-config --includes)" \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs)" \
XCADDY_GO_BUILD_FLAGS="-tags=nobadger,nomysql,nopgx,nowatcher" \
xcaddy build \
--output frankenphp \
--with github.com/abderrahimghazali/frankenphp-nats \
--with github.com/dunglas/frankenphp/caddy
# Add extra Caddy modules and FrankenPHP extensions here
The repo ships the pre-generated C/PHP boilerplate (nats.c, nats.h, nats_arginfo.h,
nats.stub.php, nats_generated.go) so consumers do not need a PHP build environment to run
xcaddy.
That's all — your custom FrankenPHP build now exposes the Abderrahim\Nats namespace to PHP.
Usage
<?php
use function Abderrahim\Nats\connect;
use function Abderrahim\Nats\publish;
use function Abderrahim\Nats\subscribe;
use function Abderrahim\Nats\nextMessage;
use function Abderrahim\Nats\unsubscribe;
use function Abderrahim\Nats\request;
use const Abderrahim\Nats\SECOND;
// Open (or reuse) a globally registered NATS connection. Persists across
// requests and worker reboots.
connect('default', ['nats://127.0.0.1:4222']);
// Fire-and-forget publish, with optional headers.
publish('default', 'orders.created', json_encode(['id' => 42]), [
'X-Trace-Id' => 'abc123',
]);
// Subscribe and pull one message synchronously.
$sub = subscribe('default', 'orders.>');
$msg = nextMessage($sub, 5 * SECOND);
if ($msg !== null) {
echo "got {$msg['subject']}: {$msg['data']}\n";
}
unsubscribe($sub);
// Synchronous request/reply.
$reply = request('default', 'svc.echo', 'ping', 1 * SECOND);
echo $reply['data'] ?? 'timeout';
API surface (v0.1.0)
All symbols live under Abderrahim\Nats:
| Function |
Purpose |
connect(name, servers, …auth…) |
Create or reuse a globally registered connection. |
publish(name, subject, data, ?headers) |
Fire-and-forget publish. |
request(name, subject, data, timeout) |
Synchronous request/reply, returns array|null. |
subscribe(name, subject, ?queue) |
Returns a subscription ID (string). |
nextMessage(subId, timeout) |
Pulls one message synchronously. |
subscriptionValid(subId) |
True if the subscription is still in the registry and the underlying NATS sub is live; lets polling loops distinguish a timeout from a connection that was closed underneath them. |
unsubscribe(subId) |
Removes a subscription. |
flush(name, timeout) |
Block until pending publishes are flushed. |
isConnected(name) |
True if currently connected. |
stats(name) |
Returns counters: in_msgs, out_msgs, in_bytes, out_bytes, reconnects. |
close(name) |
Close and remove from the global registry. |
Time-unit constants are exposed as NANOSECOND, MICROSECOND, MILLISECOND, SECOND, MINUTE.
Message arrays returned by request() and nextMessage() have the shape:
[
'subject' => string,
'data' => string,
'reply' => ?string,
'headers' => array<string, string[]>,
]
Error handling
Because the Extension Generator
does not yet expose a Go-callable API for raising PHP exceptions, failures (connection errors,
publish errors, unknown connection names) are logged via the FrankenPHP error log and surface
to PHP as zero values:
| Function |
On failure |
connect(), publish(), flush(), unsubscribe(), close() |
Logs and returns. Subsequent calls (isConnected(), stats()) reveal state. |
request(), nextMessage() |
Returns null (also returned on legitimate timeout). |
subscribe() |
Returns an empty string. |
stats() |
Returns an empty array. |
isConnected() |
Returns false. |
A future release will introduce explicit exception types once the upstream generator gains
exception-throwing primitives.
Authentication
connect() accepts the full set of NATS auth options. Exactly one auth method may be set per
call — passing more than one (e.g. username+password and a token) is rejected with a logged
error and no connection is registered. username without password (or vice versa) is treated as
no-auth and falls through.
| Argument |
Description |
username + password |
Basic auth. |
token |
Token auth. |
credsFile |
Path to a NATS .creds file (NGS / decentralised auth). |
nkeyFile |
Path to an NKey seed file. |
tls |
Enable TLS with sane defaults (TLS 1.2+). When true, server URLs of the form nats://host:port (or bare host:port) are rewritten to tls://host:port before dialing — the underlying nats.go client picks the dialer from the URL scheme, not from the Secure option alone. |
Development
This extension is built using FrankenPHP's
Extension Generator.
After editing nats.go, regenerate the C/PHP boilerplate:
GEN_STUB_SCRIPT=path/to/php-src/build/gen_stub.php \
frankenphp extension-init nats.go
This refreshes nats.c, nats.h, nats_arginfo.h, nats.stub.php, and nats_generated.go in the
repo root. All five files are committed.
[!IMPORTANT]
FrankenPHP v1.12.2's generator emits RETURN_EMPTY_ARRAY() for functions whose stubs declare
?array. PHP's nullable-array semantics require RETURN_NULL(). Two sed patches need to be
reapplied after every regeneration:
sed -i '/zend_array \*result = go_request/,/^}/{s/RETURN_EMPTY_ARRAY/RETURN_NULL/}' nats.c
sed -i '/zend_array \*result = go_nextMessage/,/^}/{s/RETURN_EMPTY_ARRAY/RETURN_NULL/}' nats.c
Without these, request() and nextMessage() return an empty array on timeout, and PHP code that
checks $msg !== null runs into "Undefined array key" warnings on every key access.
Run the Go tests against a real nats-server:
docker run -d --name nats-test -p 4222:4222 nats:latest
go test -race -tags nobadger,nomysql,nopgx,nowatcher,nobrotli -v ./...
docker rm -f nats-test
Credits
Created by Abderrahim Ghazali, inspired by
dunglas/frankenphp-etcd and
dunglas/frankenphp-grpc.