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.
- 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
composer require thesis/grpc-clientTo generate gRPC client classes from .proto, use:
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'),
);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();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')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);
}
}Resolver is selected by target scheme. You can override the resolver for a scheme:
use Amp\Cache\LocalCache;
use Thesis\Grpc\Client\Builder;
use Thesis\Grpc\Client\EndpointResolver\DnsResolver;
$client = new Builder()
->withHost('dns:///my-grpc-server:50051')
->withEndpointResolver('dns', new DnsResolver(
cache: new LocalCache(),
minResolveInterval: 60,
maxResolveInterval: 600,
))
->build();Default resolvers by scheme:
dns->DnsResolveripv4,ipv6,unix->StaticResolverpassthrough->PassthroughResolver
Register a resolver under any scheme to plug in a service registry such as etcd or
Consul. The target is then <scheme>://[authority]/<endpoint>, and the endpoint is
handed to the resolver as $target->opaque:
$client = new Builder()
->withEndpointResolver('etcd', new EtcdResolver($etcd, prefix: '/services/'))
->withLoadBalancer(new RoundRobinFactory())
->withHost('etcd:///echo')
->build();An EndpointResolver returns the current Resolution and may push updates over time
through EndpointResolverListener::onResolve(), which the balancer picks up live.
use Thesis\Grpc\InvokeError;
try {
$response = $client->authenticate(new AuthenticateRequest('root', 'secret'));
} catch (InvokeError $e) {
dump($e->statusCode, $e->statusMessage, $e->details);
}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 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();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); // 100Use 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);
}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);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();
}