thesis/grpc-client

Async gRPC client for PHP with HTTP/2 transport, streaming RPCs, interceptors, TLS/mTLS, and client-side load balancing.

Maintainers

Package info

github.com/thesis-php/grpc-client

pkg:composer/thesis/grpc-client

Transparency log

Fund package maintenance!

www.tinkoff.ru/cf/5MqZQas2dk7

Statistics

Installs: 185

Dependents: 8

Suggesters: 0

Stars: 0

Open Issues: 0

0.2.0 2026-08-03 14:15 UTC

This package is auto-updated.

Last update: 2026-08-03 14:17:35 UTC


README

Read-only subtree split from https://github.com/thesis-php/grpc.

Do not open issues/PRs here. Use the monorepo:

Async gRPC client for PHP with HTTP/2 transport, streaming RPCs, interceptors, TLS/mTLS, and client-side load balancing.

Contents

Installation

composer require thesis/grpc-client

Requirements

To generate gRPC client classes from .proto, use:

Basic usage

use Auth\Api\V1\AuthenticateRequest;
use Auth\Api\V1\AuthenticationServiceClient;
use Thesis\Grpc\Client;

$client = new AuthenticationServiceClient(
    new Client\Builder()
        ->withHost('dns:///127.0.0.1:50051')
        ->build(),
);

$response = $client->authenticate(new AuthenticateRequest('root', 'secret'));

You can pass request metadata too:

use Thesis\Grpc\Metadata;

$response = $client->authenticate(
    new AuthenticateRequest('root', 'secret'),
    new Metadata()->with('x-request-id', 'req-123'),
);

TLS and mTLS

use Amp\Socket\Certificate;
use Thesis\Grpc\Client;

$client = new Client\Builder()
    ->withTransportCredentials(
        new Client\TransportCredentials()
            ->withCaCert('/certs/ca.crt')
            ->withPeerName('localhost')
            ->withCertificate(new Certificate('/certs/client.crt', '/certs/client.key')), // optional (mTLS)
    )
    ->build();

Target addressing

The target string passed to withHost() follows gRPC name resolution format: scheme:endpoint.

Supported schemes:

Scheme Format Description
dns dns:///host:port or dns://authority/host:port Resolves hostname via DNS. Supports periodic re-resolution based on TTL.
ipv4 ipv4:addr1:port1,addr2:port2 Comma-separated IPv4 addresses, no DNS lookup.
ipv6 ipv6:[addr1]:port1,[addr2]:port2 Comma-separated IPv6 addresses in bracket notation.
unix unix:///path/to/socket Connects via Unix domain socket.
passthrough passthrough:///address Passes address through as-is without resolution.

If scheme is omitted, dns is assumed:

// Equivalent:
->withHost('my-grpc-server:50051')
->withHost('dns:///my-grpc-server:50051')

Use custom DNS server with authority:

->withHost('dns://10.0.0.1:53/my-grpc-server:50051')

Multi-endpoint target without DNS:

->withHost('ipv4:10.0.0.1:50051,10.0.0.2:50051,10.0.0.3:50051')

Load balancing

When resolution returns multiple addresses, the load balancer picks endpoint per RPC.

Built-in policies:

  • PickFirstFactory (default): shuffles endpoint list, picks one, and keeps it pinned until it disappears after refresh.
  • RoundRobinFactory: cycles through all available endpoints.

With DNS targets, resolver can re-resolve by TTL and call balancer refresh() with updated endpoints. Re-resolution interval is clamped between minimum and maximum bounds (default 30..300 seconds).

use Thesis\Grpc\Client\LoadBalancer\RoundRobinFactory;

$client = new Client\Builder()
    ->withHost('ipv4:10.0.0.1:50051,10.0.0.2:50051')
    ->withLoadBalancer(new RoundRobinFactory())
    ->build();

Custom load balancer example:

use Random\Randomizer;
use Thesis\Grpc\Client\Endpoint;
use Thesis\Grpc\Client\LoadBalancer;
use Thesis\Grpc\Client\LoadBalancerFactory;
use Thesis\Grpc\Client\PickContext;

final class RandomBalancer implements LoadBalancer
{
    /**
     * @param non-empty-list<Endpoint> $endpoints
     */
    public function __construct(
        private array $endpoints,
        private readonly Randomizer $randomizer = new Randomizer(),
    ) {}

    #[\Override]
    public function refresh(array $endpoints): void
    {
        $this->endpoints = $endpoints;
    }

    #[\Override]
    public function pick(PickContext $context): Endpoint
    {
        return $this->endpoints[$this->randomizer->getInt(0, \count($this->endpoints) - 1)];
    }
}

final readonly class RandomBalancerFactory implements LoadBalancerFactory
{
    #[\Override]
    public function name(): string
    {
        return 'random';
    }

    #[\Override]
    public function create(array $endpoints): LoadBalancer
    {
        return new RandomBalancer($endpoints);
    }
}

Endpoint resolution

Resolver is selected by target scheme. You can override resolver for a specific scheme:

use Amp\Cache\LocalCache;
use Thesis\Grpc\Client\Builder;
use Thesis\Grpc\Client\EndpointResolver\DnsResolver;
use Thesis\Grpc\Client\Scheme;

$client = new Builder()
    ->withHost('dns:///my-grpc-server:50051')
    ->withEndpointResolver(Scheme::Dns, new DnsResolver(
        cache: new LocalCache(),
        minResolveInterval: 60,
        maxResolveInterval: 600,
    ))
    ->build();

Default resolvers by scheme:

  • dns -> DnsResolver
  • ipv4, ipv6, unix -> StaticResolver
  • passthrough -> PassthroughResolver

You can also implement your own EndpointResolver for service discovery backends like Consul/etcd.

Error handling

use Thesis\Grpc\InvokeError;

try {
    $response = $client->authenticate(new AuthenticateRequest('root', 'secret'));
} catch (InvokeError $e) {
    dump($e->statusCode, $e->statusMessage, $e->details);
}

Compression

Compression reduces payload size and is useful for large protobuf messages or bandwidth-constrained links.

use Thesis\Grpc\Compression\GzipCompressor;

$client = new Client\Builder()
    ->withCompression(new GzipCompressor())
    ->build();

Interceptors

Interceptors let you add cross-cutting logic (auth, logging, tracing, metadata enrichment) without changing service stubs. Mirroring gRPC, they come in two flavours, registered on separate chains:

  • UnaryInterceptor wraps a whole unary request → response call (via withUnaryInterceptors()). A retry policy, for instance, is naturally a unary interceptor — re-invoking $invoker reuses the call's pick context, so the transport fails over to another endpoint automatically.
  • StreamInterceptor wraps the creation of a stream (via withStreamInterceptors()), and so also covers the underlying stream of every streaming RPC.

A unary call does not run the stream chain and vice versa. An interceptor that must apply to both (e.g. auth) implements both interfaces and is registered on both chains.

use Amp\Cancellation;
use Thesis\Grpc\Client;
use Thesis\Grpc\Client\Invoke;
use Thesis\Grpc\Client\StreamInterceptor;
use Thesis\Grpc\Client\UnaryInterceptor;
use Thesis\Grpc\ClientStream;
use Thesis\Grpc\Metadata;

final readonly class ClientAuthInterceptor implements UnaryInterceptor, StreamInterceptor
{
    #[\Override]
    public function interceptUnary(object $request, Invoke $invoke, Metadata $md, Cancellation $cancellation, callable $invoker): object
    {
        return $invoker($request, $invoke, $md->with('Authorization', 'supertoken'), $cancellation);
    }

    #[\Override]
    public function interceptStream(Invoke $invoke, Metadata $md, Cancellation $cancellation, callable $newStream): ClientStream
    {
        return $newStream($invoke, $md->with('Authorization', 'supertoken'), $cancellation);
    }
}

$auth = new ClientAuthInterceptor();

$client = new Client\Builder()
    ->withUnaryInterceptors($auth)
    ->withStreamInterceptors($auth)
    ->build();

Client streaming

Use client streaming when you need to send many messages and receive one final aggregated response.

use File\Api\V1\Chunk;
use File\Api\V1\FileServiceClient;

$files = new FileServiceClient(new Client\Builder()->build());
$upload = $files->upload();

for ($i = 0; $i < 10; ++$i) {
    $upload->send(new Chunk(random_bytes(10)));
}

$info = $upload->close(); // FileInfo
dump($info->size); // 100

Server streaming

Use server streaming when a single request should return a sequence of server messages.

use Topic\Api\V1\SubscribeRequest;
use Topic\Api\V1\TopicServiceClient;

$topics = new TopicServiceClient(new Client\Builder()->build());
$stream = $topics->subscribe(new SubscribeRequest('payments'));

foreach ($stream as $event) {
    dump($event->name, $event->payload);
}

Bidirectional streaming

Use bidirectional streaming for conversational protocols where both sides can send messages independently.

use Chat\Api\V1\Message;
use Chat\Api\V1\MessengerServiceClient;

$chat = new MessengerServiceClient(new Client\Builder()->build())->chat();

$chat->send(new Message('Hi from gRPC client'));
dump($chat->receive()->text);

$chat->send(new Message('Bye'));
$chat->close();
dump($chat->receive()->text);

Closing the client

Call Client::close() to stop background resolver activity and release resources, especially when DNS re-resolution is enabled.

try {
    $response = $client->authenticate(new AuthenticateRequest('root', 'secret'));
} finally {
    $client->close();
}