npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@php-websocket-rpc/codegen

v0.1.0

Published

Generates typed TypeScript interfaces and proxy configs from PHP RPC contract interfaces

Readme

@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.