Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Grpc Laravel Package

roadrunner/grpc

RoadRunner gRPC lets you build high-performance gRPC services in PHP on the RoadRunner application server. It combines protobuf-generated stubs with persistent workers, supports unary and streaming RPCs, and avoids per-request bootstrap overhead for fast, production-ready microservices.

View on GitHub
Deep Wiki
Context7

Getting Started

Install. You need two things: the PHP SDK and the RoadRunner binary.

composer require roadrunner-php/grpc spiral/roadrunner-worker
# binary (pick one)
./vendor/bin/rr get-binary          # via roadrunner-cli
# or download from github.com/roadrunner-server/roadrunner/releases

The stock rr binary ships the grpc plugin compiled in — you do not need the PHP ext-grpc, libgrpc, or OpenSSL. That is the whole point of the package: HTTP/2 and connection management happen in Go, PHP just serializes protobuf messages.

Where to look first.

  • src/Server.php, src/ServiceInterface.php, src/Context.php — server side (you implement services).
  • src/Invoker.php, src/InvokerInterface.php — client side (you call services).
  • src/Exception/ — the gRPC status-code exception hierarchy; this is your error contract.
  • The protoc plugin binary installed to vendor/bin (protoc-gen-php-grpc) — it generates the interfaces/stubs the SDK expects, not ext-grpc stubs.
  • The repo's proto/ + README worker example — copy the worker loop verbatim, it is the least obvious part.

Minimal wiring. .rr.yaml needs a rpc endpoint (the client invoker talks to the plugin over it) and a grpc section with the same .proto files you generated code from:

version: '3'
rpc:
  listen: tcp://127.0.0.1:6001
grpc:
  listen: tcp://127.0.0.1:9001
  proto:
    - proto/helloworld.proto
  pool:
    num_workers: 4
    command: php worker.php

First use case — unary hello world.

protoc -I proto \
  --php_out=./generated \
  --php-grpc_out=./generated \
  proto/helloworld.proto
// worker.php
use RoadRunner\GRPC\Server;
use Spiral\RoadRunner\Worker;
use Spiral\RoadRunner\Payload;

$server = new Server();
$server->registerService(Helloworld\GreeterInterface::class, new GreeterService());
$worker = Worker::create();

while ($payload = $worker->waitPayload()) {          // loop shape from the README
    try {
        $worker->respond(new Payload($server->serve($payload->body)));
    } catch (\Throwable $e) {
        $worker->error($e->getMessage());
    }
}
final class GreeterService implements Helloworld\GreeterInterface
{
    public function SayHello(RoadRunner\GRPC\ContextInterface $ctx, Helloworld\HelloRequest $in): Helloworld\HelloReply
    {
        return (new Helloworld\HelloReply())->setMessage('Hello ' . $in->getName());
    }
}

Call it from another RoadRunner worker (HTTP, queue, etc.):

$invoker = new RoadRunner\GRPC\Invoker($worker);           // usually a DI singleton
$client  = new Helloworld\GreeterClient($invoker, '127.0.0.1:9001');

$reply = $client->SayHello(new RoadRunner\GRPC\Context(), (new HelloRequest())->setName('Anton'));
echo $reply->getMessage();

Implementation Patterns

Keep codegen on rails. Two protoc flags, one output dir: messages via --php_out, service interfaces + client stubs via the package's gRPC plugin. Commit generated code (it diffs badly but reviews well against .proto changes), and treat proto/ as the single source of truth shared between .rr.yaml, codegen, and any Go/other-language consumers. A make proto target beats tribal knowledge.

Server: one class per proto service, one method per RPC. The service name comes from package + service in the .proto (helloworld.Greeter) and is baked into the generated interface — never hand-write an implementation of ServiceInterface with a guessed getName(). Register services once at worker boot; the loop should never re-register. Keep service classes thin: they are the transport boundary, so delegate to real application services, return proto messages only, and never return Eloquent models.

Context is your metadata bag. Server: $ctx->getValue('authorization') / $ctx->getValues() for incoming headers (keys are lowercased HTTP/2 names). Client: pass new Context(['authorization' => 'Bearer …', 'x-request-id' => $id]). Values are strings — serialize anything structured yourself.

Errors are the contract, not stack traces. Throw the SDK's typed exceptions and let the mapping do the work:

use RoadRunner\GRPC\Exception\{NotFoundException, InvalidArgumentException, UnavailableException};

if (!$user) throw new NotFoundException("User {$id} not found");

Each maps to a canonical status code that Go clients (and grpc-gateway, Envoy, retry policies) understand. Anything else is genericised — which is good, because raw PDOException messages would otherwise leak SQL to remote callers.

Client: inject InvokerInterface, not concrete clients. Bind it as a singleton in your service container and construct generated clients per target address. This gives you one extension point for everything cross-cutting — logging, timing, retry-on-Unavailable, metrics, request-id propagation:

final class LoggingInvoker implements RoadRunner\GRPC\InvokerInterface
{
    public function __construct(private InvokerInterface $inner, private LoggerInterface $log) {}

    public function invoke(/* method, ctx, in, out */): void
    {
        $start = microtime(true);
        try {
            $this->inner->invoke(...func_get_args());
        } finally {
            $this->log->debug('grpc.call', ['ms' => (microtime(true) - $start) * 1000]);
        }
    }
}

Decorating the invoker is the cleanest analogue to a client interceptor in this package.

Streaming is modelled as generators/iterables. Generated signatures for server-streaming return iterable/\Generator; you yield messages as they are produced. Remember every open stream holds a worker out of the pool for its lifetime — do not stream from a pool sized for unary traffic only.

Testing. Because everything goes through InvokerInterface, you can unit-test client-facing code with a fake invoker. For the server side you can call your service class directly (new GreeterService()->SayHello(new Context([...]), $request)) — no rr needed — and reserve rr serve for a small integration suite. Assert on exception classes and metadata, not on protobuf binary strings.

Composition in a Laravel app. One service provider: bind InvokerInterface, register generated clients as singletons keyed by target address, expose a Grpc::client(GreeterClient::class) helper or facade. Controllers stay ignorant of the transport. For long-lived queue consumers, remember the worker still needs the rpc plugin connection alive — which it has, since jobs workers are RoadRunner workers.

Gotchas and Tips

  • This client only works inside a RoadRunner worker. Invoker calls the local plugin over the RPC socket — there is no direct dial from PHP. php artisan commands, tinker, PHPUnit run from CLI, and php -S scripts will fail with "RPC not available" / connection refused. Your first debugging hour is usually spent discovering this.
  • rpc.listen is mandatory. Adding grpc: to .rr.yaml without an rpc: section gives a working server and a silently broken client.
  • Proto drift is the #1 runtime bug. The .proto list in grpc.proto must match the files you generated from. Stale worker code against new protos yields "cannot parse invalid wire-format data" or UNKNOWN with no useful message — not a schema error. Regenerate, restart rr, and re-check the list.
  • Service name mismatch (renamed package, hand-rolled implementation, generated class moved namespaces) surfaces as "service not found in the registry" at call time, not at boot. Print getName() once when onboarding a service.
  • Catch \Throwable in the worker loop. An escaping exception takes the worker down; the plugin restarts it, but you lose the request and pay restart cost. respond()/error() on every branch.
  • Proto3 scalar presence. hasFoo() is only generated for message fields and optional scalars. Empty string / 0 / false is indistinguishable from "unset" for plain scalars — a real correctness trap for PATCH-style APIs. Use optional or google.protobuf.*Value wrappers where "not sent" matters.
  • Metadata is strings and lowercase. No nested headers, no arrays, no ints. Encode JSON yourself and prefix custom keys (x-tenant-id).
  • No first-class per-call deadline in the SDK's client API in most versions; timeouts live on the plugin/pool side (grpc.pool.allocate_timeout, request timeouts). If you need hard deadlines, enforce them above the call and treat the orphaned request as an at-least-once hazard.
  • Measure before scaling. Pure-PHP protobuf serialization is the hot path; installing ext-protobuf is a large, near-free win. Watch message sizes — every request/response is copied through the Go↔PHP socket, so multi-megabyte payloads turn a worker pool into a memory multiplier. Prefer pagination/streaming over fat messages.
  • Version-pin the binary and the SDK together. The worker↔plugin protocol and the plugin RPC surface move between RoadRunner majors; a v2023-era PHP SDK against a v2025 binary produces confusing payload/protocol failures rather than clean errors.
  • Debugging toolkit: rr serve -d -v for plugin logs, rr workers to watch pool saturation, grpcurl pointed at grpc.listen to sanity-check the service is actually registered (independent of PHP), and protoc --decode_raw on a payload dump when wire format looks wrong. A stuck pool with all workers busy almost always means a stream was never closed or a service call is blocking on a slow upstream with no timeout.
  • Extension points to keep in mind: a decorated InvokerInterface (retries, metrics, tracing), a custom Context/ContextInterface for typed metadata, exception subclasses for domain-specific codes, and wrapping service implementations with your own middleware instead of pushing logic into generated classes.
  • 66 stars, near-zero dependents. You are reading source, not Stack Overflow, when something breaks. Budget for that: pin exact versions, keep an integration test that exercises a real rr serve, and prefer the boring unary path until you truly need streaming.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
uni-method/symfony-lib
tacticmedia/scim-server-bundle
neika-ai/symfony-bundle
mattleads/agent-publishing-bundle
glavweb/framework-bundle
glavweb/extra-bundle
glavweb/api-bundle
glavweb/admin-bundle
adachsoft/api-version-bundle
elsayed85/laravel-api-exceptions
ouhssini/laravel-demo-mode
daworks/ncloud-outbound-mailer-driver
gp247/cms
ignibyte/aic-gates
nexa-merchant/blog
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager