Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@php-websocket-rpc/codegen

Generates typed TypeScript interfaces and proxy configs from PHP RPC contract interfaces annotated with #[RpcStream], #[RpcSubscribe], and #[RpcPublish].

Install

npm install --save-dev @php-websocket-rpc/codegen

Quick Start

npx php-rpc-codegen --input src/Contracts/ --output src/generated/rpc-types.ts

Then use the generated types in your app:

import { RpcClient } from '@php-websocket-rpc/client';
import { ChatServiceProxy, ChatServiceConfig } from './generated/rpc-types';

const client = await RpcClient.connect('ws://127.0.0.1:9502/rpc');
const chat = client.createProxy<ChatServiceProxy>(ChatServiceConfig);

chat.onMessage((msg) => console.log(msg));
chat.send('Hello!');

CLI

Usage: php-rpc-codegen [options]

Options:
  -V, --version       output the version number
  -i, --input <paths...>  Input PHP file(s) or directory/glob patterns (required)
  -o, --output <path>     Output TypeScript file (required)
  -w, --watch             Watch mode — re-generate on file changes
  --no-banner             Omit the auto-generated header comment
  -h, --help              display help for command

npm Scripts

{
    "scripts": {
        "gen:rpc": "php-rpc-codegen --input src/Contracts/ --output src/generated/rpc-types.ts",
        "build": "npm run gen:rpc && tsc"
    }
}

Input / Output

Given this PHP file:

use PhpWebsocketRpc\Rpc\Contract\Attribute\RpcPublish;
use PhpWebsocketRpc\Rpc\Contract\Attribute\RpcStream;
use PhpWebsocketRpc\Rpc\Contract\Attribute\RpcSubscribe;

interface MathService
{
    public function add(int $a, int $b): int;
    public function log(string $message): void;
}

interface NumberStreamService
{
    #[RpcStream]
    public function count(int $limit): \Iterator;
}

interface EventService
{
    #[RpcSubscribe(channel: 'events', type: 'string')]
    public function onEvent(callable $callback): void;
}

interface ChatService
{
    #[RpcSubscribe('chat')]
    public function onMessage(callable $callback): void;

    #[RpcPublish('chat')]
    public function send(string $message): void;
}

Produces:

import type { ProxyOptions } from '@php-websocket-rpc/client';

export interface MathServiceProxy {
    add(a: number, b: number): Promise<number>;
    log(message: string): void;
}

export interface NumberStreamServiceProxy {
    count(limit: number): AsyncIterable<number>;
}

export interface EventServiceProxy {
    onEvent(callback: (value: string) => void): void;
}

export interface ChatServiceProxy {
    onMessage(callback: (value: unknown) => void): void;
    send(message: string): void;
}

export const MathServiceConfig = {
    service: 'MathService',
    notify: ['log'],
} satisfies ProxyOptions;

export const NumberStreamServiceConfig = {
    service: 'NumberStreamService',
    stream: ['count'],
} satisfies ProxyOptions;

export const EventServiceConfig = {
    service: 'EventService',
    subscribe: ['onEvent'],
    channel: 'events',
} satisfies ProxyOptions;

export const ChatServiceConfig = {
    service: 'ChatService',
    subscribe: ['onMessage'],
    publish: ['send'],
    channel: 'chat',
} satisfies ProxyOptions;

Wire Deserialization (classMap)

When the PHP server sends value objects over the wire, they are serialized as [FQCN, props] — the fully-qualified class name followed by its properties. The codegen generates a classMap that maps these FQCNs to their TypeScript interfaces so the client can deserialize them automatically.

import { RpcClient, createContractProxy } from '@php-websocket-rpc/client';
import { ChatServiceProxy, ChatServiceConfig, classMap } from './generated/rpc-types';

const client = await RpcClient.connect('ws://127.0.0.1:9502/rpc');

const chat = createContractProxy<ChatServiceProxy>(client, {
    ...ChatServiceConfig,
    classMap,  // ← enables automatic wire deserialization
});

// When the server returns a typed value object like [FQCN, props],
// the client uses classMap to reconstruct it as the correct TypeScript type
chat.onMessage((msg) => console.log(msg));  // msg is properly typed

The generated classMap looks like:

export const classMap: Record<string, (data: Record<string, unknown>) => unknown> = {
    'App\\Contract\\ChatNotification': (data) => data as ChatNotification,
    'App\\Contract\\MessageNotification': (data) => data as MessageNotification,
};

Only class DTOs get entries. Enums are scalars on the wire (string or int) and don't need deserialization.

Naming Convention

All PHP contracts, DTOs, and enums processed by the codegen must share the same namespace and be placed in the same folder (or a folder tree fed to --input). This ensures:

  • Type names are unique within the generated file.
  • The classMap can resolve all FQCNs without collisions.
  • Contract interface proxies can reference DTO and enum types correctly.

Recommended project structure:

src/
  Contract/
    ChatEventInterface.php
    ChatNotification.php
    MessageNotification.php
    MessageSenderType.php

CLI invocation:

php-rpc-codegen --input src/Contract/ --output src/generated/rpc-types.ts

Type Mapping

PHP TypeScript
int / float number
string string
bool boolean
void void
mixed / object unknown
array unknown[]
?Type Type | null
callable (...args: unknown[]) => unknown
PHP enum (parsed) enum type name (union of literals)
PHP class DTO (parsed) class interface name
custom class (not parsed) Record<string, unknown>

Pattern Detection

PHP Signature Detected Pattern TS Return Type
function f(...): T (no attribute) call Promise<T>
function f(...): void (no attribute) notify void
#[RpcStream] function f(...): \Iterator stream AsyncIterable<T>
#[RpcSubscribe] function f(callable): void subscribe void (callback-driven)
#[RpcPublish] function f(...): void publish void

Type Mapping

PHP TypeScript
int / float number
string string
bool boolean
void void
mixed / object unknown
array unknown[]
?Type Type | null
callable (...args: unknown[]) => unknown
custom class Record<string, unknown>

How It Works

The codegen uses php-parser to build a full AST from your PHP files, then walks the AST looking for interface declarations with methods. It reads PHP 8.5 attributes (#[RpcStream], #[RpcSubscribe], #[RpcPublish]) to detect the RPC pattern for each method, maps PHP types to TypeScript types, and emits ready-to-use interface definitions and config objects.

Only interface declarations are processed — class bodies, functions, and non-interface code is ignored.

About

[READONLY] Typescript PHP Websocket RPC Contract Dumper

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages