PHP Language Support
PHP Language Support
Section titled “PHP Language Support”Status: ✅ Full Support
Example: examples/php-twirp/
PHP support provides Protocol Buffer messages and Twirp RPC framework integration for building modern PHP APIs.
Features
Section titled “Features”- Protocol Buffer Messages - Generate PHP classes for all your protobuf messages
- gRPC Support - Full client and server code generation
- RoadRunner Integration - High-performance application server with persistent workers
- Framework Integration - Built-in support for Laravel and Symfony
- Async PHP - ReactPHP, Swoole/OpenSwoole, and PHP 8.1+ Fibers support
- Developer Experience - Auto-generated composer.json, examples, and helper scripts
Available Plugins
Section titled “Available Plugins”Plugin | Description | Generated Files |
---|---|---|
protoc-gen-php | Message classes | *.php , GPBMetadata/*.php |
protoc-gen-twirp_php | Twirp RPC framework | *Client.php , *Server.php |
Configuration
Section titled “Configuration”Basic Configuration
Section titled “Basic Configuration”languages.php = { enable = true; outputPath = "gen/php"; namespace = "MyApp\\Proto";};
Full Configuration
Section titled “Full Configuration”languages.php = { enable = true; outputPath = "gen/php"; namespace = "MyApp\\Proto"; options = [ "aggregate_metadata" # Single metadata file ];
twirp = { enable = true; options = [ "generate_client=true" "generate_server=true" ]; };};
Proto Example
Section titled “Proto Example”syntax = "proto3";
package example.v1;
service HelloService { rpc Hello(HelloRequest) returns (HelloResponse); rpc ListGreetings(ListGreetingsRequest) returns (ListGreetingsResponse); rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);}
message HelloRequest { string name = 1; string language = 2;}
message HelloResponse { string greeting = 1; string language = 2;}
message User { string id = 1; string name = 2; string email = 3; repeated string roles = 4; map<string, string> metadata = 5; UserStatus status = 6;}
enum UserStatus { USER_STATUS_UNSPECIFIED = 0; USER_STATUS_ACTIVE = 1; USER_STATUS_INACTIVE = 2; USER_STATUS_SUSPENDED = 3;}
message CreateUserRequest { User user = 1;}
message CreateUserResponse { User user = 1; bool success = 2; string message = 3;}
message ListGreetingsRequest { int32 limit = 1; string language = 2;}
message ListGreetingsResponse { repeated HelloResponse greetings = 1; int32 total_count = 2;}
Generated Code Usage
Section titled “Generated Code Usage”<?phprequire_once 'vendor/autoload.php';
use MyApp\Proto\Example\V1\HelloServiceServer;use MyApp\Proto\Example\V1\HelloRequest;use MyApp\Proto\Example\V1\HelloResponse;use MyApp\Proto\Example\V1\CreateUserRequest;use MyApp\Proto\Example\V1\CreateUserResponse;use MyApp\Proto\Example\V1\User;use MyApp\Proto\Example\V1\UserStatus;
class HelloServiceImpl{ private array $users = []; private array $greetings = [ 'en' => 'Hello', 'es' => 'Hola', 'fr' => 'Bonjour', 'de' => 'Guten Tag' ];
public function Hello(HelloRequest $request): HelloResponse { $language = $request->getLanguage() ?: 'en'; $greeting = $this->greetings[$language] ?? $this->greetings['en'];
$response = new HelloResponse(); $response->setGreeting($greeting . ', ' . $request->getName() . '!'); $response->setLanguage($language);
return $response; }
public function CreateUser(CreateUserRequest $request): CreateUserResponse { $user = $request->getUser(); $userId = $user->getId() ?: uniqid('user_');
// Set ID if not provided $user->setId($userId); $user->setStatus(UserStatus::USER_STATUS_ACTIVE);
// Store user $this->users[$userId] = $user;
$response = new CreateUserResponse(); $response->setUser($user); $response->setSuccess(true); $response->setMessage('User created successfully');
return $response; }
public function ListGreetings(ListGreetingsRequest $request): ListGreetingsResponse { $language = $request->getLanguage() ?: 'en'; $limit = $request->getLimit() ?: 10;
$greetings = []; $names = ['World', 'PHP', 'Protobuf', 'Twirp'];
for ($i = 0; $i < min($limit, count($names)); $i++) { $greeting = new HelloResponse(); $greeting->setGreeting($this->greetings[$language] . ', ' . $names[$i] . '!'); $greeting->setLanguage($language); $greetings[] = $greeting; }
$response = new ListGreetingsResponse(); foreach ($greetings as $greeting) { $response->getGreetings()[] = $greeting; } $response->setTotalCount(count($greetings));
return $response; }}
// Create and serve$impl = new HelloServiceImpl();$server = new HelloServiceServer($impl);
// Handle HTTP request$method = $_SERVER['REQUEST_METHOD'];$uri = $_SERVER['REQUEST_URI'];$headers = getallheaders();$body = file_get_contents('php://input');
try { $server->handle($method, $uri, $headers, $body);} catch (Exception $e) { http_response_code(500); echo json_encode(['error' => $e->getMessage()]);}
<?phprequire_once 'vendor/autoload.php';
use MyApp\Proto\Example\V1\HelloServiceClient;use MyApp\Proto\Example\V1\HelloRequest;use MyApp\Proto\Example\V1\CreateUserRequest;use MyApp\Proto\Example\V1\User;use MyApp\Proto\Example\V1\ListGreetingsRequest;
// Create client$client = new HelloServiceClient('http://localhost:8080');
// Set custom headers$client->setHeaders([ 'Authorization' => 'Bearer your-token', 'X-Client-Version' => '1.0.0']);
try { // Simple hello request $helloRequest = new HelloRequest(); $helloRequest->setName('PHP Developer'); $helloRequest->setLanguage('en');
$helloResponse = $client->Hello($helloRequest); echo "Greeting: " . $helloResponse->getGreeting() . "\n";
// Create a user $user = new User(); $user->setName('John Doe'); $user->setEmail('john@example.com'); $user->setRoles(['developer', 'admin']);
// Set metadata $metadata = $user->getMetadata(); $metadata['department'] = 'Engineering'; $metadata['location'] = 'Remote';
$createRequest = new CreateUserRequest(); $createRequest->setUser($user);
$createResponse = $client->CreateUser($createRequest); if ($createResponse->getSuccess()) { echo "Created user: " . $createResponse->getUser()->getName() . "\n"; echo "User ID: " . $createResponse->getUser()->getId() . "\n"; }
// List greetings $listRequest = new ListGreetingsRequest(); $listRequest->setLimit(5); $listRequest->setLanguage('es');
$listResponse = $client->ListGreetings($listRequest); echo "Found " . $listResponse->getTotalCount() . " greetings:\n";
foreach ($listResponse->getGreetings() as $greeting) { echo "- " . $greeting->getGreeting() . "\n"; }
} catch (TwirpError $e) { echo "Twirp Error (" . $e->getCode() . "): " . $e->getMessage() . "\n"; echo "Details: " . json_encode($e->getDetails()) . "\n";} catch (Exception $e) { echo "Error: " . $e->getMessage() . "\n";}
<?phprequire_once 'vendor/autoload.php';
use MyApp\Proto\Example\V1\User;use MyApp\Proto\Example\V1\UserStatus;
// Create a new user$user = new User();$user->setId('user123');$user->setName('Jane Smith');$user->setEmail('jane@example.com');$user->setRoles(['developer', 'team_lead']);$user->setStatus(UserStatus::USER_STATUS_ACTIVE);
// Work with map fields$metadata = $user->getMetadata();$metadata['department'] = 'Engineering';$metadata['team'] = 'Backend';$metadata['start_date'] = '2024-01-15';
// Serialize to binary format$binaryData = $user->serializeToString();echo "Binary size: " . strlen($binaryData) . " bytes\n";
// Deserialize from binary$newUser = new User();$newUser->mergeFromString($binaryData);echo "Deserialized user: " . $newUser->getName() . "\n";
// JSON serialization$jsonData = $user->serializeToJsonString();echo "JSON: " . $jsonData . "\n";
// JSON deserialization$fromJsonUser = new User();$fromJsonUser->mergeFromJsonString($jsonData);echo "From JSON: " . $fromJsonUser->getEmail() . "\n";
// Work with repeated fieldsecho "User roles:\n";foreach ($user->getRoles() as $role) { echo "- $role\n";}
// Work with map fieldsecho "User metadata:\n";foreach ($user->getMetadata() as $key => $value) { echo "- $key: $value\n";}
// Check enum valuesswitch ($user->getStatus()) { case UserStatus::USER_STATUS_ACTIVE: echo "User is active\n"; break; case UserStatus::USER_STATUS_INACTIVE: echo "User is inactive\n"; break; case UserStatus::USER_STATUS_SUSPENDED: echo "User is suspended\n"; break; default: echo "Unknown user status\n";}
<?phpnamespace App\Http\Controllers;
use Illuminate\Http\Request;use MyApp\Proto\Example\V1\HelloServiceClient;use MyApp\Proto\Example\V1\CreateUserRequest;use MyApp\Proto\Example\V1\User;
class UserController extends Controller{ private HelloServiceClient $protoClient;
public function __construct() { $this->protoClient = new HelloServiceClient( config('services.proto.url') ); }
public function store(Request $request) { $validated = $request->validate([ 'name' => 'required|string|max:255', 'email' => 'required|email|unique:users', 'roles' => 'array' ]);
try { // Create protobuf user $protoUser = new User(); $protoUser->setName($validated['name']); $protoUser->setEmail($validated['email']); $protoUser->setRoles($validated['roles'] ?? []);
$createRequest = new CreateUserRequest(); $createRequest->setUser($protoUser);
$response = $this->protoClient->CreateUser($createRequest);
if ($response->getSuccess()) { return response()->json([ 'success' => true, 'user' => [ 'id' => $response->getUser()->getId(), 'name' => $response->getUser()->getName(), 'email' => $response->getUser()->getEmail(), ] ]); }
return response()->json([ 'success' => false, 'message' => $response->getMessage() ], 400);
} catch (\Exception $e) { return response()->json([ 'success' => false, 'message' => 'Failed to create user: ' . $e->getMessage() ], 500); } }}
// config/services.phpreturn [ 'proto' => [ 'url' => env('PROTO_SERVICE_URL', 'http://localhost:8080'), ],];
Testing
Section titled “Testing”<?phpuse PHPUnit\Framework\TestCase;use MyApp\Proto\Example\V1\User;use MyApp\Proto\Example\V1\UserStatus;
class ProtoTest extends TestCase{ public function testUserCreation() { $user = new User(); $user->setId('test123'); $user->setName('Test User'); $user->setEmail('test@example.com'); $user->setStatus(UserStatus::USER_STATUS_ACTIVE);
$this->assertEquals('test123', $user->getId()); $this->assertEquals('Test User', $user->getName()); $this->assertEquals('test@example.com', $user->getEmail()); $this->assertEquals(UserStatus::USER_STATUS_ACTIVE, $user->getStatus()); }
public function testSerialization() { $user = new User(); $user->setName('Serialization Test'); $user->setEmail('serialize@test.com');
// Test binary serialization $binary = $user->serializeToString(); $this->assertNotEmpty($binary);
$decoded = new User(); $decoded->mergeFromString($binary); $this->assertEquals($user->getName(), $decoded->getName());
// Test JSON serialization $json = $user->serializeToJsonString(); $this->assertJson($json);
$fromJson = new User(); $fromJson->mergeFromJsonString($json); $this->assertEquals($user->getEmail(), $fromJson->getEmail()); }
public function testMapFields() { $user = new User(); $metadata = $user->getMetadata(); $metadata['key1'] = 'value1'; $metadata['key2'] = 'value2';
$this->assertCount(2, $user->getMetadata()); $this->assertEquals('value1', $user->getMetadata()['key1']); }}
Composer Configuration
Section titled “Composer Configuration”{ "name": "myapp/proto-example", "require": { "php": "^8.1", "google/protobuf": "^3.25" }, "require-dev": { "phpunit/phpunit": "^10.0" }, "autoload": { "psr-4": { "MyApp\\Proto\\": "gen/php/" } }, "scripts": { "proto": "bufrnix", "test": "phpunit tests/" }}
Best Practices
Section titled “Best Practices”- Namespaces: Use proper PSR-4 namespaces matching your project structure
- Error Handling: Always catch TwirpError for RPC-specific errors
- Validation: Validate data before creating protobuf messages
- Serialization: Use binary format for performance, JSON for debugging
- Testing: Test both message creation and serialization/deserialization
- Autoloading: Configure Composer autoloading for generated classes
Generated Files Structure
Section titled “Generated Files Structure”Directorygen/php/ - MyApp/ - Proto/ - Example/ - V1/ - HelloServiceClient.php - HelloServiceServer.php - GPBMetadata/ - Example/ - V1/ - Service.php
- …
Try the Example
Section titled “Try the Example”cd examples/php-twirpnix developcomposer installphp -S localhost:8080 -t .
# In another terminalphp client.php
Troubleshooting
Section titled “Troubleshooting”Autoloading Issues
Section titled “Autoloading Issues”Ensure your composer.json includes the generated namespace:
{ "autoload": { "psr-4": { "MyApp\\Proto\\": "gen/php/" } }}
Twirp Errors
Section titled “Twirp Errors”Handle Twirp-specific errors properly:
try { $response = $client->SomeMethod($request);} catch (TwirpError $e) { // Handle Twirp protocol errors echo "Twirp Error: " . $e->getCode() . " - " . $e->getMessage();} catch (Exception $e) { // Handle other errors echo "General Error: " . $e->getMessage();}
Memory Usage
Section titled “Memory Usage”For large messages, be aware of PHP’s memory limits:
ini_set('memory_limit', '256M');
Configuration Options
Section titled “Configuration Options”Core Options
Section titled “Core Options”Option | Type | Default | Description |
---|---|---|---|
enable | bool | false | Enable PHP code generation |
package | package | php with extensions | PHP package to use |
outputPath | string | ”gen/php” | Output directory for generated code |
namespace | string | ”Generated” | Base PHP namespace |
metadataNamespace | string | ”GPBMetadata” | Metadata namespace |
classPrefix | string | "" | Prefix for generated classes |
Composer Options
Section titled “Composer Options”Option | Type | Default | Description |
---|---|---|---|
composer.enable | bool | true | Enable Composer integration |
composer.autoInstall | bool | false | Auto-install dependencies |
gRPC Options
Section titled “gRPC Options”Option | Type | Default | Description |
---|---|---|---|
grpc.enable | bool | false | Enable gRPC generation |
grpc.clientOnly | bool | false | Generate only client code |
grpc.serviceNamespace | string | ”Services” | Service namespace suffix |
RoadRunner Options
Section titled “RoadRunner Options”Option | Type | Default | Description |
---|---|---|---|
roadrunner.enable | bool | false | Enable RoadRunner server |
roadrunner.workers | int | 4 | Number of worker processes |
roadrunner.maxJobs | int | 64 | Jobs before worker restart |
roadrunner.maxMemory | int | 128 | Memory limit per worker (MB) |
roadrunner.tlsEnabled | bool | false | Enable TLS support |
Usage Examples
Section titled “Usage Examples”Basic Message Usage
Section titled “Basic Message Usage”use App\Proto\Example\V1\HelloRequest;use App\Proto\Example\V1\HelloResponse;
// Create a request$request = new HelloRequest();$request->setName('World');$request->setCount(5);
// Serialize to string$data = $request->serializeToString();
// Deserialize from string$decoded = new HelloRequest();$decoded->mergeFromString($data);
echo $decoded->getName(); // "World"
gRPC Client
Section titled “gRPC Client”use App\Proto\Example\V1\Services\GreeterServiceClient;use Grpc\ChannelCredentials;
// Create client$client = new GreeterServiceClient('localhost:9001', [ 'credentials' => ChannelCredentials::createInsecure(),]);
// Make unary call[$response, $status] = $client->SayHello($request)->wait();
if ($status->code === \Grpc\STATUS_OK) { echo $response->getMessage();}
// Server streaming$call = $client->SayHelloStream($request);foreach ($call->responses() as $response) { echo $response->getMessage() . "\n";}
RoadRunner Server
Section titled “RoadRunner Server”Configuration
Section titled “Configuration”{
description = "PHP gRPC with RoadRunner Generation using Bufrnix";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
bufrnix.url = "github:conneroisu/bufrnix";
bufrnix.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = {
nixpkgs,
flake-utils,
bufrnix,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = nixpkgs.legacyPackages.${system};
# Create a bufrnix package for this project
bufrnixPackage = bufrnix.lib.mkBufrnixPackage {
inherit pkgs;
config = {
root = ./.;
debug.enable = true;
protoc = {
sourceDirectories = ["./proto"];
includeDirectories = ["./proto"];
};
languages.php = {
enable = true;
outputPath = "gen/php";
namespace = "";
metadataNamespace = "";
# Enable gRPC support
grpc = {
enable = true;
serviceNamespace = "Services";
clientOnly = false; # Generate both client and server code
};
# Enable RoadRunner for server interfaces
roadrunner = {
enable = true;
workers = 4;
maxJobs = 100;
maxMemory = 128;
};
};
};
};
in {
packages = {
default = bufrnixPackage;
};
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
# Core tools
bufrnixPackage
php82
php82Packages.composer
# Development tools
git
curl
jq
# PHP development
php82Packages.psalm
php82Packages.php-cs-fixer
];
};
});
}
use Spiral\RoadRunner\GRPC\Server;use Spiral\RoadRunner\Worker;
$server = new Server();$server->registerService( GreeterServiceInterface::class, new GreeterService());$server->serve(Worker::create());
Start the server:
./roadrunner-dev.sh start
Framework Integration
Section titled “Framework Integration”Laravel
Section titled “Laravel”// In a controller or servicepublic function __construct( private GreeterServiceClient $greeterClient) {}
public function greet(Request $request){ $grpcRequest = new HelloRequest(); $grpcRequest->setName($request->input('name'));
[$response, $status] = $this->greeterClient ->SayHello($grpcRequest) ->wait();
return response()->json([ 'message' => $response->getMessage(), ]);}
Symfony
Section titled “Symfony”// In a controller#[Route('/greet/{name}', name: 'greet')]public function greet( string $name, GreeterServiceClient $client): JsonResponse { $request = new HelloRequest(); $request->setName($name);
[$response, $status] = $client->SayHello($request)->wait();
return $this->json([ 'message' => $response->getMessage(), ]);}
Async Examples
Section titled “Async Examples”ReactPHP
Section titled “ReactPHP”use App\Proto\Async\ReactPHPClient;
$client = new ReactPHPClient('localhost:9001');
$client->sendRequestAsync($request)->then( function ($response) { echo "Async: " . $response->getMessage(); });
$client->run();
Swoole
Section titled “Swoole”use App\Proto\Async\SwooleGrpcServer;
$server = new SwooleGrpcServer('0.0.0.0', 9501);$server->registerService( GreeterServiceInterface::class, new GreeterService());$server->start();
PHP Fibers
Section titled “PHP Fibers”use App\Proto\Async\FiberProtobufHandler;
$handler = new FiberProtobufHandler();$results = $handler->processConcurrent([ 'req1' => $request1->serializeToString(), 'req2' => $request2->serializeToString(),]);
Performance Optimization
Section titled “Performance Optimization”PHP Extensions
Section titled “PHP Extensions”Install C extensions for better performance:
pecl install protobufpecl install grpc
RoadRunner Tuning
Section titled “RoadRunner Tuning”grpc: pool: num_workers: 8 # Increase for more concurrency max_jobs: 500 # More jobs before restart supervisor: max_worker_memory: 256 # Increase memory limit
OPcache Configuration
Section titled “OPcache Configuration”; php.iniopcache.enable=1opcache.enable_cli=1opcache.memory_consumption=256opcache.max_accelerated_files=20000
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”Extension not loaded
php -m | grep -E '(grpc|protobuf)'
RoadRunner workers crashing
# Check worker status./roadrunner-dev.sh workers
# View logs./roadrunner-dev.sh debug
Class not found errors
# Regenerate autoloadercomposer dump-autoload
# Check namespace configurationgrep namespace .rr.yaml
Debug Mode
Section titled “Debug Mode”Enable detailed logging:
logs: level: debug output: stdout
Migration from Twirp
Section titled “Migration from Twirp”If you’re migrating from the deprecated Twirp support:
- Update your flake.nix to enable gRPC instead of Twirp
- Regenerate your code with
buf generate
- Update service implementations to use RoadRunner interfaces
- Replace Twirp client calls with gRPC clients
Best Practices
Section titled “Best Practices”- Use RoadRunner for production deployments
- Enable extensions for better performance
- Configure workers based on your workload
- Implement health checks for monitoring
- Use streaming for large data transfers
- Add metrics for observability