Search by

jx3dev / jx3dev-php

YiwanGo

Official-style PHP SDK for the jx3dev API. HTTP and WebSocket channels, zero runtime dependencies.

1.1.0 2026-08-21 22:48 UTC

This package is auto-updated.

Last update: 2026-08-23 12:11:27 UTC


README

jx3dev 接口服务的 PHP SDK。HTTP 与 WebSocket 双通道,零运行时依赖——只用 ext-curlext-jsonext-openssl,不引入任何框架。

use Jx3Dev\Jx3Dev;

$client = Jx3Dev::make('jx3dev::3f9a1c7e…');

$status = $client->getTokenStatus();
echo $status->tokenLevel(), ' / 剩余 ', $status->callsQuota(), '', PHP_EOL;

安装

composer require jx3dev/jx3dev-php

要求 PHP 8.1+。

初始化

令牌只在这里接收一次,之后 HTTP 与 WebSocket 两条通道都会自动携带。只有令牌是必填的

$client = Jx3Dev::make('jx3dev::3f9a1c7e…');

请求前缀默认 /api/,绝大多数接口都在这里,不用配。少数在别的前缀下的接口见切换前缀

要调的话,常用的几个(全部选项见下方折叠表):

$client = Jx3Dev::make('jx3dev::3f9a1c7e…', options: [
    'timeout'        => 30.0,      // 单次请求超时,默认 10 秒
    'locale'         => 'en',      // 只影响 msg,默认不发送由服务端定
    'auto_reconnect' => true,      // WebSocket 断线重连,默认关闭
]);

// 不携带令牌
$anonymous = Jx3Dev::make('');

令牌是否有效由服务端判定。无效或不存在的令牌不会导致连接失败,问题要到第一次调接口时才以权限错误的形式暴露。

服务地址不传则连官方生产环境(Config::DEFAULT_BASE_URI)。测试环境或私有部署必须显式传入第二个参数——**忘了传会静默连到生产 **,这是全包唯一一处带环境含义的默认值:

$client = Jx3Dev::make($token, 'https://your-test-host');

令牌请从环境变量或配置文件读取。不要硬编码在代码里,示例代码里也不要出现真令牌。

$client = Jx3Dev::make(getenv('JX3DEV_TOKEN') ?: '');
全部可选项
默认 说明
prefix /api/ HTTP 请求前缀
ws_uri base_uri 推导 WebSocket 完整地址,必须 ws://wss:// 开头
ws_path /ws/ 推导时拼接的路径
timeout 10.0 单次请求 / 单次应答等待的超时秒数
connect_timeout 5.0 建立连接的超时秒数
locale 不发送 zh_CNen
verify_tls true 是否校验 TLS 证书
ping_interval 120.0 WebSocket 保活间隔,0 表示不自动发
auto_reconnect false 是否自动重连
reconnect_on_kick false 收到 event 1001 后是否仍然重连
backoff_base / backoff_max / backoff_factor / backoff_jitter 1.0 / 60.0 / 2.0 / 0.2 重连退避
max_reconnect_attempts 0(不限) 连续失败多少次后放弃
ws_token_in_query false 令牌改放握手 query,见下方警告
user_agent jx3dev-php/{版本},见 Config::VERSION 服务端识别客户端版本的依据,一般不用改

拼错的键会直接报错,不会静默生效为默认值。

ws_token_in_query 会把令牌写进 URL。 代价是它随后会出现在反向代理和服务端的访问日志 里,而日志的留存和权限通常不按密钥对待。只有在反代确实会剥离自定义请求头、拿不到 Authorization 时才打开它,并且要清楚日志里从此有一份明文令牌。

HTTP 调用

统一走 POST + JSON + op 参数式,令牌走 Authorization: Bearer 请求头。

// 接口名用文档里的下划线原名
$response = $client->call('get_token_status');

$response->ok();                 // code === 0
$response->code();               // 错误码
$response->msg();                // 服务端提示语
$response->data();               // 业务数据数组,无返回体时为 null
$response->get('token_level');   // 取 data 里的字段,支持 'a.b.c' 点号路径
$response->extra();              // request_id / request_ts 等

// 带参数
$client->call('some_endpoint', ['name' => '张三', 'page' => 2]);

// 已封装的接口返回结构化对象
$status = $client->getTokenStatus();

op / token / locale 由 SDK 填充,业务参数里出现同名键会直接报错,不会被静默覆盖。

切换前缀

默认前缀是 /api/,在初始化时定。若文档里某个接口另有前缀,单次调用可以临时切

$client->call('get_token_status');                  // 默认 /api/
$client->http('/other/api/')->call('some_op');      // 文档里另有前缀的接口

按前缀缓存通道,全部共用同一个传输实例——切前缀不会另开连接,默认前缀也不受影响。

前缀必须以斜杠开头和结尾,否则拼出的请求地址不正确。SDK 会直接报错而不是悄悄替你补——静默修正会让人以为自己写对了。

错误处理

服务端给的 msg / code / extra 一个不落地交给你,SDK 不吞、不加工、不静默重试

use Jx3Dev\Exception\{ApiException, RateLimitException, TransportException, TimeoutException};

try {
    $status = $client->getTokenStatus();
} catch (RateLimitException $e) {
    // 限流。SDK 不替你等待,退避多久由你决定
    sleep($e->retryAfter());
} catch (ApiException $e) {
    // SDK 不内置码表——具体码的含义对照官方接口文档,别指望常量
    echo $e->errorCode(), ' ', $e->getMessage(), PHP_EOL;
    print_r($e->extra());
} catch (TimeoutException $e) {
    // 请求超时,或 WebSocket 上没等到对应 echo 的应答
} catch (TransportException $e) {
    // 连不上、TLS 失败、应答不是合法 JSON —— 压根没拿到业务应答
}

不想用异常控制流就换 tryCall(),业务失败不抛,自己看 code

$response = $client->http()->tryCall('get_token_status');
if (!$response->ok()) {
    echo $response->code(), ' ', $response->msg(), PHP_EOL;
}

异常层次:

Jx3DevException
├── ApiException              服务端返回非 0 的 code
│   └── RateLimitException    20004,带 retryAfter()
└── TransportException        没拿到业务应答
    ├── TimeoutException
    └── ConnectionClosedException

用法错误不在这棵树里。 非法的配置项、非法的接口名或 scope,抛的是 SPL 的 InvalidArgumentException —— 那是代码写错了,不是调用失败,不该和「服务端拒了」「网断了」一起兜住。所以 catch (Jx3DevException) 捕不到它,这是故意的:这类错误应该在开发期就炸出来。

WebSocket 连接

一条长连接同时干两件事:收事件调接口

$ws = $client->ws();
$ws->connect();

// 保活:服务端判定的是「客户端发出的数据」,超过 600 秒没发过东西就会被关闭。
// ping_interval 到点会自动发,也可以手动发
$ws->ping();

$ws->close();

握手时读一次令牌记进连接,之后每一帧都不用再带。令牌无效不会握手失败——服务端会把你按游客接进来。

事件订阅

三步缺一不可:登记回调 → connect()run()。只登记回调收不到任何东西——没有东西在读 socket。顺序也别反,协程模式下 connect() 一返回读协程就开工了,之后再登记可能漏掉最早的几帧。

use Jx3Dev\Protocol\{EventFrame, EventCode};

// 订阅指定事件号
$sub = $ws->on(2999, function (EventFrame $event) {
    echo $event->event(), ' ', json_encode($event->data()), PHP_EOL;
});

// 订阅全部事件
$ws->onAny(fn(EventFrame $e) => error_log('event ' . $e->event()));

// 状态快照,每 30 秒一帧。可以当「连接还活着」的探针,但不能替代保活
$ws->on(EventCode::STATUS, function (EventFrame $e) {
    echo '已连接 ', $e->get('connect_secs'), '', PHP_EOL;
});

// 被服务端断开
$ws->on(EventCode::KICKED, function (EventFrame $e) {
    error_log('被断开:' . $e->kickReason());
});

$sub->cancel();     // 取消订阅

// 连接生命周期
$ws->onOpen(fn() => error_log('connected'));
$ws->onClose(fn($e) => error_log('closed: ' . $e->closeCode()));
$ws->onError(fn($t) => error_log((string) $t));   // 不注册则回调里的异常原样抛出

// 持续收帧。不传参数就一直跑,传秒数则到点返回
$ws->run();

单 handler:on() 每个事件号、onAny 各只保留一个——再次注册会顶掉前一个(后注册的生效)并发一条 E_USER_WARNING 提示。因此别把 on() / onAny 放进 onOpen 回调——重连会反复触发、每次都替换并告警;订阅在 connect() 前登记一次即可,handler 跨重连保留。

通过 WebSocket 调接口

$response = $ws->call('get_token_status');
$status = $ws->getTokenStatus();

// 指定 scope。它是第三个参数,只改它就用命名参数,别去凑空数组
$response = $ws->call('some_action', scope: 'other_scope');
$echo = $ws->send('some_action', ['page' => 2], 'other_scope');

// 连发多帧再统一取回
$a = $ws->send('endpoint_a');
$b = $ws->send('endpoint_b', ['page' => 2]);
$ws->await($a);
$ws->await($b);

scope 是帧里原样带上的字段,call() / tryCall() / send() 三个方法都有,默认 default,取值含义见接口文档。限 1–32 位 [A-Za-z0-9_],不合规直接抛 InvalidArgumentException —— 不会静默发出去等服务端报错。

echo 由 SDK 自动分配(同一连接内不重复),应答严格按 echo 对号,绝不按到达次序推断——应答不保证按发送顺序返回。等待应答期间事件回调照常触发,不会积压。

WebSocket 通道不认次数配额,只认时间订阅:没有生效中的时间订阅会拿到 10121

收帧调度(协程环境必读)

一条 socket 只能有一个执行流在读——两个协程同时 fread 会把帧撕开,症状是随机的 JSON 解析失败。SDK 按运行环境自动选调度策略:

环境 策略 谁在读 socket
普通 PHP CLI InlineLoop 没有并发,等应答的调用自己边等边读
Swoole 协程 CoroutineLoop connect() 时起一个专职读协程独占 socket,其余协程只挂在各自的等待通道上

所以在 Swoole 下可以放心这么写:

// 协程 A:后台收事件
go(function () use ($ws) {
    $ws->on(2999, fn($e) => handle($e));
    $ws->run();
});

// 协程 B:同时调接口,不会和读协程抢 socket
go(function () use ($ws) {
    $status = $ws->getTokenStatus();
});

出站写入也做了串行化:两个协程同时 call() 时,send 走一把互斥,不会撞上「socket 已被另一个协程占用」。

想显式指定策略(比如在协程里强制用阻塞模式):

$client = new Jx3Dev($config, loop: new InlineLoop());
$client->ws()->loop();     // 确认当前用的是哪个

自定义调度实现 Jx3Dev\Ws\Loop\LoopInterface 即可。

协程模式下务必注册 onError() 事件回调抛出的异常会打断读协程,之后 run() 才会把它抛给你——注册了错误处理器就不会中断收帧。

断开与重连

先看关闭码,再决定重不重连。

关闭码 怎么回事 SDK 行为
1008 服务端主动断开,原因在断开前那帧 event 1001data.reason 默认重连
其他 网络或服务端异常 开了 auto_reconnect 才退避重连
$client = Jx3Dev::make($token, $base, [
    'auto_reconnect'    => true,   // 默认关闭
    'reconnect_on_kick' => false,  // 另设参数,默认关闭
    'backoff_base'      => 1.0,
    'backoff_max'       => 60.0,
    'backoff_factor'    => 2.0,
    'backoff_jitter'    => 0.2,    // ±20% 抖动,避免所有客户端同时打回服务端
]);

reconnect_on_kick 单独设一个参数、且默认关闭,是因为 1008 意味着服务端已经判定这条连接不该再存在(比如同一令牌在别处建立了连接),无脑重连只会两边互踢。

换掉底层传输

默认实现零依赖且是阻塞式的。在 Swoole / Workerman 这类常驻协程环境里,实现对应接口注入即可,SDK 其余部分不用动。

use Jx3Dev\{Jx3Dev, Config};

$client = new Jx3Dev(
    new Config(token: $token, baseUri: $base),
    httpTransport: new MyGuzzleTransport(),      // Jx3Dev\Contract\HttpTransport
    wsTransport: new MySwooleWsTransport(),      // Jx3Dev\Contract\WebSocketTransport
);

接口覆盖

通道 覆盖
HTTP 全部接口
WebSocket 事件订阅 + 接口调用

每个接口都能调。是否类型化按需决定——值得封装的手写成真方法、返回对象(如 getTokenStatus()TokenStatus);其余用驼峰方法(@method 注解 + __call 转发,IDE 按通道补全)或直接 call() 传原名,返回泛用 ApiResponse->get('a.b') / ->data() 取值):

$status = $client->getTokenStatus();                  // 已封装,返回类型化对象
$data   = $client->someOp($params)->data();           // 驼峰方法 → call('some_op')
$data   = $client->call('some_op', $params)->data();  // 或直接 call() 传下划线原名

接口名一律保留文档里的下划线原名,不做改写。

常驻/协程环境注意

  • HTTP 每次请求独占一个 curl 句柄。 不要改成复用——SWOOLE_HOOK_ALLcurl_exec 会让出协程,共用句柄会让后来者的 curl_setopt_array 覆盖掉正在执行的请求,表现为偶发的「响应对不上请求」。
  • 一个 WsChannel 只持有一条 socket。 多协程共用同一个 WsChannel 是支持的(见收帧调度)。但要同时 开两条连接(比如两个令牌各连一条),得建两个 Jx3Dev 实例——ws() 每个实例只缓存一个 WsChannel
  • 连着的时候不要再调 connect() 旧 socket 会被顶掉但不会关闭,服务端留一条僵尸连接,第一条连接上未回的 call() 也等不到回执了。断线重连由 tick() 自己判断,不用你手动补一次 connect()
  • Jx3Dev 实例可以做容器单例。 HTTP 通道无共享可变状态;WS 通道的并发安全由调度策略保证。

测试

composer test

另有一个不依赖 composer、不联网的自检脚本,用于换环境时快速确认(尤其是没装 PHPUnit 的机器):

php verify.php

它覆盖 PHPUnit 用例之外的两处:真 socket 组帧(stream_socket_pair),以及协程调度(装了 ext-swoole 才跑,没装则跳过并提示)。 常驻环境走的是协程路径,换机器部署前值得跑一次。

覆盖 get_token_status 的成功与各类失败路径、WebSocket 连接的成功与失败路径、echo 乱序对号、事件订阅与取消、被踢后不重连。

许可

MIT