Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

thesis/grpc-client

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 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 -> DnsResolver
  • ipv4, ipv6, unix -> StaticResolver
  • passthrough -> PassthroughResolver

Custom schemes (service discovery)

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.

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();
}

About

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

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

Generated from thesis-php/template