I’m thrilled to announce a brand-new feature I unveiled today during my opening keynote at the API Platform Conference: an experimental extension for FrankenPHP that brings gRPC support to PHP! 🚀
This extension allows you to build high-performance gRPC servers using PHP, Go, or even a mix of both. It leverages the power of FrankenPHP’s Go extension support and the mature gRPC-Go
library to deliver exceptional performance.
Ready to dive into the code? The brand-new gRPC extension is free and open-source and waiting for you on GitHub! If you’re as excited about bringing high-performance gRPC to PHP as I am, head over to the repository and give it a star ⭐. Your support is hugely appreciated and helps the project grow!
Key Features of the gRPC Extension
- High-Performance Server: Run a powerful gRPC server with FrankenPHP, executing your PHP code in a persistent worker loop for maximum efficiency.
- PHP and Go Handlers: Write your gRPC service handlers in either PHP or Go, giving you the flexibility to use the best language for the job. You can even mix and match them within the same server!
- Full
gRPC-Go
Compatibility: Benefit from all the features of the robust and widely-usedgRPC-Go
library. - Pure Go Implementation: The extension is written entirely in Go, with no C code involved.
- East API Platform Integration: Seamlessly integrate with API Platform for building powerful and modern APIs.
This extension is highly experimental and not yet recommended for production use. It currently requires the main
branch of FrankenPHP and its public API may change at any time without notice.
Getting Started
Let’s walk through how to set up a gRPC server with the new FrankenPHP extension.
1. Create a New Go Module for Your gRPC Server
go mod init example.com/mygrpcserver
2. Create a Protobuf Definition
First, define your gRPC service and messages in a .proto
file. Here’s a simple example:
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;
}
Then, generate the Go code from your .proto
file:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative helloworld/helloworld.proto
3. Implement the gRPC Server in Go
Next, create the Go part of your gRPC server. This code will handle the gRPC requests and forward them to your PHP worker.
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(_ 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 := phpGrpc.HandleRequest(phpRequest)
// Convert the PHP response (a map) back to a HelloReply struct
var response pb.HelloReply
if err := mapstructure.Decode(phpResponse.(frankenphp.AssociativeArray).Map, &response); err != nil {
return nil, err
}
return &response, nil
}
4. Implement the gRPC Service Handler in PHP
Now, create a grpc-worker.php
file to handle the logic of your gRPC service.
<?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;
}
}
5. Enable the FrankenPHP Extension with a Caddyfile
Create a Caddyfile
to configure FrankenPHP and enable the gRPC extension.
{
frankenphp
grpc {
address :50051
worker grpc-worker.php
min_threads 50
}
}
6. Build and Run
Finally, build your custom FrankenPHP binary with the gRPC extension and run it.
Be sure to install FrankenPHP dependencies first.
XCADDY_DEBUG=1 \
CGO_ENABLED=1 \
XCADDY_GO_BUILD_FLAGS="-tags=nobadger,nomysql,nopgx" \
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 will now be running on localhost:50051
. I recommend using a tool like gRPC UI to test your new server.
I’m excited to see what the community builds with this new capability. Your feedback is highly welcome, so please give it a try and share your thoughts!
Here are the slides I presented during the keynote: