Skip to content

Commit ea05f36

Browse files
authored
Merge pull request #164 from Sinersis/fix-interceptors
Fix interceptors
2 parents 1575022 + 85122f4 commit ea05f36

File tree

6 files changed

+461
-15
lines changed

6 files changed

+461
-15
lines changed

README.md

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,154 @@ return [
217217
'grpc' => [
218218
'services' => [
219219
\App\GRPC\EchoServiceInterface::class => \App\GRPC\EchoService::class,
220+
221+
// Service with specific interceptors
222+
\App\GRPC\UserServiceInterface::class => [
223+
'service' => \App\GRPC\UserService::class,
224+
'interceptors' => [
225+
\App\GRPC\Interceptors\ValidationInterceptor::class,
226+
\App\GRPC\Interceptors\CacheInterceptor::class,
227+
],
228+
],
229+
]
230+
],
231+
];
232+
```
233+
234+
#### gRPC Server Interceptors
235+
236+
Create your interceptor by implementing `Spiral\Interceptors\InterceptorInterface`:
237+
238+
```php
239+
<?php
240+
241+
namespace App\GRPC\Interceptors;
242+
243+
use Spiral\Interceptors\Context\CallContextInterface;
244+
use Spiral\Interceptors\HandlerInterface;
245+
use Spiral\Interceptors\InterceptorInterface;
246+
247+
class LoggingInterceptor implements InterceptorInterface
248+
{
249+
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed
250+
{
251+
$method = $context->getTarget()->getPath();
252+
\Log::info("gRPC call: {$method}");
253+
254+
$response = $handler->handle($context);
255+
256+
\Log::info("gRPC response: {$method}");
257+
258+
return $response;
259+
}
260+
}
261+
```
262+
263+
##### Interceptors Configuration
264+
265+
Configure interceptors in `config/roadrunner.php`. You can use global interceptors that apply to all services, service-specific interceptors, or both:
266+
267+
```php
268+
return [
269+
// ... other configuration
270+
'grpc' => [
271+
'services' => [
272+
// Simple service configuration
273+
\App\GRPC\EchoServiceInterface::class => \App\GRPC\EchoService::class,
274+
275+
// Service with specific interceptors
276+
\App\GRPC\UserServiceInterface::class => [
277+
'service' => \App\GRPC\UserService::class,
278+
'interceptors' => [
279+
\App\GRPC\Interceptors\ValidationInterceptor::class,
280+
\App\GRPC\Interceptors\CacheInterceptor::class,
281+
],
282+
],
283+
],
284+
// Global interceptors - applied to all services
285+
'interceptors' => [
286+
\App\GRPC\Interceptors\LoggingInterceptor::class,
287+
\App\GRPC\Interceptors\AuthenticationInterceptor::class,
220288
],
221289
],
222290
];
223291
```
224292

293+
##### Using Attribute-Based Interceptors
294+
295+
For additional flexibility and convenience, you can use the `AttributesInterceptor` to apply interceptors via PHP attributes directly on your service classes and methods. This allows you to define which interceptors should be applied at a more granular level.
296+
297+
To enable attribute-based interceptors, add the `AttributesInterceptor` to your global interceptors list:
298+
299+
```php
300+
'interceptors' => [
301+
// ... other global interceptors before
302+
\Spiral\RoadRunnerLaravel\Common\Interceptor\AttributesInterceptor::class,
303+
// ... other global interceptors after
304+
],
305+
```
306+
307+
Then, create interceptors that can be used as attributes:
308+
309+
```php
310+
<?php
311+
312+
namespace App\GRPC\Interceptors;
313+
314+
use Spiral\Interceptors\Context\CallContextInterface;
315+
use Spiral\Interceptors\HandlerInterface;
316+
use Spiral\Interceptors\InterceptorInterface;
317+
use Attribute;
318+
319+
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
320+
class RoleInterceptor implements InterceptorInterface
321+
{
322+
public function __construct(
323+
private readonly string $role,
324+
) {}
325+
326+
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed
327+
{
328+
// Check user role
329+
if (!$this->checkRole($this->role)) {
330+
throw new \RuntimeException('Access denied');
331+
}
332+
333+
return $handler->handle($context);
334+
}
335+
}
336+
```
337+
338+
Apply interceptors to your gRPC service classes or methods:
339+
340+
```php
341+
<?php
342+
343+
namespace App\GRPC;
344+
345+
use App\GRPC\Interceptors\LoggingInterceptor;
346+
use App\GRPC\Interceptors\AuthInterceptor;
347+
use App\GRPC\Interceptors\RoleInterceptor;
348+
349+
#[LoggingInterceptor]
350+
#[AuthInterceptor]
351+
class UserService implements UserServiceInterface
352+
{
353+
#[RoleInterceptor('admin')]
354+
public function DeleteUser(GRPC\ContextInterface $ctx, DeleteUserRequest $in): DeleteUserResponse
355+
{
356+
// Implementation - will use class-level + method-level interceptors
357+
}
358+
359+
public function GetUser(GRPC\ContextInterface $ctx, GetUserRequest $in): GetUserResponse
360+
{
361+
// Implementation - will use only class-level interceptors
362+
}
363+
}
364+
```
365+
366+
Interceptors are applied in order: first class-level attributes, then method-level attributes.
367+
225368
#### gRPC Client Usage
226369

227370
The package also allows your Laravel application to act as a gRPC client, making requests to external gRPC services.
@@ -451,6 +594,75 @@ return [
451594
The key in the `workers` array should match the value of the `RR_MODE` environment variable
452595
set by the RoadRunner server for your plugin.
453596

597+
### Example: Centrifugo Worker
598+
599+
Here's an example of a custom worker for the [Centrifugo](https://docs.roadrunner.dev/docs/plugins/centrifuge) plugin:
600+
601+
```php
602+
namespace App\Workers;
603+
604+
use Spiral\RoadRunnerLaravel\WorkerInterface;
605+
use Spiral\RoadRunnerLaravel\WorkerOptionsInterface;
606+
use Spiral\RoadRunner\Centrifugo\CentrifugoWorker as RRCentrifugoWorker;
607+
use Spiral\RoadRunner\Centrifugo\CentrifugoWorkerInterface;
608+
609+
class CentrifugoWorker implements WorkerInterface
610+
{
611+
public function start(WorkerOptionsInterface $options): void
612+
{
613+
$worker = RRCentrifugoWorker::create();
614+
615+
$worker->onConnect(function (CentrifugoWorkerInterface $worker, string $client, array $request): array {
616+
// Handle client connection
617+
$app = $options->getAppContainer();
618+
619+
// Your connection handling logic
620+
621+
return ['status' => 200];
622+
});
623+
624+
$worker->onSubscribe(function (CentrifugoWorkerInterface $worker, string $client, array $request): array {
625+
// Handle client subscription
626+
$app = $options->getAppContainer();
627+
628+
// Your subscription handling logic
629+
630+
return ['status' => 200];
631+
});
632+
633+
$worker->onPublish(function (CentrifugoWorkerInterface $worker, string $client, array $request): array {
634+
// Handle client publish
635+
$app = $options->getAppContainer();
636+
637+
// Your publish handling logic
638+
639+
return ['status' => 200];
640+
});
641+
642+
$worker->start();
643+
}
644+
}
645+
```
646+
647+
Then register it in your configuration:
648+
649+
```php
650+
return [
651+
'workers' => [
652+
// ... other workers
653+
'centrifugo' => \App\Workers\CentrifugoWorker::class,
654+
],
655+
];
656+
```
657+
658+
And update your `.rr.yaml` with the Centrifugo plugin configuration:
659+
660+
```yaml
661+
centrifugo:
662+
address: "tcp://localhost:8000"
663+
api_key: "your-api-key"
664+
```
665+
454666
## Support
455667

456668
If you find this package helpful, please consider giving it a star on GitHub.

config/roadrunner.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,19 @@
1717
'grpc' => [
1818
'services' => [
1919
// GreeterInterface::class => new Greeter::class,
20+
21+
// Service with specific interceptors
22+
// AnotherGreeterInterface::class => [
23+
// 'service' => AnotherGreeterService::class,
24+
// 'interceptors' => [
25+
// AnotherGreeterServiceInterceptor::class,
26+
// ],
27+
// ],
28+
],
29+
// Global interceptors - applied to all services
30+
'interceptors' => [
31+
// \Spiral\RoadRunnerLaravel\Common\Interceptor\AttributesInterceptor::class,
32+
// AllServiceInterceptor::class,
2033
],
2134
'clients' => [
2235
'interceptors' => [
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Spiral\RoadRunnerLaravel\Common\Interceptor;
6+
7+
use Spiral\Interceptors\Context\CallContextInterface;
8+
use Spiral\Interceptors\Handler\InterceptorPipeline;
9+
use Spiral\Interceptors\HandlerInterface;
10+
use Spiral\Interceptors\InterceptorInterface;
11+
12+
/**
13+
* Interceptor for processing attributes that implement {@see InterceptorInterface}.
14+
*
15+
* It checks for the presence of attributes implementing {@see InterceptorInterface} at both class and method levels.
16+
* If such attributes are found, an interceptor pipeline is created that sequentially applies all found interceptors.
17+
*
18+
* Interceptors are applied in the order they are defined: first class attributes, then method attributes.
19+
*
20+
* Usage example:
21+
* ```
22+
* #[LoggerInterceptor]
23+
* #[AuthInterceptor]
24+
* class PingService implements PingServiceInterface
25+
* {
26+
* #[AuthRoleInterceptor(role: 'admin')]
27+
* public function Ping(
28+
* GRPC\ContextInterface $ctx,
29+
* PingRequest $in,
30+
* ): PingResponse {
31+
* return new PingResponse();
32+
* }
33+
* }
34+
* ```
35+
*
36+
* In this example, LoggerInterceptor and AuthInterceptor will be applied to all methods of the PingService class.
37+
* For the Ping method, AuthRoleInterceptor will additionally be applied.
38+
*
39+
* @author Aleksei Gagarin (roxblnfk)
40+
*/
41+
class AttributesInterceptor implements InterceptorInterface
42+
{
43+
/**
44+
* @throws \Throwable
45+
*/
46+
public function intercept(CallContextInterface $context, HandlerInterface $handler): mixed
47+
{
48+
$reflection = $context->getTarget()->getReflection();
49+
if ($reflection === null) {
50+
return $handler->handle($context);
51+
}
52+
53+
$methodAttrs = $reflection->getAttributes(InterceptorInterface::class, \ReflectionAttribute::IS_INSTANCEOF);
54+
55+
$reflection instanceof \ReflectionMethod and $classAttrs = $reflection
56+
->getDeclaringClass()
57+
?->getAttributes(InterceptorInterface::class, \ReflectionAttribute::IS_INSTANCEOF);
58+
$classAttrs ??= [];
59+
60+
if ($methodAttrs === [] && $classAttrs === []) {
61+
return $handler->handle($context);
62+
}
63+
64+
return (new InterceptorPipeline())
65+
->withInterceptors(
66+
...$this->resolveAttributes(...$classAttrs),
67+
...$this->resolveAttributes(...$methodAttrs),
68+
)
69+
->withHandler($handler)
70+
->handle($context);
71+
}
72+
73+
/**
74+
* @param \ReflectionAttribute<InterceptorInterface> ...$attributes
75+
* @return InterceptorInterface[]
76+
*/
77+
private function resolveAttributes(\ReflectionAttribute ...$attributes): array
78+
{
79+
$result = [];
80+
foreach ($attributes as $attribute) {
81+
$result[] = $attribute->newInstance();
82+
}
83+
84+
return $result;
85+
}
86+
}

src/Grpc/GrpcWorker.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
namespace Spiral\RoadRunnerLaravel\Grpc;
66

77
use Laravel\Octane\ApplicationFactory;
8+
use Spiral\Interceptors\InterceptorInterface;
89
use Spiral\RoadRunnerLaravel\OctaneWorker;
910
use Spiral\RoadRunnerLaravel\WorkerInterface;
1011
use Spiral\RoadRunnerLaravel\WorkerOptionsInterface;
11-
use Spiral\RoadRunner\GRPC\Invoker;
1212
use Spiral\RoadRunner\Worker;
1313

1414
final class GrpcWorker implements WorkerInterface
@@ -24,17 +24,31 @@ public function start(WorkerOptionsInterface $options): void
2424

2525
$server = new Server(
2626
worker: $worker,
27-
invoker: new Invoker(),
2827
options: [
2928
'debug' => $app->hasDebugModeEnabled(),
3029
],
30+
container: $app,
3131
);
3232

3333
/** @var array<class-string, class-string> $services */
3434
$services = $app->get('config')->get('roadrunner.grpc.services', []);
3535

36+
/** @var array<class-string<InterceptorInterface>> $interceptors */
37+
$interceptors = $app->get('config')->get('roadrunner.grpc.interceptors', []);
38+
3639
foreach ($services as $interface => $service) {
37-
$server->registerService($interface, $app->make($service));
40+
if (is_array($service)) {
41+
if (!isset($service['service']) || !is_string($service['service'])) {
42+
throw new \InvalidArgumentException("Service array must have a class name at index 'service' for interface: {$interface}");
43+
}
44+
45+
$serviceInterceptors = array_merge($interceptors, $service['interceptors'] ?? []);
46+
$service = $service['service'];
47+
} else {
48+
$serviceInterceptors = $interceptors;
49+
}
50+
51+
$server->registerService($interface, $app->make($service), $serviceInterceptors);
3852
}
3953

4054
$server->serve(Worker::create());

0 commit comments

Comments
 (0)