thesis / grpc-client
Async gRPC client for PHP with HTTP/2 transport, streaming RPCs, interceptors, TLS/mTLS, and client-side load balancing.
Fund package maintenance!
Requires
- php: ^8.4
- amphp/cache: ^2.0
- amphp/dns: ^2.4
- amphp/http-client: ^5.3
- amphp/socket: ^2.3
- league/uri: ^7.8
- thesis/grpc-protocol: ^0.2
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
- Requirements
- Basic usage
- TLS and mTLS
- Target addressing
- Load balancing
- Endpoint resolution
- Error handling
- Compression
- Interceptors
- Client streaming
- Server streaming
- Bidirectional streaming
- Closing the client
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->DnsResolveripv4,ipv6,unix->StaticResolverpassthrough->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:
UnaryInterceptorwraps a whole unaryrequest → responsecall (viawithUnaryInterceptors()). A retry policy, for instance, is naturally a unary interceptor — re-invoking$invokerreuses the call's pick context, so the transport fails over to another endpoint automatically.StreamInterceptorwraps the creation of a stream (viawithStreamInterceptors()), 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(); }