# folk-plugin-grpc
gRPC plugin for Folk — accepts gRPC calls via tonic/HTTP2 and dispatches to PHP workers. No Rust protobuf codegen required — all protobuf handling happens in PHP.
## Configuration
Add to `folk.build.toml`:
```toml
[[plugin]]
crate_name = "folk-plugin-grpc"
version = "0.1"
config_key = "grpc"
```
Configure in `folk.toml`:
```toml
[grpc]
listen = "0.0.0.0:50051"
```
### Settings
| `listen` | `SocketAddr` | `"0.0.0.0:50051"` | Address and port to listen on for gRPC connections. |
| `reflection` | `bool` | `false` | *Planned.* gRPC server reflection (allows `grpcurl` without `-proto` flag). Not yet implemented. |
Full example:
```toml
[grpc]
listen = "0.0.0.0:50051"
```
Currently, clients must provide the `.proto` file for service discovery:
```bash
grpcurl -plaintext -proto proto/greeter.proto localhost:50051 list
grpcurl -plaintext -proto proto/greeter.proto localhost:50051 describe greeter.Greeter
```
## How it works
```
gRPC client ──HTTP/2──> folk-plugin-grpc (Rust)
│
├── extracts service, method, payload, metadata from request
├── encodes as MessagePack {service, method, payload, metadata}
└── dispatches to PHP worker via grpc.call RPC
│
▼
PHP handler
│
├── decodes protobuf request (google/protobuf PHP)
├── executes business logic
├── encodes protobuf response
└── returns raw bytes
│
▼
folk-plugin-grpc
│
└── wraps in gRPC frame, sends HTTP/2 response
```
The Rust plugin handles HTTP/2, gRPC framing, and metadata extraction.
PHP handles protobuf serialization and business logic.
### Metadata
gRPC metadata (the equivalent of HTTP headers) is extracted from the request and passed to PHP handlers via `Context`. Standard HTTP/2 and gRPC internal headers are filtered out — everything else is available:
```php
$ctx->getValue('authorization'); // "Bearer eyJ..."
$ctx->getValue('x-request-id'); // "abc-123"
$ctx->getMetadata(); // all metadata as array
```
## Usage with folk-sdk (plain PHP)
### 1. Define your proto
```protobuf
syntax = "proto3";
package greeter;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }
```
### 2. Generate PHP classes
```bash
protoc --php_out=. proto/greeter.proto
```
This generates message classes (`HelloRequest`, `HelloReply`) but not service interfaces. You write the handler manually.
### 3. Write the handler (raw bytes mode)
```php
<?php
use Folk\Sdk\Grpc\GrpcModeHandler;
use Folk\Sdk\Grpc\Context;
class GreeterHandler implements GrpcModeHandler
{
public function call(string $service, string $method, string $payload, Context $context): string
{
return match ($method) {
'SayHello' => $this->sayHello($payload, $context),
default => throw new \RuntimeException("Unknown method: {$method}"),
};
}
private function sayHello(string $payload, Context $context): string
{
$request = new \Greeter\HelloRequest();
$request->mergeFromString($payload);
$token = $context->getValue('authorization'); // metadata access
$reply = new \Greeter\HelloReply();
$reply->setMessage("Hello, {$request->getName()}!");
return $reply->serializeToString();
}
}
```
### 4. Register
```php
$loop = new \Folk\Sdk\Worker\WorkerLoop();
$loop->registerGrpcHandler(new GreeterHandler());
$loop->run();
```
## Usage with Laravel (folk-laravel)
Laravel integration uses typed handlers with auto-detection of protobuf parameters.
### 1. Define your proto and generate PHP classes
```bash
protoc --php_out=app/Proto proto/greeter.proto
```
### 2. Create a service interface
```php
<?php
// app/Proto/Greeter/GreeterInterface.php
namespace App\Proto\Greeter;
use Folk\Sdk\Grpc\ServiceInterface;
interface GreeterInterface extends ServiceInterface
{
public const NAME = "greeter.Greeter";
public function SayHello(HelloRequest $in): HelloReply;
public function SayGoodbye(HelloRequest $in): HelloReply;
}
```
The `NAME` constant must match the protobuf `package.Service` name — this is how the router maps incoming gRPC calls to your handler.
### 3. Implement the service
```php
<?php
// app/Grpc/GreeterService.php
namespace App\Grpc;
use App\Proto\Greeter\GreeterInterface;
use App\Proto\Greeter\HelloReply;
use App\Proto\Greeter\HelloRequest;
class GreeterService implements GreeterInterface
{
public function SayHello(HelloRequest $in): HelloReply
{
$reply = new HelloReply();
$reply->setMessage("Hello, {$in->getName()}!");
return $reply;
}
public function SayGoodbye(HelloRequest $in): HelloReply
{
$reply = new HelloReply();
$reply->setMessage("Goodbye, {$in->getName()}!");
return $reply;
}
}
```
The `GrpcRouter` auto-detects protobuf-typed parameters and handles `mergeFromString()` / `serializeToString()` automatically. You work with typed objects, not raw bytes.
### 4. Register in config
```php
// config/folk.php
return [
'grpc' => [
'services' => [
\App\Proto\Greeter\GreeterInterface::class => \App\Grpc\GreeterService::class,
],
],
];
```
That's it. `FolkServiceProvider` reads the config, resolves services from the Laravel container, and registers them with the `GrpcRouter`.
### Accessing metadata in typed handlers
Add `Context` as the first parameter:
```php
use Folk\Sdk\Grpc\Context;
class GreeterService implements GreeterInterface
{
public function SayHello(Context $ctx, HelloRequest $in): HelloReply
{
$token = $ctx->getValue('authorization');
// ...
}
}
```
The router detects `Context` parameters automatically and injects the metadata.
### Multiple services
Register as many services as you need:
```php
// config/folk.php
'grpc' => [
'services' => [
\App\Proto\Greeter\GreeterInterface::class => \App\Grpc\GreeterService::class,
\App\Proto\Auth\AuthInterface::class => \App\Grpc\AuthService::class,
\App\Proto\Orders\OrdersInterface::class => \App\Grpc\OrdersService::class,
],
],
```
### Compatibility with Spiral/RoadRunner
If you have services generated by `protoc-gen-php-grpc` (Spiral), they work with Folk:
```php
// Spiral-generated interface
interface InfoInterface extends \Spiral\RoadRunner\GRPC\ServiceInterface
{
public const NAME = "myapp.Info";
public function Get(\Spiral\RoadRunner\GRPC\ContextInterface $ctx, \Google\Protobuf\GPBEmpty $in): ServiceInfo;
}
// Register as-is — Folk injects its own Context for the $ctx parameter
'services' => [
InfoInterface::class => InfoService::class,
],
```
The `NAME` constant is read from any interface the handler implements. Context parameters (regardless of type) receive Folk's `Context` instance.
## Testing with curl
```bash
# Encode HelloRequest { name: "Folk" } as protobuf
# Field 1 (string): 0x0a + length + bytes
printf '\x00\x00\x00\x00\x06\x0a\x04Folk' | curl --http2-prior-knowledge \
-X POST http://localhost:50051/greeter.Greeter/SayHello \
-H "content-type: application/grpc" \
-H "authorization: Bearer my-token" \
--data-binary @- -o response.bin
# Decode response (skip 5-byte gRPC frame)
python3 -c "
data = open('response.bin','rb').read()
proto = data[5:]
print(proto[2:2+proto[1]].decode())
"
# Output: Hello, Folk!
```
## Testing with grpcurl
```bash
# List services (requires -proto flag)
grpcurl -plaintext -proto proto/greeter.proto localhost:50051 list
# Describe a service
grpcurl -plaintext -proto proto/greeter.proto localhost:50051 describe greeter.Greeter
# Note: grpcurl RPC calls require HTTP/2 trailers support (in progress)
```
## Requirements
- PHP: `google/protobuf` package (`composer require google/protobuf`)
- `protoc` for generating PHP message classes
## License
MIT