diff --git a/README.md b/README.md index abbb585..d76b2f9 100644 --- a/README.md +++ b/README.md @@ -215,6 +215,63 @@ return [ 'grpc' => [ 'services' => [ \App\GRPC\EchoServiceInterface::class => \App\GRPC\EchoService::class, + ] + ], +]; +``` + +#### gRPC Server Interceptors + +Create your interceptor by implementing `Spiral\RoadRunnerLaravel\Grpc\GrpcServerInterceptorInterface`: + +```php + [ + 'services' => [ + // Simple service configuration + \App\GRPC\EchoServiceInterface::class => \App\GRPC\EchoService::class, + + // Service with specific interceptors + \App\GRPC\UserServiceInterface::class => [ + \App\GRPC\UserService::class, + 'interceptors' => [ + \App\GRPC\Interceptors\ValidationInterceptor::class, + \App\GRPC\Interceptors\CacheInterceptor::class, + ], + ], + ], + // Global interceptors - applied to all services + 'interceptors' => [ + \App\GRPC\Interceptors\LoggingInterceptor::class, + \App\GRPC\Interceptors\AuthenticationInterceptor::class, ], ], ]; diff --git a/config/roadrunner.php b/config/roadrunner.php index 77837ae..8f39233 100644 --- a/config/roadrunner.php +++ b/config/roadrunner.php @@ -17,6 +17,18 @@ 'grpc' => [ 'services' => [ // GreeterInterface::class => new Greeter::class, + + // Service with specific interceptors + // AnotherGreeterInterface::class => [ + // AnotherGreeterService::class, + // 'interceptors' => [ + // AnotherGreeterServiceInterceptor::class, + // ], + // ], + ], + // Global interceptors - applied to all services + 'interceptors' => [ + // AllServiceInterceptor::class, ], 'clients' => [ 'interceptors' => [ diff --git a/src/Grpc/GrpcServerInterceptorInterface.php b/src/Grpc/GrpcServerInterceptorInterface.php new file mode 100644 index 0000000..7bb8a37 --- /dev/null +++ b/src/Grpc/GrpcServerInterceptorInterface.php @@ -0,0 +1,12 @@ + $services */ $services = $app->get('config')->get('roadrunner.grpc.services', []); + /** @var array $interceptors for all services */ + $interceptors = $app->get('config')->get('roadrunner.grpc.interceptors', []); foreach ($services as $interface => $service) { - $server->registerService($interface, $app->make($service)); + if (is_array($service)) { + if (!isset($service[0]) || !is_string($service[0])) { + throw new \InvalidArgumentException("Service array must have a class name at index 0 for interface: {$interface}"); + } + + $serviceInterceptors = array_merge($interceptors, $service['interceptors'] ?? []); + $service = $service[0]; + } else { + $serviceInterceptors = $interceptors; + } + + $server->registerService($interface, $app->make($service), $serviceInterceptors); } $server->serve(Worker::create()); diff --git a/src/Grpc/Server.php b/src/Grpc/Server.php index 181fab2..b7c4d8f 100644 --- a/src/Grpc/Server.php +++ b/src/Grpc/Server.php @@ -37,6 +37,9 @@ final class Server /** @var ServiceWrapper[] */ private array $services = []; + /** @var class-string[] */ + private array $interceptors = []; + /** * @param ServerOptions $options */ @@ -58,13 +61,24 @@ public function __construct( * * @param class-string $interface Generated service interface. * @param T $service Must implement interface. + * @param array> $interceptors for this service. Must implement + * GrpcServerInterceptorInterface. * @throws ServiceException */ - public function registerService(string $interface, ServiceInterface $service): void + public function registerService(string $interface, ServiceInterface $service, array $interceptors = []): void { + foreach ($interceptors as $interceptor) { + if (!is_subclass_of($interceptor, GrpcServerInterceptorInterface::class)) { + throw new ServiceException( + sprintf('Interceptor %s must implement %s', $interceptor, GrpcServerInterceptorInterface::class) + ); + } + } + $service = new ServiceWrapper($this->invoker, $interface, $service); $this->services[$service->getName()] = $service; + $this->interceptors[$service->getName()] = $interceptors; } /** @@ -136,17 +150,30 @@ public function serve(?WorkerInterface $worker = null, ?callable $finalize = nul /** * Invoke service method with binary payload and return the response. * - * @param class-string $service + * @param class-string $serviceName * @param non-empty-string $method * @throws GRPCException */ - protected function invoke(string $service, string $method, ContextInterface $context, string $body): string + protected function invoke(string $serviceName, string $method, ContextInterface $context, string $body): string { - if (!isset($this->services[$service])) { - throw NotFoundException::create("Service `{$service}` not found.", StatusCode::NOT_FOUND); + if (!isset($this->services[$serviceName])) { + throw NotFoundException::create("Service `{$serviceName}` not found.", StatusCode::NOT_FOUND); } - return $this->services[$service]->invoke($method, $context, $body); + $service = $this->services[$serviceName]; + $interceptors = $this->interceptors[$serviceName] ?? []; + + $handler = function ($method, $context, $body) use ($service) { + return $service->invoke($method, $context, $body); + }; + + $pipeline = array_reduce( + array_reverse($interceptors), + fn($next, $interceptor) => fn($method, $context, $body) => (new $interceptor)->intercept($method, $context, $body, $next), + $handler + ); + + return $pipeline($method, $context, $body); } private function workerError(WorkerInterface $worker, string $message): void