A FrankenPHP extension that allows you to run a gRPC server triggering code written either in PHP or Go. Under the hood, this extension uses the gRPC for Go library and FrankenPHP's Go extension support.
[!WARNING]
This extension is highly experimental and not recommended for production use. The public API may change at any time without notice.
go mod init example.com/mygrpcserver
Create a .proto file describing your gRPC service and messages.
Example (in a helloworld/helloworld.proto file):
syntax = "proto3";
option go_package = "example.com/mygrpcserver/helloworld";
package helloworld;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
Generate the Go code:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative helloworld/helloworld.proto
package mygrpcserver
import (
"context"
"fmt"
pb "example.com/mygrpcserver/helloworld"
"github.com/dunglas/frankenphp"
phpGrpc "github.com/dunglas/frankenphp-grpc"
"github.com/go-viper/mapstructure/v2"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
func init() {
phpGrpc.RegisterGrpcServerFactory(func() *grpc.Server {
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
reflection.Register(s)
return s
})
}
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
if in.Name == "" {
return nil, fmt.Errorf("the Name field is required")
}
// Convert the request to a map[string]any
var phpRequest map[string]any
if err := mapstructure.Decode(in, &phpRequest); err != nil {
return nil, err
}
// Call the PHP code, pass the map as a PHP associative array
phpResponse, err := phpGrpc.HandleRequest(ctx, phpRequest)
if err != nil {
return nil, err
}
// Convert the PHP response (a map) back to a HelloReply struct
var response pb.HelloReply
if err := mapstructure.Decode(phpResponse.(frankenphp.AssociativeArray[any]).Map, &response); err != nil {
return nil, err
}
return &response, nil
}
Refer to the gRPC for Go documentation for more details on how to implement your gRPC service in Go. Rfer to the FrankenPHP extensions documentation for more details on how to pass data from Go to PHP and vice versa.
Create a file named grpc-worker.php in the same directory as the FrankenPHP binary we'll build later:
<?php
// Require the Composer autoloader here if needed (API Platform, Symfony, etc.)
//require __DIR__ . '/vendor/autoload.php';
// Handler outside the loop for better performance (doing less work)
$handler = static function (array $request): array {
// Do something with the gRPC request
return ['message' => "Hello, {$request['Name']}"];
};
$maxRequests = (int)($_SERVER['MAX_REQUESTS'] ?? 0);
for ($nbRequests = 0; !$maxRequests || $nbRequests < $maxRequests; ++$nbRequests) {
$keepRunning = \frankenphp_handle_request($handler);
// Call the garbage collector to reduce the chances of it being triggered in the middle of the handling of a request
gc_collect_cycles();
if (!$keepRunning) {
break;
}
}
CaddyfileCreate a Caddyfile in the same directory as the FrankenPHP binary we'll build later:
{
frankenphp
grpc {
address :50051 # Optional
worker grpc-worker.php # Optional
min_threads 50 # Optional, defaults to runtime.NumCPU()
}
}
Run the server:
XCADDY_DEBUG=1
CGO_ENABLED=1 \
CGO_CFLAGS="$(php-config --includes) -I/opt/homebrew/include/" \
CGO_LDFLAGS="$(php-config --ldflags) $(php-config --libs) -L/opt/homebrew/lib/ -L/usr/lib" \
xcaddy build
./caddy run
Your gRPC server should now be running on localhost:50051.
We recommend using gRPC UI (a Postman-like GUI for gRPC) to test your server.
Created by Kévin Dunglas and sponsored by Les-Tilleuls.coop.
—
$ claude mcp add frankenphp-grpc \
-- python -m otcore.mcp_server <graph>